@aztec/wallet-sdk 0.0.1-commit.023c3e5

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 (62) hide show
  1. package/README.md +321 -0
  2. package/dest/base-wallet/base_wallet.d.ts +117 -0
  3. package/dest/base-wallet/base_wallet.d.ts.map +1 -0
  4. package/dest/base-wallet/base_wallet.js +271 -0
  5. package/dest/base-wallet/index.d.ts +2 -0
  6. package/dest/base-wallet/index.d.ts.map +1 -0
  7. package/dest/base-wallet/index.js +1 -0
  8. package/dest/crypto.d.ts +192 -0
  9. package/dest/crypto.d.ts.map +1 -0
  10. package/dest/crypto.js +394 -0
  11. package/dest/emoji_alphabet.d.ts +35 -0
  12. package/dest/emoji_alphabet.d.ts.map +1 -0
  13. package/dest/emoji_alphabet.js +299 -0
  14. package/dest/extension/handlers/background_connection_handler.d.ts +158 -0
  15. package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -0
  16. package/dest/extension/handlers/background_connection_handler.js +258 -0
  17. package/dest/extension/handlers/content_script_connection_handler.d.ts +56 -0
  18. package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -0
  19. package/dest/extension/handlers/content_script_connection_handler.js +174 -0
  20. package/dest/extension/handlers/index.d.ts +12 -0
  21. package/dest/extension/handlers/index.d.ts.map +1 -0
  22. package/dest/extension/handlers/index.js +10 -0
  23. package/dest/extension/handlers/internal_message_types.d.ts +63 -0
  24. package/dest/extension/handlers/internal_message_types.d.ts.map +1 -0
  25. package/dest/extension/handlers/internal_message_types.js +22 -0
  26. package/dest/extension/provider/extension_provider.d.ts +107 -0
  27. package/dest/extension/provider/extension_provider.d.ts.map +1 -0
  28. package/dest/extension/provider/extension_provider.js +160 -0
  29. package/dest/extension/provider/extension_wallet.d.ts +131 -0
  30. package/dest/extension/provider/extension_wallet.d.ts.map +1 -0
  31. package/dest/extension/provider/extension_wallet.js +271 -0
  32. package/dest/extension/provider/index.d.ts +3 -0
  33. package/dest/extension/provider/index.d.ts.map +1 -0
  34. package/dest/extension/provider/index.js +2 -0
  35. package/dest/manager/index.d.ts +3 -0
  36. package/dest/manager/index.d.ts.map +1 -0
  37. package/dest/manager/index.js +1 -0
  38. package/dest/manager/types.d.ts +167 -0
  39. package/dest/manager/types.d.ts.map +1 -0
  40. package/dest/manager/types.js +19 -0
  41. package/dest/manager/wallet_manager.d.ts +70 -0
  42. package/dest/manager/wallet_manager.d.ts.map +1 -0
  43. package/dest/manager/wallet_manager.js +226 -0
  44. package/dest/types.d.ts +123 -0
  45. package/dest/types.d.ts.map +1 -0
  46. package/dest/types.js +11 -0
  47. package/package.json +99 -0
  48. package/src/base-wallet/base_wallet.ts +394 -0
  49. package/src/base-wallet/index.ts +1 -0
  50. package/src/crypto.ts +499 -0
  51. package/src/emoji_alphabet.ts +317 -0
  52. package/src/extension/handlers/background_connection_handler.ts +423 -0
  53. package/src/extension/handlers/content_script_connection_handler.ts +246 -0
  54. package/src/extension/handlers/index.ts +25 -0
  55. package/src/extension/handlers/internal_message_types.ts +69 -0
  56. package/src/extension/provider/extension_provider.ts +233 -0
  57. package/src/extension/provider/extension_wallet.ts +321 -0
  58. package/src/extension/provider/index.ts +7 -0
  59. package/src/manager/index.ts +12 -0
  60. package/src/manager/types.ts +177 -0
  61. package/src/manager/wallet_manager.ts +259 -0
  62. package/src/types.ts +132 -0
