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
@@ -10,27 +10,26 @@
10
10
  * - Protocol spec test vector SHA-256 verification
11
11
  */
12
12
 
13
- import { describe, it, expect } from 'vitest';
13
+ import { describe, expect, it } from 'vitest'
14
+ import type { AadHeader, SessionCryptoContext } from './crypto.js'
14
15
  import {
16
+ b64urlDecode,
17
+ b64urlEncode,
18
+ bytesToHex,
15
19
  canonicalJson,
16
- sealPayload,
17
- unsealPayload,
18
- sealJoin,
19
- unsealJoin,
20
- deriveSessionKey,
21
- deriveJoinEncryptionKey,
22
- deriveDirectionalSessionKeys,
23
20
  computeHandshakeTranscriptHash,
24
21
  computeSharedSecret,
25
- generateX25519KeyPair,
22
+ deriveDirectionalSessionKeys,
23
+ deriveJoinEncryptionKey,
24
+ deriveSessionKey,
26
25
  generateChannelId,
27
- b64urlEncode,
28
- b64urlDecode,
29
- bytesToHex,
30
- hexToBytes,
26
+ generateX25519KeyPair,
27
+ sealJoin,
28
+ sealPayload,
31
29
  sha256Hex,
32
- } from './crypto.js';
33
- import type { AadHeader, SessionCryptoContext } from './crypto.js';
30
+ unsealJoin,
31
+ unsealPayload,
32
+ } from './crypto.js'
34
33
 
35
34
  // ═══════════════════════════════════════════════════════════════════════
36
35
  // canonicalJson — comprehensive edge cases
@@ -39,105 +38,105 @@ import type { AadHeader, SessionCryptoContext } from './crypto.js';
39
38
  describe('canonicalJson — edge cases', () => {
40
39
  // --- Object key sorting ---
41
40
  it('sorts object keys lexicographically', () => {
42
- expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}');
43
- });
41
+ expect(canonicalJson({ z: 1, a: 2, m: 3 })).toBe('{"a":2,"m":3,"z":1}')
42
+ })
44
43
 
45
44
  it('sorts nested object keys recursively', () => {
46
- expect(canonicalJson({ b: { z: 1, a: 2 }, a: 1 })).toBe('{"a":1,"b":{"a":2,"z":1}}');
47
- });
45
+ expect(canonicalJson({ b: { z: 1, a: 2 }, a: 1 })).toBe('{"a":1,"b":{"a":2,"z":1}}')
46
+ })
48
47
 
49
48
  it('sorts deeply nested objects (3+ levels)', () => {
50
- const input = { c: { b: { z: 1, a: 2 }, a: 3 }, a: 0 };
51
- expect(canonicalJson(input)).toBe('{"a":0,"c":{"a":3,"b":{"a":2,"z":1}}}');
52
- });
49
+ const input = { c: { b: { z: 1, a: 2 }, a: 3 }, a: 0 }
50
+ expect(canonicalJson(input)).toBe('{"a":0,"c":{"a":3,"b":{"a":2,"z":1}}}')
51
+ })
53
52
 
54
53
  it('does not sort array elements (preserves order)', () => {
55
- expect(canonicalJson([3, 1, 2])).toBe('[3,1,2]');
56
- expect(canonicalJson(['z', 'a', 'm'])).toBe('["z","a","m"]');
57
- });
54
+ expect(canonicalJson([3, 1, 2])).toBe('[3,1,2]')
55
+ expect(canonicalJson(['z', 'a', 'm'])).toBe('["z","a","m"]')
56
+ })
58
57
 
59
58
  it('sorts keys in objects inside arrays', () => {
60
- expect(canonicalJson([{ b: 1, a: 2 }])).toBe('[{"a":2,"b":1}]');
61
- });
59
+ expect(canonicalJson([{ b: 1, a: 2 }])).toBe('[{"a":2,"b":1}]')
60
+ })
62
61
 
63
62
  // --- Primitives ---
64
63
  it('handles null', () => {
65
- expect(canonicalJson(null)).toBe('null');
66
- });
64
+ expect(canonicalJson(null)).toBe('null')
65
+ })
67
66
 
68
67
  it('handles undefined as null', () => {
69
- expect(canonicalJson(undefined)).toBe('null');
70
- });
68
+ expect(canonicalJson(undefined)).toBe('null')
69
+ })
71
70
 
72
71
  it('handles booleans', () => {
73
- expect(canonicalJson(true)).toBe('true');
74
- expect(canonicalJson(false)).toBe('false');
75
- });
72
+ expect(canonicalJson(true)).toBe('true')
73
+ expect(canonicalJson(false)).toBe('false')
74
+ })
76
75
 
