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
@@ -6,28 +6,28 @@
6
6
  * across sessions.
7
7
  */
8
8
 
9
- import { describe, it, expect, vi } from 'vitest';
10
- import { DAppSession } from './dapp-session.js';
11
- import { WalletSession } from './wallet-session.js';
12
- import { makeJoinBody, MockTransport, MockRelay } from './test-helpers.js';
9
+ import { describe, expect, it, vi } from 'vitest'
10
+ import type { SessionCryptoContext } from './crypto.js'
13
11
  import {
14
- generateX25519KeyPair,
15
- generateChannelId,
12
+ b64urlDecode,
13
+ b64urlEncode,
14
+ buildPairingUri,
15
+ bytesToHex,
16
16
  computeSharedSecret,
17
+ deriveDirectionalSessionKeys,
17
18
  deriveSessionKey,
19
+ generateChannelId,
20
+ generateX25519KeyPair,
18
21
  sealPayload,
19
22
  unsealPayload,
20
- b64urlEncode,
21
- b64urlDecode,
22
- bytesToHex,
23
- buildPairingUri,
24
- } from './crypto.js';
25
- import { deriveDirectionalSessionKeys } from './crypto.js';
26
- import type { SessionCryptoContext } from './crypto.js';
27
- import type { ProtocolMessage } from './types.js';
23
+ } from './crypto.js'
24
+ import { DAppSession } from './dapp-session.js'
25
+ import { MockTransport, makeJoinBody } from './test-helpers.js'
26
+ import type { ProtocolMessage } from './types.js'
27
+ import { WalletSession } from './wallet-session.js'
28
28
 
29
29
  function wait(ms = 50): Promise<void> {
30
- return new Promise((r) => setTimeout(r, ms));
30
+ return new Promise((r) => setTimeout(r, ms))
31
31
  }
32
32
 
33
33
  // ---------------------------------------------------------------------------
@@ -35,49 +35,68 @@ function wait(ms = 50): Promise<void> {
35
35
  // ---------------------------------------------------------------------------
36
36
 
37
37
  function setupDAppManual() {
38
- const transport = new MockTransport();
39
- const session = new DAppSession({ transport, meta: { name: 'Test', description: 'Test dApp', url: 'https://test.com', icon: 'https://test.com/icon.png' } });
40
- const walletKp = generateX25519KeyPair();
41
- return { transport, session, walletKp };
38
+ const transport = new MockTransport()
39
+ const session = new DAppSession({
40
+ transport,
41
+ meta: {
42
+ name: 'Test',
43
+ description: 'Test dApp',
44
+ url: 'https://test.com',
45
+ icon: 'https://test.com/icon.png',
46
+ },
47
+ })
48
+ const walletKp = generateX25519KeyPair()
49
+ return { transport, session, walletKp }
42
50
  }
43
51
 
44
52
  async function connectDAppManual(ctx: ReturnType<typeof setupDAppManual>) {
45
- const { transport, session, walletKp } = ctx;
46
- await session.createPairing();
53
+ const { transport, session, walletKp } = ctx
54
+ await session.createPairing()
47
55
 
48
56
  transport.receive({
49
- v: 1, t: 'join', ch: session.channelId,
50
- ts: Date.now(), from: walletKp.publicKeyB64,
51
- body: makeJoinBody(session.channelId, transport.sent[0]!.from!, walletKp),
52
- } as ProtocolMessage);
57
+ v: 1,
58
+ t: 'join',
59
+ ch: session.channelId,
60
+ ts: Date.now(),
61
+ from: walletKp.publicKeyB64,
62
+ body: makeJoinBody(session.channelId, transport.sent[0]?.from!, walletKp),
63
+ } as ProtocolMessage)
53
64
 
54
65
  transport.receive({
55
- v: 1, t: 'ready', ch: session.channelId,
56
- ts: Date.now(), from: '_adapter',
66
+ v: 1,
67
+ t: 'ready',
68
+ ch: session.channelId,
69
+ ts: Date.now(),
70
+ from: '_adapter',
57
71
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
58
- } as ProtocolMessage);
72
+ } as ProtocolMessage)
59
73
 
60
74
  // Derive the wallet's send key (walletToDappKey) which is what
