walletpair-sdk 1.0.2 → 1.0.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.
Files changed (74) hide show
  1. package/README.md +13 -0
  2. package/dist/ble/framing.d.ts.map +1 -1
  3. package/dist/ble/framing.js +2 -2
  4. package/dist/ble/framing.js.map +1 -1
  5. package/dist/ble/index.d.ts +2 -2
  6. package/dist/ble/index.d.ts.map +1 -1
  7. package/dist/ble/index.js +2 -2
  8. package/dist/ble/index.js.map +1 -1
  9. package/dist/ble/web-ble-transport.d.ts +1 -1
  10. package/dist/ble/web-ble-transport.d.ts.map +1 -1
  11. package/dist/ble/web-ble-transport.js +23 -12
  12. package/dist/ble/web-ble-transport.js.map +1 -1
  13. package/dist/crypto.d.ts.map +1 -1
  14. package/dist/crypto.js +29 -12
  15. package/dist/crypto.js.map +1 -1
  16. package/dist/dapp-session.d.ts.map +1 -1
  17. package/dist/dapp-session.js +15 -5
  18. package/dist/dapp-session.js.map +1 -1
  19. package/dist/emitter.d.ts +1 -3
  20. package/dist/emitter.d.ts.map +1 -1
  21. package/dist/emitter.js +4 -2
  22. package/dist/emitter.js.map +1 -1
  23. package/dist/evm/eip1193.d.ts +2 -2
  24. package/dist/evm/eip1193.d.ts.map +1 -1
  25. package/dist/evm/eip1193.js +32 -18
  26. package/dist/evm/eip1193.js.map +1 -1
  27. package/dist/evm/index.d.ts +2 -2
  28. package/dist/evm/index.d.ts.map +1 -1
  29. package/dist/evm/index.js.map +1 -1
  30. package/dist/wallet-session.d.ts.map +1 -1
  31. package/dist/wallet-session.js +4 -3
  32. package/dist/wallet-session.js.map +1 -1
  33. package/dist/ws-transport.d.ts +3 -2
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +13 -4
  36. package/dist/ws-transport.js.map +1 -1
  37. package/package.json +20 -1
  38. package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
  39. package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
  40. package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
  41. package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
  42. package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
  43. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
  44. package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
  45. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
  46. package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
  47. package/src/ble/framing.test.ts +122 -114
  48. package/src/ble/framing.ts +48 -51
  49. package/src/ble/index.ts +7 -7
  50. package/src/ble/web-ble-transport.test.ts +93 -84
  51. package/src/ble/web-ble-transport.ts +70 -57
  52. package/src/ble/web-bluetooth.d.ts +19 -19
  53. package/src/canonical-json.test.ts +301 -285
  54. package/src/crypto-directional.test.ts +155 -129
  55. package/src/crypto-hardening.test.ts +292 -283
  56. package/src/crypto.test.ts +364 -346
  57. package/src/crypto.ts +185 -175
  58. package/src/dapp-session.test.ts +522 -385
  59. package/src/dapp-session.ts +17 -11
  60. package/src/emitter.test.ts +122 -122
  61. package/src/emitter.ts +20 -18
  62. package/src/evm/eip1193.test.ts +283 -205
  63. package/src/evm/eip1193.ts +162 -138
  64. package/src/evm/index.ts +5 -5
  65. package/src/evm/wagmi.test.ts +1 -1
  66. package/src/integration.test.ts +329 -201
  67. package/src/security.test.ts +331 -238
  68. package/src/sequence-validation.test.ts +6 -9
  69. package/src/test-helpers.ts +102 -78
  70. package/src/types.test.ts +45 -50
  71. package/src/wallet-session.test.ts +611 -383
  72. package/src/wallet-session.ts +7 -9
  73. package/src/ws-transport.test.ts +141 -139
  74. package/src/ws-transport.ts +52 -41
@@ -8,25 +8,23 @@
8
8
  * Uses real crypto operations from the SDK — no mocks for crypto.
9
9
  */
10
10
 
11
- import { describe, it, expect } from 'vitest';
11
+ import { describe, expect, it } from 'vitest'
12
+ import type { AadHeader, SessionCryptoContext } from '../../crypto.js'
12
13
  import {
13
- generateX25519KeyPair,
14
- generateChannelId,
14
+ b64urlDecode,
15
+ b64urlEncode,
16
+ bytesToHex,
15
17
  computeSharedSecret,
16
- deriveSessionKey,
17
- deriveJoinEncryptionKey,
18
18
  deriveDirectionalSessionKeys,
19
- computeHandshakeTranscriptHash,
20
- sealPayload,
21
- unsealPayload,
19
+ deriveJoinEncryptionKey,
20
+ deriveSessionKey,
21
+ generateChannelId,
22
+ generateX25519KeyPair,
22
23
  sealJoin,
24
+ sealPayload,
23
25
  unsealJoin,
24
- b64urlEncode,
25
- b64urlDecode,
26
- bytesToHex,
27
- hexToBytes,
28
- } from '../../crypto.js';
29
- import type { AadHeader, SessionCryptoContext } from '../../crypto.js';
26
+ unsealPayload,
27
+ } from '../../crypto.js'
30
28
 
