walletpair-sdk 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +13 -0
  2. package/dist/ble/framing.d.ts.map +1 -1
  3. package/dist/ble/framing.js +2 -2
  4. package/dist/ble/framing.js.map +1 -1
  5. package/dist/ble/index.d.ts +2 -2
  6. package/dist/ble/index.d.ts.map +1 -1
  7. package/dist/ble/index.js +2 -2
  8. package/dist/ble/index.js.map +1 -1
  9. package/dist/ble/web-ble-transport.d.ts +1 -1
  10. package/dist/ble/web-ble-transport.d.ts.map +1 -1
  11. package/dist/ble/web-ble-transport.js +23 -12
  12. package/dist/ble/web-ble-transport.js.map +1 -1
  13. package/dist/crypto.d.ts.map +1 -1
  14. package/dist/crypto.js +29 -12
  15. package/dist/crypto.js.map +1 -1
  16. package/dist/dapp-session.d.ts.map +1 -1
  17. package/dist/dapp-session.js +15 -5
  18. package/dist/dapp-session.js.map +1 -1
  19. package/dist/emitter.d.ts +1 -3
  20. package/dist/emitter.d.ts.map +1 -1
  21. package/dist/emitter.js +4 -2
  22. package/dist/emitter.js.map +1 -1
  23. package/dist/evm/eip1193.d.ts +2 -2
  24. package/dist/evm/eip1193.d.ts.map +1 -1
  25. package/dist/evm/eip1193.js +32 -18
  26. package/dist/evm/eip1193.js.map +1 -1
  27. package/dist/evm/index.d.ts +2 -2
  28. package/dist/evm/index.d.ts.map +1 -1
  29. package/dist/evm/index.js.map +1 -1
  30. package/dist/wallet-session.d.ts.map +1 -1
  31. package/dist/wallet-session.js +4 -3
  32. package/dist/wallet-session.js.map +1 -1
  33. package/dist/ws-transport.d.ts +3 -2
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +13 -4
  36. package/dist/ws-transport.js.map +1 -1
  37. package/package.json +20 -1
  38. package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
  39. package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
  40. package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
  41. package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
  42. package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
  43. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
  44. package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
  45. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
  46. package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
  47. package/src/ble/framing.test.ts +122 -114
  48. package/src/ble/framing.ts +48 -51
  49. package/src/ble/index.ts +7 -7
  50. package/src/ble/web-ble-transport.test.ts +93 -84
  51. package/src/ble/web-ble-transport.ts +70 -57
  52. package/src/ble/web-bluetooth.d.ts +19 -19
  53. package/src/canonical-json.test.ts +301 -285
  54. package/src/crypto-directional.test.ts +155 -129
  55. package/src/crypto-hardening.test.ts +292 -283
  56. package/src/crypto.test.ts +364 -346
  57. package/src/crypto.ts +185 -175
  58. package/src/dapp-session.test.ts +522 -385
  59. package/src/dapp-session.ts +17 -11
  60. package/src/emitter.test.ts +122 -122
  61. package/src/emitter.ts +20 -18
  62. package/src/evm/eip1193.test.ts +283 -205
  63. package/src/evm/eip1193.ts +162 -138
  64. package/src/evm/index.ts +5 -5
  65. package/src/evm/wagmi.test.ts +1 -1
  66. package/src/integration.test.ts +329 -201
  67. package/src/security.test.ts +331 -238
  68. package/src/sequence-validation.test.ts +6 -9
  69. package/src/test-helpers.ts +102 -78
  70. package/src/types.test.ts +45 -50
  71. package/src/wallet-session.test.ts +611 -383
  72. package/src/wallet-session.ts +7 -9
  73. package/src/ws-transport.test.ts +141 -139
  74. package/src/ws-transport.ts +52 -41
@@ -8,29 +8,20 @@
8
8
  * Threat model reference: Protocol spec Section 19.
9
9
  */
10
10
 
