@verbeth/sdk 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -168
- package/dist/esm/src/addresses.d.ts +20 -0
- package/dist/esm/src/addresses.d.ts.map +1 -0
- package/dist/esm/src/addresses.js +33 -0
- package/dist/esm/src/client/HsrTagIndex.d.ts +77 -0
- package/dist/esm/src/client/HsrTagIndex.d.ts.map +1 -0
- package/dist/esm/src/client/HsrTagIndex.js +157 -0
- package/dist/esm/src/client/PendingManager.d.ts +65 -0
- package/dist/esm/src/client/PendingManager.d.ts.map +1 -0
- package/dist/esm/src/client/PendingManager.js +84 -0
- package/dist/esm/src/client/SessionManager.d.ts +65 -0
- package/dist/esm/src/client/SessionManager.d.ts.map +1 -0
- package/dist/esm/src/client/SessionManager.js +146 -0
- package/dist/esm/src/client/VerbethClient.d.ts +153 -99
- package/dist/esm/src/client/VerbethClient.d.ts.map +1 -1
- package/dist/esm/src/client/VerbethClient.js +429 -123
- package/dist/esm/src/client/VerbethClientBuilder.d.ts +105 -0
- package/dist/esm/src/client/VerbethClientBuilder.d.ts.map +1 -0
- package/dist/esm/src/client/VerbethClientBuilder.js +146 -0
- package/dist/esm/src/client/hsrMatcher.d.ts +22 -0
- package/dist/esm/src/client/hsrMatcher.d.ts.map +1 -0
- package/dist/esm/src/client/hsrMatcher.js +31 -0
- package/dist/esm/src/client/index.d.ts +6 -1
- package/dist/esm/src/client/index.d.ts.map +1 -1
- package/dist/esm/src/client/index.js +2 -0
- package/dist/esm/src/client/types.d.ts +151 -10
- package/dist/esm/src/client/types.d.ts.map +1 -1
- package/dist/esm/src/crypto(old).d.ts +46 -0
- package/dist/esm/src/crypto(old).d.ts.map +1 -0
- package/dist/esm/src/crypto(old).js +137 -0
- package/dist/esm/src/crypto.d.ts +7 -29
- package/dist/esm/src/crypto.d.ts.map +1 -1
- package/dist/esm/src/crypto.js +36 -72
- package/dist/esm/src/executor.d.ts +17 -18
- package/dist/esm/src/executor.d.ts.map +1 -1
- package/dist/esm/src/executor.js +54 -70
- package/dist/esm/src/handshake.d.ts +51 -0
- package/dist/esm/src/handshake.d.ts.map +1 -0
- package/dist/esm/src/handshake.js +105 -0
- package/dist/esm/src/identity.d.ts +24 -18
- package/dist/esm/src/identity.d.ts.map +1 -1
- package/dist/esm/src/identity.js +126 -31
- package/dist/esm/src/index.d.ts +11 -7
- package/dist/esm/src/index.d.ts.map +1 -1
- package/dist/esm/src/index.js +10 -7
- package/dist/esm/src/payload.d.ts +3 -30
- package/dist/esm/src/payload.d.ts.map +1 -1
- package/dist/esm/src/payload.js +3 -77
- package/dist/esm/src/pq/kem.d.ts +33 -0
- package/dist/esm/src/pq/kem.d.ts.map +1 -0
- package/dist/esm/src/pq/kem.js +40 -0
- package/dist/esm/src/ratchet/auth.d.ts +34 -0
- package/dist/esm/src/ratchet/auth.d.ts.map +1 -0
- package/dist/esm/src/ratchet/auth.js +88 -0
- package/dist/esm/src/ratchet/codec.d.ts +52 -0
- package/dist/esm/src/ratchet/codec.d.ts.map +1 -0
- package/dist/esm/src/ratchet/codec.js +127 -0
- package/dist/esm/src/ratchet/decrypt.d.ts +28 -0
- package/dist/esm/src/ratchet/decrypt.d.ts.map +1 -0
- package/dist/esm/src/ratchet/decrypt.js +255 -0
- package/dist/esm/src/ratchet/encrypt.d.ts +17 -0
- package/dist/esm/src/ratchet/encrypt.d.ts.map +1 -0
- package/dist/esm/src/ratchet/encrypt.js +78 -0
- package/dist/esm/src/ratchet/index.d.ts +8 -0
- package/dist/esm/src/ratchet/index.d.ts.map +1 -0
- package/dist/esm/src/ratchet/index.js +8 -0
- package/dist/esm/src/ratchet/kdf.d.ts +60 -0
- package/dist/esm/src/ratchet/kdf.d.ts.map +1 -0
- package/dist/esm/src/ratchet/kdf.js +91 -0
- package/dist/esm/src/ratchet/session.d.ts +43 -0
- package/dist/esm/src/ratchet/session.d.ts.map +1 -0
- package/dist/esm/src/ratchet/session.js +139 -0
- package/dist/esm/src/ratchet/types.d.ts +168 -0
- package/dist/esm/src/ratchet/types.d.ts.map +1 -0
- package/dist/esm/src/ratchet/types.js +27 -0
- package/dist/esm/src/safeSessionSigner.d.ts +35 -0
- package/dist/esm/src/safeSessionSigner.d.ts.map +1 -0
- package/dist/esm/src/safeSessionSigner.js +59 -0
- package/dist/esm/src/send.d.ts +32 -24
- package/dist/esm/src/send.d.ts.map +1 -1
- package/dist/esm/src/send.js +84 -39
- package/dist/esm/src/types.d.ts +8 -13
- package/dist/esm/src/types.d.ts.map +1 -1
- package/dist/esm/src/utils/safeSessionSigner.d.ts +23 -0
- package/dist/esm/src/utils/safeSessionSigner.d.ts.map +1 -0
- package/dist/esm/src/utils/safeSessionSigner.js +59 -0
- package/dist/esm/src/utils/txQueue.d.ts +12 -0
- package/dist/esm/src/utils/txQueue.d.ts.map +1 -0
- package/dist/esm/src/utils/txQueue.js +25 -0
- package/dist/esm/src/utils.d.ts +2 -3
- package/dist/esm/src/utils.d.ts.map +1 -1
- package/dist/esm/src/utils.js +5 -5
- package/dist/esm/src/verify.d.ts +9 -25
- package/dist/esm/src/verify.d.ts.map +1 -1
- package/dist/esm/src/verify.js +49 -50
- package/dist/src/addresses.d.ts +20 -0
- package/dist/src/addresses.d.ts.map +1 -0
- package/dist/src/addresses.js +33 -0
- package/dist/src/client/HsrTagIndex.d.ts +77 -0
- package/dist/src/client/HsrTagIndex.d.ts.map +1 -0
- package/dist/src/client/HsrTagIndex.js +157 -0
- package/dist/src/client/PendingManager.d.ts +65 -0
- package/dist/src/client/PendingManager.d.ts.map +1 -0
- package/dist/src/client/PendingManager.js +84 -0
- package/dist/src/client/SessionManager.d.ts +65 -0
- package/dist/src/client/SessionManager.d.ts.map +1 -0
- package/dist/src/client/SessionManager.js +146 -0
- package/dist/src/client/VerbethClient.d.ts +153 -99
- package/dist/src/client/VerbethClient.d.ts.map +1 -1
- package/dist/src/client/VerbethClient.js +429 -123
- package/dist/src/client/VerbethClientBuilder.d.ts +105 -0
- package/dist/src/client/VerbethClientBuilder.d.ts.map +1 -0
- package/dist/src/client/VerbethClientBuilder.js +146 -0
- package/dist/src/client/hsrMatcher.d.ts +22 -0
- package/dist/src/client/hsrMatcher.d.ts.map +1 -0
- package/dist/src/client/hsrMatcher.js +31 -0
- package/dist/src/client/index.d.ts +6 -1
- package/dist/src/client/index.d.ts.map +1 -1
- package/dist/src/client/index.js +2 -0
- package/dist/src/client/types.d.ts +151 -10
- package/dist/src/client/types.d.ts.map +1 -1
- package/dist/src/crypto(old).d.ts +46 -0
- package/dist/src/crypto(old).d.ts.map +1 -0
- package/dist/src/crypto(old).js +137 -0
- package/dist/src/crypto.d.ts +7 -29
- package/dist/src/crypto.d.ts.map +1 -1
- package/dist/src/crypto.js +36 -72
- package/dist/src/executor.d.ts +17 -18
- package/dist/src/executor.d.ts.map +1 -1
- package/dist/src/executor.js +54 -70
- package/dist/src/handshake.d.ts +51 -0
- package/dist/src/handshake.d.ts.map +1 -0
- package/dist/src/handshake.js +105 -0
- package/dist/src/identity.d.ts +24 -18
- package/dist/src/identity.d.ts.map +1 -1
- package/dist/src/identity.js +126 -31
- package/dist/src/index.d.ts +11 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +10 -7
- package/dist/src/payload.d.ts +3 -30
- package/dist/src/payload.d.ts.map +1 -1
- package/dist/src/payload.js +3 -77
- package/dist/src/pq/kem.d.ts +33 -0
- package/dist/src/pq/kem.d.ts.map +1 -0
- package/dist/src/pq/kem.js +40 -0
- package/dist/src/ratchet/auth.d.ts +34 -0
- package/dist/src/ratchet/auth.d.ts.map +1 -0
- package/dist/src/ratchet/auth.js +88 -0
- package/dist/src/ratchet/codec.d.ts +52 -0
- package/dist/src/ratchet/codec.d.ts.map +1 -0
- package/dist/src/ratchet/codec.js +127 -0
- package/dist/src/ratchet/decrypt.d.ts +28 -0
- package/dist/src/ratchet/decrypt.d.ts.map +1 -0
- package/dist/src/ratchet/decrypt.js +255 -0
- package/dist/src/ratchet/encrypt.d.ts +17 -0
- package/dist/src/ratchet/encrypt.d.ts.map +1 -0
- package/dist/src/ratchet/encrypt.js +78 -0
- package/dist/src/ratchet/index.d.ts +8 -0
- package/dist/src/ratchet/index.d.ts.map +1 -0
- package/dist/src/ratchet/index.js +8 -0
- package/dist/src/ratchet/kdf.d.ts +60 -0
- package/dist/src/ratchet/kdf.d.ts.map +1 -0
- package/dist/src/ratchet/kdf.js +91 -0
- package/dist/src/ratchet/session.d.ts +43 -0
- package/dist/src/ratchet/session.d.ts.map +1 -0
- package/dist/src/ratchet/session.js +139 -0
- package/dist/src/ratchet/types.d.ts +168 -0
- package/dist/src/ratchet/types.d.ts.map +1 -0
- package/dist/src/ratchet/types.js +27 -0
- package/dist/src/safeSessionSigner.d.ts +35 -0
- package/dist/src/safeSessionSigner.d.ts.map +1 -0
- package/dist/src/safeSessionSigner.js +59 -0
- package/dist/src/send.d.ts +32 -24
- package/dist/src/send.d.ts.map +1 -1
- package/dist/src/send.js +84 -39
- package/dist/src/types.d.ts +8 -13
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/safeSessionSigner.d.ts +23 -0
- package/dist/src/utils/safeSessionSigner.d.ts.map +1 -0
- package/dist/src/utils/safeSessionSigner.js +59 -0
- package/dist/src/utils/txQueue.d.ts +12 -0
- package/dist/src/utils/txQueue.d.ts.map +1 -0
- package/dist/src/utils/txQueue.js +25 -0
- package/dist/src/utils.d.ts +2 -3
- package/dist/src/utils.d.ts.map +1 -1
- package/dist/src/utils.js +5 -5
- package/dist/src/verify.d.ts +9 -25
- package/dist/src/verify.d.ts.map +1 -1
- package/dist/src/verify.js +49 -50
- package/package.json +2 -1
|
@@ -1,169 +1,456 @@
|
|
|
1
1
|
// packages/sdk/src/client/VerbethClient.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
/**
|
|
3
|
+
* High-level client for Verbeth E2EE messaging.
|
|
4
|
+
*
|
|
5
|
+
* Provides a unified API for:
|
|
6
|
+
* - Handshake operations (sendHandshake, acceptHandshake)
|
|
7
|
+
* - Session creation for both initiator and responder
|
|
8
|
+
* - Message encryption/decryption with session management
|
|
9
|
+
* - Two-phase commit for message sending
|
|
10
|
+
* - Transaction confirmation handling
|
|
11
|
+
*/
|
|
12
|
+
import { hexlify, getBytes, keccak256 } from 'ethers';
|
|
13
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
14
|
+
import { sha256 } from '@noble/hashes/sha2';
|
|
15
|
+
import { initiateHandshake, respondToHandshake } from '../handshake.js';
|
|
16
|
+
import { kem } from '../pq/kem.js';
|
|
5
17
|
import * as crypto from '../crypto.js';
|
|
6
18
|
import * as payload from '../payload.js';
|
|
7
19
|
import * as verify from '../verify.js';
|
|
8
20
|
import * as utils from '../utils.js';
|
|
9
21
|
import * as identity from '../identity.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
* executor,
|
|
20
|
-
* identityKeyPair,
|
|
21
|
-
* identityProof,
|
|
22
|
-
* signer,
|
|
23
|
-
* address: '0x...'
|
|
24
|
-
* });
|
|
25
|
-
*
|
|
26
|
-
* // Send a handshake
|
|
27
|
-
* const { tx, ephemeralKeyPair } = await client.sendHandshake(
|
|
28
|
-
* '0xBob...',
|
|
29
|
-
* 'Hello Bob!'
|
|
30
|
-
* );
|
|
31
|
-
*
|
|
32
|
-
* // Send a message
|
|
33
|
-
* await client.sendMessage(
|
|
34
|
-
* contact.topicOutbound,
|
|
35
|
-
* contact.identityPubKey,
|
|
36
|
-
* 'Hello again!'
|
|
37
|
-
* );
|
|
38
|
-
* ```
|
|
39
|
-
*/
|
|
22
|
+
import * as ratchet from '../ratchet/index.js';
|
|
23
|
+
import { ratchetEncrypt } from '../ratchet/encrypt.js';
|
|
24
|
+
import { ratchetDecrypt } from '../ratchet/decrypt.js';
|
|
25
|
+
import { packageRatchetPayload, parseRatchetPayload, isRatchetPayload } from '../ratchet/codec.js';
|
|
26
|
+
import { verifyMessageSignature } from '../ratchet/auth.js';
|
|
27
|
+
import { dh, hybridInitialSecret } from '../ratchet/kdf.js';
|
|
28
|
+
import { initSessionAsInitiator, initSessionAsResponder } from '../ratchet/session.js';
|
|
29
|
+
import { SessionManager } from './SessionManager.js';
|
|
30
|
+
import { PendingManager } from './PendingManager.js';
|
|
40
31
|
export class VerbethClient {
|
|
41
|
-
/**
|
|
42
|
-
* creates a new VerbethClient instance
|
|
43
|
-
*
|
|
44
|
-
* @param config - Client configuration with session-level parameters
|
|
45
|
-
*/
|
|
46
32
|
constructor(config) {
|
|
47
33
|
this.executor = config.executor;
|
|
48
34
|
this.identityKeyPair = config.identityKeyPair;
|
|
49
35
|
this.identityProof = config.identityProof;
|
|
50
36
|
this.signer = config.signer;
|
|
51
37
|
this.address = config.address;
|
|
38
|
+
this.callbacks = config.callbacks;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* to be called before using prepareMessage/decryptMessage/sendMessage.
|
|
42
|
+
*/
|
|
43
|
+
setSessionStore(store) {
|
|
44
|
+
this.sessionManager = new SessionManager(store);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* to be called before using sendMessage/confirmTx/revertTx.
|
|
48
|
+
*/
|
|
49
|
+
setPendingStore(store) {
|
|
50
|
+
this.pendingManager = new PendingManager(store);
|
|
51
|
+
}
|
|
52
|
+
hasSessionStore() {
|
|
53
|
+
return !!this.sessionManager;
|
|
54
|
+
}
|
|
55
|
+
hasPendingStore() {
|
|
56
|
+
return !!this.pendingManager;
|
|
52
57
|
}
|
|
53
58
|
/**
|
|
54
|
-
* Initiates a handshake with a recipient
|
|
59
|
+
* Initiates a handshake with a recipient.
|
|
55
60
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
61
|
+
* Generates an ephemeral keypair and ML-KEM keypair for this handshake.
|
|
62
|
+
* Both secretKeys must be stored for ratchet session initialization
|
|
63
|
+
* when the response arrives.
|
|
58
64
|
*
|
|
59
65
|
* @param recipientAddress - Blockchain address of the recipient
|
|
60
66
|
* @param message - Plaintext message to include in the handshake
|
|
61
|
-
* @returns Transaction response
|
|
62
|
-
*
|
|
63
|
-
* @example
|
|
64
|
-
* ```typescript
|
|
65
|
-
* const { tx, ephemeralKeyPair } = await client.sendHandshake(
|
|
66
|
-
* '0xBob...',
|
|
67
|
-
* 'Hi Bob!'
|
|
68
|
-
* );
|
|
69
|
-
*
|
|
70
|
-
* // Store ephemeralKeyPair.secretKey to decrypt Bob's response
|
|
71
|
-
* await storage.saveContact({
|
|
72
|
-
* address: '0xBob...',
|
|
73
|
-
* ephemeralKey: ephemeralKeyPair.secretKey,
|
|
74
|
-
* // ...
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
67
|
+
* @returns Transaction response, ephemeral keypair, and KEM keypair
|
|
77
68
|
*/
|
|
78
69
|
async sendHandshake(recipientAddress, message) {
|
|
79
|
-
const ephemeralKeyPair =
|
|
80
|
-
const tx = await initiateHandshake({
|
|
70
|
+
const { tx, ephemeralKeyPair, kemKeyPair } = await initiateHandshake({
|
|
81
71
|
executor: this.executor,
|
|
82
72
|
recipientAddress,
|
|
83
73
|
identityKeyPair: this.identityKeyPair,
|
|
84
|
-
ephemeralPubKey: ephemeralKeyPair.publicKey,
|
|
85
74
|
plaintextPayload: message,
|
|
86
75
|
identityProof: this.identityProof,
|
|
87
76
|
signer: this.signer,
|
|
88
77
|
});
|
|
89
|
-
return { tx, ephemeralKeyPair };
|
|
78
|
+
return { tx, ephemeralKeyPair, kemKeyPair };
|
|
90
79
|
}
|
|
91
80
|
/**
|
|
92
|
-
* Accepts a handshake from an initiator
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* @
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* handshake.ephemeralPubKey,
|
|
105
|
-
* handshake.identityPubKey,
|
|
106
|
-
* 'Hello Alice!'
|
|
107
|
-
* );
|
|
108
|
-
*
|
|
109
|
-
* // Store the topics for future messaging
|
|
110
|
-
* await storage.saveContact({
|
|
111
|
-
* address: handshake.sender,
|
|
112
|
-
* topicOutbound: duplexTopics.topicIn, // Responder writes to topicIn
|
|
113
|
-
* topicInbound: duplexTopics.topicOut, // Responder reads from topicOut
|
|
114
|
-
* // ...
|
|
115
|
-
* });
|
|
116
|
-
* ```
|
|
81
|
+
* Accepts a handshake from an initiator.
|
|
82
|
+
*
|
|
83
|
+
* Derives topics from ephemeral DH shared secret (same approach
|
|
84
|
+
* as post-handshake topic ratcheting). Returns topicOutbound/topicInbound
|
|
85
|
+
* directly instead of duplexTopics structure.
|
|
86
|
+
*
|
|
87
|
+
* Supports PQ-hybrid: if initiator includes ML-KEM public key (1216 bytes),
|
|
88
|
+
* performs KEM encapsulation and returns kemSharedSecret.
|
|
89
|
+
*
|
|
90
|
+
* @param initiatorEphemeralPubKey - Initiator's ephemeral key (32 bytes X25519 or 1216 bytes with KEM)
|
|
91
|
+
* @param note - Response message to send back
|
|
92
|
+
* @returns Transaction, derived topics, ephemeral keys for ratchet, and KEM shared secret
|
|
117
93
|
*/
|
|
118
|
-
async acceptHandshake(initiatorEphemeralPubKey,
|
|
119
|
-
const { tx, salt, tag } = await respondToHandshake({
|
|
94
|
+
async acceptHandshake(initiatorEphemeralPubKey, note) {
|
|
95
|
+
const { tx, salt, tag, responderEphemeralSecret, responderEphemeralPublic, kemSharedSecret, } = await respondToHandshake({
|
|
120
96
|
executor: this.executor,
|
|
121
|
-
|
|
97
|
+
initiatorEphemeralPubKey,
|
|
122
98
|
responderIdentityKeyPair: this.identityKeyPair,
|
|
123
99
|
note,
|
|
124
100
|
identityProof: this.identityProof,
|
|
125
101
|
signer: this.signer,
|
|
126
|
-
initiatorIdentityPubKey,
|
|
127
102
|
});
|
|
128
|
-
|
|
129
|
-
|
|
103
|
+
// Extract X25519 part for topic derivation (first 32 bytes if extended)
|
|
104
|
+
const x25519Pub = initiatorEphemeralPubKey.length > 32
|
|
105
|
+
? initiatorEphemeralPubKey.slice(0, 32)
|
|
106
|
+
: initiatorEphemeralPubKey;
|
|
107
|
+
if (!kemSharedSecret) {
|
|
108
|
+
throw new Error("KEM is required for PQ-secure handshake");
|
|
109
|
+
}
|
|
110
|
+
const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(responderEphemeralSecret, x25519Pub, salt, false, kemSharedSecret);
|
|
111
|
+
return {
|
|
112
|
+
tx,
|
|
113
|
+
topicOutbound,
|
|
114
|
+
topicInbound,
|
|
115
|
+
tag,
|
|
116
|
+
salt,
|
|
117
|
+
responderEphemeralSecret,
|
|
118
|
+
responderEphemeralPublic,
|
|
119
|
+
kemSharedSecret,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
// ===========================================================================
|
|
123
|
+
// Session Creation - Encapsulates DH and topic derivation
|
|
124
|
+
// ===========================================================================
|
|
125
|
+
/**
|
|
126
|
+
* Create a ratchet session as the handshake initiator.
|
|
127
|
+
*
|
|
128
|
+
* Call this after receiving and validating a handshake response.
|
|
129
|
+
* Handles topic derivation from ephemeral DH internally.
|
|
130
|
+
*
|
|
131
|
+
* If KEM ciphertext and secret are provided (PQ-hybrid), decapsulates
|
|
132
|
+
* to derive hybrid shared secret for post-quantum security.
|
|
133
|
+
*
|
|
134
|
+
* @param params - Session creation parameters
|
|
135
|
+
* @returns Ready-to-save RatchetSession
|
|
136
|
+
*/
|
|
137
|
+
createInitiatorSession(params) {
|
|
138
|
+
const { contactAddress, initiatorEphemeralSecret, responderEphemeralPubKey, inResponseToTag, kemCiphertext, initiatorKemSecret, } = params;
|
|
139
|
+
if (!kemCiphertext || !initiatorKemSecret) {
|
|
140
|
+
throw new Error("KEM is required for PQ-secure handshake");
|
|
141
|
+
}
|
|
142
|
+
const kemSecret = kem.decapsulate(kemCiphertext, initiatorKemSecret);
|
|
143
|
+
const salt = getBytes(inResponseToTag);
|
|
144
|
+
const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(initiatorEphemeralSecret, responderEphemeralPubKey, salt, true, kemSecret);
|
|
145
|
+
return initSessionAsInitiator({
|
|
146
|
+
myAddress: this.address,
|
|
147
|
+
contactAddress,
|
|
148
|
+
myHandshakeEphemeralSecret: initiatorEphemeralSecret,
|
|
149
|
+
theirResponderEphemeralPubKey: responderEphemeralPubKey,
|
|
150
|
+
topicOutbound,
|
|
151
|
+
topicInbound,
|
|
152
|
+
kemSecret,
|
|
153
|
+
});
|
|
130
154
|
}
|
|
131
155
|
/**
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
* Create a ratchet session as the handshake responder.
|
|
157
|
+
*
|
|
158
|
+
* Call this after sending a handshake response.
|
|
159
|
+
* Handles topic derivation from ephemeral DH internally.
|
|
160
|
+
*
|
|
161
|
+
* If kemSharedSecret is provided (PQ-hybrid), uses hybrid KDF
|
|
162
|
+
* for post-quantum security.
|
|
163
|
+
*
|
|
164
|
+
* @param params - Session creation parameters
|
|
165
|
+
* @returns Ready-to-save RatchetSession
|
|
166
|
+
*/
|
|
167
|
+
createResponderSession(params) {
|
|
168
|
+
const { contactAddress, responderEphemeralSecret, responderEphemeralPublic, initiatorEphemeralPubKey, salt, kemSharedSecret, } = params;
|
|
169
|
+
if (!kemSharedSecret) {
|
|
170
|
+
throw new Error("KEM is required for PQ-secure handshake");
|
|
171
|
+
}
|
|
172
|
+
// Extract X25519 part for topic derivation (first 32 bytes if extended)
|
|
173
|
+
const x25519Pub = initiatorEphemeralPubKey.length > 32
|
|
174
|
+
? initiatorEphemeralPubKey.slice(0, 32)
|
|
175
|
+
: initiatorEphemeralPubKey;
|
|
176
|
+
const { topicOutbound, topicInbound } = this.deriveTopicsFromDH(responderEphemeralSecret, x25519Pub, salt, false, kemSharedSecret);
|
|
177
|
+
return initSessionAsResponder({
|
|
178
|
+
myAddress: this.address,
|
|
179
|
+
contactAddress,
|
|
180
|
+
myResponderEphemeralSecret: responderEphemeralSecret,
|
|
181
|
+
myResponderEphemeralPublic: responderEphemeralPublic,
|
|
182
|
+
theirHandshakeEphemeralPubKey: x25519Pub,
|
|
183
|
+
topicOutbound,
|
|
184
|
+
topicInbound,
|
|
185
|
+
kemSecret: kemSharedSecret,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Accepting a structured HSR event object instead of individual parameters scattered across variables.
|
|
149
190
|
*/
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
191
|
+
createInitiatorSessionFromHsr(params) {
|
|
192
|
+
return this.createInitiatorSession({
|
|
193
|
+
contactAddress: params.contactAddress,
|
|
194
|
+
initiatorEphemeralSecret: params.myEphemeralSecret,
|
|
195
|
+
responderEphemeralPubKey: params.hsrEvent.responderEphemeralPubKey,
|
|
196
|
+
inResponseToTag: params.hsrEvent.inResponseToTag,
|
|
197
|
+
kemCiphertext: params.hsrEvent.kemCiphertext,
|
|
198
|
+
initiatorKemSecret: params.myKemSecret,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
deriveTopicsFromDH(mySecret, theirPublic, salt, isInitiator, kemSecret) {
|
|
202
|
+
const ephemeralShared = dh(mySecret, theirPublic);
|
|
203
|
+
const hybridSecret = hybridInitialSecret(ephemeralShared, kemSecret);
|
|
204
|
+
const deriveEpoch0Topic = (direction) => {
|
|
205
|
+
const info = `verbeth:topic-${direction}:v2`;
|
|
206
|
+
const okm = hkdf(sha256, hybridSecret, salt, info, 32);
|
|
207
|
+
return keccak256(okm);
|
|
154
208
|
};
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
209
|
+
if (isInitiator) {
|
|
210
|
+
return {
|
|
211
|
+
topicOutbound: deriveEpoch0Topic('outbound'),
|
|
212
|
+
topicInbound: deriveEpoch0Topic('inbound'),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
return {
|
|
217
|
+
topicOutbound: deriveEpoch0Topic('inbound'),
|
|
218
|
+
topicInbound: deriveEpoch0Topic('outbound'),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ===========================================================================
|
|
223
|
+
// Message Operations
|
|
224
|
+
// ===========================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Prepare a message for sending (encrypt without submitting).
|
|
227
|
+
*
|
|
228
|
+
* Two-phase commit pattern:
|
|
229
|
+
* 1. prepareMessage() - encrypts and persists session state immediately
|
|
230
|
+
* 2. Submit transaction using prepared.payload and prepared.topic
|
|
231
|
+
* 3. On confirmation, call confirmTx() to clean up pending record
|
|
232
|
+
*
|
|
233
|
+
* Session state is committed immediately for forward secrecy.
|
|
234
|
+
* If tx fails, the ratchet slot is "burned" (receiver handles via skip keys).
|
|
235
|
+
*
|
|
236
|
+
* @param conversationId - The conversation to send in
|
|
237
|
+
* @param plaintext - Message text to encrypt
|
|
238
|
+
* @returns PreparedMessage with payload ready for on-chain submission
|
|
239
|
+
*/
|
|
240
|
+
async prepareMessage(conversationId, plaintext) {
|
|
241
|
+
if (!this.sessionManager) {
|
|
242
|
+
throw new Error('SessionStore not configured. Call setSessionStore() first.');
|
|
243
|
+
}
|
|
244
|
+
const session = await this.sessionManager.getByConversationId(conversationId);
|
|
245
|
+
if (!session) {
|
|
246
|
+
throw new Error(`No session found for conversation: ${conversationId}`);
|
|
247
|
+
}
|
|
248
|
+
const plaintextBytes = new TextEncoder().encode(plaintext);
|
|
249
|
+
const encryptResult = ratchetEncrypt(session, plaintextBytes, this.identityKeyPair.signingSecretKey);
|
|
250
|
+
const packedPayload = packageRatchetPayload(encryptResult.signature, encryptResult.header, encryptResult.ciphertext);
|
|
251
|
+
await this.sessionManager.save(encryptResult.session);
|
|
252
|
+
const prepared = {
|
|
253
|
+
id: this.generatePreparedId(),
|
|
254
|
+
conversationId,
|
|
255
|
+
topic: encryptResult.topic,
|
|
256
|
+
payload: packedPayload,
|
|
257
|
+
plaintext,
|
|
258
|
+
sessionBefore: session,
|
|
259
|
+
sessionAfter: encryptResult.session,
|
|
260
|
+
messageNumber: session.sendingMsgNumber,
|
|
261
|
+
createdAt: Date.now(),
|
|
262
|
+
};
|
|
263
|
+
return prepared;
|
|
264
|
+
}
|
|
265
|
+
// Session already saved in prepareMessage for forward secrecy.
|
|
266
|
+
// So this method can be used for additional bookkeeping if needed.
|
|
267
|
+
async commitMessage(_prepared) {
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Decrypt an incoming message.
|
|
271
|
+
*
|
|
272
|
+
* Handles:
|
|
273
|
+
* - Topic routing (current, next, previous)
|
|
274
|
+
* - Signature verification (DoS protection)
|
|
275
|
+
* - Ratchet decryption
|
|
276
|
+
* - Session state updates
|
|
277
|
+
* - Automatic topic promotion
|
|
278
|
+
*
|
|
279
|
+
* @param topic - The topic the message arrived on
|
|
280
|
+
* @param payload - Raw message payload (Uint8Array)
|
|
281
|
+
* @param senderSigningKey - Sender's Ed25519 signing public key
|
|
282
|
+
* @param isOwnMessage - Whether this is our own outbound message (echo)
|
|
283
|
+
* @returns DecryptedMessage or null if decryption fails
|
|
284
|
+
*/
|
|
285
|
+
async decryptMessage(topic, payload, senderSigningKey, isOwnMessage = false) {
|
|
286
|
+
if (!this.sessionManager) {
|
|
287
|
+
throw new Error('SessionStore not configured. Call setSessionStore() first.');
|
|
288
|
+
}
|
|
289
|
+
if (isOwnMessage) {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
const result = await this.sessionManager.getByInboundTopic(topic);
|
|
293
|
+
if (!result) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const { session, topicMatch } = result;
|
|
297
|
+
if (!isRatchetPayload(payload)) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
const parsed = parseRatchetPayload(payload);
|
|
301
|
+
if (!parsed) {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
const sigValid = verifyMessageSignature(parsed.signature, parsed.header, parsed.ciphertext, senderSigningKey);
|
|
305
|
+
if (!sigValid) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
const decryptResult = ratchetDecrypt(session, parsed.header, parsed.ciphertext);
|
|
309
|
+
if (!decryptResult) {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
// Check for topic ratchet before saving
|
|
313
|
+
const topicRatcheted = decryptResult.session.topicEpoch > session.topicEpoch;
|
|
314
|
+
const previousTopicInbound = topicRatcheted ? session.currentTopicInbound : null;
|
|
315
|
+
await this.sessionManager.save(decryptResult.session);
|
|
316
|
+
// Invoke callbacks if configured
|
|
317
|
+
if (this.callbacks) {
|
|
318
|
+
if (topicRatcheted && this.callbacks.onTopicRatchet) {
|
|
319
|
+
this.callbacks.onTopicRatchet({
|
|
320
|
+
conversationId: session.conversationId,
|
|
321
|
+
previousTopicInbound,
|
|
322
|
+
currentTopicInbound: decryptResult.session.currentTopicInbound,
|
|
323
|
+
topicEpoch: decryptResult.session.topicEpoch,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
if (this.callbacks.onMessageDecrypted) {
|
|
327
|
+
this.callbacks.onMessageDecrypted({
|
|
328
|
+
conversationId: session.conversationId,
|
|
329
|
+
topicMatch,
|
|
330
|
+
topicEpoch: decryptResult.session.topicEpoch,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const plaintextStr = new TextDecoder().decode(decryptResult.plaintext);
|
|
335
|
+
return {
|
|
336
|
+
conversationId: session.conversationId,
|
|
337
|
+
plaintext: plaintextStr,
|
|
338
|
+
isOwnMessage: false,
|
|
339
|
+
session: decryptResult.session,
|
|
340
|
+
topic,
|
|
341
|
+
topicMatch,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Send a message with full lifecycle management.
|
|
346
|
+
*
|
|
347
|
+
* This is the high-level API that handles:
|
|
348
|
+
* 1. Encryption (with session commit)
|
|
349
|
+
* 2. Pending record creation
|
|
350
|
+
* 3. Transaction submission
|
|
351
|
+
* 4. Status tracking
|
|
352
|
+
*
|
|
353
|
+
* After calling this, wait for on-chain confirmation and call confirmTx().
|
|
354
|
+
*
|
|
355
|
+
* @param conversationId - Conversation to send in
|
|
356
|
+
* @param plaintext - Message text
|
|
357
|
+
* @returns SendResult with txHash and metadata
|
|
358
|
+
*/
|
|
359
|
+
async sendMessage(conversationId, plaintext) {
|
|
360
|
+
if (!this.sessionManager) {
|
|
361
|
+
throw new Error('SessionStore not configured. Call setSessionStore() first.');
|
|
362
|
+
}
|
|
363
|
+
if (!this.pendingManager) {
|
|
364
|
+
throw new Error('PendingStore not configured. Call setPendingStore() first.');
|
|
365
|
+
}
|
|
366
|
+
// 1. Prepare message (encrypts and persists session)
|
|
367
|
+
const prepared = await this.prepareMessage(conversationId, plaintext);
|
|
368
|
+
// 2. Create pending record
|
|
369
|
+
await this.pendingManager.create({
|
|
370
|
+
id: prepared.id,
|
|
371
|
+
conversationId,
|
|
372
|
+
topic: prepared.topic,
|
|
373
|
+
payloadHex: hexlify(prepared.payload),
|
|
374
|
+
plaintext,
|
|
375
|
+
sessionStateBefore: JSON.stringify(this.serializeSessionInfo(prepared.sessionBefore)),
|
|
376
|
+
sessionStateAfter: JSON.stringify(this.serializeSessionInfo(prepared.sessionAfter)),
|
|
377
|
+
createdAt: prepared.createdAt,
|
|
164
378
|
});
|
|
379
|
+
// 3. Submit transaction
|
|
380
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
381
|
+
const nonce = prepared.messageNumber;
|
|
382
|
+
try {
|
|
383
|
+
const tx = await this.executor.sendMessage(prepared.payload, prepared.topic, timestamp, BigInt(nonce));
|
|
384
|
+
// 4. Update pending with txHash
|
|
385
|
+
await this.pendingManager.markSubmitted(prepared.id, tx.hash);
|
|
386
|
+
return {
|
|
387
|
+
messageId: prepared.id,
|
|
388
|
+
txHash: tx.hash,
|
|
389
|
+
topic: prepared.topic,
|
|
390
|
+
messageNumber: nonce,
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
catch (error) {
|
|
394
|
+
// Mark as failed (ratchet slot is already burned)
|
|
395
|
+
await this.pendingManager.markFailed(prepared.id);
|
|
396
|
+
throw error;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Confirm a transaction after on-chain confirmation.
|
|
401
|
+
* Call this when you see your MessageSent event on-chain.
|
|
402
|
+
*
|
|
403
|
+
* @param txHash - Transaction hash to confirm
|
|
404
|
+
* @returns ConfirmResult or null if not found
|
|
405
|
+
*/
|
|
406
|
+
async confirmTx(txHash) {
|
|
407
|
+
if (!this.pendingManager) {
|
|
408
|
+
throw new Error('PendingStore not configured.');
|
|
409
|
+
}
|
|
410
|
+
const pending = await this.pendingManager.getByTxHash(txHash);
|
|
411
|
+
if (!pending || pending.status !== 'submitted') {
|
|
412
|
+
return null;
|
|
413
|
+
}
|
|
414
|
+
// Finalize (delete pending record)
|
|
415
|
+
const finalized = await this.pendingManager.finalize(pending.id);
|
|
416
|
+
if (!finalized) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
return {
|
|
420
|
+
conversationId: finalized.conversationId,
|
|
421
|
+
plaintext: finalized.plaintext,
|
|
422
|
+
messageId: finalized.id,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Handle transaction failure/revert.
|
|
427
|
+
*
|
|
428
|
+
* The ratchet slot is already burned (session was persisted in prepareMessage).
|
|
429
|
+
* This just cleans up the pending record.
|
|
430
|
+
*
|
|
431
|
+
* @param txHash - Transaction hash that failed
|
|
432
|
+
*/
|
|
433
|
+
async revertTx(txHash) {
|
|
434
|
+
if (!this.pendingManager) {
|
|
435
|
+
throw new Error('PendingStore not configured.');
|
|
436
|
+
}
|
|
437
|
+
const pending = await this.pendingManager.getByTxHash(txHash);
|
|
438
|
+
if (pending) {
|
|
439
|
+
await this.pendingManager.delete(pending.id);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
invalidateSessionCache(conversationId) {
|
|
443
|
+
this.sessionManager?.invalidate(conversationId);
|
|
444
|
+
}
|
|
445
|
+
clearSessionCache() {
|
|
446
|
+
this.sessionManager?.clearCache();
|
|
447
|
+
}
|
|
448
|
+
async getSession(conversationId) {
|
|
449
|
+
return this.sessionManager?.getByConversationId(conversationId) ?? null;
|
|
165
450
|
}
|
|
166
|
-
//
|
|
451
|
+
// ===========================================================================
|
|
452
|
+
// Low-level API Access
|
|
453
|
+
// ===========================================================================
|
|
167
454
|
get crypto() {
|
|
168
455
|
return crypto;
|
|
169
456
|
}
|
|
@@ -179,6 +466,9 @@ export class VerbethClient {
|
|
|
179
466
|
get identity() {
|
|
180
467
|
return identity;
|
|
181
468
|
}
|
|
469
|
+
get ratchet() {
|
|
470
|
+
return ratchet;
|
|
471
|
+
}
|
|
182
472
|
get executorInstance() {
|
|
183
473
|
return this.executor;
|
|
184
474
|
}
|
|
@@ -188,4 +478,20 @@ export class VerbethClient {
|
|
|
188
478
|
get userAddress() {
|
|
189
479
|
return this.address;
|
|
190
480
|
}
|
|
481
|
+
get identityProofInstance() {
|
|
482
|
+
return this.identityProof;
|
|
483
|
+
}
|
|
484
|
+
generatePreparedId() {
|
|
485
|
+
return `prep-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
486
|
+
}
|
|
487
|
+
serializeSessionInfo(session) {
|
|
488
|
+
return {
|
|
489
|
+
conversationId: session.conversationId,
|
|
490
|
+
topicEpoch: session.topicEpoch,
|
|
491
|
+
sendingMsgNumber: session.sendingMsgNumber,
|
|
492
|
+
receivingMsgNumber: session.receivingMsgNumber,
|
|
493
|
+
currentTopicOutbound: session.currentTopicOutbound,
|
|
494
|
+
currentTopicInbound: session.currentTopicInbound,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
191
497
|
}
|