31
29
  // ---------------------------------------------------------------------------
32
30
  // Attack 1: Nonce reuse detection
@@ -43,42 +41,42 @@ describe('Crypto Attack: Nonce reuse', () => {
43
41
  // PREVENTS: Nonce reuse in ChaCha20-Poly1305, which would break
44
42
  // confidentiality (Section 6.6.1).
45
43
 
46
- const key = new Uint8Array(32);
47
- crypto.getRandomValues(key);
48
- const ch = generateChannelId();
44
+ const key = new Uint8Array(32)
45
+ crypto.getRandomValues(key)
46
+ const ch = generateChannelId()
49
47
 
50
48
  // Two messages with same seq produce same ciphertext (deterministic nonce)
51
- const s1 = sealPayload(key, ch, 42, { data: 'same' });
52
- const s2 = sealPayload(key, ch, 42, { data: 'same' });
53
- expect(s1).toBe(s2); // same key + seq + data = same output
49
+ const s1 = sealPayload(key, ch, 42, { data: 'same' })
50
+ const s2 = sealPayload(key, ch, 42, { data: 'same' })
51
+ expect(s1).toBe(s2) // same key + seq + data = same output
54
52
 
55
53
  // Receiver would accept the first and reject the second (seq not increasing).
56
54
  // At the crypto level, verify the first decrypts correctly:
57
- const { seq, data } = unsealPayload(key, ch, s1);
58
- expect(seq).toBe(42);
59
- expect(data).toEqual({ data: 'same' });
60
- });
55
+ const { seq, data } = unsealPayload(key, ch, s1)
56
+ expect(seq).toBe(42)
57
+ expect(data).toEqual({ data: 'same' })
58
+ })
61
59
 
62
60
  it('same seq with different data produces different ciphertext but same nonce (dangerous without seq check)', () => {
63
61
  // This demonstrates WHY the protocol must enforce strictly increasing seq:
64
62
  // if two different plaintexts share the same nonce, the XOR of the two
65
63
  // ciphertexts leaks the XOR of the two plaintexts.
66
64
 
67
- const key = new Uint8Array(32);
68
- crypto.getRandomValues(key);
69
- const ch = generateChannelId();
65
+ const key = new Uint8Array(32)
66
+ crypto.getRandomValues(key)
67
+ const ch = generateChannelId()
70
68
 
71
- const s1 = sealPayload(key, ch, 0, { secret: 'alpha' });
72
- const s2 = sealPayload(key, ch, 0, { secret: 'bravo' });
69
+ const s1 = sealPayload(key, ch, 0, { secret: 'alpha' })
70
+ const s2 = sealPayload(key, ch, 0, { secret: 'bravo' })
73
71
 
74
72
  // Different plaintext -> different ciphertext (even with same nonce)
75
- expect(s1).not.toBe(s2);
73
+ expect(s1).not.toBe(s2)
76
74
 
77
75
  // Both decrypt individually (attacker with key could see both)
78
- expect(unsealPayload(key, ch, s1).data).toEqual({ secret: 'alpha' });
79
- expect(unsealPayload(key, ch, s2).data).toEqual({ secret: 'bravo' });
80
- });
81
- });
76
+ expect(unsealPayload(key, ch, s1).data).toEqual({ secret: 'alpha' })
77
+ expect(unsealPayload(key, ch, s2).data).toEqual({ secret: 'bravo' })
78
+ })
79
+ })
82
80
 
83
81
  // ---------------------------------------------------------------------------
84
82
  // Attack 2: Bit-flip in ciphertext (AEAD integrity)
@@ -93,37 +91,37 @@ describe('Crypto Attack: Ciphertext bit-flip', () => {
93
91
  // PREVENTS: Ciphertext malleability. Attacker cannot modify
94
92
  // encrypted data without detection.
95
93
 
96
- const key = new Uint8Array(32);
97
- crypto.getRandomValues(key);
98
- const ch = generateChannelId();
94
+ const key = new Uint8Array(32)
95
+ crypto.getRandomValues(key)
96
+ const ch = generateChannelId()
99
97
 
100
- const sealed = sealPayload(key, ch, 0, { amount: '1000000', to: '0xVictim' });
101
- const bytes = b64urlDecode(sealed);
98
+ const sealed = sealPayload(key, ch, 0, { amount: '1000000', to: '0xVictim' })
99
+ const bytes = b64urlDecode(sealed)
102
100
 
103
101
  // Flip a bit in the ciphertext body (byte 10, well past the 4-byte seq)
104
- const tampered = new Uint8Array(bytes);
105
- tampered[10] = tampered[10]! ^ 0x01;
102
+ const tampered = new Uint8Array(bytes)
103
+ tampered[10] = (tampered[10] ?? 0) ^ 0x01
106
104
 
107
- expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow();
108
- });
105
+ expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
106
+ })
109
107
 