11
- import { describe, it, expect, vi } from 'vitest';
12
- import { DAppSession } from '../../dapp-session.js';
13
- import { WalletSession } from '../../wallet-session.js';
14
- import { MockTransport, MockRelay, makeJoinBody, makeSealedJoin } from '../../test-helpers.js';
11
+ import { describe, expect, it, vi } from 'vitest'
15
12
  import {
16
- generateX25519KeyPair,
13
+ buildPairingUri,
17
14
  generateChannelId,
18
- computeSharedSecret,
19
- deriveSessionKey,
20
- deriveJoinEncryptionKey,
21
- deriveDirectionalSessionKeys,
15
+ generateX25519KeyPair,
22
16
  sealPayload,
23
- unsealPayload,
24
- sealJoin,
25
- b64urlEncode,
26
- b64urlDecode,
27
- buildPairingUri,
28
- parsePairingUri,
29
- } from '../../crypto.js';
30
- import type { ProtocolMessage } from '../../types.js';
17
+ } from '../../crypto.js'
18
+ import { DAppSession } from '../../dapp-session.js'
19
+ import { MockTransport, makeJoinBody, makeSealedJoin } from '../../test-helpers.js'
20
+ import type { CloseMessage, ProtocolMessage, RequestMessage } from '../../types.js'
21
+ import { WalletSession } from '../../wallet-session.js'
31
22
 
32
23
  function wait(ms = 50): Promise<void> {
33
- return new Promise((r) => setTimeout(r, ms));
24
+ return new Promise((r) => setTimeout(r, ms))
34
25
  }
35
26
 
36
27
  // ---------------------------------------------------------------------------
@@ -38,34 +29,45 @@ function wait(ms = 50): Promise<void> {
38
29
  // ---------------------------------------------------------------------------
39
30
 
40
31
  function setupDAppManual() {
41
- const transport = new MockTransport();
32
+ const transport = new MockTransport()
42
33
  const session = new DAppSession({
43
34
  transport,
44
- meta: { name: 'Test', description: 'Test dApp', url: 'https://test.com', icon: 'https://test.com/icon.png' },
45
- });
46
- const walletKp = generateX25519KeyPair();
47
- return { transport, session, walletKp };
35
+ meta: {
36
+ name: 'Test',
37
+ description: 'Test dApp',
38
+ url: 'https://test.com',
39
+ icon: 'https://test.com/icon.png',
40
+ },
41
+ })
42
+ const walletKp = generateX25519KeyPair()
43
+ return { transport, session, walletKp }
48
44
  }
49
45
 
50
46
  async function connectDAppManual(ctx: ReturnType<typeof setupDAppManual>) {
51
- const { transport, session, walletKp } = ctx;
52
- await session.createPairing();
47
+ const { transport, session, walletKp } = ctx
48
+ await session.createPairing()
53
49
 
54
50
  transport.receive({
55
- v: 1, t: 'join', ch: session.channelId,
56
- ts: Date.now(), from: walletKp.publicKeyB64,
57
- body: makeJoinBody(session.channelId, transport.sent[0]!.from!, walletKp),
58
- } as ProtocolMessage);
51
+ v: 1,
52
+ t: 'join',
53
+ ch: session.channelId,
54
+ ts: Date.now(),
55
+ from: walletKp.publicKeyB64,
56
+ body: makeJoinBody(session.channelId, transport.sent[0]?.from ?? '', walletKp),
57
+ } as ProtocolMessage)
59
58
 
60
59
  transport.receive({
61
- v: 1, t: 'ready', ch: session.channelId,
62
- ts: Date.now(), from: '_adapter',
60
+ v: 1,
61
+ t: 'ready',
62
+ ch: session.channelId,
63
+ ts: Date.now(),
64
+ from: '_adapter',
63
65
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
64
- } as ProtocolMessage);
66
+ } as ProtocolMessage)
65
67
 
66
- const recvKey = (session as any).recvKey as Uint8Array;
67
- const dappPubB64 = transport.sent[0]!.from!;
68
- return { recvKey, dappPubB64 };
68
+ const recvKey = (session as unknown as Record<string, unknown>).recvKey as Uint8Array
69
+ const dappPubB64 = transport.sent[0]?.from ?? ''
70
+ return { recvKey, dappPubB64 }
69
71
  }
70
72
 
71
73
  // ---------------------------------------------------------------------------
