generated from TBD54566975/tbd-project-template
-
Notifications
You must be signed in to change notification settings - Fork 57
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor VUK generator and add unit tests
- Loading branch information
Showing
3 changed files
with
173 additions
and
73 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'), | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}); |