61
75
  // the DAppSession expects to receive (its recvKey).
62
- const recvKey = (session as any).recvKey as Uint8Array;
63
- const dappPubB64 = transport.sent[0]!.from!;
64
- return { recvKey, dappPubB64 };
76
+ const recvKey = (session as any).recvKey as Uint8Array
77
+ const dappPubB64 = transport.sent[0]?.from!
78
+ return { recvKey, dappPubB64 }
65
79
  }
66
80
 
67
81
  function setupWalletManual() {
68
- const transport = new MockTransport();
69
- const dappKp = generateX25519KeyPair();
70
- const channelId = generateChannelId();
82
+ const transport = new MockTransport()
83
+ const dappKp = generateX25519KeyPair()
84
+ const channelId = generateChannelId()
71
85
  const session = new WalletSession({
72
86
  transport,
73
- meta: { name: 'Test Wallet', description: 'Test wallet', url: 'https://wallet.test', icon: 'https://wallet.test/icon.png' },
87
+ meta: {
88
+ name: 'Test Wallet',
89
+ description: 'Test wallet',
90
+ url: 'https://wallet.test',
91
+ icon: 'https://wallet.test/icon.png',
92
+ },
74
93
  capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
75
- });
76
- return { transport, session, dappKp, channelId };
94
+ })
95
+ return { transport, session, dappKp, channelId }
77
96
  }
78
97
 
79
98
  async function connectWalletManual(ctx: ReturnType<typeof setupWalletManual>) {
80
- const { transport, session, dappKp, channelId } = ctx;
99
+ const { transport, session, dappKp, channelId } = ctx
81
100
  const uri = buildPairingUri({
82
101
  channelId,
83
102
  pubkeyB64: dappKp.publicKeyB64,
@@ -85,19 +104,22 @@ async function connectWalletManual(ctx: ReturnType<typeof setupWalletManual>) {
85
104
  name: 'Test dApp',
86
105
  url: 'https://dapp.test',
87
106
  icon: 'https://dapp.test/icon.png',
88
- });
89
- await session.joinFromUri(uri);
107
+ })
108
+ await session.joinFromUri(uri)
90
109
 
91
110
  transport.receive({
92
- v: 1, t: 'ready', ch: channelId,
93
- ts: Date.now(), from: '_adapter',
111
+ v: 1,
112
+ t: 'ready',
113
+ ch: channelId,
114
+ ts: Date.now(),
115
+ from: '_adapter',
94
116
  body: { state: 'connected', reconnect: false, remote: dappKp.publicKeyB64 },
95
- } as ProtocolMessage);
117
+ } as ProtocolMessage)
96
118
 
97
119
  // The wallet's recvKey is dappToWalletKey
98
- const recvKey = (session as any).recvKey as Uint8Array;
99
- const walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
100
- return { recvKey, walletPubB64 };
120
+ const recvKey = (session as any).recvKey as Uint8Array
121
+ const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from!
122
+ return { recvKey, walletPubB64 }
101
123
  }
102
124
 
103
125
  // ---------------------------------------------------------------------------
