@verbeth/sdk 0.1.4 → 0.1.5
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 +16 -167
- 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 +1 -2
- package/dist/esm/src/executor.d.ts.map +1 -1
- package/dist/esm/src/executor.js +8 -24
- 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 +10 -7
- package/dist/esm/src/index.d.ts.map +1 -1
- package/dist/esm/src/index.js +9 -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/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 +1 -2
- package/dist/src/executor.d.ts.map +1 -1
- package/dist/src/executor.js +8 -24
- 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 +10 -7
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -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
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// packages/sdk/src/pq/kem.ts
|
|
2
|
+
/**
|
|
3
|
+
* ML-KEM-768 Key Encapsulation Mechanism wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Provides post-quantum key encapsulation for hybrid handshakes.
|
|
6
|
+
*/
|
|
7
|
+
import { ml_kem768 } from '@noble/post-quantum/ml-kem.js';
|
|
8
|
+
export const kem = {
|
|
9
|
+
publicKeyBytes: 1184, // in bytes
|
|
10
|
+
ciphertextBytes: 1088,
|
|
11
|
+
sharedSecretBytes: 32,
|
|
12
|
+
/**
|
|
13
|
+
* Generate a new ML-KEM-768 keypair.
|
|
14
|
+
*
|
|
15
|
+
* @returns Object containing publicKey and secretKey
|
|
16
|
+
*/
|
|
17
|
+
generateKeyPair() {
|
|
18
|
+
return ml_kem768.keygen();
|
|
19
|
+
},
|
|
20
|
+
/**
|
|
21
|
+
* Encapsulate a shared secret using the recipient's public key.
|
|
22
|
+
*
|
|
23
|
+
* @param publicKey - Recipient's ML-KEM-768 public key
|
|
24
|
+
* @returns Object containing ciphertext and sharedSecret
|
|
25
|
+
*/
|
|
26
|
+
encapsulate(publicKey) {
|
|
27
|
+
const result = ml_kem768.encapsulate(publicKey);
|
|
28
|
+
return { ciphertext: result.cipherText, sharedSecret: result.sharedSecret };
|
|
29
|
+
},
|
|
30
|
+
/**
|
|
31
|
+
* Decapsulate a ciphertext using the secret key to recover the shared secret.
|
|
32
|
+
*
|
|
33
|
+
* @param ciphertext - KEM ciphertext
|
|
34
|
+
* @param secretKey - Recipient's ML-KEM-768 secret key
|
|
35
|
+
* @returns Shared secret
|
|
36
|
+
*/
|
|
37
|
+
decapsulate(ciphertext, secretKey) {
|
|
38
|
+
return ml_kem768.decapsulate(ciphertext, secretKey);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { MessageHeader } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Verify message signature before any ratchet operations.
|
|
4
|
+
* This is the primary DoS protection layer.
|
|
5
|
+
*
|
|
6
|
+
* The signature covers (header || ciphertext), where header is the 40-byte
|
|
7
|
+
* binary encoding of (dh, pn, n).
|
|
8
|
+
*
|
|
9
|
+
* @param signature - Ed25519 signature (64 bytes)
|
|
10
|
+
* @param header - Message header
|
|
11
|
+
* @param ciphertext - Encrypted payload
|
|
12
|
+
* @param signingPublicKey - Contact's Ed25519 public key (32 bytes)
|
|
13
|
+
* @returns true if signature is valid
|
|
14
|
+
*/
|
|
15
|
+
export declare function verifyMessageSignature(signature: Uint8Array, header: MessageHeader, ciphertext: Uint8Array, signingPublicKey: Uint8Array): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* Create Ed25519 signature for a message.
|
|
18
|
+
*
|
|
19
|
+
* @param header - Message header
|
|
20
|
+
* @param ciphertext - Encrypted payload
|
|
21
|
+
* @param signingSecretKey - Ed25519 secret key (64 bytes)
|
|
22
|
+
* @returns Ed25519 signature (64 bytes)
|
|
23
|
+
*/
|
|
24
|
+
export declare function signMessage(header: MessageHeader, ciphertext: Uint8Array, signingSecretKey: Uint8Array): Uint8Array;
|
|
25
|
+
/**
|
|
26
|
+
* Validate that a parsed payload has a well-formed signature and header.
|
|
27
|
+
* Does NOT verify the signature - just checks lengths and format.
|
|
28
|
+
*
|
|
29
|
+
* @param signature - Signature bytes
|
|
30
|
+
* @param header - Parsed header
|
|
31
|
+
* @returns true if format is valid
|
|
32
|
+
*/
|
|
33
|
+
export declare function isValidPayloadFormat(signature: Uint8Array, header: MessageHeader): boolean;
|
|
34
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../../src/ratchet/auth.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAM3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,UAAU,EACtB,gBAAgB,EAAE,UAAU,GAC3B,OAAO,CAmBT;AAkBD;;;;;;;GAOG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,UAAU,EACtB,gBAAgB,EAAE,UAAU,GAC3B,UAAU,CAOZ;AAMD;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,aAAa,GACpB,OAAO,CAST"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// packages/sdk/src/ratchet/auth.ts
|
|
2
|
+
/**
|
|
3
|
+
* Message Authentication for Ratchet Protocol.
|
|
4
|
+
*/
|
|
5
|
+
import nacl from 'tweetnacl';
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Signature Verification
|
|
8
|
+
// =============================================================================
|
|
9
|
+
/**
|
|
10
|
+
* Verify message signature before any ratchet operations.
|
|
11
|
+
* This is the primary DoS protection layer.
|
|
12
|
+
*
|
|
13
|
+
* The signature covers (header || ciphertext), where header is the 40-byte
|
|
14
|
+
* binary encoding of (dh, pn, n).
|
|
15
|
+
*
|
|
16
|
+
* @param signature - Ed25519 signature (64 bytes)
|
|
17
|
+
* @param header - Message header
|
|
18
|
+
* @param ciphertext - Encrypted payload
|
|
19
|
+
* @param signingPublicKey - Contact's Ed25519 public key (32 bytes)
|
|
20
|
+
* @returns true if signature is valid
|
|
21
|
+
*/
|
|
22
|
+
export function verifyMessageSignature(signature, header, ciphertext, signingPublicKey) {
|
|
23
|
+
if (signature.length !== 64) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
if (signingPublicKey.length !== 32) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
// Reconstruct signed data: header || ciphertext
|
|
30
|
+
const headerBytes = encodeHeaderForSigning(header);
|
|
31
|
+
const dataToVerify = new Uint8Array(headerBytes.length + ciphertext.length);
|
|
32
|
+
dataToVerify.set(headerBytes, 0);
|
|
33
|
+
dataToVerify.set(ciphertext, headerBytes.length);
|
|
34
|
+
try {
|
|
35
|
+
return nacl.sign.detached.verify(dataToVerify, signature, signingPublicKey);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Encode header as 40 bytes for signature verification.
|
|
43
|
+
* Format: dh (32) + pn (4, BE) + n (4, BE)
|
|
44
|
+
*/
|
|
45
|
+
function encodeHeaderForSigning(header) {
|
|
46
|
+
const buf = new Uint8Array(40);
|
|
47
|
+
buf.set(header.dh, 0);
|
|
48
|
+
new DataView(buf.buffer).setUint32(32, header.pn, false); // big-endian
|
|
49
|
+
new DataView(buf.buffer).setUint32(36, header.n, false);
|
|
50
|
+
return buf;
|
|
51
|
+
}
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// Signature Creation
|
|
54
|
+
// =============================================================================
|
|
55
|
+
/**
|
|
56
|
+
* Create Ed25519 signature for a message.
|
|
57
|
+
*
|
|
58
|
+
* @param header - Message header
|
|
59
|
+
* @param ciphertext - Encrypted payload
|
|
60
|
+
* @param signingSecretKey - Ed25519 secret key (64 bytes)
|
|
61
|
+
* @returns Ed25519 signature (64 bytes)
|
|
62
|
+
*/
|
|
63
|
+
export function signMessage(header, ciphertext, signingSecretKey) {
|
|
64
|
+
const headerBytes = encodeHeaderForSigning(header);
|
|
65
|
+
const dataToSign = new Uint8Array(headerBytes.length + ciphertext.length);
|
|
66
|
+
dataToSign.set(headerBytes, 0);
|
|
67
|
+
dataToSign.set(ciphertext, headerBytes.length);
|
|
68
|
+
return nacl.sign.detached(dataToSign, signingSecretKey);
|
|
69
|
+
}
|
|
70
|
+
// =============================================================================
|
|
71
|
+
// Validation Helpers
|
|
72
|
+
// =============================================================================
|
|
73
|
+
/**
|
|
74
|
+
* Validate that a parsed payload has a well-formed signature and header.
|
|
75
|
+
* Does NOT verify the signature - just checks lengths and format.
|
|
76
|
+
*
|
|
77
|
+
* @param signature - Signature bytes
|
|
78
|
+
* @param header - Parsed header
|
|
79
|
+
* @returns true if format is valid
|
|
80
|
+
*/
|
|
81
|
+
export function isValidPayloadFormat(signature, header) {
|
|
82
|
+
return (signature.length === 64 &&
|
|
83
|
+
header.dh.length === 32 &&
|
|
84
|
+
header.pn >= 0 &&
|
|
85
|
+
header.n >= 0 &&
|
|
86
|
+
Number.isInteger(header.pn) &&
|
|
87
|
+
Number.isInteger(header.n));
|
|
88
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary Codec for Ratchet Messages.
|
|
3
|
+
*
|
|
4
|
+
* Wire format:
|
|
5
|
+
* ┌─────────────────────────────────────────────────────────────┐
|
|
6
|
+
* │ Offset │ Size │ Field │
|
|
7
|
+
* ├────────┼──────┼─────────────────────────────────────────────┤
|
|
8
|
+
* │ 0 │ 1 │ Version (0x01) │
|
|
9
|
+
* │ 1 │ 64 │ Ed25519 signature │
|
|
10
|
+
* │ 65 │ 32 │ DH ratchet public key │
|
|
11
|
+
* │ 97 │ 4 │ pn (uint32 BE) - previous chain length │
|
|
12
|
+
* │ 101 │ 4 │ n (uint32 BE) - message number │
|
|
13
|
+
* │ 105 │ var │ Ciphertext (nonce + AEAD output) │
|
|
14
|
+
* └─────────────────────────────────────────────────────────────┘
|
|
15
|
+
*
|
|
16
|
+
* Total minimum: 105 bytes + ciphertext
|
|
17
|
+
*/
|
|
18
|
+
import { MessageHeader, ParsedRatchetPayload } from './types.js';
|
|
19
|
+
/**
|
|
20
|
+
* Package ratchet message components into binary format.
|
|
21
|
+
*
|
|
22
|
+
* @param signature - Ed25519 signature (64 bytes)
|
|
23
|
+
* @param header - Message header (dh, pn, n)
|
|
24
|
+
* @param ciphertext - Encrypted payload (nonce + secretbox output)
|
|
25
|
+
* @returns Binary payload ready for on-chain submission
|
|
26
|
+
*/
|
|
27
|
+
export declare function packageRatchetPayload(signature: Uint8Array, header: MessageHeader, ciphertext: Uint8Array): Uint8Array;
|
|
28
|
+
/**
|
|
29
|
+
* Parse a binary ratchet payload.
|
|
30
|
+
*
|
|
31
|
+
* @param payload - Raw binary payload
|
|
32
|
+
* @returns Parsed components, or null if invalid format
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseRatchetPayload(payload: Uint8Array): ParsedRatchetPayload | null;
|
|
35
|
+
/**
|
|
36
|
+
* Check if payload is in ratchet format.
|
|
37
|
+
* Used to distinguish ratchet messages from legacy JSON format.
|
|
38
|
+
*
|
|
39
|
+
* @param payload - Raw payload bytes
|
|
40
|
+
* @returns true if payload starts with ratchet version byte
|
|
41
|
+
*/
|
|
42
|
+
export declare function isRatchetPayload(payload: Uint8Array): boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Check if hex string represents a ratchet payload.
|
|
45
|
+
*
|
|
46
|
+
* @param hexPayload - Hex string (with or without 0x prefix)
|
|
47
|
+
* @returns true if payload is ratchet format
|
|
48
|
+
*/
|
|
49
|
+
export declare function isRatchetPayloadHex(hexPayload: string): boolean;
|
|
50
|
+
export declare function hexToBytes(hex: string): Uint8Array;
|
|
51
|
+
export declare function bytesToHex(bytes: Uint8Array, prefix?: boolean): string;
|
|
52
|
+
//# sourceMappingURL=codec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codec.d.ts","sourceRoot":"","sources":["../../../src/ratchet/codec.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EAAE,aAAa,EAAE,oBAAoB,EAAsB,MAAM,YAAY,CAAC;AAKrF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,UAAU,EACrB,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,UAAU,GACrB,UAAU,CAiCZ;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,UAAU,GAAG,oBAAoB,GAAG,IAAI,CAmCpF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAE7D;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAQ/D;AAGD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAOlD;AAED,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,GAAE,OAAc,GAAG,MAAM,CAK5E"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// packages/sdk/src/ratchet/codec.ts
|
|
2
|
+
/**
|
|
3
|
+
* Binary Codec for Ratchet Messages.
|
|
4
|
+
*
|
|
5
|
+
* Wire format:
|
|
6
|
+
* ┌─────────────────────────────────────────────────────────────┐
|
|
7
|
+
* │ Offset │ Size │ Field │
|
|
8
|
+
* ├────────┼──────┼─────────────────────────────────────────────┤
|
|
9
|
+
* │ 0 │ 1 │ Version (0x01) │
|
|
10
|
+
* │ 1 │ 64 │ Ed25519 signature │
|
|
11
|
+
* │ 65 │ 32 │ DH ratchet public key │
|
|
12
|
+
* │ 97 │ 4 │ pn (uint32 BE) - previous chain length │
|
|
13
|
+
* │ 101 │ 4 │ n (uint32 BE) - message number │
|
|
14
|
+
* │ 105 │ var │ Ciphertext (nonce + AEAD output) │
|
|
15
|
+
* └─────────────────────────────────────────────────────────────┘
|
|
16
|
+
*
|
|
17
|
+
* Total minimum: 105 bytes + ciphertext
|
|
18
|
+
*/
|
|
19
|
+
import { RATCHET_VERSION_V1 } from './types.js';
|
|
20
|
+
/** Minimum payload length: version(1) + sig(64) + dh(32) + pn(4) + n(4) */
|
|
21
|
+
const MIN_PAYLOAD_LENGTH = 1 + 64 + 32 + 4 + 4; // 105 bytes
|
|
22
|
+
/**
|
|
23
|
+
* Package ratchet message components into binary format.
|
|
24
|
+
*
|
|
25
|
+
* @param signature - Ed25519 signature (64 bytes)
|
|
26
|
+
* @param header - Message header (dh, pn, n)
|
|
27
|
+
* @param ciphertext - Encrypted payload (nonce + secretbox output)
|
|
28
|
+
* @returns Binary payload ready for on-chain submission
|
|
29
|
+
*/
|
|
30
|
+
export function packageRatchetPayload(signature, header, ciphertext) {
|
|
31
|
+
if (signature.length !== 64) {
|
|
32
|
+
throw new Error(`Invalid signature length: ${signature.length}, expected 64`);
|
|
33
|
+
}
|
|
34
|
+
if (header.dh.length !== 32) {
|
|
35
|
+
throw new Error(`Invalid DH key length: ${header.dh.length}, expected 32`);
|
|
36
|
+
}
|
|
37
|
+
// Total: 1 + 64 + 32 + 4 + 4 + ciphertext.length = 105 + ciphertext.length
|
|
38
|
+
const payload = new Uint8Array(MIN_PAYLOAD_LENGTH + ciphertext.length);
|
|
39
|
+
const view = new DataView(payload.buffer);
|
|
40
|
+
let offset = 0;
|
|
41
|
+
payload[offset++] = RATCHET_VERSION_V1;
|
|
42
|
+
payload.set(signature, offset);
|
|
43
|
+
offset += 64;
|
|
44
|
+
payload.set(header.dh, offset);
|
|
45
|
+
offset += 32;
|
|
46
|
+
// pn (uint32 big-endian)
|
|
47
|
+
view.setUint32(offset, header.pn, false);
|
|
48
|
+
offset += 4;
|
|
49
|
+
// n (uint32 big-endian)
|
|
50
|
+
view.setUint32(offset, header.n, false);
|
|
51
|
+
offset += 4;
|
|
52
|
+
payload.set(ciphertext, offset);
|
|
53
|
+
return payload;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Parse a binary ratchet payload.
|
|
57
|
+
*
|
|
58
|
+
* @param payload - Raw binary payload
|
|
59
|
+
* @returns Parsed components, or null if invalid format
|
|
60
|
+
*/
|
|
61
|
+
export function parseRatchetPayload(payload) {
|
|
62
|
+
if (payload.length < MIN_PAYLOAD_LENGTH) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const view = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
|
|
66
|
+
let offset = 0;
|
|
67
|
+
const version = payload[offset++];
|
|
68
|
+
if (version !== RATCHET_VERSION_V1) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const signature = payload.slice(offset, offset + 64);
|
|
72
|
+
offset += 64;
|
|
73
|
+
const dh = payload.slice(offset, offset + 32);
|
|
74
|
+
offset += 32;
|
|
75
|
+
// pn (uint32 big-endian)
|
|
76
|
+
const pn = view.getUint32(offset, false);
|
|
77
|
+
offset += 4;
|
|
78
|
+
// n (uint32 big-endian)
|
|
79
|
+
const n = view.getUint32(offset, false);
|
|
80
|
+
offset += 4;
|
|
81
|
+
const ciphertext = payload.slice(offset);
|
|
82
|
+
return {
|
|
83
|
+
version,
|
|
84
|
+
signature,
|
|
85
|
+
header: { dh, pn, n },
|
|
86
|
+
ciphertext,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Check if payload is in ratchet format.
|
|
91
|
+
* Used to distinguish ratchet messages from legacy JSON format.
|
|
92
|
+
*
|
|
93
|
+
* @param payload - Raw payload bytes
|
|
94
|
+
* @returns true if payload starts with ratchet version byte
|
|
95
|
+
*/
|
|
96
|
+
export function isRatchetPayload(payload) {
|
|
97
|
+
return payload.length >= MIN_PAYLOAD_LENGTH && payload[0] === RATCHET_VERSION_V1;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Check if hex string represents a ratchet payload.
|
|
101
|
+
*
|
|
102
|
+
* @param hexPayload - Hex string (with or without 0x prefix)
|
|
103
|
+
* @returns true if payload is ratchet format
|
|
104
|
+
*/
|
|
105
|
+
export function isRatchetPayloadHex(hexPayload) {
|
|
106
|
+
const hex = hexPayload.startsWith('0x') ? hexPayload.slice(2) : hexPayload;
|
|
107
|
+
if (hex.length < MIN_PAYLOAD_LENGTH * 2) {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
// Check first byte is version
|
|
111
|
+
const firstByte = parseInt(hex.slice(0, 2), 16);
|
|
112
|
+
return firstByte === RATCHET_VERSION_V1;
|
|
113
|
+
}
|
|
114
|
+
export function hexToBytes(hex) {
|
|
115
|
+
const cleanHex = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
116
|
+
const bytes = new Uint8Array(cleanHex.length / 2);
|
|
117
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
118
|
+
bytes[i] = parseInt(cleanHex.slice(i * 2, i * 2 + 2), 16);
|
|
119
|
+
}
|
|
120
|
+
return bytes;
|
|
121
|
+
}
|
|
122
|
+
export function bytesToHex(bytes, prefix = true) {
|
|
123
|
+
const hex = Array.from(bytes)
|
|
124
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
125
|
+
.join('');
|
|
126
|
+
return prefix ? `0x${hex}` : hex;
|
|
127
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { RatchetSession, MessageHeader, DecryptResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decrypt a message using the ratchet.
|
|
4
|
+
*
|
|
5
|
+
* @param session - Current ratchet session state
|
|
6
|
+
* @param header - Parsed message header
|
|
7
|
+
* @param ciphertext - Encrypted payload (nonce + secretbox output)
|
|
8
|
+
* @returns Decrypt result with new session state and plaintext, or null on failure
|
|
9
|
+
*/
|
|
10
|
+
export declare function ratchetDecrypt(session: RatchetSession, header: MessageHeader, ciphertext: Uint8Array): DecryptResult | null;
|
|
11
|
+
/**
|
|
12
|
+
* Prune expired skipped keys from session.
|
|
13
|
+
*
|
|
14
|
+
* @param session - Current session
|
|
15
|
+
* @param maxAgeMs - Maximum age in milliseconds (default: 24 hours)
|
|
16
|
+
* @returns Session with pruned skipped keys
|
|
17
|
+
*/
|
|
18
|
+
export declare function pruneExpiredSkippedKeys(session: RatchetSession, maxAgeMs?: number): RatchetSession;
|
|
19
|
+
/**
|
|
20
|
+
* Check if topic matches this session.
|
|
21
|
+
* Returns match type or null.
|
|
22
|
+
*
|
|
23
|
+
* @param session - Ratchet session to check
|
|
24
|
+
* @param topic - Topic to match against
|
|
25
|
+
* @returns 'current' if matches current inbound, 'previous' if matches previous (within grace), null otherwise
|
|
26
|
+
*/
|
|
27
|
+
export declare function matchesSessionTopic(session: RatchetSession, topic: `0x${string}`): 'current' | 'previous' | null;
|
|
28
|
+
//# sourceMappingURL=decrypt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decrypt.d.ts","sourceRoot":"","sources":["../../../src/ratchet/decrypt.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,cAAc,EACd,aAAa,EACb,aAAa,EAKd,MAAM,YAAY,CAAC;AAGpB;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,MAAM,EAAE,aAAa,EACrB,UAAU,EAAE,UAAU,GACrB,aAAa,GAAG,IAAI,CA4EtB;AAgKD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,cAAc,EACvB,QAAQ,GAAE,MAA4B,GACrC,cAAc,CAyBhB;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,KAAK,MAAM,EAAE,GACnB,SAAS,GAAG,UAAU,GAAG,IAAI,CAgB/B"}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
// packages/sdk/src/ratchet/decrypt.ts
|
|
2
|
+
/**
|
|
3
|
+
* Ratchet Decryption with Skip Key Handling.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* - Normal sequential message decryption
|
|
7
|
+
* - DH ratchet steps when sender's DH key changes
|
|
8
|
+
* - Out-of-order messages via skipped keys
|
|
9
|
+
* - Topic ratcheting synchronized with DH ratchet
|
|
10
|
+
*/
|
|
11
|
+
import nacl from 'tweetnacl';
|
|
12
|
+
import { hexlify } from 'ethers';
|
|
13
|
+
import { MAX_SKIP_PER_MESSAGE, MAX_STORED_SKIPPED_KEYS, TOPIC_TRANSITION_WINDOW_MS, } from './types.js';
|
|
14
|
+
import { kdfRootKey, kdfChainKey, dh, generateDHKeyPair, deriveTopic } from './kdf.js';
|
|
15
|
+
/**
|
|
16
|
+
* Decrypt a message using the ratchet.
|
|
17
|
+
*
|
|
18
|
+
* @param session - Current ratchet session state
|
|
19
|
+
* @param header - Parsed message header
|
|
20
|
+
* @param ciphertext - Encrypted payload (nonce + secretbox output)
|
|
21
|
+
* @returns Decrypt result with new session state and plaintext, or null on failure
|
|
22
|
+
*/
|
|
23
|
+
export function ratchetDecrypt(session, header, ciphertext) {
|
|
24
|
+
// Sanity check: even authenticated messages shouldn't require insane skips
|
|
25
|
+
const skipNeeded = Math.max(0, header.n - session.receivingMsgNumber);
|
|
26
|
+
if (skipNeeded > MAX_SKIP_PER_MESSAGE || header.pn > MAX_SKIP_PER_MESSAGE) {
|
|
27
|
+
console.error(`Message requires ${skipNeeded} skips (pn=${header.pn}) — likely corrupted or malicious peer`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const dhPubHex = hexlify(header.dh);
|
|
31
|
+
const currentTheirDHHex = session.dhTheirPublicKey
|
|
32
|
+
? hexlify(session.dhTheirPublicKey)
|
|
33
|
+
: null;
|
|
34
|
+
// 1. Try skipped keys first (handles out-of-order messages)
|
|
35
|
+
const skippedResult = trySkippedKeys(session, dhPubHex, header.n, ciphertext);
|
|
36
|
+
if (skippedResult) {
|
|
37
|
+
return skippedResult;
|
|
38
|
+
}
|
|
39
|
+
// 2. Clone session for modifications
|
|
40
|
+
let newSession = { ...session, skippedKeys: [...session.skippedKeys] };
|
|
41
|
+
// 3. Check if we need to perform a DH ratchet step
|
|
42
|
+
if (dhPubHex !== currentTheirDHHex) {
|
|
43
|
+
// Skip any remaining messages from previous receiving chain
|
|
44
|
+
if (newSession.receivingChainKey) {
|
|
45
|
+
newSession = skipMessages(newSession, newSession.receivingMsgNumber, header.pn);
|
|
46
|
+
}
|
|
47
|
+
// Perform DH ratchet step
|
|
48
|
+
newSession = dhRatchetStep(newSession, header.dh);
|
|
49
|
+
}
|
|
50
|
+
// 4. Skip messages if n > receivingMsgNumber (within current epoch)
|
|
51
|
+
if (header.n > newSession.receivingMsgNumber) {
|
|
52
|
+
newSession = skipMessages(newSession, newSession.receivingMsgNumber, header.n);
|
|
53
|
+
}
|
|
54
|
+
// 5. Derive message key
|
|
55
|
+
if (!newSession.receivingChainKey) {
|
|
56
|
+
console.error('No receiving chain key available');
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const { chainKey: newReceivingChainKey, messageKey } = kdfChainKey(newSession.receivingChainKey);
|
|
60
|
+
// 6. Decrypt
|
|
61
|
+
const plaintext = decryptWithKey(ciphertext, messageKey);
|
|
62
|
+
// 7. Wipe message key
|
|
63
|
+
try {
|
|
64
|
+
messageKey.fill(0);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore if fill fails
|
|
68
|
+
}
|
|
69
|
+
if (!plaintext) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
// 8. Update session state
|
|
73
|
+
newSession = {
|
|
74
|
+
...newSession,
|
|
75
|
+
receivingChainKey: newReceivingChainKey,
|
|
76
|
+
receivingMsgNumber: header.n + 1,
|
|
77
|
+
updatedAt: Date.now(),
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
session: newSession,
|
|
81
|
+
plaintext,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* DH ratchet step on receipt of a message with a new remote DH public key.
|
|
86
|
+
*
|
|
87
|
+
* Topic derivation is sender-centric: `deriveTopicFromDH(x, 'outbound')` denotes
|
|
88
|
+
* the topic used by the party who *sent* the DH pubkey for their sending direction.
|
|
89
|
+
* Therefore, when we ratchet on receive, we swap labels for topics derived from `dhReceive`.
|
|
90
|
+
*/
|
|
91
|
+
function dhRatchetStep(session, theirNewDHPub) {
|
|
92
|
+
// Advance receiving chain (based on our current DH secret and their new DH pubkey)
|
|
93
|
+
const dhReceive = dh(session.dhMySecretKey, theirNewDHPub);
|
|
94
|
+
const { rootKey: rootKey1, chainKey: receivingChainKey } = kdfRootKey(session.rootKey, dhReceive);
|
|
95
|
+
// Generate new DH keypair for our next sending chain
|
|
96
|
+
const newDHKeyPair = generateDHKeyPair();
|
|
97
|
+
// Advance sending chain
|
|
98
|
+
const dhSend = dh(newDHKeyPair.secretKey, theirNewDHPub);
|
|
99
|
+
const { rootKey: rootKey2, chainKey: sendingChainKey } = kdfRootKey(rootKey1, dhSend);
|
|
100
|
+
// Current topics (post ratchet) - swapped since we're the receiver
|
|
101
|
+
// Use rootKey1 (derived from dhReceive) for PQ-secure topic derivation
|
|
102
|
+
const newTopicOut = deriveTopic(rootKey1, dhReceive, 'inbound');
|
|
103
|
+
const newTopicIn = deriveTopic(rootKey1, dhReceive, 'outbound');
|
|
104
|
+
// Next topics (for our next DH pubkey) - normal labels because we will be the sender
|
|
105
|
+
// Use rootKey2 (derived from dhSend) for PQ-secure topic derivation
|
|
106
|
+
const nextTopicOut = deriveTopic(rootKey2, dhSend, 'outbound');
|
|
107
|
+
const nextTopicIn = deriveTopic(rootKey2, dhSend, 'inbound');
|
|
108
|
+
return {
|
|
109
|
+
...session,
|
|
110
|
+
rootKey: rootKey2,
|
|
111
|
+
dhMySecretKey: newDHKeyPair.secretKey,
|
|
112
|
+
dhMyPublicKey: newDHKeyPair.publicKey,
|
|
113
|
+
dhTheirPublicKey: theirNewDHPub,
|
|
114
|
+
receivingChainKey,
|
|
115
|
+
receivingMsgNumber: 0,
|
|
116
|
+
sendingChainKey,
|
|
117
|
+
sendingMsgNumber: 0,
|
|
118
|
+
previousChainLength: session.sendingMsgNumber,
|
|
119
|
+
nextTopicOutbound: nextTopicOut,
|
|
120
|
+
nextTopicInbound: nextTopicIn,
|
|
121
|
+
currentTopicOutbound: newTopicOut,
|
|
122
|
+
currentTopicInbound: newTopicIn,
|
|
123
|
+
previousTopicInbound: session.currentTopicInbound,
|
|
124
|
+
previousTopicExpiry: Date.now() + TOPIC_TRANSITION_WINDOW_MS,
|
|
125
|
+
topicEpoch: session.topicEpoch + 1,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Skip messages by deriving and storing their keys for later out-of-order decryption.
|
|
130
|
+
*
|
|
131
|
+
* Called when:
|
|
132
|
+
* - header.n > receivingMsgNumber (messages skipped in current epoch)
|
|
133
|
+
* - DH ratchet step with header.pn > 0 (messages from previous epoch)
|
|
134
|
+
*/
|
|
135
|
+
function skipMessages(session, start, until) {
|
|
136
|
+
if (!session.receivingChainKey || until <= start) {
|
|
137
|
+
return session;
|
|
138
|
+
}
|
|
139
|
+
const skippedKeys = [...session.skippedKeys];
|
|
140
|
+
let chainKey = session.receivingChainKey;
|
|
141
|
+
const dhPubHex = hexlify(session.dhTheirPublicKey);
|
|
142
|
+
const now = Date.now();
|
|
143
|
+
for (let i = start; i < until; i++) {
|
|
144
|
+
const { chainKey: newChainKey, messageKey } = kdfChainKey(chainKey);
|
|
145
|
+
skippedKeys.push({
|
|
146
|
+
dhPubKeyHex: dhPubHex,
|
|
147
|
+
msgNumber: i,
|
|
148
|
+
messageKey: new Uint8Array(messageKey),
|
|
149
|
+
createdAt: now,
|
|
150
|
+
});
|
|
151
|
+
chainKey = newChainKey;
|
|
152
|
+
}
|
|
153
|
+
// Prune if we have too many skipped keys
|
|
154
|
+
let prunedKeys = skippedKeys;
|
|
155
|
+
if (skippedKeys.length > MAX_STORED_SKIPPED_KEYS) {
|
|
156
|
+
prunedKeys = skippedKeys
|
|
157
|
+
.sort((a, b) => b.createdAt - a.createdAt)
|
|
158
|
+
.slice(0, MAX_STORED_SKIPPED_KEYS);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
...session,
|
|
162
|
+
receivingChainKey: chainKey,
|
|
163
|
+
skippedKeys: prunedKeys,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function trySkippedKeys(session, dhPubHex, msgNumber, ciphertext) {
|
|
167
|
+
const idx = session.skippedKeys.findIndex((sk) => sk.dhPubKeyHex === dhPubHex && sk.msgNumber === msgNumber);
|
|
168
|
+
if (idx === -1) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const skippedKey = session.skippedKeys[idx];
|
|
172
|
+
const plaintext = decryptWithKey(ciphertext, skippedKey.messageKey);
|
|
173
|
+
if (!plaintext) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const newSkippedKeys = [...session.skippedKeys];
|
|
177
|
+
newSkippedKeys.splice(idx, 1);
|
|
178
|
+
// Wipe the key
|
|
179
|
+
try {
|
|
180
|
+
skippedKey.messageKey.fill(0);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
session: {
|
|
186
|
+
...session,
|
|
187
|
+
skippedKeys: newSkippedKeys,
|
|
188
|
+
updatedAt: Date.now(),
|
|
189
|
+
},
|
|
190
|
+
plaintext,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Decrypt ciphertext with message key using XSalsa20-Poly1305.
|
|
195
|
+
* Ciphertext format: nonce (24 bytes) + secretbox output
|
|
196
|
+
*/
|
|
197
|
+
function decryptWithKey(ciphertext, messageKey) {
|
|
198
|
+
if (ciphertext.length < nacl.secretbox.nonceLength) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
const nonce = ciphertext.slice(0, nacl.secretbox.nonceLength);
|
|
202
|
+
const box = ciphertext.slice(nacl.secretbox.nonceLength);
|
|
203
|
+
const result = nacl.secretbox.open(box, nonce, messageKey);
|
|
204
|
+
return result || null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Prune expired skipped keys from session.
|
|
208
|
+
*
|
|
209
|
+
* @param session - Current session
|
|
210
|
+
* @param maxAgeMs - Maximum age in milliseconds (default: 24 hours)
|
|
211
|
+
* @returns Session with pruned skipped keys
|
|
212
|
+
*/
|
|
213
|
+
export function pruneExpiredSkippedKeys(session, maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
214
|
+
const now = Date.now();
|
|
215
|
+
const cutoff = now - maxAgeMs;
|
|
216
|
+
const prunedKeys = session.skippedKeys.filter((sk) => sk.createdAt > cutoff);
|
|
217
|
+
// Wipe expired keys
|
|
218
|
+
for (const sk of session.skippedKeys) {
|
|
219
|
+
if (sk.createdAt <= cutoff) {
|
|
220
|
+
try {
|
|
221
|
+
sk.messageKey.fill(0);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (prunedKeys.length === session.skippedKeys.length) {
|
|
228
|
+
return session; // No change
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
...session,
|
|
232
|
+
skippedKeys: prunedKeys,
|
|
233
|
+
updatedAt: now,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Check if topic matches this session.
|
|
238
|
+
* Returns match type or null.
|
|
239
|
+
*
|
|
240
|
+
* @param session - Ratchet session to check
|
|
241
|
+
* @param topic - Topic to match against
|
|
242
|
+
* @returns 'current' if matches current inbound, 'previous' if matches previous (within grace), null otherwise
|
|
243
|
+
*/
|
|
244
|
+
export function matchesSessionTopic(session, topic) {
|
|
245
|
+
const t = topic.toLowerCase();
|
|
246
|
+
if (session.currentTopicInbound.toLowerCase() === t) {
|
|
247
|
+
return 'current';
|
|
248
|
+
}
|
|
249
|
+
if (session.previousTopicInbound?.toLowerCase() === t &&
|
|
250
|
+
session.previousTopicExpiry &&
|
|
251
|
+
Date.now() < session.previousTopicExpiry) {
|
|
252
|
+
return 'previous';
|
|
253
|
+
}
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { RatchetSession, MessageHeader, EncryptResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Encode message header as 40 bytes for signing.
|
|
4
|
+
* Format: dh (32) + pn (4, BE) + n (4, BE)
|
|
5
|
+
*/
|
|
6
|
+
export declare function encodeHeader(header: MessageHeader): Uint8Array;
|
|
7
|
+
/**
|
|
8
|
+
* Encrypt a message using the ratchet.
|
|
9
|
+
*
|
|
10
|
+
* @param session - Current ratchet session state
|
|
11
|
+
* @param plaintext - Message to encrypt
|
|
12
|
+
* @param signingSecretKey - Ed25519 secret key for signing (64 bytes)
|
|
13
|
+
* @returns Encrypt result with new session state, header, ciphertext, signature, and topic
|
|
14
|
+
* @throws If session is not ready to send (no sending chain key)
|
|
15
|
+
*/
|
|
16
|
+
export declare function ratchetEncrypt(session: RatchetSession, plaintext: Uint8Array, signingSecretKey: Uint8Array): EncryptResult;
|
|
17
|
+
//# sourceMappingURL=encrypt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encrypt.d.ts","sourceRoot":"","sources":["../../../src/ratchet/encrypt.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAG1E;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAM9D;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,UAAU,EACrB,gBAAgB,EAAE,UAAU,GAC3B,aAAa,CAqDf"}
|