# eth-crypto puts Ethereum keys, signatures and ECIES in one npm package

> eth-crypto is a JavaScript library that derives Ethereum identities from a private key, signs and recovers signatures, and encrypts messages for another identity with a hybrid encryption scheme, with tutorials for pairing it with web3 and Solidity. It is a convenience layer, and the version numbering in the repository has drifted enough that you should check what you actually get from npm.

**pubkey/eth-crypto** — Cryptographic javascript-functions for ethereum and tutorials to use them with web3js and solidity

- Repository: https://github.com/pubkey/eth-crypto
- Stars: 922 · Forks: 166
- Language: JavaScript
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/pubkey-eth-crypto

## What the package actually covers

The repository description is plain about the scope: cryptographic JavaScript functions for Ethereum and tutorials on how to use them together with web3js and Solidity.
The API list in the README is the honest inventory. It has createIdentity, publicKeyByPrivateKey, publicKey.toAddress, publicKey.compress, publicKey.decompress, sign, recover, recoverPublicKey, encryptWithPublicKey, decryptWithPrivateKey, cipher.stringify, cipher.parse, signTransaction, txDataByCompiled and calculateContractAddress, plus hex compress and decompress.
That is a complete set for one specific job: an application that holds a key in JavaScript, signs things with it, and sometimes sends an encrypted message to someone else. It is not a general purpose crypto library and it is not a blockchain client. The keywords in package.json name the underlying primitives, including secp256k1 and ecies.
The tutorials are what make the package more than a function list. There are three of them, and each one is a complete scenario. Creating keys and use them for Ethereum transactions creates an Ethereum identity and uses it to send transactions to the blockchain. Sign and validate data with Solidity signs data in JavaScript and validates the signature inside a smart contract. Sending encrypted and signed data to other identities uses Ethereum identities and asymmetric cryptography to send an encrypted and signed message from Alice to Bob.
The third one is the reason to choose this library over a generic secp256k1 wrapper, because encrypting a message to a recipient's public key and proving the sender with a signature are two different cryptographic operations, and having them work together is most of the work.

## Identity creation, and the entropy warning

The entry point is createIdentity, which creates a new Ethereum identity with a privateKey, publicKey and address as hex strings.
```javascript
  const identity = EthCrypto.createIdentity();
```
The values it returns are shown in the README as an address beginning 0x3f243FdacE01Cfd9719f7359c94BA11361f32471, a private key with the 0x prefix, and a public key as a hex string without one.
You can also supply your own entropy buffer.
```javascript
  const entropy = Buffer.from('f2dacf...', 'utf-8'); // must contain at least 128 chars
  const identity = EthCrypto.createIdentity(entropy);
```
The comment in that example is the important part. The entropy must contain at least 128 chars, and the README attaches a warning to the whole approach: use this with caution, a bad entropy can result in an unsecure private key.
That warning deserves emphasis in any adoption decision. Generating a key from a predictable buffer produces a predictable key, and a predictable key on Ethereum means a wallet anybody can drain. The library will happily derive a key from bytes you hand it, and it is the caller's job to source entropy from a cryptographically secure source.
The rest of the key derivation chain is ordinary and well documented. publicKeyByPrivateKey derives the public key from a private key and returns it as a hex string, and publicKey.toAddress derives the Ethereum address from the public key. The README also documents compress and decompress for public keys, noting that compressed keys start with 02 or 03 while non-compressed keys start with 04 or have no prefix.

## Signing a hash and recovering the signer

