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
package/src/crypto.ts CHANGED
@@ -4,48 +4,46 @@
4
4
  * Pure JS (noble libraries v2), no native modules required.
5
5
  */
6
6
 
7
- import _canonicalize from 'canonicalize';
8
- const canonicalize = (_canonicalize as { default: typeof _canonicalize.default }).default ?? _canonicalize.default;
9
- import { x25519 } from '@noble/curves/ed25519';
10
- import { hkdf } from '@noble/hashes/hkdf';
11
- import { sha256 } from '@noble/hashes/sha256';
12
- import { hmac } from '@noble/hashes/hmac';
13
- import { chacha20poly1305 } from '@noble/ciphers/chacha';
14
- import {
15
- bytesToHex,
16
- hexToBytes,
17
- utf8ToBytes,
18
- concatBytes,
19
- } from '@noble/hashes/utils';
20
-
21
- import type { PairingParams } from './types.js';
7
+ import _canonicalize from 'canonicalize'
8
+
9
+ // Handle CJS/ESM interop: bundlers may wrap the default export in { default: fn }
10
+ const canonicalize: (input: unknown) => string | undefined =
11
+ typeof _canonicalize === 'function'
12
+ ? _canonicalize
13
+ : (_canonicalize as unknown as { default: (input: unknown) => string | undefined }).default
14
+
15
+ import { chacha20poly1305 } from '@noble/ciphers/chacha'
16
+ import { x25519 } from '@noble/curves/ed25519'
17
+ import { hkdf } from '@noble/hashes/hkdf'
18
+ import { hmac } from '@noble/hashes/hmac'
19
+ import { sha256 } from '@noble/hashes/sha256'
20
+ import { bytesToHex, concatBytes, hexToBytes, utf8ToBytes } from '@noble/hashes/utils'
21
+
22
+ import type { PairingParams } from './types.js'
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Re-exports
25
26
  // ---------------------------------------------------------------------------
26
27
 
27
- export { bytesToHex, hexToBytes };
28
+ export { bytesToHex, hexToBytes }
28
29
 
29
30
  // ---------------------------------------------------------------------------
30
31
  // Base64url (no padding)
31
32
  // ---------------------------------------------------------------------------
32
33
 
33
34
  export function b64urlEncode(bytes: Uint8Array): string {
34
- let binary = '';
35
- for (const b of bytes) binary += String.fromCharCode(b);
36
- return btoa(binary)
37
- .replace(/\+/g, '-')
38
- .replace(/\//g, '_')
39
- .replace(/=/g, '');
35
+ let binary = ''
36
+ for (const b of bytes) binary += String.fromCharCode(b)
37
+ return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '')
40
38
  }
41
39
 
42
40
  export function b64urlDecode(str: string): Uint8Array {
43
- const b64 = str.replace(/-/g, '+').replace(/_/g, '/');
44
- const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4);
45
- const binary = atob(padded);
46
- const bytes = new Uint8Array(binary.length);
47
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
48
- return bytes;
41
+ const b64 = str.replace(/-/g, '+').replace(/_/g, '/')
42
+ const padded = b64 + '='.repeat((4 - (b64.length % 4)) % 4)
43
+ const binary = atob(padded)
44
+ const bytes = new Uint8Array(binary.length)
45
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)
46
+ return bytes
49
47
  }
50
48
 
51
49
  // ---------------------------------------------------------------------------
