Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit d6c6bba

Browse files
nazarhussainjdevcs
andauthored
Upgrade ethereum-cryptography to 1.0 (#5211)
* ⬆️ Update ethereum-cryptography package * Apply suggestions from code review Co-authored-by: Junaid <[email protected]> * 🎨 Update the code as per feedback Co-authored-by: Junaid <[email protected]>
1 parent 94f65a3 commit d6c6bba

File tree

7 files changed

+231
-179
lines changed

7 files changed

+231
-179
lines changed

packages/web3-eth-accounts/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,7 @@
4242
"dependencies": {
4343
"web3-errors": "1.0.0-alpha.0",
4444
"@ethereumjs/tx": "^3.4.0",
45-
"ethereum-cryptography": "^0.2.1",
46-
"secp256k1": "^4.0.2",
45+
"ethereum-cryptography": "^1.1.0",
4746
"web3-common": "^1.0.0-alpha.0",
4847
"web3-utils": "^4.0.0-alpha.1",
4948
"web3-validator": "^0.1.0-alpha.0"

packages/web3-eth-accounts/src/account.ts

Lines changed: 97 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -15,55 +15,83 @@ You should have received a copy of the GNU Lesser General Public License
1515
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
1616
*/
1717

18+
import { TransactionFactory, TypedTransaction } from '@ethereumjs/tx';
19+
import { decrypt as createDecipheriv, encrypt as createCipheriv } from 'ethereum-cryptography/aes';
20+
import { pbkdf2Sync } from 'ethereum-cryptography/pbkdf2';
21+
import { scryptSync } from 'ethereum-cryptography/scrypt';
22+
import { getPublicKey, recoverPublicKey, signSync, utils } from 'ethereum-cryptography/secp256k1';
1823
import {
24+
InvalidKdfError,
25+
InvalidPasswordError,
1926
InvalidPrivateKeyError,
20-
PrivateKeyLengthError,
21-
UndefinedRawTransactionError,
22-
SignerError,
2327
InvalidSignatureError,
24-
InvalidKdfError,
28+
IVLengthError,
2529
KeyDerivationError,
2630
KeyStoreVersionError,
27-
InvalidPasswordError,
28-
IVLengthError,
2931
PBKDF2IterationsError,
32+
PrivateKeyLengthError,
33+
SignerError,
34+
UndefinedRawTransactionError,
3035
} from 'web3-errors';
31-
import { utils, getPublicKey } from 'ethereum-cryptography/secp256k1';
32-
import { keccak256 } from 'ethereum-cryptography/keccak';
33-
import { TransactionFactory, TypedTransaction } from '@ethereumjs/tx';
34-
import { ecdsaSign, ecdsaRecover } from 'secp256k1';
35-
import { pbkdf2Sync } from 'ethereum-cryptography/pbkdf2';
36-
import { scryptSync } from 'ethereum-cryptography/scrypt';
37-
import { encrypt as createCipheriv, decrypt as createDecipheriv } from 'ethereum-cryptography/aes';
3836
import {
39-
toChecksumAddress,
37+
Address,
38+
Bytes,
39+
bytesToBuffer,
4040
bytesToHex,
41-
sha3Raw,
4241
HexString,
43-
randomBytes,
4442
hexToBytes,
45-
Address,
4643
isHexStrict,
44+
numberToHex,
45+
randomBytes,
46+
sha3Raw,
47+
toChecksumAddress,
4748
utf8ToHex,
4849
} from 'web3-utils';
49-
import { validator, isBuffer, isHexString32Bytes, isString, isNullish } from 'web3-validator';
50+
import { isBuffer, isNullish, isString, validator } from 'web3-validator';
51+
import { keyStoreSchema } from './schemas';
5052
import {
53+
CipherOptions,
54+
KeyStore,
55+
PBKDF2SHA256Params,
56+
ScryptParams,
5157
SignatureObject,
5258
SignResult,
5359
SignTransactionResult,
54-
KeyStore,
55-
ScryptParams,
56-
PBKDF2SHA256Params,
57-
CipherOptions,
5860
Web3Account,
5961
} from './types';
60-
import { keyStoreSchema } from './schemas';
62+
63+
/**
64+
* Get the private key buffer after the validation
65+
*
66+
* @param data - The data in any bytes format
67+
* @returns
68+
*/
69+
export const parseAndValidatePrivateKey = (data: Bytes): Buffer => {
70+
let privateKeyBuffer: Buffer;
71+
72+
// To avoid the case of 1 character less in a hex string which is prefixed with '0' by using 'bytesToBuffer'
73+
if (typeof data === 'string' && isHexStrict(data) && data.length !== 66) {
74+
throw new PrivateKeyLengthError();
75+
}
76+
77+
try {
78+
privateKeyBuffer = Buffer.isBuffer(data) ? data : bytesToBuffer(data);
79+
} catch {
80+
throw new InvalidPrivateKeyError();
81+
}
82+
83+
if (privateKeyBuffer.byteLength !== 32) {
84+
throw new PrivateKeyLengthError();
85+
}
86+
87+
return privateKeyBuffer;
88+
};
6189

6290
/**
6391
*
6492
* Hashes the given message. The data will be UTF-8 HEX decoded and enveloped as follows: "\x19Ethereum Signed Message:\n" + message.length + message and hashed using keccak256.
6593
*
66-
* @param message A message to hash, if its HEX it will be UTF8 decoded.
94+
* @param message - A message to hash, if its HEX it will be UTF8 decoded.
6795
* @returns The hashed message
6896
* ```ts
6997
* hashMessage("Hello world")
@@ -77,11 +105,14 @@ export const hashMessage = (message: string): string => {
77105

78106
const messageBytes = hexToBytes(messageHex);
79107

80-
const preamble = `\x19Ethereum Signed Message:\n${messageBytes.length}`;
108+
const preamble = Buffer.from(
109+
`\x19Ethereum Signed Message:\n${messageBytes.byteLength}`,
110+
'utf8',
111+
);
81112

82-
const ethMessage = Buffer.concat([Buffer.from(preamble), Buffer.from(messageBytes)]);
113+
const ethMessage = Buffer.concat([preamble, messageBytes]);
83114

84-
return `0x${Buffer.from(keccak256(ethMessage)).toString('hex')}`;
115+
return sha3Raw(ethMessage); // using keccak in web3-utils.sha3Raw instead of SHA3 (NIST Standard) as both are different
85116
};
86117

87118
/**
@@ -103,31 +134,30 @@ export const hashMessage = (message: string): string => {
103134
* }
104135
* ```
105136
*/
106-
export const sign = (data: string, privateKey: HexString): SignResult => {
107-
const privateKeyParam = privateKey.startsWith('0x') ? privateKey.substring(2) : privateKey;
108-
109-
if (!isHexString32Bytes(privateKeyParam, false)) {
110-
throw new PrivateKeyLengthError();
111-
}
137+
export const sign = (data: string, privateKey: Bytes): SignResult => {
138+
const privateKeyBuffer = parseAndValidatePrivateKey(privateKey);
112139

113140
const hash = hashMessage(data);
114141

115-
const signObj = ecdsaSign(
116-
Buffer.from(hash.substring(2), 'hex'),
117-
Buffer.from(privateKeyParam, 'hex'),
118-
);
142+
const [signature, recoverId] = signSync(hash.substring(2), privateKeyBuffer, {
143+
// Makes signatures compatible with libsecp256k1
144+
recovered: true,
145+
146+
// Returned signature should be in DER format ( non compact )
147+
der: false,
148+
});
119149

120-
const r = Buffer.from(signObj.signature.slice(0, 32));
121-
const s = Buffer.from(signObj.signature.slice(32, 64));
122-
const v = signObj.recid + 27;
150+
const r = Buffer.from(signature.slice(0, 32));
151+
const s = Buffer.from(signature.slice(32, 64));
152+
const v = recoverId + 27;
123153

124154
return {
125155
message: data,
126156
messageHash: hash,
127-
v: `0x${v.toString(16)}`,
128-
r: `0x${r.toString('hex')}`,
129-
s: `0x${s.toString('hex')}`,
130-
signature: `0x${Buffer.from(signObj.signature).toString('hex')}${v.toString(16)}`,
157+
v: numberToHex(v),
158+
r: bytesToHex(r),
159+
s: bytesToHex(s),
160+
signature: `0x${Buffer.from(signature).toString('hex')}${v.toString(16)}`,
131161
};
132162
};
133163

@@ -234,7 +264,7 @@ export const signTransaction = async (
234264
}
235265

236266
const rawTx = bytesToHex(signedTx.serialize());
237-
const txHash = keccak256(hexToBytes(rawTx));
267+
const txHash = sha3Raw(rawTx); // using keccak in web3-utils.sha3Raw instead of SHA3 (NIST Standard) as both are different
238268

239269
return {
240270
messageHash: bytesToHex(Buffer.from(signedTx.getMessageToSign(true))),
@@ -302,10 +332,10 @@ export const recover = (
302332

303333
const v = signature.substring(V_INDEX); // 0x + r + s + v
304334

305-
const ecPublicKey = ecdsaRecover(
335+
const ecPublicKey = recoverPublicKey(
336+
Buffer.from(hashedMessage.substring(2), 'hex'),
306337
Buffer.from(signature.substring(2, V_INDEX), 'hex'),
307338
parseInt(v, 16) - 27,
308-
Buffer.from(hashedMessage.substring(2), 'hex'),
309339
false,
310340
);
311341

@@ -360,29 +390,20 @@ const uuidV4 = (): string => {
360390
* > "0xEB014f8c8B418Db6b45774c326A0E64C78914dC0"
361391
* ```
362392
*/
363-
export const privateKeyToAddress = (privateKey: string | Buffer): string => {
364-
if (!(isString(privateKey) || isBuffer(privateKey))) {
365-
throw new InvalidPrivateKeyError();
366-
}
393+
export const privateKeyToAddress = (privateKey: Bytes): string => {
394+
const privateKeyBuffer = parseAndValidatePrivateKey(privateKey);
367395

368-
const stringPrivateKey = Buffer.isBuffer(privateKey)
369-
? Buffer.from(privateKey).toString('hex')
370-
: privateKey;
396+
// Get public key from private key in compressed format
397+
const publicKey = getPublicKey(privateKeyBuffer);
371398

372-
const stringPrivateKeyNoPrefix = stringPrivateKey.startsWith('0x')
373-
? stringPrivateKey.slice(2)
374-
: stringPrivateKey;
375-
376-
if (!isHexString32Bytes(stringPrivateKeyNoPrefix, false)) {
377-
throw new PrivateKeyLengthError();
378-
}
399+
// Uncompressed ECDSA public key contains the prefix `0x04` which is not used in the Ethereum public key
400+
const publicKeyHash = sha3Raw(publicKey.slice(1));
379401

380-
const publicKey = getPublicKey(stringPrivateKeyNoPrefix);
402+
// The hash is returned as 256 bits (32 bytes) or 64 hex characters
403+
// To get the address, take the last 20 bytes of the public hash
404+
const address = publicKeyHash.slice(-40);
381405

382-
const publicKeyString = `0x${publicKey.slice(2)}`;
383-
const publicHash = sha3Raw(publicKeyString);
384-
const publicHashHex = bytesToHex(publicHash);
385-
return toChecksumAddress(publicHashHex.slice(-40)); // To get the address, take the last 20 bytes of the public hash
406+
return toChecksumAddress(`0x${address}`);
386407
};
387408

388409
/**
@@ -460,21 +481,11 @@ export const privateKeyToAddress = (privateKey: string | Buffer): string => {
460481
*```
461482
*/
462483
export const encrypt = async (
463-
privateKey: HexString,
484+
privateKey: Bytes,
464485
password: string | Buffer,
465486
options?: CipherOptions,
466487
): Promise<KeyStore> => {
467-
if (!(isString(privateKey) || isBuffer(privateKey))) {
468-
throw new InvalidPrivateKeyError();
469-
}
470-
471-
const stringPrivateKey = Buffer.isBuffer(privateKey)
472-
? Buffer.from(privateKey).toString('hex')
473-
: privateKey;
474-
475-
if (!isHexString32Bytes(stringPrivateKey)) {
476-
throw new PrivateKeyLengthError();
477-
}
488+
const privateKeyBuffer = parseAndValidatePrivateKey(privateKey);
478489

479490
// if given salt or iv is a string, convert it to a Uint8Array
480491
let salt;
@@ -546,10 +557,8 @@ export const encrypt = async (
546557
throw new InvalidKdfError();
547558
}
548559

549-
const cipherKey = Buffer.from(stringPrivateKey.replace('0x', ''), 'hex');
550-
551560
const cipher = await createCipheriv(
552-
cipherKey,
561+
privateKeyBuffer,
553562
Buffer.from(derivedKey.slice(0, 16)),
554563
initializationVector,
555564
'aes-128-ctr',
@@ -562,7 +571,7 @@ export const encrypt = async (
562571
return {
563572
version: 3,
564573
id: uuidV4(),
565-
address: privateKeyToAddress(stringPrivateKey).toLowerCase().replace('0x', ''),
574+
address: privateKeyToAddress(privateKeyBuffer).toLowerCase().replace('0x', ''),
566575
crypto: {
567576
ciphertext,
568577
cipherparams: {
@@ -596,19 +605,19 @@ export const encrypt = async (
596605
* }
597606
* ```
598607
*/
599-
export const privateKeyToAccount = (privateKey: string | Buffer): Web3Account => {
600-
const pKey = Buffer.isBuffer(privateKey) ? Buffer.from(privateKey).toString('hex') : privateKey;
608+
export const privateKeyToAccount = (privateKey: Bytes): Web3Account => {
609+
const privateKeyBuffer = parseAndValidatePrivateKey(privateKey);
601610

602611
return {
603-
address: privateKeyToAddress(pKey),
604-
privateKey: pKey,
612+
address: privateKeyToAddress(privateKeyBuffer),
613+
privateKey: bytesToHex(privateKeyBuffer),
605614
signTransaction: (_tx: Record<string, unknown>) => {
606615
throw new SignerError('Do not have network access to sign the transaction');
607616
},
608617
sign: (data: Record<string, unknown> | string) =>
609-
sign(typeof data === 'string' ? data : JSON.stringify(data), pKey),
618+
sign(typeof data === 'string' ? data : JSON.stringify(data), privateKeyBuffer),
610619
encrypt: async (password: string, options?: Record<string, unknown>) => {
611-
const data = await encrypt(pKey, password, options);
620+
const data = await encrypt(privateKeyBuffer, password, options);
612621

613622
return JSON.stringify(data);
614623
},

packages/web3-eth-accounts/test/fixtures/account.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,33 @@ import {
2828
import { sign, signTransaction, encrypt } from '../../src/account';
2929
import { CipherOptions, KeyStore } from '../../src/types';
3030

31+
export const validPrivateKeyToAddressData: [string, string][] = [
32+
[
33+
'0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709',
34+
'0xb8CE9ab6943e0eCED004cDe8e3bBed6568B2Fa01',
35+
],
36+
[
37+
'0x9e93921f9bca358a96aa66efcccbde12850473be95f63c1453e29656feafeb35',
38+
'0x118C2E5F57FD62C2B5b46a5ae9216F4FF4011a07',
39+
],
40+
[
41+
'0xf44e0436edb0afd26b09f7b9f1e7a280d2365fc530aebccf893f1158a449d20a',
42+
'0x8824eEA7A9FF8E051e63ACAc443460151CB6fd92',
43+
],
44+
[
45+
'0xf4a2b939592564feb35ab10a8e04f6f2fe0943579fb3c9c33505298978b74893',
46+
'0xd5e099c71B797516c10ED0F0d895f429C2781142',
47+
],
48+
];
49+
50+
export const invalidPrivateKeyToAddressData: [
51+
any,
52+
PrivateKeyLengthError | InvalidPrivateKeyError,
53+
][] = [
54+
['', new InvalidPrivateKeyError()],
55+
[Buffer.from([]), new PrivateKeyLengthError()],
56+
];
57+
3158
export const validPrivateKeytoAccountData: [string, any][] = [
3259
[
3360
'0x348ce564d427a3311b6536bbcff9390d69395b06ed6c486954e971d960fe8709',
@@ -52,6 +79,17 @@ export const validPrivateKeytoAccountData: [string, any][] = [
5279
];
5380

5481
export const signatureRecoverData: [string, any][] = [
82+
[
83+
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
84+
{
85+
address: '0x6E599DA0bfF7A6598AC1224E4985430Bf16458a4',
86+
privateKey: '0xcb89ec4b01771c6c8272f4c0aafba2f8ee0b101afb22273b786939a8af7c1912',
87+
data: 'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
88+
// signature done with personal_sign
89+
signature:
90+
'0x2ac888726c80494b80b63996455d109aef5db27e673dd92f277ac6e48dc300db3dfc7549744c2a33a03a2eaa0f2837f54c5951b80d5e05257d605bc695c2ae7f1c',
91+
},
92+
],
5593
[
5694
'Some data',
5795
{
@@ -129,7 +167,7 @@ export const invalidPrivateKeytoAccountData: [
129167
any,
130168
PrivateKeyLengthError | InvalidPrivateKeyError,
131169
][] = [
132-
['', new PrivateKeyLengthError()],
170+
['', new InvalidPrivateKeyError()],
133171
[Buffer.from([]), new PrivateKeyLengthError()],
134172
];
135173

@@ -392,3 +430,13 @@ export const invalidDecryptData: [[any, string], InvalidKdfError | KeyDerivation
392430
new KeyDerivationError(),
393431
],
394432
];
433+
434+
export const validHashMessageData: [string, string][] = [
435+
['🤗', '0x716ce69c5d2d629c168bc02e24a961456bdc5a362d366119305aea73978a0332'],
436+
[
437+
'Some long text with integers 1233 and special characters and unicode \u1234 as well.',
438+
'0xff21294f27c6b1e416215feb0b0b904c552c874c4e11b2314dd3afc1714ed8a8',
439+
],
440+
['non utf8 string', '0x8862c6a425a83c082216090e4f0e03b64106189e93c29b11d0112e77b477cce2'],
441+
['', '0x5f35dce98ba4fba25530a026ed80b2cecdaa31091ba4958b99b52ea1d068adad'],
442+
];

0 commit comments

Comments
 (0)