walletpair-sdk 1.0.2 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +13 -0
  2. package/dist/ble/framing.d.ts.map +1 -1
  3. package/dist/ble/framing.js +2 -2
  4. package/dist/ble/framing.js.map +1 -1
  5. package/dist/ble/index.d.ts +2 -2
  6. package/dist/ble/index.d.ts.map +1 -1
  7. package/dist/ble/index.js +2 -2
  8. package/dist/ble/index.js.map +1 -1
  9. package/dist/ble/web-ble-transport.d.ts +1 -1
  10. package/dist/ble/web-ble-transport.d.ts.map +1 -1
  11. package/dist/ble/web-ble-transport.js +23 -12
  12. package/dist/ble/web-ble-transport.js.map +1 -1
  13. package/dist/crypto.d.ts.map +1 -1
  14. package/dist/crypto.js +29 -12
  15. package/dist/crypto.js.map +1 -1
  16. package/dist/dapp-session.d.ts.map +1 -1
  17. package/dist/dapp-session.js +15 -5
  18. package/dist/dapp-session.js.map +1 -1
  19. package/dist/emitter.d.ts +1 -3
  20. package/dist/emitter.d.ts.map +1 -1
  21. package/dist/emitter.js +4 -2
  22. package/dist/emitter.js.map +1 -1
  23. package/dist/evm/eip1193.d.ts +2 -2
  24. package/dist/evm/eip1193.d.ts.map +1 -1
  25. package/dist/evm/eip1193.js +32 -18
  26. package/dist/evm/eip1193.js.map +1 -1
  27. package/dist/evm/index.d.ts +2 -2
  28. package/dist/evm/index.d.ts.map +1 -1
  29. package/dist/evm/index.js.map +1 -1
  30. package/dist/wallet-session.d.ts.map +1 -1
  31. package/dist/wallet-session.js +4 -3
  32. package/dist/wallet-session.js.map +1 -1
  33. package/dist/ws-transport.d.ts +3 -2
  34. package/dist/ws-transport.d.ts.map +1 -1
  35. package/dist/ws-transport.js +13 -4
  36. package/dist/ws-transport.js.map +1 -1
  37. package/package.json +20 -1
  38. package/src/__tests__/adversarial/crypto-attacks.test.ts +240 -233
  39. package/src/__tests__/adversarial/malicious-dapp.test.ts +228 -194
  40. package/src/__tests__/adversarial/malicious-relay.test.ts +292 -220
  41. package/src/__tests__/adversarial/malicious-wallet.test.ts +246 -180
  42. package/src/__tests__/spec-compliance/canonical-json.test.ts +105 -105
  43. package/src/__tests__/spec-compliance/crypto-vectors.test.ts +149 -154
  44. package/src/__tests__/spec-compliance/message-format.test.ts +180 -151
  45. package/src/__tests__/spec-compliance/sequence-numbers.test.ts +142 -149
  46. package/src/__tests__/spec-compliance/state-machine.test.ts +203 -180
  47. package/src/ble/framing.test.ts +122 -114
  48. package/src/ble/framing.ts +48 -51
  49. package/src/ble/index.ts +7 -7
  50. package/src/ble/web-ble-transport.test.ts +93 -84
  51. package/src/ble/web-ble-transport.ts +70 -57
  52. package/src/ble/web-bluetooth.d.ts +19 -19
  53. package/src/canonical-json.test.ts +301 -285
  54. package/src/crypto-directional.test.ts +155 -129
  55. package/src/crypto-hardening.test.ts +292 -283
  56. package/src/crypto.test.ts +364 -346
  57. package/src/crypto.ts +185 -175
  58. package/src/dapp-session.test.ts +522 -385
  59. package/src/dapp-session.ts +17 -11
  60. package/src/emitter.test.ts +122 -122
  61. package/src/emitter.ts +20 -18
  62. package/src/evm/eip1193.test.ts +283 -205
  63. package/src/evm/eip1193.ts +162 -138
  64. package/src/evm/index.ts +5 -5
  65. package/src/evm/wagmi.test.ts +1 -1
  66. package/src/integration.test.ts +329 -201
  67. package/src/security.test.ts +331 -238
  68. package/src/sequence-validation.test.ts +6 -9
  69. package/src/test-helpers.ts +102 -78
  70. package/src/types.test.ts +45 -50
  71. package/src/wallet-session.test.ts +611 -383
  72. package/src/wallet-session.ts +7 -9
  73. package/src/ws-transport.test.ts +141 -139
  74. package/src/ws-transport.ts +52 -41
