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
@@ -9,18 +9,14 @@
9
9
  * correctly, protecting the dApp from a compromised or malicious wallet.
10
10
  */
11
11
 
12
- import { describe, it, expect, vi } from 'vitest';
13
- import { DAppSession } from '../../dapp-session.js';
14
- import { MockTransport, makeJoinBody } from '../../test-helpers.js';
15
- import {
16
- generateX25519KeyPair,
17
- sealPayload,
18
- b64urlEncode,
19
- } from '../../crypto.js';
20
- import type { ProtocolMessage } from '../../types.js';
12
+ import { describe, expect, it, vi } from 'vitest'
13
+ import { generateX25519KeyPair, sealPayload } from '../../crypto.js'
14
+ import { DAppSession } from '../../dapp-session.js'
15
+ import { MockTransport, makeJoinBody } from '../../test-helpers.js'
16
+ import type { ProtocolMessage } from '../../types.js'
21
17
 
22
18
  function wait(ms = 50): Promise<void> {
23
- return new Promise((r) => setTimeout(r, ms));
19
+ return new Promise((r) => setTimeout(r, ms))
24
20
  }
25
21
 
26
22
  // ---------------------------------------------------------------------------
@@ -28,34 +24,45 @@ function wait(ms = 50): Promise<void> {
28
24
  // ---------------------------------------------------------------------------
29
25
 
30
26
  function setupDAppManual() {
31
- const transport = new MockTransport();
27
+ const transport = new MockTransport()
32
28
  const session = new DAppSession({
33
29
  transport,
34
- meta: { name: 'Test', description: 'Test dApp', url: 'https://test.com', icon: 'https://test.com/icon.png' },
35
- });
36
- const walletKp = generateX25519KeyPair();
37
- return { transport, session, walletKp };
30
+ meta: {
31
+ name: 'Test',
32
+ description: 'Test dApp',
33
+ url: 'https://test.com',
34
+ icon: 'https://test.com/icon.png',
35
+ },
36
+ })
37
+ const walletKp = generateX25519KeyPair()
38
+ return { transport, session, walletKp }
38
39
  }
39
40
 
40
41
  async function connectDAppManual(ctx: ReturnType<typeof setupDAppManual>) {
41
- const { transport, session, walletKp } = ctx;
42
- await session.createPairing();
42
+ const { transport, session, walletKp } = ctx
43
+ await session.createPairing()
43
44
 
44
45
  transport.receive({
45
- v: 1, t: 'join', ch: session.channelId,
46
- ts: Date.now(), from: walletKp.publicKeyB64,
47
- body: makeJoinBody(session.channelId, transport.sent[0]!.from!, walletKp),
48
- } as ProtocolMessage);
46
+ v: 1,
47
+ t: 'join',
48
+ ch: session.channelId,
49
+ ts: Date.now(),
50
+ from: walletKp.publicKeyB64,
51
+ body: makeJoinBody(session.channelId, transport.sent[0]?.from ?? '', walletKp),
52
+ } as ProtocolMessage)
49
53
 
50
54
  transport.receive({
51
- v: 1, t: 'ready', ch: session.channelId,
52
- ts: Date.now(), from: '_adapter',
55
+ v: 1,
56
+ t: 'ready',
57
+ ch: session.channelId,
58
+ ts: Date.now(),
59
+ from: '_adapter',
53
60
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
54
- } as ProtocolMessage);
61
+ } as ProtocolMessage)
55
62
 
56
- const recvKey = (session as any).recvKey as Uint8Array;
57
- const dappPubB64 = transport.sent[0]!.from!;
58
- return { recvKey, dappPubB64 };
63
+ const recvKey = (session as unknown as Record<string, unknown>).recvKey as Uint8Array
64
+ const dappPubB64 = transport.sent[0]?.from ?? ''
65
+ return { recvKey, dappPubB64 }
59
66
  }
60
67
 
61
68
  // ---------------------------------------------------------------------------
