@nlindley/eccrypto
Version:
JavaScript Elliptic curve cryptography library
336 lines (301 loc) • 10.5 kB
JavaScript
/**
* Node.js eccrypto implementation.
* @module eccrypto
*/
"use strict";
const EC_GROUP_ORDER = Buffer.from(
"fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141",
"hex"
);
const ZERO32 = Buffer.alloc(32, 0);
const crypto = require("crypto");
const ecdh = require("ecdh");
const secp256k1 = require("./secp256k1");
const isScalar = (x) => {
return Buffer.isBuffer(x) && x.length === 32;
};
const isValidPrivateKey = (privateKey) => {
if (!isScalar(privateKey)) {
return false;
}
return (
privateKey.compare(ZERO32) > 0 && // > 0
privateKey.compare(EC_GROUP_ORDER) < 0
); // < G
};
const assert = (condition, message) => {
if (!condition) {
throw new Error(message || "Assertion failed");
}
};
const sha512 = (msg) => {
return crypto.createHash("sha512").update(msg).digest();
};
const aes256CbcEncrypt = (iv, key, plaintext) => {
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
const firstChunk = cipher.update(plaintext);
const secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
};
const aes256CbcDecrypt = (iv, key, ciphertext) => {
const cipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
const firstChunk = cipher.update(ciphertext);
const secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
};
const hmacSha256 = (key, msg) => {
return crypto.createHmac("sha256", key).update(msg).digest();
};
// Compare two buffers in constant time to prevent timing attacks.
const equalConstTime = (b1, b2) => {
if (b1.length !== b2.length) {
return false;
}
let res = 0;
for (let i = 0; i < b1.length; i++) {
res |= b1[i] ^ b2[i]; // jshint ignore:line
}
return res === 0;
};
const pad32 = (msg) => {
if (msg.length < 32) {
const buf = Buffer.alloc(32);
buf.fill(0);
msg.copy(buf, 32 - msg.length);
return buf;
} else {
return msg;
}
};
/**
* Generate a new valid private key. Will use crypto.randomBytes as source.
* @return {Buffer} A 32-byte private key.
* @function
*/
const generatePrivate = () => {
let privateKey = crypto.randomBytes(32);
while (!isValidPrivateKey(privateKey)) {
privateKey = crypto.randomBytes(32);
}
return privateKey;
};
/**
* Compute the public key for a given private key.
* @param {Buffer} privateKey - A 32-byte private key
* @return {Buffer} A 65-byte public key.
* @function
*/
const getPublic = (privateKey) => {
assert(privateKey.length === 32, "Bad private key");
assert(isValidPrivateKey(privateKey), "Bad private key");
// See https://github.com/wanderer/secp256k1-node/issues/46
const compressed = secp256k1.publicKeyCreate(privateKey);
return Buffer.from(secp256k1.publicKeyConvert(compressed, false));
};
/**
* Get compressed version of public key.
*/
const getPublicCompressed = (privateKey) => {
// jshint ignore:line
assert(privateKey.length === 32, "Bad private key");
assert(isValidPrivateKey(privateKey), "Bad private key");
// See https://github.com/wanderer/secp256k1-node/issues/46
return Buffer.from(secp256k1.publicKeyCreate(privateKey));
};
/**
* Create an ECDSA signature.
* @param {Buffer} privateKey - A 32-byte private key
* @param {Buffer} msg - The message being signed
* @return {Buffer} A promise that resolves with the
* signature and rejects on bad key or message.
*/
const signSync = (privateKey, msg) => {
assert(privateKey.length === 32, "Bad private key");
assert(isValidPrivateKey(privateKey), "Bad private key");
assert(msg.length > 0, "Message should not be empty");
assert(msg.length <= 32, "Message is too long");
msg = pad32(msg);
const sig = secp256k1.ecdsaSign(msg, privateKey).signature;
return Buffer.from(secp256k1.signatureExport(sig));
};
/**
* Create an ECDSA signature.
* @param {Buffer} privateKey - A 32-byte private key
* @param {Buffer} msg - The message being signed
* @return {Promise.<Buffer>} A promise that resolves with the
* signature and rejects on bad key or message.
*/
const sign = async (privateKey, msg) => {
return signSync(privateKey, msg);
};
/**
* Verify an ECDSA signature.
* @param {Buffer} publicKey - A 65-byte public key
* @param {Buffer} msg - The message being verified
* @param {Buffer} sig - The signature
* @return {null} A promise that resolves on correct signature
* and rejects on bad key or signature.
*/
const verifySync = (publicKey, msg, sig) => {
assert(msg.length > 0, "Message should not be empty");
assert(msg.length <= 32, "Message is too long");
msg = pad32(msg);
sig = secp256k1.signatureImport(sig);
if (secp256k1.ecdsaVerify(sig, msg, publicKey)) {
return null;
} else {
throw new Error("Bad signature");
}
};
/**
* Verify an ECDSA signature.
* @param {Buffer} publicKey - A 65-byte public key
* @param {Buffer} msg - The message being verified
* @param {Buffer} sig - The signature
* @return {Promise.<null>} A promise that resolves on correct signature
* and rejects on bad key or signature.
*/
const verify = async (publicKey, msg, sig) => {
return verifySync(publicKey, msg, sig);
};
/**
* Test if the key is compressed or uncompressed.
* - An uncompressed key has a prefix byte of 0x04
* - A compressed key has a prefix byte of 0x02
*
* @param {Uint8Array} key
* @returns {boolean}
*/
const isCompressed = (key) => key[0] === 0x02;
/**
* Derive shared secret for given private and public keys.
* @param {Buffer} privateKeyA - Sender's private key (32 bytes)
* @param {Buffer} publicKeyB - Recipient's public key (65 bytes)
* @return {Buffer} The derived shared secret (Px, 32 bytes).
*/
const deriveSync = (privateKeyA, publicKeyB) => {
assert(privateKeyA.length === 32, "Bad private key");
assert(isValidPrivateKey(privateKeyA), "Bad private key");
const publicKey = isCompressed(publicKeyB)
? Buffer.from(secp256k1.publicKeyConvert(publicKeyB, false))
: publicKeyB;
const curve = ecdh.getCurve("secp256k1");
const privKey = ecdh.PrivateKey.fromBuffer(curve, privateKeyA);
const pubKey = ecdh.PublicKey.fromBuffer(curve, publicKey.subarray(1));
return privKey.deriveSharedSecret(pubKey);
};
/**
* Derive shared secret for given private and public keys.
* @param {Buffer} privateKeyA - Sender's private key (32 bytes)
* @param {Buffer} publicKeyB - Recipient's public key (65 bytes)
* @return {Promise.<Buffer>} The derived shared secret (Px, 32 bytes).
*/
const derive = async (privateKeyA, publicKeyB) => {
return deriveSync(privateKeyA, publicKeyB);
};
/**
* Input/output structure for ECIES operations.
* @typedef {Object} Ecies
* @property {Buffer} iv - Initialization vector (16 bytes)
* @property {Buffer} ephemPublicKey - Ephemeral public key (65 bytes)
* @property {Buffer} ciphertext - The result of encryption (variable size)
* @property {Buffer} mac - Message authentication code (32 bytes)
*/
/**
* Encrypt message for given recepient's public key.
* @param {Buffer} publicKeyTo - Recipient's public key (65 bytes)
* @param {Buffer} msg - The message being encrypted
* @param {{iv?: Buffer, ephemPrivateKey?: Buffer}} [opts] - You may also
* specify initialization vector (16 bytes) and ephemeral private key
* (32 bytes) to get deterministic results.
* @return {Ecies} - A promise that resolves with the ECIES
* structure on successful encryption and rejects on failure.
*/
const encryptSync = (publicKeyTo, msg, opts) => {
opts = opts || {};
// Tmp variable to save context from flat promises;
let ephemPublicKey;
let ephemPrivateKey = opts.ephemPrivateKey || crypto.randomBytes(32);
// There is a very unlikely possibility that it is not a valid key
while (!isValidPrivateKey(ephemPrivateKey)) {
ephemPrivateKey = opts.ephemPrivateKey || crypto.randomBytes(32);
}
ephemPublicKey = getPublic(ephemPrivateKey);
const px = deriveSync(ephemPrivateKey, publicKeyTo);
const hash = sha512(px);
const iv = opts.iv || crypto.randomBytes(16);
const encryptionKey = hash.subarray(0, 32);
const macKey = hash.subarray(32);
const ciphertext = aes256CbcEncrypt(iv, encryptionKey, msg);
const dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]);
const mac = hmacSha256(macKey, dataToMac);
return {
iv: iv,
ephemPublicKey: ephemPublicKey,
ciphertext: ciphertext,
mac: mac,
};
};
/**
* Encrypt message for given recepient's public key.
* @param {Buffer} publicKeyTo - Recipient's public key (65 bytes)
* @param {Buffer} msg - The message being encrypted
* @param {{iv?: Buffer, ephemPrivateKey?: Buffer}} [opts] - You may also
* specify initialization vector (16 bytes) and ephemeral private key
* (32 bytes) to get deterministic results.
* @return {Promise.<Ecies>} - A promise that resolves with the ECIES
* structure on successful encryption and rejects on failure.
*/
const encrypt = async (publicKeyTo, msg, opts) => {
return encryptSync(publicKeyTo, msg, opts);
};
/**
* Decrypt message using given private key.
* @param {Buffer} privateKey - A 32-byte private key of recepient of
* the mesage
* @param {Ecies} opts - ECIES structure (result of ECIES encryption)
* @return {Buffer} - A promise that resolves with the
* plaintext on successful decryption and rejects on failure.
*/
const decryptSync = (privateKey, opts) => {
assert(privateKey.length === 32, "Bad private key");
assert(isValidPrivateKey(privateKey), "Bad private key");
const px = deriveSync(privateKey, opts.ephemPublicKey);
const hash = sha512(px);
const encryptionKey = hash.subarray(0, 32);
const macKey = hash.subarray(32);
const dataToMac = Buffer.concat([
opts.iv,
opts.ephemPublicKey,
opts.ciphertext,
]);
const realMac = hmacSha256(macKey, dataToMac);
assert(equalConstTime(opts.mac, realMac), "Bad MAC");
return aes256CbcDecrypt(opts.iv, encryptionKey, opts.ciphertext);
};
/**
* Decrypt message using given private key.
* @param {Buffer} privateKey - A 32-byte private key of recepient of
* the mesage
* @param {Ecies} opts - ECIES structure (result of ECIES encryption)
* @return {Promise.<Buffer>} - A promise that resolves with the
* plaintext on successful decryption and rejects on failure.
*/
const decrypt = async (privateKey, opts) => {
return decryptSync(privateKey, opts);
};
module.exports = {
generatePrivate,
getPublic,
getPublicCompressed,
signSync,
sign,
verifySync,
verify,
deriveSync,
derive,
encryptSync,
encrypt,
decryptSync,
decrypt,
};