ethereum-cryptography
ethereum-cryptography is a pure JavaScript collection of the low-level primitives commonly needed by Ethereum software: Keccak and other hashes, secp256k1 signatures, BLS and BN254 curves, secure random bytes, BIP39 mnemonics, BIP32 hierarchical keys, PBKDF2, scrypt, AES, and byte conversion helpers. Version 3.2.0 mostly re-exports pinned noble and scure implementations through stable subpaths. It is cryptographic plumbing, not a wallet, signer, transaction encoder, key vault, or Ethereum RPC client.
A strong low-level default for Ethereum implementers who know exactly which bytes and domain rules they need. Most application teams should install viem or ethers instead, and no team should treat the audit badge as a substitute for protocol and key-management review.
Use it if
- You are implementing Ethereum infrastructure and need audited pure-JavaScript primitives that behave in supported browsers and Node on x86 or arm64
- You want one version-pinned surface for noble hashes, curves, ciphers and scure BIP32/BIP39 instead of coordinating those packages yourself
- You need direct byte-level control over Keccak, secp256k1, BLS12-381, BN254, mnemonic seeds, or hierarchical keys
- You can keep secret-key handling and protocol encoding in a separately reviewed layer and will import only the submodules you use
- You need to connect to Ethereum, encode transactions, implement EIP-712, add the personal-sign prefix, checksum addresses, manage nonces, or talk to a wallet: the README only exposes primitives, so use viem or ethers for protocol-level work
- You are building ordinary application encryption: the AES wrapper documents CTR and CBC modes, neither authenticates ciphertext, and recommends CBC only for decrypting existing data; use an authenticated construction such as AES-GCM or a high-level envelope library
- Your threat model needs hardware-backed, non-exportable, or remotely isolated keys: every private key and mnemonic here is a Uint8Array or string in application memory
- You cannot review cryptographic protocol choices: the API will happily hash the wrong serialization, reuse an IV, sign a digest without domain separation, derive a weak password key, or expose distinct decrypt errors; its caveats explicitly put these duties on the caller
- You target an unlisted runtime or older platform: the README explicitly supports major browsers plus Node on x86 and arm64, while other runtimes and platforms are best effort and BigInt-era browser support is assumed
- You want dependency versions to float automatically: the README deliberately pins six noble and scure dependencies and asks applications to pin ethereum-cryptography itself for supply-chain control
- You need a single convenient root import: the README says the package intentionally has no useful all-in-one entry point because a failed tree-shake could create a huge web bundle
Setup reality
There are no native builds, credentials, daemon processes, or config files. Install version 3.2.0 exactly if you follow the project's own supply-chain advice, then import a specific path such as ethereum-cryptography/keccak.js or ethereum-cryptography/secp256k1.js. Do not expect import { keccak256 } from 'ethereum-cryptography' to be the supported style; the README deliberately separates primitives so web bundlers cannot accidentally retain the whole suite. The package provides both ESM and CommonJS export conditions and includes TypeScript declarations. Its manifest supports Node ^14.21.3 or Node 16 and newer and declares npm 9 or newer. Browser builds require secure crypto.getRandomValues; Node uses crypto.randomBytes, and random generation throws rather than falling back when neither exists. Rollup needs the CommonJS and node-resolve plugins with browser: true and preferBuiltins: false, according to the browser caveat. Data crosses the API as Uint8Array or accepted hex strings, not Node Buffer, so encode text with utf8ToBytes and render bytes with bytesToHex. That boundary is a common source of double-hex and wrong-message bugs. The low-level layer does not add Ethereum protocol framing: secp256k1.sign signs the digest you supply, Keccak is not standardized SHA3-256, and deriving an address still requires dropping the uncompressed public-key prefix, hashing, and taking the final 20 bytes. BIP39 wordlists are separate imports to keep bundles smaller. Asynchronous PBKDF2 and scrypt avoid the explicitly discouraged sync browser calls, but you still need reviewed parameters, a unique random salt, progress or worker strategy, and denial-of-service limits. AES callers must generate a unique IV, preserve it with ciphertext, derive keys from passwords through a KDF, avoid returning detailed decrypt errors, and add authentication outside this wrapper. The January 2022 Cure53 audit is valuable evidence for the rewritten stack, not a guarantee that your protocol or the current dependency set is safe.
Patterns
Hash UTF-8 data with Ethereum Keccak-256hash-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));
Keccak-256 is the Ethereum hash and is not interchangeable with standardized SHA3-256. Always make the input encoding explicit.
Compute a SHA-256 digesthash-sha256
import { sha256 } from 'ethereum-cryptography/sha256.js';
import { bytesToHex, utf8ToBytes } from 'ethereum-cryptography/utils.js';
const digestHex = bytesToHex(sha256(utf8ToBytes('artifact contents')));
Hash bytes, not a visual representation of bytes. Hashing the UTF-8 characters of a hex string produces a different digest from hashing decoded hex.
Generate a valid secp256k1 private keygenerate-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));
Do not log a private key in real code. The curve helper rejects invalid scalars and uses the package's secure randomness backend.
Derive a lowercase Ethereum addressderive-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 returns a lowercase address, not an EIP-55 checksum address. Drop the 0x04 uncompressed-key prefix before hashing.
Sign and verify a 32-byte digestsign-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);
This is raw digest signing. It does not add the EIP-191 personal-sign prefix, EIP-712 domain data, or transaction encoding.
Recover the public key from a signaturerecover-public-key
const signature = secp256k1.sign(digest, privateKey);
const recoveredPoint = signature.recoverPublicKey(digest);
const recoveredPublicKey = recoveredPoint.toRawBytes(false);
const expectedPublicKey = secp256k1.getPublicKey(privateKey, false);
Version 2 moved recovery onto the Signature instance. Keep the recovery bit carried by the object returned from sign.
Generate and validate an English BIP39 mnemonicgenerate-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 wordlist is a separate import. Never log or send the mnemonic; anyone who obtains it can derive the wallet keys.
Derive an Ethereum BIP44 child keyderive-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');
A BIP39 passphrase changes every derived key. Losing or mistyping it is indistinguishable from using the wrong wallet.
Derive key bytes with asynchronous PBKDF2derive-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 salt and reviewed parameters with the derived output. Parameter suitability depends on your threat model and hardware, so benchmark and revisit it.
Derive key bytes with asynchronous scryptderive-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);
These parameters consume substantial CPU and memory. Bound concurrent requests and do not let an unauthenticated caller choose N, r, or p.
Decrypt an existing AES-CBC payloaddecrypt-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 recommends it only for existing data. Return one generic error so padding details do not leak.
Sign and verify with BLS12-381sign-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);
BLS protocols depend on ciphersuite, domain-separation tag, key validation, and proof-of-possession choices. Match the protocol specification, not just this round trip.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| viem | npm | Choose it for typed Ethereum RPC, accounts, transaction and message utilities instead of assembling protocol behavior from primitives |
| ethers | npm | Choose it for a broad wallet, provider, contract, ABI, signing, and utility toolkit with familiar Ethereum conventions |
| @noble/hashes | npm | Choose it directly when you only need audited hash and KDF implementations and do not need the Ethereum compatibility surface |
| @noble/curves | npm | Choose it directly when curve APIs are the whole requirement and you prefer following noble's current releases yourself |