77
76
  it('handles integers', () => {
78
- expect(canonicalJson(0)).toBe('0');
79
- expect(canonicalJson(1)).toBe('1');
80
- expect(canonicalJson(-1)).toBe('-1');
81
- expect(canonicalJson(42)).toBe('42');
82
- });
77
+ expect(canonicalJson(0)).toBe('0')
78
+ expect(canonicalJson(1)).toBe('1')
79
+ expect(canonicalJson(-1)).toBe('-1')
80
+ expect(canonicalJson(42)).toBe('42')
81
+ })
83
82
 
84
83
  it('handles floating point numbers', () => {
85
- expect(canonicalJson(1.5)).toBe('1.5');
86
- expect(canonicalJson(0.1)).toBe('0.1');
87
- });
84
+ expect(canonicalJson(1.5)).toBe('1.5')
85
+ expect(canonicalJson(0.1)).toBe('0.1')
86
+ })
88
87
 
89
88
  it('handles negative zero as 0', () => {
90
89
  // JSON.stringify(-0) produces "0" in JS
91
- expect(canonicalJson(-0)).toBe('0');
92
- });
90
+ expect(canonicalJson(-0)).toBe('0')
91
+ })
93
92
 
94
93
  // --- Strings ---
95
94
  it('handles empty string', () => {
96
- expect(canonicalJson('')).toBe('""');
97
- });
95
+ expect(canonicalJson('')).toBe('""')
96
+ })
98
97
 
99
98
  it('handles unicode strings', () => {
100
- expect(canonicalJson('你好')).toBe('"你好"');
101
- expect(canonicalJson('🌍')).toBe('"🌍"');
102
- });
99
+ expect(canonicalJson('你好')).toBe('"你好"')
100
+ expect(canonicalJson('🌍')).toBe('"🌍"')
101
+ })
103
102
 
104
103
  it('escapes control characters', () => {
105
- expect(canonicalJson('\n')).toBe('"\\n"');
106
- expect(canonicalJson('\t')).toBe('"\\t"');
107
- expect(canonicalJson('\r')).toBe('"\\r"');
108
- });
104
+ expect(canonicalJson('\n')).toBe('"\\n"')
105
+ expect(canonicalJson('\t')).toBe('"\\t"')
106
+ expect(canonicalJson('\r')).toBe('"\\r"')
107
+ })
109
108
 
110
109
  it('escapes backslash and double quote', () => {
111
- expect(canonicalJson('\\')).toBe('"\\\\"');
112
- expect(canonicalJson('"')).toBe('"\\""');
113
- });
110
+ expect(canonicalJson('\\')).toBe('"\\\\"')
111
+ expect(canonicalJson('"')).toBe('"\\""')
112
+ })
114
113
 
115
114
  it('does not escape forward slash', () => {
116
- expect(canonicalJson('a/b')).toBe('"a/b"');
117
- });
115
+ expect(canonicalJson('a/b')).toBe('"a/b"')
116
+ })
118
117
 
119
118
  // --- Empty containers ---
120
119
  it('handles empty object', () => {
121
- expect(canonicalJson({})).toBe('{}');
122
- });
120
+ expect(canonicalJson({})).toBe('{}')
121
+ })
123
122
 
124
123
  it('handles empty array', () => {
125
- expect(canonicalJson([])).toBe('[]');
126
- });
124
+ expect(canonicalJson([])).toBe('[]')
125
+ })
127
126
 
128
127
  // --- No whitespace ---
129
128
  it('produces no whitespace', () => {
130
- const result = canonicalJson({ a: [1, 2], b: { c: 3 } });
131
- expect(result).not.toMatch(/\s/);
132
- expect(result).toBe('{"a":[1,2],"b":{"c":3}}');
133
- });
129
+ const result = canonicalJson({ a: [1, 2], b: { c: 3 } })
130
+ expect(result).not.toMatch(/\s/)
131
+ expect(result).toBe('{"a":[1,2],"b":{"c":3}}')
132
+ })
134
133
 
135
134
  // --- Omits undefined values in objects ---
136
135
  it('omits keys with undefined values (matches JSON.stringify)', () => {
137
- const input = { a: 1, b: undefined, c: 3 };
138
- const result = canonicalJson(input);
139
- expect(result).toBe('{"a":1,"c":3}');
140
- });
136
+ const input = { a: 1, b: undefined, c: 3 }
137
+ const result = canonicalJson(input)
138
+ expect(result).toBe('{"a":1,"c":3}')
139
+ })
141
140
 
142
141
  // --- Spec test vector with SHA-256 ---
143
142
  it('matches protocol spec test vector byte-for-byte with correct SHA-256', () => {
@@ -145,26 +144,26 @@ describe('canonicalJson — edge cases', () => {
145
144
  methods: ['wallet_signTransaction', 'wallet_signMessage'],
146
145
  events: ['accountsChanged', 'chainChanged'],
147
146
  chains: ['eip155:1', 'eip155:137'],
148
- };
149
- const output = canonicalJson(input);
147
+ }
148
+ const output = canonicalJson(input)
150
149
  const expected =
151
- '{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}';
152
- expect(output).toBe(expected);
150
+ '{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]}'
151
+ expect(output).toBe(expected)
153
152
 
154
153
  // Verify SHA-256 from protocol spec
155
- const hash = sha256Hex(new TextEncoder().encode(output));
156
- expect(hash).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333');
157
- });
154
+ const hash = sha256Hex(new TextEncoder().encode(output))
155
+ expect(hash).toBe('4da366e2aae26b47b3d90fff52410752348733350ce2525dce7d64510f571333')
156
+ })
158
157
 
