standard-ecies
Version:
Standard ECIES implemention for NodeJS based on crypto module with no other dependencies.
189 lines (168 loc) • 5.15 kB
JavaScript
// Implemention of ECIES specified in https://en.wikipedia.org/wiki/Integrated_Encryption_Scheme
;
const crypto = require('crypto');
const assert = require('assert');
const empty_buffer = Buffer.allocUnsafe ? Buffer.allocUnsafe(0) : new Buffer([]);
// E
function symmetricEncrypt(cypherName, iv, key, plaintext) {
let cipher;
if (iv === undefined) {
cipher = crypto.createCipher(cypherName, key);
}
else {
if (iv == null) {
// to support node 6.x
iv = empty_buffer;
}
cipher = crypto.createCipheriv(cypherName, key, iv);
}
const firstChunk = cipher.update(plaintext);
const secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
}
// E-1
function symmetricDecrypt(cypherName, iv, key, ciphertext) {
let cipher;
if (iv === undefined) {
cipher = crypto.createDecipher(cypherName, key);
}
else {
if (iv == null) {
// to support node 6.x
iv = empty_buffer;
}
cipher = crypto.createDecipheriv(cypherName, key, iv);
}
const firstChunk = cipher.update(ciphertext);
const secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
}
// KDF
function hashMessage(cypherName, message) {
return crypto.createHash(cypherName).update(message).digest();
}
// MAC
function macMessage(cypherName, key, message) {
return crypto.createHmac(cypherName, key).update(message).digest();
}
// Compare two buffers in constant time to prevent timing attacks.
function equalConstTime(b1, b2) {
if (b1.length !== b2.length) {
return false;
}
let result = 0;
for (let i = 0; i < b1.length; i++) {
result |= b1[i] ^ b2[i]; // jshint ignore:line
}
return result === 0;
}
function makeUpOptions(options) {
options = options || {};
if (options.hashName == undefined) {
options.hashName = 'sha256';
}
if (options.hashLength == undefined) {
options.hashLength = hashMessage(options.hashName, '').length;
}
if (options.macName == undefined) {
options.macName = 'sha256';
}
if (options.macLength == undefined) {
options.macLength = macMessage(options.hashName, '', '').length;
}
if (options.curveName == undefined) {
options.curveName = 'secp256k1';
}
if (options.symmetricCypherName == undefined) {
options.symmetricCypherName = 'aes-128-ecb';
// use options.iv to determine is the cypher in ecb mode
options.iv = empty_buffer;
}
if (options.keyFormat == undefined) {
options.keyFormat = 'uncompressed';
}
// S1 (optional shared information1)
if (options.s1 == undefined) {
options.s1 = empty_buffer;
}
// S2 (optional shared information2)
if (options.s2 == undefined) {
options.s2 = empty_buffer;
}
return options;
}
exports.encrypt = function (publicKey, message, options) {
options = makeUpOptions(options);
const ecdh = crypto.createECDH(options.curveName);
// R
const R = ecdh.generateKeys(null, options.keyFormat);
// S
const sharedSecret = ecdh.computeSecret(publicKey);
// uses KDF to derive a symmetric encryption and a MAC keys:
// Ke || Km = KDF(S || S1)
const hash = hashMessage(
options.hashName,
Buffer.concat(
[sharedSecret, options.s1],
sharedSecret.length + options.s1.length
)
);
// Ke
const encryptionKey = hash.slice(0, hash.length / 2);
// Km
const macKey = hash.slice(hash.length / 2);
// encrypts the message:
// c = E(Ke; m);
const cipherText = symmetricEncrypt(options.symmetricCypherName, options.iv, encryptionKey, message);
// computes the tag of encrypted message and S2:
// d = MAC(Km; c || S2)
const tag = macMessage(
options.macName,
macKey,
Buffer.concat(
[cipherText, options.s2],
cipherText.length + options.s2.length
)
);
// outputs R || c || d
return Buffer.concat([R, cipherText, tag]);
};
exports.decrypt = function (ecdh, message, options) {
options = makeUpOptions(options);
const publicKeyLength = ecdh.getPublicKey(null, options.keyFormat).length;
// R
const R = message.slice(0, publicKeyLength);
// c
const cipherText = message.slice(publicKeyLength, message.length - options.macLength);
// d
const messageTag = message.slice(message.length - options.macLength);
// S
const sharedSecret = ecdh.computeSecret(R);
// derives keys the same way as Alice did:
// Ke || Km = KDF(S || S1)
const hash = hashMessage(
options.hashName,
Buffer.concat(
[sharedSecret, options.s1],
sharedSecret.length + options.s1.length
)
);
// Ke
const encryptionKey = hash.slice(0, hash.length / 2);
// Km
const macKey = hash.slice(hash.length / 2);
// uses MAC to check the tag
const keyTag = macMessage(
options.macName,
macKey,
Buffer.concat(
[cipherText, options.s2],
cipherText.length + options.s2.length
)
);
// outputs failed if d != MAC(Km; c || S2);
assert(equalConstTime(messageTag, keyTag), "Bad MAC");
// uses symmetric encryption scheme to decrypt the message
// m = E-1(Ke; c)
return symmetricDecrypt(options.symmetricCypherName, options.iv, encryptionKey, cipherText);
}