@@ -105,326 +127,397 @@ async function connectWalletManual(ctx: ReturnType<typeof setupWalletManual>) {
105
127
  // ---------------------------------------------------------------------------
106
128
 
107
129
  describe('Security: Replay detection', () => {
108
-
109
130
  it('same sequence number is rejected', async () => {
110
- const ctx = setupDAppManual();
111
- const { transport, session, walletKp } = ctx;
112
- const { recvKey } = await connectDAppManual(ctx);
131
+ const ctx = setupDAppManual()
132
+ const { transport, session, walletKp } = ctx
133
+ const { recvKey } = await connectDAppManual(ctx)
113
134
 
114
135
  // First request -> seq=0 response accepted
115
- const p0 = session.request('wallet_getAccounts');
116
- await wait(20);
117
- const req0 = transport.sent.find(m => m.t === 'req') as any;
118
- const req0Id = req0.body.id;
136
+ const p0 = session.request('wallet_getAccounts')
137
+ await wait(20)
138
+ const req0 = transport.sent.find((m) => m.t === 'req') as any
139
+ const req0Id = req0.body.id
119
140
 
120
141
  transport.receive({
121
- v: 1, t: 'res', ch: session.channelId,
122
- ts: Date.now(), from: walletKp.publicKeyB64,
123
- body: { id: req0Id, sealed: sealPayload(recvKey, session.channelId, 0, { _ok: true, _result: ['0xa'] },
124
- { type: 'res', from: walletKp.publicKeyB64, id: req0Id }) },
125
- } as ProtocolMessage);
126
- expect(await p0).toEqual(['0xa']);
142
+ v: 1,
143
+ t: 'res',
144
+ ch: session.channelId,
145
+ ts: Date.now(),
146
+ from: walletKp.publicKeyB64,
147
+ body: {
148
+ id: req0Id,
149
+ sealed: sealPayload(
150
+ recvKey,
151
+ session.channelId,
152
+ 0,
153
+ { _ok: true, _result: ['0xa'] },
154
+ { type: 'res', from: walletKp.publicKeyB64, id: req0Id },
155
+ ),
156
+ },
157
+ } as ProtocolMessage)
158
+ expect(await p0).toEqual(['0xa'])
127
159
 
128
160
  // Second request -> seq=0 again (replay) must be rejected
129
- const p1 = session.request('wallet_getAccounts');
130
- await wait(20);
131
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
132
- const req1Id = req1.body.id;
161
+ const p1 = session.request('wallet_getAccounts')
162
+ await wait(20)
163
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1] as any
164
+ const req1Id = req1.body.id
133
165
 
134
166
  transport.receive({
135
- v: 1, t: 'res', ch: session.channelId,
136
- ts: Date.now(), from: walletKp.publicKeyB64,
137
- body: { id: req1Id, sealed: sealPayload(recvKey, session.channelId, 0, { _ok: true, _result: ['replay'] },
138
- { type: 'res', from: walletKp.publicKeyB64, id: req1Id }) },
139
- } as ProtocolMessage);
140
- await expect(p1).rejects.toThrow('Replay detected');
141
- });
167
+ v: 1,
168
+ t: 'res',
169
+ ch: session.channelId,
170
+ ts: Date.now(),
171
+ from: walletKp.publicKeyB64,
172
+ body: {
173
+ id: req1Id,
174
+ sealed: sealPayload(
175
+ recvKey,
176
+ session.channelId,
177
+ 0,
178
+ { _ok: true, _result: ['replay'] },
179
+ { type: 'res', from: walletKp.publicKeyB64, id: req1Id },
180
+ ),
181
+ },
182
+ } as ProtocolMessage)
183
+ await expect(p1).rejects.toThrow('Replay detected')
184
+ })
142
185
 
143
186
  it('lower sequence number is rejected', async () => {
144
- const ctx = setupDAppManual();
145
- const { transport, session, walletKp } = ctx;
146
- const { recvKey } = await connectDAppManual(ctx);
187
+ const ctx = setupDAppManual()
188
+ const { transport, session, walletKp } = ctx
189
+ const { recvKey } = await connectDAppManual(ctx)
147
190
 
148
191
  // seq=5 accepted
149
- const p0 = session.request('wallet_getAccounts');
150
- await wait(20);
151
- const req0 = transport.sent.find(m => m.t === 'req') as any;
152
- const r0id = req0.body.id;
192
+ const p0 = session.request('wallet_getAccounts')
193
+ await wait(20)
194
+ const req0 = transport.sent.find((m) => m.t === 'req') as any
195
+ const r0id = req0.body.id
153
196
  transport.receive({
154
- v: 1, t: 'res', ch: session.channelId,
155
- ts: Date.now(), from: walletKp.publicKeyB64,
156
- body: { id: r0id, sealed: sealPayload(recvKey, session.channelId, 5, { _ok: true, _result: 'ok' },
157
- { type: 'res', from: walletKp.publicKeyB64, id: r0id }) },
158
- } as ProtocolMessage);
159
- expect(await p0).toBe('ok');
197
+ v: 1,
198
+ t: 'res',
199
+ ch: session.channelId,
200
+ ts: Date.now(),
201
+ from: walletKp.publicKeyB64,
202
+ body: {
203
+ id: r0id,
204
+ sealed: sealPayload(
205
+ recvKey,
206
+ session.channelId,
207
+ 5,
208
+ { _ok: true, _result: 'ok' },
209
+ { type: 'res', from: walletKp.publicKeyB64, id: r0id },
210
+ ),
211
+ },
212
+ } as ProtocolMessage)
213
+ expect(await p0).toBe('ok')
160
214
 