110
108
  it('bit flip in Poly1305 tag causes decryption failure', () => {
111
109
  // ATTACK: Attacker flips the last byte (part of the 16-byte tag).
112
110
  //
113
111
  // PREVENTS: Tag forgery.
114
112
 
115
- const key = new Uint8Array(32);
116
- crypto.getRandomValues(key);
117
- const ch = generateChannelId();
113
+ const key = new Uint8Array(32)
114
+ crypto.getRandomValues(key)
115
+ const ch = generateChannelId()
118
116
 
119
- const sealed = sealPayload(key, ch, 0, { data: 'protected' });
120
- const bytes = b64urlDecode(sealed);
117
+ const sealed = sealPayload(key, ch, 0, { data: 'protected' })
118
+ const bytes = b64urlDecode(sealed)
121
119
 
122
- const tampered = new Uint8Array(bytes);
123
- tampered[tampered.length - 1] = tampered[tampered.length - 1]! ^ 0xff;
120
+ const tampered = new Uint8Array(bytes)
121
+ tampered[tampered.length - 1] = (tampered[tampered.length - 1] ?? 0) ^ 0xff
124
122
 
125
- expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow();
126
- });
123
+ expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
124
+ })
127
125
 
128
126
  it('bit flip in seq bytes changes nonce and causes decryption failure', () => {
129
127
  // ATTACK: Attacker modifies the seq bytes (first 4 bytes of sealed).
@@ -131,19 +129,19 @@ describe('Crypto Attack: Ciphertext bit-flip', () => {
131
129
  //
132
130
  // PREVENTS: Sequence number tampering.
133
131
 
134
- const key = new Uint8Array(32);
135
- crypto.getRandomValues(key);
136
- const ch = generateChannelId();
132
+ const key = new Uint8Array(32)
133
+ crypto.getRandomValues(key)
134
+ const ch = generateChannelId()
137
135
 
138
- const sealed = sealPayload(key, ch, 7, { msg: 'test' });
139
- const bytes = b64urlDecode(sealed);
136
+ const sealed = sealPayload(key, ch, 7, { msg: 'test' })
137
+ const bytes = b64urlDecode(sealed)
140
138
 
141
- const tampered = new Uint8Array(bytes);
142
- tampered[3] = tampered[3]! ^ 0x01; // flip low bit of seq
139
+ const tampered = new Uint8Array(bytes)
140
+ tampered[3] = (tampered[3] ?? 0) ^ 0x01 // flip low bit of seq
143
141
 
144
- expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow();
145
- });
146
- });
142
+ expect(() => unsealPayload(key, ch, b64urlEncode(tampered))).toThrow()
143
+ })
144
+ })
147
145
 
148
146
  // ---------------------------------------------------------------------------
149
147
  // Attack 3: Bit-flip in AAD fields
@@ -157,17 +155,17 @@ describe('Crypto Attack: AAD field manipulation', () => {
157
155
  //
158
156
  // PREVENTS: Type confusion attacks where a req is reinterpreted as res.
159
157
 
160
- const key = new Uint8Array(32);
161
- crypto.getRandomValues(key);
162
- const ch = generateChannelId();
158
+ const key = new Uint8Array(32)
159
+ crypto.getRandomValues(key)
160
+ const ch = generateChannelId()
163
161
 
164
- const hdr: AadHeader = { type: 'req', from: 'dapp-key', id: 'req-1' };
165
- const sealed = sealPayload(key, ch, 0, { _method: 'wallet_signMessage' }, hdr);
162
+ const hdr: AadHeader = { type: 'req', from: 'dapp-key', id: 'req-1' }
163
+ const sealed = sealPayload(key, ch, 0, { _method: 'wallet_signMessage' }, hdr)
166
164
 
167
165
  // Try to decrypt with 'res' type — AAD mismatch
168
- const wrongHdr: AadHeader = { type: 'res', from: 'dapp-key', id: 'req-1' };
169
- expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow();
170
- });
166
+ const wrongHdr: AadHeader = { type: 'res', from: 'dapp-key', id: 'req-1' }
167
+ expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
168
+ })
171
169
 
172
170
  it('changing from in AAD causes decryption failure', () => {
173
171
  // ATTACK: Relay substitutes the from field in the envelope.
@@ -175,16 +173,16 @@ describe('Crypto Attack: AAD field manipulation', () => {
175
173
  //
176
174
  // PREVENTS: Sender impersonation at the AEAD level.
177
175
 
178
- const key = new Uint8Array(32);
179
- crypto.getRandomValues(key);
180
- const ch = generateChannelId();
176
+ const key = new Uint8Array(32)
177
+ crypto.getRandomValues(key)
178
+ const ch = generateChannelId()
181
179
 
182
- const hdr: AadHeader = { type: 'req', from: 'real-dapp-key', id: 'r1' };
183
- const sealed = sealPayload(key, ch, 0, { _method: 'test' }, hdr);
180
+ const hdr: AadHeader = { type: 'req', from: 'real-dapp-key', id: 'r1' }
181
+ const sealed = sealPayload(key, ch, 0, { _method: 'test' }, hdr)
184
182
 
185
- const spoofed: AadHeader = { type: 'req', from: 'relay-fake-key', id: 'r1' };
186
- expect(() => unsealPayload(key, ch, sealed, spoofed)).toThrow();
187
- });
183
+ const spoofed: AadHeader = { type: 'req', from: 'relay-fake-key', id: 'r1' }
184
+ expect(() => unsealPayload(key, ch, sealed, spoofed)).toThrow()
185
+ })
188
186
 
