@aztec/wallet-sdk 0.0.1-commit.d1f2d6c → 0.0.1-commit.d431d1c
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.
- package/README.md +294 -217
- package/dest/base-wallet/base_wallet.d.ts +4 -4
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +4 -9
- package/dest/crypto.d.ts +50 -59
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +108 -202
- package/dest/manager/index.d.ts +8 -2
- package/dest/manager/index.d.ts.map +1 -1
- package/dest/manager/index.js +6 -0
- package/dest/manager/types.d.ts +6 -88
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/types.js +1 -17
- package/dest/manager/wallet_manager.d.ts +7 -50
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +44 -174
- package/dest/providers/extension/extension_provider.d.ts +63 -0
- package/dest/providers/extension/extension_provider.d.ts.map +1 -0
- package/dest/providers/extension/extension_provider.js +124 -0
- package/dest/providers/extension/extension_wallet.d.ts +155 -0
- package/dest/providers/extension/extension_wallet.d.ts.map +1 -0
- package/dest/{extension/provider → providers/extension}/extension_wallet.js +95 -48
- package/dest/providers/extension/index.d.ts +6 -0
- package/dest/providers/extension/index.d.ts.map +1 -0
- package/dest/{extension/provider → providers/extension}/index.js +2 -0
- package/dest/types.d.ts +12 -43
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +2 -3
- package/package.json +9 -10
- package/src/base-wallet/base_wallet.ts +8 -15
- package/src/crypto.ts +113 -237
- package/src/manager/index.ts +10 -2
- package/src/manager/types.ts +5 -91
- package/src/manager/wallet_manager.ts +46 -192
- package/src/providers/extension/extension_provider.ts +167 -0
- package/src/{extension/provider → providers/extension}/extension_wallet.ts +110 -52
- package/src/providers/extension/index.ts +5 -0
- package/src/types.ts +10 -44
- package/dest/emoji_alphabet.d.ts +0 -35
- package/dest/emoji_alphabet.d.ts.map +0 -1
- package/dest/emoji_alphabet.js +0 -299
- package/dest/extension/handlers/background_connection_handler.d.ts +0 -158
- package/dest/extension/handlers/background_connection_handler.d.ts.map +0 -1
- package/dest/extension/handlers/background_connection_handler.js +0 -258
- package/dest/extension/handlers/content_script_connection_handler.d.ts +0 -56
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +0 -1
- package/dest/extension/handlers/content_script_connection_handler.js +0 -174
- package/dest/extension/handlers/index.d.ts +0 -12
- package/dest/extension/handlers/index.d.ts.map +0 -1
- package/dest/extension/handlers/index.js +0 -10
- package/dest/extension/handlers/internal_message_types.d.ts +0 -63
- package/dest/extension/handlers/internal_message_types.d.ts.map +0 -1
- package/dest/extension/handlers/internal_message_types.js +0 -22
- package/dest/extension/provider/extension_provider.d.ts +0 -107
- package/dest/extension/provider/extension_provider.d.ts.map +0 -1
- package/dest/extension/provider/extension_provider.js +0 -160
- package/dest/extension/provider/extension_wallet.d.ts +0 -131
- package/dest/extension/provider/extension_wallet.d.ts.map +0 -1
- package/dest/extension/provider/index.d.ts +0 -3
- package/dest/extension/provider/index.d.ts.map +0 -1
- package/src/emoji_alphabet.ts +0 -317
- package/src/extension/handlers/background_connection_handler.ts +0 -423
- package/src/extension/handlers/content_script_connection_handler.ts +0 -246
- package/src/extension/handlers/index.ts +0 -25
- package/src/extension/handlers/internal_message_types.ts +0 -69
- package/src/extension/provider/extension_provider.ts +0 -233
- package/src/extension/provider/index.ts +0 -7
package/src/crypto.ts
CHANGED
|
@@ -4,61 +4,34 @@
|
|
|
4
4
|
* This module provides ECDH key exchange and AES-GCM encryption primitives
|
|
5
5
|
* for establishing secure communication channels between dApps and wallet extensions.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* The crypto flow uses HKDF for key derivation with domain separation:
|
|
10
|
-
*
|
|
7
|
+
* The crypto flow:
|
|
11
8
|
* 1. Both parties generate ECDH key pairs using {@link generateKeyPair}
|
|
12
9
|
* 2. Public keys are exchanged (exported via {@link exportPublicKey}, imported via {@link importPublicKey})
|
|
13
|
-
* 3. Both parties derive
|
|
14
|
-
*
|
|
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.
|
|
10
|
+
* 3. Both parties derive the same shared secret using {@link deriveSharedKey}
|
|
11
|
+
* 4. Messages are encrypted/decrypted using {@link encrypt} and {@link decrypt}
|
|
30
12
|
*
|
|
31
13
|
* @example
|
|
32
14
|
* ```typescript
|
|
33
|
-
* // Party A
|
|
15
|
+
* // Party A
|
|
34
16
|
* const keyPairA = await generateKeyPair();
|
|
35
17
|
* const publicKeyA = await exportPublicKey(keyPairA.publicKey);
|
|
36
18
|
*
|
|
37
|
-
* // Party B
|
|
19
|
+
* // Party B
|
|
38
20
|
* const keyPairB = await generateKeyPair();
|
|
39
21
|
* const publicKeyB = await exportPublicKey(keyPairB.publicKey);
|
|
40
22
|
*
|
|
41
|
-
* // Exchange public keys, then derive
|
|
42
|
-
* // App side: isApp = true
|
|
23
|
+
* // Exchange public keys, then derive shared secret
|
|
43
24
|
* const importedB = await importPublicKey(publicKeyB);
|
|
44
|
-
* const
|
|
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);
|
|
25
|
+
* const sharedKeyA = await deriveSharedKey(keyPairA.privateKey, importedB);
|
|
53
26
|
*
|
|
54
27
|
* // Encrypt and decrypt
|
|
55
|
-
* const encrypted = await encrypt(
|
|
56
|
-
* const decrypted = await decrypt(
|
|
28
|
+
* const encrypted = await encrypt(sharedKeyA, { message: 'hello' });
|
|
29
|
+
* const decrypted = await decrypt(sharedKeyB, encrypted);
|
|
57
30
|
* ```
|
|
58
31
|
*
|
|
59
32
|
* @packageDocumentation
|
|
60
33
|
*/
|
|
61
|
-
import {
|
|
34
|
+
import { jsonStringify } from '@aztec/foundation/json-rpc';
|
|
62
35
|
|
|
63
36
|
/**
|
|
64
37
|
* Exported public key in JWK format for transmission over untrusted channels.
|
|
@@ -101,31 +74,11 @@ export interface SecureKeyPair {
|
|
|
101
74
|
privateKey: CryptoKey;
|
|
102
75
|
}
|
|
103
76
|
|
|
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
77
|
/**
|
|
125
78
|
* Generates an ECDH P-256 key pair for key exchange.
|
|
126
79
|
*
|
|
127
|
-
* The generated key pair can be used to derive
|
|
128
|
-
* party's public key using {@link
|
|
80
|
+
* The generated key pair can be used to derive a shared secret with another
|
|
81
|
+
* party's public key using {@link deriveSharedKey}.
|
|
129
82
|
*
|
|
130
83
|
* @returns A new ECDH key pair
|
|
131
84
|
*
|
|
@@ -142,8 +95,8 @@ export async function generateKeyPair(): Promise<SecureKeyPair> {
|
|
|
142
95
|
name: 'ECDH',
|
|
143
96
|
namedCurve: 'P-256',
|
|
144
97
|
},
|
|
145
|
-
true, // extractable (needed to export public key
|
|
146
|
-
['
|
|
98
|
+
true, // extractable (needed to export public key)
|
|
99
|
+
['deriveKey'],
|
|
147
100
|
);
|
|
148
101
|
return {
|
|
149
102
|
publicKey: keyPair.publicKey,
|
|
@@ -180,16 +133,16 @@ export async function exportPublicKey(publicKey: CryptoKey): Promise<ExportedPub
|
|
|
180
133
|
/**
|
|
181
134
|
* Imports a public key from JWK format.
|
|
182
135
|
*
|
|
183
|
-
* Used to import the other party's public key for deriving
|
|
136
|
+
* Used to import the other party's public key for deriving a shared secret.
|
|
184
137
|
*
|
|
185
138
|
* @param exported - The public key in JWK format
|
|
186
|
-
* @returns A CryptoKey that can be used with {@link
|
|
139
|
+
* @returns A CryptoKey that can be used with {@link deriveSharedKey}
|
|
187
140
|
*
|
|
188
141
|
* @example
|
|
189
142
|
* ```typescript
|
|
190
|
-
* //
|
|
191
|
-
* const
|
|
192
|
-
* const
|
|
143
|
+
* // Receive exported public key from other party
|
|
144
|
+
* const theirPublicKey = await importPublicKey(receivedPublicKey);
|
|
145
|
+
* const sharedKey = await deriveSharedKey(myPrivateKey, theirPublicKey);
|
|
193
146
|
* ```
|
|
194
147
|
*/
|
|
195
148
|
export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey> {
|
|
@@ -205,171 +158,67 @@ export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey>
|
|
|
205
158
|
name: 'ECDH',
|
|
206
159
|
namedCurve: 'P-256',
|
|
207
160
|
},
|
|
208
|
-
|
|
161
|
+
false,
|
|
209
162
|
[],
|
|
210
163
|
);
|
|
211
164
|
}
|
|
212
165
|
|
|
213
166
|
/**
|
|
214
|
-
*
|
|
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")
|
|
167
|
+
* Derives a shared AES-256-GCM key from ECDH key exchange.
|
|
272
168
|
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
169
|
+
* Both parties will derive the same shared key when using their own private key
|
|
170
|
+
* and the other party's public key. This is the core of ECDH key agreement.
|
|
275
171
|
*
|
|
276
|
-
* @param
|
|
277
|
-
* @param
|
|
278
|
-
* @
|
|
279
|
-
* @returns Session keys containing encryption key and verificationHash
|
|
172
|
+
* @param privateKey - Your ECDH private key
|
|
173
|
+
* @param publicKey - The other party's ECDH public key
|
|
174
|
+
* @returns An AES-256-GCM key for encryption/decryption
|
|
280
175
|
*
|
|
281
176
|
* @example
|
|
282
177
|
* ```typescript
|
|
283
|
-
* //
|
|
284
|
-
* const
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* // sessionA.verificationHash === sessionB.verificationHash
|
|
178
|
+
* // Both parties derive the same key
|
|
179
|
+
* const sharedKeyA = await deriveSharedKey(privateKeyA, publicKeyB);
|
|
180
|
+
* const sharedKeyB = await deriveSharedKey(privateKeyB, publicKeyA);
|
|
181
|
+
* // sharedKeyA and sharedKeyB are equivalent
|
|
288
182
|
* ```
|
|
289
183
|
*/
|
|
290
|
-
export
|
|
291
|
-
|
|
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(
|
|
184
|
+
export function deriveSharedKey(privateKey: CryptoKey, publicKey: CryptoKey): Promise<CryptoKey> {
|
|
185
|
+
return crypto.subtle.deriveKey(
|
|
297
186
|
{
|
|
298
187
|
name: 'ECDH',
|
|
299
|
-
public:
|
|
188
|
+
public: publicKey,
|
|
300
189
|
},
|
|
301
|
-
|
|
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(
|
|
190
|
+
privateKey,
|
|
317
191
|
{
|
|
318
|
-
name: '
|
|
319
|
-
|
|
320
|
-
salt,
|
|
321
|
-
info: HKDF_INFO,
|
|
192
|
+
name: 'AES-GCM',
|
|
193
|
+
length: 256,
|
|
322
194
|
},
|
|
323
|
-
|
|
324
|
-
|
|
195
|
+
true, // extractable - needed for hashing
|
|
196
|
+
['encrypt', 'decrypt'],
|
|
325
197
|
);
|
|
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
198
|
}
|
|
351
199
|
|
|
352
200
|
/**
|
|
353
201
|
* Encrypts data using AES-256-GCM.
|
|
354
202
|
*
|
|
355
|
-
* A random 12-byte IV is
|
|
203
|
+
* The data is JSON serialized before encryption. A random 12-byte IV is
|
|
204
|
+
* generated for each encryption operation.
|
|
356
205
|
*
|
|
357
206
|
* AES-GCM provides both confidentiality and authenticity - any tampering
|
|
358
207
|
* with the ciphertext will cause decryption to fail.
|
|
359
208
|
*
|
|
360
|
-
* @param key - The AES-GCM key (from {@link
|
|
361
|
-
* @param data - The
|
|
209
|
+
* @param key - The AES-GCM key (from {@link deriveSharedKey})
|
|
210
|
+
* @param data - The data to encrypt (will be JSON serialized)
|
|
362
211
|
* @returns The encrypted payload with IV and ciphertext
|
|
363
212
|
*
|
|
364
213
|
* @example
|
|
365
214
|
* ```typescript
|
|
366
|
-
* const encrypted = await encrypt(
|
|
215
|
+
* const encrypted = await encrypt(sharedKey, { action: 'transfer', amount: 100 });
|
|
367
216
|
* // encrypted.iv and encrypted.ciphertext are base64 strings
|
|
368
217
|
* ```
|
|
369
218
|
*/
|
|
370
|
-
export async function encrypt(key: CryptoKey, data:
|
|
219
|
+
export async function encrypt(key: CryptoKey, data: unknown): Promise<EncryptedPayload> {
|
|
371
220
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
372
|
-
const encoded = new TextEncoder().encode(data);
|
|
221
|
+
const encoded = new TextEncoder().encode(jsonStringify(data));
|
|
373
222
|
|
|
374
223
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
|
|
375
224
|
|
|
@@ -385,7 +234,7 @@ export async function encrypt(key: CryptoKey, data: string): Promise<EncryptedPa
|
|
|
385
234
|
* The decrypted data is JSON parsed before returning.
|
|
386
235
|
*
|
|
387
236
|
* @typeParam T - The expected type of the decrypted data
|
|
388
|
-
* @param key - The AES-GCM key (from {@link
|
|
237
|
+
* @param key - The AES-GCM key (from {@link deriveSharedKey})
|
|
389
238
|
* @param payload - The encrypted payload from {@link encrypt}
|
|
390
239
|
* @returns The decrypted and parsed data
|
|
391
240
|
*
|
|
@@ -393,7 +242,7 @@ export async function encrypt(key: CryptoKey, data: string): Promise<EncryptedPa
|
|
|
393
242
|
*
|
|
394
243
|
* @example
|
|
395
244
|
* ```typescript
|
|
396
|
-
* const decrypted = await decrypt<{ action: string; amount: number }>(
|
|
245
|
+
* const decrypted = await decrypt<{ action: string; amount: number }>(sharedKey, encrypted);
|
|
397
246
|
* console.log(decrypted.action); // 'transfer'
|
|
398
247
|
* ```
|
|
399
248
|
*/
|
|
@@ -434,66 +283,93 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
|
434
283
|
}
|
|
435
284
|
|
|
436
285
|
/**
|
|
437
|
-
*
|
|
286
|
+
* Emoji alphabet for visual verification of shared secrets.
|
|
287
|
+
* 32 distinct, easily recognizable emojis for anti-spoofing verification.
|
|
438
288
|
* @internal
|
|
439
289
|
*/
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
290
|
+
const EMOJI_ALPHABET = [
|
|
291
|
+
'🔵',
|
|
292
|
+
'🟢',
|
|
293
|
+
'🔴',
|
|
294
|
+
'🟡',
|
|
295
|
+
'🟣',
|
|
296
|
+
'🟠',
|
|
297
|
+
'⚫',
|
|
298
|
+
'⚪',
|
|
299
|
+
'🌟',
|
|
300
|
+
'🌙',
|
|
301
|
+
'☀️',
|
|
302
|
+
'🌈',
|
|
303
|
+
'🔥',
|
|
304
|
+
'💧',
|
|
305
|
+
'🌸',
|
|
306
|
+
'🍀',
|
|
307
|
+
'🦋',
|
|
308
|
+
'🐬',
|
|
309
|
+
'🦊',
|
|
310
|
+
'🐼',
|
|
311
|
+
'🦁',
|
|
312
|
+
'🐯',
|
|
313
|
+
'🐸',
|
|
314
|
+
'🦉',
|
|
315
|
+
'🎵',
|
|
316
|
+
'🎨',
|
|
317
|
+
'🎯',
|
|
318
|
+
'🎲',
|
|
319
|
+
'🔔',
|
|
320
|
+
'💎',
|
|
321
|
+
'🔑',
|
|
322
|
+
'🏆',
|
|
323
|
+
];
|
|
452
324
|
|
|
453
325
|
/**
|
|
454
|
-
*
|
|
455
|
-
*
|
|
326
|
+
* Hashes a shared AES key to a hex string for verification.
|
|
327
|
+
*
|
|
328
|
+
* This extracts the raw key material and hashes it with SHA-256,
|
|
329
|
+
* returning the first 16 bytes as a hex string.
|
|
330
|
+
*
|
|
331
|
+
* @param sharedKey - The AES-GCM shared key (must be extractable)
|
|
332
|
+
* @returns A hex string representation of the hash
|
|
333
|
+
*
|
|
334
|
+
* @example
|
|
335
|
+
* ```typescript
|
|
336
|
+
* const hash = await hashSharedSecret(sharedKey);
|
|
337
|
+
* const emoji = hashToEmoji(hash);
|
|
338
|
+
* ```
|
|
456
339
|
*/
|
|
457
|
-
function
|
|
458
|
-
const
|
|
340
|
+
export async function hashSharedSecret(sharedKey: CryptoKey): Promise<string> {
|
|
341
|
+
const rawKey = await crypto.subtle.exportKey('raw', sharedKey);
|
|
342
|
+
const hash = await crypto.subtle.digest('SHA-256', rawKey);
|
|
343
|
+
const bytes = new Uint8Array(hash.slice(0, 16));
|
|
459
344
|
return Array.from(bytes)
|
|
460
345
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
461
346
|
.join('');
|
|
462
347
|
}
|
|
463
348
|
|
|
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
349
|
/**
|
|
471
350
|
* Converts a hex hash to an emoji sequence for visual verification.
|
|
472
351
|
*
|
|
473
|
-
* This is used for verification - both the dApp and wallet
|
|
474
|
-
* independently compute the same emoji sequence from the
|
|
352
|
+
* This is used for anti-MITM verification - both the dApp and wallet
|
|
353
|
+
* independently compute the same emoji sequence from the shared secret.
|
|
475
354
|
* Users can visually compare the sequences to detect interception.
|
|
476
355
|
*
|
|
477
|
-
*
|
|
478
|
-
* 72 bits of security (9 * 8 = 72 bits), making brute-force attacks
|
|
479
|
-
* computationally infeasible.
|
|
356
|
+
* Similar to SAS (Short Authentication String) in ZRTP/Signal.
|
|
480
357
|
*
|
|
481
|
-
* @param hash - Hex string from
|
|
482
|
-
* @param
|
|
358
|
+
* @param hash - Hex string from {@link hashSharedSecret}
|
|
359
|
+
* @param length - Number of emojis to generate (default: 4)
|
|
483
360
|
* @returns A string of emojis representing the hash
|
|
484
361
|
*
|
|
485
362
|
* @example
|
|
486
363
|
* ```typescript
|
|
487
|
-
* const
|
|
488
|
-
* const emoji = hashToEmoji(
|
|
489
|
-
* // Display
|
|
364
|
+
* const hash = await hashSharedSecret(sharedKey);
|
|
365
|
+
* const emoji = hashToEmoji(hash); // e.g., "🔵🦋🎯🐼"
|
|
366
|
+
* // Display to user for verification
|
|
490
367
|
* ```
|
|
491
368
|
*/
|
|
492
|
-
export function hashToEmoji(hash: string,
|
|
493
|
-
const
|
|
494
|
-
for (let i = 0; i < hash.length &&
|
|
495
|
-
|
|
496
|
-
emojis.push(EMOJI_ALPHABET[byteValue % EMOJI_ALPHABET_SIZE]);
|
|
369
|
+
export function hashToEmoji(hash: string, length: number = 4): string {
|
|
370
|
+
const bytes: number[] = [];
|
|
371
|
+
for (let i = 0; i < hash.length && bytes.length < length; i += 2) {
|
|
372
|
+
bytes.push(parseInt(hash.slice(i, i + 2), 16));
|
|
497
373
|
}
|
|
498
|
-
return
|
|
374
|
+
return bytes.map(b => EMOJI_ALPHABET[b % EMOJI_ALPHABET.length]).join('');
|
|
499
375
|
}
|
package/src/manager/index.ts
CHANGED
|
@@ -5,8 +5,16 @@ export type {
|
|
|
5
5
|
WebWalletConfig,
|
|
6
6
|
WalletProviderType,
|
|
7
7
|
WalletProvider,
|
|
8
|
-
PendingConnection,
|
|
9
8
|
ProviderDisconnectionCallback,
|
|
10
9
|
DiscoverWalletsOptions,
|
|
11
|
-
DiscoverySession,
|
|
12
10
|
} from './types.js';
|
|
11
|
+
|
|
12
|
+
// Re-export types and enums from providers for convenience
|
|
13
|
+
export { WalletMessageType } from '../types.js';
|
|
14
|
+
export type { WalletInfo, WalletMessage, WalletResponse, DiscoveryRequest, DiscoveryResponse } from '../types.js';
|
|
15
|
+
|
|
16
|
+
// Re-export commonly needed utilities for wallet integration
|
|
17
|
+
export { ChainInfoSchema } from '@aztec/aztec.js/account';
|
|
18
|
+
export type { ChainInfo } from '@aztec/aztec.js/account';
|
|
19
|
+
export { WalletSchema } from '@aztec/aztec.js/wallet';
|
|
20
|
+
export { jsonStringify } from '@aztec/foundation/json-rpc';
|
package/src/manager/types.ts
CHANGED
|
@@ -1,37 +1,6 @@
|
|
|
1
1
|
import type { ChainInfo } from '@aztec/aztec.js/account';
|
|
2
2
|
import type { Wallet } from '@aztec/aztec.js/wallet';
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* A pending connection that requires user verification before finalizing.
|
|
6
|
-
*
|
|
7
|
-
* After key exchange, both dApp and wallet compute a verification hash independently.
|
|
8
|
-
* The dApp should display this hash (typically as emojis) and let the user confirm
|
|
9
|
-
* it matches what's shown in their wallet before calling `confirm()`.
|
|
10
|
-
*
|
|
11
|
-
* This protects the dApp from connecting to a malicious wallet that might be
|
|
12
|
-
* intercepting the connection (MITM attack).
|
|
13
|
-
*/
|
|
14
|
-
export interface PendingConnection {
|
|
15
|
-
/**
|
|
16
|
-
* The verification hash computed from the shared secret.
|
|
17
|
-
* Use `hashToEmoji()` to convert to a visual representation for user verification.
|
|
18
|
-
* The user should confirm this matches what their wallet displays.
|
|
19
|
-
*/
|
|
20
|
-
verificationHash: string;
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Confirms the connection after user verifies the emojis match.
|
|
24
|
-
* @returns The connected wallet instance
|
|
25
|
-
*/
|
|
26
|
-
confirm(): Promise<Wallet>;
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Cancels the pending connection.
|
|
30
|
-
* Call this if the user indicates the emojis don't match.
|
|
31
|
-
*/
|
|
32
|
-
cancel(): void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
4
|
/**
|
|
36
5
|
* Configuration for extension wallets
|
|
37
6
|
*/
|
|
@@ -65,7 +34,7 @@ export interface WalletManagerConfig {
|
|
|
65
34
|
/**
|
|
66
35
|
* Type of wallet provider
|
|
67
36
|
*/
|
|
68
|
-
export type WalletProviderType = 'extension' | 'web';
|
|
37
|
+
export type WalletProviderType = 'extension' | 'web' | 'embedded';
|
|
69
38
|
|
|
70
39
|
/**
|
|
71
40
|
* Callback type for wallet disconnect events at the provider level.
|
|
@@ -88,32 +57,13 @@ export interface WalletProvider {
|
|
|
88
57
|
/** Additional metadata */
|
|
89
58
|
metadata?: Record<string, unknown>;
|
|
90
59
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* This performs the ECDH key exchange and returns a pending connection with the
|
|
94
|
-
* verification hash. The channel is encrypted but NOT yet verified - the dApp
|
|
95
|
-
* should display the hash (as emojis) to the user and let them confirm it
|
|
96
|
-
* matches their wallet before calling `confirm()`.
|
|
97
|
-
*
|
|
60
|
+
* Connect to this wallet provider with an application ID
|
|
98
61
|
* @param appId - Application identifier for the requesting dapp
|
|
99
|
-
* @returns A pending connection with verification hash and confirm/cancel methods
|
|
100
|
-
*
|
|
101
|
-
* @example
|
|
102
|
-
* ```typescript
|
|
103
|
-
* const pending = await provider.establishSecureChannel('my-app');
|
|
104
|
-
*
|
|
105
|
-
* // Show emojis to user for verification
|
|
106
|
-
* const emojis = hashToEmoji(pending.verificationHash);
|
|
107
|
-
* showDialog(`Verify these match your wallet: ${emojis}`);
|
|
108
|
-
*
|
|
109
|
-
* // User confirms emojis match
|
|
110
|
-
* const wallet = await pending.confirm();
|
|
111
|
-
* ```
|
|
112
62
|
*/
|
|
113
|
-
|
|
63
|
+
connect(appId: string): Promise<Wallet>;
|
|
114
64
|
/**
|
|
115
65
|
* Disconnects the current wallet and cleans up resources.
|
|
116
|
-
* After calling this, the wallet returned from
|
|
66
|
+
* After calling this, the wallet returned from connect() should no longer be used.
|
|
117
67
|
* @returns A promise that resolves when disconnection is complete
|
|
118
68
|
*/
|
|
119
69
|
disconnect?(): Promise<void>;
|
|
@@ -136,42 +86,6 @@ export interface WalletProvider {
|
|
|
136
86
|
export interface DiscoverWalletsOptions {
|
|
137
87
|
/** Chain information to filter by */
|
|
138
88
|
chainInfo: ChainInfo;
|
|
139
|
-
/**
|
|
140
|
-
appId: string;
|
|
141
|
-
/** Discovery timeout in milliseconds. Default: 60000 (60s) */
|
|
89
|
+
/** Discovery timeout in milliseconds */
|
|
142
90
|
timeout?: number;
|
|
143
|
-
/**
|
|
144
|
-
* Callback invoked when a wallet provider is discovered.
|
|
145
|
-
* Use this to show wallets to users as they approve them, rather than
|
|
146
|
-
* waiting for the full timeout.
|
|
147
|
-
*/
|
|
148
|
-
onWalletDiscovered?: (provider: WalletProvider) => void;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* A cancellable discovery session.
|
|
153
|
-
*
|
|
154
|
-
* Returned by `WalletManager.getAvailableWallets()` to allow consumers to
|
|
155
|
-
* cancel discovery when no longer needed (e.g., network changes, user navigates away).
|
|
156
|
-
*
|
|
157
|
-
* @example
|
|
158
|
-
* ```typescript
|
|
159
|
-
* const discovery = WalletManager.configure({...}).getAvailableWallets({...});
|
|
160
|
-
*
|
|
161
|
-
* // Iterate over discovered wallets
|
|
162
|
-
* for await (const wallet of discovery.wallets) {
|
|
163
|
-
* console.log(`Found: ${wallet.name}`);
|
|
164
|
-
* }
|
|
165
|
-
*
|
|
166
|
-
* // Cancel discovery when no longer needed
|
|
167
|
-
* discovery.cancel();
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
export interface DiscoverySession {
|
|
171
|
-
/** Async iterator that yields wallet providers as they're discovered */
|
|
172
|
-
wallets: AsyncIterable<WalletProvider>;
|
|
173
|
-
/** Promise that resolves when discovery completes or is cancelled */
|
|
174
|
-
done: Promise<void>;
|
|
175
|
-
/** Cancel discovery immediately and clean up resources */
|
|
176
|
-
cancel: () => void;
|
|
177
91
|
}
|