161
215
  // seq=3 (lower) must be rejected
162
- const p1 = session.request('wallet_getAccounts');
163
- await wait(20);
164
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
165
- const r1id = req1.body.id;
216
+ const p1 = session.request('wallet_getAccounts')
217
+ await wait(20)
218
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1] as any
219
+ const r1id = req1.body.id
166
220
  transport.receive({
167
- v: 1, t: 'res', ch: session.channelId,
168
- ts: Date.now(), from: walletKp.publicKeyB64,
169
- body: { id: r1id, sealed: sealPayload(recvKey, session.channelId, 3, { _ok: true, _result: 'stale' },
170
- { type: 'res', from: walletKp.publicKeyB64, id: r1id }) },
171
- } as ProtocolMessage);
172
- await expect(p1).rejects.toThrow('Replay detected');
173
- });
221
+ v: 1,
222
+ t: 'res',
223
+ ch: session.channelId,
224
+ ts: Date.now(),
225
+ from: walletKp.publicKeyB64,
226
+ body: {
227
+ id: r1id,
228
+ sealed: sealPayload(
229
+ recvKey,
230
+ session.channelId,
231
+ 3,
232
+ { _ok: true, _result: 'stale' },
233
+ { type: 'res', from: walletKp.publicKeyB64, id: r1id },
234
+ ),
235
+ },
236
+ } as ProtocolMessage)
237
+ await expect(p1).rejects.toThrow('Replay detected')
238
+ })
174
239
 
175
240
  it('higher sequence number is accepted', async () => {
176
- const ctx = setupDAppManual();
177
- const { transport, session, walletKp } = ctx;
178
- const { recvKey } = await connectDAppManual(ctx);
241
+ const ctx = setupDAppManual()
242
+ const { transport, session, walletKp } = ctx
243
+ const { recvKey } = await connectDAppManual(ctx)
179
244
 
180
245
  // seq=0 accepted
181
- const p0 = session.request('wallet_getAccounts');
182
- await wait(20);
183
- const req0 = transport.sent.find(m => m.t === 'req') as any;
184
- const r0id = req0.body.id;
246
+ const p0 = session.request('wallet_getAccounts')
247
+ await wait(20)
248
+ const req0 = transport.sent.find((m) => m.t === 'req') as any
249
+ const r0id = req0.body.id
185
250
  transport.receive({
186
- v: 1, t: 'res', ch: session.channelId,
187
- ts: Date.now(), from: walletKp.publicKeyB64,
188
- body: { id: r0id, sealed: sealPayload(recvKey, session.channelId, 0, { _ok: true, _result: 'first' },
189
- { type: 'res', from: walletKp.publicKeyB64, id: r0id }) },
190
- } as ProtocolMessage);
191
- expect(await p0).toBe('first');
251
+ v: 1,
252
+ t: 'res',
253
+ ch: session.channelId,
254
+ ts: Date.now(),
255
+ from: walletKp.publicKeyB64,
256
+ body: {
257
+ id: r0id,
258
+ sealed: sealPayload(
259
+ recvKey,
260
+ session.channelId,
261
+ 0,
262
+ { _ok: true, _result: 'first' },
263
+ { type: 'res', from: walletKp.publicKeyB64, id: r0id },
264
+ ),
265
+ },
266
+ } as ProtocolMessage)
267
+ expect(await p0).toBe('first')
192
268
 
193
269
  // seq=10 (higher) accepted