@@ -84,46 +86,54 @@ describe('Malicious Relay: Public key substitution in join', () => {
84
86
  // PREVENTS: Man-in-the-middle — relay cannot impersonate the wallet
85
87
  // because sealed_join is bound to the wallet's key pair.
86
88
 
87
- const transport = new MockTransport();
89
+ const transport = new MockTransport()
88
90
  const session = new DAppSession({
89
91
  transport,
90
- meta: { name: 'Test', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
92
+ meta: {
93
+ name: 'Test',
94
+ description: 'Test',
95
+ url: 'https://test.com',
96
+ icon: 'https://test.com/icon.png',
97
+ },
91
98
  autoAccept: true,
92
- });
99
+ })
93
100
 
94
- const errorHandler = vi.fn();
95
- session.on('error', errorHandler);
101
+ const errorHandler = vi.fn()
102
+ session.on('error', errorHandler)
96
103
 
97
- await session.createPairing();
98
- const dappPubB64 = transport.sent[0]!.from!;
104
+ await session.createPairing()
105
+ const dappPubB64 = transport.sent[0]?.from ?? ''
99
106
 
100
107
  // Real wallet generates sealed_join with its own key pair
101
- const realWalletKp = generateX25519KeyPair();
102
- const sealedJoin = makeSealedJoin(session.channelId, dappPubB64, realWalletKp);
108
+ const realWalletKp = generateX25519KeyPair()
109
+ const sealedJoin = makeSealedJoin(session.channelId, dappPubB64, realWalletKp)
103
110
 
104
111
  // Relay substitutes a DIFFERENT key in the "from" field
105
- const relayFakeKp = generateX25519KeyPair();
112
+ const relayFakeKp = generateX25519KeyPair()
106
113
 
107
114
  transport.receive({
108
- v: 1, t: 'join', ch: session.channelId,
109
- ts: Date.now(), from: relayFakeKp.publicKeyB64, // substituted key!
115
+ v: 1,
116
+ t: 'join',
117
+ ch: session.channelId,
118
+ ts: Date.now(),
119
+ from: relayFakeKp.publicKeyB64, // substituted key!
110
120
  body: { sealed_join: sealedJoin }, // sealed with real wallet's key
111
- } as ProtocolMessage);
121
+ } as ProtocolMessage)
112
122
 
113
- await wait();
123
+ await wait()
114
124
 
115
125
  // DApp should fail to decrypt sealed_join because it derives keys
116
126
  // using the fake relay key, which doesn't match the sealed_join
117
- expect(errorHandler).toHaveBeenCalled();
118
- const errorMsg = errorHandler.mock.calls[0]?.[0]?.message;
119
- expect(errorMsg).toContain('decrypt');
127
+ expect(errorHandler).toHaveBeenCalled()
128
+ const errorMsg = errorHandler.mock.calls[0]?.[0]?.message
129
+ expect(errorMsg).toContain('decrypt')
120
130
 
121
131
  // DApp should have sent close with decryption_failed
122
- const closeMsg = transport.sent.find(m => m.t === 'close') as any;
123
- expect(closeMsg).toBeTruthy();
124
- expect(closeMsg.body.reason).toBe('decryption_failed');
125
- });
126
- });
132
+ const closeMsg = transport.sent.find((m) => m.t === 'close') as CloseMessage | undefined
133
+ expect(closeMsg).toBeTruthy()
134
+ expect(closeMsg?.body.reason).toBe('decryption_failed')
135
+ })
136
+ })
127
137
 
128
138
  // ---------------------------------------------------------------------------
129
139
  // Attack 2: Relay replays an old sealed message
@@ -139,96 +149,119 @@ describe('Malicious Relay: Message replay', () => {
139
149
  // PREVENTS: Replay attacks that could cause duplicate signing or
140
150
  // duplicate transaction submission.
141
151
 
142
- const ctx = setupDAppManual();
143
- const { transport, session, walletKp } = ctx;
144
- const { recvKey } = await connectDAppManual(ctx);
152
+ const ctx = setupDAppManual()
153
+ const { transport, session, walletKp } = ctx
154
+ const { recvKey } = await connectDAppManual(ctx)
145
155
 
146
156
  // First request succeeds with seq=0
147
- const p0 = session.request('wallet_getAccounts');
148
- await wait(20);
149
- const req0 = transport.sent.find(m => m.t === 'req') as any;
150
- const req0Id = req0.body.id;
157
+ const p0 = session.request('wallet_getAccounts')
158
+ await wait(20)
159
+ const req0 = transport.sent.find((m) => m.t === 'req') as RequestMessage | undefined
160
+ const req0Id = req0?.body.id ?? ''
151
161
 
152
162
  const validSealed = sealPayload(
153
- recvKey, session.channelId, 0,
163
+ recvKey,
164
+ session.channelId,
165
+ 0,
154
166
  { _ok: true, _result: ['0xAddr'] },
155
167
  { type: 'res', from: walletKp.publicKeyB64, id: req0Id },
156
- );
168
+ )
157
169
 
158
170
  transport.receive({
159
- v: 1, t: 'res', ch: session.channelId,
160
- ts: Date.now(), from: walletKp.publicKeyB64,
171
+ v: 1,
172
+ t: 'res',
173
+ ch: session.channelId,
174
+ ts: Date.now(),
175
+ from: walletKp.publicKeyB64,
161
176
  body: { id: req0Id, sealed: validSealed },
162
- } as ProtocolMessage);
163
- expect(await p0).toEqual(['0xAddr']);
177
+ } as ProtocolMessage)
178
+ expect(await p0).toEqual(['0xAddr'])
164
179
 
