From c2a0622475755fe44dccfe39dbe5d0b1650b0879 Mon Sep 17 00:00:00 2001 From: Simon Warta Date: Sun, 27 Feb 2022 13:49:12 +0100 Subject: [PATCH] Pull out pbkdf2Sha512 --- packages/crypto/src/bip39.ts | 54 +------------ packages/crypto/src/pbkdf2.spec.ts | 122 +++++++++++++++++++++++++++++ packages/crypto/src/pbkdf2.ts | 52 ++++++++++++ 3 files changed, 176 insertions(+), 52 deletions(-) create mode 100644 packages/crypto/src/pbkdf2.spec.ts create mode 100644 packages/crypto/src/pbkdf2.ts diff --git a/packages/crypto/src/bip39.ts b/packages/crypto/src/bip39.ts index b8ac4536..73c194b7 100644 --- a/packages/crypto/src/bip39.ts +++ b/packages/crypto/src/bip39.ts @@ -1,5 +1,6 @@ import { toUtf8 } from "@cosmjs/encoding"; +import { pbkdf2Sha512 } from "./pbkdf2"; import { sha256 } from "./sha"; const wordlist = [ @@ -2177,18 +2178,6 @@ export class EnglishMnemonic { } } -async function getSubtle(): Promise { - const g: any = globalThis; - let subtle = g.crypto && g.crypto.subtle; - if (!subtle) { - const crypto: any = await import("crypto"); - if (crypto.webcrypto && crypto.webcrypto.subtle) { - subtle = crypto.webcrypto.subtle; - } - } - return subtle; -} - export class Bip39 { /** * Encodes raw entropy of length 16, 20, 24, 28 or 32 bytes as an English mnemonic between 12 and 24 words. @@ -2217,45 +2206,6 @@ export class Bip39 { const mnemonicBytes = toUtf8(normalize(mnemonic.toString())); const salt = "mnemonic" + (password ? normalize(password) : ""); const saltBytes = toUtf8(salt); - return this.pbkdf2Sha512(mnemonicBytes, saltBytes, 2048, 64); - } - - // convert pbkdf2's callback interface to Promise interface - private static async pbkdf2Sha512( - secret: Uint8Array, - salt: Uint8Array, - iterations: number, - keylen: number, - ): Promise { - const subtle = await getSubtle(); - if (subtle) { - return subtle - .importKey("raw", secret, { name: "PBKDF2" }, false, ["deriveBits"]) - .then((key: Uint8Array) => - subtle - .deriveBits( - { - name: "PBKDF2", - salt: salt, - iterations: iterations, - hash: { name: "SHA-512" }, - }, - key, - keylen * 8, - ) - .then((buffer: ArrayBuffer) => new Uint8Array(buffer)), - ); - } else { - const module = await import("crypto"); - return new Promise((resolve, reject) => { - module.pbkdf2(secret, salt, iterations, keylen, "sha512", (error, result) => { - if (error) { - reject(error); - } else { - resolve(Uint8Array.from(result)); - } - }); - }); - } + return pbkdf2Sha512(mnemonicBytes, saltBytes, 2048, 64); } } diff --git a/packages/crypto/src/pbkdf2.spec.ts b/packages/crypto/src/pbkdf2.spec.ts new file mode 100644 index 00000000..b67ccdcf --- /dev/null +++ b/packages/crypto/src/pbkdf2.spec.ts @@ -0,0 +1,122 @@ +import { fromHex, toAscii, toUtf8 } from "@cosmjs/encoding"; + +import { pbkdf2Sha512 } from "./pbkdf2"; + +interface TestVector { + secret: Uint8Array; + salt: Uint8Array; + iterations: number; + keylen: number; + expected: Uint8Array; +} + +describe("pbkdf2", () => { + // https://github.com/randombit/botan/blob/master/src/tests/data/pbkdf/pbkdf2.vec#L70-L74 + const botanTest: TestVector = { + secret: toAscii("xyz"), + salt: fromHex("0001020304050607"), + iterations: 10000, + keylen: 48, + expected: fromHex( + "DAF8A734327745EB63D19054DBD4018A682CEF11086A1BFB63FDBC16158C2F8B0742802F36AEF1B1DF92ACCBEA5D31A5", + ), + }; + + // https://github.com/brycx/Test-Vector-Generation/blob/f88d152db/PBKDF2/pbkdf2-hmac-sha2-test-vectors.md + const brycxTests: TestVector[] = [ + // Test Case 1 + { + secret: toUtf8("password"), + salt: toUtf8("salt"), + iterations: 1, + keylen: 20, + expected: fromHex("867f70cf1ade02cff3752599a3a53dc4af34c7a6"), + }, + // Test Case 2 + { + secret: toUtf8("password"), + salt: toUtf8("salt"), + iterations: 2, + keylen: 20, + expected: fromHex("e1d9c16aa681708a45f5c7c4e215ceb66e011a2e"), + }, + // Test Case 3 + { + secret: toUtf8("password"), + salt: toUtf8("salt"), + iterations: 4096, + keylen: 20, + expected: fromHex("d197b1b33db0143e018b12f3d1d1479e6cdebdcc"), + }, + // Test Case 4 + { + secret: toUtf8("password"), + salt: toUtf8("salt"), + iterations: 16777216, + keylen: 20, + expected: fromHex("6180a3ceabab45cc3964112c811e0131bca93a35"), + }, + // Test Case 5 + { + secret: toUtf8("passwordPASSWORDpassword"), + salt: toUtf8("saltSALTsaltSALTsaltSALTsaltSALTsalt"), + iterations: 4096, + keylen: 25, + expected: fromHex("8c0511f4c6e597c6ac6315d8f0362e225f3c501495ba23b868"), + }, + // Test Case 6 + { + secret: toUtf8("pass\0word"), + salt: toUtf8("sa\0lt"), + iterations: 4096, + keylen: 16, + expected: fromHex("9d9e9c4cd21fe4be24d5b8244c759665"), + }, + // Test Case 7 + { + secret: toUtf8("passwd"), + salt: toUtf8("salt"), + iterations: 1, + keylen: 128, + expected: fromHex( + "c74319d99499fc3e9013acff597c23c5baf0a0bec5634c46b8352b793e324723d55caa76b2b25c43402dcfdc06cdcf66f95b7d0429420b39520006749c51a04ef3eb99e576617395a178ba33214793e48045132928a9e9bf2661769fdc668f31798597aaf6da70dd996a81019726084d70f152baed8aafe2227c07636c6ddece", + ), + }, + // Test Case 8 + { + secret: toUtf8("Password"), + salt: toUtf8("NaCl"), + iterations: 80000, + keylen: 128, + expected: fromHex( + "e6337d6fbeb645c794d4a9b5b75b7b30dac9ac50376a91df1f4460f6060d5addb2c1fd1f84409abacc67de7eb4056e6bb06c2d82c3ef4ccd1bded0f675ed97c65c33d39f81248454327aa6d03fd049fc5cbb2b5e6dac08e8ace996cdc960b1bd4530b7e754773d75f67a733fdb99baf6470e42ffcb753c15c352d4800fb6f9d6", + ), + }, + // Test Case 9 + { + secret: toUtf8("Password"), + salt: toUtf8("sa\0lt"), + iterations: 4096, + keylen: 256, + expected: fromHex( + "10176fb32cb98cd7bb31e2bb5c8f6e425c103333a2e496058e3fd2bd88f657485c89ef92daa0668316bc23ebd1ef88f6dd14157b2320b5d54b5f26377c5dc279b1dcdec044bd6f91b166917c80e1e99ef861b1d2c7bce1b961178125fb86867f6db489a2eae0022e7bc9cf421f044319fac765d70cb89b45c214590e2ffb2c2b565ab3b9d07571fde0027b1dc57f8fd25afa842c1056dd459af4074d7510a0c020b914a5e202445d4d3f151070589dd6a2554fc506018c4f001df6239643dc86771286ae4910769d8385531bba57544d63c3640b90c98f1445ebdd129475e02086b600f0beb5b05cc6ca9b3633b452b7dad634e9336f56ec4c3ac0b4fe54ced8", + ), + }, + ]; + + describe("pbkdf2Sha512", () => { + it("works", async () => { + { + const { secret, salt, iterations, keylen, expected } = botanTest; + const hash = await pbkdf2Sha512(secret, salt, iterations, keylen); + expect(hash).toEqual(expected); + } + + for (const [index, test] of brycxTests.entries()) { + const { secret, salt, iterations, keylen, expected } = test; + const hash = await pbkdf2Sha512(secret, salt, iterations, keylen); + expect(hash).withContext(`Index ${index}`).toEqual(expected); + } + }); + }); +}); diff --git a/packages/crypto/src/pbkdf2.ts b/packages/crypto/src/pbkdf2.ts new file mode 100644 index 00000000..5faf56e4 --- /dev/null +++ b/packages/crypto/src/pbkdf2.ts @@ -0,0 +1,52 @@ +async function getSubtle(): Promise { + const g: any = globalThis; + let subtle = g.crypto && g.crypto.subtle; + if (!subtle) { + const crypto: any = await import("crypto"); + if (crypto.webcrypto && crypto.webcrypto.subtle) { + subtle = crypto.webcrypto.subtle; + } + } + return subtle; +} + +/** + * A pbkdf2 implementation for BIP39. This is not exported at package level and thus a private API. + */ +export async function pbkdf2Sha512( + secret: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen: number, +): Promise { + const subtle = await getSubtle(); + if (subtle) { + return subtle + .importKey("raw", secret, { name: "PBKDF2" }, false, ["deriveBits"]) + .then((key: Uint8Array) => + subtle + .deriveBits( + { + name: "PBKDF2", + salt: salt, + iterations: iterations, + hash: { name: "SHA-512" }, + }, + key, + keylen * 8, + ) + .then((buffer: ArrayBuffer) => new Uint8Array(buffer)), + ); + } else { + const module = await import("crypto"); + return new Promise((resolve, reject) => { + module.pbkdf2(secret, salt, iterations, keylen, "sha512", (error, result) => { + if (error) { + reject(error); + } else { + resolve(Uint8Array.from(result)); + } + }); + }); + } +}