Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand Down
197 changes: 156 additions & 41 deletions src/hashing.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend reviewing without white-space diffs, since I changed the nesting here

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',
);
Comment thread
cursor[bot] marked this conversation as resolved.
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();
});
});
});
52 changes: 52 additions & 0 deletions src/hashing.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -23,3 +27,51 @@ export async function sha256(bytes: Uint8Array): Promise<Uint8Array> {
}
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<Uint8Array> {
// 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<Uint8Array> {
// 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);
}
2 changes: 2 additions & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,8 @@ describe('index', () => {
"remove0x",
"satisfiesVersionRange",
"sha256",
"sha384",
"sha512",
"signedBigIntToBytes",
"stringToBytes",
"timeSince",
Expand Down
2 changes: 2 additions & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ describe('node', () => {
"remove0x",
"satisfiesVersionRange",
"sha256",
"sha384",
"sha512",
"signedBigIntToBytes",
"stringToBytes",
"timeSince",
Expand Down
Loading