159
158
  // --- Determinism ---
160
159
  it('is deterministic across multiple calls', () => {
161
- const obj = { z: [3, 1], a: { y: 'hello', x: true } };
162
- const r1 = canonicalJson(obj);
163
- const r2 = canonicalJson(obj);
164
- const r3 = canonicalJson(JSON.parse(JSON.stringify(obj)));
165
- expect(r1).toBe(r2);
166
- expect(r1).toBe(r3);
167
- });
160
+ const obj = { z: [3, 1], a: { y: 'hello', x: true } }
161
+ const r1 = canonicalJson(obj)
162
+ const r2 = canonicalJson(obj)
163
+ const r3 = canonicalJson(JSON.parse(JSON.stringify(obj)))
164
+ expect(r1).toBe(r2)
165
+ expect(r1).toBe(r3)
166
+ })
168
167
 
169
168
  // --- Complex mixed structures ---
170
169
  it('handles mixed nested structures', () => {
@@ -175,13 +174,13 @@ describe('canonicalJson — edge cases', () => {
175
174
  chains: ['eip155:1'],
176
175
  },
177
176
  meta: { name: 'MyWallet' },
178
- };
179
- const result = canonicalJson(input);
177
+ }
178
+ const result = canonicalJson(input)
180
179
  // Keys sorted: capabilities < meta; inner keys sorted too
181
180
  expect(result).toBe(
182
181
  '{"capabilities":{"chains":["eip155:1"],"events":["accountsChanged"],"methods":["wallet_signTransaction"]},"meta":{"name":"MyWallet"}}',
183
- );
184
- });
182
+ )
183
+ })
185
184
 
186
185
  // --- Matches test vector for join plaintext ---
187
186
  it('matches join plaintext test vector from protocol appendix', () => {
@@ -192,223 +191,223 @@ describe('canonicalJson — edge cases', () => {
192
191
  chains: ['eip155:1', 'eip155:137'],
193
192
  },
194
193
  meta: { name: 'MyWallet' },
195
- };
196
- const result = canonicalJson(input);
194
+ }
195
+ const result = canonicalJson(input)
197
196
  expect(result).toBe(
198
197
  '{"capabilities":{"chains":["eip155:1","eip155:137"],"events":["accountsChanged","chainChanged"],"methods":["wallet_signTransaction","wallet_signMessage"]},"meta":{"name":"MyWallet"}}',
199
- );
200
- });
201
- });
198
+ )
199
+ })
200
+ })
202
201
 
203
202
  // ═══════════════════════════════════════════════════════════════════════
204
203
  // Nonce determinism and uniqueness
205
204
  // ═══════════════════════════════════════════════════════════════════════
206
205
 
207
206
  describe('sealPayload nonce determinism', () => {
208
- const key = new Uint8Array(32).fill(0xaa);
209
- const ch = 'bb'.repeat(32);
207
+ const key = new Uint8Array(32).fill(0xaa)
208
+ const ch = 'bb'.repeat(32)
210
209
 
211
210
  it('same key + same seq + same data = same ciphertext (deterministic nonce)', () => {
212
- const data = { test: true };
213
- const s1 = sealPayload(key, ch, 0, data);
214
- const s2 = sealPayload(key, ch, 0, data);
215
- expect(s1).toBe(s2);
216
- });
211
+ const data = { test: true }
212
+ const s1 = sealPayload(key, ch, 0, data)
213
+ const s2 = sealPayload(key, ch, 0, data)
214
+ expect(s1).toBe(s2)
215
+ })
217
216
 
218
217
  it('same key + different seq = different ciphertext (different nonce)', () => {
219
- const data = { test: true };
220
- const s0 = sealPayload(key, ch, 0, data);
221
- const s1 = sealPayload(key, ch, 1, data);
222
- expect(s0).not.toBe(s1);
223
- });
218
+ const data = { test: true }
219
+ const s0 = sealPayload(key, ch, 0, data)
220
+ const s1 = sealPayload(key, ch, 1, data)
221
+ expect(s0).not.toBe(s1)
222
+ })
224
223
 
225
224
  it('different key + same seq = different ciphertext (different nonce derivation)', () => {
226
- const key2 = new Uint8Array(32).fill(0xcc);
227
- const data = { test: true };
228
- const s1 = sealPayload(key, ch, 0, data);
229
- const s2 = sealPayload(key2, ch, 0, data);
230
- expect(s1).not.toBe(s2);
231
- });
225
+ const key2 = new Uint8Array(32).fill(0xcc)
226
+ const data = { test: true }
227
+ const s1 = sealPayload(key, ch, 0, data)
228
+ const s2 = sealPayload(key2, ch, 0, data)
229
+ expect(s1).not.toBe(s2)
230
+ })
232
231
 
