From 9ca30a36ffb54c03cf776ac139f25e59692091a4 Mon Sep 17 00:00:00 2001 From: Leo Ribeiro Date: Sun, 24 Sep 2023 08:22:48 -0400 Subject: [PATCH 1/4] Generate VUK with crypto.subtle --- packages/agent/src/app-data-store.ts | 35 ++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index dca19c401..a2ff01c11 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -5,9 +5,7 @@ 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'; @@ -143,20 +141,33 @@ export class AppDataVault implements AppDataStore { }): Promise { const { passphrase, salt } = options; - /** The salt value derived in Step 3 and the passphrase entered by the + const passwordBuffer = new TextEncoder().encode(passphrase); + + const importedKey = await crypto.subtle.importKey( + 'raw', + passwordBuffer, + 'PBKDF2', + false, + ['deriveBits'] + ); + + /** + * 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 + * key that will be referred to as the Vault Unlock Key (VUK). + */ + const vaultUnlockKey = await crypto.subtle.deriveBits( { - c : this._keyDerivationWorkFactor, // key derivation work factor - dkLen : 32 // derived key length, in bytes - } + name : 'PBKDF2', + hash : 'SHA-512', + salt : salt, + iterations : this._keyDerivationWorkFactor, + }, + importedKey, + 32 * 8, // 32 bytes ); - return vaultUnlockKey; + return new Uint8Array(vaultUnlockKey); } async getDid(): Promise { From c8a521b3818798a06d663adde4c121fc42a441c7 Mon Sep 17 00:00:00 2001 From: Leo Ribeiro Date: Thu, 5 Oct 2023 16:32:46 -0400 Subject: [PATCH 2/4] Check for crypto subtle then fallback to noble --- packages/agent/src/app-data-store.ts | 53 ++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index a2ff01c11..1420b21cd 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -135,7 +135,53 @@ export class AppDataVault implements AppDataStore { throw new Error ('Not implemented'); } - private async generateVaultUnlockKey(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). + */ + private generateVaultUnlockKey(options: { + passphrase: string, + salt: Uint8Array + }): Promise { + if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) { + return this.generateVaultUnlockKeyWithSubtleCrypto(options); + } else { + return this.generateVaultUnlockKeyWithNoble(options); + } + } + + private async generateVaultUnlockKeyWithSubtleCrypto(options: { + passphrase: string, + salt: Uint8Array + }): Promise { + const { passphrase, salt } = 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 : this._keyDerivationWorkFactor, + }, + importedKey, + 32 * 8, // 32 bytes + ); + + return new Uint8Array(vaultUnlockKey); + } + + private async generateVaultUnlockKeyWithNoble(options: { passphrase: string, salt: Uint8Array }): Promise { @@ -151,11 +197,6 @@ export class AppDataVault implements AppDataStore { ['deriveBits'] ); - /** - * 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 crypto.subtle.deriveBits( { name : 'PBKDF2', From 9874883459cf7f327565460f0c104cf42bafa8f8 Mon Sep 17 00:00:00 2001 From: Leo Ribeiro Date: Tue, 10 Oct 2023 16:39:26 -0400 Subject: [PATCH 3/4] replace noble with node:crypto --- packages/agent/src/app-data-store.ts | 50 ++++++++++++---------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index 1420b21cd..c5ad7e234 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -147,7 +147,7 @@ export class AppDataVault implements AppDataStore { if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) { return this.generateVaultUnlockKeyWithSubtleCrypto(options); } else { - return this.generateVaultUnlockKeyWithNoble(options); + return this.generateVaultUnlockKeyWithNodeCrypto(options); } } @@ -181,34 +181,28 @@ export class AppDataVault implements AppDataStore { return new Uint8Array(vaultUnlockKey); } - private async generateVaultUnlockKeyWithNoble(options: { - passphrase: string, - salt: Uint8Array - }): Promise { + private async generateVaultUnlockKeyWithNodeCrypto( + options: { passphrase: string; salt: Uint8Array } + ): Promise { const { passphrase, salt } = 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 : this._keyDerivationWorkFactor, - }, - importedKey, - 32 * 8, // 32 bytes - ); - - return new Uint8Array(vaultUnlockKey); + const { pbkdf2 } = await import('node:crypto'); + + return new Promise((resolve, reject) => { + pbkdf2( + passphrase, + salt, + this._keyDerivationWorkFactor, + 32, + 'sha512', + (err, derivedKey) => { + if (err) { + reject(err); + } else { + resolve(new Uint8Array(derivedKey)); + } + } + ); + }); } async getDid(): Promise { From f400157f3b206cd23e9f4415666a4041e586f164 Mon Sep 17 00:00:00 2001 From: Leo Ribeiro Date: Wed, 11 Oct 2023 12:59:53 -0400 Subject: [PATCH 4/4] Refactor VUK generator and add unit tests --- packages/agent/src/app-data-store.ts | 90 ++++------------------ packages/agent/src/vuk-generator.ts | 76 ++++++++++++++++++ packages/agent/tests/vuk-generator.spec.ts | 80 +++++++++++++++++++ 3 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 packages/agent/src/vuk-generator.ts create mode 100644 packages/agent/tests/vuk-generator.spec.ts diff --git a/packages/agent/src/app-data-store.ts b/packages/agent/src/app-data-store.ts index c5ad7e234..d9e642c89 100644 --- a/packages/agent/src/app-data-store.ts +++ b/packages/agent/src/app-data-store.ts @@ -8,6 +8,7 @@ import { sha256 } from '@noble/hashes/sha256'; import { randomBytes } from '@web5/crypto/utils'; import { Convert, MemoryStore } from '@web5/common'; import { CryptoKey, Jose, XChaCha20Poly1305 } from '@web5/crypto'; +import { generateVaultUnlockKey } from './vuk-generator.js'; export type AppDataBackup = { /** @@ -135,76 +136,6 @@ export class AppDataVault implements AppDataStore { throw new Error ('Not implemented'); } - /** - * 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). - */ - private generateVaultUnlockKey(options: { - passphrase: string, - salt: Uint8Array - }): Promise { - if (crypto && typeof crypto.subtle === 'object' && crypto.subtle != null) { - return this.generateVaultUnlockKeyWithSubtleCrypto(options); - } else { - return this.generateVaultUnlockKeyWithNodeCrypto(options); - } - } - - private async generateVaultUnlockKeyWithSubtleCrypto(options: { - passphrase: string, - salt: Uint8Array - }): Promise { - const { passphrase, salt } = 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 : this._keyDerivationWorkFactor, - }, - importedKey, - 32 * 8, // 32 bytes - ); - - return new Uint8Array(vaultUnlockKey); - } - - private async generateVaultUnlockKeyWithNodeCrypto( - options: { passphrase: string; salt: Uint8Array } - ): Promise { - const { passphrase, salt } = options; - const { pbkdf2 } = await import('node:crypto'); - - return new Promise((resolve, reject) => { - pbkdf2( - passphrase, - salt, - this._keyDerivationWorkFactor, - 32, - 'sha512', - (err, derivedKey) => { - if (err) { - reject(err); - } else { - resolve(new Uint8Array(derivedKey)); - } - } - ); - }); - } - async getDid(): Promise { // Get the Vault Key Set JWE from the data store. const vaultKeySet = await this._store.get('vaultKeySet'); @@ -323,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 }); @@ -406,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