@@ -10,26 +10,24 @@
10
10
  * malicious dApp.
11
11
  */
12
12
 
13
- import { describe, it, expect, vi } from 'vitest';
14
- import { DAppSession } from '../../dapp-session.js';
15
- import { WalletSession } from '../../wallet-session.js';
16
- import { MockTransport, MockRelay, makeJoinBody } from '../../test-helpers.js';
13
+ import { describe, expect, it, vi } from 'vitest'
17
14
  import {
18
- generateX25519KeyPair,
19
- generateChannelId,
15
+ b64urlDecode,
16
+ buildPairingUri,
20
17
  computeSharedSecret,
21
- deriveSessionKey,
22
18
  deriveDirectionalSessionKeys,
19
+ deriveSessionKey,
20
+ generateChannelId,
21
+ generateX25519KeyPair,
23
22
  sealPayload,
24
- unsealPayload,
25
- b64urlEncode,
26
- b64urlDecode,
27
- buildPairingUri,
28
- } from '../../crypto.js';
29
- import type { ProtocolMessage, Capabilities } from '../../types.js';
23
+ } from '../../crypto.js'
24
+ import { DAppSession } from '../../dapp-session.js'
25
+ import { MockRelay, MockTransport, makeJoinBody } from '../../test-helpers.js'
26
+ import type { Capabilities, ProtocolMessage } from '../../types.js'
27
+ import { WalletSession } from '../../wallet-session.js'
30
28
 
31
29
  function wait(ms = 50): Promise<void> {
32
- return new Promise((r) => setTimeout(r, ms));
30
+ return new Promise((r) => setTimeout(r, ms))
33
31
  }
34
32
 
35
33
  // ---------------------------------------------------------------------------
@@ -37,59 +35,69 @@ function wait(ms = 50): Promise<void> {
37
35
  // ---------------------------------------------------------------------------
38
36
 
39
37
  function setupWalletManual(caps?: Capabilities) {
40
- const transport = new MockTransport();
41
- const dappKp = generateX25519KeyPair();
42
- const channelId = generateChannelId();
38
+ const transport = new MockTransport()
39
+ const dappKp = generateX25519KeyPair()
40
+ const channelId = generateChannelId()
43
41
  const session = new WalletSession({
44
42
  transport,
45
- meta: { name: 'W', description: 'Wallet', url: 'https://wallet.test', icon: 'https://wallet.test/i.png' },
43
+ meta: {
44
+ name: 'W',
45
+ description: 'Wallet',
46
+ url: 'https://wallet.test',
47
+ icon: 'https://wallet.test/i.png',
48
+ },
46
49
  capabilities: caps ?? {
47
50
  methods: ['wallet_getAccounts', 'wallet_signMessage'],
48
51
  events: ['accountsChanged'],
49
52
  chains: ['eip155:1'],
50
53
  },
51
- });
52
- return { transport, session, dappKp, channelId };
54
+ })
55
+ return { transport, session, dappKp, channelId }
53
56
  }
54
57
 
55
58
  async function connectWalletManual(ctx: ReturnType<typeof setupWalletManual>) {
56
- const { transport, session, dappKp, channelId } = ctx;
59
+ const { transport, session, dappKp, channelId } = ctx
57
60
  const uri = buildPairingUri({
58
61
  channelId,
59
62
  pubkeyB64: dappKp.publicKeyB64,
60
63
  relayUrl: 'ws://localhost/v1',
61
- name: 'Evil dApp', url: 'https://evil.test', icon: 'https://evil.test/i.png',
62
- });
63
- await session.joinFromUri(uri);
64
+ name: 'Evil dApp',
65
+ url: 'https://evil.test',
66
+ icon: 'https://evil.test/i.png',
67
+ })
68
+ await session.joinFromUri(uri)
64
69
 
65
70
  transport.receive({
66
- v: 1, t: 'ready', ch: channelId,
67
- ts: Date.now(), from: '_adapter',
71
+ v: 1,
72
+ t: 'ready',
73
+ ch: channelId,
74
+ ts: Date.now(),
75
+ from: '_adapter',
68
76
  body: { state: 'connected', reconnect: false, remote: dappKp.publicKeyB64 },
69
- } as ProtocolMessage);
77
+ } as ProtocolMessage)
70
78
 
