Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cryptographic functions with option to specify custom functions #197

Merged
merged 11 commits into from
Nov 25, 2024
130 changes: 130 additions & 0 deletions src/cryptography.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { bytesToHex } from '@metamask/utils';

import {
hmacSha512,
keccak256,
pbkdf2Sha512,
ripemd160,
sha256,
} from './cryptography';
import * as utils from './utils';

describe('hmacSha512', () => {
it('returns the HMAC-SHA-512 when using a custom implementation', async () => {
const key = new Uint8Array(32);
const data = new Uint8Array(32);

const hash = new Uint8Array(64).fill(1);
const customHmacSha512 = jest.fn().mockResolvedValue(hash);

const result = await hmacSha512(key, data, {
hmacSha512: customHmacSha512,
});

expect(result).toBe(hash);
expect(customHmacSha512).toHaveBeenCalledWith(key, data);
});

it('returns the HMAC-SHA-512 when using the Web Crypto API', async () => {
const key = new Uint8Array(32);
const data = new Uint8Array(32);

const result = await hmacSha512(key, data);
expect(bytesToHex(result)).toBe(
'0xbae46cebebbb90409abc5acf7ac21fdb339c01ce15192c52fb9e8aa11a8de9a4ea15a045f2be245fbb98916a9ae81b353e33b9c42a55380c5158241daeb3c6dd',
);
});

it('returns the HMAC-SHA-512 when using the fallback', async () => {
jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValueOnce(false);

const key = new Uint8Array(32);
const data = new Uint8Array(32);

const result = await hmacSha512(key, data);
expect(bytesToHex(result)).toBe(
'0xbae46cebebbb90409abc5acf7ac21fdb339c01ce15192c52fb9e8aa11a8de9a4ea15a045f2be245fbb98916a9ae81b353e33b9c42a55380c5158241daeb3c6dd',
);
});
});

describe('keccak256', () => {
it('returns the keccak-256 hash of the data', () => {
const data = new Uint8Array(32).fill(1);
const hash = keccak256(data);

expect(bytesToHex(hash)).toBe(
'0xcebc8882fecbec7fb80d2cf4b312bec018884c2d66667c67a90508214bd8bafc',
);
});
});

describe('pbkdf2Sha512', () => {
it('returns the PBKDF2-SHA-512 when using a custom implementation', async () => {
const password = new Uint8Array(32);
const salt = new Uint8Array(32);
const iterations = 1000;
const keyLength = 64;

const hash = new Uint8Array(64).fill(1);
const customPbkdf2Sha512 = jest.fn().mockResolvedValue(hash);

const result = await pbkdf2Sha512(password, salt, iterations, keyLength, {
pbkdf2Sha512: customPbkdf2Sha512,
});

expect(result).toBe(hash);
expect(customPbkdf2Sha512).toHaveBeenCalledWith(
password,
salt,
iterations,
keyLength,
);
});

it('returns the PBKDF2-SHA-512 when using the Web Crypto API', async () => {
const password = new Uint8Array(32);
const salt = new Uint8Array(32);
const iterations = 1000;
const keyLength = 64;

const result = await pbkdf2Sha512(password, salt, iterations, keyLength);
expect(bytesToHex(result)).toBe(
'0xab3d65e9e6341a924c752a77b8dc6b78f1e6db5d31df7dd0cc534039dd9662a97bcaf0b959fe78248a49859c7952ddb25d66840f052b27ef1ab60b9446c0c9fd',
);
});

it('returns the PBKDF2-SHA-512 when using the fallback', async () => {
jest.spyOn(utils, 'isWebCryptoSupported').mockReturnValueOnce(false);

const password = new Uint8Array(32);
const salt = new Uint8Array(32);
const iterations = 1000;
const keyLength = 64;

const result = await pbkdf2Sha512(password, salt, iterations, keyLength);
expect(bytesToHex(result)).toBe(
'0xab3d65e9e6341a924c752a77b8dc6b78f1e6db5d31df7dd0cc534039dd9662a97bcaf0b959fe78248a49859c7952ddb25d66840f052b27ef1ab60b9446c0c9fd',
);
});
});

describe('ripemd160', () => {
it('returns the RIPEMD-160 hash of the data', () => {
const data = new Uint8Array(32).fill(1);
const hash = ripemd160(data);

expect(bytesToHex(hash)).toBe('0x422d0010f16ae8539c53eb57a912890244a9eb5a');
});
});

describe('sha256', () => {
it('returns the SHA-256 hash of the data', () => {
const data = new Uint8Array(32).fill(1);
const hash = sha256(data);

expect(bytesToHex(hash)).toBe(
'0x72cd6e8422c407fb6d098690f1130b7ded7ec2f7f5e1d30bd9d521f015363793',
);
});
});
176 changes: 176 additions & 0 deletions src/cryptography.ts
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { hmac as nobleHmac } from '@noble/hashes/hmac';
import { pbkdf2Async as noblePbkdf2 } from '@noble/hashes/pbkdf2';
import { ripemd160 as nobleRipemd160 } from '@noble/hashes/ripemd160';
import { sha256 as nobleSha256 } from '@noble/hashes/sha256';
import { keccak_256 as nobleKeccak256 } from '@noble/hashes/sha3';
import { sha512 as nobleSha512 } from '@noble/hashes/sha512';

import { isWebCryptoSupported } from './utils';

