diff --git a/yarn-project/cli/src/cmds/validator_keys/shared.ts b/yarn-project/cli/src/cmds/validator_keys/shared.ts index eb8d722a05d0..d8036dfa9bfe 100644 --- a/yarn-project/cli/src/cmds/validator_keys/shared.ts +++ b/yarn-project/cli/src/cmds/validator_keys/shared.ts @@ -228,7 +228,7 @@ export async function writeBn254BlsKeystore( ): Promise { mkdirSync(outDir, { recursive: true }); - const keystore = createBn254Keystore(password, privateKeyHex, pubkeyHex, derivationPath); + const keystore = await createBn254Keystore(password, privateKeyHex, pubkeyHex, derivationPath); const safeBase = fileNameBase.replace(/[^a-zA-Z0-9_-]/g, '_'); const outPath = join(outDir, `keystore-${safeBase}.json`); @@ -241,28 +241,29 @@ export async function writeBlsBn254ToFile( validators: ValidatorKeyStore[], options: { outDir: string; password: string; blsPath?: string }, ): Promise { - for (let i = 0; i < validators.length; i++) { - const v = validators[i]; - if (!v || typeof v !== 'object' || !('attester' in v)) { - continue; - } - const att = (v as any).attester; + await Promise.all( + validators.map(async (v, i) => { + if (!v || typeof v !== 'object' || !('attester' in v)) { + return; + } + const att = (v as any).attester; - // Shapes: { bls: } or { eth: , bls?: } or plain EthAccount - const blsKey: string | undefined = typeof att === 'object' && 'bls' in att ? (att as any).bls : undefined; - if (!blsKey || typeof blsKey !== 'string') { - continue; - } + // Shapes: { bls: } or { eth: , bls?: } or plain EthAccount + const blsKey: string | undefined = typeof att === 'object' && 'bls' in att ? (att as any).bls : undefined; + if (!blsKey || typeof blsKey !== 'string') { + return; + } - const pub = await computeBlsPublicKeyCompressed(blsKey); - const path = options.blsPath ?? defaultBlsPath; - const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`; - const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path); + const pub = await computeBlsPublicKeyCompressed(blsKey); + const path = options.blsPath ?? defaultBlsPath; + const fileBase = `${String(i + 1)}_${pub.slice(2, 18)}`; + const keystorePath = await writeBn254BlsKeystore(options.outDir, fileBase, options.password, blsKey, pub, path); - if (typeof att === 'object') { - (att as any).bls = { path: keystorePath, password: options.password }; - } - } + if (typeof att === 'object') { + (att as any).bls = { path: keystorePath, password: options.password }; + } + }), + ); } /** Writes an Ethereum JSON V3 keystore using ethers, returns absolute path */ @@ -295,32 +296,31 @@ export async function writeEthJsonV3ToFile( return account; }; - for (let i = 0; i < validators.length; i++) { - const v = validators[i]; - if (!v || typeof v !== 'object') { - continue; - } + await Promise.all( + validators.map(async (v, i) => { + if (!v || typeof v !== 'object') { + return; + } - // attester may be string (eth), object with eth, or remote signer - const att = (v as any).attester; - if (typeof att === 'string') { - (v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`); - } else if (att && typeof att === 'object' && 'eth' in att) { - (att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`); - } + // attester may be string (eth), object with eth, or remote signer + const att = (v as any).attester; + if (typeof att === 'string') { + (v as any).attester = await maybeEncryptEth(att, `attester_${i + 1}`); + } else if (att && typeof att === 'object' && 'eth' in att) { + (att as any).eth = await maybeEncryptEth((att as any).eth, `attester_${i + 1}`); + } - // publisher can be single or array - if ('publisher' in v) { - const pub = (v as any).publisher; - if (Array.isArray(pub)) { - const out: any[] = []; - for (let j = 0; j < pub.length; j++) { - out.push(await maybeEncryptEth(pub[j], `publisher_${i + 1}_${j + 1}`)); + // publisher can be single or array + if ('publisher' in v) { + const pub = (v as any).publisher; + if (Array.isArray(pub)) { + (v as any).publisher = await Promise.all( + pub.map((account, j) => maybeEncryptEth(account, `publisher_${i + 1}_${j + 1}`)), + ); + } else if (pub !== undefined) { + (v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`); } - (v as any).publisher = out; - } else if (pub !== undefined) { - (v as any).publisher = await maybeEncryptEth(pub, `publisher_${i + 1}`); } - } - } + }), + ); } diff --git a/yarn-project/foundation/src/crypto/bls/bn254_keystore.test.ts b/yarn-project/foundation/src/crypto/bls/bn254_keystore.test.ts index 5cc7d831714b..a636bf6b7d95 100644 --- a/yarn-project/foundation/src/crypto/bls/bn254_keystore.test.ts +++ b/yarn-project/foundation/src/crypto/bls/bn254_keystore.test.ts @@ -28,8 +28,8 @@ describe('BN254 Keystore', () => { }); describe('createBn254Keystore', () => { - it('creates a valid BN254 keystore structure', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('creates a valid BN254 keystore structure', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); expect(keystore.version).toBe(4); expect(keystore.path).toBe(testPath); @@ -50,8 +50,8 @@ describe('BN254 Keystore', () => { expect(keystore.crypto.checksum.message).toMatch(/^[0-9a-f]{64}$/); }); - it('encrypts the private key so it can be decrypted', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('encrypts the private key so it can be decrypted', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); // Derive the decryption key using the same KDF const salt = Buffer.from(keystore.crypto.kdf.params.salt, 'hex'); @@ -74,87 +74,87 @@ describe('BN254 Keystore', () => { expect('0x' + decrypted.toString('hex')).toBe(testPrivateKey); }); - it('produces different ciphertexts for the same key with different passwords', () => { - const keystore1 = createBn254Keystore('password1', testPrivateKey, testPublicKey, testPath); - const keystore2 = createBn254Keystore('password2', testPrivateKey, testPublicKey, testPath); + it('produces different ciphertexts for the same key with different passwords', async () => { + const keystore1 = await createBn254Keystore('password1', testPrivateKey, testPublicKey, testPath); + const keystore2 = await createBn254Keystore('password2', testPrivateKey, testPublicKey, testPath); expect(keystore1.crypto.cipher.message).not.toBe(keystore2.crypto.cipher.message); expect(keystore1.crypto.checksum.message).not.toBe(keystore2.crypto.checksum.message); }); - it('produces different ciphertexts on each call due to random salt and IV', () => { - const keystore1 = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); - const keystore2 = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('produces different ciphertexts on each call due to random salt and IV', async () => { + const keystore1 = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + const keystore2 = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); expect(keystore1.crypto.kdf.params.salt).not.toBe(keystore2.crypto.kdf.params.salt); expect(keystore1.crypto.cipher.params.iv).not.toBe(keystore2.crypto.cipher.params.iv); expect(keystore1.crypto.cipher.message).not.toBe(keystore2.crypto.cipher.message); }); - it('accepts private keys with or without 0x prefix', () => { - const withPrefix = createBn254Keystore(testPassword, '0x' + '42'.repeat(32), testPublicKey, testPath); - const withoutPrefix = createBn254Keystore(testPassword, '42'.repeat(32), testPublicKey, testPath); + it('accepts private keys with or without 0x prefix', async () => { + const withPrefix = await createBn254Keystore(testPassword, '0x' + '42'.repeat(32), testPublicKey, testPath); + const withoutPrefix = await createBn254Keystore(testPassword, '42'.repeat(32), testPublicKey, testPath); // Both should produce valid keystores with same length ciphertext expect(withPrefix.crypto.cipher.message.length).toBe(64); expect(withoutPrefix.crypto.cipher.message.length).toBe(64); }); - it('stores public key without 0x prefix in description field', () => { + it('stores public key without 0x prefix in description field', async () => { const pubkeyWithPrefix = '0x' + 'ab'.repeat(33); - const keystore = createBn254Keystore(testPassword, testPrivateKey, pubkeyWithPrefix, testPath); + const keystore = await createBn254Keystore(testPassword, testPrivateKey, pubkeyWithPrefix, testPath); expect(keystore.description).toBe('ab'.repeat(33)); expect(keystore.pubkey).toBe(pubkeyWithPrefix); }); - it('throws error for invalid private key length', () => { + it('throws error for invalid private key length', async () => { const shortKey = '0x1234'; - expect(() => createBn254Keystore(testPassword, shortKey, testPublicKey, testPath)).toThrow( + await expect(createBn254Keystore(testPassword, shortKey, testPublicKey, testPath)).rejects.toThrow( 'BLS private key must be 32-byte hex', ); const longKey = '0x' + '42'.repeat(33); - expect(() => createBn254Keystore(testPassword, longKey, testPublicKey, testPath)).toThrow( + await expect(createBn254Keystore(testPassword, longKey, testPublicKey, testPath)).rejects.toThrow( 'BLS private key must be 32-byte hex', ); }); - it('throws error for non-hex private key', () => { + it('throws error for non-hex private key', async () => { const invalidKey = '0xGGGG' + '42'.repeat(30); - expect(() => createBn254Keystore(testPassword, invalidKey, testPublicKey, testPath)).toThrow( + await expect(createBn254Keystore(testPassword, invalidKey, testPublicKey, testPath)).rejects.toThrow( 'BLS private key must be 32-byte hex', ); }); - it('normalizes password using NFKD', () => { + it('normalizes password using NFKD', async () => { // Password with combining characters const password = 'café'; // é can be represented as single char or e + combining accent - const keystore = createBn254Keystore(password, testPrivateKey, testPublicKey, testPath); + const keystore = await createBn254Keystore(password, testPrivateKey, testPublicKey, testPath); // Should successfully create keystore (normalization happens internally) expect(keystore).toBeDefined(); expect(keystore.version).toBe(4); }); - it('handles empty password', () => { - const keystore = createBn254Keystore('', testPrivateKey, testPublicKey, testPath); + it('handles empty password', async () => { + const keystore = await createBn254Keystore('', testPrivateKey, testPublicKey, testPath); expect(keystore).toBeDefined(); expect(keystore.crypto.cipher.message).toMatch(/^[0-9a-f]{64}$/); }); - it('preserves derivation path exactly as provided', () => { + it('preserves derivation path exactly as provided', async () => { const customPath = 'm/12381/3600/99/88/77'; - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, customPath); + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, customPath); expect(keystore.path).toBe(customPath); }); }); describe('loadBn254Keystore', () => { - it('loads and validates a valid BN254 keystore file', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('loads and validates a valid BN254 keystore file', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); const filePath = join(tempDir, 'valid-keystore.json'); writeFileSync(filePath, JSON.stringify(keystore)); @@ -191,8 +191,8 @@ describe('BN254 Keystore', () => { }); describe('decryptBn254Keystore', () => { - it('successfully decrypts a keystore with correct password', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('successfully decrypts a keystore with correct password', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); const filePath = join(tempDir, 'decrypt-test.json'); writeFileSync(filePath, JSON.stringify(keystore)); @@ -201,8 +201,8 @@ describe('BN254 Keystore', () => { expect(decrypted).toBe(testPrivateKey); }); - it('throws on incorrect password', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('throws on incorrect password', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); const filePath = join(tempDir, 'wrong-password-test.json'); writeFileSync(filePath, JSON.stringify(keystore)); @@ -210,9 +210,9 @@ describe('BN254 Keystore', () => { expect(() => decryptBn254Keystore(filePath, 'wrong-password')).toThrow(/Checksum verification failed/); }); - it('works with empty password if keystore was created with empty password', () => { + it('works with empty password if keystore was created with empty password', async () => { const emptyPassword = ''; - const keystore = createBn254Keystore(emptyPassword, testPrivateKey, testPublicKey, testPath); + const keystore = await createBn254Keystore(emptyPassword, testPrivateKey, testPublicKey, testPath); const filePath = join(tempDir, 'empty-password-test.json'); writeFileSync(filePath, JSON.stringify(keystore)); @@ -223,16 +223,16 @@ describe('BN254 Keystore', () => { }); describe('decryptBn254KeystoreFromObject', () => { - it('decrypts from an in-memory keystore object', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('decrypts from an in-memory keystore object', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); const decrypted = decryptBn254KeystoreFromObject(keystore, testPassword); expect(decrypted).toBe(testPrivateKey); }); - it('throws on unsupported KDF function', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('throws on unsupported KDF function', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); // Manually modify to unsupported KDF (keystore.crypto.kdf as any).function = 'scrypt'; @@ -240,8 +240,8 @@ describe('BN254 Keystore', () => { expect(() => decryptBn254KeystoreFromObject(keystore, testPassword)).toThrow(/Unsupported KDF function/); }); - it('throws on unsupported cipher function', () => { - const keystore = createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); + it('throws on unsupported cipher function', async () => { + const keystore = await createBn254Keystore(testPassword, testPrivateKey, testPublicKey, testPath); // Manually modify to unsupported cipher (keystore.crypto.cipher as any).function = 'aes-256-gcm'; @@ -251,14 +251,14 @@ describe('BN254 Keystore', () => { }); describe('round-trip encryption and decryption', () => { - it('encrypts and then decrypts to get original key', () => { + it('encrypts and then decrypts to get original key', async () => { const originalKey = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; const password = 'super-secret'; const pubkey = '0x' + 'ff'.repeat(33); const path = 'm/12381/3600/5/0/0'; // Create encrypted keystore - const encrypted = createBn254Keystore(password, originalKey, pubkey, path); + const encrypted = await createBn254Keystore(password, originalKey, pubkey, path); // Decrypt it const decrypted = decryptBn254KeystoreFromObject(encrypted, password); @@ -266,7 +266,7 @@ describe('BN254 Keystore', () => { expect(decrypted).toBe(originalKey); }); - it('round-trips multiple different keys', () => { + it('round-trips multiple different keys', async () => { const testCases = [ { key: '0x' + '11'.repeat(32), password: 'pass1' }, { key: '0x' + '22'.repeat(32), password: 'pass2' }, @@ -274,7 +274,7 @@ describe('BN254 Keystore', () => { ]; for (const { key, password } of testCases) { - const encrypted = createBn254Keystore(password, key, testPublicKey, testPath); + const encrypted = await createBn254Keystore(password, key, testPublicKey, testPath); const decrypted = decryptBn254KeystoreFromObject(encrypted, password); expect(decrypted).toBe(key); } diff --git a/yarn-project/foundation/src/crypto/bls/bn254_keystore.ts b/yarn-project/foundation/src/crypto/bls/bn254_keystore.ts index 88c8dd8932f2..44e3277631d0 100644 --- a/yarn-project/foundation/src/crypto/bls/bn254_keystore.ts +++ b/yarn-project/foundation/src/crypto/bls/bn254_keystore.ts @@ -1,7 +1,8 @@ import { randomBytes } from '@aztec/foundation/crypto/random'; -import { createCipheriv, createDecipheriv, createHash, pbkdf2Sync, randomUUID } from 'crypto'; +import { createCipheriv, createDecipheriv, createHash, pbkdf2, pbkdf2Sync, randomUUID } from 'crypto'; import { readFileSync } from 'fs'; +import { promisify } from 'util'; import { z } from 'zod'; /** @@ -100,24 +101,15 @@ export interface Bn254KeystoreInterface { version: number; } -/** - * Creates a BN254 keystore object for a BN254 BLS private key. - * - * Uses PBKDF2 with SHA-256 for key derivation and AES-128-CTR for encryption, - * following the EIP-2335 specification format. - * - * @param password - Password for encrypting the private key - * @param privateKeyHex - Private key as 0x-prefixed hex string (32 bytes) - * @param pubkeyHex - Public key as hex string (compressed or uncompressed) - * @param derivationPath - BIP-44 style derivation path (e.g., "m/12381/3600/0/0/0") - * @returns BN254 keystore object ready to be serialized to JSON - * @throws Error if private key is not 32-byte hex - */ -export function createBn254Keystore( - password: string, +const pbkdf2Async = promisify(pbkdf2); + +function createBn254KeystoreFromDerivedKey( privateKeyHex: string, pubkeyHex: string, derivationPath: string, + salt: Buffer, + iv: Buffer, + dk: Buffer, ): Bn254Keystore { const ensureHex = (hex: string) => hex.replace(/^0x/i, ''); const privHex = ensureHex(privateKeyHex); @@ -125,11 +117,7 @@ export function createBn254Keystore( throw new Error('BLS private key must be 32-byte hex'); } - const salt = randomBytes(32); - const iv = randomBytes(16); - const dk = pbkdf2Sync(Buffer.from(password.normalize('NFKD'), 'utf8'), salt, 262144, 32, 'sha256'); const cipherKey = dk.subarray(0, 16); - const cipher = createCipheriv('aes-128-ctr', cipherKey, iv); const plaintext = Buffer.from(privHex, 'hex'); const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]); @@ -166,6 +154,31 @@ export function createBn254Keystore( }; } +/** + * Creates a BN254 keystore object for a BN254 BLS private key. + * + * Uses PBKDF2 with SHA-256 for key derivation and AES-128-CTR for encryption, + * following the EIP-2335 specification format. + * + * @param password - Password for encrypting the private key + * @param privateKeyHex - Private key as 0x-prefixed hex string (32 bytes) + * @param pubkeyHex - Public key as hex string (compressed or uncompressed) + * @param derivationPath - BIP-44 style derivation path (e.g., "m/12381/3600/0/0/0") + * @returns BN254 keystore object ready to be serialized to JSON + * @throws Error if private key is not 32-byte hex + */ +export async function createBn254Keystore( + password: string, + privateKeyHex: string, + pubkeyHex: string, + derivationPath: string, +): Promise { + const salt = randomBytes(32); + const iv = randomBytes(16); + const dk = await pbkdf2Async(Buffer.from(password.normalize('NFKD'), 'utf8'), salt, 262144, 32, 'sha256'); + return createBn254KeystoreFromDerivedKey(privateKeyHex, pubkeyHex, derivationPath, salt, iv, dk); +} + /** * Loads and validates a BN254 keystore file. *