71
79
  // Derive the dApp's send key (dappToWalletKey) to craft malicious requests.
72
80
  // The wallet's public key is in the join message's "from" field.
73
- const walletPubB64 = transport.sent.find(m => m.t === 'join')!.from!;
74
- const walletPubKey = b64urlDecode(walletPubB64);
75
- const shared = computeSharedSecret(dappKp.privateKey, walletPubKey);
76
- const rootKey = deriveSessionKey(shared, channelId);
81
+ const walletPubB64 = transport.sent.find((m) => m.t === 'join')?.from ?? ''
82
+ const walletPubKey = b64urlDecode(walletPubB64)
83
+ const shared = computeSharedSecret(dappKp.privateKey, walletPubKey)
84
+ const rootKey = deriveSessionKey(shared, channelId)
77
85
  const ctx2 = {
78
86
  dappPubKeyB64: dappKp.publicKeyB64,
79
87
  walletPubKeyB64: walletPubB64,
80
- capabilities: (session as any).effectiveCapabilities,
81
- walletMeta: (session as any).meta,
88
+ capabilities: (session as unknown as Record<string, unknown>).effectiveCapabilities,
89
+ walletMeta: (session as unknown as Record<string, unknown>).meta,
82
90
  dappName: 'Evil dApp',
83
- };
84
- const keys = deriveDirectionalSessionKeys(rootKey, channelId, ctx2);
85
- shared.fill(0);
86
- rootKey.fill(0);
91
+ }
92
+ const keys = deriveDirectionalSessionKeys(rootKey, channelId, ctx2)
93
+ shared.fill(0)
94
+ rootKey.fill(0)
87
95
 
88
96
  return {
89
97
  sendKey: keys.dappToWalletKey, // dApp -> wallet encryption key
90
98
  walletPubB64,
91
99
  dappPubB64: dappKp.publicKeyB64,
92
- };
100
+ }
93
101
  }
94
102
 
95
103
  function craftReq(
@@ -101,14 +109,17 @@ function craftReq(
101
109
  method: string,
102
110
  params?: Record<string, unknown>,
103
111
  ): ProtocolMessage {
104
- const payload = { _method: method, ...(params ?? {}) };
105
- const hdr = { type: 'req' as const, from: dappPubB64, id };
106
- const sealed = sealPayload(sendKey, channelId, seq, payload, hdr);
112
+ const payload = { _method: method, ...(params ?? {}) }
113
+ const hdr = { type: 'req' as const, from: dappPubB64, id }
114
+ const sealed = sealPayload(sendKey, channelId, seq, payload, hdr)
107
115
  return {
108
- v: 1, t: 'req', ch: channelId,
109
- ts: Date.now(), from: dappPubB64,
116
+ v: 1,
117
+ t: 'req',
118
+ ch: channelId,
119
+ ts: Date.now(),
120
+ from: dappPubB64,
110
121
  body: { id, sealed },
111
- } as ProtocolMessage;
122
+ } as ProtocolMessage
112
123
  }
113
124
 
114
125
  // ---------------------------------------------------------------------------