165
180
  // Second request: relay replays the SAME sealed payload (seq=0)
166
- const p1 = session.request('wallet_getAccounts');
167
- await wait(20);
168
- const req1 = transport.sent.filter(m => m.t === 'req')[1] as any;
169
- const req1Id = req1.body.id;
181
+ const p1 = session.request('wallet_getAccounts')
182
+ await wait(20)
183
+ const req1 = transport.sent.filter((m) => m.t === 'req')[1] as RequestMessage | undefined
184
+ const req1Id = req1?.body.id ?? ''
170
185
 
171
186
  // Relay creates a new envelope but copies the old sealed (seq=0)
172
187
  // Note: the AAD won't match because id differs, so it fails at AEAD level.
173
188
  // For a more precise replay, the relay would need the same req.id which
174
189
  // the idempotency cache would handle. Either way, the attack fails.
175
190
  transport.receive({
176
- v: 1, t: 'res', ch: session.channelId,
177
- ts: Date.now(), from: walletKp.publicKeyB64,
178
- body: { id: req1Id, sealed: sealPayload(
179
- recvKey, session.channelId, 0, // replayed seq=0
180
- { _ok: true, _result: ['0xAddr'] },
181
- { type: 'res', from: walletKp.publicKeyB64, id: req1Id },
182
- )},
183
- } as ProtocolMessage);
184
- await expect(p1).rejects.toThrow('Replay detected');
185
- });
191
+ v: 1,
192
+ t: 'res',
193
+ ch: session.channelId,
194
+ ts: Date.now(),
195
+ from: walletKp.publicKeyB64,
196
+ body: {
197
+ id: req1Id,
198
+ sealed: sealPayload(
199
+ recvKey,
200
+ session.channelId,
201
+ 0, // replayed seq=0
202
+ { _ok: true, _result: ['0xAddr'] },
203
+ { type: 'res', from: walletKp.publicKeyB64, id: req1Id },
204
+ ),
205
+ },
206
+ } as ProtocolMessage)
207
+ await expect(p1).rejects.toThrow('Replay detected')
208
+ })
186
209
 
187
210
  it('replayed event with old seq is silently dropped', async () => {
188
211
  // ATTACK: Relay replays a previously captured event to confuse the dApp.
189
212
  //
190
213
  // PREVENTS: Stale event injection.
191
214
 
192
- const ctx = setupDAppManual();
193
- const { transport, session, walletKp } = ctx;
194
- const { recvKey } = await connectDAppManual(ctx);
215
+ const ctx = setupDAppManual()
216
+ const { transport, session, walletKp } = ctx
217
+ const { recvKey } = await connectDAppManual(ctx)
195
218
 
196
- const eventHandler = vi.fn();
197
- session.on('event', eventHandler);
219
+ const eventHandler = vi.fn()
220
+ session.on('event', eventHandler)
198
221
 
199
222
  // First event at seq=0 accepted
200
223
  transport.receive({
201
- v: 1, t: 'evt', ch: session.channelId,
202
- ts: Date.now(), from: walletKp.publicKeyB64,
224
+ v: 1,
225
+ t: 'evt',
226
+ ch: session.channelId,
227
+ ts: Date.now(),
228
+ from: walletKp.publicKeyB64,
203
229
  body: {
204
230
  id: 'evt-1',
205
231
  sealed: sealPayload(
206
- recvKey, session.channelId, 0,
232
+ recvKey,
233
+ session.channelId,
234
+ 0,
207
235
  { _event: 'accountsChanged', accounts: ['0xA'] },
208
236
  { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-1' },
209
237
  ),
210
238
  },
211
- } as ProtocolMessage);
212
- await wait();
213
- expect(eventHandler).toHaveBeenCalledTimes(1);
239
+ } as ProtocolMessage)
240
+ await wait()
241
+ expect(eventHandler).toHaveBeenCalledTimes(1)
214
242
 
215
243
  // Replay same seq=0 — should be silently dropped
216
244
  transport.receive({
217
- v: 1, t: 'evt', ch: session.channelId,
218
- ts: Date.now(), from: walletKp.publicKeyB64,
245
+ v: 1,
246
+ t: 'evt',
247
+ ch: session.channelId,
248
+ ts: Date.now(),
249
+ from: walletKp.publicKeyB64,
219
250
  body: {
220
251
  id: 'evt-replay',
221
252
  sealed: sealPayload(
222
- recvKey, session.channelId, 0,
253
+ recvKey,
254
+ session.channelId,
255
+ 0,
223
256
  { _event: 'accountsChanged', accounts: ['0xEvil'] },
224
257
  { type: 'evt', from: walletKp.publicKeyB64, id: 'evt-replay' },
225
258
  ),
226
259
  },
227
- } as ProtocolMessage);
228
- await wait();
229
- expect(eventHandler).toHaveBeenCalledTimes(1); // still 1, replay dropped
230
- });
231
- });
260
+ } as ProtocolMessage)
261
+ await wait()
262
+ expect(eventHandler).toHaveBeenCalledTimes(1) // still 1, replay dropped
263
+ })
264
+ })
232
265
 
