From b224fd6234bf935db98cf123e6bb40a7ce013620 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 12:45:22 +0200 Subject: [PATCH 1/6] feat: Add sha512 and sha384 --- src/hashing.test.ts | 195 ++++++++++++++++++++++++++++++++++---------- src/hashing.ts | 52 ++++++++++++ 2 files changed, 205 insertions(+), 42 deletions(-) diff --git a/src/hashing.test.ts b/src/hashing.test.ts index 4e2f9b86..6ab5fa77 100644 --- a/src/hashing.test.ts +++ b/src/hashing.test.ts @@ -1,65 +1,176 @@ -import * as nobleHashes from '@noble/hashes/sha256'; +import * as nobleHashes256 from '@noble/hashes/sha256'; +import * as nobleHashes512 from '@noble/hashes/sha512'; import { webcrypto } from 'crypto'; import { parse } from 'semver'; import { bytesToHex, stringToBytes } from './bytes'; -import { sha256 } from './hashing'; +import { sha256, sha512, sha384 } from './hashing'; -describe('sha256', () => { - const isNode18 = parse(process.version)?.major === 18; +describe('hash functions', () => { + const originalSubtle = globalThis.crypto.subtle ?? webcrypto.subtle; - // The global does not exist in Node 18, so we must add it. - // eslint-disable-next-line jest/no-if - if (isNode18) { - Object.defineProperty(globalThis, 'crypto', { - value: webcrypto, - writable: true, - }); - } + beforeEach(() => { + const isNode18 = parse(process.version)?.major === 18; + + // The global does not exist in Node 18, so we must add it. + // eslint-disable-next-line jest/no-if + if (isNode18) { + Object.defineProperty(globalThis, 'crypto', { + value: webcrypto, + writable: true, + }); + } - it('returns a digest for a byte array', async () => { - const digest = await sha256(stringToBytes('foo bar')); - expect(bytesToHex(digest)).toBe( - '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', - ); + // Restore subtle if previous tests broke it + if (!globalThis.crypto.subtle) { + Object.defineProperty(globalThis.crypto, 'subtle', { + value: originalSubtle, + writable: true, + }); + } }); - it('returns a digest for a larger byte array', async () => { - const digest = await sha256(new Uint8Array(1024).fill(1)); - expect(bytesToHex(digest)).toBe( - '0x5a648d8015900d89664e00e125df179636301a2d8fa191c1aa2bd9358ea53a69', - ); + describe('sha256', () => { + it('returns a digest for a byte array', async () => { + const digest = await sha256(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', + ); + }); + + it('returns a digest for a larger byte array', async () => { + const digest = await sha256(new Uint8Array(1024).fill(1)); + expect(bytesToHex(digest)).toBe( + '0x5a648d8015900d89664e00e125df179636301a2d8fa191c1aa2bd9358ea53a69', + ); + }); + + it('falls back to noble when digest function is unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes256, 'sha256'); + + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: undefined, + writable: true, + }); + + const digest = await sha256(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); + + it('falls back to noble when subtle APIs are unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes256, 'sha256'); + + Object.defineProperty(globalThis.crypto, 'subtle', { + value: undefined, + writable: true, + }); + + const digest = await sha256(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); }); - it('falls back to noble when digest function is unavailable', async () => { - const nobleSpy = jest.spyOn(nobleHashes, 'sha256'); + describe('sha512', () => { + it('returns a digest for a byte array', async () => { + const digest = await sha512(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x65019286222ace418f742556366f9b9da5aaf6797527d2f0cba5bfe6b2f8ed24746542a0f2be1da8d63c2477f688b608eb53628993afa624f378b03f10090ce7', + ); + }); - Object.defineProperty(globalThis.crypto.subtle, 'digest', { - value: undefined, - writable: true, + it('returns a digest for a larger byte array', async () => { + const digest = await sha512(new Uint8Array(1024).fill(1)); + expect(bytesToHex(digest)).toBe( + '0x19c6841f3d6e33a4d28e7cb47ff938728479c56bb930f3e8535ec24d9453d9665b7dc1163181b94a1ada9554e953a094ed44fd6faee7a9bbde6615375bab4ae8', + ); }); - const digest = await sha256(stringToBytes('foo bar')); - expect(bytesToHex(digest)).toBe( - '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', - ); + it('falls back to noble when digest function is unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes512, 'sha512'); + + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: undefined, + writable: true, + }); - expect(nobleSpy).toHaveBeenCalled(); + const digest = await sha512(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x65019286222ace418f742556366f9b9da5aaf6797527d2f0cba5bfe6b2f8ed24746542a0f2be1da8d63c2477f688b608eb53628993afa624f378b03f10090ce7', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); + + it('falls back to noble when subtle APIs are unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes512, 'sha512'); + + Object.defineProperty(globalThis.crypto, 'subtle', { + value: undefined, + writable: true, + }); + + const digest = await sha512(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x65019286222ace418f742556366f9b9da5aaf6797527d2f0cba5bfe6b2f8ed24746542a0f2be1da8d63c2477f688b608eb53628993afa624f378b03f10090ce7', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); }); - it('falls back to noble when subtle APIs are unavailable', async () => { - const nobleSpy = jest.spyOn(nobleHashes, 'sha256'); + describe('sha384', () => { + it('returns a digest for a byte array', async () => { + const digest = await sha384(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x6839312f3db343477070d3c0b2becd417b357154d48794d01d78cfb4617ed5ab819a77b6832f6542dd18bb738131ef7e', + ); + }); + + it('returns a digest for a larger byte array', async () => { + const digest = await sha384(new Uint8Array(1024).fill(1)); + expect(bytesToHex(digest)).toBe( + '0x45730a19acff8481e7e2b99c4100a09a0288a3bc45df56ff7e72dd92ef9e4c92f925c9d6ba1ea96c934a5f1e782a7cc7', + ); + }); + + it('falls back to noble when digest function is unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes512, 'sha384'); + + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: undefined, + writable: true, + }); - Object.defineProperty(globalThis.crypto, 'subtle', { - value: undefined, - writable: true, + const digest = await sha384(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x6839312f3db343477070d3c0b2becd417b357154d48794d01d78cfb4617ed5ab819a77b6832f6542dd18bb738131ef7e', + ); + + expect(nobleSpy).toHaveBeenCalled(); }); - const digest = await sha256(stringToBytes('foo bar')); - expect(bytesToHex(digest)).toBe( - '0xfbc1a9f858ea9e177916964bd88c3d37b91a1e84412765e29950777f265c4b75', - ); + it('falls back to noble when subtle APIs are unavailable', async () => { + const nobleSpy = jest.spyOn(nobleHashes512, 'sha384'); + + Object.defineProperty(globalThis.crypto, 'subtle', { + value: undefined, + writable: true, + }); - expect(nobleSpy).toHaveBeenCalled(); + const digest = await sha384(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x6839312f3db343477070d3c0b2becd417b357154d48794d01d78cfb4617ed5ab819a77b6832f6542dd18bb738131ef7e', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); }); }); diff --git a/src/hashing.ts b/src/hashing.ts index 6a8619ec..c29e1d8d 100644 --- a/src/hashing.ts +++ b/src/hashing.ts @@ -1,4 +1,8 @@ import { sha256 as nobleSha256 } from '@noble/hashes/sha256'; +import { + sha512 as nobleSha512, + sha384 as nobleSha384, +} from '@noble/hashes/sha512'; /** * Compute a SHA-256 digest for a given byte array. @@ -23,3 +27,51 @@ export async function sha256(bytes: Uint8Array): Promise { } return nobleSha256(bytes); } + +/** + * Compute a SHA-512 digest for a given byte array. + * + * Uses the native crypto implementation and falls back to noble. + * + * @param bytes - A byte array. + * @returns The SHA-512 hash as a byte array. + */ +export async function sha512(bytes: Uint8Array): Promise { + // Use crypto.subtle.digest whenever possible as it is faster. + if ( + 'crypto' in globalThis && + typeof globalThis.crypto === 'object' && + // eslint-disable-next-line no-restricted-globals + globalThis.crypto.subtle?.digest + ) { + // eslint-disable-next-line no-restricted-globals + return new Uint8Array( + await globalThis.crypto.subtle.digest('SHA-512', bytes), + ); + } + return nobleSha512(bytes); +} + +/** + * Compute a SHA-384 digest for a given byte array. + * + * Uses the native crypto implementation and falls back to noble. + * + * @param bytes - A byte array. + * @returns The SHA-384 hash as a byte array. + */ +export async function sha384(bytes: Uint8Array): Promise { + // Use crypto.subtle.digest whenever possible as it is faster. + if ( + 'crypto' in globalThis && + typeof globalThis.crypto === 'object' && + // eslint-disable-next-line no-restricted-globals + globalThis.crypto.subtle?.digest + ) { + // eslint-disable-next-line no-restricted-globals + return new Uint8Array( + await globalThis.crypto.subtle.digest('SHA-384', bytes), + ); + } + return nobleSha384(bytes); +} From 3fbef8767fe23eb6717fd72aa5e566de85b872ee Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 12:47:27 +0200 Subject: [PATCH 2/6] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b5bbb11..d75609e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `uint8ArrayToMnemonic` converts little-endian `Uint16` English BIP-39 wordlist indices into a mnemonic string. - `convertMnemonicToWordlistIndices` converts a mnemonic string into little-endian `Uint16` wordlist indices encoded as a `Uint8Array`. - Add `@metamask/scure-bip39` as a dependency ([#300](https://github.com/MetaMask/utils/pull/300)) +- Add optimized `sha512` and `sha384` utility functions ([#305](https://github.com/MetaMask/utils/pull/305)) ## [11.11.0] From 0787f0c6bf45c3233e3d39c902d4b161e4fd4d43 Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 12:48:48 +0200 Subject: [PATCH 3/6] Fix export snapshots --- src/index.test.ts | 2 ++ src/node.test.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/index.test.ts b/src/index.test.ts index f4e41bf3..418d50fc 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -155,6 +155,8 @@ describe('index', () => { "remove0x", "satisfiesVersionRange", "sha256", + "sha384", + "sha512", "signedBigIntToBytes", "stringToBytes", "timeSince", diff --git a/src/node.test.ts b/src/node.test.ts index 8926d905..8f9718bc 100644 --- a/src/node.test.ts +++ b/src/node.test.ts @@ -162,6 +162,8 @@ describe('node', () => { "remove0x", "satisfiesVersionRange", "sha256", + "sha384", + "sha512", "signedBigIntToBytes", "stringToBytes", "timeSince", From c562295277a32c5130f70e783ecfca76fedb50cb Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 12:55:32 +0200 Subject: [PATCH 4/6] Fix test cleanup --- src/hashing.test.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/hashing.test.ts b/src/hashing.test.ts index 6ab5fa77..8a26aeef 100644 --- a/src/hashing.test.ts +++ b/src/hashing.test.ts @@ -7,7 +7,8 @@ import { bytesToHex, stringToBytes } from './bytes'; import { sha256, sha512, sha384 } from './hashing'; describe('hash functions', () => { - const originalSubtle = globalThis.crypto.subtle ?? webcrypto.subtle; + const originalSubtle = globalThis.crypto?.subtle ?? webcrypto.subtle; + const originalDigest = originalSubtle?.digest?.bind(originalSubtle); beforeEach(() => { const isNode18 = parse(process.version)?.major === 18; @@ -21,12 +22,16 @@ describe('hash functions', () => { }); } - // Restore subtle if previous tests broke it - if (!globalThis.crypto.subtle) { + // Restore digest if previous tests broke it + if (!globalThis.crypto.subtle?.digest) { Object.defineProperty(globalThis.crypto, 'subtle', { value: originalSubtle, writable: true, }); + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: originalDigest, + writable: true, + }); } }); From 0e65dac230ca23f988b5f108ba20ef065c28a7ef Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 13:34:22 +0200 Subject: [PATCH 5/6] Slightly simplify test cleanup --- src/hashing.test.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/hashing.test.ts b/src/hashing.test.ts index 8a26aeef..273edb79 100644 --- a/src/hashing.test.ts +++ b/src/hashing.test.ts @@ -21,18 +21,13 @@ describe('hash functions', () => { writable: true, }); } + }); - // Restore digest if previous tests broke it - if (!globalThis.crypto.subtle?.digest) { - Object.defineProperty(globalThis.crypto, 'subtle', { - value: originalSubtle, - writable: true, - }); - Object.defineProperty(globalThis.crypto.subtle, 'digest', { - value: originalDigest, - writable: true, - }); - } + afterEach(() => { + Object.defineProperty(globalThis.crypto, 'subtle', { + value: { ...originalSubtle, digest: originalDigest }, + writable: true, + }); }); describe('sha256', () => { From c92144d38a9dc5d08dd2666fc3ae5545a792732b Mon Sep 17 00:00:00 2001 From: Frederik Bolding Date: Thu, 27 Aug 2026 13:47:11 +0200 Subject: [PATCH 6/6] Partial revert --- src/hashing.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/hashing.test.ts b/src/hashing.test.ts index 273edb79..a51815b7 100644 --- a/src/hashing.test.ts +++ b/src/hashing.test.ts @@ -25,7 +25,11 @@ describe('hash functions', () => { afterEach(() => { Object.defineProperty(globalThis.crypto, 'subtle', { - value: { ...originalSubtle, digest: originalDigest }, + value: originalSubtle, + writable: true, + }); + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: originalDigest, writable: true, }); });