@@ -124,13 +135,13 @@ describe('Malicious DApp: Request flooding (>32 pending)', () => {
124
135
  // PREVENTS: Resource exhaustion on the wallet side. Section 15 rule 11
125
136
  // limits pending requests to 32 per channel.
126
137
 
127
- const ctx = setupWalletManual();
128
- const { transport, session, channelId } = ctx;
129
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
138
+ const ctx = setupWalletManual()
139
+ const { transport, session, channelId } = ctx
140
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
130
141
 
131
142
  // Do NOT handle requests (let them pile up as pending)
132
- const requests: Array<{ id: string; method: string }> = [];
133
- session.on('request', (req) => requests.push(req));
143
+ const requests: Array<{ id: string; method: string }> = []
144
+ session.on('request', (req) => requests.push(req))
134
145
 
135
146
  // Send 32 requests one at a time, waiting for each to be processed.
136
147
  // The transport.receive() is synchronous — it invokes handleMessage
@@ -138,27 +149,27 @@ describe('Malicious DApp: Request flooding (>32 pending)', () => {
138
149
  for (let i = 0; i < 32; i++) {
139
150
  transport.receive(
140
151
  craftReq(sendKey, channelId, dappPubB64, i, `req-${i}`, 'wallet_getAccounts'),
141
- );
152
+ )
142
153
  }
143
154
  // All 32 should have been emitted as requests
144
- expect(requests).toHaveLength(32);
155
+ expect(requests).toHaveLength(32)
145
156
 
146
157
  // 33rd request should be rate-limited
147
158
  transport.receive(
148
159
  craftReq(sendKey, channelId, dappPubB64, 32, 'req-overflow', 'wallet_getAccounts'),
149
- );
160
+ )
150
161
 
151
162
  // Wallet should have sent a res with rate_limited error (NOT close the channel)
152
- const rateLimitedRes = transport.sent.find(m =>
153
- m.t === 'res' && (m as any).body?.id === 'req-overflow',
154
- ) as any;
155
- expect(rateLimitedRes).toBeTruthy();
163
+ const rateLimitedRes = transport.sent.find(
164
+ (m) => m.t === 'res' && (m.body as Record<string, unknown>)?.id === 'req-overflow',
165
+ )
166
+ expect(rateLimitedRes).toBeTruthy()
156
167
 
157
168
  // Verify the session is still alive (do NOT close for rate_limited)
158
- expect(session.phase).toBe('connected'); // NOT closed!
159
- expect(requests).toHaveLength(32); // 33rd was NOT emitted
160
- });
161
- });
169
+ expect(session.phase).toBe('connected') // NOT closed!
170
+ expect(requests).toHaveLength(32) // 33rd was NOT emitted
171
+ })
172
+ })
162
173
 
163
174
  // ---------------------------------------------------------------------------
164
175
  // Attack 2: DApp sends req before ready.connected
@@ -173,36 +184,41 @@ describe('Malicious DApp: Request before connected', () => {
173
184
  // PREVENTS: Out-of-order message processing that could bypass
174
185
  // handshake security (Section 15 rule 7).
175
186
 
176
- const ctx = setupWalletManual();
177
- const { transport, session, dappKp, channelId } = ctx;
187
+ const ctx = setupWalletManual()
188
+ const { transport, session, dappKp, channelId } = ctx
178
189
 
179
- const requestHandler = vi.fn();
180
- session.on('request', requestHandler);
190
+ const requestHandler = vi.fn()
191
+ session.on('request', requestHandler)
181
192
 
182
193
  const uri = buildPairingUri({
183
194
  channelId,
184
195
  pubkeyB64: dappKp.publicKeyB64,
185
196
  relayUrl: 'ws://localhost/v1',
186
- name: 'D', url: 'https://d.test', icon: 'https://d.test/i.png',
187
- });
188
- await session.joinFromUri(uri);
197
+ name: 'D',
198
+ url: 'https://d.test',
199
+ icon: 'https://d.test/i.png',
200
+ })
201
+ await session.joinFromUri(uri)
189
202
  // At this point, wallet is in waiting_accept, NOT connected
190
203
 
191
204
  // DApp sends req before ready.connected
192
205
  transport.receive({
193
- v: 1, t: 'req', ch: channelId,
194
- ts: Date.now(), from: dappKp.publicKeyB64,
206
+ v: 1,
207
+ t: 'req',
208
+ ch: channelId,
209
+ ts: Date.now(),
210
+ from: dappKp.publicKeyB64,
195
211
  body: { id: 'premature-req', sealed: 'fake-sealed-data' },
196
- } as ProtocolMessage);
212
+ } as ProtocolMessage)
197
213
 
198
- await wait();
214
+ await wait()
199
215
 
200
216
  // Request should NOT have been processed (recvKey is set but phase check
201
217
  // happens via from matching — the wallet will try to decrypt and fail
202
218
  // because sealed data is invalid, or the request is dropped)
203
- expect(requestHandler).not.toHaveBeenCalled();
204
- });
205
- });
219
+ expect(requestHandler).not.toHaveBeenCalled()
220
+ })
221
+ })
206
222
 
207
223
  // ---------------------------------------------------------------------------
208
224
  // Attack 3: DApp sends req after close
