Skip to content

Commit

Permalink
Refactor VUK generator and add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
leordev committed Oct 11, 2023
1 parent 9874883 commit f400157
Show file tree
Hide file tree
Showing 3 changed files with 173 additions and 73 deletions.
90 changes: 17 additions & 73 deletions packages/agent/src/app-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<string> {
// Get the Vault Key Set JWE from the data store.
const vaultKeySet = await this._store.get('vaultKeySet');
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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
});

Check warning on line 353 in packages/agent/src/app-data-store.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/app-data-store.ts#L349-L353

Added lines #L349 - L353 were not covered by tests
}

return true;
Expand Down
76 changes: 76 additions & 0 deletions packages/agent/src/vuk-generator.ts
Original file line number Diff line number Diff line change
@@ -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<Uint8Array> => {
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<Uint8Array> => {
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<Uint8Array> => {
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);

Check warning on line 65 in packages/agent/src/vuk-generator.ts

View check run for this annotation

Codecov / codecov/patch

packages/agent/src/vuk-generator.ts#L65

Added line #L65 was not covered by tests
} else {
resolve(new Uint8Array(derivedKey));
}
}
);
});
};

export const dynamicImports = {
getNodeCrypto: async () => await import('node:crypto'),
};
80 changes: 80 additions & 0 deletions packages/agent/tests/vuk-generator.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});

0 comments on commit f400157

Please sign in to comment.