Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate VUK with crypto.subtle #224

Closed
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
44 changes: 17 additions & 27 deletions packages/agent/src/app-data-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down Expand Up @@ -137,28 +136,6 @@ export class AppDataVault implements AppDataStore {
throw new Error ('Not implemented');
}

private async generateVaultUnlockKey(options: {
passphrase: string,
salt: Uint8Array
}): Promise<Uint8Array> {
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<string> {
// Get the Vault Key Set JWE from the data store.
const vaultKeySet = await this._store.get('vaultKeySet');
Expand Down Expand Up @@ -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 });
Expand Down Expand Up @@ -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;
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);
} 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);
});
});