@@ -71,34 +78,39 @@ describe('Malicious Wallet: Wrong response ID', () => {
71
78
  // PREVENTS: Response injection for non-existent requests.
72
79
  // The dApp should silently ignore responses with unknown IDs.
73
80
 
74
- const ctx = setupDAppManual();
75
- const { transport, session, walletKp } = ctx;
76
- const { recvKey } = await connectDAppManual(ctx);
81
+ const ctx = setupDAppManual()
82
+ const { transport, session, walletKp } = ctx
83
+ const { recvKey } = await connectDAppManual(ctx)
77
84
 
78
- const responseHandler = vi.fn();
79
- session.on('response', responseHandler);
85
+ const responseHandler = vi.fn()
86
+ session.on('response', responseHandler)
80
87
 
81
88
  // Wallet sends a response with a fabricated request ID
82
89
  transport.receive({
83
- v: 1, t: 'res', ch: session.channelId,
84
- ts: Date.now(), from: walletKp.publicKeyB64,
90
+ v: 1,
91
+ t: 'res',
92
+ ch: session.channelId,
93
+ ts: Date.now(),
94
+ from: walletKp.publicKeyB64,
85
95
  body: {
86
96
  id: 'fabricated-req-id',
87
97
  sealed: sealPayload(
88
- recvKey, session.channelId, 0,
98
+ recvKey,
99
+ session.channelId,
100
+ 0,
89
101
  { _ok: true, _result: 'injected-data' },
90
102
  { type: 'res', from: walletKp.publicKeyB64, id: 'fabricated-req-id' },
91
103
  ),
92
104
  },
93
- } as ProtocolMessage);
105
+ } as ProtocolMessage)
94
106
 
95
- await wait();
107
+ await wait()
96
108
 
97
109
  // Response handler should NOT have been called (no matching pending request)
98
- expect(responseHandler).not.toHaveBeenCalled();
110
+ expect(responseHandler).not.toHaveBeenCalled()
99
111
  // Session should remain healthy
100
- expect(session.phase).toBe('connected');
101
- });
112
+ expect(session.phase).toBe('connected')
113
+ })
102
114
 
103
115
  it('response to wrong req.id does not resolve a different pending request', async () => {
104
116
  // ATTACK: Wallet sends a response with a different request's ID,
@@ -106,56 +118,66 @@ describe('Malicious Wallet: Wrong response ID', () => {
106
118
  //
107
119
  // PREVENTS: Cross-request response substitution.
108
120
 
109
- const ctx = setupDAppManual();
110
- const { transport, session, walletKp } = ctx;
111
- const { recvKey } = await connectDAppManual(ctx);
121
+ const ctx = setupDAppManual()
122
+ const { transport, session, walletKp } = ctx
123
+ const { recvKey } = await connectDAppManual(ctx)
112
124
 
113
125
  // Send two requests
114
- const p1 = session.request('wallet_getAccounts');
115
- const p2 = session.request('wallet_signMessage', { message: 'test' });
116
- await wait(20);
126
+ const p1 = session.request('wallet_getAccounts')
127
+ const p2 = session.request('wallet_signMessage', { message: 'test' })
128
+ await wait(20)
117
129
 
118
- const reqs = transport.sent.filter(m => m.t === 'req') as any[];
119
- const req1Id = reqs[0]!.body.id;
120
- const req2Id = reqs[1]!.body.id;
130
+ const reqs = transport.sent.filter((m) => m.t === 'req')
131
+ const req1Id = (reqs[0]?.body as Record<string, unknown>)?.id as string
132
+ const req2Id = (reqs[1]?.body as Record<string, unknown>)?.id as string
121
133
 
122
134
  // Wallet responds to req2 with req1's ID (cross-wired)
123
135
  // The AAD includes the id field, so if the id doesn't match,
124
136
  // AEAD decryption will fail (or the wrong request will be resolved
125
137
  // with potentially confusing data).
126
138
  transport.receive({
127
- v: 1, t: 'res', ch: session.channelId,
128
- ts: Date.now(), from: walletKp.publicKeyB64,
139
+ v: 1,
140
+ t: 'res',
141
+ ch: session.channelId,
142
+ ts: Date.now(),
143
+ from: walletKp.publicKeyB64,
129
144
  body: {
130
145
  id: req1Id,
131
146
  sealed: sealPayload(
132
- recvKey, session.channelId, 0,
147
+ recvKey,
148
+ session.channelId,
149
+ 0,
133
150
  { _ok: true, _result: 'correct-for-req1' },
134
151
  { type: 'res', from: walletKp.publicKeyB64, id: req1Id },
135
152
  ),
136
153
  },
137
- } as ProtocolMessage);
154
+ } as ProtocolMessage)
138
155
 
139
156
  // req1 should resolve correctly
140
- expect(await p1).toBe('correct-for-req1');
157
+ expect(await p1).toBe('correct-for-req1')
141
158
 
142
159
  // Now respond to req2 normally
143
160
  transport.receive({
144
- v: 1, t: 'res', ch: session.channelId,
145
- ts: Date.now(), from: walletKp.publicKeyB64,
161
+ v: 1,
162
+ t: 'res',
163
+ ch: session.channelId,
164
+ ts: Date.now(),
165
+ from: walletKp.publicKeyB64,
146
166
  body: {
147
167
  id: req2Id,
148
168
  sealed: sealPayload(
149
- recvKey, session.channelId, 1,
169
+ recvKey,
170
+ session.channelId,
171
+ 1,
150
172
  { _ok: true, _result: 'correct-for-req2' },
151
173
  { type: 'res', from: walletKp.publicKeyB64, id: req2Id },
152
174
  ),
153
175
  },
154
- } as ProtocolMessage);
176
+ } as ProtocolMessage)
155
177
 