233
232
  it('seq=0 and seq=2^31-1 both work', () => {
234
- const data = { x: 1 };
235
- const s0 = sealPayload(key, ch, 0, data);
236
- const sMax = sealPayload(key, ch, 2 ** 31 - 1, data);
237
- expect(unsealPayload(key, ch, s0).seq).toBe(0);
238
- expect(unsealPayload(key, ch, sMax).seq).toBe(2 ** 31 - 1);
239
- });
240
- });
233
+ const data = { x: 1 }
234
+ const s0 = sealPayload(key, ch, 0, data)
235
+ const sMax = sealPayload(key, ch, 2 ** 31 - 1, data)
236
+ expect(unsealPayload(key, ch, s0).seq).toBe(0)
237
+ expect(unsealPayload(key, ch, sMax).seq).toBe(2 ** 31 - 1)
238
+ })
239
+ })
241
240
 
242
241
  // ═══════════════════════════════════════════════════════════════════════
243
242
  // AAD header type differentiation
244
243
  // ═══════════════════════════════════════════════════════════════════════
245
244
 
246
245
  describe('seal/unseal with AAD headers', () => {
247
- const key = new Uint8Array(32).fill(0x55);
248
- const ch = 'cc'.repeat(32);
249
- const data = { method: 'eth_sign', params: [] };
246
+ const key = new Uint8Array(32).fill(0x55)
247
+ const ch = 'cc'.repeat(32)
248
+ const data = { method: 'eth_sign', params: [] }
250
249
 
251
250
  it('req type AAD produces different ciphertext than res type', () => {
252
- const reqHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' };
253
- const resHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' };
254
- const sealed1 = sealPayload(key, ch, 0, data, reqHdr);
255
- const sealed2 = sealPayload(key, ch, 0, data, resHdr);
256
- expect(sealed1).not.toBe(sealed2);
257
- });
251
+ const reqHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
252
+ const resHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
253
+ const sealed1 = sealPayload(key, ch, 0, data, reqHdr)
254
+ const sealed2 = sealPayload(key, ch, 0, data, resHdr)
255
+ expect(sealed1).not.toBe(sealed2)
256
+ })
258
257
 
259
258
  it('different from in AAD produces different ciphertext', () => {
260
- const hdr1: AadHeader = { type: 'req', from: 'peerA', id: 'r1' };
261
- const hdr2: AadHeader = { type: 'req', from: 'peerB', id: 'r1' };
262
- const sealed1 = sealPayload(key, ch, 0, data, hdr1);
263
- const sealed2 = sealPayload(key, ch, 0, data, hdr2);
264
- expect(sealed1).not.toBe(sealed2);
265
- });
259
+ const hdr1: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
260
+ const hdr2: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
261
+ const sealed1 = sealPayload(key, ch, 0, data, hdr1)
262
+ const sealed2 = sealPayload(key, ch, 0, data, hdr2)
263
+ expect(sealed1).not.toBe(sealed2)
264
+ })
266
265
 
267
266
  it('different id in AAD produces different ciphertext', () => {
268
- const hdr1: AadHeader = { type: 'req', from: 'peer1', id: 'id-1' };
269
- const hdr2: AadHeader = { type: 'req', from: 'peer1', id: 'id-2' };
270
- const sealed1 = sealPayload(key, ch, 0, data, hdr1);
271
- const sealed2 = sealPayload(key, ch, 0, data, hdr2);
272
- expect(sealed1).not.toBe(sealed2);
273
- });
267
+ const hdr1: AadHeader = { type: 'req', from: 'peer1', id: 'id-1' }
268
+ const hdr2: AadHeader = { type: 'req', from: 'peer1', id: 'id-2' }
269
+ const sealed1 = sealPayload(key, ch, 0, data, hdr1)
270
+ const sealed2 = sealPayload(key, ch, 0, data, hdr2)
271
+ expect(sealed1).not.toBe(sealed2)
272
+ })
274
273
 
275
274
  it('decrypt fails if AAD header type mismatches', () => {
276
- const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' };
277
- const sealed = sealPayload(key, ch, 0, data, hdr);
278
- const wrongHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' };
279
- expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow();
280
- });
275
+ const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
276
+ const sealed = sealPayload(key, ch, 0, data, hdr)
277
+ const wrongHdr: AadHeader = { type: 'res', from: 'peer1', id: 'r1' }
278
+ expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
279
+ })
281
280
 
