Skip to content

Commit

Permalink
Add PBKDF2 and BIP39 Support (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimtendo authored and bitjson committed Feb 23, 2024
1 parent cb5c88b commit 8e032c2
Show file tree
Hide file tree
Showing 19 changed files with 22,736 additions and 3 deletions.
4 changes: 3 additions & 1 deletion docs/encodings-and-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ These functions include:

# Encoding

All message and data formats supported by Libauth have a matching `encode*` function. Encoding functions accept the data to encode and return either the encoded data (as a `Uint8Array`) or – if encoding can fail – an error message (`string`).
All message and data formats supported by Libauth have a matching `encode*` function. Encoding functions accept the data to encode and return either the encoded data or – if encoding can fail – an error message (`string`).

These functions include:

Expand All @@ -54,6 +54,7 @@ These functions include:
- `encodeBase58Address`
- `encodeBase58AddressFormat`
- `encodeBech32`
- `encodeBip39Mnemonic`
- `encodeCashAddress`
- `encodeCashAddressFormat`
- `encodeCashAddressNonStandard`
Expand Down Expand Up @@ -93,6 +94,7 @@ The `decode*` utility functions include:
- `decodeBase58Address`
- `decodeBase58AddressFormat`
- `decodeBech32`
- `decodeBip39Mnemonic`
- `decodeBitcoinSignature`
- `decodeCashAddress`
- `decodeCashAddressFormat`
Expand Down
1 change: 1 addition & 0 deletions src/lib/crypto/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from './combinations.js';
export * from './default-crypto-instances.js';
export * from './hmac.js';
export * from './pbkdf2.js';
export * from './ripemd160.js';
export * from './secp256k1.js';
export * from './secp256k1-types.js';
Expand Down
12 changes: 10 additions & 2 deletions src/lib/crypto/hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ import {
import { flattenBinArray } from '../format/format.js';
import type { Sha256, Sha512 } from '../lib.js';

export type HmacFunction = (
secret: Uint8Array,
message: Uint8Array,
) => Uint8Array;

/**
* Instantiate a hash-based message authentication code (HMAC) function as
* specified by RFC 2104.
Expand All @@ -14,8 +19,11 @@ import type { Sha256, Sha512 } from '../lib.js';
* @param blockByteLength - the byte-length of blocks used in `hashFunction`
*/
export const instantiateHmacFunction =
(hashFunction: (input: Uint8Array) => Uint8Array, blockByteLength: number) =>
(secret: Uint8Array, message: Uint8Array) => {
(
hashFunction: (input: Uint8Array) => Uint8Array,
blockByteLength: number,
): HmacFunction =>
(secret, message) => {
const key = new Uint8Array(blockByteLength).fill(0);
// eslint-disable-next-line functional/no-expression-statements
key.set(secret.length > blockByteLength ? hashFunction(secret) : secret, 0);
Expand Down
194 changes: 194 additions & 0 deletions src/lib/crypto/pbkdf2.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import test from 'ava';

import {
hexToBin,
hmacSha256,
instantiatePbkdf2Function,
Pbkdf2Error,
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.
* Test vectors from: https://github.com/brycx/Test-Vector-Generation/blob/72810c03e22af1b26fe5b254340e9ae5d9e44b06/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'),
},
});

/*
* Not regularly tested due to iteration count:
*
* test('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'),
},
});

/*
* Not regularly tested due to iteration count:
*
* test('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) => {
t.is(
instantiatePbkdf2Function(
hmacSha256,
0,
)({
derivedKeyLength: 256,
iterations: 4096,
password: utf8ToBin('password'),
salt: utf8ToBin('salt'),
}),
`${Pbkdf2Error.invalidHmacLength} HMAC length: 0.`,
);
t.is(
instantiatePbkdf2Function(
hmacSha256,
32,
)({
derivedKeyLength: 0,
iterations: 4096,
password: utf8ToBin('password'),
salt: utf8ToBin('salt'),
}),
`${Pbkdf2Error.invalidDerivedKeyLength} Derived key length: 0.`,
);
t.is(
instantiatePbkdf2Function(
hmacSha256,
32,
)({
derivedKeyLength: 256,
iterations: 0,
password: utf8ToBin('password'),
salt: utf8ToBin('salt'),
}),
`${Pbkdf2Error.invalidIterations} Iterations parameter: 0.`,
);
});
102 changes: 102 additions & 0 deletions src/lib/crypto/pbkdf2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { formatError, numberToBinUint32BE } from '../format/format.js';

import type { HmacFunction } from './hmac.js';
import { hmacSha256, hmacSha512 } from './hmac.js';

export enum Pbkdf2Error {
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 formatError(
Pbkdf2Error.invalidIterations,
`Iterations parameter: ${iterations}.`,
);
}
if (!Number.isInteger(derivedKeyLength) || derivedKeyLength <= 0) {
return formatError(
Pbkdf2Error.invalidDerivedKeyLength,
`Derived key length: ${derivedKeyLength}.`,
);
}
if (!Number.isInteger(hmacByteLength) || hmacByteLength <= 0) {
return formatError(
Pbkdf2Error.invalidHmacLength,
`HMAC length: ${hmacByteLength}.`,
);
}
const iterationCountByteLength = 4;
const derivedKey = new Uint8Array(derivedKeyLength);
const block = new Uint8Array(salt.length + iterationCountByteLength);
block.set(salt, 0);
let writePosition = 0;
const length = Math.ceil(derivedKeyLength / hmacByteLength);
for (let i = 1; i <= length; i++) {
const iterationUint32BEEncoded = numberToBinUint32BE(i);
block.set(iterationUint32BEEncoded, salt.length);
const accumulatedMac = hmacFunction(password, block);
let intermediateMac = accumulatedMac;
for (let j = 1; j < iterations; j++) {
intermediateMac = hmacFunction(password, intermediateMac);
for (let k = 0; k < hmacByteLength; k++) {
accumulatedMac[k] ^= intermediateMac[k]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
}
}
const truncatedResult = accumulatedMac.subarray(0, derivedKeyLength);
derivedKey.set(truncatedResult, writePosition);
writePosition += 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);
Loading

0 comments on commit 8e032c2

Please sign in to comment.