156
- expect(await p2).toBe('correct-for-req2');
157
- });
158
- });
178
+ expect(await p2).toBe('correct-for-req2')
179
+ })
180
+ })
159
181
 
160
182
  // ---------------------------------------------------------------------------
161
183
  // Attack 2: Wallet sends req (role violation)
@@ -169,37 +191,42 @@ describe('Malicious Wallet: Sends req (role violation)', () => {
169
191
  //
170
192
  // PREVENTS: Role reversal attack where wallet tries to command the dApp.
171
193
 
172
- const ctx = setupDAppManual();
173
- const { transport, session, walletKp } = ctx;
174
- const { recvKey } = await connectDAppManual(ctx);
194
+ const ctx = setupDAppManual()
195
+ const { transport, session, walletKp } = ctx
196
+ const { recvKey } = await connectDAppManual(ctx)
175
197
 
176
- const errorHandler = vi.fn();
177
- session.on('error', errorHandler);
198
+ const errorHandler = vi.fn()
199
+ session.on('error', errorHandler)
178
200
 
179
201
  // Wallet sends a req (which it should never do)
180
202
  transport.receive({
181
- v: 1, t: 'req', ch: session.channelId,
182
- ts: Date.now(), from: walletKp.publicKeyB64,
203
+ v: 1,
204
+ t: 'req',
205
+ ch: session.channelId,
206
+ ts: Date.now(),
207
+ from: walletKp.publicKeyB64,
183
208
  body: {
184
209
  id: 'evil-req-1',
185
210
  sealed: sealPayload(
186
- recvKey, session.channelId, 0,
211
+ recvKey,
212
+ session.channelId,
213
+ 0,
187
214
  { _method: 'dapp_executeTransaction' },
188
215
  { type: 'req', from: walletKp.publicKeyB64, id: 'evil-req-1' },
189
216
  ),
190
217
  },
191
- } as ProtocolMessage);
218
+ } as ProtocolMessage)
192
219
 
193
- await wait();
220
+ await wait()
194
221
 
195
222
  // DApp does not have a request handler (it only sends requests).
196
223
  // The message should be silently ignored or cause no state change.
197
224
  // Key insight: DAppSession.handleMessage() has no case for 'req'
198
225
  // messages from the wallet, so it falls through to the default
199
226
  // case (no-op).
200
- expect(session.phase).toBe('connected');
201
- });
202
- });
227
+ expect(session.phase).toBe('connected')
228
+ })
229
+ })
203
230
 
204
231
  // ---------------------------------------------------------------------------