282
281
  it('decrypt fails if AAD from field mismatches', () => {
283
- const hdr: AadHeader = { type: 'req', from: 'peerA', id: 'r1' };
284
- const sealed = sealPayload(key, ch, 0, data, hdr);
285
- const wrongHdr: AadHeader = { type: 'req', from: 'peerB', id: 'r1' };
286
- expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow();
287
- });
282
+ const hdr: AadHeader = { type: 'req', from: 'peerA', id: 'r1' }
283
+ const sealed = sealPayload(key, ch, 0, data, hdr)
284
+ const wrongHdr: AadHeader = { type: 'req', from: 'peerB', id: 'r1' }
285
+ expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
286
+ })
288
287
 
289
288
  it('decrypt fails if AAD id field mismatches', () => {
290
- const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' };
291
- const sealed = sealPayload(key, ch, 0, data, hdr);
292
- const wrongHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r2' };
293
- expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow();
294
- });
289
+ const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
290
+ const sealed = sealPayload(key, ch, 0, data, hdr)
291
+ const wrongHdr: AadHeader = { type: 'req', from: 'peer1', id: 'r2' }
292
+ expect(() => unsealPayload(key, ch, sealed, wrongHdr)).toThrow()
293
+ })
295
294
 
296
295
  it('decrypt succeeds with matching AAD header', () => {
297
- const hdr: AadHeader = { type: 'evt', from: 'wallet', id: 'e1' };
298
- const sealed = sealPayload(key, ch, 5, data, hdr);
299
- const { seq, data: d } = unsealPayload(key, ch, sealed, hdr);
300
- expect(seq).toBe(5);
301
- expect(d).toEqual(data);
302
- });
296
+ const hdr: AadHeader = { type: 'evt', from: 'wallet', id: 'e1' }
297
+ const sealed = sealPayload(key, ch, 5, data, hdr)
298
+ const { seq, data: d } = unsealPayload(key, ch, sealed, hdr)
299
+ expect(seq).toBe(5)
300
+ expect(d).toEqual(data)
301
+ })
303
302
 
304
303
  it('sealed with AAD cannot be decrypted without AAD', () => {
305
- const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' };
306
- const sealed = sealPayload(key, ch, 0, data, hdr);
304
+ const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
305
+ const sealed = sealPayload(key, ch, 0, data, hdr)
307
306
  // Decrypt without AAD — should fail because AAD mismatch
308
- expect(() => unsealPayload(key, ch, sealed)).toThrow();
309
- });
307
+ expect(() => unsealPayload(key, ch, sealed)).toThrow()
308
+ })
310
309
 
311
310
  it('sealed without AAD cannot be decrypted with AAD', () => {
312
- const sealed = sealPayload(key, ch, 0, data);
313
- const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' };
314
- expect(() => unsealPayload(key, ch, sealed, hdr)).toThrow();
315
- });
316
- });
311
+ const sealed = sealPayload(key, ch, 0, data)
312
+ const hdr: AadHeader = { type: 'req', from: 'peer1', id: 'r1' }
313
+ expect(() => unsealPayload(key, ch, sealed, hdr)).toThrow()
314
+ })
315
+ })
317
316
 
318
317
  // ═══════════════════════════════════════════════════════════════════════
319
318
  // unsealPayload error paths
320
319
  // ═══════════════════════════════════════════════════════════════════════
321
320
 
322
321
  describe('unsealPayload error paths', () => {
323
- const key = new Uint8Array(32).fill(0x77);
324
- const ch = 'dd'.repeat(32);
322
+ const key = new Uint8Array(32).fill(0x77)
323
+ const ch = 'dd'.repeat(32)
325
324
 
326
325
  it('throws on empty sealed string', () => {
327
- expect(() => unsealPayload(key, ch, '')).toThrow();
328
- });
326
+ expect(() => unsealPayload(key, ch, '')).toThrow()
327
+ })
329
328
 
330
329
  it('throws on truncated sealed (too short for seq + tag)', () => {
331
- const truncated = b64urlEncode(new Uint8Array(4)); // only seq, no ciphertext
332
- expect(() => unsealPayload(key, ch, truncated)).toThrow();
333
- });
330
+ const truncated = b64urlEncode(new Uint8Array(4)) // only seq, no ciphertext
331
+ expect(() => unsealPayload(key, ch, truncated)).toThrow()
332
+ })
334
333
 
335
334
  it('throws on tampered seq bytes', () => {
336
- const sealed = sealPayload(key, ch, 5, { test: true });
337
- const bytes = b64urlDecode(sealed);
338
- bytes[0]! ^= 0xff; // tamper seq byte
339
- const tampered = b64urlEncode(bytes);
335
+ const sealed = sealPayload(key, ch, 5, { test: true })
336
+ const bytes = b64urlDecode(sealed)
337
+ bytes[0] = (bytes[0] ?? 0) ^ 0xff // tamper seq byte
338
+ const tampered = b64urlEncode(bytes)
340
339
  // Changing seq changes nonce → decryption fails
341
- expect(() => unsealPayload(key, ch, tampered)).toThrow();
342
- });
340
+ expect(() => unsealPayload(key, ch, tampered)).toThrow()
341
+ })
343
342
 