189
187
  it('changing id in AAD causes decryption failure', () => {
190
188
  // ATTACK: Relay swaps the request ID to redirect a response to
@@ -192,16 +190,16 @@ describe('Crypto Attack: AAD field manipulation', () => {
192
190
  //
193
191
  // PREVENTS: Request ID substitution.
194
192
 
195
- const key = new Uint8Array(32);
196
- crypto.getRandomValues(key);
197
- const ch = generateChannelId();
193
+ const key = new Uint8Array(32)
194
+ crypto.getRandomValues(key)
195
+ const ch = generateChannelId()
198
196
 
199
- const hdr: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-42' };
200
- const sealed = sealPayload(key, ch, 0, { _ok: true, _result: 'secret' }, hdr);
197
+ const hdr: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-42' }
198
+ const sealed = sealPayload(key, ch, 0, { _ok: true, _result: 'secret' }, hdr)
201
199
 
202
- const swapped: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-1' };
203
- expect(() => unsealPayload(key, ch, sealed, swapped)).toThrow();
204
- });
200
+ const swapped: AadHeader = { type: 'res', from: 'wallet-key', id: 'req-1' }
201
+ expect(() => unsealPayload(key, ch, sealed, swapped)).toThrow()
202
+ })
205
203
 
206
204
  it('changing channel ID causes decryption failure', () => {
207
205
  // ATTACK: Relay forwards a message from one channel to another.
@@ -209,15 +207,15 @@ describe('Crypto Attack: AAD field manipulation', () => {
209
207
  //
210
208
  // PREVENTS: Cross-channel message injection.
211
209
 
212
- const key = new Uint8Array(32);
213
- crypto.getRandomValues(key);
214
- const ch1 = generateChannelId();
215
- const ch2 = generateChannelId();
210
+ const key = new Uint8Array(32)
211
+ crypto.getRandomValues(key)
212
+ const ch1 = generateChannelId()
213
+ const ch2 = generateChannelId()
216
214
 
217
- const sealed = sealPayload(key, ch1, 0, { data: 'channel-1' });
218
- expect(() => unsealPayload(key, ch2, sealed)).toThrow();
219
- });
220
- });
215
+ const sealed = sealPayload(key, ch1, 0, { data: 'channel-1' })
216
+ expect(() => unsealPayload(key, ch2, sealed)).toThrow()
217
+ })
218
+ })
221
219
 
222
220
  // ---------------------------------------------------------------------------
223
221
  // Attack 4: Wrong traffic key direction
@@ -232,57 +230,63 @@ describe('Crypto Attack: Directional key confusion', () => {
232
230
  //
233
231
  // PREVENTS: Direction reversal and reflection attacks (Section 6.2).
234
232
 
235
- const dappKp = generateX25519KeyPair();
236
- const walletKp = generateX25519KeyPair();
237
- const ch = generateChannelId();
233
+ const dappKp = generateX25519KeyPair()
234
+ const walletKp = generateX25519KeyPair()
235
+ const ch = generateChannelId()
238
236
 
239
- const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey);
240
- const rootKey = deriveSessionKey(shared, ch);
237
+ const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
238
+ const rootKey = deriveSessionKey(shared, ch)
241
239
  const ctx: SessionCryptoContext = {
242
240
  dappPubKeyB64: dappKp.publicKeyB64,
243
241
  walletPubKeyB64: walletKp.publicKeyB64,
244
242
  capabilities: { methods: ['test'], events: [], chains: [] },
245
243
  dappName: 'Test',
246
- };
247
- const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx);
244
+ }
245
+ const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
248
246
 
249
247
  // Encrypt with dappToWalletKey
250
- const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { secret: 'hello' });
248
+ const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { secret: 'hello' })
251
249
 
252
250
  // Cannot decrypt with walletToDappKey
253
- expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow();
251
+ expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
254
252
 
255
253
  // Can decrypt with correct key
256
- const { data } = unsealPayload(keys.dappToWalletKey, ch, sealed);
257
- expect(data).toEqual({ secret: 'hello' });
258
- });
254
+ const { data } = unsealPayload(keys.dappToWalletKey, ch, sealed)
255
+ expect(data).toEqual({ secret: 'hello' })
256
+ })
259
257
 