194
- const p1 = session.request('wallet_getAccounts');
195
- await wait(20);
196
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
197
- const r1id = req1.body.id;
270
+ const p1 = session.request('wallet_getAccounts')
271
+ await wait(20)
272
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1] as any
273
+ const r1id = req1.body.id
198
274
  transport.receive({
199
- v: 1, t: 'res', ch: session.channelId,
200
- ts: Date.now(), from: walletKp.publicKeyB64,
201
- body: { id: r1id, sealed: sealPayload(recvKey, session.channelId, 10, { _ok: true, _result: 'second' },
202
- { type: 'res', from: walletKp.publicKeyB64, id: r1id }) },
203
- } as ProtocolMessage);
204
- expect(await p1).toBe('second');
205
- });
206
- });
275
+ v: 1,
276
+ t: 'res',
277
+ ch: session.channelId,
278
+ ts: Date.now(),
279
+ from: walletKp.publicKeyB64,
280
+ body: {
281
+ id: r1id,
282
+ sealed: sealPayload(
283
+ recvKey,
284
+ session.channelId,
285
+ 10,
286
+ { _ok: true, _result: 'second' },
287
+ { type: 'res', from: walletKp.publicKeyB64, id: r1id },
288
+ ),
289
+ },
290
+ } as ProtocolMessage)
291
+ expect(await p1).toBe('second')
292
+ })
293
+ })
207
294
 
208
295
  // ---------------------------------------------------------------------------
209
296
  // Tests: AAD tampering
210
297
  // ---------------------------------------------------------------------------
211
298
 
212
299
  describe('Security: AAD tampering', () => {
213
-
214
300
  it('tampered AAD (wrong id) causes decryption failure', () => {
215
- const key = new Uint8Array(32);
216
- crypto.getRandomValues(key);
217
- const channelId = generateChannelId();
301
+ const key = new Uint8Array(32)
302
+ crypto.getRandomValues(key)
303
+ const channelId = generateChannelId()
218
304
 
219
- const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' };
220
- const sealed = sealPayload(key, channelId, 0, { foo: 'bar' }, hdr);
305
+ const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' }
306
+ const sealed = sealPayload(key, channelId, 0, { foo: 'bar' }, hdr)
221
307
 
222
- const tamperedHdr = { ...hdr, id: 'req-999' };
223
- expect(() => unsealPayload(key, channelId, sealed, tamperedHdr)).toThrow();
224
- });
308
+ const tamperedHdr = { ...hdr, id: 'req-999' }
309
+ expect(() => unsealPayload(key, channelId, sealed, tamperedHdr)).toThrow()
310
+ })
225
311
 
226
312
  it('tampered AAD (wrong from) causes decryption failure', () => {
227
- const key = new Uint8Array(32);
228
- crypto.getRandomValues(key);
229
- const channelId = generateChannelId();
313
+ const key = new Uint8Array(32)
314
+ crypto.getRandomValues(key)
315
+ const channelId = generateChannelId()
230
316
 
231
- const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' };
232
- const sealed = sealPayload(key, channelId, 0, {}, hdr);
317
+ const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' }
318
+ const sealed = sealPayload(key, channelId, 0, {}, hdr)
233
319
 
234
- const tamperedHdr = { ...hdr, from: 'evil-relay' };
235
- expect(() => unsealPayload(key, channelId, sealed, tamperedHdr)).toThrow();
236
- });
237
- });
320
+ const tamperedHdr = { ...hdr, from: 'evil-relay' }
321
+ expect(() => unsealPayload(key, channelId, sealed, tamperedHdr)).toThrow()
322
+ })
323
+ })
238
324
 
239
325
  // ---------------------------------------------------------------------------
240
326
  // Tests: Ciphertext and key tampering
241
327
  // ---------------------------------------------------------------------------
242
328
 
243
329
  describe('Security: Ciphertext and key tampering', () => {
244
-
245
330
  it('tampered ciphertext causes decryption failure', () => {
246
- const key = new Uint8Array(32);
247
- crypto.getRandomValues(key);
248
- const channelId = generateChannelId();
331
+ const key = new Uint8Array(32)
332
+ crypto.getRandomValues(key)
333
+ const channelId = generateChannelId()
249
334
 
250
- const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' };
251
- const sealed = sealPayload(key, channelId, 0, { secret: true }, hdr);
335
+ const hdr = { type: 'req' as const, from: 'dapp', id: 'req-1' }
336
+ const sealed = sealPayload(key, channelId, 0, { secret: true }, hdr)
252
337
 
253
338
  // Decode, flip a byte in the ciphertext, re-encode
254
- const bytes = b64urlDecode(sealed);
255
- bytes[10] = bytes[10]! ^ 0xff;
256
- const tampered = b64urlEncode(bytes);
339
+ const bytes = b64urlDecode(sealed)
340
+ bytes[10] = bytes[10]! ^ 0xff
341
+ const tampered = b64urlEncode(bytes)
257
342
 
258
- expect(() => unsealPayload(key, channelId, tampered, hdr)).toThrow();
259
- });
343
+ expect(() => unsealPayload(key, channelId, tampered, hdr)).toThrow()
344
+ })
260
345
 
