From cd217b2d026f152eecc702ba9cc83294167a8274 Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Tue, 13 Jan 2015 16:21:11 +0300 Subject: [PATCH] ECIES (Browser) --- browser.js | 118 ++++++++++++++++++++++++++++++++++++++++++++++++--- index.js | 2 +- package.json | 3 +- test.js | 109 +++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 221 insertions(+), 11 deletions(-) diff --git a/browser.js b/browser.js index 1452843..1663ddf 100644 --- a/browser.js +++ b/browser.js @@ -13,6 +13,8 @@ var EC = require("elliptic").ec; var ec = new EC("secp256k1"); +// TODO(Kagami): Try to support IE11. +var subtle = window.crypto.subtle || window.crypto.webkitSubtle; function assert(condition, message) { if (!condition) { @@ -20,9 +22,54 @@ function assert(condition, message) { } } -exports.getPublic = function(privateKey) { +function randomBytes(size) { + var arr = new Uint8Array(size); + window.crypto.getRandomValues(arr); + return new Buffer(arr); +} + +function sha512(msg) { + return subtle.digest({name: "SHA-512"}, msg).then(function(hash) { + return new Buffer(new Uint8Array(hash)); + }); +} + +function getAes(op) { + return function(iv, key, msg) { + 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)); + }); + }; +} + +var aesCbcEncrypt = getAes("encrypt"); +var aesCbcDecrypt = getAes("decrypt"); + +function hmacSha256Sign(key, msg) { + var algorithm = {name: "HMAC", hash: {name: "SHA-256"}}; + var keyp = subtle.importKey("raw", key, algorithm, false, ["sign"]); + return keyp.then(function(cryptoKey) { + return subtle.sign(algorithm, cryptoKey, msg); + }).then(function(sig) { + return new Buffer(new Uint8Array(sig)); + }); +} + +function hmacSha256Verify(key, msg, sig) { + var algorithm = {name: "HMAC", hash: {name: "SHA-256"}}; + var keyp = subtle.importKey("raw", key, algorithm, false, ["verify"]); + return keyp.then(function(cryptoKey) { + return subtle.verify(algorithm, cryptoKey, sig, msg); + }); +} + +var getPublic = exports.getPublic = function(privateKey) { // This function has sync API so we throw an error immediately. - // (`elliptic` doesn't do this). assert(privateKey.length === 32, "Bad private key"); // XXX(Kagami): `elliptic.utils.encode` returns array for every // encoding except `hex`. @@ -31,23 +78,82 @@ exports.getPublic = function(privateKey) { exports.sign = function(privateKey, msg) { return new Promise(function(resolve) { + assert(privateKey.length === 32, "Bad private key"); var key = ec.keyPair(privateKey); resolve(new Buffer(key.sign(msg).toDER())); }); }; -exports.verify = function(key, msg, sig) { +exports.verify = function(publicKey, msg, sig) { return new Promise(function(resolve, reject) { - key = ec.keyPair(key); + assert(publicKey.length === 65, "Bad public key"); + assert(publicKey[0] === 4, "Bad public key"); + var key = ec.keyPair(publicKey); return key.verify(msg, sig) ? resolve() : reject(); }); }; -exports.derive = function(privateKeyA, publicKeyB) { +var derive = exports.derive = function(privateKeyA, publicKeyB) { return new Promise(function(resolve) { + assert(privateKeyA.length === 32, "Bad private key"); + assert(publicKeyB.length === 65, "Bad public key"); + assert(publicKeyB[0] === 4, "Bad public key"); var keyA = ec.keyPair(privateKeyA); var keyB = ec.keyPair(publicKeyB); var Px = keyA.derive(keyB.getPublic()); // BN instance - resolve(new Buffer(Px.toString(16), "hex")); + resolve(new Buffer(Px.toString(16, 2), "hex")); + }); +}; + +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; + return new Promise(function(resolve) { + var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); + ephemPublicKey = getPublic(ephemPrivateKey); + resolve(derive(ephemPrivateKey, publicKeyTo)); + }).then(function(Px) { + return sha512(Px); + }).then(function(hash) { + iv = opts.iv || randomBytes(16); + 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]); + return hmacSha256Sign(macKey, dataToMac); + }).then(function(mac) { + return { + iv: iv, + ephemPublicKey: ephemPublicKey, + cipherText: cipherText, + mac: mac, + }; + }); +}; + +exports.decrypt = function(privateKey, opts) { + assert(subtle, "WebCryptoAPI is not supported"); + // Tmp variables to save context from flat promises; + var encryptionKey; + return derive(privateKey, opts.ephemPublicKey).then(function(Px) { + return sha512(Px); + }).then(function(hash) { + encryptionKey = hash.slice(0, 32); + var macKey = hash.slice(32); + var dataToMac = Buffer.concat([ + opts.iv, + opts.ephemPublicKey, + opts.cipherText + ]); + return hmacSha256Verify(macKey, dataToMac, opts.mac); + }).then(function(goodMac) { + assert(goodMac, "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 176d91d..c3ece93 100644 --- a/index.js +++ b/index.js @@ -16,7 +16,7 @@ var secp256k1 = require("secp256k1"); * @return {Buffer} A 65-byte public key. * @function */ -var getPublic = exports.getPublic = secp256k1.createPublicKey; +exports.getPublic = secp256k1.createPublicKey; /** * Create an ECDSA signature. diff --git a/package.json b/package.json index 943ef16..c83a22c 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ }, "homepage": "https://github.com/bitchan/eccrypto", "devDependencies": { + "buffer-equal": "~0.0.1", "chai": "*", "jshint": "*", "karma": "^0.12.28", @@ -46,7 +47,7 @@ "mocha": "*" }, "dependencies": { - "elliptic": "^1.0.0", + "elliptic": "^1.0.1", "es6-promise": "^2.0.1", "secp256k1": "~0.0.13" } diff --git a/test.js b/test.js index 4419354..3f4161b 100644 --- a/test.js +++ b/test.js @@ -1,5 +1,6 @@ var expect = require("chai").expect; var crypto = require("crypto"); +var bufferEqual = require("buffer-equal"); var eccrypto = require("./"); var msg = crypto.createHash("sha256").update("test").digest(); @@ -48,15 +49,22 @@ describe("ECDSA", function() { }); it("should reject promise on invalid key when signing", function(done) { - eccrypto.sign(Buffer("test"), msg).catch(function() { - done(); + var k4 = Buffer("test"); + var k192 = Buffer("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "hex"); + var k384 = Buffer("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "hex"); + eccrypto.sign(k4, msg).catch(function() { + eccrypto.sign(k192, msg).catch(function() { + eccrypto.sign(k384, msg).catch(function() { + done(); + }); + }); }); }); it("should reject promise on invalid key when verifying", function(done) { eccrypto.sign(privateKey, msg).then(function(sig) { expect(Buffer.isBuffer(sig)).to.be.true; - eccrypto.verify(Buffer("test"), msg, sig).catch(function() { + eccrypto.verify(Buffer("test"), msg, sig).catch(function(e) { done(); }); }); @@ -74,6 +82,7 @@ describe("ECDSA", function() { }); if (typeof window !== "undefined") { + describe("ECDH", function() { it("should derive shared secret from privkey A and pubkey B", function() { return eccrypto.derive(privateKeyA, publicKeyB).then(function(Px) { @@ -87,5 +96,99 @@ describe("ECDH", function() { }); }); }); + + it("should reject promise on bad keys", function(done) { + eccrypto.derive(Buffer("test"), publicKeyB).catch(function() { + eccrypto.derive(publicKeyB, publicKeyB).catch(function() { + eccrypto.derive(privateKeyA, privateKeyA).catch(function() { + eccrypto.derive(privateKeyB, Buffer("test")).catch(function() { + done(); + }); + }); + }); + }); + }); }); + +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 mac = Buffer("dbb14a9b53dbd6b763dba24dc99520f570cdf8095a8571db4bf501b535fda1ed", "hex"); + var encOpts = {ephemPrivateKey: ephemPrivateKey, iv: iv}; + 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; + }); + }); + + it("should decrypt", function() { + return eccrypto.decrypt(privateKeyB, decOpts) + .then(function(msg) { + expect(msg.toString()).to.equal("test"); + }); + }); + + it("should encrypt and decrypt", function() { + return eccrypto.encrypt(publicKeyA, Buffer("b to a")).then(function(res) { + return eccrypto.decrypt(privateKeyA, res); + }).then(function(msg) { + expect(msg.toString()).to.equal("b 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() { + 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() { + 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() { + 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() { + 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(); + }); + }); + }); +}); + }