-
Notifications
You must be signed in to change notification settings - Fork 52
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
PBKDF2 and BIP39 Support #109
Changes from 6 commits
96514b8
a008f75
c92e5f9
358dfa7
240c304
0ba4b58
72ed9ca
506e142
44f784a
27c7f39
c165b8e
f325686
1cbccee
f083f85
ce54a82
9498ffb
0af3934
35d087f
0455d39
4aefb8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import test from 'ava'; | ||
|
||
import { | ||
hexToBin, | ||
pbkdf2HmacSha256, | ||
pbkdf2HmacSha512, | ||
utf8ToBin, | ||
} from '../lib.js'; | ||
import type { Pbkdf2Parameters } from '../lib.js'; | ||
|
||
const vectors = test.macro< | ||
[ | ||
{ | ||
parameters: Pbkdf2Parameters; | ||
expectedSha256: Uint8Array; | ||
expectedSha512: Uint8Array; | ||
} | ||
] | ||
>({ | ||
exec: (t, vector) => { | ||
t.deepEqual(pbkdf2HmacSha256(vector.parameters), vector.expectedSha256); | ||
t.deepEqual(pbkdf2HmacSha512(vector.parameters), vector.expectedSha512); | ||
}, | ||
title: (title) => `[crypto] PBKDF2 Test Vector #${title ?? '?'} (RFC 2898)`, | ||
}); | ||
|
||
/* | ||
* NOTE: RFC 2898 does NOT provide test vectors for SHA2 hash functions. | ||
* The following have been used instead: https://github.com/brycx/Test-Vector-Generation/blob/master/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md | ||
*/ | ||
|
||
test('1', vectors, { | ||
expectedSha256: hexToBin('120fb6cffcf8b32c43e7225256c4f837a86548c9'), | ||
expectedSha512: hexToBin('867f70cf1ade02cff3752599a3a53dc4af34c7a6'), | ||
parameters: { | ||
derivedKeyLength: 20, | ||
iterations: 1, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}, | ||
}); | ||
|
||
test('2', vectors, { | ||
expectedSha256: hexToBin('ae4d0c95af6b46d32d0adff928f06dd02a303f8e'), | ||
expectedSha512: hexToBin('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e'), | ||
parameters: { | ||
derivedKeyLength: 20, | ||
iterations: 2, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}, | ||
}); | ||
|
||
test('3', vectors, { | ||
expectedSha256: hexToBin('c5e478d59288c841aa530db6845c4c8d962893a0'), | ||
expectedSha512: hexToBin('d197b1b33db0143e018b12f3d1d1479e6cdebdcc'), | ||
parameters: { | ||
derivedKeyLength: 20, | ||
iterations: 4096, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}, | ||
}); | ||
|
||
// NOTE: Skipped due to high iteration count. | ||
test.skip('4', vectors, { | ||
expectedSha256: hexToBin('cf81c66fe8cfc04d1f31ecb65dab4089f7f179e8'), | ||
expectedSha512: hexToBin('6180a3ceabab45cc3964112c811e0131bca93a35'), | ||
parameters: { | ||
derivedKeyLength: 20, | ||
iterations: 16777216, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}, | ||
}); | ||
|
||
test('5', vectors, { | ||
expectedSha256: hexToBin( | ||
'348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c' | ||
), | ||
expectedSha512: hexToBin( | ||
'8c0511f4c6e597c6ac6315d8f0362e225f3c501495ba23b868' | ||
), | ||
parameters: { | ||
derivedKeyLength: 25, | ||
iterations: 4096, | ||
password: utf8ToBin('passwordPASSWORDpassword'), | ||
salt: utf8ToBin('saltSALTsaltSALTsaltSALTsaltSALTsalt'), | ||
}, | ||
}); | ||
|
||
test('6', vectors, { | ||
expectedSha256: hexToBin('89b69d0516f829893c696226650a8687'), | ||
expectedSha512: hexToBin('9d9e9c4cd21fe4be24d5b8244c759665'), | ||
parameters: { | ||
derivedKeyLength: 16, | ||
iterations: 4096, | ||
password: utf8ToBin('pass\0word'), | ||
salt: utf8ToBin('sa\0lt'), | ||
}, | ||
}); | ||
|
||
test('7', vectors, { | ||
expectedSha256: hexToBin( | ||
'55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783c294e850150390e1160c34d62e9665d659ae49d314510fc98274cc79681968104b8f89237e69b2d549111868658be62f59bd715cac44a1147ed5317c9bae6b2a' | ||
), | ||
expectedSha512: hexToBin( | ||
'c74319d99499fc3e9013acff597c23c5baf0a0bec5634c46b8352b793e324723d55caa76b2b25c43402dcfdc06cdcf66f95b7d0429420b39520006749c51a04ef3eb99e576617395a178ba33214793e48045132928a9e9bf2661769fdc668f31798597aaf6da70dd996a81019726084d70f152baed8aafe2227c07636c6ddece' | ||
), | ||
parameters: { | ||
derivedKeyLength: 128, | ||
iterations: 1, | ||
password: utf8ToBin('passwd'), | ||
salt: utf8ToBin('salt'), | ||
}, | ||
}); | ||
|
||
// NOTE: Skipped due to high iteration count. | ||
test.skip('8', vectors, { | ||
expectedSha256: hexToBin( | ||
'4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d62aae85a11cdde829d89cb6ffd1ab0e63a981f8747d2f2f9fe5874165c83c168d2eed1d2d5ca4052dec2be5715623da019b8c0ec87dc36aa751c38f9893d15c3' | ||
), | ||
expectedSha512: hexToBin( | ||
'e6337d6fbeb645c794d4a9b5b75b7b30dac9ac50376a91df1f4460f6060d5addb2c1fd1f84409abacc67de7eb4056e6bb06c2d82c3ef4ccd1bded0f675ed97c65c33d39f81248454327aa6d03fd049fc5cbb2b5e6dac08e8ace996cdc960b1bd4530b7e754773d75f67a733fdb99baf6470e42ffcb753c15c352d4800fb6f9d6' | ||
), | ||
parameters: { | ||
derivedKeyLength: 128, | ||
iterations: 80000, | ||
password: utf8ToBin('Password'), | ||
salt: utf8ToBin('NaCl'), | ||
}, | ||
}); | ||
|
||
test('9', vectors, { | ||
expectedSha256: hexToBin( | ||
'436c82c6af9010bb0fdb274791934ac7dee21745dd11fb57bb90112ab187c495ad82df776ad7cefb606f34fedca59baa5922a57f3e91bc0e11960da7ec87ed0471b456a0808b60dff757b7d313d4068bf8d337a99caede24f3248f87d1bf16892b70b076a07dd163a8a09db788ae34300ff2f2d0a92c9e678186183622a636f4cbce15680dfea46f6d224e51c299d4946aa2471133a649288eef3e4227b609cf203dba65e9fa69e63d35b6ff435ff51664cbd6773d72ebc341d239f0084b004388d6afa504eee6719a7ae1bb9daf6b7628d851fab335f1d13948e8ee6f7ab033a32df447f8d0950809a70066605d6960847ed436fa52cdfbcf261b44d2a87061' | ||
), | ||
expectedSha512: hexToBin( | ||
'10176fb32cb98cd7bb31e2bb5c8f6e425c103333a2e496058e3fd2bd88f657485c89ef92daa0668316bc23ebd1ef88f6dd14157b2320b5d54b5f26377c5dc279b1dcdec044bd6f91b166917c80e1e99ef861b1d2c7bce1b961178125fb86867f6db489a2eae0022e7bc9cf421f044319fac765d70cb89b45c214590e2ffb2c2b565ab3b9d07571fde0027b1dc57f8fd25afa842c1056dd459af4074d7510a0c020b914a5e202445d4d3f151070589dd6a2554fc506018c4f001df6239643dc86771286ae4910769d8385531bba57544d63c3640b90c98f1445ebdd129475e02086b600f0beb5b05cc6ca9b3633b452b7dad634e9336f56ec4c3ac0b4fe54ced8' | ||
), | ||
parameters: { | ||
derivedKeyLength: 256, | ||
iterations: 4096, | ||
password: utf8ToBin('Password'), | ||
salt: utf8ToBin('sa\0lt'), | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { numberToBinUint32BE } from '../format/format.js'; | ||
|
||
import type { HmacFunction} from './crypto.js'; | ||
import { hmacSha256, hmacSha512 } from './crypto.js'; | ||
|
||
export enum Pbkdf2Errors { | ||
invalidIterationsError = 'Invalid PBKDF2 Parameters: Iterations must be a positive integer', | ||
invalidDerivedKeyLengthError = 'Invalid PBKDF2 Parameters: Derived Key Length must be a positive integer', | ||
invalidHmacLength = 'Invalid HMAC length: HMAC length must be a positive integer', | ||
} | ||
|
||
/** | ||
* An object representing the parameters to use with PBKDF2 (Password-Based Key Derivation Function 2). | ||
*/ | ||
export interface Pbkdf2Parameters { | ||
/** The length of the derived key in bytes. */ | ||
derivedKeyLength: number; | ||
password: Uint8Array; | ||
iterations: number; | ||
salt: Uint8Array; | ||
} | ||
|
||
export const instantiatePbkdf2Function = | ||
(hmacFunction: HmacFunction, hmacByteLength: number) => | ||
(parameters: Pbkdf2Parameters) => { | ||
/* eslint-disable functional/no-let, functional/no-loop-statement, functional/no-expression-statement, no-bitwise, no-plusplus */ | ||
const { password, salt, iterations, derivedKeyLength } = parameters; | ||
|
||
if (!Number.isInteger(iterations) || iterations <= 0) { | ||
return Pbkdf2Errors.invalidIterationsError; | ||
} | ||
|
||
if (!Number.isInteger(derivedKeyLength) || derivedKeyLength <= 0) { | ||
return Pbkdf2Errors.invalidDerivedKeyLengthError; | ||
} | ||
|
||
if (!Number.isInteger(hmacByteLength) || hmacByteLength <= 0) { | ||
return Pbkdf2Errors.invalidHmacLength; | ||
} | ||
|
||
const iterationCountByteLength = 4; | ||
|
||
const derivedKey = new Uint8Array(derivedKeyLength); | ||
const block = new Uint8Array(salt.length + iterationCountByteLength); | ||
block.set(salt, 0); | ||
|
||
let destPos = 0; | ||
const length = Math.ceil(derivedKeyLength / hmacByteLength); | ||
|
||
for (let i = 1; i <= length; i++) { | ||
const iterationUint32BEEncoded = numberToBinUint32BE(i); | ||
block.set(iterationUint32BEEncoded, salt.length); | ||
|
||
const T = hmacFunction(password, block); | ||
let U = T; | ||
|
||
for (let j = 1; j < iterations; j++) { | ||
U = hmacFunction(password, U); | ||
|
||
for (let k = 0; k < hmacByteLength; k++) { | ||
// @ts-expect-error-next-line | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bitjson No idea how to get rid of this one. Algo is almost verbatim https://github.com/browserify/pbkdf2/blob/master/lib/sync-browser.js with a few minor changes (e.g. Buffer to Uint8Array). I'm wondering if Typescript is doing something funky because we're doing a bitwise op. If you have any ideas, please let me know. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, looks like the error is just that T[k] ^= U[k]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion (Breaking rules is fine in sensible places, the linting just forces us to be explicit and deliberate about it. 👍) |
||
T[k] ^= U[k]; | ||
} | ||
} | ||
|
||
// Truncate the results to the maximum key length. | ||
const truncated = T.subarray(0, derivedKeyLength); | ||
|
||
derivedKey.set(truncated, destPos); | ||
destPos += hmacByteLength; | ||
} | ||
|
||
return derivedKey; | ||
/* eslint-enable functional/no-let, functional/no-loop-statement, functional/no-expression-statement, no-bitwise, no-plusplus */ | ||
}; | ||
|
||
const hmacSha256ByteLength = 32; | ||
|
||
export const pbkdf2HmacSha256 = ( | ||
parameters: Pbkdf2Parameters, | ||
sha256Hmac: HmacFunction = hmacSha256 | ||
) => instantiatePbkdf2Function( | ||
sha256Hmac, | ||
hmacSha256ByteLength | ||
)(parameters); | ||
|
||
const hmacSha512ByteLength = 64; | ||
|
||
export const pbkdf2HmacSha512 = ( | ||
parameters: Pbkdf2Parameters, | ||
sha512Hmac: HmacFunction = hmacSha512 | ||
) => instantiatePbkdf2Function( | ||
sha512Hmac, | ||
hmacSha512ByteLength | ||
)(parameters); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bitjson If we include these checks, we break a linter "complexity" rule (max is 5, we end up on 10).
I'm not sure if these are maybe overkill? Or maybe should be split out into a separate
verifyPbkdf2Parameters
function?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOTE: If we keep these in, will try to add fail test cases for completeness.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These checks are great! They make sense here, and I don't really see a reason to break them out into a separate function (it would be quite the stretch to use just this validation in some other context). So this is a good place to break our complexity rule (right above the relevant function):
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have placed this one in.