From f6e4252651444adbaccbd6624a4e471f95d7a2c5 Mon Sep 17 00:00:00 2001 From: Kyle Cox Date: Fri, 9 Oct 2026 05:42:09 +0000 Subject: [PATCH] fix(crypto): don't mutate the caller's options in blake2b() blake2b() converted the key, salt, and personalization fields of the caller's options object from hex strings to Uint8Array in place, so the object came back changed (and a frozen options object threw a TypeError in strict mode). The same conversion already happened again when building the noble options object, so the in-place writes were pure side effect. Convert only into the fresh options object; hash outputs are unchanged (full suite green, existing vectors untouched). --- .changeset/tidy-hashes-keep.md | 5 ++++ packages/crypto/src/hashes.spec.ts | 43 ++++++++++++++++++++++++++++++ packages/crypto/src/hashes.ts | 6 ++--- 3 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 .changeset/tidy-hashes-keep.md diff --git a/.changeset/tidy-hashes-keep.md b/.changeset/tidy-hashes-keep.md new file mode 100644 index 00000000..45e550c7 --- /dev/null +++ b/.changeset/tidy-hashes-keep.md @@ -0,0 +1,5 @@ +--- +"@fleet-sdk/crypto": patch +--- + +Fix `blake2b()` mutating the caller's options object: the `key`, `salt`, and `personalization` fields were converted from hex strings to `Uint8Array` in place, so a caller's options object came back changed (and a frozen options object threw a `TypeError`). The conversion now happens in a fresh options object, leaving the caller's object untouched; hash outputs are unchanged. diff --git a/packages/crypto/src/hashes.spec.ts b/packages/crypto/src/hashes.spec.ts index 6e482410..4b34301f 100644 --- a/packages/crypto/src/hashes.spec.ts +++ b/packages/crypto/src/hashes.spec.ts @@ -76,6 +76,49 @@ describe("Hashes smoke tests", () => { expect(sha256(byte)).to.be.deep.equal(sha256(hex)); }); + it("Should not mutate the caller's options object", () => { + const msg = utf8.decode("blake2b256"); + const options = { + dkLen: 64, + key: "6a9059057f259d733766f6b32081b66c", + salt: "d877f8df03fd687ab7c0052e3ce88372", + personalization: "77616c6c65747320636865636b73756d" // "wallets checksum" + }; + const snapshot = { ...options }; + + const result = blake2b(msg, options); + + // the options object must be left exactly as the caller passed it: + // hex-string fields must not be replaced with Uint8Arrays + expect(options).to.be.deep.equal(snapshot); + expect(options.key).to.be.equal(snapshot.key); + expect(options.salt).to.be.equal(snapshot.salt); + expect(options.personalization).to.be.equal(snapshot.personalization); + + // and the hash must match hashing with pre-decoded byte options + expect(result).to.be.deep.equal( + blake2b(msg, { + dkLen: 64, + key: hex.decode(snapshot.key), + salt: hex.decode(snapshot.salt), + personalization: hex.decode(snapshot.personalization) + }) + ); + }); + + it("Should accept a frozen options object", () => { + const msg = utf8.decode("blake2b256"); + const options = Object.freeze({ + key: "6a9059057f259d733766f6b32081b66c", + salt: "d877f8df03fd687ab7c0052e3ce88372" + }); + + expect(() => blake2b(msg, options)).not.to.throw(); + expect(blake2b(msg, options)).to.be.deep.equal( + blake2b(msg, { key: hex.decode(options.key), salt: hex.decode(options.salt) }) + ); + }); + it("Should hash message using sha256", () => { expect(sha256(utf8.decode("sha256"))).to.be.deep.equal( hex.decode("5d5b09f6dcb2d53a5fffc60c4ac0d55fabdf556069d6631545f42aa6e3500f2e") diff --git a/packages/crypto/src/hashes.ts b/packages/crypto/src/hashes.ts index 29e521b6..4cc6a63a 100644 --- a/packages/crypto/src/hashes.ts +++ b/packages/crypto/src/hashes.ts @@ -18,10 +18,8 @@ export function ensureBytes(input: ByteInput): Uint8Array { } export function blake2b(message: ByteInput, options?: Blake2bOptions): Uint8Array { - if (options?.key) options.key = ensureBytes(options.key); - if (options?.salt) options.salt = ensureBytes(options.salt); - if (options?.personalization) options.personalization = ensureBytes(options.personalization); - + // convert hex-string fields into a fresh options object; never mutate the + // caller's options object (it may be frozen or reused across calls) const opts: Blake2Opts = { key: options?.key ? ensureBytes(options?.key) : undefined, salt: options?.salt ? ensureBytes(options?.salt) : undefined,