260
258
  it('wallet response encrypted with dappToWallet key cannot be decrypted by dApp', () => {
261
259
  // If a wallet mistakenly uses the wrong key direction for responses,
262
260
  // the dApp cannot decrypt them.
263
261
 
264
- const dappKp = generateX25519KeyPair();
265
- const walletKp = generateX25519KeyPair();
266
- const ch = generateChannelId();
262
+ const dappKp = generateX25519KeyPair()
263
+ const walletKp = generateX25519KeyPair()
264
+ const ch = generateChannelId()
267
265
 
268
- const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey);
269
- const rootKey = deriveSessionKey(shared, ch);
266
+ const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
267
+ const rootKey = deriveSessionKey(shared, ch)
270
268
  const ctx: SessionCryptoContext = {
271
269
  dappPubKeyB64: dappKp.publicKeyB64,
272
270
  walletPubKeyB64: walletKp.publicKeyB64,
273
271
  capabilities: null,
274
272
  dappName: 'Test',
275
- };
276
- const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx);
273
+ }
274
+ const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
277
275
 
278
276
  // Wallet "accidentally" encrypts response with dappToWalletKey (wrong direction)
279
- const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: 'r1' };
280
- const sealedWrong = sealPayload(keys.dappToWalletKey, ch, 0, { _ok: true, _result: 'oops' }, hdr);
277
+ const hdr: AadHeader = { type: 'res', from: walletKp.publicKeyB64, id: 'r1' }
278
+ const sealedWrong = sealPayload(
279
+ keys.dappToWalletKey,
280
+ ch,
281
+ 0,
282
+ { _ok: true, _result: 'oops' },
283
+ hdr,
284
+ )
281
285
 
282
286
  // DApp tries to decrypt with walletToDappKey (correct for responses)
283
- expect(() => unsealPayload(keys.walletToDappKey, ch, sealedWrong, hdr)).toThrow();
284
- });
285
- });
287
+ expect(() => unsealPayload(keys.walletToDappKey, ch, sealedWrong, hdr)).toThrow()
288
+ })
289
+ })
286
290
 
287
291
  // ---------------------------------------------------------------------------
288
292
  // Attack 5: All-zero shared secret (low-order point)
@@ -298,23 +302,23 @@ describe('Crypto Attack: Low-order point rejection', () => {
298
302
  // PREVENTS: Key exchange with degenerate public keys that produce
299
303
  // trivial shared secrets (RFC 7748 Section 6, Protocol Section 6.2).
300
304
 
301
- const myKp = generateX25519KeyPair();
305
+ const myKp = generateX25519KeyPair()
302
306
 
303
307
  // Known low-order point: the all-zero point
304
- const allZeroKey = new Uint8Array(32);
308
+ const allZeroKey = new Uint8Array(32)
305
309
 
306
310
  // The noble library may throw its own error before our all-zero check,
307
311
  // or our code throws "all-zero shared secret". Either way, it must throw.
308
- expect(() => computeSharedSecret(myKp.privateKey, allZeroKey)).toThrow();
309
- });
312
+ expect(() => computeSharedSecret(myKp.privateKey, allZeroKey)).toThrow()
313
+ })
310
314
 
311
315
  it('another low-order point (order 2: point at x=1) is rejected', () => {
312
316
  // The point with x-coordinate = 1 has order 2 on Curve25519.
313
317
  // X25519 with this point should produce all-zero output.
314
318
 
315
- const myKp = generateX25519KeyPair();
316
- const lowOrderPoint = new Uint8Array(32);
317
- lowOrderPoint[0] = 1; // x = 1
319
+ const myKp = generateX25519KeyPair()
320
+ const lowOrderPoint = new Uint8Array(32)
321
+ lowOrderPoint[0] = 1 // x = 1
318
322
 
319
323
  // This should either throw due to all-zero result or be safe
320
324
  // depending on the X25519 implementation. The important thing
@@ -322,37 +326,31 @@ describe('Crypto Attack: Low-order point rejection', () => {
322
326
  // The noble library may reject this at the X25519 level or our
323
327
  // code rejects the all-zero output. Either way, it must throw.
324
328
  try {
325
- computeSharedSecret(myKp.privateKey, lowOrderPoint);
329
+ computeSharedSecret(myKp.privateKey, lowOrderPoint)
326
330
  // If it didn't throw, the implementation clamped and produced
327
331
  // non-zero output — this is also acceptable behavior.
328
332
  } catch {
329
333
  // Expected: low-order point rejected
330
334
  }
331
- });
335
+ })
332
336
 
333
337
  it('remote public key must be exactly 32 bytes', () => {
334
338
  // ATTACK: Attacker sends a malformed public key (wrong length).
335
339
  //
336
340
  // PREVENTS: Buffer overflows or unexpected behavior from wrong-sized keys.
337
341
 
338
- const myKp = generateX25519KeyPair();
342
+ const myKp = generateX25519KeyPair()
339
343
 
340
344
  // Too short
341
- expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(16))).toThrow(
342
- '32 bytes',
343
- );
345
+ expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(16))).toThrow('32 bytes')
344
346
 
