Skip to content

Commit 8be945d

Browse files
authored
chore: Accumulated backports to v4-next (#22036)
BEGIN_COMMIT_OVERRIDE feat: iframe wallets sdk (#21978) END_COMMIT_OVERRIDE
2 parents f0e431b + 91c61f0 commit 8be945d

File tree

14 files changed

+1260
-23
lines changed

14 files changed

+1260
-23
lines changed

yarn-project/foundation/src/schemas/api.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@ export type ApiSchema = {
3737
};
3838

3939
/** Return whether an API schema defines a valid function schema for a given method name. */
40-
export function schemaHasMethod(schema: ApiSchema, methodName: string) {
40+
export function schemaHasMethod<T extends ApiSchema>(
41+
schema: T,
42+
methodName: string,
43+
): methodName is Extract<keyof T, string> {
4144
return (
4245
typeof methodName === 'string' &&
4346
Object.hasOwn(schema, methodName) &&

yarn-project/wallet-sdk/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
"./base-wallet": "./dest/base-wallet/index.js",
88
"./extension/handlers": "./dest/extension/handlers/index.js",
99
"./extension/provider": "./dest/extension/provider/index.js",
10+
"./iframe/handlers": "./dest/iframe/handlers/index.js",
11+
"./iframe/provider": "./dest/iframe/provider/index.js",
1012
"./crypto": "./dest/crypto.js",
1113
"./types": "./dest/types.js",
1214
"./manager": "./dest/manager/index.js"
@@ -16,6 +18,8 @@
1618
"./src/base-wallet/index.ts",
1719
"./src/extension/handlers/index.ts",
1820
"./src/extension/provider/index.ts",
21+
"./src/iframe/handlers/index.ts",
22+
"./src/iframe/provider/index.ts",
1923
"./src/crypto.ts",
2024
"./src/types.ts",
2125
"./src/manager/index.ts"

yarn-project/wallet-sdk/src/crypto.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,107 @@ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZ
497497
}
498498
return emojis.join('');
499499
}
500+
501+
// ─── Passphrase-based encryption (PBKDF2 + AES-256-GCM) ───────────────────
502+
503+
/** Default PBKDF2 iteration count. High to compensate for short PINs (~1-2s on modern hardware). */
504+
const DEFAULT_PBKDF2_ITERATIONS = 2_000_000;
505+
const PBKDF2_SALT_BYTES = 16;
506+
const PBKDF2_IV_BYTES = 12;
507+
508+
/**
509+
* Derives an AES-256-GCM key from a passphrase using PBKDF2-SHA256.
510+
*
511+
* @param passphrase - The user-provided passphrase or PIN
512+
* @param salt - Random salt bytes
513+
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
514+
* @returns An AES-256-GCM CryptoKey
515+
*/
516+
export async function deriveKeyFromPassphrase(
517+
passphrase: string,
518+
salt: Uint8Array,
519+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
520+
): Promise<CryptoKey> {
521+
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, [
522+
'deriveKey',
523+
]);
524+
return crypto.subtle.deriveKey(
525+
{ name: 'PBKDF2', salt: salt as BufferSource, iterations, hash: 'SHA-256' },
526+
keyMaterial,
527+
{ name: 'AES-GCM', length: 256 },
528+
false,
529+
['encrypt', 'decrypt'],
530+
);
531+
}
532+
533+
/**
534+
* Encrypts arbitrary bytes with a passphrase using PBKDF2 + AES-256-GCM.
535+
*
536+
* Output layout: `[salt (16)] [iv (12)] [ciphertext (...)]`
537+
*
538+
* @param plaintext - Data to encrypt
539+
* @param passphrase - User passphrase or PIN
540+
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
541+
* @returns A Uint8Array containing salt + iv + ciphertext
542+
*/
543+
export async function encryptWithPassphrase(
544+
plaintext: Uint8Array,
545+
passphrase: string,
546+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
547+
): Promise<Uint8Array> {
548+
const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES));
549+
const iv = crypto.getRandomValues(new Uint8Array(PBKDF2_IV_BYTES));
550+
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
551+
const ciphertext = new Uint8Array(
552+
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext as BufferSource),
553+
);
554+
const result = new Uint8Array(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES + ciphertext.length);
555+
result.set(salt, 0);
556+
result.set(iv, PBKDF2_SALT_BYTES);
557+
result.set(ciphertext, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
558+
return result;
559+
}
560+
561+
/**
562+
* Decrypts data produced by {@link encryptWithPassphrase}.
563+
*
564+
* @param data - The encrypted blob (salt + iv + ciphertext)
565+
* @param passphrase - The passphrase used during encryption
566+
* @param iterations - PBKDF2 iteration count (must match encryption)
567+
* @returns The decrypted plaintext bytes
568+
* @throws On wrong passphrase (AES-GCM auth tag mismatch)
569+
*/
570+
export async function decryptWithPassphrase(
571+
data: Uint8Array,
572+
passphrase: string,
573+
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
574+
): Promise<Uint8Array> {
575+
const salt = data.slice(0, PBKDF2_SALT_BYTES);
576+
const iv = data.slice(PBKDF2_SALT_BYTES, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
577+
const ciphertext = data.slice(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
578+
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
579+
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext as BufferSource));
580+
}
581+
582+
/**
583+
* Converts a Uint8Array to a base64 string.
584+
*/
585+
export function uint8ToBase64(bytes: Uint8Array): string {
586+
let binary = '';
587+
for (const b of bytes) {
588+
binary += String.fromCharCode(b);
589+
}
590+
return btoa(binary);
591+
}
592+
593+
/**
594+
* Converts a base64 string to a Uint8Array.
595+
*/
596+
export function base64ToUint8(b64: string): Uint8Array {
597+
const binary = atob(b64);
598+
const bytes = new Uint8Array(binary.length);
599+
for (let i = 0; i < binary.length; i++) {
600+
bytes[i] = binary.charCodeAt(i);
601+
}
602+
return bytes;
603+
}

yarn-project/wallet-sdk/src/extension/provider/extension_wallet.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
66
import type { FunctionsOf } from '@aztec/foundation/types';
77

88
import { type EncryptedPayload, decrypt, encrypt } from '../../crypto.js';
9-
import { type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js';
9+
import { type DisconnectCallback, type WalletMessage, WalletMessageType, type WalletResponse } from '../../types.js';
1010

1111
/**
1212
* Internal type representing a wallet method call before encryption.
@@ -19,11 +19,6 @@ type WalletMethodCall = {
1919
args: unknown[];
2020
};
2121

22-
/**
23-
* Callback type for wallet disconnect events.
24-
*/
25-
export type DisconnectCallback = () => void;
26-
2722
/**
2823
* A wallet implementation that communicates with browser extension wallets
2924
* using an encrypted MessageChannel.

yarn-project/wallet-sdk/src/extension/provider/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export { ExtensionWallet, type DisconnectCallback } from './extension_wallet.js';
1+
export { ExtensionWallet } from './extension_wallet.js';
22
export {
33
ExtensionProvider,
44
type DiscoveredWallet,

0 commit comments

Comments
 (0)