@@ -218,64 +234,64 @@ describe('Malicious DApp: Request after close', () => {
218
234
  // Even if a message somehow reaches the handler, it cannot be
219
235
  // decrypted (keys are zeroed) and no response can be sent.
220
236
 
221
- const ctx = setupWalletManual();
222
- const { transport, session, channelId } = ctx;
223
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
237
+ const ctx = setupWalletManual()
238
+ const { transport, session, channelId } = ctx
239
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
224
240
 
225
241
  // Destroy the session (close + key erasure)
226
- session.destroy();
227
- expect(session.phase).toBe('closed');
242
+ session.destroy()
243
+ expect(session.phase).toBe('closed')
228
244
 
229
- const sentBefore = transport.sent.length;
245
+ const sentBefore = transport.sent.length
230
246
 
231
247
  // DApp sends req after destroy — keys are zeroed, decryption fails
232
248
  transport.receive(
233
249
  craftReq(sendKey, channelId, dappPubB64, 0, 'post-close-req', 'wallet_getAccounts'),
234
- );
250
+ )
235
251
 
236
- await wait();
252
+ await wait()
237
253
 
238
254
  // No new messages should have been sent (cannot encrypt response
239
255
  // because sendKey was zeroed)
240
- const newMessages = transport.sent.slice(sentBefore);
241
- const resMessages = newMessages.filter(m => m.t === 'res');
242
- expect(resMessages).toHaveLength(0);
243
- });
256
+ const newMessages = transport.sent.slice(sentBefore)
257
+ const resMessages = newMessages.filter((m) => m.t === 'res')
258
+ expect(resMessages).toHaveLength(0)
259
+ })
244
260
 
245
261
  it('close followed by receive via close message stops further processing', async () => {
246
262
  // When the wallet receives a close message from the peer, it
247
263
  // transitions to 'closed' and sets intentionalClose. Subsequent
248
264
  // messages on the transport should be ignored or fail gracefully.
249
265
 
250
- const dappTransport = new MockTransport();
251
- const walletTransport = new MockTransport();
252
- const _relay = new MockRelay(dappTransport, walletTransport);
266
+ const dappTransport = new MockTransport()
267
+ const walletTransport = new MockTransport()
268
+ new MockRelay(dappTransport, walletTransport)
253
269
 
254
270
  const dappSession = new DAppSession({
255
271
  transport: dappTransport,
256
272
  meta: { name: 'D', description: 'D', url: 'https://d.test', icon: 'https://d.test/i.png' },
257
- });
273
+ })
258
274
  const walletSession = new WalletSession({
259
275
  transport: walletTransport,
260
276
  capabilities: { methods: ['wallet_getAccounts'], events: [], chains: ['eip155:1'] },
261
277
  meta: { name: 'W', description: 'W', url: 'https://w.test', icon: 'https://w.test/i.png' },
262
- });
278
+ })
263
279
 
264
- const uri = await dappSession.createPairing();
265
- await walletSession.joinFromUri(uri);
266
- await wait();
267
- await wait();
280
+ const uri = await dappSession.createPairing()
281
+ await walletSession.joinFromUri(uri)
282
+ await wait()
283
+ await wait()
268
284
 
269
- expect(walletSession.phase).toBe('connected');
285
+ expect(walletSession.phase).toBe('connected')
270
286
 
271
287
  // DApp closes the session
272
- dappSession.close();
273
- await wait();
288
+ dappSession.close()
289
+ await wait()
274
290
 
275
291
  // Wallet should now be closed
276
- expect(walletSession.phase).toBe('closed');
277
- });
278
- });
292
+ expect(walletSession.phase).toBe('closed')
293
+ })
294
+ })
279
295
 
280
296
  // ---------------------------------------------------------------------------
281
297
  // Attack 4: DApp retries request with same ID but different params
@@ -291,35 +307,39 @@ describe('Malicious DApp: Duplicate request ID with different params', () => {
291
307
  // PREVENTS: Request parameter substitution attacks. Section 9.1
292
308
  // requires constant-time params hash comparison.
293
309
 
294
- const ctx = setupWalletManual();
295
- const { transport, session, channelId } = ctx;
296
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
310
+ const ctx = setupWalletManual()
311
+ const { transport, session, channelId } = ctx
312
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
297
313
 
298
314
  session.on('request', ({ id }) => {
299
- session.approve(id, 'approved');
300
- });
315
+ session.approve(id, 'approved')
316
+ })
301
317
 