261
346
  it('wrong key causes decryption failure', () => {
262
- const key = new Uint8Array(32);
263
- crypto.getRandomValues(key);
264
- const wrongKey = new Uint8Array(32);
265
- crypto.getRandomValues(wrongKey);
266
- const channelId = generateChannelId();
347
+ const key = new Uint8Array(32)
348
+ crypto.getRandomValues(key)
349
+ const wrongKey = new Uint8Array(32)
350
+ crypto.getRandomValues(wrongKey)
351
+ const channelId = generateChannelId()
267
352
 
268
- const sealed = sealPayload(key, channelId, 0, { data: 'test' });
269
- expect(() => unsealPayload(wrongKey, channelId, sealed)).toThrow();
270
- });
271
- });
353
+ const sealed = sealPayload(key, channelId, 0, { data: 'test' })
354
+ expect(() => unsealPayload(wrongKey, channelId, sealed)).toThrow()
355
+ })
356
+ })
272
357
 
273
358
  // ---------------------------------------------------------------------------
274
359
  // Tests: Sequence overflow closes session
275
360
  // ---------------------------------------------------------------------------
276
361
 
277
362
  describe('Security: Sequence overflow', () => {
278
-
279
363
  it('DAppSession: overflow at MAX_SEND_SEQ causes session close', async () => {
280
- const ctx = setupDAppManual();
281
- const { session } = ctx;
282
- await connectDAppManual(ctx);
364
+ const ctx = setupDAppManual()
365
+ const { session } = ctx
366
+ await connectDAppManual(ctx)
283
367
 
284
- (session as any).sendSeq = 2 ** 31;
368
+ ;(session as any).sendSeq = 2 ** 31
285
369
 
286
- const errorHandler = vi.fn();
287
- session.on('error', errorHandler);
370
+ const errorHandler = vi.fn()
371
+ session.on('error', errorHandler)
288
372
 
289
- await expect(session.request('wallet_getAccounts')).rejects.toThrow('Send sequence overflow');
290
- expect(errorHandler).toHaveBeenCalled();
291
- expect(session.phase).toBe('closed');
292
- });
373
+ await expect(session.request('wallet_getAccounts')).rejects.toThrow('Send sequence overflow')
374
+ expect(errorHandler).toHaveBeenCalled()
375
+ expect(session.phase).toBe('closed')
376
+ })
293
377
 
294
378
  it('WalletSession: overflow at MAX_SEND_SEQ causes session close via pushEvent', async () => {
295
- const ctx = setupWalletManual();
296
- const { session } = ctx;
297
- await connectWalletManual(ctx);
379
+ const ctx = setupWalletManual()
380
+ const { session } = ctx
381
+ await connectWalletManual(ctx)
298
382
 
299
- (session as any).sendSeq = 2 ** 31 - 1;
383
+ ;(session as any).sendSeq = 2 ** 31 - 1
300
384
 
301
- const errorHandler = vi.fn();
302
- session.on('error', errorHandler);
385
+ const errorHandler = vi.fn()
386
+ session.on('error', errorHandler)
303
387
 
304
388
  // Last allowed
305
- session.pushEvent('accountsChanged', { accounts: ['0xa'] });
306
- expect(session.phase).toBe('connected');
389
+ session.pushEvent('accountsChanged', { accounts: ['0xa'] })
390
+ expect(session.phase).toBe('connected')
307
391
 
308
392
  // Overflow
309
- session.pushEvent('accountsChanged', { accounts: ['0xb'] });
310
- expect(errorHandler).toHaveBeenCalled();
311
- expect(session.phase).toBe('closed');
312
- });
313
- });
393
+ session.pushEvent('accountsChanged', { accounts: ['0xb'] })
394
+ expect(errorHandler).toHaveBeenCalled()
395
+ expect(session.phase).toBe('closed')
396
+ })
397
+ })
314
398
 
