Skip to content

Commit 8e032c2

Browse files
jimtendobitjson
authored andcommitted
Add PBKDF2 and BIP39 Support (#109)
1 parent cb5c88b commit 8e032c2

19 files changed

Lines changed: 22736 additions & 3 deletions

docs/encodings-and-formats.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ These functions include:
4242

4343
# Encoding
4444

45-
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`).
45+
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`).
4646

4747
These functions include:
4848

@@ -54,6 +54,7 @@ These functions include:
5454
- `encodeBase58Address`
5555
- `encodeBase58AddressFormat`
5656
- `encodeBech32`
57+
- `encodeBip39Mnemonic`
5758
- `encodeCashAddress`
5859
- `encodeCashAddressFormat`
5960
- `encodeCashAddressNonStandard`
@@ -93,6 +94,7 @@ The `decode*` utility functions include:
9394
- `decodeBase58Address`
9495
- `decodeBase58AddressFormat`
9596
- `decodeBech32`
97+
- `decodeBip39Mnemonic`
9698
- `decodeBitcoinSignature`
9799
- `decodeCashAddress`
98100
- `decodeCashAddressFormat`

src/lib/crypto/crypto.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './combinations.js';
22
export * from './default-crypto-instances.js';
33
export * from './hmac.js';
4+
export * from './pbkdf2.js';
45
export * from './ripemd160.js';
56
export * from './secp256k1.js';
67
export * from './secp256k1-types.js';