233
266
  // ---------------------------------------------------------------------------
234
267
  // Attack 3: Relay reflects dApp's own message back as wallet message
@@ -245,30 +278,33 @@ describe('Malicious Relay: Reflection attack', () => {
245
278
  // messages back — directional keys ensure each direction uses a
246
279
  // unique key (Section 6.2).
247
280
 
248
- const ctx = setupDAppManual();
249
- const { transport, session, walletKp } = ctx;
250
- await connectDAppManual(ctx);
281
+ const ctx = setupDAppManual()
282
+ const { transport, session, walletKp } = ctx
283
+ await connectDAppManual(ctx)
251
284
 
252
- const p = session.request('wallet_getAccounts');
253
- await wait(20);
285
+ const p = session.request('wallet_getAccounts')
286
+ await wait(20)
254
287
 
255
288
  // Capture the outbound req
256
- const reqMsg = transport.sent.find(m => m.t === 'req') as any;
257
- const reqId = reqMsg.body.id;
258
- const reqSealed = reqMsg.body.sealed;
289
+ const reqMsg = transport.sent.find((m) => m.t === 'req') as RequestMessage | undefined
290
+ const reqId = reqMsg?.body.id ?? ''
291
+ const reqSealed = reqMsg?.body.sealed ?? ''
259
292
 
260
293
  // Relay reflects the req's sealed payload back as a res
261
294
  transport.receive({
262
- v: 1, t: 'res', ch: session.channelId,
263
- ts: Date.now(), from: walletKp.publicKeyB64,
295
+ v: 1,
296
+ t: 'res',
297
+ ch: session.channelId,
298
+ ts: Date.now(),
299
+ from: walletKp.publicKeyB64,
264
300
  body: { id: reqId, sealed: reqSealed }, // reflected sealed!
265
- } as ProtocolMessage);
301
+ } as ProtocolMessage)
266
302
 
267
303
  // Decryption must fail — dApp uses walletToDappKey to decrypt res,
268
304
  // but the sealed payload was encrypted with dappToWalletKey
269
- await expect(p).rejects.toThrow('Decryption failed');
270
- });
271
- });
305
+ await expect(p).rejects.toThrow('Decryption failed')
306
+ })
307
+ })
272
308
 
273
309
  // ---------------------------------------------------------------------------
274
310
  // Attack 4: Relay sends terminate with fake reasons (DoS)
@@ -285,37 +321,43 @@ describe('Malicious Relay: Fake terminate', () => {
285
321
  // the session transitions cleanly to closed state without leaking
286
322
  // any key material or corrupting state.
287
323
 
288
- const ctx = setupDAppManual();
289
- const { transport, session } = ctx;
290
- await connectDAppManual(ctx);
324
+ const ctx = setupDAppManual()
325
+ const { transport, session } = ctx
326
+ await connectDAppManual(ctx)
291
327
 
292
- expect(session.phase).toBe('connected');
328
+ expect(session.phase).toBe('connected')
293
329
 
294
330
  // Relay sends a fake terminate
295
331
  transport.receive({
296
- v: 1, t: 'terminate', ch: session.channelId,
297
- ts: Date.now(), from: '_adapter',
332
+ v: 1,
333
+ t: 'terminate',
334
+ ch: session.channelId,
335
+ ts: Date.now(),
336
+ from: '_adapter',
298
337
  body: { reason: 'rate_limited' },
299
- } as ProtocolMessage);
338
+ } as ProtocolMessage)
300
339
 
301
- expect(session.phase).toBe('closed');
302
- });
340
+ expect(session.phase).toBe('closed')
341
+ })
303
342
 