315
399
  // ---------------------------------------------------------------------------
316
400
  // Tests: Mandatory encryption enforcement
317
401
  // ---------------------------------------------------------------------------
318
402
 
319
403
  describe('Security: Mandatory encryption', () => {
320
-
321
404
  it('DAppSession rejects unsealed responses', async () => {
322
- const ctx = setupDAppManual();
323
- const { transport, session, walletKp } = ctx;
324
- await connectDAppManual(ctx);
405
+ const ctx = setupDAppManual()
406
+ const { transport, session, walletKp } = ctx
407
+ await connectDAppManual(ctx)
325
408
 
326
- const p = session.request('wallet_getAccounts');
327
- await wait(20);
328
- const req = transport.sent.find(m => m.t === 'req') as any;
329
- const reqId = req.body.id;
409
+ const p = session.request('wallet_getAccounts')
410
+ await wait(20)
411
+ const req = transport.sent.find((m) => m.t === 'req') as any
412
+ const reqId = req.body.id
330
413
 
331
414
  // Send a response without sealed field
332
415
  transport.receive({
333
- v: 1, t: 'res', ch: session.channelId,
334
- ts: Date.now(), from: walletKp.publicKeyB64,
416
+ v: 1,
417
+ t: 'res',
418
+ ch: session.channelId,
419
+ ts: Date.now(),
420
+ from: walletKp.publicKeyB64,
335
421
  body: { id: reqId },
336
422
  // no sealed field in body
337
- } as ProtocolMessage);
423
+ } as ProtocolMessage)
338
424
 
339
- await expect(p).rejects.toThrow('Response must be encrypted');
340
- });
425
+ await expect(p).rejects.toThrow('Response must be encrypted')
426
+ })
341
427
 
342
428
  it('WalletSession rejects unsealed requests', async () => {
343
- const ctx = setupWalletManual();
344
- const { transport, session, dappKp, channelId } = ctx;
345
- await connectWalletManual(ctx);
429
+ const ctx = setupWalletManual()
430
+ const { transport, session, dappKp, channelId } = ctx
431
+ await connectWalletManual(ctx)
346
432
 
347
- const requestHandler = vi.fn();
348
- session.on('request', requestHandler);
433
+ const requestHandler = vi.fn()
434
+ session.on('request', requestHandler)
349
435
 
350
436
  // Send a request without sealed field in body
351
437
  transport.receive({
352
- v: 1, t: 'req', ch: channelId,
353
- ts: Date.now(), from: dappKp.publicKeyB64,
438
+ v: 1,
439
+ t: 'req',
440
+ ch: channelId,
441
+ ts: Date.now(),
442
+ from: dappKp.publicKeyB64,
354
443
  body: { id: 'req-unseal' },
355
444
  // no sealed field
356
- } as ProtocolMessage);
445
+ } as ProtocolMessage)
357
446
 
358
447
  // The request handler should NOT be called
359
- expect(requestHandler).not.toHaveBeenCalled();
448
+ expect(requestHandler).not.toHaveBeenCalled()
360
449
 
361
450
  // Wallet should have sent a rejection response
362
- const rejectionMsg = transport.sent.find(m => m.t === 'res' && (m as any).body?.id === 'req-unseal') as any;
363
- expect(rejectionMsg).toBeTruthy();
451
+ const rejectionMsg = transport.sent.find(
452
+ (m) => m.t === 'res' && (m as any).body?.id === 'req-unseal',
453
+ ) as any
454
+ expect(rejectionMsg).toBeTruthy()
364
455
  // ok no longer exists on wire body
365
- });
456
+ })
366
457
 
