Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion yarn-project/foundation/src/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ export type ApiSchema = {
};

/** Return whether an API schema defines a valid function schema for a given method name. */
export function schemaHasMethod(schema: ApiSchema, methodName: string) {
export function schemaHasMethod<T extends ApiSchema>(
schema: T,
methodName: string,
): methodName is Extract<keyof T, string> {
return (
typeof methodName === 'string' &&
Object.hasOwn(schema, methodName) &&
Expand Down
4 changes: 4 additions & 0 deletions yarn-project/wallet-sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"./base-wallet": "./dest/base-wallet/index.js",
"./extension/handlers": "./dest/extension/handlers/index.js",
"./extension/provider": "./dest/extension/provider/index.js",
"./iframe/handlers": "./dest/iframe/handlers/index.js",
"./iframe/provider": "./dest/iframe/provider/index.js",
"./crypto": "./dest/crypto.js",
"./types": "./dest/types.js",
"./manager": "./dest/manager/index.js"
Expand All @@ -16,6 +18,8 @@
"./src/base-wallet/index.ts",
"./src/extension/handlers/index.ts",
"./src/extension/provider/index.ts",
"./src/iframe/handlers/index.ts",
"./src/iframe/provider/index.ts",
"./src/crypto.ts",
"./src/types.ts",
"./src/manager/index.ts"
Expand Down
104 changes: 104 additions & 0 deletions yarn-project/wallet-sdk/src/crypto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -497,3 +497,107 @@ export function hashToEmoji(hash: string, count: number = DEFAULT_EMOJI_GRID_SIZ
}
return emojis.join('');
}

// ─── Passphrase-based encryption (PBKDF2 + AES-256-GCM) ───────────────────

/** Default PBKDF2 iteration count. High to compensate for short PINs (~1-2s on modern hardware). */
const DEFAULT_PBKDF2_ITERATIONS = 2_000_000;
const PBKDF2_SALT_BYTES = 16;
const PBKDF2_IV_BYTES = 12;

/**
* Derives an AES-256-GCM key from a passphrase using PBKDF2-SHA256.
*
* @param passphrase - The user-provided passphrase or PIN
* @param salt - Random salt bytes
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
* @returns An AES-256-GCM CryptoKey
*/
export async function deriveKeyFromPassphrase(
passphrase: string,
salt: Uint8Array,
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
): Promise<CryptoKey> {
const keyMaterial = await crypto.subtle.importKey('raw', new TextEncoder().encode(passphrase), 'PBKDF2', false, [
'deriveKey',
]);
return crypto.subtle.deriveKey(
{ name: 'PBKDF2', salt: salt as BufferSource, iterations, hash: 'SHA-256' },
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt', 'decrypt'],
);
}

/**
* Encrypts arbitrary bytes with a passphrase using PBKDF2 + AES-256-GCM.
*
* Output layout: `[salt (16)] [iv (12)] [ciphertext (...)]`
*
* @param plaintext - Data to encrypt
* @param passphrase - User passphrase or PIN
* @param iterations - PBKDF2 iteration count (default: 2,000,000)
* @returns A Uint8Array containing salt + iv + ciphertext
*/
export async function encryptWithPassphrase(
plaintext: Uint8Array,
passphrase: string,
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
): Promise<Uint8Array> {
const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES));
const iv = crypto.getRandomValues(new Uint8Array(PBKDF2_IV_BYTES));
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
const ciphertext = new Uint8Array(
await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, plaintext as BufferSource),
);
const result = new Uint8Array(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES + ciphertext.length);
result.set(salt, 0);
result.set(iv, PBKDF2_SALT_BYTES);
result.set(ciphertext, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
return result;
}

/**
* Decrypts data produced by {@link encryptWithPassphrase}.
*
* @param data - The encrypted blob (salt + iv + ciphertext)
* @param passphrase - The passphrase used during encryption
* @param iterations - PBKDF2 iteration count (must match encryption)
* @returns The decrypted plaintext bytes
* @throws On wrong passphrase (AES-GCM auth tag mismatch)
*/
export async function decryptWithPassphrase(
data: Uint8Array,
passphrase: string,
iterations: number = DEFAULT_PBKDF2_ITERATIONS,
): Promise<Uint8Array> {
const salt = data.slice(0, PBKDF2_SALT_BYTES);
const iv = data.slice(PBKDF2_SALT_BYTES, PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
const ciphertext = data.slice(PBKDF2_SALT_BYTES + PBKDF2_IV_BYTES);
const key = await deriveKeyFromPassphrase(passphrase, salt, iterations);
return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext as BufferSource));
}

/**
* Converts a Uint8Array to a base64 string.
*/
export function uint8ToBase64(bytes: Uint8Array): string {
let binary = '';
for (const b of bytes) {
binary += String.fromCharCode(b);
}
return btoa(binary);
}

/**
* Converts a base64 string to a Uint8Array.
*/
export function base64ToUint8(b64: string): Uint8Array {
const binary = atob(b64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { schemaHasMethod } from '@aztec/foundation/schemas';
import type { FunctionsOf } from '@aztec/foundation/types';

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

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

/**
* Callback type for wallet disconnect events.
*/
export type DisconnectCallback = () => void;

/**
* A wallet implementation that communicates with browser extension wallets
* using an encrypted MessageChannel.
Expand Down
2 changes: 1 addition & 1 deletion yarn-project/wallet-sdk/src/extension/provider/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { ExtensionWallet, type DisconnectCallback } from './extension_wallet.js';
export { ExtensionWallet } from './extension_wallet.js';
export {
ExtensionProvider,
type DiscoveredWallet,
Expand Down
Loading
Loading