205
232
  // Attack 3: Wallet manipulates sequence numbers
@@ -210,31 +237,36 @@ describe('Malicious Wallet: Sequence number manipulation', () => {
210
237
  // Per Section 6.6.1: "Gaps are valid (expected after reconnect)."
211
238
  // This is NOT an attack — verifying correct behavior.
212
239
 
213
- const ctx = setupDAppManual();
214
- const { transport, session, walletKp } = ctx;
215
- const { recvKey } = await connectDAppManual(ctx);
240
+ const ctx = setupDAppManual()
241
+ const { transport, session, walletKp } = ctx
242
+ const { recvKey } = await connectDAppManual(ctx)
216
243
 
217
- const p = session.request('wallet_getAccounts');
218
- await wait(20);
219
- const req = transport.sent.find(m => m.t === 'req') as any;
220
- const reqId = req.body.id;
244
+ const p = session.request('wallet_getAccounts')
245
+ await wait(20)
246
+ const req = transport.sent.find((m) => m.t === 'req')
247
+ const reqId = (req?.body as Record<string, unknown>)?.id as string
221
248
 
222
249
  // Wallet responds with seq=5 (skipping 0-4) — should be accepted
223
250
  transport.receive({
224
- v: 1, t: 'res', ch: session.channelId,
225
- ts: Date.now(), from: walletKp.publicKeyB64,
251
+ v: 1,
252
+ t: 'res',
253
+ ch: session.channelId,
254
+ ts: Date.now(),
255
+ from: walletKp.publicKeyB64,
226
256
  body: {
227
257
  id: reqId,
228
258
  sealed: sealPayload(
229
- recvKey, session.channelId, 5,
259
+ recvKey,
260
+ session.channelId,
261
+ 5,
230
262
  { _ok: true, _result: 'gap-ok' },
231
263
  { type: 'res', from: walletKp.publicKeyB64, id: reqId },
232
264
  ),
233
265
  },
234
- } as ProtocolMessage);
266
+ } as ProtocolMessage)
235
267
 
236
- expect(await p).toBe('gap-ok');
237
- });
268
+ expect(await p).toBe('gap-ok')
269
+ })
238
270
 
239
271
  it('reset sequence to 0 after receiving higher seq is rejected', async () => {
240
272
  // ATTACK: Wallet sends seq=10, then tries to reset to seq=0.
@@ -244,97 +276,117 @@ describe('Malicious Wallet: Sequence number manipulation', () => {
244
276
  //
245
277
  // PREVENTS: Sequence counter reset allowing message replay.
246
278
 
247
- const ctx = setupDAppManual();
248
- const { transport, session, walletKp } = ctx;
249
- const { recvKey } = await connectDAppManual(ctx);
279
+ const ctx = setupDAppManual()
280
+ const { transport, session, walletKp } = ctx
281
+ const { recvKey } = await connectDAppManual(ctx)
250
282
 
251
283
  // First response at seq=10 (accepted)
252
- const p0 = session.request('wallet_getAccounts');
253
- await wait(20);
254
- const req0 = transport.sent.find(m => m.t === 'req') as any;
255
- const r0id = req0.body.id;
284
+ const p0 = session.request('wallet_getAccounts')
285
+ await wait(20)
286
+ const req0 = transport.sent.find((m) => m.t === 'req')
287
+ const r0id = (req0?.body as Record<string, unknown>)?.id as string
256
288
  transport.receive({
257
- v: 1, t: 'res', ch: session.channelId,
258
- ts: Date.now(), from: walletKp.publicKeyB64,
289
+ v: 1,
290
+ t: 'res',
291
+ ch: session.channelId,
292
+ ts: Date.now(),
293
+ from: walletKp.publicKeyB64,
259
294
  body: {
260
295
  id: r0id,
261
296
  sealed: sealPayload(
262
- recvKey, session.channelId, 10,
297
+ recvKey,
298
+ session.channelId,
299
+ 10,
263
300
  { _ok: true, _result: 'first' },
264
301
  { type: 'res', from: walletKp.publicKeyB64, id: r0id },
265
302
  ),
266
303
  },
267
- } as ProtocolMessage);
268
- expect(await p0).toBe('first');
304
+ } as ProtocolMessage)
305
+ expect(await p0).toBe('first')
269
306
 
270
307
  // Second response at seq=0 (reset attempt — MUST be rejected)
271
- const p1 = session.request('wallet_getAccounts');
272
- await wait(20);
273
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
274
- const r1id = req1.body.id;
308
+ const p1 = session.request('wallet_getAccounts')
309
+ await wait(20)
310
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1]
311
+ const r1id = (req1?.body as Record<string, unknown>)?.id as string
275
312
  transport.receive({
276
- v: 1, t: 'res', ch: session.channelId,
277
- ts: Date.now(), from: walletKp.publicKeyB64,
313
+ v: 1,
314
+ t: 'res',
315
+ ch: session.channelId,
316
+ ts: Date.now(),
317
+ from: walletKp.publicKeyB64,
278
318
  body: {
279
319
  id: r1id,
280
320
  sealed: sealPayload(
281
- recvKey, session.channelId, 0, // reset to 0!
321
+ recvKey,
322
+ session.channelId,
323
+ 0, // reset to 0!
282
324
  { _ok: true, _result: 'replayed' },
283
325
  { type: 'res', from: walletKp.publicKeyB64, id: r1id },
284
326
  ),
285
327
  },
286
- } as ProtocolMessage);
287
- await expect(p1).rejects.toThrow('Replay detected');
288
- });
328
+ } as ProtocolMessage)
329
+ await expect(p1).rejects.toThrow('Replay detected')
330
+ })
289
331
 