367
458
  it('DAppSession drops unsealed events', async () => {
368
- const ctx = setupDAppManual();
369
- const { transport, session, walletKp } = ctx;
370
- await connectDAppManual(ctx);
459
+ const ctx = setupDAppManual()
460
+ const { transport, session, walletKp } = ctx
461
+ await connectDAppManual(ctx)
371
462
 
372
- const eventHandler = vi.fn();
373
- session.on('event', eventHandler);
463
+ const eventHandler = vi.fn()
464
+ session.on('event', eventHandler)
374
465
 
375
466
  // Send an event without sealed field in body
376
467
  transport.receive({
377
- v: 1, t: 'evt', ch: session.channelId,
378
- ts: Date.now(), from: walletKp.publicKeyB64,
468
+ v: 1,
469
+ t: 'evt',
470
+ ch: session.channelId,
471
+ ts: Date.now(),
472
+ from: walletKp.publicKeyB64,
379
473
  body: { id: 'evt-1' },
380
474
  // no sealed field
381
- } as ProtocolMessage);
475
+ } as ProtocolMessage)
382
476
 
383
- await wait(20);
384
- expect(eventHandler).not.toHaveBeenCalled();
385
- });
386
- });
477
+ await wait(20)
478
+ expect(eventHandler).not.toHaveBeenCalled()
479
+ })
480
+ })
387
481
 
388
482
  // ---------------------------------------------------------------------------
389
483
  // Tests: Key isolation across sessions
390
484
  // ---------------------------------------------------------------------------
391
485
 
392
486
  describe('Security: Key isolation across sessions', () => {
393
-
394
487
  it('session key changes if wallet pubkey changes (no key reuse)', async () => {
395
- const dappKp = generateX25519KeyPair();
396
- const channelId = generateChannelId();
488
+ const dappKp = generateX25519KeyPair()
489
+ const channelId = generateChannelId()
397
490
 
398
491
  // First wallet
399
- const wallet1 = generateX25519KeyPair();
400
- const shared1 = computeSharedSecret(dappKp.privateKey, wallet1.publicKey);
401
- const root1 = deriveSessionKey(shared1, channelId);
492
+ const wallet1 = generateX25519KeyPair()
493
+ const shared1 = computeSharedSecret(dappKp.privateKey, wallet1.publicKey)
494
+ const root1 = deriveSessionKey(shared1, channelId)
402
495
  const ctx1: SessionCryptoContext = {
403
496
  dappPubKeyB64: dappKp.publicKeyB64,
404
497
  walletPubKeyB64: wallet1.publicKeyB64,
405
498
  capabilities: null,
406
499
  walletMeta: null,
407
500
  dappName: 'App',
408
- };
409
- const keys1 = deriveDirectionalSessionKeys(root1, channelId, ctx1);
501
+ }
502
+ const keys1 = deriveDirectionalSessionKeys(root1, channelId, ctx1)
410
503
 
411
504
  // Second wallet (different pubkey)
412
- const wallet2 = generateX25519KeyPair();
413
- const shared2 = computeSharedSecret(dappKp.privateKey, wallet2.publicKey);
414
- const root2 = deriveSessionKey(shared2, channelId);
505
+ const wallet2 = generateX25519KeyPair()
506
+ const shared2 = computeSharedSecret(dappKp.privateKey, wallet2.publicKey)
507
+ const root2 = deriveSessionKey(shared2, channelId)
415
508
  const ctx2: SessionCryptoContext = {
416
509
  dappPubKeyB64: dappKp.publicKeyB64,
417
510
  walletPubKeyB64: wallet2.publicKeyB64,
418
511
  capabilities: null,
419
512
  walletMeta: null,
420
513
  dappName: 'App',
421
- };
422
- const keys2 = deriveDirectionalSessionKeys(root2, channelId, ctx2);
514
+ }
515
+ const keys2 = deriveDirectionalSessionKeys(root2, channelId, ctx2)
423
516
 
424
517
  // All keys must differ
425
- expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey));
426
- expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey));
427
- expect(bytesToHex(keys1.rootKey)).not.toBe(bytesToHex(keys2.rootKey));
428
- expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash));
429
- });
430
- });
518
+ expect(bytesToHex(keys1.dappToWalletKey)).not.toBe(bytesToHex(keys2.dappToWalletKey))
519
+ expect(bytesToHex(keys1.walletToDappKey)).not.toBe(bytesToHex(keys2.walletToDappKey))
520
+ expect(bytesToHex(keys1.rootKey)).not.toBe(bytesToHex(keys2.rootKey))
521
+ expect(bytesToHex(keys1.transcriptHash)).not.toBe(bytesToHex(keys2.transcriptHash))
522
+ })
523
+ })