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,