export type CryptographicFunctions = {
/**
* Compute the HMAC-SHA-512 of the given data using the given key.
*
* @param key - The key to use.
* @param data - The data to hash.
* @returns The HMAC-SHA-512 of the data.
*/
hmacSha512?: (key: Uint8Array, data: Uint8Array) => Promise<Uint8Array>;

/**
* Compute the PBKDF2 of the given password, salt, iterations, and key length.
* The hash function used is SHA-512.
*
* @param password - The password to hash.
* @param salt - The salt to use.
* @param iterations - The number of iterations.
* @param keyLength - The desired key length.
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
* @returns The PBKDF2 of the password.
*/
pbkdf2Sha512?: (
password: Uint8Array,
salt: Uint8Array,
iterations: number,
keyLength: number,
) => Promise<Uint8Array>;
};

/**
* Compute the HMAC-SHA-512 of the given data using the given key.
*
* This function uses the Web Crypto API if available, falling back to a
* JavaScript implementation if not.
*
* @param key - The key to use.
* @param data - The data to hash.
* @param cryptographicFunctions - The cryptographic functions to use. If
* provided, these will be used instead of the built-in implementations.
* @returns The HMAC-SHA-512 of the data.
*/
export async function hmacSha512(
key: Uint8Array,
data: Uint8Array,
cryptographicFunctions: CryptographicFunctions = {},
): Promise<Uint8Array> {
if (cryptographicFunctions.hmacSha512) {
return await cryptographicFunctions.hmacSha512(key, data);
}

if (isWebCryptoSupported()) {
/* eslint-disable no-restricted-globals */
const subtleKey = await crypto.subtle.importKey(
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
'raw',
key,
{ name: 'HMAC', hash: 'SHA-512' },
false,
['sign'],
);

const result = await crypto.subtle.sign('HMAC', subtleKey, data);
return new Uint8Array(result);
/* eslint-enable no-restricted-globals */
}

return nobleHmac(nobleSha512, key, data);
}

/**
* Compute the Keccak-256 of the given data synchronously.
*
* Right now this is just a wrapper around `keccak256` from the `@noble/hashes`
* package, but it's here in case we want to change the implementation in the
* future to allow for asynchronous hashing.
*
* @param data - The data to hash.
* @returns The Keccak-256 of the data.
*/
export function keccak256(data: Uint8Array): Uint8Array {
return nobleKeccak256(data);
}

/**
* Compute the PBKDF2 of the given password, salt, iterations, and key length.
* The hash function used is SHA-512.
*
* @param password - The password to hash.
* @param salt - The salt to use.
* @param iterations - The number of iterations.
* @param keyLength - The desired key length.
* @param cryptographicFunctions - The cryptographic functions to use. If
* provided, these will be used instead of the built-in implementations.
* @returns The PBKDF2 of the password.
*/
export async function pbkdf2Sha512(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
keyLength: number,
cryptographicFunctions: CryptographicFunctions = {},
): Promise<Uint8Array> {
if (cryptographicFunctions.pbkdf2Sha512) {
return await cryptographicFunctions.pbkdf2Sha512(
password,
salt,
iterations,
keyLength,
);
}

if (isWebCryptoSupported()) {
/* eslint-disable no-restricted-globals */
const result = await crypto.subtle.importKey(
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
'raw',
password,
{ name: 'PBKDF2' },
false,
['deriveBits'],
);

const derivedBits = await crypto.subtle.deriveBits(
{
name: 'PBKDF2',
salt,
iterations,
hash: { name: 'SHA-512' },
},
result,
keyLength * 8,
Mrtenz marked this conversation as resolved.
Show resolved Hide resolved
);

return new Uint8Array(derivedBits);
/* eslint-enable no-restricted-globals */
}

return await noblePbkdf2(nobleSha512, password, salt, {
c: iterations,
dkLen: keyLength,
});
}

/**
* Compute the RIPEMD-160 of the given data.
*
* Right now this is just a wrapper around `ripemd160` from the `@noble/hashes`
* package, but it's here in case we want to change the implementation in the
* future to allow for asynchronous hashing.
*
* @param data - The data to hash.
* @returns The RIPEMD-160 of the data.
*/
export function ripemd160(data: Uint8Array): Uint8Array {
return nobleRipemd160(data);
}

/**
* Compute the SHA-256 of the given data synchronously.
*
* Right now this is just a wrapper around `sha256` from the `@noble/hashes`
* package, but it's here in case we want to change the implementation in the
* future to allow for asynchronous hashing.
*
* @param data - The data to hash.
* @returns The SHA-256 of the data.
*/
export function sha256(data: Uint8Array): Uint8Array {
return nobleSha256(data);
}
6 changes: 3 additions & 3 deletions src/derivers/bip32.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { assert } from '@metamask/utils';
import { keccak_256 as keccak256 } from '@noble/hashes/sha3';

import type { DeriveChildKeyArgs } from '.';
import { BYTES_KEY_LENGTH } from '../constants';
import { keccak256 } from '../cryptography';
import { secp256k1 } from '../curves';
import type { SLIP10Node } from '../SLIP10Node';
import { isValidBytesKey, validateBIP32Index } from '../utils';
Expand Down Expand Up @@ -102,7 +102,7 @@ async function handleError(
curve,
});

const newEntropy = generateEntropy({
const newEntropy = await generateEntropy({
chainCode,
extension: secretExtension,
});
Expand All @@ -119,7 +119,7 @@ async function handleError(
childIndex: childIndex + 1,
});

const newEntropy = generateEntropy({
const newEntropy = await generateEntropy({
chainCode,
extension: publicExtension,
});
Expand Down
Loading
Loading