Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 3 additions & 193 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,203 +5,13 @@

"use strict";

var promise = typeof Promise === "undefined" ?
require("es6-promise").Promise :
Promise;
var crypto = require("crypto");
// try to use secp256k1, fallback to browser implementation
// try to use Node.js-specific dependencies, fallback to browser implementation
try {
var secp256k1 = require("secp256k1");
var ecdh = require("./build/Release/ecdh");
module.exports = require("./node");
} catch (e) {
if (process.env.ECCRYPTO_NO_FALLBACK) {
throw e;
} else {
return (module.exports = require("./browser"));
module.exports = require("./browser");
}
}

function assert(condition, message) {
if (!condition) {
throw new Error(message || "Assertion failed");
}
}

function sha512(msg) {
return crypto.createHash("sha512").update(msg).digest();
}

function aes256CbcEncrypt(iv, key, plaintext) {
var cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
var firstChunk = cipher.update(plaintext);
var secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
}

function aes256CbcDecrypt(iv, key, ciphertext) {
var cipher = crypto.createDecipheriv("aes-256-cbc", key, iv);
var firstChunk = cipher.update(ciphertext);
var secondChunk = cipher.final();
return Buffer.concat([firstChunk, secondChunk]);
}

function hmacSha256(key, msg) {
return crypto.createHmac("sha256", key).update(msg).digest();
}

// Compare two buffers in constant time to prevent timing attacks.
function equalConstTime(b1, b2) {
if (b1.length !== b2.length) {
return false;
}
var res = 0;
for (var i = 0; i < b1.length; i++) {
res |= b1[i] ^ b2[i]; // jshint ignore:line
}
return res === 0;
}

function pad32(msg){
var buf;
if (msg.length < 32) {
buf = new Buffer(32);
buf.fill(0);
msg.copy(buf, 32 - msg.length);
return buf;
} else {
return msg;
}
}

/**
* 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
*/
var getPublic = exports.getPublic = function(privateKey) {
assert(privateKey.length === 32, "Bad private key");
// See https://github.com/wanderer/secp256k1-node/issues/46
var compressed = secp256k1.publicKeyCreate(privateKey);
return secp256k1.publicKeyConvert(compressed, false);
};

/**
* 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.
*/
exports.sign = function(privateKey, msg) {
return new promise(function(resolve) {
assert(msg.length > 0, "Message should not be empty");
assert(msg.length <= 32, "Message is too long");
msg = pad32(msg);
var sig = secp256k1.signSync(msg, privateKey).signature;
resolve(secp256k1.signatureExport(sig));
});
};

/**
* 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.
*/
exports.verify = function(publicKey, msg, sig) {
return new promise(function(resolve, reject) {
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.verifySync(msg, sig, publicKey)) {
resolve(null);
} else {
reject(new Error("Bad signature"));
}
});
};

/**
* 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>} A promise that resolves with the derived
* shared secret (Px, 32 bytes) and rejects on bad key.
*/
var derive = exports.derive = function(privateKeyA, publicKeyB) {
return new promise(function(resolve) {
resolve(ecdh.derive(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 {Promise.<Ecies>} - A promise that resolves with the ECIES
* structure on successful encryption and rejects on failure.
*/
exports.encrypt = function(publicKeyTo, msg, opts) {
opts = opts || {};
// Tmp variable to save context from flat promises;
var ephemPublicKey;
return new promise(function(resolve) {
var ephemPrivateKey = opts.ephemPrivateKey || crypto.randomBytes(32);
ephemPublicKey = getPublic(ephemPrivateKey);
resolve(derive(ephemPrivateKey, publicKeyTo));
}).then(function(Px) {
var hash = sha512(Px);
var iv = opts.iv || crypto.randomBytes(16);
var encryptionKey = hash.slice(0, 32);
var macKey = hash.slice(32);
var ciphertext = aes256CbcEncrypt(iv, encryptionKey, msg);
var dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]);
var mac = hmacSha256(macKey, dataToMac);
return {
iv: iv,
ephemPublicKey: ephemPublicKey,
ciphertext: ciphertext,
mac: mac,
};
});
};

/**
* 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.
*/
exports.decrypt = function(privateKey, opts) {
return derive(privateKey, opts.ephemPublicKey).then(function(Px) {
var hash = sha512(Px);
var encryptionKey = hash.slice(0, 32);
var macKey = hash.slice(32);
var dataToMac = Buffer.concat([
opts.iv,
opts.ephemPublicKey,
opts.ciphertext
]);
var realMac = hmacSha256(macKey, dataToMac);
assert(equalConstTime(opts.mac, realMac), "Bad MAC");
return aes256CbcDecrypt(opts.iv, encryptionKey, opts.ciphertext);
});
};
Loading