ethereum-cryptography review
ethereum-cryptography 3.2.0 packages the byte-level pieces Ethereum software repeatedly needs: Keccak-256, secp256k1, BLS12-381, BN254, BIP32 and BIP39, secure random bytes, KDFs, AES, and encoding helpers. The v3 line added BLS, BN, and math modules, moved AES to synchronous non-native code, and changed the re-exported secp256k1 API. It does not know about RPC, ABI encoding, EIP-712, transactions, wallets, or custody. Our Node 22 check also found that loading the package root failed through both `require()` and ESM `import`; the documented interface is the named subpaths such as `ethereum-cryptography/keccak.js`.
ethereum-cryptography 3.2.0 installed in 3.4 seconds with 0 audit findings, but its root CommonJS and ESM loads both failed on Node 22.23.2; install it only if explicit cryptographic subpaths fit your design. Application teams that need Ethereum conventions should start with viem or ethers, while protocol authors still need an independent review of every byte and domain rule.
We installed it
| Install | ✓ · 3.4s | 8 packages on disk · 7 MB |
| Import | ✗ | ESM import fails · require() fails · CommonJS package with exports map |
| Browser | 0.1 KB | gzipped (0.1 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does ethereum-cryptography install cleanly?
Yes. In a fresh container with an empty cache, npm install ethereum-cryptography finished in 3 seconds, leaving 8 packages and 7 MB on disk. npm audit reported no known vulnerabilities.
How much does ethereum-cryptography add to a browser bundle?
0.1 KB gzipped (0.1 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does ethereum-cryptography work with both ESM and CommonJS?
Neither plain import nor require succeeded in our sandbox, so it needs a bundler or extra setup.
Does ethereum-cryptography include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
ethereum-cryptography or viem: which should you use?
viem: Use it when the job is typed Ethereum RPC, accounts, ABI work, messages, and transactions rather than raw curve and hash operations. ethereum-cryptography 3.2.0 installed in 3.4 seconds with 0 audit findings, but its root CommonJS and ESM loads both failed on Node 22.23.2; install it only if explicit cryptographic subpaths fit your design.
When should you not use ethereum-cryptography?
You need providers, contracts, ABI codecs, EIP-712, transaction signing, checksum addresses, or wallet connections. The README exposes primitives only; viem or ethers covers that protocol layer.
Use it if
- You are writing Ethereum infrastructure and need Keccak, secp256k1, BLS, BN254, or wallet derivation as byte-oriented primitives.
- You want the noble and scure implementations behind one package whose dependency versions are pinned rather than floated.
- Your browser and Node builds can import only the explicit subpaths they use and can pass `Uint8Array` values across the boundary.
- Your team already has a reviewed protocol layer for message framing, key custody, authenticated encryption, and error handling.
- You need providers, contracts, ABI codecs, EIP-712, transaction signing, checksum addresses, or wallet connections. The README exposes primitives only; viem or ethers covers that protocol layer.
- You need a working package-root import. On Node 22.23.2, both our CommonJS `require()` and ESM `import` checks failed even though the package has an exports map.
- You are choosing encryption for new application data. Its AES wrapper tests CTR and CBC, and the README recommends CBC only for decrypting existing data because these modes do not authenticate ciphertext.
- Private keys must stay hardware-backed or non-exportable. This API accepts and returns key material in ordinary process memory as strings or `Uint8Array` values.
- You cannot own the protocol review. The library will sign the digest it receives and cannot detect a missing EIP-191 prefix, wrong EIP-712 domain, repeated IV, or incorrect serialization.
- Your runtime falls outside major browsers or Node on x86 and arm64. The README calls other platforms best effort, and secure randomness throws if neither browser nor Node crypto is available.
Setup reality
We installed ethereum-cryptography 3.2.0 in 3.4 seconds in a fresh Node 22 Bookworm sandbox. The install succeeded and left 8 packages using 7 MB. npm audit reported 0 known vulnerabilities. The package itself is 508 KB unpacked, declares 5 direct dependencies and no peers, bundles TypeScript declarations, and requires Node ^14.21.3 || >=16 with npm 9 or newer.
The surprise came after installation. The package is marked CommonJS and has an exports map, yet both root require() and root ESM import failed on Node 22.23.2. Use the README's explicit paths, for example ethereum-cryptography/keccak.js or /secp256k1.js, and test those paths in your actual loader. Our all-package esbuild probe produced only a 0.1 KB minified, 0.1 KB gzipped stub, which is another reason not to read that number as the cost of the useful submodules.
No credentials, native compiler, or config file is involved. Inputs are normally Uint8Array; convert text with utf8ToBytes and display output with bytesToHex. BIP39 wordlists live behind separate imports. Random bytes come from crypto.getRandomValues in browsers or crypto.randomBytes in Node and the call throws when neither secure source exists.
The package leaves Ethereum framing to you. secp256k1.sign signs a digest, Keccak-256 is different from standardized SHA3-256, and a lowercase 20-byte address is not an EIP-55 checksum address. PBKDF2 and scrypt parameters, salts, worker limits, AES IV storage, authentication, and uniform decrypt errors remain application decisions. The README links a Cure53 audit dated January 5, 2022; it does not cover your surrounding protocol.
Patterns
Hash UTF-8 data with Ethereum Keccak-256 hash-keccak256
import { keccak256 } from 'ethereum-cryptography/keccak.js';
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils.js';
const digest = keccak256(utf8ToBytes('hello'));
console.log(bytesToHex(digest));
Ethereum uses Keccak-256 here, not standardized SHA3-256. The two functions return different digests for the same input.
Compute a SHA-256 digest hash-sha256
import { sha256 } from 'ethereum-cryptography/sha256.js';
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils.js';
const digestHex = bytesToHex(sha256(utf8ToBytes('artifact contents')));
Decode hexadecimal input before hashing it. Hashing the UTF-8 characters `0a` is different from hashing the single byte `0x0a`.
Generate a valid secp256k1 private key generate-private-key
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
import { bytesToHex } from 'ethereum-cryptography/utils.js';
const privateKey = secp256k1.utils.randomPrivateKey();
const publicKey = secp256k1.getPublicKey(privateKey, false);
console.log(bytesToHex(publicKey));
The random private key is secret application data. The helper uses the package's secure randomness source, but logging the result still compromises it.
Derive a lowercase Ethereum address derive-ethereum-address
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
import { keccak256 } from 'ethereum-cryptography/keccak.js';
import { bytesToHex } from 'ethereum-cryptography/utils.js';
const publicKey = secp256k1.getPublicKey(privateKey, false);
const addressBytes = keccak256(publicKey.slice(1)).slice(-20);
const address = `0x${bytesToHex(addressBytes)}`;
This produces a lowercase 20-byte address. EIP-55 checksum casing requires another step, and the uncompressed public-key prefix must be removed before hashing.
Sign and verify a 32-byte digest sign-and-verify-digest
import { secp256k1 } from 'ethereum-cryptography/secp256k1.js';
import { keccak256 } from 'ethereum-cryptography/keccak.js';
import { utf8ToBytes } from 'ethereum-cryptography/utils.js';
const digest = keccak256(utf8ToBytes('domain-separated message'));
const signature = secp256k1.sign(digest, privateKey);
const publicKey = secp256k1.getPublicKey(privateKey);
const valid = secp256k1.verify(signature, digest, publicKey);
The function signs the 32-byte digest exactly as supplied. It adds neither the EIP-191 personal-message prefix nor EIP-712 domain data.
Recover the public key from a signature recover-public-key
const signature = secp256k1.sign(digest, privateKey);
const recoveredPoint = signature.recoverPublicKey(digest);
const recoveredPublicKey = recoveredPoint.toRawBytes(false);
const expectedPublicKey = secp256k1.getPublicKey(privateKey, false);
In the v2 and v3 API, recovery lives on the signature object. Preserve its recovery bit if a later process must recover the public key.
Generate and validate an English BIP39 mnemonic generate-mnemonic
import { generateMnemonic, validateMnemonic } from 'ethereum-cryptography/bip39/index.js';
import { wordlist } from 'ethereum-cryptography/bip39/wordlists/english.js';
const mnemonic = generateMnemonic(wordlist);
if (!validateMnemonic(mnemonic, wordlist)) throw new Error('invalid mnemonic');
The English wordlist is a separate import. A mnemonic exposes every derived account, so it must never enter logs, analytics, or crash reports.
Derive an Ethereum BIP44 child key derive-hd-wallet
import { mnemonicToSeed } from 'ethereum-cryptography/bip39/index.js';
import { wordlist } from 'ethereum-cryptography/bip39/wordlists/english.js';
import { validateMnemonic } from 'ethereum-cryptography/bip39/index.js';
import { HDKey } from 'ethereum-cryptography/hdkey.js';
if (!validateMnemonic(mnemonic, wordlist)) throw new Error('invalid mnemonic');
const seed = await mnemonicToSeed(mnemonic, passphrase);
const account = HDKey.fromMasterSeed(seed).derive("m/44'/60'/0'/0/0");
if (!account.privateKey) throw new Error('private key unavailable');
The passphrase is part of BIP39 seed derivation. A different or lost passphrase deterministically opens a different wallet tree.
Derive key bytes with asynchronous PBKDF2 derive-key-pbkdf2
import { pbkdf2 } from 'ethereum-cryptography/pbkdf2.js';
import { getRandomBytes } from 'ethereum-cryptography/random.js';
import { utf8ToBytes } from 'ethereum-cryptography/utils.js';
const salt = await getRandomBytes(16);
const key = await pbkdf2(utf8ToBytes(password), salt, 131072, 32, 'sha256');
Store the random salt and parameter set beside the derived result. Tune iteration count on your supported hardware rather than copying it blindly.
Derive key bytes with asynchronous scrypt derive-key-scrypt
import { scrypt } from 'ethereum-cryptography/scrypt.js';
import { getRandomBytes } from 'ethereum-cryptography/random.js';
import { utf8ToBytes } from 'ethereum-cryptography/utils.js';
const salt = await getRandomBytes(16);
const key = await scrypt(utf8ToBytes(password), salt, 262144, 8, 1, 32);
scrypt can consume substantial CPU and memory. Cap parallel calls and never accept N, r, or p directly from an unauthenticated request.
Decrypt an existing AES-CBC payload decrypt-legacy-aes
import { decrypt } from 'ethereum-cryptography/aes.js';
function decryptLegacy(ciphertext, key, iv) {
try {
return decrypt(ciphertext, key, iv, 'aes-256-cbc', true);
} catch {
throw new Error('decryption failed');
}
}
CBC does not authenticate ciphertext, and the README limits this mode to existing data. Collapse all decrypt failures into the same application error to avoid exposing padding details.
Sign and verify with BLS12-381 sign-bls-message
import { bls12_381 as bls } from 'ethereum-cryptography/bls.js';
const privateKey = bls.utils.randomPrivateKey();
const publicKey = bls.getPublicKey(privateKey);
const signature = bls.sign(messageBytes, privateKey);
const valid = bls.verify(signature, messageBytes, publicKey);
A BLS round trip does not select a protocol ciphersuite. Follow the protocol's domain tag, key validation, aggregation, and proof-of-possession rules.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| viem | npm | Use it when the job is typed Ethereum RPC, accounts, ABI work, messages, and transactions rather than raw curve and hash operations. |
| ethers | npm | Use it for providers, contracts, wallets, signing conventions, and the broad utilities expected by an Ethereum application. |
| @noble/hashes | npm | Install it directly when hashes and KDFs are the complete requirement and you can track noble releases yourself. |
| @noble/curves | npm | Install it directly for current curve implementations without the Ethereum compatibility wrapper and wallet helpers. |
More security guides
cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

