Do not use promises in crypto

Because sha.js is faster than WebCryptoAPI for POW.
This commit is contained in:
Kagami Hiiragi 2015-01-03 17:52:27 +03:00
parent f6165d891b
commit bf8b663c5d
10 changed files with 163 additions and 258 deletions

View File

@ -12,14 +12,6 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/
* [Protocol specification](https://bitmessage.org/wiki/Protocol_specification)
* [Whitepaper](https://bitmessage.org/bitmessage.pdf)
## Implementation details
With the help of browserify `bitmessage` provides different implementations for Browser and Node.js with the same API. Because WebCryptoAPI defines asynchronous promise-driven API, implementation for Node needs to use promises too.
* Use Node.js crypto module/library bindings where possible
* Use WebCryptoAPI where possible
* Promise-driven API
## Feature matrix (both Browser and Node)
- [ ] crypto
@ -83,13 +75,10 @@ With the help of browserify `bitmessage` provides different implementations for
## Usage
```js
// Generating a new Bitmessage identity.
// Generate a new random Bitmessage identity.
var Address = require("bitmessage").Address;
Address.fromRandom().then(function(addr) {
addr.encode().then(function(str) {
console.log("New random Bitmessage address:", str);
});
});
var addr = Address.fromRandom();
console.log("New random Bitmessage address:", addr.encode());
```
## License

View File

@ -6,7 +6,6 @@
"use strict";
require("es6-promise").polyfill();
require("object.assign").shim();
var assert = require("assert");
var bufferEqual = require("buffer-equal");
@ -30,14 +29,20 @@ function Address(opts) {
assert(this.version >= 1, "Version too low");
this.stream = this.stream || 1;
if (this.ripe) {
assertripelen(getripelen(this.ripe), this.version);
assertripelen(getripelen(this.ripe), this.version, this.ripe);
if (this.ripe.length < 20) {
var fullripe = new Buffer(20);
fullripe.fill(0);
this.ripe.copy(fullripe, 20 - this.ripe.length);
this.ripe = fullripe;
}
}
}
/**
* Parse Bitmessage address into address object.
* @param {String} str - Address string (with or without `BM-` prefix)
* @return {Promise.<Address>} Decoded address object.
* @return {Address} Decoded address object.
*/
Address.decode = function(str) {
str = str.trim();
@ -45,18 +50,10 @@ Address.decode = function(str) {
str = str.slice(3);
}
var bytes;
try {
bytes = bs58.decode(str);
} catch(e) {
return Promise.reject(e);
}
// Checksum validating.
var bytes = bs58.decode(str);
var data = new Buffer(bytes.slice(0, -4));
var checksum = new Buffer(bytes.slice(-4));
return getchecksum(data).then(function(realchecksum) {
assert(bufferEqual(checksum, realchecksum), "Bad checkum");
assert(bufferEqual(checksum, getchecksum(data)), "Bad checkum");
var decoded = var_int.decode(data);
var version = decoded.value;
@ -66,26 +63,16 @@ Address.decode = function(str) {
var stream = decoded.value;
var ripe = decoded.rest;
var ripelen = ripe.length;
if (version === 4) {
assert(ripe[0] !== 0, "Ripe encode error");
}
// Prevent extra allocation. God, kill me please for premature
// optimizations.
if (ripelen < 20) {
var zeroes = new Buffer(Array(20 - ripelen));
ripe = Buffer.concat([zeroes, ripe]);
}
return new Address({version: version, stream: stream, ripe: ripe});
});
};
// Compute the Bitmessage checksum for the given data.
function getchecksum(data) {
return bmcrypto.sha512(data).then(bmcrypto.sha512).then(function(dhash) {
return dhash.slice(0, 4);
});
return bmcrypto.sha512(bmcrypto.sha512(data)).slice(0, 4);
}
// Get RIPEMD160(SHA512(SIGN_PUBLIC_KEY || ENC_PUBLIC_KEY)).
@ -104,39 +91,36 @@ function keys2ripe(signKey, encKey) {
encPublicKey = encKey;
}
var concat = Buffer.concat([signPublicKey, encPublicKey]);
return bmcrypto.sha512(concat).then(bmcrypto.ripemd160);
return bmcrypto.ripemd160(bmcrypto.sha512(concat));
}
/**
* Calculate the Ripe hash of the address.
* Calculate the ripe hash of the address.
* @param {?Object} opts - Options
* @return {Promise.<Buffer>} Resulting Ripe hash.
* @return {Buffer} Resulting ripe hash.
*/
Address.prototype.getRipe = function(opts) {
var self = this;
var ripepromise;
if (self.ripe) {
ripepromise = Promise.resolve(self.ripe);
} else {
var ripe;
opts = opts || {};
var signKey = self.signPrivateKey || self.signPublicKey;
if (this.ripe) {
ripe = this.ripe;
} else {
var signKey = this.signPrivateKey || this.signPublicKey;
assert(signKey, "No signing key");
var encKey = self.encPrivateKey || self.encPublicKey;
var encKey = this.encPrivateKey || this.encPublicKey;
assert(encKey, "No encryption key");
ripepromise = keys2ripe(signKey, encKey);
ripe = keys2ripe(signKey, encKey);
}
return ripepromise.then(function(ripe) {
var ripelen = getripelen(ripe);
assertripelen(ripelen, self.version);
assertripelen(ripelen, this.version, ripe);
if (opts.short) {
return ripe.slice(20 - ripelen);
} else {
return ripe;
}
});
};
// Get truncated Ripe hash length.
// Get truncated ripe hash length.
function getripelen(ripe) {
var zeroes = 0;
for (var i = 0; i < 20, ripe[i] === 0; i++) {
@ -145,21 +129,24 @@ function getripelen(ripe) {
return 20 - zeroes;
}
// Do neccessary checkings of the truncated Ripe hash length depending
// Do neccessary checkings of the truncated ripe hash length depending
// on the address version.
function assertripelen(ripelen, version) {
function assertripelen(ripelen, version, ripe) {
if (ripe) {
assert(ripe.length <= 20, "Bad ripe");
}
switch (version) {
case 1:
assert(ripelen === 20, "Bad ripe length");
break;
case 2:
case 3:
assert(ripelen >= 18, "Ripe too short");
assert(ripelen <= 20, "Ripe too long");
assert(ripelen >= 18, "Ripe is too short");
assert(ripelen <= 20, "Ripe is too long");
break;
case 4:
assert(ripelen >= 4, "Ripe too short");
assert(ripelen <= 20, "Ripe too long");
assert(ripelen >= 4, "Ripe is too short");
assert(ripelen <= 20, "Ripe is too long");
break;
default:
throw new Error("Bad version");
@ -179,61 +166,52 @@ function checkripelen(ripelen, version) {
/**
* Encode Bitmessage address object into address string.
* @return {Promise.<string>} Address string.
* @return {string} Address string.
*/
Address.prototype.encode = function() {
var self = this;
return self.getRipe({short: true}).then(function(ripe) {
var ripe = this.getRipe({short: true});
var data = Buffer.concat([
var_int.encode(self.version),
var_int.encode(self.stream),
var_int.encode(this.version),
var_int.encode(this.stream),
ripe,
]);
return getchecksum(data).then(function(checksum) {
var addr = Buffer.concat([data, checksum]);
var addr = Buffer.concat([data, getchecksum(data)]);
return "BM-" + bs58.encode(addr);
});
});
};
function popkey(obj, key) {
var value = obj[key];
delete obj[key];
return value;
}
/**
* Create new Bitmessage address from random encryption and signing
* private keys.
* @param {?Object} opts - Address options
* @return {Promise.<Address>} Generated address object.
* @return {Address} Generated address object.
*/
Address.fromRandom = function(opts) {
opts = opts || {};
var version = opts.version || 4;
var ripelen = opts.ripelen || 19;
try {
opts = Object.assign({}, opts);
var version = opts.version = opts.version || 4;
var ripelen = popkey(opts, "ripelen") || 19;
assertripelen(ripelen, version);
} catch(e) {
return Promise.reject(e);
}
// Should the generated Ripe length be strictly equal to the specified
// (less-or-equal by default);
var strictripelen = !!opts.strictripelen;
var nextTick = typeof setImmediate === "undefined" ?
process.nextTick :
setImmediate;
// Should the generated ripe length be strictly equal to the specified
// (less or equal by default).
var strictripelen = !!popkey(opts, "strictripelen");
// TODO(Kagami): Speed it up using web workers in Browser.
// TODO(Kagami): Bind to C++ version of this code in Node.
var encPrivateKey, encPublicKey, ripe;
var signPrivateKey = bmcrypto.getPrivate();
var signPublicKey = bmcrypto.getPublic(signPrivateKey);
// FIXME(Kagami): This function is rather slow in browsers so
// generation of ripelen=18 currently is disabled (see `test.js`). It
// should be heavily profiled to determine the bottleneck.
// TODO(Kagami): We may want to run this in the web worker to speedup
// the search. Currently WebCryptoAPI is not available in Firefox in
// web workers (see
// <https://bugzilla.mozilla.org/show_bug.cgi?id=842818>) but is
// available in Chrome (at least in 39.0+).
return new Promise(function(resolve, reject) {
function tryKey() {
var encPrivateKey = bmcrypto.getPrivate();
var encPublicKey = bmcrypto.getPublic(encPrivateKey);
return keys2ripe(signPublicKey, encPublicKey).then(function(ripe) {
var keysbuf = Buffer(130);
signPublicKey.copy(keysbuf);
while (true) {
encPrivateKey = bmcrypto.getPrivate();
encPublicKey = bmcrypto.getPublic(encPrivateKey);
encPublicKey.copy(keysbuf, 65);
ripe = bmcrypto.ripemd160(bmcrypto.sha512(keysbuf));
var len = getripelen(ripe);
if (
(strictripelen && len === ripelen) ||
@ -241,20 +219,14 @@ Address.fromRandom = function(opts) {
) {
// TODO(Kagami): Do we need to put all these properties or compute
// them manually via ECMA5 getters/setters instead?
resolve(new Address(Object.assign({
signPrivateKey: signPrivateKey,
signPublicKey: signPublicKey,
encPrivateKey: encPrivateKey,
encPublicKey: encPublicKey,
ripe: ripe,
}, opts)));
} else {
nextTick(tryKey);
opts.signPrivateKey = signPrivateKey;
opts.signPublicKey = signPublicKey;
opts.encPrivateKey = encPrivateKey;
opts.encPublicKey = encPublicKey;
opts.ripe = ripe;
return new Address(opts);
}
}).catch(reject);
}
tryKey();
});
};
module.exports = Address;

View File

@ -12,7 +12,7 @@ var platform = require("./platform");
/**
* Calculate SHA-512 hash.
* @param {Buffer} buf - Input data
* @return {Promise.<Buffer>} Resulting hash.
* @return {Buffer} Resulting hash.
* @function
*/
exports.sha512 = platform.sha512;
@ -20,7 +20,7 @@ exports.sha512 = platform.sha512;
/**
* Calculate SHA-256 hash.
* @param {Buffer} buf - Input data
* @return {Promise.<Buffer>} Resulting hash.
* @return {Buffer} Resulting hash.
* @function
*/
exports.sha256 = platform.sha256;
@ -28,7 +28,7 @@ exports.sha256 = platform.sha256;
/**
* Calculate RIPEMD-160 hash.
* @param {Buffer} buf - Input data
* @return {Promise.<Buffer>} Resulting hash.
* @return {Buffer} Resulting hash.
* @function
*/
exports.ripemd160 = platform.ripemd160;

View File

@ -1,5 +1,5 @@
/**
* Working with messages.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#Message_types}
* @module bitmessage/message
* @module bitmessage/messages
*/

View File

@ -1,5 +1,5 @@
/**
* Working with objects.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#Object_types}
* @module bitmessage/object
* @module bitmessage/objects
*/

View File

@ -1,38 +1,21 @@
/**
* Browser implementation of platform-specific routines.
* @see {@link http://www.w3.org/TR/WebCryptoAPI/}
* @see {@link http://caniuse.com/#feat=cryptography}
* @see {@link https://sites.google.com/a/chromium.org/dev/blink/webcrypto}
*/
"use strict";
require("es6-promise").polyfill();
var assert = require("assert");
var createHash = require("sha.js");
var ripemd160 = require("ripemd160");
// Support `webkit` prefix for Safari (not tested yet).
// TODO(Kagami): Try to support IE11.
var subtle = window.crypto.subtle || window.crypto.webkitSubtle;
assert(subtle, "WebCryptoAPI is not supported");
exports.sha512 = function(buf) {
return subtle.digest({name: "SHA-512"}, buf).then(function(arr) {
return new Buffer(new Uint8Array(arr));
});
return createHash("sha512").update(buf).digest();
};
exports.sha256 = function(buf) {
return subtle.digest({name: "SHA-256"}, buf).then(function(arr) {
return new Buffer(new Uint8Array(arr));
});
return createHash("sha256").update(buf).digest();
};
exports.ripemd160 = function(buf) {
// XXX(Kagami): RIPEMD is not defined in WebCryptoAPI so we provide it
// using pure JS third-party implementation.
return Promise.resolve(ripemd160(buf));
};
exports.ripemd160 = ripemd160;
exports.randomBytes = function(size) {
var arr = new Uint8Array(size);

View File

@ -1,29 +1,22 @@
/**
* Node.js implementation of platform-specific routines.
* @see {@link http://nodejs.org/api/crypto.html}
*/
"use strict";
require("es6-promise").polyfill();
var crypto = require("crypto");
var createHash = crypto.createHash;
exports.sha512 = function(buf) {
var hash = crypto.createHash("sha512");
hash.update(buf);
return Promise.resolve(hash.digest());
return createHash("sha512").update(buf).digest();
};
exports.sha256 = function(buf) {
var hash = crypto.createHash("sha256");
hash.update(buf);
return Promise.resolve(hash.digest());
return createHash("sha256").update(buf).digest();
};
exports.ripemd160 = function(buf) {
var hash = crypto.createHash("ripemd160");
hash.update(buf);
return Promise.resolve(hash.digest());
return createHash("ripemd160").update(buf).digest();
};
exports.randomBytes = crypto.randomBytes;

View File

@ -6,7 +6,6 @@
"use strict";
require("es6-promise").polyfill();
var assert = require("assert");
var bufferEqual = require("buffer-equal");
var bs58 = require("bs58");
@ -14,41 +13,31 @@ var bmcrypto = require("./crypto");
// Compute the WIF checksum for the given data.
function getchecksum(data) {
return bmcrypto.sha256(data).then(bmcrypto.sha256).then(function(dhash) {
return dhash.slice(0, 4);
});
return bmcrypto.sha256(bmcrypto.sha256(data)).slice(0, 4);
}
/**
* Decode WIF encoded private key.
* @param {string} wif - Encoded key
* @return {Promise.<Buffer>} Private key.
* @return {Buffer} Private key.
*/
exports.decode = function(wif) {
var bytes;
try {
bytes = bs58.decode(wif);
var bytes = bs58.decode(wif);
assert(bytes[0] === 0x80, "Bad WIF");
} catch(e) {
return Promise.reject(e);
}
var data = new Buffer(bytes.slice(0, -4));
var checksum = new Buffer(bytes.slice(-4));
return getchecksum(data).then(function(realchecksum) {
assert(bufferEqual(checksum, realchecksum), "Bad checkum");
assert(bufferEqual(checksum, getchecksum(data)), "Bad checkum");
return data.slice(1);
});
};
/**
* Convert private key to a WIF.
* @param {Buffer} privateKey - A private key to encode
* @return {Promise.<string>} Encoded private key.
* @return {string} Encoded private key.
*/
exports.encode = function(privateKey) {
var data = Buffer.concat([new Buffer([0x80]), privateKey]);
return getchecksum(data).then(function(checksum) {
var checksum = getchecksum(data);
var bytes = Buffer.concat([data, checksum]);
return bs58.encode(bytes);
});
};

View File

@ -48,8 +48,8 @@
"bs58": "^2.0.0",
"buffer-equal": "~0.0.1",
"eccrypto": "^0.1.1",
"es6-promise": "^2.0.1",
"object.assign": "^1.1.1",
"ripemd160": "^0.2.0"
"ripemd160": "^0.2.0",
"sha.js": "^2.3.0"
}
}

53
test.js
View File

@ -13,21 +13,15 @@ var Address = bitmessage.Address;
describe("Crypto", function() {
it("should implement SHA-512 hash", function() {
return bmcrypto.sha512(Buffer("test")).then(function(res) {
expect(res.toString("hex")).to.equal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff");
});
expect(bmcrypto.sha512(Buffer("test")).toString("hex")).to.equal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff");
});
it("should implement SHA-256 hash", function() {
return bmcrypto.sha256(Buffer("test")).then(function(res) {
expect(res.toString("hex")).to.equal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
});
expect(bmcrypto.sha256(Buffer("test")).toString("hex")).to.equal("9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08");
});
it("should implement RIPEMD-160 hash", function() {
return bmcrypto.ripemd160(Buffer("test")).then(function(res) {
expect(res.toString("hex")).to.equal("5e52fee47e6b070565f74372468cdc699de89107");
});
expect(bmcrypto.ripemd160(Buffer("test")).toString("hex")).to.equal("5e52fee47e6b070565f74372468cdc699de89107");
});
it("should implement cryptographically secure PRNG", function() {
@ -165,78 +159,63 @@ describe("WIF", function() {
var encPrivateKey = Buffer("9f9969c93c2d186787a7653f70e49be34c03c4a853e6ad0c867db0946bc433c6", "hex");
it("should decode", function() {
return WIF.decode(wifSign)
.then(function(key1) {
var key1 = WIF.decode(wifSign);
expect(Buffer.isBuffer(key1)).to.be.true;
expect(key1.length).to.equal(32);
expect(key1.toString("hex")).to.equal(signPrivateKey.toString("hex"));
return WIF.decode(wifEnc).then(function(key2) {
var key2 = WIF.decode(wifEnc);
expect(Buffer.isBuffer(key2)).to.be.true;
expect(key2.length).to.equal(32);
expect(key2.toString("hex")).to.equal(encPrivateKey.toString("hex"));
return Address({signPrivateKey: key1, encPrivateKey: key2}).encode();
});
}).then(function(str) {
expect(str).to.equal("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z");
});
var addrStr = Address({signPrivateKey: key1, encPrivateKey: key2}).encode();
expect(addrStr).to.equal("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z");
});
it("should encode", function() {
return WIF.encode(signPrivateKey).then(function(wif1) {
var wif1 = WIF.encode(signPrivateKey);
expect(wif1).to.equal(wifSign);
return WIF.encode(encPrivateKey);
}).then(function(wif2) {
var wif2 = WIF.encode(encPrivateKey);
expect(wif2).to.equal(wifEnc);
});
});
});
describe("Address", function() {
it("should decode Bitmessage address", function() {
return Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z")
.then(function(addr) {
var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z")
expect(addr.version).to.equal(4);
expect(addr.stream).to.equal(1);
expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56");
});
});
it("should decode Bitmessage address badly formatted", function() {
return Address.decode(" 2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z ")
.then(function(addr) {
var addr = Address.decode(" 2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z ")
expect(addr.version).to.equal(4);
expect(addr.stream).to.equal(1);
expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56");
});
});
it("should allow to generate new Bitmessage address", function() {
this.timeout(10000);
return Address.fromRandom().then(function(addr) {
var addr = Address.fromRandom();
expect(addr.version).to.equal(4);
expect(addr.stream).to.equal(1);
expect(addr.signPrivateKey.length).to.equal(32);
expect(addr.encPrivateKey.length).to.equal(32);
return addr.encode().then(function(str) {
var str = addr.encode();
expect(str.slice(0, 3)).to.equal("BM-");
return Address.decode(str).then(function(addr2) {
var addr2 = Address.decode(str);
expect(addr2.version).to.equal(4);
expect(addr2.stream).to.equal(1);
expect(addr2.ripe.length).to.equal(20);
expect(addr2.ripe[0]).to.equal(0);
});
});
});
});
if (allTests) {
it("should allow to generate shorter address", function() {
this.timeout(60000);
return Address.fromRandom({ripelen: 18}).then(function(addr) {
return addr.getRipe({short: true}).then(function(ripe) {
var addr = Address.fromRandom({ripelen: 18});
var ripe = addr.getRipe({short: true});
expect(ripe.length).to.be.at.most(18);
});
});
});
}
});