344
343
  it('throws on single-bit flip in ciphertext', () => {
345
- const sealed = sealPayload(key, ch, 0, { x: 1 });
346
- const bytes = b64urlDecode(sealed);
347
- bytes[bytes.length - 1]! ^= 0x01; // flip 1 bit in tag
348
- expect(() => unsealPayload(key, ch, b64urlEncode(bytes))).toThrow();
349
- });
350
- });
344
+ const sealed = sealPayload(key, ch, 0, { x: 1 })
345
+ const bytes = b64urlDecode(sealed)
346
+ bytes[bytes.length - 1] = (bytes[bytes.length - 1] ?? 0) ^ 0x01 // flip 1 bit in tag
347
+ expect(() => unsealPayload(key, ch, b64urlEncode(bytes))).toThrow()
348
+ })
349
+ })
351
350
 
352
351
  // ═══════════════════════════════════════════════════════════════════════
353
352
  // sealJoin / unsealJoin error paths
354
353
  // ═══════════════════════════════════════════════════════════════════════
355
354
 
356
355
  describe('sealJoin / unsealJoin error paths', () => {
357
- const rootKey = new Uint8Array(32).fill(0x33);
358
- const ch = 'ee'.repeat(32);
359
- const joinKey = deriveJoinEncryptionKey(rootKey, ch);
356
+ const rootKey = new Uint8Array(32).fill(0x33)
357
+ const ch = 'ee'.repeat(32)
358
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch)
360
359
 
361
360
  it('decrypt fails with wrong key', () => {
362
- const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] };
363
- const sealed = sealJoin(joinKey, ch, caps, { name: 'W' });
364
- const wrongKey = new Uint8Array(32).fill(0x99);
365
- expect(() => unsealJoin(wrongKey, ch, sealed)).toThrow();
366
- });
361
+ const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
362
+ const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
363
+ const wrongKey = new Uint8Array(32).fill(0x99)
364
+ expect(() => unsealJoin(wrongKey, ch, sealed)).toThrow()
365
+ })
367
366
 
368
367
  it('decrypt fails with wrong channel ID', () => {
369
- const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] };
370
- const sealed = sealJoin(joinKey, ch, caps, { name: 'W' });
371
- const wrongCh = 'ff'.repeat(32);
372
- expect(() => unsealJoin(joinKey, wrongCh, sealed)).toThrow();
373
- });
368
+ const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
369
+ const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
370
+ const wrongCh = 'ff'.repeat(32)
371
+ expect(() => unsealJoin(joinKey, wrongCh, sealed)).toThrow()
372
+ })
374
373
 
375
374
  it('decrypt fails with tampered ciphertext', () => {
376
- const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] };
377
- const sealed = sealJoin(joinKey, ch, caps, { name: 'W' });
378
- const bytes = b64urlDecode(sealed);
379
- bytes[bytes.length - 1]! ^= 0xff;
380
- expect(() => unsealJoin(joinKey, ch, b64urlEncode(bytes))).toThrow();
381
- });
375
+ const caps = { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] }
376
+ const sealed = sealJoin(joinKey, ch, caps, { name: 'W' })
377
+ const bytes = b64urlDecode(sealed)
378
+ bytes[bytes.length - 1] = (bytes[bytes.length - 1] ?? 0) ^ 0xff
379
+ expect(() => unsealJoin(joinKey, ch, b64urlEncode(bytes))).toThrow()
380
+ })
382
381
 
383
382
  it('throws on envelope smaller than nonce + tag', () => {
384
- const tiny = b64urlEncode(new Uint8Array(10));
385
- expect(() => unsealJoin(joinKey, ch, tiny)).toThrow('Invalid sealed_join');
386
- });
383
+ const tiny = b64urlEncode(new Uint8Array(10))
384
+ expect(() => unsealJoin(joinKey, ch, tiny)).toThrow('Invalid sealed_join')
385
+ })
387
386
 
388
387
  it('round-trips with null meta', () => {
389
- const caps = { methods: ['test'], events: [], chains: [] };
390
- const sealed = sealJoin(joinKey, ch, caps);
391
- const { capabilities, meta } = unsealJoin(joinKey, ch, sealed);
392
- expect(capabilities).toEqual(caps);
393
- expect(meta).toBeNull();
394
- });
395
- });
388
+ const caps = { methods: ['test'], events: [], chains: [] }
389
+ const sealed = sealJoin(joinKey, ch, caps)
390
+ const { capabilities, meta } = unsealJoin(joinKey, ch, sealed)
391
+ expect(capabilities).toEqual(caps)
392
+ expect(meta).toBeNull()
393
+ })
394
+ })
396
395
 
397
396
  // ═══════════════════════════════════════════════════════════════════════
398
397
  // Key separation guarantees
399
398
  // ═══════════════════════════════════════════════════════════════════════
400
399
 