302
318
  // First request with id "req-1"
303
319
  transport.receive(
304
- craftReq(sendKey, channelId, dappPubB64, 0, 'req-1', 'wallet_signMessage', { message: 'hello' }),
305
- );
306
- await wait();
320
+ craftReq(sendKey, channelId, dappPubB64, 0, 'req-1', 'wallet_signMessage', {
321
+ message: 'hello',
322
+ }),
323
+ )
324
+ await wait()
307
325
 
308
326
  // Retry with same id "req-1" but DIFFERENT params
309
327
  transport.receive(
310
- craftReq(sendKey, channelId, dappPubB64, 1, 'req-1', 'wallet_signMessage', { message: 'send_all_funds' }),
311
- );
312
- await wait();
328
+ craftReq(sendKey, channelId, dappPubB64, 1, 'req-1', 'wallet_signMessage', {
329
+ message: 'send_all_funds',
330
+ }),
331
+ )
332
+ await wait()
313
333
 
314
334
  // Wallet should have sent a rejection response for the second attempt
315
335
  // Find the response messages
316
- const responses = transport.sent.filter(m => m.t === 'res');
317
- expect(responses.length).toBeGreaterThanOrEqual(2);
336
+ const responses = transport.sent.filter((m) => m.t === 'res')
337
+ expect(responses.length).toBeGreaterThanOrEqual(2)
318
338
 
319
339
  // The session must still be alive (do NOT close for invalid_params)
320
- expect(session.phase).toBe('connected');
321
- });
322
- });
340
+ expect(session.phase).toBe('connected')
341
+ })
342
+ })
323
343
 
324
344
  // ---------------------------------------------------------------------------
325
345
  // Attack 5: DApp sends message >64KB
@@ -336,74 +356,83 @@ describe('Malicious DApp: Oversized message', () => {
336
356
  // We test the sendRaw() guard directly: the session emits an error
337
357
  // and the message is NOT actually delivered to the transport.
338
358
 
339
- const ctx = setupWalletManual();
340
- const { transport, session, channelId } = ctx;
341
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
359
+ const ctx = setupWalletManual()
360
+ const { transport, session, channelId } = ctx
361
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
342
362
 
343
- const requestHandler = vi.fn();
344
- session.on('request', requestHandler);
363
+ const requestHandler = vi.fn()
364
+ session.on('request', requestHandler)
345
365
 
346
366
  // Craft a request with a payload that when JSON-serialized
347
367
  // would exceed 64 KB (the protocol message envelope adds overhead)
348
- const hugeData = 'x'.repeat(80_000);
349
- const payload = { _method: 'wallet_signMessage', data: hugeData };
350
- const hdr = { type: 'req' as const, from: dappPubB64, id: 'huge-req' };
351
- const sealed = sealPayload(sendKey, channelId, 0, payload, hdr);
368
+ const hugeData = 'x'.repeat(80_000)
369
+ const payload = { _method: 'wallet_signMessage', data: hugeData }
370
+ const hdr = { type: 'req' as const, from: dappPubB64, id: 'huge-req' }
371
+ const sealed = sealPayload(sendKey, channelId, 0, payload, hdr)
352
372
 
353
373
  // The total message JSON will be > 64KB
354
374
  const msg = {
355
- v: 1, t: 'req', ch: channelId,
356
- ts: Date.now(), from: dappPubB64,
375
+ v: 1,
376
+ t: 'req',
377
+ ch: channelId,
378
+ ts: Date.now(),
379
+ from: dappPubB64,
357
380
  body: { id: 'huge-req', sealed },
358
- } as ProtocolMessage;
381
+ } as ProtocolMessage
359
382
 
360
- const msgSize = new TextEncoder().encode(JSON.stringify(msg)).length;
383
+ const msgSize = new TextEncoder().encode(JSON.stringify(msg)).length
361
384
  // Verify our test setup actually exceeds the limit
362
- expect(msgSize).toBeGreaterThan(65536);
385
+ expect(msgSize).toBeGreaterThan(65536)
363
386
 
364
387
  // The wallet receives this directly (bypassing its own sendRaw check).
365
388
  // The wallet will try to process it. The relay should have blocked it,
366
389
  // but if it reaches the wallet, the wallet processes it normally
367
390
  // (the 64KB check is a send-side guard, not a receive-side guard).
368
391
  // The key security property is that the SENDER enforces the limit.
369
- transport.receive(msg);
370
- await wait();
392
+ transport.receive(msg)
393
+ await wait()
371
394
 
372
395
  // For a send-side test, verify DAppSession's sendRaw blocks oversized messages
373
- const dappTransport = new MockTransport();
396
+ const dappTransport = new MockTransport()
374
397
  const dappSession = new DAppSession({
375
398
  transport: dappTransport,
376
399
  meta: { name: 'D', description: 'D', url: 'https://d.test', icon: 'https://d.test/i.png' },
377
- });
400
+ })
378
401
 