290
332
  it('reused sequence number is rejected', async () => {
291
333
  // ATTACK: Wallet sends the same sequence number twice.
292
334
  //
293
335
  // PREVENTS: Nonce reuse in AEAD encryption.
294
336
 
295
- const ctx = setupDAppManual();
296
- const { transport, session, walletKp } = ctx;
297
- const { recvKey } = await connectDAppManual(ctx);
337
+ const ctx = setupDAppManual()
338
+ const { transport, session, walletKp } = ctx
339
+ const { recvKey } = await connectDAppManual(ctx)
298
340
 
299
341
  // First at seq=3 (accepted)
300
- const p0 = session.request('wallet_getAccounts');
301
- await wait(20);
302
- const req0 = transport.sent.find(m => m.t === 'req') as any;
303
- const r0id = req0.body.id;
342
+ const p0 = session.request('wallet_getAccounts')
343
+ await wait(20)
344
+ const req0 = transport.sent.find((m) => m.t === 'req')
345
+ const r0id = (req0?.body as Record<string, unknown>)?.id as string
304
346
  transport.receive({
305
- v: 1, t: 'res', ch: session.channelId,
306
- ts: Date.now(), from: walletKp.publicKeyB64,
347
+ v: 1,
348
+ t: 'res',
349
+ ch: session.channelId,
350
+ ts: Date.now(),
351
+ from: walletKp.publicKeyB64,
307
352
  body: {
308
353
  id: r0id,
309
354
  sealed: sealPayload(
310
- recvKey, session.channelId, 3,
355
+ recvKey,
356
+ session.channelId,
357
+ 3,
311
358
  { _ok: true, _result: 'ok' },
312
359
  { type: 'res', from: walletKp.publicKeyB64, id: r0id },
313
360
  ),
314
361
  },
315
- } as ProtocolMessage);
316
- expect(await p0).toBe('ok');
362
+ } as ProtocolMessage)
363
+ expect(await p0).toBe('ok')
317
364
 
318
365
  // Second at seq=3 (reuse — MUST be rejected)