package/src/crypto.ts ADDED
@@ -0,0 +1,499 @@
1
+ /**
2
+ * Cryptographic utilities for secure wallet communication.
3
+ *
4
+ * This module provides ECDH key exchange and AES-GCM encryption primitives
5
+ * for establishing secure communication channels between dApps and wallet extensions.
6
+ *
7
+ * ## Security Model
8
+ *
9
+ * The crypto flow uses HKDF for key derivation with domain separation:
10
+ *
11
+ * 1. Both parties generate ECDH key pairs using {@link generateKeyPair}
12
+ * 2. Public keys are exchanged (exported via {@link exportPublicKey}, imported via {@link importPublicKey})
13
+ * 3. Both parties derive keys using {@link deriveSessionKeys}:
14
+ * - ECDH produces raw shared secret
15
+ * - HKDF expands the secret into 512 bits using concatenated public keys as salt
16
+ * - The 512 bits are split: first 256 bits for AES-GCM, second 256 bits for HMAC
17
+ * 4. Fingerprint is computed as HMAC(HMAC_KEY, "aztec-wallet-verification-verificationHash")
18
+ * 5. Messages are encrypted/decrypted using {@link encrypt} and {@link decrypt}
19
+ *
20
+ * This design ensures:
21
+ * - The encryption key is never exposed (verificationHash uses separate HMAC key)
22
+ * - Public keys are bound to the derived keys via HKDF salt
23
+ * - Single HKDF derivation with domain-separated output splitting
24
+ *
25
+ * ## Curve Choice
26
+ *
27
+ * We use P-256 (secp256r1) because it's the only ECDH curve with broad Web Crypto API
28
+ * support across all browsers. X25519 would be preferable for its simplicity and
29
+ * resistance to implementation errors, but it lacks universal browser support.
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // Party A (dApp)
34
+ * const keyPairA = await generateKeyPair();
35
+ * const publicKeyA = await exportPublicKey(keyPairA.publicKey);
36
+ *
37
+ * // Party B (wallet)
38
+ * const keyPairB = await generateKeyPair();
39
+ * const publicKeyB = await exportPublicKey(keyPairB.publicKey);
40
+ *
41
+ * // Exchange public keys, then derive session keys
42
+ * // App side: isApp = true
43
+ * const importedB = await importPublicKey(publicKeyB);
44
+ * const sessionA = await deriveSessionKeys(keyPairA, importedB, true);
45
+ *
46
+ * // Wallet side: isApp = false
47
+ * const importedA = await importPublicKey(publicKeyA);
48
+ * const sessionB = await deriveSessionKeys(keyPairB, importedA, false);
49
+ *
50
+ * // Both parties compute the same verificationHash for verification
51
+ * const verificationHashA = sessionA.verificationHash;
52
+ * const emojiA = hashToEmoji(verificationHashA);
53
+ *
54
+ * // Encrypt and decrypt
55
+ * const encrypted = await encrypt(sessionA.encryptionKey, JSON.stringify({ message: 'hello' }));
56
+ * const decrypted = await decrypt(sessionB.encryptionKey, encrypted);
57
+ * ```
58
+ *
59
+ * @packageDocumentation
60
+ */
61
+ import { EMOJI_ALPHABET, EMOJI_ALPHABET_SIZE } from './emoji_alphabet.js';
62
+
63
+ /**
64
+ * Exported public key in JWK format for transmission over untrusted channels.
65
+ *
66
+ * Contains only the public components of an ECDH P-256 key, safe to share.
67
+ */
68
+ export interface ExportedPublicKey {
69
+ /** Key type - always "EC" for elliptic curve */
70
+ kty: string;
71
+ /** Curve name - always "P-256" */
72
+ crv: string;
73
+ /** X coordinate (base64url encoded) */
74
+ x: string;
75
+ /** Y coordinate (base64url encoded) */
76
+ y: string;
77
+ }
78
+
79
+ /**
80
+ * Encrypted message payload containing ciphertext and initialization vector.
81
+ *
82
+ * Both fields are base64-encoded for safe transmission as JSON.
83
+ */
84
+ export interface EncryptedPayload {
85
+ /** Initialization vector (base64 encoded, 12 bytes) */
86
+ iv: string;
87
+ /** Ciphertext (base64 encoded) */
88
+ ciphertext: string;
89
+ }
90
+
91
+ /**
92
+ * ECDH key pair for secure communication.
93
+ *
94
+ * The private key should never be exported or transmitted.
95
+ * The public key can be exported via {@link exportPublicKey} for exchange.
96
+ */
97
+ export interface SecureKeyPair {
98
+ /** Public key - safe to share */
99
+ publicKey: CryptoKey;
100
+ /** Private key - keep secret, used for key derivation */
101
+ privateKey: CryptoKey;
102
+ }
103
+
104
+ /**
105
+ * Session keys derived from ECDH key exchange.
106
+ *
107
+ * Contains both the encryption key and the verification hash (verificationHash)
108
+ * computed from a separate HMAC key.
109
+ */
110
+ export interface SessionKeys {
111
+ /** AES-256-GCM key for message encryption/decryption */
112
+ encryptionKey: CryptoKey;
113
+ /** Hex-encoded verificationHash for verification */
114
+ verificationHash: string;
115
+ }
116
+
117
+ /** P-256 coordinate size in bytes */
118
+ const P256_COORDINATE_SIZE = 32;
119
+
120
+ // HKDF info string for key derivation
121
+ const HKDF_INFO = new TextEncoder().encode('Aztec Wallet DAPP Key derivation');
122
+ const FINGERPRINT_DATA = new TextEncoder().encode('aztec-wallet-verification-verificationHash');
123
+
124
+ /**
125
+ * Generates an ECDH P-256 key pair for key exchange.
126
+ *
127
+ * The generated key pair can be used to derive session keys with another
128
+ * party's public key using {@link deriveSessionKeys}.
129
+ *
130
+ * @returns A new ECDH key pair
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * const keyPair = await generateKeyPair();
135
+ * const publicKey = await exportPublicKey(keyPair.publicKey);
136
+ * // Send publicKey to the other party
137
+ * ```
138
+ */
139
+ export async function generateKeyPair(): Promise<SecureKeyPair> {
140
+ const keyPair = await crypto.subtle.generateKey(
141
+ {
142
+ name: 'ECDH',
143
+ namedCurve: 'P-256',
144
+ },
145
+ true, // extractable (needed to export public key and derive bits)
146
+ ['deriveBits'],
147
+ );
148
+ return {
149
+ publicKey: keyPair.publicKey,
150
+ privateKey: keyPair.privateKey,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Exports a public key to JWK format for transmission.
156
+ *
157
+ * The exported key contains only public components and is safe to transmit
158
+ * over untrusted channels.
159
+ *
160
+ * @param publicKey - The CryptoKey public key to export
161
+ * @returns The public key in JWK format
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const keyPair = await generateKeyPair();
166
+ * const exported = await exportPublicKey(keyPair.publicKey);
167
+ * // exported can be JSON serialized and sent to another party
168
+ * ```
169
+ */
170
+ export async function exportPublicKey(publicKey: CryptoKey): Promise<ExportedPublicKey> {
171
+ const jwk = await crypto.subtle.exportKey('jwk', publicKey);
172
+ return {
173
+ kty: jwk.kty!,
174
+ crv: jwk.crv!,
175
+ x: jwk.x!,
176
+ y: jwk.y!,
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Imports a public key from JWK format.
182
+ *
183
+ * Used to import the other party's public key for deriving session keys.
184
+ *
185
+ * @param exported - The public key in JWK format
186
+ * @returns A CryptoKey that can be used with {@link deriveSessionKeys}
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * // App side: receive wallet's public key and derive session
191
+ * const walletPublicKey = await importPublicKey(receivedWalletKey);
192
+ * const session = await deriveSessionKeys(appKeyPair, walletPublicKey, true);
193
+ * ```
194
+ */
195
+ export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey> {
196
+ return crypto.subtle.importKey(
197
+ 'jwk',
198
+ {
199
+ kty: exported.kty,
200
+ crv: exported.crv,
201
+ x: exported.x,
202
+ y: exported.y,
203
+ },
204
+ {
205
+ name: 'ECDH',
206
+ namedCurve: 'P-256',
207
+ },
208
+ true, // extractable - needed for deriveSessionKeys to export for salt. Safe for public keys.
209
+ [],
210
+ );
211
+ }
212
+
213
+ /**
214
+ * Decodes a base64url-encoded coordinate to fixed-size bytes.
215
+ *
216
+ * For P-256, coordinates are always 32 bytes. This function ensures
217
+ * consistent serialization regardless of leading zeros.
218
+ *
219
+ * @param base64url - Base64url-encoded coordinate
220
+ * @param size - Expected size in bytes (32 for P-256)
221
+ * @returns Fixed-size Uint8Array, left-padded with zeros if needed
222
+ */
223
+ function decodeCoordinateFixedSize(base64url: string, size: number): Uint8Array {
224
+ const decoded = base64UrlToBytes(base64url);
225
+ if (decoded.length === size) {
226
+ return decoded;
227
+ }
228
+ if (decoded.length > size) {
229
+ throw new Error(`Invalid P-256 coordinate: expected ${size} bytes, got ${decoded.length}`);
230
+ }
231
+ // Left-pad with zeros
232
+ const padded = new Uint8Array(size);
233
+ padded.set(decoded, size - decoded.length);
234
+ return padded;
235
+ }
236
+
237
+ /**
238
+ * Creates HKDF salt from public keys with fixed ordering by party role.
239
+ *
240
+ * The app's public key always comes first, followed by the wallet's public key.
241
+ * This ensures both parties produce the same salt.
242
+ *
243
+ * @param appKey - The app's public key in exported format
244
+ * @param walletKey - The wallet's public key in exported format
245
+ * @returns Concatenated bytes: app_x || app_y || wallet_x || wallet_y (128 bytes for P-256)
246
+ */
247
+ function createSaltFromPublicKeys(appKey: ExportedPublicKey, walletKey: ExportedPublicKey): ArrayBuffer {
248
+ // Fixed ordering: app first, then wallet
249
+ // Each coordinate is fixed at 32 bytes for P-256
250
+ const appX = decodeCoordinateFixedSize(appKey.x, P256_COORDINATE_SIZE);
251
+ const appY = decodeCoordinateFixedSize(appKey.y, P256_COORDINATE_SIZE);
252
+ const walletX = decodeCoordinateFixedSize(walletKey.x, P256_COORDINATE_SIZE);
253
+ const walletY = decodeCoordinateFixedSize(walletKey.y, P256_COORDINATE_SIZE);
254
+
255
+ // Total: 4 * 32 = 128 bytes
256
+ const salt = new Uint8Array(4 * P256_COORDINATE_SIZE);
257
+ salt.set(appX, 0);
258
+ salt.set(appY, P256_COORDINATE_SIZE);
259
+ salt.set(walletX, 2 * P256_COORDINATE_SIZE);
260
+ salt.set(walletY, 3 * P256_COORDINATE_SIZE);
261
+
262
+ return salt.buffer as ArrayBuffer;
263
+ }
264
+
265
+ /**
266
+ * Derives session keys from ECDH key exchange using HKDF.
267
+ *
268
+ * This is the main key derivation function that produces:
269
+ * 1. An AES-256-GCM encryption key (first 256 bits)
270
+ * 2. An HMAC key for verificationHash computation (second 256 bits)
271
+ * 3. A verificationHash computed as HMAC(hmacKey, "aztec-wallet-verification-verificationHash")
272
+ *
273
+ * The keys are derived using a single HKDF call that produces 512 bits,
274
+ * then split into the two keys.
275
+ *
276
+ * @param ownKeyPair - The caller's ECDH key pair (private for ECDH, public for salt)
277
+ * @param peerPublicKey - The peer's ECDH public key (for ECDH and salt)
278
+ * @param isApp - true if caller is the app, false if caller is the wallet
279
+ * @returns Session keys containing encryption key and verificationHash
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * // App side
284
+ * const sessionA = await deriveSessionKeys(appKeyPair, walletPublicKey, true);
285
+ * // Wallet side
286
+ * const sessionB = await deriveSessionKeys(walletKeyPair, appPublicKey, false);
287
+ * // sessionA.verificationHash === sessionB.verificationHash
288
+ * ```
289
+ */
290
+ export async function deriveSessionKeys(
291
+ ownKeyPair: SecureKeyPair,
292
+ peerPublicKey: CryptoKey,
293
+ isApp: boolean,
294
+ ): Promise<SessionKeys> {
295
+ // Step 1: ECDH to get raw shared secret
296
+ const sharedSecretBits = await crypto.subtle.deriveBits(
297
+ {
298
+ name: 'ECDH',
299
+ public: peerPublicKey,
300
+ },
301
+ ownKeyPair.privateKey,
302
+ 256,
303
+ );
304
+
305
+ // Step 2: Import shared secret as HKDF key material
306
+ const hkdfKey = await crypto.subtle.importKey('raw', sharedSecretBits, { name: 'HKDF' }, false, ['deriveBits']);
307
+
308
+ // Step 3: Export public keys and create salt (app first, wallet second)
309
+ const ownExportedKey = await exportPublicKey(ownKeyPair.publicKey);
310
+ const peerExportedKey = await exportPublicKey(peerPublicKey);
311
+ const appPublicKey = isApp ? ownExportedKey : peerExportedKey;
312
+ const walletPublicKey = isApp ? peerExportedKey : ownExportedKey;
313
+ const salt = createSaltFromPublicKeys(appPublicKey, walletPublicKey);
314
+
315
+ // Step 4: Derive 512 bits in a single HKDF call
316
+ const derivedBits = await crypto.subtle.deriveBits(
317
+ {
318
+ name: 'HKDF',
319
+ hash: 'SHA-256',
320
+ salt,
321
+ info: HKDF_INFO,
322
+ },
323
+ hkdfKey,
324
+ 512, // 256 bits for GCM + 256 bits for HMAC
325
+ );
326
+
327
+ // Step 5: Split into GCM key (first 256 bits) and HMAC key (second 256 bits)
328
+ const gcmKeyBits = derivedBits.slice(0, 32);
329
+ const hmacKeyBits = derivedBits.slice(32, 64);
330
+
331
+ // Step 6: Import GCM key
332
+ const encryptionKey = await crypto.subtle.importKey('raw', gcmKeyBits, { name: 'AES-GCM', length: 256 }, false, [
333
+ 'encrypt',
334
+ 'decrypt',
335
+ ]);
336
+
337
+ // Step 7: Import HMAC key
338
+ const hmacKey = await crypto.subtle.importKey('raw', hmacKeyBits, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
339
+
340
+ // Step 8: Compute verificationHash as HMAC of fixed string
341
+ const verificationHashBytes = await crypto.subtle.sign('HMAC', hmacKey, FINGERPRINT_DATA);
342
+
343
+ // Convert to hex string
344
+ const verificationHash = arrayBufferToHex(verificationHashBytes);
345
+
346
+ return {
347
+ encryptionKey,
348
+ verificationHash,
349
+ };
350
+ }
351
+
352
+ /**
353
+ * Encrypts data using AES-256-GCM.
354
+ *
355
+ * A random 12-byte IV is generated for each encryption operation.
356
+ *
357
+ * AES-GCM provides both confidentiality and authenticity - any tampering
358
+ * with the ciphertext will cause decryption to fail.
359
+ *
360
+ * @param key - The AES-GCM key (from {@link deriveSessionKeys})
361
+ * @param data - The string data to encrypt (caller is responsible for serialization)
362
+ * @returns The encrypted payload with IV and ciphertext
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * const encrypted = await encrypt(session.encryptionKey, JSON.stringify({ action: 'transfer', amount: 100 }));
367
+ * // encrypted.iv and encrypted.ciphertext are base64 strings
368
+ * ```
369
+ */
370
+ export async function encrypt(key: CryptoKey, data: string): Promise<EncryptedPayload> {
371
+ const iv = crypto.getRandomValues(new Uint8Array(12));
372
+ const encoded = new TextEncoder().encode(data);
373
+
374
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
375
+
376
+ return {
377
+ iv: arrayBufferToBase64(iv),
378
+ ciphertext: arrayBufferToBase64(ciphertext),
379
+ };
380
+ }
381
+
382
+ /**
383
+ * Decrypts data using AES-256-GCM.
384
+ *
385
+ * The decrypted data is JSON parsed before returning.
386
+ *
387
+ * @typeParam T - The expected type of the decrypted data
388
+ * @param key - The AES-GCM key (from {@link deriveSessionKeys})
389
+ * @param payload - The encrypted payload from {@link encrypt}
390
+ * @returns The decrypted and parsed data
391
+ *
392
+ * @throws Error if decryption fails (wrong key or tampered ciphertext)
393
+ *
394
+ * @example
395
+ * ```typescript
396
+ * const decrypted = await decrypt<{ action: string; amount: number }>(session.encryptionKey, encrypted);
397
+ * console.log(decrypted.action); // 'transfer'
398
+ * ```
399
+ */
400
+ export async function decrypt<T = unknown>(key: CryptoKey, payload: EncryptedPayload): Promise<T> {
401
+ const iv = base64ToArrayBuffer(payload.iv);
402
+ const ciphertext = base64ToArrayBuffer(payload.ciphertext);
403
+
404
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
405
+
406
+ const decoded = new TextDecoder().decode(decrypted);
407
+ return JSON.parse(decoded) as T;
408
+ }
409
+
410
+ /**
411
+ * Converts ArrayBuffer to base64 string.
412
+ * @internal
413
+ */
414
+ function arrayBufferToBase64(buffer: ArrayBuffer | Uint8Array): string {
415
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
416
+ let binary = '';
417
+ for (let i = 0; i < bytes.byteLength; i++) {
418
+ binary += String.fromCharCode(bytes[i]);
419
+ }
420
+ return btoa(binary);
421
+ }
422
+
423
+ /**
424
+ * Converts base64 string to ArrayBuffer.
425
+ * @internal
426
+ */
427
+ function base64ToArrayBuffer(base64: string): ArrayBuffer {
428
+ const binary = atob(base64);
429
+ const bytes = new Uint8Array(binary.length);
430
+ for (let i = 0; i < binary.length; i++) {
431
+ bytes[i] = binary.charCodeAt(i);
432
+ }
433
+ return bytes.buffer;
434
+ }
435
+
436
+ /**
437
+ * Converts base64url string to Uint8Array.
438
+ * @internal
439
+ */
440
+ function base64UrlToBytes(base64url: string): Uint8Array {
441
+ // Convert base64url to base64
442
+ const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
443
+ // Add padding if needed
444
+ const padded = base64 + '='.repeat((4 - (base64.length % 4)) % 4);
445
+ const binary = atob(padded);
446
+ const bytes = new Uint8Array(binary.length);
447
+ for (let i = 0; i < binary.length; i++) {
448
+ bytes[i] = binary.charCodeAt(i);
449
+ }
450
+ return bytes;
451
+ }
452
+
453
+ /**
454
+ * Converts ArrayBuffer to hex string.
455
+ * @internal
456
+ */
457
+ function arrayBufferToHex(buffer: ArrayBuffer): string {
458
+ const bytes = new Uint8Array(buffer);
459
+ return Array.from(bytes)
460
+ .map(b => b.toString(16).padStart(2, '0'))
461
+ .join('');
462
+ }
463
+
464
+ /**
465
+ * Default grid size for emoji verification display.
466
+ * 3x3 grid = 9 emojis = 72 bits of security.
467
+ */
468
+ export const DEFAULT_EMOJI_GRID_SIZE = 9;
469
+
470
+ /**
471
+ * Converts a hex hash to an emoji sequence for visual verification.
472
+ *
473
+ * This is used for verification - both the dApp and wallet
474
+ * independently compute the same emoji sequence from the derived keys.
475
+ * Users can visually compare the sequences to detect interception.
476
+ *
477
+ * With a 256-emoji alphabet and 9 emojis (3x3 grid), this provides
478
+ * 72 bits of security (9 * 8 = 72 bits), making brute-force attacks
479
+ * computationally infeasible.
480
+ *
481
+ * @param hash - Hex string from verification hash (64 chars = 32 bytes)
482
+ * @param count - Number of emojis to generate (default: 9 for 3x3 grid)
483
+ * @returns A string of emojis representing the hash
484
+ *
485
+ * @example
486
+ * ```typescript
487
+ * const session = await deriveSessionKeys(...);
488
+ * const emoji = hashToEmoji(session.verificationHash); // e.g., "πŸ”΅πŸ¦‹πŸŽ―πŸΌπŸŒŸπŸŽ²πŸ¦ŠπŸΈπŸ’Ž"
489
+ * // Display as 3x3 grid to user for verification
490
+ * ```
491
+ */
492
+ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZE): string {
493
+ const emojis: string[] = [];
494
+ for (let i = 0; i < hash.length && emojis.length < count; i += 2) {
495
+ const byteValue = parseInt(hash.slice(i, i + 2), 16);
496
+ emojis.push(EMOJI_ALPHABET[byteValue % EMOJI_ALPHABET_SIZE]);
497
+ }
498
+ return emojis.join('');
499
+ }