ECIES (Node)

This commit is contained in:
Kagami Hiiragi 2015-01-14 02:29:39 +03:00
parent 4e1001a842
commit 1cf476250a
4 changed files with 182 additions and 49 deletions

View File

@ -74,6 +74,29 @@ eccrypto.derive(privateKeyA, publicKeyB).then(function(sharedKey1) {
### ECIES ### ECIES
```js ```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 ## License

View File

@ -35,14 +35,14 @@ function sha512(msg) {
} }
function getAes(op) { function getAes(op) {
return function(iv, key, msg) { return function(iv, key, data) {
var importAlgorithm = {name: "AES-CBC"}; var importAlgorithm = {name: "AES-CBC"};
var keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]); var keyp = subtle.importKey("raw", key, importAlgorithm, false, [op]);
return keyp.then(function(cryptoKey) { return keyp.then(function(cryptoKey) {
var encAlgorithm = {name: "AES-CBC", iv: iv}; var encAlgorithm = {name: "AES-CBC", iv: iv};
return subtle[op](encAlgorithm, cryptoKey, msg); return subtle[op](encAlgorithm, cryptoKey, data);
}).then(function(cipherText) { }).then(function(result) {
return new Buffer(new Uint8Array(cipherText)); return new Buffer(new Uint8Array(result));
}); });
}; };
} }
@ -109,7 +109,7 @@ exports.encrypt = function(publicKeyTo, msg, opts) {
assert(subtle, "WebCryptoAPI is not supported"); assert(subtle, "WebCryptoAPI is not supported");
opts = opts || {}; opts = opts || {};
// Tmp variables to save context from flat promises; // Tmp variables to save context from flat promises;
var iv, ephemPublicKey, cipherText, macKey; var iv, ephemPublicKey, ciphertext, macKey;
return new Promise(function(resolve) { return new Promise(function(resolve) {
var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32); var ephemPrivateKey = opts.ephemPrivateKey || randomBytes(32);
ephemPublicKey = getPublic(ephemPrivateKey); ephemPublicKey = getPublic(ephemPrivateKey);
@ -121,15 +121,15 @@ exports.encrypt = function(publicKeyTo, msg, opts) {
var encryptionKey = hash.slice(0, 32); var encryptionKey = hash.slice(0, 32);
macKey = hash.slice(32); macKey = hash.slice(32);
return aesCbcEncrypt(iv, encryptionKey, msg); return aesCbcEncrypt(iv, encryptionKey, msg);
}).then(function(encrypted) { }).then(function(data) {
cipherText = encrypted; ciphertext = data;
var dataToMac = Buffer.concat([iv, ephemPublicKey, cipherText]); var dataToMac = Buffer.concat([iv, ephemPublicKey, ciphertext]);
return hmacSha256Sign(macKey, dataToMac); return hmacSha256Sign(macKey, dataToMac);
}).then(function(mac) { }).then(function(mac) {
return { return {
iv: iv, iv: iv,
ephemPublicKey: ephemPublicKey, ephemPublicKey: ephemPublicKey,
cipherText: cipherText, ciphertext: ciphertext,
mac: mac, mac: mac,
}; };
}); });
@ -137,7 +137,7 @@ exports.encrypt = function(publicKeyTo, msg, opts) {
exports.decrypt = function(privateKey, opts) { exports.decrypt = function(privateKey, opts) {
assert(subtle, "WebCryptoAPI is not supported"); assert(subtle, "WebCryptoAPI is not supported");
// Tmp variables to save context from flat promises; // Tmp variable to save context from flat promises;
var encryptionKey; var encryptionKey;
return derive(privateKey, opts.ephemPublicKey).then(function(Px) { return derive(privateKey, opts.ephemPublicKey).then(function(Px) {
return sha512(Px); return sha512(Px);
@ -147,12 +147,12 @@ exports.decrypt = function(privateKey, opts) {
var dataToMac = Buffer.concat([ var dataToMac = Buffer.concat([
opts.iv, opts.iv,
opts.ephemPublicKey, opts.ephemPublicKey,
opts.cipherText opts.ciphertext
]); ]);
return hmacSha256Verify(macKey, dataToMac, opts.mac); return hmacSha256Verify(macKey, dataToMac, opts.mac);
}).then(function(goodMac) { }).then(function(macGood) {
assert(goodMac, "Bad MAC"); assert(macGood, "Bad MAC");
return aesCbcDecrypt(opts.iv, encryptionKey, opts.cipherText); return aesCbcDecrypt(opts.iv, encryptionKey, opts.ciphertext);
}).then(function(msg) { }).then(function(msg) {
return new Buffer(new Uint8Array(msg)); return new Buffer(new Uint8Array(msg));
}); });

119
index.js
View File

@ -8,18 +8,59 @@
var promise = typeof Promise === "undefined" ? var promise = typeof Promise === "undefined" ?
require("es6-promise").Promise : require("es6-promise").Promise :
Promise; Promise;
var crypto = require("crypto");
// TODO(Kagami): We may fallback to pure JS implementation // TODO(Kagami): We may fallback to pure JS implementation
// (`browser.js`) if this modules are failed to load. // (`browser.js`) if this modules are failed to load.
var secp256k1 = require("secp256k1"); var secp256k1 = require("secp256k1");
var ecdh = require("./build/Release/ecdh"); 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. * Compute the public key for a given private key.
* @param {Buffer} privateKey - A 32-byte private key * @param {Buffer} privateKey - A 32-byte private key
* @return {Buffer} A 65-byte public key. * @return {Buffer} A 65-byte public key.
* @function * @function
*/ */
exports.getPublic = secp256k1.createPublicKey; var getPublic = exports.getPublic = secp256k1.createPublicKey;
/** /**
* Create an ECDSA signature. * Create an ECDSA signature.
@ -50,13 +91,81 @@ exports.verify = function(publicKey, msg, sig) {
/** /**
* Derive shared secret for given private and public keys. * Derive shared secret for given private and public keys.
* @param {Buffer} privateKeyA - Sender's private key * @param {Buffer} privateKeyA - Sender's private key (32 bytes)
* @param {Buffer} publicKeyB - Recipient's public key * @param {Buffer} publicKeyB - Recipient's public key (65 bytes)
* @return {Promise.<Buffer>} A promise that resolves with the derived * @return {Promise.<Buffer>} 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) { return new promise(function(resolve) {
resolve(ecdh.derive(privateKeyA, publicKeyB)); 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);
});
};

59
test.js
View File

@ -108,26 +108,24 @@ describe("ECDH", function() {
}); });
}); });
if (typeof window !== "undefined") {
describe("ECIES", function() { describe("ECIES", function() {
var ephemPrivateKey = Buffer(32); var ephemPrivateKey = Buffer(32);
ephemPrivateKey.fill(4); ephemPrivateKey.fill(4);
var ephemPublicKey = eccrypto.getPublic(ephemPrivateKey); var ephemPublicKey = eccrypto.getPublic(ephemPrivateKey);
var iv = Buffer(16); var iv = Buffer(16);
iv.fill(5); iv.fill(5);
var cipherText = Buffer("bbf3f0e7486b552b0e2ba9c4ca8c4579", "hex"); var ciphertext = Buffer("bbf3f0e7486b552b0e2ba9c4ca8c4579", "hex");
var mac = Buffer("dbb14a9b53dbd6b763dba24dc99520f570cdf8095a8571db4bf501b535fda1ed", "hex"); var mac = Buffer("dbb14a9b53dbd6b763dba24dc99520f570cdf8095a8571db4bf501b535fda1ed", "hex");
var encOpts = {ephemPrivateKey: ephemPrivateKey, iv: iv}; 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() { it("should encrypt", function() {
return eccrypto.encrypt(publicKeyB, Buffer("test"), encOpts) return eccrypto.encrypt(publicKeyB, Buffer("test"), encOpts)
.then(function(res) { .then(function(enc) {
expect(bufferEqual(res.iv, iv)).to.be.true; expect(bufferEqual(enc.iv, iv)).to.be.true;
expect(bufferEqual(res.ephemPublicKey, ephemPublicKey)).to.be.true; expect(bufferEqual(enc.ephemPublicKey, ephemPublicKey)).to.be.true;
expect(bufferEqual(res.cipherText, cipherText)).to.be.true; expect(bufferEqual(enc.ciphertext, ciphertext)).to.be.true;
expect(bufferEqual(res.mac, mac)).to.be.true; expect(bufferEqual(enc.mac, mac)).to.be.true;
}); });
}); });
@ -139,56 +137,59 @@ describe("ECIES", function() {
}); });
it("should encrypt and decrypt", function() { it("should encrypt and decrypt", function() {
return eccrypto.encrypt(publicKeyA, Buffer("b to a")).then(function(res) { return eccrypto.encrypt(publicKeyA, Buffer("to a")).then(function(enc) {
return eccrypto.decrypt(privateKeyA, res); return eccrypto.decrypt(privateKeyA, enc);
}).then(function(msg) { }).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) { it("should reject promise on bad private key when decrypting", function(done) {
eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) {
eccrypto.decrypt(privateKeyB, res).catch(function() { eccrypto.decrypt(privateKeyB, enc).catch(function() {
done(); done();
}); });
}); });
}); });
it("should reject promise on bad IV when decrypting", function(done) { it("should reject promise on bad IV when decrypting", function(done) {
eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) {
res.iv[0] ^= 1; enc.iv[0] ^= 1;
eccrypto.decrypt(privateKeyA, res).catch(function() { eccrypto.decrypt(privateKeyA, enc).catch(function() {
done(); done();
}); });
}); });
}); });
it("should reject promise on bad R when decrypting", function(done) { it("should reject promise on bad R when decrypting", function(done) {
eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) {
res.ephemPublicKey[0] ^= 1; enc.ephemPublicKey[0] ^= 1;
eccrypto.decrypt(privateKeyA, res).catch(function() { eccrypto.decrypt(privateKeyA, enc).catch(function() {
done(); done();
}); });
}); });
}); });
it("should reject promise on bad cipher text when decrypting", function(done) { it("should reject promise on bad ciphertext when decrypting", function(done) {
eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) {
res.cipherText[0] ^= 1; enc.ciphertext[0] ^= 1;
eccrypto.decrypt(privateKeyA, res).catch(function() { eccrypto.decrypt(privateKeyA, enc).catch(function() {
done(); done();
}); });
}); });
}); });
it("should reject promise on bad MAC when decrypting", function(done) { it("should reject promise on bad MAC when decrypting", function(done) {
eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(res) { eccrypto.encrypt(publicKeyA, Buffer("test")).then(function(enc) {
res.mac[0] ^= 1; var origMac = enc.mac;
eccrypto.decrypt(privateKeyA, res).catch(function() { 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(); done();
}); });
}); });
}); });
});
}); });
}