319
- const p1 = session.request('wallet_getAccounts');
320
- await wait(20);
321
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
322
- const r1id = req1.body.id;
366
+ const p1 = session.request('wallet_getAccounts')
367
+ await wait(20)
368
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1]
369
+ const r1id = (req1?.body as Record<string, unknown>)?.id as string
323
370
  transport.receive({
324
- v: 1, t: 'res', ch: session.channelId,
325
- ts: Date.now(), from: walletKp.publicKeyB64,
371
+ v: 1,
372
+ t: 'res',
373
+ ch: session.channelId,
374
+ ts: Date.now(),
375
+ from: walletKp.publicKeyB64,
326
376
  body: {
327
377
  id: r1id,
328
378
  sealed: sealPayload(
329
- recvKey, session.channelId, 3, // same seq!
379
+ recvKey,
380
+ session.channelId,
381
+ 3, // same seq!
330
382
  { _ok: true, _result: 'reused' },
331
383
  { type: 'res', from: walletKp.publicKeyB64, id: r1id },
332
384
  ),
333
385
  },
334
- } as ProtocolMessage);
335
- await expect(p1).rejects.toThrow('Replay detected');
336
- });
337
- });
386
+ } as ProtocolMessage)
387
+ await expect(p1).rejects.toThrow('Replay detected')
388
+ })
389
+ })
338
390
 
339
391
  // ---------------------------------------------------------------------------
340
392
  // Attack 4: Wallet sends evt before ready.connected
@@ -348,46 +400,52 @@ describe('Malicious Wallet: Event before connected', () => {
348
400
  //
349
401
  // PREVENTS: Pre-connection event injection.
350
402
 
351
- const transport = new MockTransport();
403
+ const transport = new MockTransport()
352
404
  const session = new DAppSession({
353
405
  transport,
354
406
  meta: { name: 'T', description: 'T', url: 'https://t.test', icon: 'https://t.test/i.png' },
355
407
  autoAccept: false, // manual accept to control timing
356
- });
408
+ })
357
409
 
358
- const eventHandler = vi.fn();
359
- session.on('event', eventHandler);
410
+ const eventHandler = vi.fn()
411
+ session.on('event', eventHandler)
360
412
 
361
- await session.createPairing();
362
- const dappPubB64 = transport.sent[0]!.from!;
413
+ await session.createPairing()
414
+ const dappPubB64 = transport.sent[0]?.from ?? ''
363
415
 
364
- const walletKp = generateX25519KeyPair();
416
+ const walletKp = generateX25519KeyPair()
365
417
 
366
418
  // Wallet joins
367
419
  transport.receive({
368
- v: 1, t: 'join', ch: session.channelId,
369
- ts: Date.now(), from: walletKp.publicKeyB64,
420
+ v: 1,
421
+ t: 'join',
422
+ ch: session.channelId,
423
+ ts: Date.now(),
424
+ from: walletKp.publicKeyB64,
370
425
  body: makeJoinBody(session.channelId, dappPubB64, walletKp),
371
- } as ProtocolMessage);
426
+ } as ProtocolMessage)
372
427
 
373
- await wait();
428
+ await wait()
374
429
  // Session is now in pending_accept, NOT connected
375
430
 
376
431
  // Wallet tries to send an event before connected
377
432
  transport.receive({
378
- v: 1, t: 'evt', ch: session.channelId,
379
- ts: Date.now(), from: walletKp.publicKeyB64,
433
+ v: 1,
434
+ t: 'evt',
435
+ ch: session.channelId,
436
+ ts: Date.now(),
437
+ from: walletKp.publicKeyB64,
380
438
  body: { id: 'premature-evt', sealed: 'fake-sealed' },
381
- } as ProtocolMessage);
439
+ } as ProtocolMessage)
382
440
 
383
- await wait();
441
+ await wait()
384
442
 
385
443
  // Event should NOT have been processed (recvKey exists but
386
444
  // the sealed data is invalid, or the event is silently dropped
387
445
  // due to decryption failure)
388
- expect(eventHandler).not.toHaveBeenCalled();
389
- });
390
- });
446
+ expect(eventHandler).not.toHaveBeenCalled()
447
+ })
448
+ })
391
449
 
392
450
  // ---------------------------------------------------------------------------
393
451
  // Attack 5: Wallet sends response from a different key
