diff --git a/CHANGELOG.md b/CHANGELOG.md index 465110ae..e037bb59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,10 +12,6 @@ and this project adheres to - all: Add full support for Node.js 18 and run all CI tests with it ([#1240]). - @cosmjs/tendermint-rpc: Remove unused `index` field from `RpcTxEvent` and `TxEvent`. This is unset starting with Tendermint 0.34. -- @cosmjs/crypto: The pbkdf2 implementation for old Node.js versions - `pbkdf2Sha512Crypto` was removed. Node.js has sufficient support for WebCrypto - these days and we still have a pure-JS fallback implementation. This avoids - unnecessary problems around importing Node.js modules. ([#1341]) - @cosmjs/proto-signing: Make input and output of `decodePubkey` non-optional ([#1289]). - @cosmjs/stargate: Remove unnecessary address prefix argument from @@ -31,7 +27,6 @@ and this project adheres to [#1289]: https://github.com/cosmos/cosmjs/issues/1289 [#1291]: https://github.com/cosmos/cosmjs/issues/1291 [#1329]: https://github.com/cosmos/cosmjs/pull/1329 -[#1341]: https://github.com/cosmos/cosmjs/issues/1341 ## [0.29.5] - 2022-12-07 diff --git a/packages/crypto/src/pbkdf2.spec.ts b/packages/crypto/src/pbkdf2.spec.ts index e22e3199..a4d969b8 100644 --- a/packages/crypto/src/pbkdf2.spec.ts +++ b/packages/crypto/src/pbkdf2.spec.ts @@ -1,6 +1,13 @@ import { fromHex, toAscii, toUtf8 } from "@cosmjs/encoding"; -import { getSubtle, pbkdf2Sha512, pbkdf2Sha512Noble, pbkdf2Sha512Subtle } from "./pbkdf2"; +import { + getNodeCrypto, + getSubtle, + pbkdf2Sha512, + pbkdf2Sha512Noble, + pbkdf2Sha512NodeCrypto, + pbkdf2Sha512Subtle, +} from "./pbkdf2"; interface TestVector { secret: Uint8Array; @@ -139,6 +146,25 @@ describe("pbkdf2", () => { }); }); + describe("pbkdf2Sha512NodeCrypto", () => { + it("works", async () => { + const nodeCrypto = await getNodeCrypto(); + if (!nodeCrypto) pending("The crypto module is not available in this environment"); + + { + const { secret, salt, iterations, keylen, expected } = botanTest; + const hash = await pbkdf2Sha512NodeCrypto(nodeCrypto, 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 pbkdf2Sha512NodeCrypto(nodeCrypto, secret, salt, iterations, keylen); + expect(hash).withContext(`brycx tests index ${index}`).toEqual(expected); + } + }); + }); + describe("pbkdf2Sha512Noble", () => { it("works", async () => { { diff --git a/packages/crypto/src/pbkdf2.ts b/packages/crypto/src/pbkdf2.ts index fb60f836..5176346d 100644 --- a/packages/crypto/src/pbkdf2.ts +++ b/packages/crypto/src/pbkdf2.ts @@ -2,6 +2,27 @@ import { assert } from "@cosmjs/utils"; import { pbkdf2Async as noblePbkdf2Async } from "@noble/hashes/pbkdf2"; import { sha512 as nobleSha512 } from "@noble/hashes/sha512"; +/** + * Returns the Node.js crypto module when available and `undefined` + * otherwise. + * + * Detects an unimplemented fallback module from Webpack 5 and returns + * `undefined` in that case. + */ +export async function getNodeCrypto(): Promise { + try { + const nodeCrypto = await import("crypto"); + // We get `Object{default: Object{}}` as a fallback when using + // `crypto: false` in Webpack 5, which we interprete as unavailable. + if (typeof nodeCrypto === "object" && Object.keys(nodeCrypto).length <= 1) { + return undefined; + } + return nodeCrypto; + } catch { + return undefined; + } +} + export async function getSubtle(): Promise { // From Node.js 15 onwards, webcrypto is available in globalThis. // In version 15 and 16 this was stored under the webcrypto key. @@ -47,6 +68,33 @@ export async function pbkdf2Sha512Subtle( ); } +/** + * Implements pbkdf2-sha512 using the Node.js crypro module (`import "crypto"`). + * This does not use subtle from [Crypto](https://developer.mozilla.org/en-US/docs/Web/API/Crypto). + */ +export async function pbkdf2Sha512NodeCrypto( + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + nodeCrypto: any, + secret: Uint8Array, + salt: Uint8Array, + iterations: number, + keylen: number, +): Promise { + assert(nodeCrypto, "Argument nodeCrypto is falsy"); + assert(typeof nodeCrypto === "object", "Argument nodeCrypto is not of type object"); + assert(typeof nodeCrypto.pbkdf2 === "function", "nodeCrypto.pbkdf2 is not a function"); + + return new Promise((resolve, reject) => { + nodeCrypto.pbkdf2(secret, salt, iterations, keylen, "sha512", (error: any, result: any) => { + if (error) { + reject(error); + } else { + resolve(Uint8Array.from(result)); + } + }); + }); +} + export async function pbkdf2Sha512Noble( secret: Uint8Array, salt: Uint8Array, @@ -69,6 +117,11 @@ export async function pbkdf2Sha512( if (subtle) { return pbkdf2Sha512Subtle(subtle, secret, salt, iterations, keylen); } else { - return pbkdf2Sha512Noble(secret, salt, iterations, keylen); + const nodeCrypto = await getNodeCrypto(); + if (nodeCrypto) { + return pbkdf2Sha512NodeCrypto(nodeCrypto, secret, salt, iterations, keylen); + } else { + return pbkdf2Sha512Noble(secret, salt, iterations, keylen); + } } }