Signing takes a hash and a private key, and returns the signature as a hex string. The message itself is hashed first, with keccak256 provided on the hash namespace of the library.
```javascript
  const message = 'foobar';
  const messageHash = EthCrypto.hash.keccak256(message);
  const signature = EthCrypto.sign(
      '0x107be946709e41b7895eea9f2dacf998a0a9124acbb786f0fd1a826101581a07', // privateKey
      messageHash // hash of message
  );
```
The recovery functions are the inverse, and they come in two flavours. recover recovers the signer's address from the signature, and recoverPublicKey recovers the signer's publicKey from the signature. Both take the signature and the signed message hash.
```javascript
    const signer = EthCrypto.recover(
      '0xc04b809d8f33c46ff80c44ba58e866ff0d5..',
      EthCrypto.hash.keccak256('foobar') // signed message hash
  );
```
The design here is that the hash, not the message, is what gets signed. That matters because it is what makes the Solidity tutorial possible: a contract can recompute the keccak256 hash of the payload and then run ecrecover on the signature, and the two sides agree without hashing twice differently.
The address returned by recover in the example matches the address returned by createIdentity earlier in the README, which is the smallest possible demonstration that the round trip is consistent.

## Encrypted messages: an ephemeral key, an iv, a ciphertext and a mac

The encryption functions are asynchronous, and the shape of the returned object tells you the scheme.
encryptWithPublicKey encrypts the message with the public key so that only the corresponding private key can decrypt it, and returns the encrypted data as an object with hex strings.
```javascript
    const encrypted = await EthCrypto.encryptWithPublicKey(
        'bf1cc3154424dc22191941d9f4f50b063a2b663a2337e5548abea633c1d06ece...', // publicKey
        'foobar' // message
    );
```
The result carries four fields, and the README shows the values: an iv, an ephemPublicKey, a ciphertext and a mac.
The ephemeral public key is the giveaway that this is a hybrid scheme. The sender generates a fresh key for each message, encrypts the payload with it, encrypts that fresh key with the recipient's public key, and sends the encrypted key alongside the payload. The recipient's private key unwraps the ephemeral key, and the ephemeral key decrypts the message. That is why decryptWithPrivateKey needs the private key and the whole object, and why the message is not encrypted directly with the recipient's public key.
The mac is what makes the scheme authenticated rather than merely encrypted. The decryptWithPrivateKey example takes the private key and the same four field object and returns the message as a string, with the README showing foobar as the result.
Two utility functions sit alongside this. cipher.stringify transforms the object with the encrypted data into a smaller string representation, and cipher.parse goes the other way. That matters for transport, since the four field object is awkward to put in a URL or a JSON body without bloating it.

## Transaction helpers and the Solidity side

The last group of functions exists to bridge JavaScript and the chain. signTransaction signs a transaction, txDataByCompiled derives transaction data from compiled contract output, and calculateContractAddress computes the address a contract will live at.
The third tutorial explains why the first of these is not enough on its own. In the encrypted message tutorial, the point is not only that the message is confidential, but that the recipient can verify who sent it. Signing the payload and encrypting it are two separate steps performed with two separate keys.
The repository ships the Solidity side of that story as well. The top level entries include a contracts/ directory and a config/ directory, and the lint script runs solhint over the contracts in addition to eslint over the JavaScript. There is a build:sol script that compiles the Solidity sources into a gen directory using solidity-cli, which means the smart contracts are built as part of the same pipeline as the library rather than being maintained separately.
That pairing is what distinguishes this project from a bare signing library. The validation story lives in the contracts, the signing story lives in the JavaScript, and the tutorials walk through both sides of the same flow.
The sponsorship and the community link round out the README, and the tutorials directory carries the three guides described above.

## Building and testing: two environments and a size check