@@ -401,39 +459,44 @@ describe('Malicious Wallet: Response from wrong peer', () => {
401
459
  //
402
460
  // PREVENTS: Third-party response injection.
403
461
 
404
- const ctx = setupDAppManual();
405
- const { transport, session, walletKp } = ctx;
406
- const { recvKey } = await connectDAppManual(ctx);
462
+ const ctx = setupDAppManual()
463
+ const { transport, session } = ctx
464
+ const { recvKey } = await connectDAppManual(ctx)
407
465
 
408
- const p = session.request('wallet_getAccounts');
409
- await wait(20);
410
- const req = transport.sent.find(m => m.t === 'req') as any;
411
- const reqId = req.body.id;
466
+ const p = session.request('wallet_getAccounts')
467
+ await wait(20)
468
+ const req = transport.sent.find((m) => m.t === 'req')
469
+ const reqId = (req?.body as Record<string, unknown>)?.id as string
412
470
 
413
471
  // Impersonator uses a different key
414
- const impersonatorKp = generateX25519KeyPair();
472
+ const impersonatorKp = generateX25519KeyPair()
415
473
  transport.receive({
416
- v: 1, t: 'res', ch: session.channelId,
417
- ts: Date.now(), from: impersonatorKp.publicKeyB64, // wrong key!
474
+ v: 1,
475
+ t: 'res',
476
+ ch: session.channelId,
477
+ ts: Date.now(),
478
+ from: impersonatorKp.publicKeyB64, // wrong key!
418
479
  body: {
419
480
  id: reqId,
420
481
  sealed: sealPayload(
421
- recvKey, session.channelId, 0,
482
+ recvKey,
483
+ session.channelId,
484
+ 0,
422
485
  { _ok: true, _result: 'evil' },
423
486
  { type: 'res', from: impersonatorKp.publicKeyB64, id: reqId },
424
487
  ),
425
488
  },
426
- } as ProtocolMessage);
489
+ } as ProtocolMessage)
427
490
 
428
- await wait(20);
491
+ await wait(20)
429
492
 
430
493
  // The response should have been silently dropped (from mismatch)
431
494
  // The request should still be pending (not resolved)
432
495
  // Clean up by closing
433
- session.close();
434
- await expect(p).rejects.toThrow('Session closed');
435
- });
436
- });
496
+ session.close()
497
+ await expect(p).rejects.toThrow('Session closed')
498
+ })
499
+ })
437
500
 
438
501
  // ---------------------------------------------------------------------------
439
502
  // Attack 6: Wallet sends unsupported protocol version
@@ -446,22 +509,25 @@ describe('Malicious Wallet: Unsupported protocol version', () => {
446
509
  //
447
510
  // PREVENTS: Version confusion attacks. Section 15 rule 12.
448
511
 
449
- const ctx = setupDAppManual();
450
- const { transport, session, walletKp } = ctx;
451
- await connectDAppManual(ctx);
512
+ const ctx = setupDAppManual()
513
+ const { transport, session, walletKp } = ctx
514
+ await connectDAppManual(ctx)
452
515
 
453
516
  transport.receive({
454
- v: 99 as any, t: 'res', ch: session.channelId,
455
- ts: Date.now(), from: walletKp.publicKeyB64,
517
+ v: 99 as ProtocolMessage['v'],
518
+ t: 'res',
519
+ ch: session.channelId,
520
+ ts: Date.now(),
521
+ from: walletKp.publicKeyB64,
456
522
  body: { id: 'req-1', sealed: 'whatever' },
457
- } as ProtocolMessage);
523
+ } as ProtocolMessage)
458
524
 
459
- await wait();
525
+ await wait()
460
526
 
461
527
  // DApp should close with unsupported_version
462
- expect(session.phase).toBe('closed');
463
- const closeMsg = transport.sent.find(m => m.t === 'close') as any;
464
- expect(closeMsg).toBeTruthy();
465
- expect(closeMsg.body.reason).toBe('unsupported_version');
466
- });
467
- });
528
+ expect(session.phase).toBe('closed')
529
+ const closeMsg = transport.sent.find((m) => m.t === 'close')
530
+ expect(closeMsg).toBeTruthy()
531
+ expect((closeMsg?.body as Record<string, unknown>)?.reason).toBe('unsupported_version')
532
+ })
533
+ })