401
400
  describe('Key separation', () => {
402
- const alice = generateX25519KeyPair();
403
- const bob = generateX25519KeyPair();
404
- const ch = generateChannelId();
405
- const shared = computeSharedSecret(alice.privateKey, bob.publicKey);
406
- const rootKey = deriveSessionKey(shared, ch);
401
+ const alice = generateX25519KeyPair()
402
+ const bob = generateX25519KeyPair()
403
+ const ch = generateChannelId()
404
+ const shared = computeSharedSecret(alice.privateKey, bob.publicKey)
405
+ const rootKey = deriveSessionKey(shared, ch)
407
406
 
408
407
  it('joinEncryptionKey differs from rootKey', () => {
409
- const joinKey = deriveJoinEncryptionKey(rootKey, ch);
410
- expect(bytesToHex(joinKey)).not.toBe(bytesToHex(rootKey));
411
- });
408
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch)
409
+ expect(bytesToHex(joinKey)).not.toBe(bytesToHex(rootKey))
410
+ })
412
411
 
413
412
  it('dappToWalletKey differs from walletToDappKey', () => {
414
413
  const ctx: SessionCryptoContext = {
@@ -416,23 +415,23 @@ describe('Key separation', () => {
416
415
  walletPubKeyB64: bob.publicKeyB64,
417
416
  capabilities: { methods: ['test'], events: [], chains: [] },
418
417
  dappName: 'Test',
419
- };
420
- const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx);
421
- expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey));
422
- });
418
+ }
419
+ const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
420
+ expect(bytesToHex(keys.dappToWalletKey)).not.toBe(bytesToHex(keys.walletToDappKey))
421
+ })
423
422
 
424
423
  it('joinEncryptionKey differs from both directional keys', () => {
425
- const joinKey = deriveJoinEncryptionKey(rootKey, ch);
424
+ const joinKey = deriveJoinEncryptionKey(rootKey, ch)
426
425
  const ctx: SessionCryptoContext = {
427
426
  dappPubKeyB64: alice.publicKeyB64,
428
427
  walletPubKeyB64: bob.publicKeyB64,
429
428
  capabilities: { methods: ['test'], events: [], chains: [] },
430
429
  dappName: 'Test',
431
- };
432
- const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx);
433
- expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.dappToWalletKey));
434
- expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.walletToDappKey));
435
- });
430
+ }
431
+ const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
432
+ expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.dappToWalletKey))
433
+ expect(bytesToHex(joinKey)).not.toBe(bytesToHex(keys.walletToDappKey))
434
+ })
436
435
 
437
436
  it('different capabilities produce different directional keys', () => {
438
437
  const ctx1: SessionCryptoContext = {
@@ -440,31 +439,35 @@ describe('Key separation', () => {
440
439
  walletPubKeyB64: bob.publicKeyB64,
441
440
  capabilities: { methods: ['wallet_signMessage'], events: [], chains: [] },
442
441
  dappName: 'Test',
443
- };
442
+ }
444
443
  const ctx2: SessionCryptoContext = {
445
444
  dappPubKeyB64: alice.publicKeyB64,
446
445
  walletPubKeyB64: bob.publicKeyB64,
447
446
  capabilities: { methods: ['wallet_sendTransaction'], events: [], chains: [] },
448
447
  dappName: 'Test',
449
- };
450
- const k1 = deriveDirectionalSessionKeys(rootKey, ch, ctx1);
451
- const k2 = deriveDirectionalSessionKeys(rootKey, ch, ctx2);
452
- expect(bytesToHex(k1.dappToWalletKey)).not.toBe(bytesToHex(k2.dappToWalletKey));
453
- });
448
+ }
449
+ const k1 = deriveDirectionalSessionKeys(rootKey, ch, ctx1)
450
+ const k2 = deriveDirectionalSessionKeys(rootKey, ch, ctx2)
451
+ expect(bytesToHex(k1.dappToWalletKey)).not.toBe(bytesToHex(k2.dappToWalletKey))
452
+ })
454
453
 
455
454
  it('different dappName produces different transcript hash', () => {
456
455
  const ctx1: SessionCryptoContext = {
457
- dappPubKeyB64: 'pub1', walletPubKeyB64: 'pub2',
458
- capabilities: null, dappName: 'AppA',
459
- };
456
+ dappPubKeyB64: 'pub1',
457
+ walletPubKeyB64: 'pub2',
458
+ capabilities: null,
459
+ dappName: 'AppA',
460
+ }
460
461
  const ctx2: SessionCryptoContext = {
461
- dappPubKeyB64: 'pub1', walletPubKeyB64: 'pub2',
462
- capabilities: null, dappName: 'AppB',
463
- };
464
- const h1 = computeHandshakeTranscriptHash(ch, ctx1);
465
- const h2 = computeHandshakeTranscriptHash(ch, ctx2);
466
- expect(bytesToHex(h1)).not.toBe(bytesToHex(h2));
467
- });
462
+ dappPubKeyB64: 'pub1',
463
+ walletPubKeyB64: 'pub2',
464
+ capabilities: null,
465
+ dappName: 'AppB',
466
+ }
467
+ const h1 = computeHandshakeTranscriptHash(ch, ctx1)
468
+ const h2 = computeHandshakeTranscriptHash(ch, ctx2)
469
+ expect(bytesToHex(h1)).not.toBe(bytesToHex(h2))
470
+ })
468
471
 