The package ships both a CommonJS and an ES module build, plus typings.
```javascript
// es6
import EthCrypto from 'eth-crypto';

// node
const EthCrypto = require('eth-crypto');
```
The package.json points main at ./dist/lib/index.js, jsnext:main and module at ./dist/es/index.js, and types at ./typings/index.d.ts. The build script compiles the src/ directory to dist/es with babel under NODE_ENV=es6 and to dist/lib with a plain babel run for the older target, and it does the two in parallel with concurrently.
Tests run in two environments, and the split is deliberate. The test script runs the node suite and then the browser suite. The node suite builds first and then runs mocha against ./test/index.test.js with a six second timeout and the exit flag. The browser suite builds and starts karma with the configuration in ./config/karma.conf.js in single run mode.
There are two more checks worth knowing about. test:typings runs mocha against ./test/typings.test.js with a twelve second timeout, which validates the TypeScript declarations. And test:size builds a webpack bundle and reports the minified and gzipped size, which is the number that matters if this is going into a web page.
The dependencies are checked too, with test:deps running dependency-check against package.json with no dev dependencies, unused detection, and @types/bn.js ignored. The lint script runs eslint over src, test and config, plus solhint over the Solidity contracts.
The repository also carries a renovate.json, which is how the pinned dependency versions in a project like this stay current.

## Version drift and a two year gap between releases

The version numbers do not agree, and that is the first thing to resolve before you pin anything.
The version field in package.json reads 4.1.0. The recent releases listed for this repository are 2.7.0 from 2024-11-03, 2.0.0 from 2021-07-05 and 1.9.0 from 2021-03-20. The package description also differs slightly between the two places, with the README saying web3js and the package metadata saying web3.
So the published tag line and the working tree are on different numbering schemes. That is a strong argument for pinning to an exact version and verifying the API surface you rely on against the code you actually installed, rather than assuming that the newest tag contains everything in package.json.
Activity is still there. The last push was on 2026-10-16, the repository is not archived, and the default branch is master.
The licence is MIT, with a LICENSE file at the root of the tree, which is the permissive case and leaves little to negotiate if you ship it inside an application.
For an evaluation, the three checks are: pin a version and read its README rather than the current one, confirm which of the API functions you need exist in that version, and decide where key generation happens. If a browser dApp needs to create an identity at runtime, treat the entropy warning as a hard requirement rather than a footnote.

## Conclusion

Adopt eth-crypto when you are building a browser side dApp or a script that needs to create an identity, sign a hash and recover the signer without pulling in a full web3 stack. Do not adopt it as the only cryptography layer in a product, because the README itself warns that a bad entropy buffer yields an insecure private key, so key generation and custody should stay with a system built for that. Before pinning a version, check the package version in package.json against the latest tagged release, since the two do not agree.

## FAQ

### How do I install eth-crypto?

Run npm install eth-crypto --save. The package can then be imported as import EthCrypto from 'eth-crypto' in ES6 or required with const EthCrypto = require('eth-crypto') in Node.

### How do I create an Ethereum identity with eth-crypto?

Call EthCrypto.createIdentity(), which returns an object with address, privateKey and publicKey as hex strings. You can pass your own entropy buffer, which must contain at least 128 chars, but the README warns that bad entropy can result in an insecure private key.

### How do I sign data and recover the signer address?

Hash the message with EthCrypto.hash.keccak256, sign the hash with EthCrypto.sign and your private key, then call EthCrypto.recover with the signature and the same hash to get the signer's address, or EthCrypto.recoverPublicKey to get the signer's public key.

### What fields does encryptWithPublicKey return?

It returns an object with hex strings containing iv, ephemPublicKey, ciphertext and mac. You decrypt it with EthCrypto.decryptWithPrivateKey using the recipient's private key and the same object.

### Does eth-crypto work with Solidity contracts?

Yes. The repository ships Solidity contracts and a tutorial on signing and validating data with Solidity, where data is signed in JavaScript and the signature is validated inside a smart contract. The lint script also runs solhint over the contracts.

## Sources

- [Issues](https://github.com/pubkey/eth-crypto/issues)
- [License: MIT](https://github.com/pubkey/eth-crypto/blob/master/LICENSE)
- [pubkey/eth-crypto on GitHub](https://github.com/pubkey/eth-crypto)
- [README](https://github.com/pubkey/eth-crypto/blob/master/README.md)
- [Releases](https://github.com/pubkey/eth-crypto/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/pubkey-eth-crypto