304
343
  it('terminate does not prevent session from being properly destroyed', async () => {
305
- const ctx = setupDAppManual();
306
- const { transport, session } = ctx;
307
- await connectDAppManual(ctx);
344
+ const ctx = setupDAppManual()
345
+ const { transport, session } = ctx
346
+ await connectDAppManual(ctx)
308
347
 
309
348
  transport.receive({
310
- v: 1, t: 'terminate', ch: session.channelId,
311
- ts: Date.now(), from: '_adapter',
349
+ v: 1,
350
+ t: 'terminate',
351
+ ch: session.channelId,
352
+ ts: Date.now(),
353
+ from: '_adapter',
312
354
  body: { reason: 'timeout' },
313
- } as ProtocolMessage);
355
+ } as ProtocolMessage)
314
356
 
315
357
  // Should not throw — session should be cleanly closeable
316
- expect(() => session.destroy()).not.toThrow();
317
- });
318
- });
358
+ expect(() => session.destroy()).not.toThrow()
359
+ })
360
+ })
319
361
 
320
362
  // ---------------------------------------------------------------------------
321
363
  // Attack 5: Relay drops accept -> dApp times out
@@ -329,37 +371,45 @@ describe('Malicious Relay: Dropped accept', () => {
329
371
  // PREVENTS: Session hang from a relay that silently drops messages.
330
372
  // The dApp should time out from pending_accept phase.
331
373
 
332
- const transport = new MockTransport();
374
+ const transport = new MockTransport()
333
375
  const session = new DAppSession({
334
376
  transport,
335
- meta: { name: 'Test', description: 'Test', url: 'https://test.com', icon: 'https://test.com/icon.png' },
377
+ meta: {
378
+ name: 'Test',
379
+ description: 'Test',
380
+ url: 'https://test.com',
381
+ icon: 'https://test.com/icon.png',
382
+ },
336
383
  autoAccept: true,
337
384
  requestTimeout: 200, // short timeout for test
338
- });
385
+ })
339
386
 
340
- await session.createPairing();
341
- const dappPubB64 = transport.sent[0]!.from!;
387
+ await session.createPairing()
388
+ const dappPubB64 = transport.sent[0]?.from ?? ''
342
389
 
343
- const walletKp = generateX25519KeyPair();
390
+ const walletKp = generateX25519KeyPair()
344
391
 
345
392
  // Wallet joins
346
393
  transport.receive({
347
- v: 1, t: 'join', ch: session.channelId,
348
- ts: Date.now(), from: walletKp.publicKeyB64,
394
+ v: 1,
395
+ t: 'join',
396
+ ch: session.channelId,
397
+ ts: Date.now(),
398
+ from: walletKp.publicKeyB64,
349
399
  body: makeJoinBody(session.channelId, dappPubB64, walletKp),
350
- } as ProtocolMessage);
400
+ } as ProtocolMessage)
351
401
 
352
- await wait();
402
+ await wait()
353
403
  // dApp auto-accepts and sends accept message
354
- const acceptMsg = transport.sent.find(m => m.t === 'accept');
355
- expect(acceptMsg).toBeTruthy();
404
+ const acceptMsg = transport.sent.find((m) => m.t === 'accept')
405
+ expect(acceptMsg).toBeTruthy()
356
406
 
357
407
  // But relay NEVER sends ready.connected
358
408
  // Session should still be in pending_accept, not connected
359
409
  // Trying to send a request while not connected should fail immediately
360
- await expect(session.request('wallet_getAccounts')).rejects.toThrow('Not connected');
361
- });
362
- });
410
+ await expect(session.request('wallet_getAccounts')).rejects.toThrow('Not connected')
411
+ })
412
+ })
363
413
 
364
414
  // ---------------------------------------------------------------------------
365
415
  // Attack 6: Relay sends ready.connected with wrong remote key
@@ -375,79 +425,90 @@ describe('Malicious Relay: Wrong remote key in ready.connected', () => {
375
425
  // PREVENTS: Relay routing a different peer into an established
376
426
  // handshake after the key exchange has already occurred.
377
427
 
378
- const ctx = setupDAppManual();
379
- const { transport, session, walletKp } = ctx;
428
+ const ctx = setupDAppManual()
429
+ const { transport, session, walletKp } = ctx
380
430
 
381
- await session.createPairing();
382
- const dappPubB64 = transport.sent[0]!.from!;
431
+ await session.createPairing()
432
+ const dappPubB64 = transport.sent[0]?.from ?? ''
383
433
 
384
434
  // Wallet joins — dApp derives keys using walletKp
385
435
  transport.receive({
386
- v: 1, t: 'join', ch: session.channelId,
387
- ts: Date.now(), from: walletKp.publicKeyB64,
436
+ v: 1,
437
+ t: 'join',
438
+ ch: session.channelId,
439
+ ts: Date.now(),
440
+ from: walletKp.publicKeyB64,
388
441
  body: makeJoinBody(session.channelId, dappPubB64, walletKp),
389
- } as ProtocolMessage);
442
+ } as ProtocolMessage)
390
443
 
