diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index dca19c401..d9e642c89 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -5,11 +5,10 @@ import type { JweHeaderParams, PublicKeyJwk, Web5Crypto } from '@web5/crypto'; import { DidKeyMethod } from '@web5/dids'; import { hkdf } from '@noble/hashes/hkdf'; import { sha256 } from '@noble/hashes/sha256'; -import { sha512 } from '@noble/hashes/sha512'; import { randomBytes } from '@web5/crypto/utils'; -import { pbkdf2Async } from '@noble/hashes/pbkdf2'; import { Convert, MemoryStore } from '@web5/common'; import { CryptoKey, Jose, XChaCha20Poly1305 } from '@web5/crypto'; +import { generateVaultUnlockKey } from './vuk-generator.js'; export type AppDataBackup = { /** @@ -137,28 +136,6 @@ export class AppDataVault implements AppDataStore { throw new Error ('Not implemented'); } - private async generateVaultUnlockKey(options: { - passphrase: string, - salt: Uint8Array - }): Promise { - const { passphrase, salt } = options; - - /** The salt value derived in Step 3 and the passphrase entered by the - * end-user are inputs to the PBKDF2 algorithm to derive a 32-byte secret - * key that will be referred to as the Vault Unlock Key (VUK). */ - const vaultUnlockKey = await pbkdf2Async( - sha512, // hash function - passphrase, // password - salt, // salt - { - c : this._keyDerivationWorkFactor, // key derivation work factor - dkLen : 32 // derived key length, in bytes - } - ); - - return vaultUnlockKey; - } - async getDid(): Promise { // Get the Vault Key Set JWE from the data store. const vaultKeySet = await this._store.get('vaultKeySet'); @@ -277,8 +254,17 @@ export class AppDataVault implements AppDataStore { /** * Generate a vault unlock key (VUK), which will be used as a - * key encryption key (KEK) for wrapping the private key */ - this._vaultUnlockKey = await this.generateVaultUnlockKey({ passphrase, salt }); + * key encryption key (KEK) for wrapping the private key. + * + * The salt value derived in Step 3 and the passphrase entered by the + * end-user are inputs to the PBKDF2 algorithm to derive a 32-byte secret + * key that will be referred to as the Vault Unlock Key (VUK). + */ + this._vaultUnlockKey = await generateVaultUnlockKey({ + passphrase, + salt, + keyDerivationWorkFactor: this._keyDerivationWorkFactor, + }); /** Convert the public crypto key to JWK format to store within the JWE. */ const wrappedKey = await Jose.cryptoKeyToJwk({ key: keyPair.publicKey }); @@ -360,7 +346,11 @@ export class AppDataVault implements AppDataStore { // Derive the Vault Unlock Key (VUK). if (protectedHeader.p2s !== undefined) { const salt = Convert.base64Url(protectedHeader.p2s).toUint8Array(); - this._vaultUnlockKey = await this.generateVaultUnlockKey({ passphrase, salt }); + this._vaultUnlockKey = await generateVaultUnlockKey({ + passphrase, + salt, + keyDerivationWorkFactor: this._keyDerivationWorkFactor + }); } return true; diff --git a/packages/agent/src/vuk-generator.ts b/packages/agent/src/vuk-generator.ts new file mode 100644 index 000000000..7f88c6552 --- /dev/null +++ b/packages/agent/src/vuk-generator.ts @@ -0,0 +1,76 @@ +/** VUK Generation Options */ +export interface VukOptions { + /** The passphrase to use to generate the VUK */ + passphrase: string; + + /** The salt to use to generate the VUK */ + salt: Uint8Array; + + /** The key derivation work factor to use to generate the VUK */ + keyDerivationWorkFactor: number; +} + +/** Generates the VUK with Crypto Subtle if present, otherwise fallback to node:crypto */ +export const generateVaultUnlockKey = (options: VukOptions): Promise => { + if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) { + return generateVaultUnlockKeyWithSubtleCrypto(options); + } else { + return generateVaultUnlockKeyWithNodeCrypto(options); + } +}; + +/** Generates the VUK with Crypto Subtle */ +export const generateVaultUnlockKeyWithSubtleCrypto = async (options: VukOptions): Promise => { + const { passphrase, salt, keyDerivationWorkFactor } = options; + + const passwordBuffer = new TextEncoder().encode(passphrase); + + const importedKey = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + 'PBKDF2', + false, + ['deriveBits'] + ); + + const vaultUnlockKey = await crypto.subtle.deriveBits( + { + name : 'PBKDF2', + hash : 'SHA-512', + salt : salt, + iterations : keyDerivationWorkFactor, + }, + importedKey, + 32 * 8, // 32 bytes + ); + + return new Uint8Array(vaultUnlockKey); +}; + +/** Generates the VUK with node:crypto */ +export const generateVaultUnlockKeyWithNodeCrypto = async (options: VukOptions): Promise => { + const { passphrase, salt, keyDerivationWorkFactor } = options; + + const { pbkdf2 } = await dynamicImports.getNodeCrypto(); + + return new Promise((resolve, reject) => { + pbkdf2( + passphrase, + salt, + keyDerivationWorkFactor, + 32, + 'sha512', + (err, derivedKey) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(derivedKey)); + } + } + ); + }); +}; + +export const dynamicImports = { + getNodeCrypto: async () => await import('node:crypto'), +}; diff --git a/packages/agent/tests/vuk-generator.spec.ts b/packages/agent/tests/vuk-generator.spec.ts new file mode 100644 index 000000000..4d9e4c529 --- /dev/null +++ b/packages/agent/tests/vuk-generator.spec.ts @@ -0,0 +1,80 @@ +import * as sinon from 'sinon'; +import chai, { expect } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; + +import { sha512 } from '@noble/hashes/sha512'; +import { pbkdf2Async } from '@noble/hashes/pbkdf2'; + +chai.use(chaiAsPromised); + +import { + generateVaultUnlockKey, + generateVaultUnlockKeyWithNodeCrypto, + generateVaultUnlockKeyWithSubtleCrypto, + dynamicImports, +} from '../src/vuk-generator.js'; + +const keyDerivationWorkFactor = 650_000; + +describe('VUK Generator', () => { + it('should use crypto subtle by default', async () => { + const subtleImportKeySpy = sinon.spy(crypto.subtle, 'importKey'); + const subtleDeriveBitsSpy = sinon.spy(crypto.subtle, 'deriveBits'); + + const passphrase = 'dumbbell-krakatoa-ditty'; + const salt = new Uint8Array(32); + + const vuk = await generateVaultUnlockKey({ + passphrase, + salt, + keyDerivationWorkFactor + }); + + expect(vuk.length).to.equal(32); + + expect(subtleImportKeySpy.called).to.be.true; + expect(subtleDeriveBitsSpy.called).to.be.true; + + subtleImportKeySpy.restore(); + subtleDeriveBitsSpy.restore(); + }); + + it('should fallback to node:crypto if subtle is not present', async () => { + sinon.stub(crypto, 'subtle').value(null); + const getNodeCrypto = sinon.spy(dynamicImports, 'getNodeCrypto'); + + const passphrase = 'dumbbell-krakatoa-ditty'; + const salt = new Uint8Array(32); + + const vuk = await generateVaultUnlockKey({ passphrase, salt, keyDerivationWorkFactor }); + expect(vuk.length).to.equal(32); + + expect(getNodeCrypto.called).to.be.true; + + getNodeCrypto.restore(); + sinon.restore(); + }); + + it('vuks are the same regardless of algorithm', async () => { + const passphrase = 'dumbbell-krakatoa-ditty'; + const salt = new Uint8Array(32); + const options = { passphrase, salt, keyDerivationWorkFactor: 10_000 }; + + const subtleVuk = await generateVaultUnlockKeyWithNodeCrypto(options); + const nodeCryptoVuk = await generateVaultUnlockKeyWithSubtleCrypto(options); + expect(subtleVuk).to.deep.equal(nodeCryptoVuk); + + // asserts that the previously used noble algo matches too + const nobleVuk = await pbkdf2Async( + sha512, + passphrase, + salt, + { + c : options.keyDerivationWorkFactor, + dkLen : 32 + } + ); + + expect(nobleVuk).to.deep.equal(subtleVuk); + }); +}); \ No newline at end of file