345
347
  // Too long
346
- expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(64))).toThrow(
347
- '32 bytes',
348
- );
348
+ expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(64))).toThrow('32 bytes')
349
349
 
350
350
  // Empty
351
- expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(0))).toThrow(
352
- '32 bytes',
353
- );
354
- });
355
- });
351
+ expect(() => computeSharedSecret(myKp.privateKey, new Uint8Array(0))).toThrow('32 bytes')
352
+ })
353
+ })
356
354
 
357
355
  // ---------------------------------------------------------------------------
358
356
  // Attack 6: Modified sealed_join
@@ -366,25 +364,30 @@ describe('Crypto Attack: Sealed join tampering', () => {
366
364
  // PREVENTS: Capability injection — relay cannot grant methods that
367
365
  // the wallet did not authorize.
368
366
 
369
- const dappKp = generateX25519KeyPair();
370
- const walletKp = generateX25519KeyPair();
371
- const ch = generateChannelId();
367
+ const dappKp = generateX25519KeyPair()
368
+ const walletKp = generateX25519KeyPair()
369
+ const ch = generateChannelId()
372
370
 
373
- const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey);
374
- const rootKey = deriveSessionKey(shared, ch);
375
- const joinKey = deriveJoinEncryptionKey(rootKey, ch);
371
+ const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
372
+ const rootKey = deriveSessionKey(shared, ch)
373
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch)
376
374
 
377
- const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] };
378
- const meta = { name: 'W', description: 'W', url: 'https://w.test', icon: 'https://w.test/i.png' };
379
- const sealed = sealJoin(joinKey, ch, caps, meta);
375
+ const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
376
+ const meta = {
377
+ name: 'W',
378
+ description: 'W',
379
+ url: 'https://w.test',
380
+ icon: 'https://w.test/i.png',
381
+ }
382
+ const sealed = sealJoin(joinKey, ch, caps, meta)
380
383
 
381
384
  // Tamper with the ciphertext
382
- const bytes = b64urlDecode(sealed);
383
- const tampered = new Uint8Array(bytes);
384
- tampered[20] = tampered[20]! ^ 0xff; // flip a byte in ciphertext
385
+ const bytes = b64urlDecode(sealed)
386
+ const tampered = new Uint8Array(bytes)
387
+ tampered[20] = (tampered[20] ?? 0) ^ 0xff // flip a byte in ciphertext
385
388
 
386
- expect(() => unsealJoin(joinKey, ch, b64urlEncode(tampered))).toThrow();
387
- });
389
+ expect(() => unsealJoin(joinKey, ch, b64urlEncode(tampered))).toThrow()
390
+ })
388
391
 
389
392
  it('sealed_join with wrong channel ID fails decryption', () => {
390
393
  // ATTACK: Relay forwards a sealed_join from one channel to another.
@@ -392,21 +395,21 @@ describe('Crypto Attack: Sealed join tampering', () => {
392
395
  //
393
396
  // PREVENTS: Cross-channel sealed_join replay.
394
397
 
395
- const dappKp = generateX25519KeyPair();
396
- const walletKp = generateX25519KeyPair();
397
- const ch1 = generateChannelId();
398
- const ch2 = generateChannelId();
398
+ const dappKp = generateX25519KeyPair()
399
+ const walletKp = generateX25519KeyPair()
400
+ const ch1 = generateChannelId()
401
+ const ch2 = generateChannelId()
399
402
 
400
- const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey);
401
- const rootKey = deriveSessionKey(shared, ch1);
402
- const joinKey = deriveJoinEncryptionKey(rootKey, ch1);
403
+ const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
404
+ const rootKey = deriveSessionKey(shared, ch1)
405
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch1)
403
406
 
404
- const caps = { methods: ['wallet_signMessage'], events: [], chains: [] };
405
- const sealed = sealJoin(joinKey, ch1, caps);
407
+ const caps = { methods: ['wallet_signMessage'], events: [], chains: [] }
408
+ const sealed = sealJoin(joinKey, ch1, caps)
406
409
 
407
410
  // Same key but different channel ID → AAD mismatch
408
- expect(() => unsealJoin(joinKey, ch2, sealed)).toThrow();
409
- });
411
+ expect(() => unsealJoin(joinKey, ch2, sealed)).toThrow()
412
+ })
410
413
 
