@aztec/wallet-sdk 0.0.1-commit.f295ac2 → 0.0.1-commit.fc805bf
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 +217 -294
- package/dest/base-wallet/base_wallet.d.ts +20 -7
- package/dest/base-wallet/base_wallet.d.ts.map +1 -1
- package/dest/base-wallet/base_wallet.js +29 -11
- package/dest/crypto.d.ts +59 -50
- package/dest/crypto.d.ts.map +1 -1
- package/dest/crypto.js +202 -108
- package/dest/emoji_alphabet.d.ts +35 -0
- package/dest/emoji_alphabet.d.ts.map +1 -0
- package/dest/emoji_alphabet.js +299 -0
- package/dest/extension/handlers/background_connection_handler.d.ts +158 -0
- package/dest/extension/handlers/background_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/background_connection_handler.js +258 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts +56 -0
- package/dest/extension/handlers/content_script_connection_handler.d.ts.map +1 -0
- package/dest/extension/handlers/content_script_connection_handler.js +174 -0
- package/dest/extension/handlers/index.d.ts +12 -0
- package/dest/extension/handlers/index.d.ts.map +1 -0
- package/dest/extension/handlers/index.js +10 -0
- package/dest/extension/handlers/internal_message_types.d.ts +63 -0
- package/dest/extension/handlers/internal_message_types.d.ts.map +1 -0
- package/dest/extension/handlers/internal_message_types.js +22 -0
- package/dest/extension/provider/extension_provider.d.ts +107 -0
- package/dest/extension/provider/extension_provider.d.ts.map +1 -0
- package/dest/extension/provider/extension_provider.js +160 -0
- package/dest/extension/provider/extension_wallet.d.ts +131 -0
- package/dest/extension/provider/extension_wallet.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/extension_wallet.js +48 -95
- package/dest/extension/provider/index.d.ts +3 -0
- package/dest/extension/provider/index.d.ts.map +1 -0
- package/dest/{providers/extension → extension/provider}/index.js +0 -2
- package/dest/manager/index.d.ts +2 -8
- package/dest/manager/index.d.ts.map +1 -1
- package/dest/manager/index.js +0 -6
- package/dest/manager/types.d.ts +88 -6
- package/dest/manager/types.d.ts.map +1 -1
- package/dest/manager/types.js +17 -1
- package/dest/manager/wallet_manager.d.ts +50 -7
- package/dest/manager/wallet_manager.d.ts.map +1 -1
- package/dest/manager/wallet_manager.js +174 -44
- package/dest/types.d.ts +43 -12
- package/dest/types.d.ts.map +1 -1
- package/dest/types.js +3 -2
- package/package.json +10 -9
- package/src/base-wallet/base_wallet.ts +45 -20
- package/src/crypto.ts +237 -113
- package/src/emoji_alphabet.ts +317 -0
- package/src/extension/handlers/background_connection_handler.ts +423 -0
- package/src/extension/handlers/content_script_connection_handler.ts +246 -0
- package/src/extension/handlers/index.ts +25 -0
- package/src/extension/handlers/internal_message_types.ts +69 -0
- package/src/extension/provider/extension_provider.ts +233 -0
- package/src/{providers/extension → extension/provider}/extension_wallet.ts +52 -110
- package/src/extension/provider/index.ts +7 -0
- package/src/manager/index.ts +2 -10
- package/src/manager/types.ts +91 -5
- package/src/manager/wallet_manager.ts +192 -46
- package/src/types.ts +44 -10
- package/dest/providers/extension/extension_provider.d.ts +0 -63
- package/dest/providers/extension/extension_provider.d.ts.map +0 -1
- package/dest/providers/extension/extension_provider.js +0 -124
- package/dest/providers/extension/extension_wallet.d.ts +0 -155
- package/dest/providers/extension/extension_wallet.d.ts.map +0 -1
- package/dest/providers/extension/index.d.ts +0 -6
- package/dest/providers/extension/index.d.ts.map +0 -1
- package/src/providers/extension/extension_provider.ts +0 -167
- package/src/providers/extension/index.ts +0 -5
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/wallet-sdk",
|
|
3
3
|
"homepage": "https://github.com/AztecProtocol/aztec-packages/tree/master/yarn-project/wallet-sdk",
|
|
4
|
-
"version": "0.0.1-commit.
|
|
4
|
+
"version": "0.0.1-commit.fc805bf",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
"./base-wallet": "./dest/base-wallet/index.js",
|
|
8
|
-
"./
|
|
8
|
+
"./extension/handlers": "./dest/extension/handlers/index.js",
|
|
9
|
+
"./extension/provider": "./dest/extension/provider/index.js",
|
|
9
10
|
"./crypto": "./dest/crypto.js",
|
|
10
11
|
"./types": "./dest/types.js",
|
|
11
12
|
"./manager": "./dest/manager/index.js"
|
|
@@ -64,15 +65,15 @@
|
|
|
64
65
|
]
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
|
-
"@aztec/aztec.js": "0.0.1-commit.
|
|
68
|
-
"@aztec/constants": "0.0.1-commit.
|
|
69
|
-
"@aztec/entrypoints": "0.0.1-commit.
|
|
70
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
71
|
-
"@aztec/pxe": "0.0.1-commit.
|
|
72
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
68
|
+
"@aztec/aztec.js": "0.0.1-commit.fc805bf",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.fc805bf",
|
|
70
|
+
"@aztec/entrypoints": "0.0.1-commit.fc805bf",
|
|
71
|
+
"@aztec/foundation": "0.0.1-commit.fc805bf",
|
|
72
|
+
"@aztec/pxe": "0.0.1-commit.fc805bf",
|
|
73
|
+
"@aztec/stdlib": "0.0.1-commit.fc805bf"
|
|
73
74
|
},
|
|
74
75
|
"devDependencies": {
|
|
75
|
-
"@aztec/noir-contracts.js": "0.0.1-commit.
|
|
76
|
+
"@aztec/noir-contracts.js": "0.0.1-commit.fc805bf",
|
|
76
77
|
"@jest/globals": "^30.0.0",
|
|
77
78
|
"@types/jest": "^30.0.0",
|
|
78
79
|
"@types/node": "^22.15.17",
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import type { Account } from '@aztec/aztec.js/account';
|
|
2
2
|
import type { CallIntent, IntentInnerHash } from '@aztec/aztec.js/authorization';
|
|
3
|
+
import { type InteractionWaitOptions, NO_WAIT, type SendReturn } from '@aztec/aztec.js/contracts';
|
|
3
4
|
import type { FeePaymentMethod } from '@aztec/aztec.js/fee';
|
|
5
|
+
import { waitForTx } from '@aztec/aztec.js/node';
|
|
4
6
|
import type {
|
|
5
7
|
Aliased,
|
|
8
|
+
AppCapabilities,
|
|
6
9
|
BatchResults,
|
|
7
10
|
BatchedMethod,
|
|
8
11
|
PrivateEvent,
|
|
@@ -11,6 +14,7 @@ import type {
|
|
|
11
14
|
SendOptions,
|
|
12
15
|
SimulateOptions,
|
|
13
16
|
Wallet,
|
|
17
|
+
WalletCapabilities,
|
|
14
18
|
} from '@aztec/aztec.js/wallet';
|
|
15
19
|
import {
|
|
16
20
|
GAS_ESTIMATION_DA_GAS_LIMIT,
|
|
@@ -43,9 +47,7 @@ import { siloNullifier } from '@aztec/stdlib/hash';
|
|
|
43
47
|
import type { AztecNode } from '@aztec/stdlib/interfaces/client';
|
|
44
48
|
import type {
|
|
45
49
|
TxExecutionRequest,
|
|
46
|
-
TxHash,
|
|
47
50
|
TxProfileResult,
|
|
48
|
-
TxReceipt,
|
|
49
51
|
TxSimulationResult,
|
|
50
52
|
UtilitySimulationResult,
|
|
51
53
|
} from '@aztec/stdlib/tx';
|
|
@@ -119,15 +121,39 @@ export abstract class BaseWallet implements Wallet {
|
|
|
119
121
|
? mergeExecutionPayloads([feeExecutionPayload, executionPayload])
|
|
120
122
|
: executionPayload;
|
|
121
123
|
const fromAccount = await this.getAccountFromAddress(from);
|
|
122
|
-
|
|
124
|
+
const chainInfo = await this.getChainInfo();
|
|
125
|
+
return fromAccount.createTxExecutionRequest(
|
|
126
|
+
finalExecutionPayload,
|
|
127
|
+
feeOptions.gasSettings,
|
|
128
|
+
chainInfo,
|
|
129
|
+
executionOptions,
|
|
130
|
+
);
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
public async createAuthWit(
|
|
126
134
|
from: AztecAddress,
|
|
127
|
-
messageHashOrIntent:
|
|
135
|
+
messageHashOrIntent: IntentInnerHash | CallIntent,
|
|
128
136
|
): Promise<AuthWitness> {
|
|
129
137
|
const account = await this.getAccountFromAddress(from);
|
|
130
|
-
|
|
138
|
+
const chainInfo = await this.getChainInfo();
|
|
139
|
+
return account.createAuthWit(messageHashOrIntent, chainInfo);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Request capabilities from the wallet.
|
|
144
|
+
*
|
|
145
|
+
* This method is wallet-implementation-dependent and must be provided by classes extending BaseWallet.
|
|
146
|
+
* Embedded wallets typically don't support capability-based authorization (no user authorization flow),
|
|
147
|
+
* while external wallets (browser extensions, hardware wallets) implement this to reduce authorization
|
|
148
|
+
* friction by allowing apps to request permissions upfront.
|
|
149
|
+
*
|
|
150
|
+
* TODO: Consider making it abstract so implementing it is a conscious decision. Leaving it as-is
|
|
151
|
+
* while the feature stabilizes.
|
|
152
|
+
*
|
|
153
|
+
* @param _manifest - Application capability manifest declaring what operations the app needs
|
|
154
|
+
*/
|
|
155
|
+
public requestCapabilities(_manifest: AppCapabilities): Promise<WalletCapabilities> {
|
|
156
|
+
throw new Error('Not implemented');
|
|
131
157
|
}
|
|
132
158
|
|
|
133
159
|
public async batch<const T extends readonly BatchedMethod[]>(methods: T): Promise<BatchResults<T>> {
|
|
@@ -275,7 +301,10 @@ export abstract class BaseWallet implements Wallet {
|
|
|
275
301
|
return this.pxe.profileTx(txRequest, opts.profileMode, opts.skipProofGeneration ?? true);
|
|
276
302
|
}
|
|
277
303
|
|
|
278
|
-
async sendTx
|
|
304
|
+
public async sendTx<W extends InteractionWaitOptions = undefined>(
|
|
305
|
+
executionPayload: ExecutionPayload,
|
|
306
|
+
opts: SendOptions<W>,
|
|
307
|
+
): Promise<SendReturn<W>> {
|
|
279
308
|
const feeOptions = await this.completeFeeOptions(opts.from, executionPayload.feePayer, opts.fee?.gasSettings);
|
|
280
309
|
const txRequest = await this.createTxExecutionRequestFromPayloadAndFee(executionPayload, opts.from, feeOptions);
|
|
281
310
|
const provenTx = await this.pxe.proveTx(txRequest);
|
|
@@ -289,7 +318,15 @@ export abstract class BaseWallet implements Wallet {
|
|
|
289
318
|
throw this.contextualizeError(err, inspect(tx));
|
|
290
319
|
});
|
|
291
320
|
this.log.info(`Sent transaction ${txHash}`);
|
|
292
|
-
|
|
321
|
+
|
|
322
|
+
// If wait is NO_WAIT, return txHash immediately
|
|
323
|
+
if (opts.wait === NO_WAIT) {
|
|
324
|
+
return txHash as SendReturn<W>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Otherwise, wait for the full receipt (default behavior on wait: undefined)
|
|
328
|
+
const waitOpts = typeof opts.wait === 'object' ? opts.wait : undefined;
|
|
329
|
+
return (await waitForTx(this.aztecNode, txHash, waitOpts)) as SendReturn<W>;
|
|
293
330
|
}
|
|
294
331
|
|
|
295
332
|
protected contextualizeError(err: Error, ...context: string[]): Error {
|
|
@@ -310,10 +347,6 @@ export abstract class BaseWallet implements Wallet {
|
|
|
310
347
|
return this.pxe.simulateUtility(call, authwits);
|
|
311
348
|
}
|
|
312
349
|
|
|
313
|
-
getTxReceipt(txHash: TxHash): Promise<TxReceipt> {
|
|
314
|
-
return this.aztecNode.getTxReceipt(txHash);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
350
|
async getPrivateEvents<T>(
|
|
318
351
|
eventDef: EventMetadataDefinition,
|
|
319
352
|
eventFilter: PrivateEventFilter,
|
|
@@ -338,14 +371,7 @@ export abstract class BaseWallet implements Wallet {
|
|
|
338
371
|
const instance = await this.pxe.getContractInstance(address);
|
|
339
372
|
const initNullifier = await siloNullifier(address, address.toField());
|
|
340
373
|
const publiclyRegisteredContract = await this.aztecNode.getContract(address);
|
|
341
|
-
const
|
|
342
|
-
this.aztecNode.getNullifierMembershipWitness('latest', initNullifier),
|
|
343
|
-
publiclyRegisteredContract
|
|
344
|
-
? this.aztecNode.getContractClass(
|
|
345
|
-
publiclyRegisteredContract.currentContractClassId || instance?.currentContractClassId,
|
|
346
|
-
)
|
|
347
|
-
: undefined,
|
|
348
|
-
]);
|
|
374
|
+
const initNullifierMembershipWitness = await this.aztecNode.getNullifierMembershipWitness('latest', initNullifier);
|
|
349
375
|
const isContractUpdated =
|
|
350
376
|
publiclyRegisteredContract &&
|
|
351
377
|
!publiclyRegisteredContract.currentContractClassId.equals(publiclyRegisteredContract.originalContractClassId);
|
|
@@ -353,7 +379,6 @@ export abstract class BaseWallet implements Wallet {
|
|
|
353
379
|
instance: instance ?? undefined,
|
|
354
380
|
isContractInitialized: !!initNullifierMembershipWitness,
|
|
355
381
|
isContractPublished: !!publiclyRegisteredContract,
|
|
356
|
-
isContractClassPubliclyRegistered: !!publiclyRegisteredContractClass,
|
|
357
382
|
isContractUpdated: !!isContractUpdated,
|
|
358
383
|
updatedContractClassId: isContractUpdated ? publiclyRegisteredContract.currentContractClassId : undefined,
|
|
359
384
|
};
|
package/src/crypto.ts
CHANGED
|
@@ -4,34 +4,61 @@
|
|
|
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
|
-
*
|
|
7
|
+
* ## Security Model
|
|
8
|
+
*
|
|
9
|
+
* The crypto flow uses HKDF for key derivation with domain separation:
|
|
10
|
+
*
|
|
8
11
|
* 1. Both parties generate ECDH key pairs using {@link generateKeyPair}
|
|
9
12
|
* 2. Public keys are exchanged (exported via {@link exportPublicKey}, imported via {@link importPublicKey})
|
|
10
|
-
* 3. Both parties derive
|
|
11
|
-
*
|
|
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.
|
|
12
30
|
*
|
|
13
31
|
* @example
|
|
14
32
|
* ```typescript
|
|
15
|
-
* // Party A
|
|
33
|
+
* // Party A (dApp)
|
|
16
34
|
* const keyPairA = await generateKeyPair();
|
|
17
35
|
* const publicKeyA = await exportPublicKey(keyPairA.publicKey);
|
|
18
36
|
*
|
|
19
|
-
* // Party B
|
|
37
|
+
* // Party B (wallet)
|
|
20
38
|
* const keyPairB = await generateKeyPair();
|
|
21
39
|
* const publicKeyB = await exportPublicKey(keyPairB.publicKey);
|
|
22
40
|
*
|
|
23
|
-
* // Exchange public keys, then derive
|
|
41
|
+
* // Exchange public keys, then derive session keys
|
|
42
|
+
* // App side: isApp = true
|
|
24
43
|
* const importedB = await importPublicKey(publicKeyB);
|
|
25
|
-
* const
|
|
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);
|
|
26
53
|
*
|
|
27
54
|
* // Encrypt and decrypt
|
|
28
|
-
* const encrypted = await encrypt(
|
|
29
|
-
* const decrypted = await decrypt(
|
|
55
|
+
* const encrypted = await encrypt(sessionA.encryptionKey, JSON.stringify({ message: 'hello' }));
|
|
56
|
+
* const decrypted = await decrypt(sessionB.encryptionKey, encrypted);
|
|
30
57
|
* ```
|
|
31
58
|
*
|
|
32
59
|
* @packageDocumentation
|
|
33
60
|
*/
|
|
34
|
-
import {
|
|
61
|
+
import { EMOJI_ALPHABET, EMOJI_ALPHABET_SIZE } from './emoji_alphabet.js';
|
|
35
62
|
|
|
36
63
|
/**
|
|
37
64
|
* Exported public key in JWK format for transmission over untrusted channels.
|
|
@@ -74,11 +101,31 @@ export interface SecureKeyPair {
|
|
|
74
101
|
privateKey: CryptoKey;
|
|
75
102
|
}
|
|
76
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
|
+
|
|
77
124
|
/**
|
|
78
125
|
* Generates an ECDH P-256 key pair for key exchange.
|
|
79
126
|
*
|
|
80
|
-
* The generated key pair can be used to derive
|
|
81
|
-
* party's public key using {@link
|
|
127
|
+
* The generated key pair can be used to derive session keys with another
|
|
128
|
+
* party's public key using {@link deriveSessionKeys}.
|
|
82
129
|
*
|
|
83
130
|
* @returns A new ECDH key pair
|
|
84
131
|
*
|
|
@@ -95,8 +142,8 @@ export async function generateKeyPair(): Promise<SecureKeyPair> {
|
|
|
95
142
|
name: 'ECDH',
|
|
96
143
|
namedCurve: 'P-256',
|
|
97
144
|
},
|
|
98
|
-
true, // extractable (needed to export public key)
|
|
99
|
-
['
|
|
145
|
+
true, // extractable (needed to export public key and derive bits)
|
|
146
|
+
['deriveBits'],
|
|
100
147
|
);
|
|
101
148
|
return {
|
|
102
149
|
publicKey: keyPair.publicKey,
|
|
@@ -133,16 +180,16 @@ export async function exportPublicKey(publicKey: CryptoKey): Promise<ExportedPub
|
|
|
133
180
|
/**
|
|
134
181
|
* Imports a public key from JWK format.
|
|
135
182
|
*
|
|
136
|
-
* Used to import the other party's public key for deriving
|
|
183
|
+
* Used to import the other party's public key for deriving session keys.
|
|
137
184
|
*
|
|
138
185
|
* @param exported - The public key in JWK format
|
|
139
|
-
* @returns A CryptoKey that can be used with {@link
|
|
186
|
+
* @returns A CryptoKey that can be used with {@link deriveSessionKeys}
|
|
140
187
|
*
|
|
141
188
|
* @example
|
|
142
189
|
* ```typescript
|
|
143
|
-
* //
|
|
144
|
-
* const
|
|
145
|
-
* const
|
|
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);
|
|
146
193
|
* ```
|
|
147
194
|
*/
|
|
148
195
|
export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey> {
|
|
@@ -158,67 +205,171 @@ export function importPublicKey(exported: ExportedPublicKey): Promise<CryptoKey>
|
|
|
158
205
|
name: 'ECDH',
|
|
159
206
|
namedCurve: 'P-256',
|
|
160
207
|
},
|
|
161
|
-
|
|
208
|
+
true, // extractable - needed for deriveSessionKeys to export for salt. Safe for public keys.
|
|
162
209
|
[],
|
|
163
210
|
);
|
|
164
211
|
}
|
|
165
212
|
|
|
166
213
|
/**
|
|
167
|
-
*
|
|
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")
|
|
168
272
|
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
273
|
+
* The keys are derived using a single HKDF call that produces 512 bits,
|
|
274
|
+
* then split into the two keys.
|
|
171
275
|
*
|
|
172
|
-
* @param
|
|
173
|
-
* @param
|
|
174
|
-
* @
|
|
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
|
|
175
280
|
*
|
|
176
281
|
* @example
|
|
177
282
|
* ```typescript
|
|
178
|
-
* //
|
|
179
|
-
* const
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
|
182
288
|
* ```
|
|
183
289
|
*/
|
|
184
|
-
export function
|
|
185
|
-
|
|
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(
|
|
186
297
|
{
|
|
187
298
|
name: 'ECDH',
|
|
188
|
-
public:
|
|
299
|
+
public: peerPublicKey,
|
|
189
300
|
},
|
|
190
|
-
privateKey,
|
|
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(
|
|
191
317
|
{
|
|
192
|
-
name: '
|
|
193
|
-
|
|
318
|
+
name: 'HKDF',
|
|
319
|
+
hash: 'SHA-256',
|
|
320
|
+
salt,
|
|
321
|
+
info: HKDF_INFO,
|
|
194
322
|
},
|
|
195
|
-
|
|
196
|
-
|
|
323
|
+
hkdfKey,
|
|
324
|
+
512, // 256 bits for GCM + 256 bits for HMAC
|
|
197
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
|
+
};
|
|
198
350
|
}
|
|
199
351
|
|
|
200
352
|
/**
|
|
201
353
|
* Encrypts data using AES-256-GCM.
|
|
202
354
|
*
|
|
203
|
-
*
|
|
204
|
-
* generated for each encryption operation.
|
|
355
|
+
* A random 12-byte IV is generated for each encryption operation.
|
|
205
356
|
*
|
|
206
357
|
* AES-GCM provides both confidentiality and authenticity - any tampering
|
|
207
358
|
* with the ciphertext will cause decryption to fail.
|
|
208
359
|
*
|
|
209
|
-
* @param key - The AES-GCM key (from {@link
|
|
210
|
-
* @param data - The data to encrypt (
|
|
360
|
+
* @param key - The AES-GCM key (from {@link deriveSessionKeys})
|
|
361
|
+
* @param data - The string data to encrypt (caller is responsible for serialization)
|
|
211
362
|
* @returns The encrypted payload with IV and ciphertext
|
|
212
363
|
*
|
|
213
364
|
* @example
|
|
214
365
|
* ```typescript
|
|
215
|
-
* const encrypted = await encrypt(
|
|
366
|
+
* const encrypted = await encrypt(session.encryptionKey, JSON.stringify({ action: 'transfer', amount: 100 }));
|
|
216
367
|
* // encrypted.iv and encrypted.ciphertext are base64 strings
|
|
217
368
|
* ```
|
|
218
369
|
*/
|
|
219
|
-
export async function encrypt(key: CryptoKey, data:
|
|
370
|
+
export async function encrypt(key: CryptoKey, data: string): Promise<EncryptedPayload> {
|
|
220
371
|
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
221
|
-
const encoded = new TextEncoder().encode(
|
|
372
|
+
const encoded = new TextEncoder().encode(data);
|
|
222
373
|
|
|
223
374
|
const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
|
|
224
375
|
|
|
@@ -234,7 +385,7 @@ export async function encrypt(key: CryptoKey, data: unknown): Promise<EncryptedP
|
|
|
234
385
|
* The decrypted data is JSON parsed before returning.
|
|
235
386
|
*
|
|
236
387
|
* @typeParam T - The expected type of the decrypted data
|
|
237
|
-
* @param key - The AES-GCM key (from {@link
|
|
388
|
+
* @param key - The AES-GCM key (from {@link deriveSessionKeys})
|
|
238
389
|
* @param payload - The encrypted payload from {@link encrypt}
|
|
239
390
|
* @returns The decrypted and parsed data
|
|
240
391
|
*
|
|
@@ -242,7 +393,7 @@ export async function encrypt(key: CryptoKey, data: unknown): Promise<EncryptedP
|
|
|
242
393
|
*
|
|
243
394
|
* @example
|
|
244
395
|
* ```typescript
|
|
245
|
-
* const decrypted = await decrypt<{ action: string; amount: number }>(
|
|
396
|
+
* const decrypted = await decrypt<{ action: string; amount: number }>(session.encryptionKey, encrypted);
|
|
246
397
|
* console.log(decrypted.action); // 'transfer'
|
|
247
398
|
* ```
|
|
248
399
|
*/
|
|
@@ -283,93 +434,66 @@ function base64ToArrayBuffer(base64: string): ArrayBuffer {
|
|
|
283
434
|
}
|
|
284
435
|
|
|
285
436
|
/**
|
|
286
|
-
*
|
|
287
|
-
* 32 distinct, easily recognizable emojis for anti-spoofing verification.
|
|
437
|
+
* Converts base64url string to Uint8Array.
|
|
288
438
|
* @internal
|
|
289
439
|
*/
|
|
290
|
-
|
|
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
|
-
];
|
|
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
|
+
}
|
|
324
452
|
|
|
325
453
|
/**
|
|
326
|
-
*
|
|
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
|
-
* ```
|
|
454
|
+
* Converts ArrayBuffer to hex string.
|
|
455
|
+
* @internal
|
|
339
456
|
*/
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
const hash = await crypto.subtle.digest('SHA-256', rawKey);
|
|
343
|
-
const bytes = new Uint8Array(hash.slice(0, 16));
|
|
457
|
+
function arrayBufferToHex(buffer: ArrayBuffer): string {
|
|
458
|
+
const bytes = new Uint8Array(buffer);
|
|
344
459
|
return Array.from(bytes)
|
|
345
460
|
.map(b => b.toString(16).padStart(2, '0'))
|
|
346
461
|
.join('');
|
|
347
462
|
}
|
|
348
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
|
+
|
|
349
470
|
/**
|
|
350
471
|
* Converts a hex hash to an emoji sequence for visual verification.
|
|
351
472
|
*
|
|
352
|
-
* This is used for
|
|
353
|
-
* independently compute the same emoji sequence from the
|
|
473
|
+
* This is used for verification - both the dApp and wallet
|
|
474
|
+
* independently compute the same emoji sequence from the derived keys.
|
|
354
475
|
* Users can visually compare the sequences to detect interception.
|
|
355
476
|
*
|
|
356
|
-
*
|
|
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.
|
|
357
480
|
*
|
|
358
|
-
* @param hash - Hex string from
|
|
359
|
-
* @param
|
|
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)
|
|
360
483
|
* @returns A string of emojis representing the hash
|
|
361
484
|
*
|
|
362
485
|
* @example
|
|
363
486
|
* ```typescript
|
|
364
|
-
* const
|
|
365
|
-
* const emoji = hashToEmoji(
|
|
366
|
-
* // Display to user for verification
|
|
487
|
+
* const session = await deriveSessionKeys(...);
|
|
488
|
+
* const emoji = hashToEmoji(session.verificationHash); // e.g., "🔵🦋🎯🐼🌟🎲🦊🐸💎"
|
|
489
|
+
* // Display as 3x3 grid to user for verification
|
|
367
490
|
* ```
|
|
368
491
|
*/
|
|
369
|
-
export function hashToEmoji(hash: string,
|
|
370
|
-
const
|
|
371
|
-
for (let i = 0; i < hash.length &&
|
|
372
|
-
|
|
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]);
|
|
373
497
|
}
|
|
374
|
-
return
|
|
498
|
+
return emojis.join('');
|
|
375
499
|
}
|