391
- await wait();
444
+ await wait()
392
445
 
393
- const errorHandler = vi.fn();
394
- session.on('error', errorHandler);
446
+ const errorHandler = vi.fn()
447
+ session.on('error', errorHandler)
395
448
 
396
449
  // Relay sends ready.connected with a DIFFERENT remote key
397
- const fakeRemoteKp = generateX25519KeyPair();
450
+ const fakeRemoteKp = generateX25519KeyPair()
398
451
  transport.receive({
399
- v: 1, t: 'ready', ch: session.channelId,
400
- ts: Date.now(), from: '_adapter',
452
+ v: 1,
453
+ t: 'ready',
454
+ ch: session.channelId,
455
+ ts: Date.now(),
456
+ from: '_adapter',
401
457
  body: { state: 'connected', reconnect: false, remote: fakeRemoteKp.publicKeyB64 },
402
- } as ProtocolMessage);
458
+ } as ProtocolMessage)
403
459
 
404
- await wait();
460
+ await wait()
405
461
 
406
462
  // DApp should reject and close
407
- expect(errorHandler).toHaveBeenCalled();
408
- expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('remote does not match');
409
- expect(session.phase).toBe('closed');
410
- });
463
+ expect(errorHandler).toHaveBeenCalled()
464
+ expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('remote does not match')
465
+ expect(session.phase).toBe('closed')
466
+ })
411
467
 
412
468
  it('wallet rejects ready.connected when remote does not match paired dApp', async () => {
413
469
  // Same attack from the wallet side: relay lies about who the remote is.
414
470
 
415
- const transport = new MockTransport();
416
- const dappKp = generateX25519KeyPair();
417
- const channelId = generateChannelId();
471
+ const transport = new MockTransport()
472
+ const dappKp = generateX25519KeyPair()
473
+ const channelId = generateChannelId()
418
474
 
419
475
  const session = new WalletSession({
420
476
  transport,
421
477
  meta: { name: 'W', description: 'W', url: 'https://w.test', icon: 'https://w.test/i.png' },
422
478
  capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
423
- });
479
+ })
424
480
 
425
481
  const uri = buildPairingUri({
426
482
  channelId,
427
483
  pubkeyB64: dappKp.publicKeyB64,
428
484
  relayUrl: 'ws://localhost/v1',
429
- name: 'D', url: 'https://d.test', icon: 'https://d.test/i.png',
430
- });
431
- await session.joinFromUri(uri);
485
+ name: 'D',
486
+ url: 'https://d.test',
487
+ icon: 'https://d.test/i.png',
488
+ })
489
+ await session.joinFromUri(uri)
432
490
 
433
- const errorHandler = vi.fn();
434
- session.on('error', errorHandler);
491
+ const errorHandler = vi.fn()
492
+ session.on('error', errorHandler)
435
493
 
436
494
  // Relay sends ready.connected with wrong remote
437
- const fakeKp = generateX25519KeyPair();
495
+ const fakeKp = generateX25519KeyPair()
438
496
  transport.receive({
439
- v: 1, t: 'ready', ch: channelId,
440
- ts: Date.now(), from: '_adapter',
497
+ v: 1,
498
+ t: 'ready',
499
+ ch: channelId,
500
+ ts: Date.now(),
501
+ from: '_adapter',
441
502
  body: { state: 'connected', reconnect: false, remote: fakeKp.publicKeyB64 },
442
- } as ProtocolMessage);
503
+ } as ProtocolMessage)
443
504
 
444
- await wait();
505
+ await wait()
445
506
 
446
- expect(errorHandler).toHaveBeenCalled();
447
- expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('remote does not match');
448
- expect(session.phase).toBe('closed');
449
- });
450
- });
507
+ expect(errorHandler).toHaveBeenCalled()
508
+ expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('remote does not match')
509
+ expect(session.phase).toBe('closed')
510
+ })
511
+ })
451
512
 
452
513
  // ---------------------------------------------------------------------------
