From 1cf476250a56668e97123d1352886170e797537a Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Wed, 14 Jan 2015 02:29:39 +0300 Subject: [PATCH] ECIES (Node) --- README.md | 23 +++++++++++ browser.js | 28 ++++++------- index.js | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++--- test.js | 61 +++++++++++++-------------- 4 files changed, 182 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 2f05c4a..4d77658 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,29 @@ eccrypto.derive(privateKeyA, publicKeyB).then(function(sharedKey1) { ### ECIES ```js +var crypto = require("crypto"); +var eccrypto = require("eccrypto"); + +var privateKeyA = crypto.randomBytes(32); +var publicKeyA = eccrypto.getPublic(privateKeyA); +var privateKeyB = crypto.randomBytes(32); +var publicKeyB = eccrypto.getPublic(privateKeyB); + +// Encrypting the message for B. +eccrypto.encrypt(publicKeyB, Buffer("msg to b")).then(function(encrypted) { + // B decrypting the message. + eccrypto.decrypt(privateKeyB, encrypted).then(function(plaintext) { + console.log("Message to part B:", plaintext.toString()); + }); +}); + +// Encrypting the message for A. +eccrypto.encrypt(publicKeyA, Buffer("msg to a")).then(function(encrypted) { + // A decrypting the message. + eccrypto.decrypt(privateKeyA, encrypted).then(function(plaintext) { + console.log("Message to part A:", plaintext.toString()); + }); +}); ``` ## License diff --git a/browser.js b/browser.js index 1663ddf..e3a0f40 100644 --- a/browser.js +++ b/browser.js @@ -35,14 +35,14 @@ function sha512(msg) { } function getAes(op) { - return function(iv, key, msg) { + return function(iv, key, data) { var importAlgorithm = {name: "AES-CBC"}; var keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]); return keyp.then(function(cryptoKey) { var encAlgorithm = {name: "AES-CBC", iv: iv}; - return subtle[op](encAlgorithm, cryptoKey, msg); - }).then(function(cipherText) { - return new Buffer(new Uint8Array(cipherText)); + return subtle[op](encAlgorithm, cryptoKey, data); + }).then(function(result) { + return new Buffer(new Uint8Array(result)); }); }; } @@ -109,7 +109,7 @@ exports.encrypt = function(publicKeyTo, msg, opts) { assert(subtle, "WebCryptoAPI is not supported"); opts = opts || {}; // Tmp variables to save context from flat promises; - var iv, ephemPublicKey, cipherText, macKey; + var iv, ephemPublicKey, ciphertext, macKey; return new Promise(function(resolve) { var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); ephemPublicKey = getPublic(ephemPrivateKey); @@ -121,15 +121,15 @@ exports.encrypt = function(publicKeyTo, msg, opts) { var encryptionKey = hash.slice(0, 32); macKey = hash.slice(32); return aesCbcEncrypt(iv, encryptionKey, msg); - }).then(function(encrypted) { - cipherText = encrypted; - var dataToMac = Buffer.concat([iv, ephemPublicKey, cipherText]); + }).then(function(data) { + ciphertext = data; + var dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]); return hmacSha256Sign(macKey, dataToMac); }).then(function(mac) { return { iv: iv, ephemPublicKey: ephemPublicKey, - cipherText: cipherText, + ciphertext: ciphertext, mac: mac, }; }); @@ -137,7 +137,7 @@ exports.encrypt = function(publicKeyTo, msg, opts) { exports.decrypt = function(privateKey, opts) { assert(subtle, "WebCryptoAPI is not supported"); - // Tmp variables to save context from flat promises; + // Tmp variable to save context from flat promises; var encryptionKey; return derive(privateKey, opts.ephemPublicKey).then(function(Px) { return sha512(Px); @@ -147,12 +147,12 @@ exports.decrypt = function(privateKey, opts) { var dataToMac = Buffer.concat([ opts.iv, opts.ephemPublicKey, - opts.cipherText + opts.ciphertext ]); return hmacSha256Verify(macKey, dataToMac, opts.mac); - }).then(function(goodMac) { - assert(goodMac, "Bad MAC"); - return aesCbcDecrypt(opts.iv, encryptionKey, opts.cipherText); + }).then(function(macGood) { + assert(macGood, "Bad MAC"); + return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext); }).then(function(msg) { return new Buffer(new Uint8Array(msg)); }); diff --git a/index.js b/index.js index 2dce2e0..8d9191b 100644 --- a/index.js +++ b/index.js @@ -8,18 +8,59 @@ var promise = typeof Promise === "undefined" ? require("es6-promise").Promise : Promise; +var crypto = require("crypto"); // TODO(Kagami): We may fallback to pure JS implementation // (`browser.js`) if this modules are failed to load. var secp256k1 = require("secp256k1"); var ecdh = require("./build/Release/ecdh"); +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; +} + /** * 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 */ -exports.getPublic = secp256k1.createPublicKey; +var getPublic = exports.getPublic = secp256k1.createPublicKey; /** * Create an ECDSA signature. @@ -50,13 +91,81 @@ exports.verify = function(publicKey, msg, sig) { /** * Derive shared secret for given private and public keys. - * @param {Buffer} privateKeyA - Sender's private key - * @param {Buffer} publicKeyB - Recipient's public key + * @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) and rejects on bad key. + * shared secret (Px, 32 bytes) and rejects on bad key. */ -exports.derive = function(privateKeyA, publicKeyB) { +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/test.js b/test.js index 2098cb9..c9f9153 100644 --- a/test.js +++ b/test.js @@ -108,26 +108,24 @@ describe("ECDH", function() { }); }); -if (typeof window !== "undefined") { - describe("ECIES", function() { var ephemPrivateKey = Buffer(32); ephemPrivateKey.fill(4); var ephemPublicKey = eccrypto.getPublic(ephemPrivateKey); var iv = Buffer(16); iv.fill(5); - var cipherText = Buffer("bbf3f0e7486b552b0e2ba9c4ca8c4579", "hex"); + var ciphertext = Buffer("bbf3f0e7486b552b0e2ba9c4ca8c4579", "hex"); var mac = Buffer("dbb14a9b53dbd6b763dba24dc99520f570cdf8095a8571db4bf501b535fda1ed", "hex"); var encOpts = {ephemPrivateKey: ephemPrivateKey, iv: iv}; - var decOpts = {iv: iv, ephemPublicKey: ephemPublicKey, cipherText: cipherText, mac: mac}; + var decOpts = {iv: iv, ephemPublicKey: ephemPublicKey, ciphertext: ciphertext, mac: mac}; it("should encrypt", function() { return eccrypto.encrypt(publicKeyB, Buffer("test"), encOpts) - .then(function(res) { - expect(bufferEqual(res.iv, iv)).to.be.true; - expect(bufferEqual(res.ephemPublicKey, ephemPublicKey)).to.be.true; - expect(bufferEqual(res.cipherText, cipherText)).to.be.true; - expect(bufferEqual(res.mac, mac)).to.be.true; + .then(function(enc) { + expect(bufferEqual(enc.iv, iv)).to.be.true; + expect(bufferEqual(enc.ephemPublicKey, ephemPublicKey)).to.be.true; + expect(bufferEqual(enc.ciphertext, ciphertext)).to.be.true; + expect(bufferEqual(enc.mac, mac)).to.be.true; }); }); @@ -139,56 +137,59 @@ describe("ECIES", function() { }); it("should encrypt and decrypt", function() { - return eccrypto.encrypt(publicKeyA, Buffer("b to a")).then(function(res) { - return eccrypto.decrypt(privateKeyA, res); + return eccrypto.encrypt(publicKeyA, Buffer("to a")).then(function(enc) { + return eccrypto.decrypt(privateKeyA, enc); }).then(function(msg) { - expect(msg.toString()).to.equal("b to a"); + expect(msg.toString()).to.equal("to a"); }); }); it("should reject promise on bad private key when decrypting", function(done) { - eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { - eccrypto.decrypt(privateKeyB, res).catch(function() { + eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) { + eccrypto.decrypt(privateKeyB, enc).catch(function() { done(); }); }); }); it("should reject promise on bad IV when decrypting", function(done) { - eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { - res.iv[0] ^= 1; - eccrypto.decrypt(privateKeyA, res).catch(function() { + eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) { + enc.iv[0] ^= 1; + eccrypto.decrypt(privateKeyA, enc).catch(function() { done(); }); }); }); it("should reject promise on bad R when decrypting", function(done) { - eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { - res.ephemPublicKey[0] ^= 1; - eccrypto.decrypt(privateKeyA, res).catch(function() { + eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) { + enc.ephemPublicKey[0] ^= 1; + eccrypto.decrypt(privateKeyA, enc).catch(function() { done(); }); }); }); - it("should reject promise on bad cipher text when decrypting", function(done) { - eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { - res.cipherText[0] ^= 1; - eccrypto.decrypt(privateKeyA, res).catch(function() { + it("should reject promise on bad ciphertext when decrypting", function(done) { + eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) { + enc.ciphertext[0] ^= 1; + eccrypto.decrypt(privateKeyA, enc).catch(function() { done(); }); }); }); it("should reject promise on bad MAC when decrypting", function(done) { - eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { - res.mac[0] ^= 1; - eccrypto.decrypt(privateKeyA, res).catch(function() { - done(); + eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) { + var origMac = enc.mac; + enc.mac = mac.slice(1); + eccrypto.decrypt(privateKeyA, enc).catch(function() { + enc.mac = origMac; + enc.mac[10] ^= 1; + eccrypto.decrypt(privateKeyA, enc).catch(function() { + done(); + }); }); }); }); }); - -}