Skip to content
Open
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
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ module.exports = {
collectCoverage: true,

// An array of glob patterns indicating a set of files for which coverage information should be collected
collectCoverageFrom: ['./src/**/*.ts'],
collectCoverageFrom: ['./src/**/*.ts', '!./src/**/*.native.ts'],

// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
Expand Down
14 changes: 14 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@metamask/eslint-config-jest": "^14.0.0",
"@metamask/eslint-config-nodejs": "^14.0.0",
"@metamask/eslint-config-typescript": "^14.0.0",
"@metamask/native-utils": "^0.7.0",
"@ts-bridge/cli": "^0.6.0",
"@types/jest": "^28.1.6",
"@types/node": "^18.18",
Expand All @@ -81,12 +82,25 @@
"jest-it-up": "^2.0.2",
"prettier": "^3.3.3",
"prettier-plugin-packagejson": "^2.3.0",
"react-native-quick-crypto": "^0.7.17",
"ts-jest": "^28.0.7",
"ts-node": "^10.9.1",
"typedoc": "^0.26.11",
"typescript": "~5.4.5",
"typescript-eslint": "^8.6.0"
},
"peerDependencies": {
"@metamask/native-utils": "*",
"react-native-quick-crypto": "*"
},
"peerDependenciesMeta": {
"@metamask/native-utils": {
"optional": true
},
"react-native-quick-crypto": {
"optional": true
}
},
"packageManager": "yarn@4.1.1",
"engines": {
"node": "^18.20 || ^20.17 || >=22"
Expand Down
9 changes: 7 additions & 2 deletions src/SLIP10Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
SLIP10PathTuple,
} from './constants';
import type { CryptographicFunctions } from './cryptography';
import { getPublicKeyForCurve } from './cryptography';
import type { SupportedCurve } from './curves';
import { getCurveByName } from './curves';
import { deriveKeyFromPath } from './derivation';
Expand Down Expand Up @@ -587,7 +588,8 @@ export class SLIP10Node implements SLIP10NodeInterface {
'Either a private key or public key is required.',
);

this.#publicKeyBytes = getCurveByName(this.curve).getPublicKey(
this.#publicKeyBytes = getPublicKeyForCurve(
this.curve,
this.privateKeyBytes,
);

Expand All @@ -609,13 +611,16 @@ export class SLIP10Node implements SLIP10NodeInterface {
);
}

return bytesToHex(publicKeyToEthAddress(this.publicKeyBytes));
return bytesToHex(
publicKeyToEthAddress(this.publicKeyBytes, this.#cryptographicFunctions),
);
}

public get fingerprint(): number {
return getFingerprint(
this.compressedPublicKeyBytes,
getCurveByName(this.curve).compressedPublicKeyLength,
this.#cryptographicFunctions,
);
}

Expand Down
134 changes: 134 additions & 0 deletions src/cryptography/cryptography.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import {
getPublicKey as nativeGetPublicKeySecp256k1,
getPublicKeyEd25519 as nativeGetPublicKeyEd25519,
hmacSha512 as nativeHmacSha512,
keccak256 as nativeKeccak256,
} from '@metamask/native-utils';
import { ripemd160 as nobleRipemd160 } from '@noble/hashes/ripemd160';
import * as quickCrypto from 'react-native-quick-crypto';

import type { CryptographicFunctionsBase } from './cryptography.types';
import type { SupportedCurve } from '../curves';
import * as ed25519Bip32Curve from '../curves/ed25519Bip32';

/**
* Compute the HMAC-SHA-512 of the given data using the given key.
*
* This function uses the native implementation from @metamask/native-utils.
*
* @param key - The key to use.
* @param data - The data to hash.
* @returns The HMAC-SHA-512 of the data.
*/
export async function hmacSha512(
key: Uint8Array,
data: Uint8Array,
): Promise<Uint8Array> {
return Promise.resolve(nativeHmacSha512(key, data));
}