@@ -53,19 +51,19 @@ export function b64urlDecode(str: string): Uint8Array {
53
51
  // ---------------------------------------------------------------------------
54
52
 
55
53
  export interface X25519KeyPair {
56
- privateKey: Uint8Array;
57
- publicKey: Uint8Array;
58
- publicKeyB64: string;
54
+ privateKey: Uint8Array
55
+ publicKey: Uint8Array
56
+ publicKeyB64: string
59
57
  }
60
58
 
61
59
  export function generateX25519KeyPair(): X25519KeyPair {
62
- const privateKey = x25519.utils.randomPrivateKey();
63
- const publicKey = x25519.getPublicKey(privateKey);
64
- return { privateKey, publicKey, publicKeyB64: b64urlEncode(publicKey) };
60
+ const privateKey = x25519.utils.randomPrivateKey()
61
+ const publicKey = x25519.getPublicKey(privateKey)
62
+ return { privateKey, publicKey, publicKeyB64: b64urlEncode(publicKey) }
65
63
  }
66
64
 
67
65
  export function getPublicKey(privateKey: Uint8Array): Uint8Array {
68
- return x25519.getPublicKey(privateKey);
66
+ return x25519.getPublicKey(privateKey)
69
67
  }
70
68
 
71
69
  // ---------------------------------------------------------------------------
@@ -77,58 +75,57 @@ export function computeSharedSecret(
77
75
  remotePubKey: Uint8Array,
78
76
  ): Uint8Array {
79
77
  if (remotePubKey.length !== 32) {
80
- throw new Error('Remote public key must be exactly 32 bytes');
78
+ throw new Error('Remote public key must be exactly 32 bytes')
81
79
  }
82
- const shared = x25519.getSharedSecret(myPrivateKey, remotePubKey);
80
+ const shared = x25519.getSharedSecret(myPrivateKey, remotePubKey)
83
81
  // RFC 7748 §6: low-order points produce all-zero output — reject to
84
82
  // prevent invalid key derivation.
85
- let acc = 0;
86
- for (let i = 0; i < shared.length; i++) acc |= shared[i]!;
83
+ let acc = 0
84
+ for (let i = 0; i < shared.length; i++) acc |= shared[i] ?? 0
87
85
  if (acc === 0) {
88
- throw new Error('X25519 produced all-zero shared secret (low-order public key)');
86
+ throw new Error('X25519 produced all-zero shared secret (low-order public key)')
89
87
  }
90
- return shared;
88
+ return shared
91
89
  }
92
90
 
93
- export function deriveSessionKey(
94
- sharedSecret: Uint8Array,
95
- channelIdHex: string,
96
- ): Uint8Array {
97
- return hkdf(sha256, sharedSecret, hexToBytes(channelIdHex), 'walletpair-v1 root', 32);
91
+ export function deriveSessionKey(sharedSecret: Uint8Array, channelIdHex: string): Uint8Array {
92
+ return hkdf(sha256, sharedSecret, hexToBytes(channelIdHex), 'walletpair-v1 root', 32)
98
93
  }
99
94
 
100
95
  export interface SessionCryptoContext {
101
- dappPubKeyB64: string;
102
- walletPubKeyB64: string;
103
- capabilities?: unknown;
104
- walletMeta?: unknown;
105
- dappName?: string | undefined;
96
+ dappPubKeyB64: string
97
+ walletPubKeyB64: string
98
+ capabilities?: unknown
99
+ walletMeta?: unknown
100
+ dappName?: string | undefined
106
101
  }
107
102
 
108
103
  export interface DirectionalSessionKeys {
109
- rootKey: Uint8Array;
110
- dappToWalletKey: Uint8Array;
111
- walletToDappKey: Uint8Array;
112
- transcriptHash: Uint8Array;
104
+ rootKey: Uint8Array
105
+ dappToWalletKey: Uint8Array
106
+ walletToDappKey: Uint8Array
107
+ transcriptHash: Uint8Array
113
108
  }
114
109
 
115
110
  export function canonicalJson(value: unknown): string {
116
- return canonicalize(value) ?? 'null';
111
+ return canonicalize(value) ?? 'null'
117
112
  }
118
113
 
119
114
  export function computeHandshakeTranscriptHash(
120
115
  channelIdHex: string,
121
116
  context: SessionCryptoContext,
122
117
  ): Uint8Array {
123
- return sha256(concatBytes(
124
- utf8ToBytes('walletpair-v1-transcript'),
125
- hexToBytes(channelIdHex),
126
- lp(context.dappPubKeyB64),
127
- lp(context.walletPubKeyB64),
128
- lp(canonicalJson(context.capabilities ?? null)),
129
- lp(canonicalJson(context.walletMeta ?? null)),
130
- lp(context.dappName ?? ''),
131
- ));
118
+ return sha256(
119
+ concatBytes(
120
+ utf8ToBytes('walletpair-v1-transcript'),
121
+ hexToBytes(channelIdHex),
122
+ lp(context.dappPubKeyB64),
123
+ lp(context.walletPubKeyB64),
124
+ lp(canonicalJson(context.capabilities ?? null)),
125
+ lp(canonicalJson(context.walletMeta ?? null)),
126
+ lp(context.dappName ?? ''),
127
+ ),
128
+ )
132
129
  }
133
130
 
134
131
  export function deriveDirectionalSessionKeys(
@@ -136,41 +133,37 @@ export function deriveDirectionalSessionKeys(
136
133
  channelIdHex: string,
137
134
  context: SessionCryptoContext,
138
135
  ): DirectionalSessionKeys {
139
- const transcriptHash = computeHandshakeTranscriptHash(channelIdHex, context);
136
+ const transcriptHash = computeHandshakeTranscriptHash(channelIdHex, context)
140
137
  return {
141
138
  rootKey,
142
139
  transcriptHash,
143
140
  dappToWalletKey: hkdf(sha256, rootKey, transcriptHash, 'walletpair-v1 dapp-to-wallet', 32),
144
141
  walletToDappKey: hkdf(sha256, rootKey, transcriptHash, 'walletpair-v1 wallet-to-dapp', 32),
145
- };
142
+ }
146
143
  }
147
144
 
148
145
  // ---------------------------------------------------------------------------
149
146
  // Session fingerprint (protocol Section 7.3)
150
147
  // ---------------------------------------------------------------------------
151
148
 
152
- export function computeSessionFingerprint(
153
- channelIdHex: string,
154
- dappPubKeyB64: string,
155
- ): string {
156
- const hash = sha256(concatBytes(
157
- utf8ToBytes('walletpair-v1-session-fingerprint'),
158
- hexToBytes(channelIdHex),
159
- b64urlDecode(dappPubKeyB64),
160
- ));
161
- const view = new DataView(hash.buffer, hash.byteOffset, 4);
162
- return (view.getUint32(0) % 10000).toString().padStart(4, '0');
149
+ export function computeSessionFingerprint(channelIdHex: string, dappPubKeyB64: string): string {
150
+ const hash = sha256(
151
+ concatBytes(
152
+ utf8ToBytes('walletpair-v1-session-fingerprint'),
153
+ hexToBytes(channelIdHex),
154
+ b64urlDecode(dappPubKeyB64),
155
+ ),
156
+ )
157
+ const view = new DataView(hash.buffer, hash.byteOffset, 4)
158
+ return (view.getUint32(0) % 10000).toString().padStart(4, '0')
163
159
  }
164
160
 
165
161
  // ---------------------------------------------------------------------------
166
162
  // Join encryption key (protocol Section 7.5 — private handshake)
167
163
  // ---------------------------------------------------------------------------
168
164
 
169
- export function deriveJoinEncryptionKey(
170
- rootKey: Uint8Array,
171
- channelIdHex: string,
172
- ): Uint8Array {
173
- return hkdf(sha256, rootKey, hexToBytes(channelIdHex), 'walletpair-v1 join-encryption', 32);
165
+ export function deriveJoinEncryptionKey(rootKey: Uint8Array, channelIdHex: string): Uint8Array {
166
+ return hkdf(sha256, rootKey, hexToBytes(channelIdHex), 'walletpair-v1 join-encryption', 32)
174
167
  }
175
168
 
176
169
  /**
@@ -183,12 +176,12 @@ export function sealJoin(
183
176
  capabilities: unknown,
184
177
  meta?: unknown,
185
178
  ): string {
186
- const plainObj: Record<string, unknown> = { capabilities, meta: meta ?? null };
187
- const plaintext = utf8ToBytes(canonicalJson(plainObj));
188
- const nonce = crypto.getRandomValues(new Uint8Array(12));
189
- const aad = concatBytes(hexToBytes(channelIdHex), new Uint8Array([0x04]));
190
- const ciphertext = chacha20poly1305(joinEncryptionKey, nonce, aad).encrypt(plaintext);
191
- return b64urlEncode(concatBytes(nonce, ciphertext));
179
+ const plainObj: Record<string, unknown> = { capabilities, meta: meta ?? null }
180
+ const plaintext = utf8ToBytes(canonicalJson(plainObj))
181
+ const nonce = crypto.getRandomValues(new Uint8Array(12))
182
+ const aad = concatBytes(hexToBytes(channelIdHex), new Uint8Array([0x04]))
183
+ const ciphertext = chacha20poly1305(joinEncryptionKey, nonce, aad).encrypt(plaintext)
184
+ return b64urlEncode(concatBytes(nonce, ciphertext))
192
185
  }
193
186
 
194
187
  /**
@@ -200,15 +193,19 @@ export function unsealJoin(
200
193
  channelIdHex: string,
201
194
  sealedJoin: string,
202
195
  ): { capabilities: unknown; meta?: unknown } {
203
- const envelope = b64urlDecode(sealedJoin);
196
+ const envelope = b64urlDecode(sealedJoin)
204
197
  if (envelope.length < 12 + 16) {
205
- throw new Error('Invalid sealed_join envelope');
198
+ throw new Error('Invalid sealed_join envelope')
199
+ }
200
+ const nonce = envelope.slice(0, 12)
201
+ const ciphertext = envelope.slice(12)
202
+ const aad = concatBytes(hexToBytes(channelIdHex), new Uint8Array([0x04]))
203
+ const plaintext = chacha20poly1305(joinEncryptionKey, nonce, aad).decrypt(ciphertext)
204
+ try {
205
+ return JSON.parse(new TextDecoder().decode(plaintext))
206
+ } catch {
207
+ throw new Error('Decrypted sealed_join payload is not valid JSON')
206
208
  }
207
- const nonce = envelope.slice(0, 12);
208
- const ciphertext = envelope.slice(12);
209
- const aad = concatBytes(hexToBytes(channelIdHex), new Uint8Array([0x04]));
210
- const plaintext = chacha20poly1305(joinEncryptionKey, nonce, aad).decrypt(ciphertext);
211
- return JSON.parse(new TextDecoder().decode(plaintext));
212
209
  }
213
210
 
214
211
  // ---------------------------------------------------------------------------
@@ -222,35 +219,38 @@ export function unsealJoin(
222
219
  export type AadHeader =
223
220
  | { type: 'req'; from: string; id: string }
224
221
  | { type: 'res'; from: string; id: string }
225
- | { type: 'evt'; from: string; id: string };
222
+ | { type: 'evt'; from: string; id: string }
226
223
 
227
224
  /** Length-prefix a UTF-8 string: uint16_be(byte_length) || utf8_bytes */
228
225
  function lp(s: string): Uint8Array {
229
- const bytes = utf8ToBytes(s);
226
+ const bytes = utf8ToBytes(s)
230
227
  if (bytes.length > 0xffff) {
231
- throw new Error('AAD field exceeds 65535 bytes');
228
+ throw new Error('AAD field exceeds 65535 bytes')
232
229
  }
233
- const len = new Uint8Array(2);
234
- new DataView(len.buffer).setUint16(0, bytes.length);
235
- return concatBytes(len, bytes);
230
+ const len = new Uint8Array(2)
231
+ new DataView(len.buffer).setUint16(0, bytes.length)
232
+ return concatBytes(len, bytes)
236
233
  }
237
234
 
238
235
  /**
239
236
  * Build AEAD AAD = channel_id_bytes || type_byte || lp(fields...)
240
237
  */
241
238
  function buildAad(channelIdHex: string, header?: AadHeader): Uint8Array {
242
- const chBytes = hexToBytes(channelIdHex);
243
- if (!header) return chBytes;
239
+ const chBytes = hexToBytes(channelIdHex)
240
+ if (!header) return chBytes
244
241
  switch (header.type) {
245
242
  case 'req':
246
- return concatBytes(chBytes, new Uint8Array([0x01]), lp(header.from), lp(header.id));
243
+ return concatBytes(chBytes, new Uint8Array([0x01]), lp(header.from), lp(header.id))
247
244
  case 'res':
248
- return concatBytes(chBytes, new Uint8Array([0x02]), lp(header.from), lp(header.id));
245
+ return concatBytes(chBytes, new Uint8Array([0x02]), lp(header.from), lp(header.id))
249
246
  case 'evt':
250
- return concatBytes(chBytes, new Uint8Array([0x03]), lp(header.from), lp(header.id));
247
+ return concatBytes(chBytes, new Uint8Array([0x03]), lp(header.from), lp(header.id))
251
248
  }
252
249
  }
253
250
 
251
+ /** Maximum sequence number (2^32 - 1). Session MUST close before reaching this. */
252
+ const MAX_SEQ = 0xffffffff
253
+
254
254
  export function sealPayload(
255
255
  encryptionKey: Uint8Array,
256
256
  channelIdHex: string,
@@ -258,13 +258,16 @@ export function sealPayload(
258
258
  data: unknown,
259
259
  header?: AadHeader,
260
260
  ): string {
261
- const seqBytes = new Uint8Array(4);
262
- new DataView(seqBytes.buffer).setUint32(0, seq);
263
- const nonce = hmac(sha256, encryptionKey, seqBytes).slice(0, 12);
264
- const plaintext = utf8ToBytes(canonicalJson(data));
265
- const aad = buildAad(channelIdHex, header);
266
- const ciphertext = chacha20poly1305(encryptionKey, nonce, aad).encrypt(plaintext);
267
- return b64urlEncode(concatBytes(seqBytes, ciphertext));
261
+ if (!Number.isInteger(seq) || seq < 0 || seq > MAX_SEQ) {
262
+ throw new Error(`Sequence number out of range: ${seq} (must be 0..${MAX_SEQ})`)
263
+ }
264
+ const seqBytes = new Uint8Array(4)
265
+ new DataView(seqBytes.buffer).setUint32(0, seq)
266
+ const nonce = hmac(sha256, encryptionKey, seqBytes).slice(0, 12)
267
+ const plaintext = utf8ToBytes(canonicalJson(data))
268
+ const aad = buildAad(channelIdHex, header)
269
+ const ciphertext = chacha20poly1305(encryptionKey, nonce, aad).encrypt(plaintext)
270
+ return b64urlEncode(concatBytes(seqBytes, ciphertext))
268
271
  }
269
272
 
270
273
  export function unsealPayload(
@@ -273,43 +276,49 @@ export function unsealPayload(
273
276
  sealed: string,
274
277
  header?: AadHeader,
275
278
  ): { seq: number; data: unknown; plaintext: Uint8Array; plaintextJson: string } {
276
- const bytes = b64urlDecode(sealed);
277
- const seqBytes = bytes.slice(0, 4);
278
- const ciphertext = bytes.slice(4);
279
- const nonce = hmac(sha256, encryptionKey, seqBytes).slice(0, 12);
280
- const aad = buildAad(channelIdHex, header);
281
- const plaintext = chacha20poly1305(encryptionKey, nonce, aad).decrypt(ciphertext);
282
- const seq = new DataView(seqBytes.buffer, seqBytes.byteOffset, 4).getUint32(0);
283
- const plaintextJson = new TextDecoder().decode(plaintext);
284
- return { seq, data: JSON.parse(plaintextJson), plaintext, plaintextJson };
279
+ const bytes = b64urlDecode(sealed)
280
+ const seqBytes = bytes.slice(0, 4)
281
+ const ciphertext = bytes.slice(4)
282
+ const nonce = hmac(sha256, encryptionKey, seqBytes).slice(0, 12)
283
+ const aad = buildAad(channelIdHex, header)
284
+ const plaintext = chacha20poly1305(encryptionKey, nonce, aad).decrypt(ciphertext)
285
+ const seq = new DataView(seqBytes.buffer, seqBytes.byteOffset, 4).getUint32(0)
286
+ const plaintextJson = new TextDecoder().decode(plaintext)
287
+ let data: unknown
288
+ try {
289
+ data = JSON.parse(plaintextJson)
290
+ } catch {
291
+ throw new Error('Decrypted payload is not valid JSON')
292
+ }
293
+ return { seq, data, plaintext, plaintextJson }
285
294
  }
286
295
 
287
296
  export function sha256Hex(bytes: Uint8Array): string {
288
- return bytesToHex(sha256(bytes));
297
+ return bytesToHex(sha256(bytes))
289
298
  }
290
299
 
291
300
  /** Constant-time string comparison to prevent timing side-channels (§9.1). */
292
301
  export function constantTimeEqual(a: string, b: string): boolean {
293
- if (a.length !== b.length) return false;
294
- let result = 0;
302
+ if (a.length !== b.length) return false
303
+ let result = 0
295
304
  for (let i = 0; i < a.length; i++) {
296
- result |= a.charCodeAt(i) ^ b.charCodeAt(i);
305
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i)
297
306
  }
298
- return result === 0;
307
+ return result === 0
299
308
  }
300
309
 
301
310
  // ---------------------------------------------------------------------------
302
311
  // Snapshot integrity (HMAC for serialized session state)
303
312
  // ---------------------------------------------------------------------------
304
313
 
305
- const SNAPSHOT_HMAC_INFO = utf8ToBytes('walletpair-v1-snapshot-hmac');
314
+ const SNAPSHOT_HMAC_INFO = utf8ToBytes('walletpair-v1-snapshot-hmac')
306
315
 
307
316
  /**
308
317
  * Derive a dedicated HMAC key from the send key so we don't reuse
309
318
  * traffic keys for a different purpose.
310
319
  */
311
320
  function deriveSnapshotHmacKey(sendKey: Uint8Array): Uint8Array {
312
- return hmac(sha256, sendKey, SNAPSHOT_HMAC_INFO);
321
+ return hmac(sha256, sendKey, SNAPSHOT_HMAC_INFO)
313
322
  }
314
323
 
315
324
  /**
@@ -317,9 +326,9 @@ function deriveSnapshotHmacKey(sendKey: Uint8Array): Uint8Array {
317
326
  * Returns `<hex-mac>.<json-payload>`.
318
327
  */
319
328
  export function signSnapshot(sendKey: Uint8Array, json: string): string {
320
- const macKey = deriveSnapshotHmacKey(sendKey);
321
- const mac = hmac(sha256, macKey, utf8ToBytes(json));
322
- return bytesToHex(mac) + '.' + json;
329
+ const macKey = deriveSnapshotHmacKey(sendKey)
330
+ const mac = hmac(sha256, macKey, utf8ToBytes(json))
331
+ return `${bytesToHex(mac)}.${json}`
323
332
  }
324
333
 
325
334
  /**
@@ -327,13 +336,13 @@ export function signSnapshot(sendKey: Uint8Array, json: string): string {
327
336
  * Returns the JSON payload on success, or `null` if the HMAC is invalid.
328
337
  */
329
338
  export function verifySnapshot(sendKey: Uint8Array, signed: string): string | null {
330
- const dot = signed.indexOf('.');
331
- if (dot !== 64) return null; // HMAC-SHA256 hex = 64 chars
332
- const macHex = signed.slice(0, 64);
333
- const json = signed.slice(65);
334
- const macKey = deriveSnapshotHmacKey(sendKey);
335
- const expected = bytesToHex(hmac(sha256, macKey, utf8ToBytes(json)));
336
- return constantTimeEqual(macHex, expected) ? json : null;
339
+ const dot = signed.indexOf('.')
340
+ if (dot !== 64) return null // HMAC-SHA256 hex = 64 chars
341
+ const macHex = signed.slice(0, 64)
342
+ const json = signed.slice(65)
343
+ const macKey = deriveSnapshotHmacKey(sendKey)
344
+ const expected = bytesToHex(hmac(sha256, macKey, utf8ToBytes(json)))
345
+ return constantTimeEqual(macHex, expected) ? json : null
337
346
  }
338
347
 
339
348
  // ---------------------------------------------------------------------------
@@ -341,7 +350,7 @@ export function verifySnapshot(sendKey: Uint8Array, signed: string): string | nu
341
350
  // ---------------------------------------------------------------------------
342
351
 
343
352
  export function generateChannelId(): string {
344
- return bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
353
+ return bytesToHex(crypto.getRandomValues(new Uint8Array(32)))
345
354
  }
346
355
 
347
356
  // ---------------------------------------------------------------------------
@@ -349,49 +358,50 @@ export function generateChannelId(): string {
349
358
  // ---------------------------------------------------------------------------
350
359
 
351
360
  export function buildPairingUri(params: {
352
- channelId: string;
353
- pubkeyB64: string;
354
- relayUrl?: string | undefined;
355
- name: string;
356
- url: string;
357
- icon: string;
361
+ channelId: string
362
+ pubkeyB64: string
363
+ relayUrl?: string | undefined
364
+ name: string
365
+ url: string
366
+ icon: string
358
367
  /** Methods the dApp intends to call (§9.1). */
359
- methods?: string[] | undefined;
368
+ methods?: string[] | undefined
360
369
  /** CAIP-2 chains the dApp intends to use (§9.1). */
361
- chains?: string[] | undefined;
370
+ chains?: string[] | undefined
362
371
  }): string {
363
- let uri = `walletpair:?ch=${params.channelId}&pubkey=${params.pubkeyB64}`;
364
- if (params.relayUrl) uri += `&relay=${encodeURIComponent(params.relayUrl)}`;
365
- uri += `&name=${encodeURIComponent(params.name)}`;
366
- uri += `&url=${encodeURIComponent(params.url)}`;
367
- uri += `&icon=${encodeURIComponent(params.icon)}`;
368
- if (params.methods?.length) uri += `&methods=${params.methods.join(',')}`;
369
- if (params.chains?.length) uri += `&chains=${params.chains.join(',')}`;
370
- return uri;
372
+ let uri = `walletpair:?ch=${params.channelId}&pubkey=${params.pubkeyB64}`
373
+ if (params.relayUrl) uri += `&relay=${encodeURIComponent(params.relayUrl)}`
374
+ uri += `&name=${encodeURIComponent(params.name)}`
375
+ uri += `&url=${encodeURIComponent(params.url)}`
376
+ uri += `&icon=${encodeURIComponent(params.icon)}`
377
+ if (params.methods?.length) uri += `&methods=${params.methods.join(',')}`
378
+ if (params.chains?.length) uri += `&chains=${params.chains.join(',')}`
379
+ return uri
371
380
  }
372
381
 
373
382
  export function parsePairingUri(uri: string): PairingParams {
374
- const qs = uri.replace(/^walletpair:\?/, '');
375
- const params = new URLSearchParams(qs);
376
- const ch = params.get('ch');
377
- const pubkey = params.get('pubkey');
378
- if (!ch || !pubkey) throw new Error('Invalid pairing URI: missing ch or pubkey');
383
+ const qs = uri.replace(/^walletpair:\?/, '')
384
+ const params = new URLSearchParams(qs)
385
+ const ch = params.get('ch')
386
+ const pubkey = params.get('pubkey')
387
+ if (!ch || !pubkey) throw new Error('Invalid pairing URI: missing ch or pubkey')
379
388
  // §8.1: ch must be 64 hex characters (32 bytes)
380
- if (!/^[0-9a-f]{64}$/.test(ch)) throw new Error('Invalid pairing URI: ch must be 64 lowercase hex chars');
389
+ if (!/^[0-9a-f]{64}$/.test(ch))
390
+ throw new Error('Invalid pairing URI: ch must be 64 lowercase hex chars')
381
391
  // §8.1: pubkey must decode to 32 bytes
382
- const pubkeyBytes = b64urlDecode(pubkey);
383
- if (pubkeyBytes.length !== 32) throw new Error('Invalid pairing URI: pubkey must be 32 bytes');
392
+ const pubkeyBytes = b64urlDecode(pubkey)
393
+ if (pubkeyBytes.length !== 32) throw new Error('Invalid pairing URI: pubkey must be 32 bytes')
384
394
  // §8.1: name, url, icon are required
385
- const name = params.get('name');
386
- const url = params.get('url');
387
- const icon = params.get('icon');
388
- if (!name) throw new Error('Invalid pairing URI: missing required param "name"');
389
- if (!url) throw new Error('Invalid pairing URI: missing required param "url"');
390
- if (!icon) throw new Error('Invalid pairing URI: missing required param "icon"');
395
+ const name = params.get('name')
396
+ const url = params.get('url')
397
+ const icon = params.get('icon')
398
+ if (!name) throw new Error('Invalid pairing URI: missing required param "name"')
399
+ if (!url) throw new Error('Invalid pairing URI: missing required param "url"')
400
+ if (!icon) throw new Error('Invalid pairing URI: missing required param "icon"')
391
401
  // §8.1: icon MUST be https:
392
- if (!icon.startsWith('https:')) throw new Error('Invalid pairing URI: icon must use https:');
393
- const methodsStr = params.get('methods');
394
- const chainsStr = params.get('chains');
402
+ if (!icon.startsWith('https:')) throw new Error('Invalid pairing URI: icon must use https:')
403
+ const methodsStr = params.get('methods')
404
+ const chainsStr = params.get('chains')
395
405
  return {
396
406
  ch,
397
407
  pubkey,
@@ -401,5 +411,5 @@ export function parsePairingUri(uri: string): PairingParams {
401
411
  icon,
402
412
  methods: methodsStr ? methodsStr.split(',').filter(Boolean) : undefined,
403
413
  chains: chainsStr ? chainsStr.split(',').filter(Boolean) : undefined,
404
- };
414
+ }
405
415
  }