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] diff --git a/src/hashing.test.ts b/src/hashing.test.ts index 4e2f9b86..a51815b7 100644 --- a/src/hashing.test.ts +++ b/src/hashing.test.ts @@ -1,65 +1,180 @@ -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; + const originalDigest = originalSubtle?.digest?.bind(originalSubtle); - // 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, + 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, + }); + } + }); + + afterEach(() => { + Object.defineProperty(globalThis.crypto, 'subtle', { + value: originalSubtle, + writable: true, + }); + Object.defineProperty(globalThis.crypto.subtle, 'digest', { + value: originalDigest, writable: true, }); - } - - 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', - ); + 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, + }); + + const digest = await sha512(stringToBytes('foo bar')); + expect(bytesToHex(digest)).toBe( + '0x65019286222ace418f742556366f9b9da5aaf6797527d2f0cba5bfe6b2f8ed24746542a0f2be1da8d63c2477f688b608eb53628993afa624f378b03f10090ce7', + ); + + expect(nobleSpy).toHaveBeenCalled(); + }); - 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', + ); + }); - Object.defineProperty(globalThis.crypto, 'subtle', { - value: undefined, - writable: true, + 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, + }); + + 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); +} 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",