ECIES (Browser)

This commit is contained in:
Kagami Hiiragi 2015-01-13 16:21:11 +03:00
parent 4e3f857332
commit cd217b2d02
4 changed files with 221 additions and 11 deletions

View File

@ -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));
});
};

View File

@ -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.

View File

@ -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"
}

109
test.js
View File

@ -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();
});
});
});
});
}