From 2157a2f0d4040f6a7298c0a398a221dd0fdea5f9 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Sat, 24 Mar 2018 18:52:32 +0300 Subject: [PATCH 1/2] Avoid using `return` outside of a function Node.js allows this due to the way globals are implemented there (it wraps all modules into a function), but it's not a correct JavaScript. One of the side-effects of this is that bundlers like nexe/nexe don't work with applications that use eccrypto, as these bundlers use Acorn to parse all sources into AST and then combine them into a single JS file, and Acorn chokes on out-of-function return. --- index.js | 196 +----------------------------------------------------- node.js | 198 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 193 deletions(-) create mode 100644 node.js diff --git a/index.js b/index.js index 8d5fcf5..8f05944 100644 --- a/index.js +++ b/index.js @@ -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.} 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.} 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.} 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.} - 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.} - 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); - }); -}; diff --git a/node.js b/node.js new file mode 100644 index 0000000..f821e20 --- /dev/null +++ b/node.js @@ -0,0 +1,198 @@ +/** + * Node.js eccrypto implementation. + */ + +"use strict"; + +var secp256k1 = require("secp256k1"); +var ecdh = require("./build/Release/ecdh"); + +var promise = typeof Promise === "undefined" ? + require("es6-promise").Promise : + Promise; +var crypto = require("crypto"); + +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.} 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.} 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.} 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.} - 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.} - 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); + }); +}; From 2bb889b5056330b59637974777386063dfddb1e9 Mon Sep 17 00:00:00 2001 From: Sam Kozin Date: Sun, 25 Mar 2018 21:51:23 +0300 Subject: [PATCH 2/2] Use ".node" extension when importing native module This allow bundlers like nexe/nexe detect this as a native module import and process it accordingly. --- node.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node.js b/node.js index f821e20..494f7d3 100644 --- a/node.js +++ b/node.js @@ -5,7 +5,7 @@ "use strict"; var secp256k1 = require("secp256k1"); -var ecdh = require("./build/Release/ecdh"); +var ecdh = require("./build/Release/ecdh.node"); var promise = typeof Promise === "undefined" ? require("es6-promise").Promise :