/**
* Compute the Keccak-256 of the given data synchronously.
*
* This function uses the native implementation from @metamask/native-utils.
*
* @param data - The data to hash.
* @returns The Keccak-256 of the data.
*/
export function keccak256(data: Uint8Array): Uint8Array {
return nativeKeccak256(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.
* @returns The PBKDF2 of the password.
*/
export async function pbkdf2Sha512(
password: Uint8Array,
salt: Uint8Array,
iterations: number,
keyLength: number,
): Promise<Uint8Array> {
const derivedKey = quickCrypto.default.pbkdf2Sync(
password,
salt,
iterations,
keyLength,
'sha512',
);
return Promise.resolve(new Uint8Array(derivedKey));
}

/**
* 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.
*
* This function uses `createHash` from `react-native-quick-crypto`.
*
* @param data - The data to hash.
* @returns The SHA-256 of the data.
*/
export function sha256(data: Uint8Array): Uint8Array {
const hash = quickCrypto.default.createHash('sha256');
// Convert Uint8Array to ArrayBuffer for hash.update()
const arrayBuffer = new Uint8Array(data).buffer;
hash.update(arrayBuffer);
return new Uint8Array(hash.digest());
}

/**
* Get the public key for a given private key using the specified curve.
*
* This function uses the native implementations from @metamask/native-utils.
*
* @param curveName - The name of the curve to use ('ed25519' or 'secp256k1').
* @param privateKey - The private key.
* @param compressed - Whether the public key should be compressed (only applies to secp256k1).
* @returns The public key.
*/
export function getPublicKeyForCurve(
curveName: SupportedCurve,
privateKey: Uint8Array,
compressed?: boolean,
): Uint8Array {
switch (curveName) {
case 'ed25519':
return nativeGetPublicKeyEd25519(privateKey);
case 'secp256k1':
return nativeGetPublicKeySecp256k1(privateKey, compressed);
case 'ed25519Bip32':
return ed25519Bip32Curve.getPublicKey(privateKey);
default:
// eslint-disable-next-line
curveName as never;
throw new Error(`Unsupported curve: ${String(curveName)}`);
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _cryptographicFunctions: CryptographicFunctionsBase = {
hmacSha512,
pbkdf2Sha512,
sha256,
keccak256,
getPublicKeyForCurve,
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { bytesToHex } from '@metamask/utils';
import { webcrypto } from 'crypto';

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

// Node.js <20 doesn't have `globalThis.crypto`, so we need to define it.
// TODO: Remove this once we drop support for Node.js <20.
Expand Down Expand Up @@ -134,3 +135,38 @@ describe('sha256', () => {
);
});
});

describe('getPublicKeyForCurve', () => {
const privateKey = new Uint8Array(32).fill(1);

it('returns the public key for ed25519', () => {
const publicKey = getPublicKeyForCurve('ed25519', privateKey);
expect(publicKey).toBeInstanceOf(Uint8Array);
expect(publicKey).toHaveLength(33);
});

it('returns the public key for secp256k1', () => {
const publicKey = getPublicKeyForCurve('secp256k1', privateKey);
expect(publicKey).toBeInstanceOf(Uint8Array);
expect(publicKey).toHaveLength(65);
});

it('returns the compressed public key for secp256k1', () => {
const publicKey = getPublicKeyForCurve('secp256k1', privateKey, true);
expect(publicKey).toBeInstanceOf(Uint8Array);
expect(publicKey).toHaveLength(33);
});

it('returns the public key for ed25519Bip32', () => {
const publicKey = getPublicKeyForCurve('ed25519Bip32', privateKey);
expect(publicKey).toBeInstanceOf(Uint8Array);
expect(publicKey).toHaveLength(32);
});

it('throws for unsupported curves', () => {
expect(() =>
// @ts-expect-error Testing invalid curve
getPublicKeyForCurve('unsupported', privateKey),
).toThrow('Unsupported curve: unsupported');
});
});
78 changes: 45 additions & 33 deletions src/cryptography.ts → src/cryptography/cryptography.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,15 @@
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 in bytes.
* @returns The PBKDF2 of the password.
*/
pbkdf2Sha512?: (
password: Uint8Array,
salt: Uint8Array,
iterations: number,
keyLength: number,
) => Promise<Uint8Array>;
};
import type {
CryptographicFunctionsBase,
CryptographicFunctions,
} from './cryptography.types';
import type { SupportedCurve } from '../curves/curve';
import * as ed25519Curve from '../curves/ed25519';
import * as ed25519Bip32Curve from '../curves/ed25519Bip32';
import * as secp256k1Curve from '../curves/secp256k1';
import { isWebCryptoSupported } from '../utils';

/**
* Compute the HMAC-SHA-512 of the given data using the given key.
Expand All @@ -57,8 +37,7 @@
}

if (isWebCryptoSupported()) {
/* eslint-disable no-restricted-globals */
const subtleKey = await crypto.subtle.importKey(

Check failure on line 40 in src/cryptography/cryptography.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Unexpected use of 'crypto'. This global is not available in the Node.js and browser environment
'raw',
key,
{ name: 'HMAC', hash: 'SHA-512' },
Expand All @@ -66,9 +45,8 @@
['sign'],
);

const result = await crypto.subtle.sign('HMAC', subtleKey, data);

Check failure on line 48 in src/cryptography/cryptography.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Unexpected use of 'crypto'. This global is not available in the Node.js and browser environment
return new Uint8Array(result);
/* eslint-enable no-restricted-globals */
}

return nobleHmac(nobleSha512, key, data);
Expand Down Expand Up @@ -117,8 +95,7 @@
}

if (isWebCryptoSupported()) {
/* eslint-disable no-restricted-globals */
const key = await crypto.subtle.importKey(

Check failure on line 98 in src/cryptography/cryptography.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Unexpected use of 'crypto'. This global is not available in the Node.js and browser environment
'raw',
password,
{ name: 'PBKDF2' },
Expand All @@ -126,7 +103,7 @@
['deriveBits'],
);

const derivedBits = await crypto.subtle.deriveBits(

Check failure on line 106 in src/cryptography/cryptography.ts

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint (22.x)

Unexpected use of 'crypto'. This global is not available in the Node.js and browser environment
{
name: 'PBKDF2',
salt,
Expand All @@ -140,7 +117,6 @@
);

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

return await noblePbkdf2(nobleSha512, password, salt, {
Expand Down Expand Up @@ -176,3 +152,39 @@
export function sha256(data: Uint8Array): Uint8Array {
return nobleSha256(data);
}

/**
* Get the public key for a given private key using the specified curve.
*
* @param curveName - The name of the curve to use ('ed25519' or 'secp256k1').
* @param privateKey - The private key.
* @param compressed - Whether the public key should be compressed (only applies to secp256k1).
* @returns The public key.
*/
export function getPublicKeyForCurve(
curveName: SupportedCurve,
privateKey: Uint8Array,
compressed?: boolean,
): Uint8Array {
switch (curveName) {
case 'ed25519':
return ed25519Curve.getPublicKey(privateKey);
case 'secp256k1':
return secp256k1Curve.getPublicKey(privateKey, compressed);
case 'ed25519Bip32':
return ed25519Bip32Curve.getPublicKey(privateKey);
default:
// eslint-disable-next-line
curveName as never;
throw new Error(`Unsupported curve: ${String(curveName)}`);
}
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _cryptographicFunctions: CryptographicFunctionsBase = {
hmacSha512,
pbkdf2Sha512,
sha256,
keccak256,
getPublicKeyForCurve,
};
Loading
Loading