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

chore: migrate to @noble/curves and ecies/js #1236

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
4 changes: 2 additions & 2 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@
"test:watch": "yarn test --watch"
},
"dependencies": {
"@noble/curves": "1.2.0",
"@requestnetwork/types": "0.37.0",
"@toruslabs/eccrypto": "4.0.0",
"eciesjs": "0.4.5",
"ethers": "5.5.1",
"secp256k1": "4.0.2",
"tslib": "2.5.0"
},
"devDependencies": {
Expand Down
141 changes: 41 additions & 100 deletions packages/utils/src/crypto/ec-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { publicKeyConvert, ecdsaRecover } from 'secp256k1';
import { ethers } from 'ethers';
import { Ecies, decrypt, encrypt } from '@toruslabs/eccrypto';

import { decrypt, ECIES_CONFIG, encrypt, PublicKey } from 'eciesjs';
import { secp256k1 } from '@noble/curves/secp256k1';
import { computeAddress } from 'ethers/lib/utils';

/**
* Function to manage Elliptic-curve cryptography
Expand All @@ -14,6 +16,11 @@ export {
ecSign,
};

ECIES_CONFIG.ellipticCurve = 'secp256k1';
ECIES_CONFIG.isEphemeralKeyCompressed = false;
ECIES_CONFIG.symmetricAlgorithm = 'aes-256-gcm';
ECIES_CONFIG.symmetricNonceLength = 16;

/**
* Function to derive the address from an EC private key
*
Expand Down Expand Up @@ -42,13 +49,13 @@ function getAddressFromPrivateKey(privateKey: string): string {
/**
* Function to derive the address from an EC public key
*
* @param publicKey the public key to derive
* @param publicKeyHex the public key to derive
*
* @returns the address
*/
function getAddressFromPublicKey(publicKey: string): string {
function getAddressFromPublicKey(publicKeyHex: string): string {
try {
return ethers.utils.computeAddress(compressPublicKey(publicKey));
return ethers.utils.computeAddress(PublicKey.fromHex(publicKeyHex).toHex(true));
} catch (e) {
if (
e.message === 'public key length is invalid' ||
Expand All @@ -62,22 +69,20 @@ function getAddressFromPublicKey(publicKey: string): string {
}

/**
* Function ecSigndata with ECDSA
* Function ecSign data with ECDSA
*
* @param data the data to sign
* @param privateKey the private key used to sign the message
* @param dataHash the data to sign
*
* @returns the signature
*/
function ecSign(privateKey: string, data: string): string {
function ecSign(privateKey: string, dataHash: string): string {
try {
const signingKey = new ethers.utils.SigningKey(privateKey);
return ethers.utils.joinSignature(signingKey.signDigest(data));
privateKey = privateKey.replace(/^0x/, '');
dataHash = dataHash.replace(/^0x/, '');
return `0x${secp256k1.sign(dataHash, privateKey).toCompactHex()}1b`;
} catch (e) {
if (
e.message === 'private key length is invalid' ||
e.message === 'Expected private key to be an Uint8Array with length 32' ||
e.code === 'INVALID_ARGUMENT'
) {
if (e.message === 'private key must be 32 bytes, hex or bigint, not string') {
throw new Error('The private key must be a string representing 32 bytes');
}
throw e;
Expand All @@ -87,37 +92,25 @@ function ecSign(privateKey: string, data: string): string {
/**
* Function to recover address from a signature
*
* @param signature the signature
* @param data the data signed
* @param signatureHex the signature
* @param dataHash the data signed
*
* @returns the address
*/
function ecRecover(signature: string, data: string): string {
function ecRecover(signatureHex: string, dataHash: string): string {
try {
signature = signature.replace(/^0x/, '');
data = data.replace(/^0x/, '');
// split into v-value and sig
const sigOnly = signature.substring(0, signature.length - 2); // all but last 2 chars
const vValue = signature.slice(-2); // last 2 chars
signatureHex = signatureHex.replace(/^0x/, '');
dataHash = dataHash.replace(/^0x/, '');

const sigOnly = signatureHex.substring(0, signatureHex.length - 2); // all but last 2 chars
const vValue = signatureHex.slice(-2); // last 2 chars
const recoveryNumber = vValue === '1c' ? 1 : 0;

return ethers.utils.computeAddress(
Buffer.from(
ecdsaRecover(
new Uint8Array(Buffer.from(sigOnly, 'hex')),
recoveryNumber,
new Uint8Array(Buffer.from(data, 'hex')),
false,
),
),
);
const signature = secp256k1.Signature.fromCompact(sigOnly);
const signatureRecover = signature.addRecoveryBit(recoveryNumber);
return computeAddress(`0x${signatureRecover.recoverPublicKey(dataHash).toHex()}`);
} catch (e) {
if (
e.message === 'signature length is invalid' ||
e.message === 'Expected signature to be an Uint8Array with length 64' ||
e.code === 'INVALID_ARGUMENT'
) {
if (e.message === 'compactSignature expected 64 bytes, got 0') {
throw new Error('The signature must be a string representing 66 bytes');
}
throw e;
Expand All @@ -132,24 +125,11 @@ function ecRecover(signature: string, data: string): string {
*
* @returns the encrypted data
*/
async function ecEncrypt(publicKey: string, data: string): Promise<string> {
function ecEncrypt(publicKey: string, data: string): string {
try {
// encrypts the data with the publicKey, returns the encrypted data with encryption parameters (such as IV..)
const compressed = compressPublicKey(publicKey);
const encrypted = await encrypt(Buffer.from(compressed), Buffer.from(data));

// Transforms the object with the encrypted data into a smaller string-representation.
return Buffer.concat([
encrypted.iv,
publicKeyConvert(encrypted.ephemPublicKey),
encrypted.mac,
encrypted.ciphertext,
]).toString('hex');
return encrypt(publicKey, Buffer.from(data)).toString('hex').slice(2);
} catch (e) {
if (
e.message === 'public key length is invalid' ||
e.message === 'Expected public key to be an Uint8Array with length [33, 65]'
) {
if (e.message === 'second arg must be public key') {
throw new Error('The public key must be a string representing 64 bytes');
}
throw e;
Expand All @@ -164,58 +144,19 @@ async function ecEncrypt(publicKey: string, data: string): Promise<string> {
*
* @returns the decrypted data
*/
async function ecDecrypt(privateKey: string, data: string): Promise<string> {
function ecDecrypt(privateKey: string, data: string): string {
try {
const buf = await decrypt(Buffer.from(privateKey.replace(/^0x/, ''), 'hex'), eciesSplit(data));
return buf.toString();
if (!data.startsWith('04')) {
data = `04${data}`;
}
return decrypt(privateKey, Buffer.from(data, 'hex')).toString();
} catch (e) {
if (
e.message === 'Bad private key' ||
e.message === 'Expected private key to be an Uint8Array with length 32'
) {
if (e.message === 'Invalid private key') {
throw new Error('The private key must be a string representing 32 bytes');
}
if (
e.message === 'public key length is invalid' ||
e.message === 'Expected public key to be an Uint8Array with length [33, 65]' ||
e.message === 'Bad MAC' ||
e.message === 'bad MAC after trying padded' ||
e.message === 'the public key could not be parsed or is invalid' ||
e.message === 'Public Key could not be parsed'
) {
if (e.message === 'second arg must be public key') {
throw new Error('The encrypted data is not well formatted');
}
throw e;
}
}

/**
* Converts a public key to its compressed form.
*/
function compressPublicKey(publicKey: string): Uint8Array {
publicKey = publicKey.replace(/^0x/, '');
// if there are more bytes than the key itself, it means there is already a prefix
if (publicKey.length % 32 === 0) {
publicKey = `04${publicKey}`;
}
return publicKeyConvert(Buffer.from(publicKey, 'hex'));
}

/**
* Split an encrypted string to ECIES params
* inspired from https://github.com/pubkey/eth-crypto/blob/master/src/ecDecrypt-with-private-key.js
*/
const eciesSplit = (str: string): Ecies => {
const buf = Buffer.from(str, 'hex');

const ephemPublicKeyStr = buf.toString('hex', 16, 49);

return {
iv: Buffer.from(buf.toString('hex', 0, 16), 'hex'),
mac: Buffer.from(buf.toString('hex', 49, 81), 'hex'),
ciphertext: Buffer.from(buf.toString('hex', 81, buf.length), 'hex'),
ephemPublicKey: Buffer.from(
publicKeyConvert(new Uint8Array(Buffer.from(ephemPublicKeyStr, 'hex')), false),
),
};
};
32 changes: 16 additions & 16 deletions packages/utils/test/crypto/ec-utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,31 +97,31 @@ describe('Utils/EcUtils', () => {

describe('encrypt', () => {
it('can encrypt', async () => {
const encryptedData = await ecEncrypt(rawId.publicKey, anyData);
const encryptedData = ecEncrypt(rawId.publicKey, anyData);
// 'encrypt() error'
expect(encryptedData.length).toBe(226);
// 'decrypt() error'
expect(await ecDecrypt(rawId.privateKey, encryptedData)).toBe(anyData);
expect(ecDecrypt(rawId.privateKey, encryptedData)).toBe(anyData);
});

it('can encrypt with other public key formats', async () => {
const encryptedData = await ecEncrypt(
const encryptedData = ecEncrypt(
'0396212fc129c2f78771218b2e93da7a5aac63490a42bb41b97848c39c14fe65cd',
anyData,
);
expect(encryptedData.length).toBe(226);
});

it('cannot encrypt data with a wrong public key', async () => {
await expect(ecEncrypt('cf4a', anyData)).rejects.toThrowError(
expect(() => ecEncrypt('cf4a', anyData)).toThrow(
'The public key must be a string representing 64 bytes',
);
});
});

describe('decrypt', () => {
it('can decrypt', async () => {
const data = await ecDecrypt(
const data = ecDecrypt(
rawId.privateKey,
'307bac038efaa5bf8a0ac8db53fd4de8024a0c0baf37283a9e6671589eba18edc12b3915ff0df66e6ffad862440228a65ead99e3320e50aa90008961e3d68acc35b314e98020e3280bf4ce4258419dbb775185e60b43e7b88038a776a9322ff7cb3e886b2d92060cff2951ef3beedcc70a',
);
Expand All @@ -130,36 +130,36 @@ describe('Utils/EcUtils', () => {
});

it('cannot decrypt data with a wrong private key', async () => {
await expect(
expect(() =>
ecDecrypt(
'0xaa',
'307bac038efaa5bf8a0ac8db53fd4de8024a0c0baf37283a9e6671589eba18edc12b3915ff0df66e6ffad862440228a65ead99e3320e50aa90008961e3d68acc35b314e98020e3280bf4ce4258419dbb775185e60b43e7b88038a776a9322ff7cb3e886b2d92060cff2951ef3beedcc70a',
),
).rejects.toThrowError('The private key must be a string representing 32 bytes');
).toThrow('The private key must be a string representing 32 bytes');
});

it('cannot decrypt data with a wrong encrypted data: public key too short', async () => {
await expect(ecDecrypt(rawId.privateKey, 'aa')).rejects.toThrowError(
expect(() => ecDecrypt(rawId.privateKey, 'aa')).toThrow(
'The encrypted data is not well formatted',
);
});

it('cannot decrypt data with a wrong encrypted data: public key not parsable', async () => {
await expect(
expect(() =>
ecDecrypt(
rawId.privateKey,
'e50aa90008961e3d68acc35b314e98020e3280bf4ce4258419dbb775185e60b43e7b88038a776a9322ff7cb3e886b2d92060cff2951ef3beedcc7',
),
).rejects.toThrowError('The encrypted data is not well formatted');
).toThrow('The encrypted data is not well formatted');
});

it('cannot decrypt data with a wrong encrypted data: bad MAC', async () => {
await expect(
expect(() =>
ecDecrypt(
rawId.privateKey,
'307bac038efaa5bf8a0ac8db53fd4de8024a0c0baf37283a9e6671589eba18edc12b3915ff0df66e6ffad862440228a65ead99e3320e50aa90008961e3d68acc35b314e98020e3280bf4ce4258419dbb775185e60b43e7b88038a776a9322ff7cb3e886b2d92060cff2951ef3beedcc7',
),
).rejects.toThrowError('The encrypted data is not well formatted');
).toThrow('The encrypted data is not well formatted');
});

it.each([
Expand All @@ -168,22 +168,22 @@ describe('Utils/EcUtils', () => {
])('should be compatible with legacy $type implementation of eccrypto', async ({ array }) => {
for (const row of array) {
const { data, key, encrypted } = row;
const decrypted = await ecDecrypt(key, encrypted);
const decrypted = ecDecrypt(key, encrypted);
expect(decrypted).toBe(data);
}
});
});

it('can encrypt()', async () => {
const encryptedData = await ecEncrypt(rawId.publicKey, anyData);
const encryptedData = ecEncrypt(rawId.publicKey, anyData);
// 'encrypt() error'
expect(encryptedData.length).toBe(226);
// 'decrypt() error'
expect(await ecDecrypt(rawId.privateKey, encryptedData)).toBe(anyData);
expect(ecDecrypt(rawId.privateKey, encryptedData)).toBe(anyData);
});

it('can decrypt()', async () => {
const data = await ecDecrypt(
const data = ecDecrypt(
rawId.privateKey,
'307bac038efaa5bf8a0ac8db53fd4de8024a0c0baf37283a9e6671589eba18edc12b3915ff0df66e6ffad862440228a65ead99e3320e50aa90008961e3d68acc35b314e98020e3280bf4ce4258419dbb775185e60b43e7b88038a776a9322ff7cb3e886b2d92060cff2951ef3beedcc70a',
);
Expand Down
Loading
Loading