411
414
  it('modified sealed_join produces different transcript hash and traffic keys', () => {
412
415
  // If sealed_join content were somehow different (e.g., different
@@ -416,12 +419,12 @@ describe('Crypto Attack: Sealed join tampering', () => {
416
419
  //
417
420
  // PREVENTS: Capability downgrade attack at the transcript level.
418
421
 
419
- const dappKp = generateX25519KeyPair();
420
- const walletKp = generateX25519KeyPair();
421
- const ch = generateChannelId();
422
+ const dappKp = generateX25519KeyPair()
423
+ const walletKp = generateX25519KeyPair()
424
+ const ch = generateChannelId()
422
425
 
423
- const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey);
424
- const rootKey = deriveSessionKey(shared, ch);
426
+ const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
427
+ const rootKey = deriveSessionKey(shared, ch)
425
428
 
426
429
  // Transcript with capabilities A
427
430
  const ctx1: SessionCryptoContext = {
@@ -429,44 +432,48 @@ describe('Crypto Attack: Sealed join tampering', () => {
429
432
  walletPubKeyB64: walletKp.publicKeyB64,
430
433
  capabilities: { methods: ['wallet_signMessage'], events: [], chains: [] },
431
434
  dappName: 'App',
432
- };
433
- const keys1 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx1);
435
+ }
436
+ const keys1 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx1)
434
437
 
435
438
  // Transcript with capabilities B (relay tried to modify sealed_join)
436
439
  const ctx2: SessionCryptoContext = {
437
440
  dappPubKeyB64: dappKp.publicKeyB64,
438
441
  walletPubKeyB64: walletKp.publicKeyB64,
439
- capabilities: { methods: ['wallet_signMessage', 'wallet_drainFunds'], events: [], chains: [] },
442
+ capabilities: {
443
+ methods: ['wallet_signMessage', 'wallet_drainFunds'],
444
+ events: [],
445
+ chains: [],
446
+ },
440
447
  dappName: 'App',
441
- };
442
- const keys2 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx2);
448
+ }
449
+ const keys2 = deriveDirectionalSessionKeys(new Uint8Array(rootKey), ch, ctx2)
443
450
 
444
451
  // Traffic keys must differ — peers would not be able to communicate
445
- expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey));
446
- expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey));
447
- expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash));
448
- });
452
+ expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey))
453
+ expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey))
454
+ expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash))
455
+ })
449
456
 
450
457
  it('sealed_join with truncated data fails', () => {
451
458
  // ATTACK: Relay truncates the sealed_join envelope.
452
459
 
453
- const dappKp = generateX25519KeyPair();
454
- const walletKp = generateX25519KeyPair();
455
- const ch = generateChannelId();
460
+ const dappKp = generateX25519KeyPair()
461
+ const walletKp = generateX25519KeyPair()
462
+ const ch = generateChannelId()
456
463
 
457
- const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey);
458
- const rootKey = deriveSessionKey(shared, ch);
459
- const joinKey = deriveJoinEncryptionKey(rootKey, ch);
464
+ const shared = computeSharedSecret(walletKp.privateKey, dappKp.publicKey)
465
+ const rootKey = deriveSessionKey(shared, ch)
466
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch)
460
467
 
461
- const caps = { methods: ['test'], events: [], chains: [] };
462
- const sealed = sealJoin(joinKey, ch, caps);
463
- const bytes = b64urlDecode(sealed);
468
+ const caps = { methods: ['test'], events: [], chains: [] }
469
+ const sealed = sealJoin(joinKey, ch, caps)
470
+ const bytes = b64urlDecode(sealed)
464
471
 
465
472
  // Truncate to just nonce (12 bytes) + partial ciphertext
466
- const truncated = b64urlEncode(bytes.slice(0, 20));
467
- expect(() => unsealJoin(joinKey, ch, truncated)).toThrow();
468
- });
469
- });
473
+ const truncated = b64urlEncode(bytes.slice(0, 20))
474
+ expect(() => unsealJoin(joinKey, ch, truncated)).toThrow()
475
+ })
476
+ })
470
477
 
471
478
  // ---------------------------------------------------------------------------
472
479
  // Attack 7: Cross-session key isolation
@@ -481,43 +488,43 @@ describe('Crypto Attack: Cross-session key isolation', () => {
481
488
  //
482
489
  // PREVENTS: Cross-session key reuse.
483
490
 
484
- const dappKp = generateX25519KeyPair();
485
- const walletKp = generateX25519KeyPair();
486
- const ch1 = generateChannelId();
487
- const ch2 = generateChannelId();
491
+ const dappKp = generateX25519KeyPair()
492
+ const walletKp = generateX25519KeyPair()
493
+ const ch1 = generateChannelId()
494
+ const ch2 = generateChannelId()
488
495
 
489
- const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey);
496
+ const shared = computeSharedSecret(dappKp.privateKey, walletKp.publicKey)
490
497
 
491
- const root1 = deriveSessionKey(shared, ch1);
492
- const root2 = deriveSessionKey(shared, ch2);
498
+ const root1 = deriveSessionKey(shared, ch1)
499
+ const root2 = deriveSessionKey(shared, ch2)
493
500
 
494
501
  // Root keys differ (different channel ID salt)
495
- expect(bytesToHex(root1)).not.toBe(bytesToHex(root2));
502
+ expect(bytesToHex(root1)).not.toBe(bytesToHex(root2))
496
503
 
497
504
  const ctx: SessionCryptoContext = {
498
505
  dappPubKeyB64: dappKp.publicKeyB64,
499
506
  walletPubKeyB64: walletKp.publicKeyB64,
500
507
  capabilities: null,
501
508
  dappName: 'App',
502
- };
509
+ }
503
510
 
