-
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 all 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,194 @@ | ||
import test from 'ava'; | ||
|
||
import { | ||
hexToBin, | ||
hmacSha256, | ||
instantiatePbkdf2Function, | ||
Pbkdf2Errors, | ||
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'), | ||
}, | ||
}); | ||
|
||
test('returns error on invalid parameters', (t) => { | ||
// Invalid HMAC length. | ||
t.is( | ||
instantiatePbkdf2Function( | ||
hmacSha256, | ||
0, | ||
)({ | ||
derivedKeyLength: 256, | ||
iterations: 4096, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}), | ||
Pbkdf2Errors.invalidHmacLength, | ||
); | ||
|
||
// Invalid derived key length. | ||
t.is( | ||
instantiatePbkdf2Function( | ||
hmacSha256, | ||
32, | ||
)({ | ||
derivedKeyLength: 0, | ||
iterations: 4096, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}), | ||
Pbkdf2Errors.invalidDerivedKeyLength, | ||
); | ||
|
||
// Invalid iterations. | ||
t.is( | ||
instantiatePbkdf2Function( | ||
hmacSha256, | ||
32, | ||
)({ | ||
derivedKeyLength: 256, | ||
iterations: 0, | ||
password: utf8ToBin('password'), | ||
salt: utf8ToBin('salt'), | ||
}), | ||
Pbkdf2Errors.invalidIterations, | ||
); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
import { numberToBinUint32BE } from '../format/format.js'; | ||
|
||
import type { HmacFunction } from './hmac.js'; | ||
import { hmacSha256, hmacSha512 } from './hmac.js'; | ||
|
||
export enum Pbkdf2Errors { | ||
invalidIterations = 'Invalid PBKDF2 Parameters: Iterations must be a positive integer', | ||
invalidDerivedKeyLength = '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 type Pbkdf2Parameters = { | ||
/** The length of the derived key in bytes. */ | ||
derivedKeyLength: number; | ||
password: Uint8Array; | ||
iterations: number; | ||
salt: Uint8Array; | ||
}; | ||
|
||
/** | ||
* Instantiate a PBKDF2 function as specified by RFC 2898. | ||
* | ||
* @param hmacFunction - the HMAC function to use | ||
* @param hmacByteLength - the byte-length of the HMAC function | ||
*/ | ||
export const instantiatePbkdf2Function = | ||
(hmacFunction: HmacFunction, hmacByteLength: number) => | ||
// eslint-disable-next-line complexity | ||
(parameters: Pbkdf2Parameters) => { | ||
/* eslint-disable functional/immutable-data, functional/no-let, functional/no-loop-statements, functional/no-expression-statements, no-bitwise, no-plusplus */ | ||
const { password, salt, iterations, derivedKeyLength } = parameters; | ||
|
||
if (!Number.isInteger(iterations) || iterations <= 0) { | ||
return Pbkdf2Errors.invalidIterations; | ||
} | ||
|
||
if (!Number.isInteger(derivedKeyLength) || derivedKeyLength <= 0) { | ||
return Pbkdf2Errors.invalidDerivedKeyLength; | ||
} | ||
|
||
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/immutable-data, functional/no-let, functional/no-loop-statements, functional/no-expression-statements, no-bitwise, no-plusplus */ | ||
}; | ||
|
||
const hmacSha256ByteLength = 32; | ||
|
||
/** | ||
* Derive a key using PBKDF2 and the HMAC SHA256 function as specified in RFC 2898. | ||
* | ||
* @param parameters - the PBKDF2 parameters to use | ||
* @param sha256Hmac - the SHA256 HMAC implementation to use | ||
*/ | ||
export const pbkdf2HmacSha256 = ( | ||
parameters: Pbkdf2Parameters, | ||
sha256Hmac: HmacFunction = hmacSha256, | ||
) => instantiatePbkdf2Function(sha256Hmac, hmacSha256ByteLength)(parameters); | ||
|
||
const hmacSha512ByteLength = 64; | ||
|
||
/** | ||
* Derive a key using PBKDF2 and the HMAC SHA512 function as specified in RFC 2898. | ||
* | ||
* @param parameters - the PBKDF2 parameters to use | ||
* @param sha512Hmac - the SHA512 HMAC implementation to use | ||
*/ | ||
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.