469
472
  it('cross-direction decryption fails', () => {
470
473
  const ctx: SessionCryptoContext = {
@@ -472,16 +475,16 @@ describe('Key separation', () => {
472
475
  walletPubKeyB64: bob.publicKeyB64,
473
476
  capabilities: null,
474
477
  dappName: 'Test',
475
- };
476
- const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx);
478
+ }
479
+ const keys = deriveDirectionalSessionKeys(rootKey, ch, ctx)
477
480
  // Encrypt with dapp→wallet key
478
- const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { msg: 'hello' });
481
+ const sealed = sealPayload(keys.dappToWalletKey, ch, 0, { msg: 'hello' })
479
482
  // Decrypt with wallet→dapp key should fail
480
- expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow();
483
+ expect(() => unsealPayload(keys.walletToDappKey, ch, sealed)).toThrow()
481
484
  // Decrypt with correct key should succeed
482
- expect(unsealPayload(keys.dappToWalletKey, ch, sealed).data).toEqual({ msg: 'hello' });
483
- });
484
- });
485
+ expect(unsealPayload(keys.dappToWalletKey, ch, sealed).data).toEqual({ msg: 'hello' })
486
+ })
487
+ })
485
488
 
486
489
  // ═══════════════════════════════════════════════════════════════════════
487
490
  // Transcript hash determinism
@@ -489,41 +492,47 @@ describe('Key separation', () => {
489
492
 
490
493
  describe('transcriptHash determinism', () => {
491
494
  it('same inputs always produce same transcript hash', () => {
492
- const ch = 'aa'.repeat(32);
495
+ const ch = 'aa'.repeat(32)
493
496
  const ctx: SessionCryptoContext = {
494
497
  dappPubKeyB64: 'dappPub',
495
498
  walletPubKeyB64: 'walletPub',
496
499
  capabilities: { methods: ['test'], events: [], chains: ['eip155:1'] },
497
500
  walletMeta: { name: 'W' },
498
501
  dappName: 'D',
499
- };
500
- const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx));
501
- const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx));
502
- expect(h1).toBe(h2);
503
- });
502
+ }
503
+ const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
504
+ const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx))
505
+ expect(h1).toBe(h2)
506
+ })
504
507
 
505
508
  it('different channel ID produces different hash', () => {
506
509
  const ctx: SessionCryptoContext = {
507
- dappPubKeyB64: 'pub1', walletPubKeyB64: 'pub2',
508
- capabilities: null, dappName: 'X',
509
- };
510
- const h1 = bytesToHex(computeHandshakeTranscriptHash('aa'.repeat(32), ctx));
511
- const h2 = bytesToHex(computeHandshakeTranscriptHash('bb'.repeat(32), ctx));
512
- expect(h1).not.toBe(h2);
513
- });
510
+ dappPubKeyB64: 'pub1',
511
+ walletPubKeyB64: 'pub2',
512
+ capabilities: null,
513
+ dappName: 'X',
514
+ }
515
+ const h1 = bytesToHex(computeHandshakeTranscriptHash('aa'.repeat(32), ctx))
516
+ const h2 = bytesToHex(computeHandshakeTranscriptHash('bb'.repeat(32), ctx))
517
+ expect(h1).not.toBe(h2)
518
+ })
514
519
 
515
520
  it('swapped pub keys produce different hash', () => {
516
- const ch = 'cc'.repeat(32);
521
+ const ch = 'cc'.repeat(32)
517
522
  const ctx1: SessionCryptoContext = {
518
- dappPubKeyB64: 'keyA', walletPubKeyB64: 'keyB',
519
- capabilities: null, dappName: 'X',
520
- };
523
+ dappPubKeyB64: 'keyA',
524
+ walletPubKeyB64: 'keyB',
525
+ capabilities: null,
526
+ dappName: 'X',
527
+ }
521
528
  const ctx2: SessionCryptoContext = {
522
- dappPubKeyB64: 'keyB', walletPubKeyB64: 'keyA',
523
- capabilities: null, dappName: 'X',
524
- };
525
- const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx1));
526
- const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx2));
527
- expect(h1).not.toBe(h2);
528
- });
529
- });
529
+ dappPubKeyB64: 'keyB',
530
+ walletPubKeyB64: 'keyA',
531
+ capabilities: null,
532
+ dappName: 'X',
533
+ }
534
+ const h1 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx1))
535
+ const h2 = bytesToHex(computeHandshakeTranscriptHash(ch, ctx2))
536
+ expect(h1).not.toBe(h2)
537
+ })
538
+ })