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
65 changes: 65 additions & 0 deletions src/hashing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import * as nobleHashes from '@noble/hashes/sha256';
import { webcrypto } from 'crypto';
import { parse } from 'semver';

import { bytesToHex, stringToBytes } from './bytes';
import { sha256 } from './hashing';

describe('sha256', () => {
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',
);
});

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(nobleHashes, '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(nobleHashes, '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();
});
});
25 changes: 25 additions & 0 deletions src/hashing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { sha256 as nobleSha256 } from '@noble/hashes/sha256';

/**
* Compute a SHA-256 digest for a given byte array.
*
* Uses the native crypto implementation and falls back to noble.
*
* @param bytes - A byte array.
* @returns The SHA-256 hash as a byte array.
*/
export async function sha256(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-256', bytes),
);
}
return nobleSha256(bytes);
}
1 change: 1 addition & 0 deletions src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ describe('index', () => {
"parseCaipChainId",
"remove0x",
"satisfiesVersionRange",
"sha256",
"signedBigIntToBytes",
"stringToBytes",
"timeSince",
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './coercers';
export * from './collections';
export * from './encryption-types';
export * from './errors';
export * from './hashing';
export type { Hex } from './hex';
export {
HexStruct,
Expand Down
1 change: 1 addition & 0 deletions src/node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ describe('node', () => {
"readJsonFile",
"remove0x",
"satisfiesVersionRange",
"sha256",
"signedBigIntToBytes",
"stringToBytes",
"timeSince",
Expand Down
Loading