453
514
  // Attack 7: Relay forges ping/pong with fake from
@@ -461,24 +522,27 @@ describe('Malicious Relay: Forged ping/pong', () => {
461
522
  //
462
523
  // PREVENTS: Adapter impersonation in non-adapter message types.
463
524
 
464
- const ctx = setupDAppManual();
465
- const { transport, session } = ctx;
466
- await connectDAppManual(ctx);
525
+ const ctx = setupDAppManual()
526
+ const { transport, session } = ctx
527
+ await connectDAppManual(ctx)
467
528
 
468
- const errorHandler = vi.fn();
469
- session.on('error', errorHandler);
529
+ const errorHandler = vi.fn()
530
+ session.on('error', errorHandler)
470
531
 
471
532
  transport.receive({
472
- v: 1, t: 'ping', ch: session.channelId,
473
- ts: Date.now(), from: '_adapter', // spoofed!
533
+ v: 1,
534
+ t: 'ping',
535
+ ch: session.channelId,
536
+ ts: Date.now(),
537
+ from: '_adapter', // spoofed!
474
538
  body: {},
475
- } as ProtocolMessage);
539
+ } as ProtocolMessage)
476
540
 
477
- await wait();
541
+ await wait()
478
542
 
479
- expect(errorHandler).toHaveBeenCalled();
480
- expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('spoofed _adapter');
481
- });
543
+ expect(errorHandler).toHaveBeenCalled()
544
+ expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('spoofed _adapter')
545
+ })
482
546
 
483
547
  it('ping/pong from unknown peer does not compromise encrypted state', async () => {
484
548
  // ATTACK: Relay injects a ping from a random key. Since ping/pong
@@ -487,42 +551,50 @@ describe('Malicious Relay: Forged ping/pong', () => {
487
551
  //
488
552
  // PREVENTS: Verifies that heartbeat messages cannot leak secrets.
489
553
 
490
- const ctx = setupDAppManual();
491
- const { transport, session, walletKp } = ctx;
492
- const { recvKey } = await connectDAppManual(ctx);
554
+ const ctx = setupDAppManual()
555
+ const { transport, session, walletKp } = ctx
556
+ const { recvKey } = await connectDAppManual(ctx)
493
557
 
494
- const unknownKp = generateX25519KeyPair();
558
+ const unknownKp = generateX25519KeyPair()
495
559
 
496
560
  transport.receive({
497
- v: 1, t: 'ping', ch: session.channelId,
498
- ts: Date.now(), from: unknownKp.publicKeyB64,
561
+ v: 1,
562
+ t: 'ping',
563
+ ch: session.channelId,
564
+ ts: Date.now(),
565
+ from: unknownKp.publicKeyB64,
499
566
  body: {},
500
- } as ProtocolMessage);
567
+ } as ProtocolMessage)
501
568
 
502
- await wait();
569
+ await wait()
503
570
 
504
571
  // DApp may reply with pong (no security issue) — verify session still works
505
- expect(session.phase).toBe('connected');
572
+ expect(session.phase).toBe('connected')
506
573
 
507
574
  // Verify encrypted communication still works after the forged ping
508
- const p = session.request('wallet_getAccounts');
509
- await wait(20);
510
- const req = transport.sent.filter(m => m.t === 'req').pop() as any;
511
- const reqId = req.body.id;
575
+ const p = session.request('wallet_getAccounts')
576
+ await wait(20)
577
+ const req = transport.sent.filter((m) => m.t === 'req').pop() as RequestMessage | undefined
578
+ const reqId = req?.body.id ?? ''
512
579
 
513
580
  transport.receive({
514
- v: 1, t: 'res', ch: session.channelId,
515
- ts: Date.now(), from: walletKp.publicKeyB64,
581
+ v: 1,
582
+ t: 'res',
583
+ ch: session.channelId,
584
+ ts: Date.now(),
585
+ from: walletKp.publicKeyB64,
516
586
  body: {
517
587
  id: reqId,
518
588
  sealed: sealPayload(
519
- recvKey, session.channelId, 0,
589
+ recvKey,
590
+ session.channelId,
591
+ 0,
520
592
  { _ok: true, _result: 'still-works' },
521
593
  { type: 'res', from: walletKp.publicKeyB64, id: reqId },
522
594
  ),
523
595
  },
524
- } as ProtocolMessage);
596
+ } as ProtocolMessage)
525
597
 
526
- expect(await p).toBe('still-works');
527
- });
528
- });
598
+ expect(await p).toBe('still-works')
599
+ })
600
+ })