JavaScript Library for Elliptic Curve Cryptography
js-ecutils is a JavaScript library for Elliptic Curve Cryptography (ECC) on short Weierstrass curves (y² = x³ + ax + b over a prime field F_p). It provides ECDSA digital signatures, key exchange protocols (Diffie-Hellman, Massey-Omura), Koblitz message encoding, SEC 1 point compression, and both affine and Jacobian coordinate arithmetic. Suitable for cryptography education and secure systems.
- Point arithmetic — addition, doubling, scalar multiplication in affine and Jacobian coordinates
- 8 standard curves — secp192k1, secp192r1, secp224k1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1
- ECDSA signatures —
sign(),verify(),signMessage(),verifyMessage()with SHA-256 - Koblitz encoding — encode/decode text messages as elliptic curve points
- Diffie-Hellman key exchange — ECDH shared secret computation
- Massey-Omura three-pass protocol — key-free message exchange via encrypt/decrypt
- SEC 1 point compression —
compress(),decompress(),compressSec1(),toUncompressedSec1(),fromSec1() - Modular arithmetic utilities — modular square root (Tonelli-Shanks), quadratic residue testing
- Secure nonce generation — uses
crypto.getRandomValues()(CSPRNG) instead ofMath.random() - Cross-platform — works in Node.js and browsers (Web Crypto API)
Using npm:
npm install js-ecutilsOr, for web usage:
<script src="https://unpkg.com/js-ecutils@latest/dist/web/min.js"></script>const {
Point, CurveParams, getCurve, getGenerator,
DigitalSignature, Koblitz,
DiffieHellman, MasseyOmura
} = require('js-ecutils')
// Point arithmetic on a toy curve y² = x³ + x + 1 over F₂₃
const curve = new CurveParams({ p: 23n, a: 1n, b: 1n, n: 28n, h: 1n })
const P = new Point(0n, 1n, curve)
const Q = new Point(6n, 19n, curve)
const R = P.add(Q)
console.log(`P + Q = (${R.x}, ${R.y})`) // (13, 16)
// Scalar multiplication on secp256k1
const G = getGenerator('secp256k1')
const pubKey = G.mul(42n)
console.log(`42·G = (${pubKey.x}, ${pubKey.y})`)<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>js-ecutils Example</title>
<script src="https://unpkg.com/js-ecutils@latest/dist/web/min.js"></script>
<script>
window.onload = function() {
const { Point, CurveParams } = window.ecutils
const curve = new CurveParams({ p: 23n, a: 1n, b: 1n, n: 28n, h: 1n })
const P = new Point(0n, 1n, curve)
const Q = new Point(6n, 19n, curve)
const R = P.add(Q)
console.log(`P + Q = (${R.x}, ${R.y})`)
}
</script>
</head>
<body>
<h1>js-ecutils Example</h1>
</body>
</html>Represents a point on an elliptic curve. The identity element (point at infinity) is represented as Point(0n, 0n).
const { Point, CurveParams } = require('js-ecutils')
const curve = new CurveParams({ p: 23n, a: 1n, b: 1n, n: 28n, h: 1n })
const P = new Point(0n, 1n, curve)Constructor: new Point(x, y, curve, _trusted)
x— x-coordinate (BigInt)y— y-coordinate (BigInt)curve—CurveParamsinstance (optional, can be borrowed from another point during operations)_trusted— skip on-curve validation (internal use only)
Properties:
isIdentity—trueif this is the point at infinity
Methods:
isOnCurve()— verifies y² ≡ x³ + ax + b (mod p)neg()— additive inverse: returns (x, p - y)add(other)— point addition: P + Qsub(other)— point subtraction: P - Qmul(k)— scalar multiplication: k·P (double-and-add)compress()— returns[x, yParity]where yParity is 0n or 1ndecompress(x, yParity, curve)— (static) recovers a point from compressed formcompressSec1()— SEC 1 compressed format (Uint8Array:02/03prefix + x)toUncompressedSec1()— SEC 1 uncompressed format (Uint8Array:04prefix + x + y)fromSec1(data, curve)— (static) parses SEC 1 compressed or uncompressed bytestoString()— human-readable representation
Immutable container for elliptic curve domain parameters.
const { CurveParams, CoordinateSystem } = require('js-ecutils')
const curve = new CurveParams({
p: 23n, a: 1n, b: 1n, n: 28n, h: 1n,
coord: CoordinateSystem.JACOBIAN // default
})Constructor: new CurveParams({ p, a, b, n, h, coord })
p— prime modulus of the finite field F_pa,b— curve equation coefficients (y² = x³ + ax + b)n— order of the generator pointh— cofactor (default: 1n)coord— coordinate system:CoordinateSystem.AFFINEorCoordinateSystem.JACOBIAN(default)
Validates that the discriminant 4a³ + 27b² ≢ 0 (mod p), ensuring the curve is non-singular.
Enum for selecting the internal arithmetic representation.
CoordinateSystem.AFFINE— standard (x, y) coordinates, uses modular inversion per operationCoordinateSystem.JACOBIAN— projective (X, Y, Z) coordinates where x = X/Z², y = Y/Z³, ~3x faster
Look up standard curve parameters and generator points by name.
const { getCurve, getGenerator } = require('js-ecutils')
const curve = getCurve('secp256k1') // returns CurveParams
const G = getGenerator('secp256k1') // returns Point (generator)Supported curves: secp192k1, secp192r1, secp224k1, secp224r1, secp256k1, secp256r1, secp384r1, secp521r1
Lookup is case-insensitive. Throws an error for unknown curve names.
ECDSA signature generation and verification.
const { DigitalSignature } = require('js-ecutils')
const ds = new DigitalSignature(123456n, 'secp256k1')Constructor: new DigitalSignature(privateKey, curveName = 'secp256k1')
Properties:
publicKey— the public key point Q = d·G
Methods:
sign(messageHash)— generates an ECDSA signature[r, s]for an integer hashverify(publicKey, messageHash, r, s)— verifies a signature, returnsbooleansignMessage(message, hashFunc?)— hashes a message (SHA-256 by default) and signs it, returnsPromise<[r, s]>verifyMessage(publicKey, message, r, s, hashFunc?)— hashes and verifies, returnsPromise<boolean>
The nonce k is generated using a cryptographically secure random number generator (crypto.getRandomValues()).
Encode/decode text messages as elliptic curve points using Koblitz's method.
const { Koblitz } = require('js-ecutils')
const kob = new Koblitz('secp521r1')
const [point, j] = kob.encode('Hello, world!')
const text = kob.decode(point, j)
// text === 'Hello, world!'Constructor: new Koblitz(curveName = 'secp521r1', alphabetSize = 256n)
curveName— larger curves can encode longer messages in a single pointalphabetSize— 256n for ASCII, 65536n for Unicode
Methods:
encode(message)— returns[Point, j]where j is an auxiliary value needed for decodingdecode(point, j)— recovers the original text string
Elliptic Curve Diffie-Hellman (ECDH) key exchange.
const { DiffieHellman } = require('js-ecutils')
const alice = new DiffieHellman(0xAn, 'secp256k1')
const bob = new DiffieHellman(0xBn, 'secp256k1')
const sharedA = alice.computeSharedSecret(bob.publicKey)
const sharedB = bob.computeSharedSecret(alice.publicKey)
// sharedA.x === sharedB.x && sharedA.y === sharedB.yConstructor: new DiffieHellman(privateKey, curveName = 'secp256k1')
Properties:
publicKey— H = d·G
Methods:
computeSharedSecret(otherPublicKey)— returns the shared secret point S = d·Q_other
Massey-Omura three-pass protocol for key-free message exchange.
const { MasseyOmura, Koblitz } = require('js-ecutils')
const kob = new Koblitz('secp521r1')
const alice = new MasseyOmura(0xA1n, 'secp521r1')
const bob = new MasseyOmura(0xB2n, 'secp521r1')
const [M, j] = kob.encode('Secret message')
const c1 = alice.encrypt(M) // Alice → Bob
const c2 = bob.encrypt(c1) // Bob → Alice
const c3 = alice.decrypt(c2) // Alice → Bob
const plaintext = bob.decrypt(c3) // Bob recovers M
const text = kob.decode(plaintext, j)
// text === 'Secret message'Constructor: new MasseyOmura(privateKey, curveName = 'secp521r1')
privateKeymust be coprime with n (so that e⁻¹ mod n exists)
Methods:
encrypt(point)— C = e·P (multiply by private key)decrypt(point)— P = e⁻¹·C (multiply by modular inverse)
Tests whether a is a quadratic residue modulo prime p using Euler's criterion: a^((p-1)/2) ≡ 1 (mod p).
Computes √a mod p. Uses the direct formula when p ≡ 3 (mod 4), otherwise falls back to the Tonelli-Shanks algorithm. Returns null if a is not a quadratic residue.
const { Point, CurveParams, getGenerator } = require('js-ecutils')
// Toy curve: y² = x³ + x + 1 over F₂₃ (order 28)
const curve = new CurveParams({ p: 23n, a: 1n, b: 1n, n: 28n, h: 1n })
const P = new Point(0n, 1n, curve)
// Doubling: 2·P
const P2 = P.mul(2n)
console.log(`2·P = (${P2.x}, ${P2.y})`) // (6, 19)
// Group order: 28·P = O (identity)
const identity = P.mul(28n)
console.log(`28·P is identity: ${identity.isIdentity}`) // true
// Negation: -P = (x, p - y)
const neg = P.neg()
console.log(`-P = (${neg.x}, ${neg.y})`) // (0, 22)const { getGenerator, Point, getCurve } = require('js-ecutils')
const G = getGenerator('secp256k1')
// Compress to SEC 1 format (33 bytes for 256-bit curve)
const compressed = G.compressSec1()
console.log(`Compressed: ${compressed[0] === 0x02 ? '02' : '03'}...`)
// Decompress back
const curve = getCurve('secp256k1')
const recovered = Point.fromSec1(compressed, curve)
console.log(`Match: ${recovered.x === G.x && recovered.y === G.y}`) // trueconst { DigitalSignature } = require('js-ecutils')
const ds = new DigitalSignature(123456n, 'secp256k1')
// Sign a raw hash
const hash = 0xDEADBEEFn
const [r, s] = ds.sign(hash)
console.log(`Valid: ${ds.verify(ds.publicKey, hash, r, s)}`) // true
// Sign a message string (async, uses SHA-256)
async function demo() {
const [r, s] = await ds.signMessage('Hello, ECDSA!')
const valid = await ds.verifyMessage(ds.publicKey, 'Hello, ECDSA!', r, s)
console.log(`Message signature valid: ${valid}`) // true
}
demo()const { Koblitz } = require('js-ecutils')
const kob = new Koblitz('secp521r1')
const [point, j] = kob.encode('Hello, EC!')
const decoded = kob.decode(point, j)
console.log(decoded) // 'Hello, EC!'const { DiffieHellman } = require('js-ecutils')
const alice = new DiffieHellman(12345n, 'secp256k1')
const bob = new DiffieHellman(67890n, 'secp256k1')
const sharedA = alice.computeSharedSecret(bob.publicKey)
const sharedB = bob.computeSharedSecret(alice.publicKey)
console.log(`Shared secrets match: ${sharedA.x === sharedB.x}`) // trueconst { Koblitz, MasseyOmura } = require('js-ecutils')
const kob = new Koblitz('secp521r1')
const alice = new MasseyOmura(70604135n, 'secp521r1')
const bob = new MasseyOmura(48239108668n, 'secp521r1')
// Encode the message as a curve point
const [M, j] = kob.encode('Hello, world!')
// Three-pass exchange
const c1 = alice.encrypt(M) // Step 1: Alice encrypts
const c2 = bob.encrypt(c1) // Step 2: Bob encrypts
const c3 = alice.decrypt(c2) // Step 3: Alice removes her encryption
const recovered = bob.decrypt(c3) // Step 4: Bob removes his encryption
const text = kob.decode(recovered, j)
console.log(text) // 'Hello, world!'Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
This project is licensed under the MIT License. See the LICENSE file for details.
- Python —
ecutilson PyPI - Go —
go-ecutilson GitHub