src/lib/crypto/hmac.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ import {
55
import { flattenBinArray } from '../format/format.js';
66
import type { Sha256, Sha512 } from '../lib.js';
77

8+
export type HmacFunction = (
9+
secret: Uint8Array,
10+
message: Uint8Array,
11+
) => Uint8Array;
12+
813
/**
914
* Instantiate a hash-based message authentication code (HMAC) function as
1015
* specified by RFC 2104.
@@ -14,8 +19,11 @@ import type { Sha256, Sha512 } from '../lib.js';
1419
* @param blockByteLength - the byte-length of blocks used in `hashFunction`
1520
*/
1621
export const instantiateHmacFunction =
17-
(hashFunction: (input: Uint8Array) => Uint8Array, blockByteLength: number) =>
18-
(secret: Uint8Array, message: Uint8Array) => {
22+
(
23+
hashFunction: (input: Uint8Array) => Uint8Array,
24+
blockByteLength: number,
25+
): HmacFunction =>
26+
(secret, message) => {
1927
const key = new Uint8Array(blockByteLength).fill(0);
2028
// eslint-disable-next-line functional/no-expression-statements
2129
key.set(secret.length > blockByteLength ? hashFunction(secret) : secret, 0);

src/lib/crypto/pbkdf2.spec.ts

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import test from 'ava';
2+
3+
import {
4+
hexToBin,
5+
hmacSha256,
6+
instantiatePbkdf2Function,
7+
Pbkdf2Error,
8+
pbkdf2HmacSha256,
9+
pbkdf2HmacSha512,
10+
utf8ToBin,
11+
} from '../lib.js';
12+
import type { Pbkdf2Parameters } from '../lib.js';
13+
14+
const vectors = test.macro<
15+
[
16+
{
17+
parameters: Pbkdf2Parameters;
18+
expectedSha256: Uint8Array;
19+
expectedSha512: Uint8Array;
20+
},
21+
]
22+
>({
23+
exec: (t, vector) => {
24+
t.deepEqual(pbkdf2HmacSha256(vector.parameters), vector.expectedSha256);
25+
t.deepEqual(pbkdf2HmacSha512(vector.parameters), vector.expectedSha512);
26+
},
27+
title: (title) => `[crypto] PBKDF2 Test Vector #${title ?? '?'} (RFC 2898)`,
28+
});
29+
30+
/*
31+
* NOTE: RFC 2898 does NOT provide test vectors for SHA2 hash functions.
32+
* Test vectors from: https://github.com/brycx/Test-Vector-Generation/blob/72810c03e22af1b26fe5b254340e9ae5d9e44b06/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md
33+
*/
34+
test('1', vectors, {
35+
expectedSha256: hexToBin('120fb6cffcf8b32c43e7225256c4f837a86548c9'),
36+
expectedSha512: hexToBin('867f70cf1ade02cff3752599a3a53dc4af34c7a6'),
37+
parameters: {
38+
derivedKeyLength: 20,
39+
iterations: 1,
40+
password: utf8ToBin('password'),
41+
salt: utf8ToBin('salt'),
42+
},
43+
});
44+
45+
test('2', vectors, {
46+
expectedSha256: hexToBin('ae4d0c95af6b46d32d0adff928f06dd02a303f8e'),
47+
expectedSha512: hexToBin('e1d9c16aa681708a45f5c7c4e215ceb66e011a2e'),
48+
parameters: {
49+
derivedKeyLength: 20,
50+
iterations: 2,
51+
password: utf8ToBin('password'),
52+
salt: utf8ToBin('salt'),
53+
},
54+
});
55+
56+
test('3', vectors, {
57+
expectedSha256: hexToBin('c5e478d59288c841aa530db6845c4c8d962893a0'),
58+
expectedSha512: hexToBin('d197b1b33db0143e018b12f3d1d1479e6cdebdcc'),
59+
parameters: {
60+
derivedKeyLength: 20,
61+
iterations: 4096,
62+
password: utf8ToBin('password'),
63+
salt: utf8ToBin('salt'),
64+
},
65+
});
66+
67+
/*
68+
* Not regularly tested due to iteration count:
69+
*
70+
* test('4', vectors, {
71+
* expectedSha256: hexToBin('cf81c66fe8cfc04d1f31ecb65dab4089f7f179e8'),
72+
* expectedSha512: hexToBin('6180a3ceabab45cc3964112c811e0131bca93a35'),
73+
* parameters: {
74+
* derivedKeyLength: 20,
75+
* iterations: 16777216,
76+
* password: utf8ToBin('password'),
77+
* salt: utf8ToBin('salt'),
78+
* },
79+
* });
80+
*/
81+
82+
test('5', vectors, {
83+
expectedSha256: hexToBin(
84+
'348c89dbcbd32b2f32d814b8116e84cf2b17347ebc1800181c',
85+
),
86+
expectedSha512: hexToBin(
87+
'8c0511f4c6e597c6ac6315d8f0362e225f3c501495ba23b868',
88+
),
89+
parameters: {
90+
derivedKeyLength: 25,
91+
iterations: 4096,
92+
password: utf8ToBin('passwordPASSWORDpassword'),
93+
salt: utf8ToBin('saltSALTsaltSALTsaltSALTsaltSALTsalt'),
94+
},
95+
});
96+
97+
test('6', vectors, {
98+
expectedSha256: hexToBin('89b69d0516f829893c696226650a8687'),
99+
expectedSha512: hexToBin('9d9e9c4cd21fe4be24d5b8244c759665'),
100+
parameters: {
101+
derivedKeyLength: 16,
102+
iterations: 4096,
103+
password: utf8ToBin('pass\0word'),
104+
salt: utf8ToBin('sa\0lt'),
105+
},
106+
});
107+
108+
test('7', vectors, {
109+
expectedSha256: hexToBin(
110+
'55ac046e56e3089fec1691c22544b605f94185216dde0465e68b9d57c20dacbc49ca9cccf179b645991664b39d77ef317c71b845b1e30bd509112041d3a19783c294e850150390e1160c34d62e9665d659ae49d314510fc98274cc79681968104b8f89237e69b2d549111868658be62f59bd715cac44a1147ed5317c9bae6b2a',
111+
),
112+
expectedSha512: hexToBin(
113+
'c74319d99499fc3e9013acff597c23c5baf0a0bec5634c46b8352b793e324723d55caa76b2b25c43402dcfdc06cdcf66f95b7d0429420b39520006749c51a04ef3eb99e576617395a178ba33214793e48045132928a9e9bf2661769fdc668f31798597aaf6da70dd996a81019726084d70f152baed8aafe2227c07636c6ddece',
114+
),
115+
parameters: {
116+
derivedKeyLength: 128,
117+
iterations: 1,
118+
password: utf8ToBin('passwd'),
119+
salt: utf8ToBin('salt'),
120+
},
121+
});
122+
123+
/*
124+
* Not regularly tested due to iteration count:
125+
*
126+
* test('8', vectors, {
127+
* expectedSha256: hexToBin(
128+
* '4ddcd8f60b98be21830cee5ef22701f9641a4418d04c0414aeff08876b34ab56a1d425a1225833549adb841b51c9b3176a272bdebba1d078478f62b397f33c8d62aae85a11cdde829d89cb6ffd1ab0e63a981f8747d2f2f9fe5874165c83c168d2eed1d2d5ca4052dec2be5715623da019b8c0ec87dc36aa751c38f9893d15c3',
129+
* ),
130+
* expectedSha512: hexToBin(
131+
* 'e6337d6fbeb645c794d4a9b5b75b7b30dac9ac50376a91df1f4460f6060d5addb2c1fd1f84409abacc67de7eb4056e6bb06c2d82c3ef4ccd1bded0f675ed97c65c33d39f81248454327aa6d03fd049fc5cbb2b5e6dac08e8ace996cdc960b1bd4530b7e754773d75f67a733fdb99baf6470e42ffcb753c15c352d4800fb6f9d6',
132+
* ),
133+
* parameters: {
134+
* derivedKeyLength: 128,
135+
* iterations: 80000,
136+
* password: utf8ToBin('Password'),
137+
* salt: utf8ToBin('NaCl'),
138+
* },
139+
* });
140+
*/
141+
142+
test('9', vectors, {
143+
expectedSha256: hexToBin(
144+
'436c82c6af9010bb0fdb274791934ac7dee21745dd11fb57bb90112ab187c495ad82df776ad7cefb606f34fedca59baa5922a57f3e91bc0e11960da7ec87ed0471b456a0808b60dff757b7d313d4068bf8d337a99caede24f3248f87d1bf16892b70b076a07dd163a8a09db788ae34300ff2f2d0a92c9e678186183622a636f4cbce15680dfea46f6d224e51c299d4946aa2471133a649288eef3e4227b609cf203dba65e9fa69e63d35b6ff435ff51664cbd6773d72ebc341d239f0084b004388d6afa504eee6719a7ae1bb9daf6b7628d851fab335f1d13948e8ee6f7ab033a32df447f8d0950809a70066605d6960847ed436fa52cdfbcf261b44d2a87061',
145+
),
146+
expectedSha512: hexToBin(
147+
'10176fb32cb98cd7bb31e2bb5c8f6e425c103333a2e496058e3fd2bd88f657485c89ef92daa0668316bc23ebd1ef88f6dd14157b2320b5d54b5f26377c5dc279b1dcdec044bd6f91b166917c80e1e99ef861b1d2c7bce1b961178125fb86867f6db489a2eae0022e7bc9cf421f044319fac765d70cb89b45c214590e2ffb2c2b565ab3b9d07571fde0027b1dc57f8fd25afa842c1056dd459af4074d7510a0c020b914a5e202445d4d3f151070589dd6a2554fc506018c4f001df6239643dc86771286ae4910769d8385531bba57544d63c3640b90c98f1445ebdd129475e02086b600f0beb5b05cc6ca9b3633b452b7dad634e9336f56ec4c3ac0b4fe54ced8',
148+
),
149+
parameters: {
150+
derivedKeyLength: 256,
151+
iterations: 4096,
152+
password: utf8ToBin('Password'),
153+
salt: utf8ToBin('sa\0lt'),
154+
},
155+
});
156+
157+
test('returns error on invalid parameters', (t) => {
158+
t.is(
159+
instantiatePbkdf2Function(
160+
hmacSha256,
161+
0,
162+
)({
163+
derivedKeyLength: 256,
164+
iterations: 4096,
165+
password: utf8ToBin('password'),
166+
salt: utf8ToBin('salt'),
167+
}),
168+
`${Pbkdf2Error.invalidHmacLength} HMAC length: 0.`,
169+
);
170+
t.is(
171+
instantiatePbkdf2Function(
172+
hmacSha256,
173+
32,
174+
)({
175+
derivedKeyLength: 0,
176+
iterations: 4096,
177+
password: utf8ToBin('password'),
178+
salt: utf8ToBin('salt'),
179+
}),
180+
`${Pbkdf2Error.invalidDerivedKeyLength} Derived key length: 0.`,
181+
);
182+
t.is(
183+
instantiatePbkdf2Function(
184+
hmacSha256,
185+
32,
186+
)({
187+
derivedKeyLength: 256,
188+
iterations: 0,
189+
password: utf8ToBin('password'),
190+
salt: utf8ToBin('salt'),
191+
}),
192+
`${Pbkdf2Error.invalidIterations} Iterations parameter: 0.`,
193+
);
194+
});

src/lib/crypto/pbkdf2.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { formatError, numberToBinUint32BE } from '../format/format.js';
2+
3+
import type { HmacFunction } from './hmac.js';
4+
import { hmacSha256, hmacSha512 } from './hmac.js';
5+
6+
export enum Pbkdf2Error {
7+
invalidIterations = 'Invalid PBKDF2 parameters: iterations must be a positive integer.',
8+
invalidDerivedKeyLength = 'Invalid PBKDF2 parameters: derived key length must be a positive integer.',
9+
invalidHmacLength = 'Invalid HMAC length: HMAC length must be a positive integer.',
10+
}
11+
12+
/**
13+
* An object representing the parameters to use with PBKDF2 (Password-Based Key Derivation Function 2).
14+
*/
15+
export type Pbkdf2Parameters = {
16+
/** The length of the derived key in bytes. */
17+
derivedKeyLength: number;
18+
password: Uint8Array;
19+
iterations: number;
20+
salt: Uint8Array;
21+
};
22+
23+
/**
24+
* Instantiate a PBKDF2 function as specified by RFC 2898.
25+
*
26+
* @param hmacFunction - the HMAC function to use
27+
* @param hmacByteLength - the byte-length of the HMAC function
28+
*/
29+
export const instantiatePbkdf2Function =
30+
(hmacFunction: HmacFunction, hmacByteLength: number) =>
31+
// eslint-disable-next-line complexity
32+
(parameters: Pbkdf2Parameters) => {
33+
/* eslint-disable functional/immutable-data, functional/no-let, functional/no-loop-statements, functional/no-expression-statements, no-bitwise, no-plusplus */
34+
const { password, salt, iterations, derivedKeyLength } = parameters;
35+
if (!Number.isInteger(iterations) || iterations <= 0) {
36+
return formatError(
37+
Pbkdf2Error.invalidIterations,
38+
`Iterations parameter: ${iterations}.`,
39+
);
40+
}
41+
if (!Number.isInteger(derivedKeyLength) || derivedKeyLength <= 0) {
42+
return formatError(
43+
Pbkdf2Error.invalidDerivedKeyLength,
44+
`Derived key length: ${derivedKeyLength}.`,
45+
);
46+
}
47+
if (!Number.isInteger(hmacByteLength) || hmacByteLength <= 0) {
48+
return formatError(
49+
Pbkdf2Error.invalidHmacLength,
50+
`HMAC length: ${hmacByteLength}.`,
51+
);
52+
}
53+
const iterationCountByteLength = 4;
54+
const derivedKey = new Uint8Array(derivedKeyLength);
55+
const block = new Uint8Array(salt.length + iterationCountByteLength);
56+
block.set(salt, 0);
57+
let writePosition = 0;
58+
const length = Math.ceil(derivedKeyLength / hmacByteLength);
59+
for (let i = 1; i <= length; i++) {
60+
const iterationUint32BEEncoded = numberToBinUint32BE(i);
61+
block.set(iterationUint32BEEncoded, salt.length);
62+
const accumulatedMac = hmacFunction(password, block);
63+
let intermediateMac = accumulatedMac;
64+
for (let j = 1; j < iterations; j++) {
65+
intermediateMac = hmacFunction(password, intermediateMac);
66+
for (let k = 0; k < hmacByteLength; k++) {
67+
accumulatedMac[k] ^= intermediateMac[k]!; // eslint-disable-line @typescript-eslint/no-non-null-assertion
68+
}
69+
}
70+
const truncatedResult = accumulatedMac.subarray(0, derivedKeyLength);
71+
derivedKey.set(truncatedResult, writePosition);
72+
writePosition += hmacByteLength;
73+
}
74+
return derivedKey;
75+
/* eslint-enable functional/immutable-data, functional/no-let, functional/no-loop-statements, functional/no-expression-statements, no-bitwise, no-plusplus */
76+
};
77+
78+
const hmacSha256ByteLength = 32;
79+
80+
/**
81+
* Derive a key using PBKDF2 and the HMAC SHA256 function as specified in RFC 2898.
82+
*
83+
* @param parameters - the PBKDF2 parameters to use
84+
* @param sha256Hmac - the SHA256 HMAC implementation to use
85+
*/
86+
export const pbkdf2HmacSha256 = (
87+
parameters: Pbkdf2Parameters,
88+
sha256Hmac: HmacFunction = hmacSha256,
89+
) => instantiatePbkdf2Function(sha256Hmac, hmacSha256ByteLength)(parameters);
90+
91+
const hmacSha512ByteLength = 64;
92+
93+
/**
94+
* Derive a key using PBKDF2 and the HMAC SHA512 function as specified in RFC 2898.
95+
*
96+
* @param parameters - the PBKDF2 parameters to use
97+
* @param sha512Hmac - the SHA512 HMAC implementation to use
98+
*/
99+
export const pbkdf2HmacSha512 = (
100+
parameters: Pbkdf2Parameters,
101+
sha512Hmac: HmacFunction = hmacSha512,
102+
) => instantiatePbkdf2Function(sha512Hmac, hmacSha512ByteLength)(parameters);

0 commit comments

Comments
 (0)