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
```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

View File

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

119
index.js
View File

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

61
test.js
View File

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