379
- const dappErrors: Error[] = [];
380
- dappSession.on('error', (e) => dappErrors.push(e));
402
+ const dappErrors: Error[] = []
403
+ dappSession.on('error', (e) => dappErrors.push(e))
381
404
 
382
405
  // Simulate connected state
383
- await dappSession.createPairing();
384
- const walletKp = generateX25519KeyPair();
406
+ await dappSession.createPairing()
407
+ const walletKp = generateX25519KeyPair()
385
408
  dappTransport.receive({
386
- v: 1, t: 'join', ch: dappSession.channelId,
387
- ts: Date.now(), from: walletKp.publicKeyB64,
388
- body: makeJoinBody(dappSession.channelId, dappTransport.sent[0]!.from!, walletKp),
389
- } as ProtocolMessage);
409
+ v: 1,
410
+ t: 'join',
411
+ ch: dappSession.channelId,
412
+ ts: Date.now(),
413
+ from: walletKp.publicKeyB64,
414
+ body: makeJoinBody(dappSession.channelId, dappTransport.sent[0]?.from ?? '', walletKp),
415
+ } as ProtocolMessage)
390
416
  dappTransport.receive({
391
- v: 1, t: 'ready', ch: dappSession.channelId,
392
- ts: Date.now(), from: '_adapter',
417
+ v: 1,
418
+ t: 'ready',
419
+ ch: dappSession.channelId,
420
+ ts: Date.now(),
421
+ from: '_adapter',
393
422
  body: { state: 'connected', reconnect: false, remote: walletKp.publicKeyB64 },
394
- } as ProtocolMessage);
423
+ } as ProtocolMessage)
395
424
 
396
- const sentBefore = dappTransport.sent.length;
425
+ const _sentBefore = dappTransport.sent.length
397
426
 
398
427
  // Try to send a huge request — sendRaw should catch the 64KB limit
399
428
  // and emit an error instead of sending
400
- dappSession.request('wallet_signMessage', { data: hugeData });
401
- await wait();
429
+ dappSession.request('wallet_signMessage', { data: hugeData })
430
+ await wait()
402
431
 
403
432
  // The error should have been emitted
404
- expect(dappErrors.some(e => e.message.includes('64 KB'))).toBe(true);
405
- });
406
- });
433
+ expect(dappErrors.some((e) => e.message.includes('64 KB'))).toBe(true)
434
+ })
435
+ })
407
436
 
408
437
  // ---------------------------------------------------------------------------
409
438
  // Attack 6: DApp calls method not in capabilities
@@ -421,53 +450,55 @@ describe('Malicious DApp: Capability violation', () => {
421
450
  methods: ['wallet_getAccounts'], // only getAccounts, NOT signMessage
422
451
  events: [],
423
452
  chains: ['eip155:1'],
424
- });
425
- const { transport, session, channelId } = ctx;
426
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
453
+ })
454
+ const { transport, session, channelId } = ctx
455
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
427
456
 
428
- const requestHandler = vi.fn();
429
- session.on('request', requestHandler);
457
+ const requestHandler = vi.fn()
458
+ session.on('request', requestHandler)
430
459
 
431
460
  // DApp tries to call wallet_signMessage which is NOT in capabilities
432
461
  transport.receive(
433
- craftReq(sendKey, channelId, dappPubB64, 0, 'req-evil', 'wallet_signMessage', { message: 'hack' }),
434
- );
435
- await wait();
462
+ craftReq(sendKey, channelId, dappPubB64, 0, 'req-evil', 'wallet_signMessage', {
463
+ message: 'hack',
464
+ }),
465
+ )
466
+ await wait()
436
467
 