504
- const keys1 = deriveDirectionalSessionKeys(root1, ch1, ctx);
505
- const keys2 = deriveDirectionalSessionKeys(root2, ch2, ctx);
511
+ const keys1 = deriveDirectionalSessionKeys(root1, ch1, ctx)
512
+ const keys2 = deriveDirectionalSessionKeys(root2, ch2, ctx)
506
513
 
507
514
  // Encrypt with session 1 key
508
- const sealed = sealPayload(keys1.dappToWalletKey, ch1, 0, { secret: 'session1' });
515
+ const sealed = sealPayload(keys1.dappToWalletKey, ch1, 0, { secret: 'session1' })
509
516
 
510
517
  // Cannot decrypt with session 2 key
511
- expect(() => unsealPayload(keys2.dappToWalletKey, ch2, sealed)).toThrow();
518
+ expect(() => unsealPayload(keys2.dappToWalletKey, ch2, sealed)).toThrow()
512
519
 
513
520
  // Cannot decrypt with session 2 key + session 1 channel
514
- expect(() => unsealPayload(keys2.dappToWalletKey, ch1, sealed)).toThrow();
521
+ expect(() => unsealPayload(keys2.dappToWalletKey, ch1, sealed)).toThrow()
515
522
 
516
523
  // Can decrypt with correct key + channel
517
- const { data } = unsealPayload(keys1.dappToWalletKey, ch1, sealed);
518
- expect(data).toEqual({ secret: 'session1' });
519
- });
520
- });
524
+ const { data } = unsealPayload(keys1.dappToWalletKey, ch1, sealed)
525
+ expect(data).toEqual({ secret: 'session1' })
526
+ })
527
+ })
521
528
 
522
529
  // ---------------------------------------------------------------------------
523
530
  // Attack 8: Empty and malformed sealed payloads
@@ -525,33 +532,33 @@ describe('Crypto Attack: Cross-session key isolation', () => {
525
532
 
526
533
  describe('Crypto Attack: Malformed sealed payloads', () => {
527
534
  it('empty string sealed payload throws', () => {
528
- const key = new Uint8Array(32);
529
- crypto.getRandomValues(key);
530
- const ch = generateChannelId();
531
- expect(() => unsealPayload(key, ch, '')).toThrow();
532
- });
535
+ const key = new Uint8Array(32)
536
+ crypto.getRandomValues(key)
537
+ const ch = generateChannelId()
538
+ expect(() => unsealPayload(key, ch, '')).toThrow()
539
+ })
533
540
 
534
541
  it('sealed payload with only seq bytes (no ciphertext) throws', () => {
535
- const key = new Uint8Array(32);
536
- crypto.getRandomValues(key);
537
- const ch = generateChannelId();
538
- const onlySeq = b64urlEncode(new Uint8Array(4));
539
- expect(() => unsealPayload(key, ch, onlySeq)).toThrow();
540
- });
542
+ const key = new Uint8Array(32)
543
+ crypto.getRandomValues(key)
544
+ const ch = generateChannelId()
545
+ const onlySeq = b64urlEncode(new Uint8Array(4))
546
+ expect(() => unsealPayload(key, ch, onlySeq)).toThrow()
547
+ })
541
548
 
542
549
  it('sealed payload with random garbage throws', () => {
543
- const key = new Uint8Array(32);
544
- crypto.getRandomValues(key);
545
- const ch = generateChannelId();
546
- const garbage = b64urlEncode(crypto.getRandomValues(new Uint8Array(100)));
547
- expect(() => unsealPayload(key, ch, garbage)).toThrow();
548
- });
550
+ const key = new Uint8Array(32)
551
+ crypto.getRandomValues(key)
552
+ const ch = generateChannelId()
553
+ const garbage = b64urlEncode(crypto.getRandomValues(new Uint8Array(100)))
554
+ expect(() => unsealPayload(key, ch, garbage)).toThrow()
555
+ })
549
556
 
550
557
  it('sealed_join with envelope smaller than nonce+tag rejects early', () => {
551
- const key = new Uint8Array(32);
552
- crypto.getRandomValues(key);
553
- const ch = generateChannelId();
554
- const tiny = b64urlEncode(new Uint8Array(10)); // < 12 + 16
555
- expect(() => unsealJoin(key, ch, tiny)).toThrow('Invalid sealed_join');
556
- });
557
- });
558
+ const key = new Uint8Array(32)
559
+ crypto.getRandomValues(key)
560
+ const ch = generateChannelId()
561
+ const tiny = b64urlEncode(new Uint8Array(10)) // < 12 + 16
562
+ expect(() => unsealJoin(key, ch, tiny)).toThrow('Invalid sealed_join')
563
+ })
564
+ })