437
468
  // Request should NOT have been emitted to the application
438
- expect(requestHandler).not.toHaveBeenCalled();
469
+ expect(requestHandler).not.toHaveBeenCalled()
439
470
 
440
471
  // Wallet should have sent an error response
441
- const rejectRes = transport.sent.find(m =>
442
- m.t === 'res' && (m as any).body?.id === 'req-evil',
443
- );
444
- expect(rejectRes).toBeTruthy();
472
+ const rejectRes = transport.sent.find(
473
+ (m) => m.t === 'res' && (m.body as Record<string, unknown>)?.id === 'req-evil',
474
+ )
475
+ expect(rejectRes).toBeTruthy()
445
476
 
446
477
  // Session must remain open (do NOT close for unsupported_method)
447
- expect(session.phase).toBe('connected');
448
- });
478
+ expect(session.phase).toBe('connected')
479
+ })
449
480
 
450
481
  it('wallet rejects completely unknown method', async () => {
451
482
  const ctx = setupWalletManual({
452
483
  methods: ['wallet_getAccounts'],
453
484
  events: [],
454
485
  chains: ['eip155:1'],
455
- });
456
- const { transport, session, channelId } = ctx;
457
- const { sendKey, dappPubB64 } = await connectWalletManual(ctx);
486
+ })
487
+ const { transport, session, channelId } = ctx
488
+ const { sendKey, dappPubB64 } = await connectWalletManual(ctx)
458
489
 
459
- const requestHandler = vi.fn();
460
- session.on('request', requestHandler);
490
+ const requestHandler = vi.fn()
491
+ session.on('request', requestHandler)
461
492
 
462
493
  transport.receive(
463
494
  craftReq(sendKey, channelId, dappPubB64, 0, 'req-unknown', 'evil_drainWallet'),
464
- );
465
- await wait();
495
+ )
496
+ await wait()
466
497
 
467
- expect(requestHandler).not.toHaveBeenCalled();
468
- expect(session.phase).toBe('connected');
469
- });
470
- });
498
+ expect(requestHandler).not.toHaveBeenCalled()
499
+ expect(session.phase).toBe('connected')
500
+ })
501
+ })
471
502
 
472
503
  // ---------------------------------------------------------------------------
473
504
  // Attack 7: DApp sends req with _adapter spoofed from
@@ -481,25 +512,28 @@ describe('Malicious DApp: Spoofed _adapter from', () => {
481
512
  //
482
513
  // PREVENTS: Adapter impersonation in peer message types.
483
514
 
484
- const ctx = setupWalletManual();
485
- const { transport, session, channelId } = ctx;
486
- await connectWalletManual(ctx);
515
+ const ctx = setupWalletManual()
516
+ const { transport, session, channelId } = ctx
517
+ await connectWalletManual(ctx)
487
518
 
488
- const errorHandler = vi.fn();
489
- session.on('error', errorHandler);
490
- const requestHandler = vi.fn();
491
- session.on('request', requestHandler);
519
+ const errorHandler = vi.fn()
520
+ session.on('error', errorHandler)
521
+ const requestHandler = vi.fn()
522
+ session.on('request', requestHandler)
492
523
 
493
524
  transport.receive({
494
- v: 1, t: 'req', ch: channelId,
495
- ts: Date.now(), from: '_adapter', // spoofed!
525
+ v: 1,
526
+ t: 'req',
527
+ ch: channelId,
528
+ ts: Date.now(),
529
+ from: '_adapter', // spoofed!
496
530
  body: { id: 'spoofed-req', sealed: 'fake' },
497
- } as ProtocolMessage);
531
+ } as ProtocolMessage)
498
532
 
499
- await wait();
533
+ await wait()
500
534
 
501
- expect(errorHandler).toHaveBeenCalled();
502
- expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('spoofed _adapter');
503
- expect(requestHandler).not.toHaveBeenCalled();
504
- });
505
- });
535
+ expect(errorHandler).toHaveBeenCalled()
536
+ expect(errorHandler.mock.calls[0]?.[0]?.message).toContain('spoofed _adapter')
537
+ expect(requestHandler).not.toHaveBeenCalled()
538
+ })
539
+ })