diff --git a/karma-all-tests.js b/karma-all-tests.js new file mode 100644 index 0000000..8b55cbe --- /dev/null +++ b/karma-all-tests.js @@ -0,0 +1 @@ +window.ALL_TESTS = true; diff --git a/karma.conf.js b/karma.conf.js index eec8a7a..b931c5a 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -1,4 +1,10 @@ process.env.CHROME_BIN = "chromium-browser"; +var allTests = !!process.env.ALL_TESTS; +var files = ["test.js"]; +// Kludgy way to pass a variable to `test.js`. +if (allTests) { + files.unshift("karma-all-tests.js"); +}; module.exports = function(config) { config.set({ @@ -13,9 +19,7 @@ module.exports = function(config) { // list of files / patterns to load in the browser - files: [ - "test.js" - ], + files: files, // preprocess matching files before serving them to the browser @@ -67,5 +71,8 @@ module.exports = function(config) { // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true, + + + browserNoActivityTimeout: allTests ? 60000 : 10000, }); }; diff --git a/lib/address.js b/lib/address.js index c055106..268ed0a 100644 --- a/lib/address.js +++ b/lib/address.js @@ -5,6 +5,7 @@ "use strict"; +require("es6-promise").polyfill(); var assert = require("assert"); var bufferEqual = require("buffer-equal"); var bs58 = require("bs58"); @@ -12,8 +13,9 @@ var varint = require("./varint"); var bmcrypto = require("./crypto"); /** - * Parse Bitmessage Base58 encoded address (with or without `BM-` - * prefix) into address object. + * Parse Bitmessage address into address object. + * @param {String} str - Address string (with or without `BM-` prefix) + * @return {Promise.} Decoded address object */ exports.decode = function(str) { str = str.trim(); @@ -31,34 +33,23 @@ exports.decode = function(str) { // Checksum validating. var data = new Buffer(bytes.slice(0, -4)); var checksum = new Buffer(bytes.slice(-4)); - return bmcrypto.sha512(data).then(bmcrypto.sha512).then(function(dhash) { - assert(bufferEqual(dhash.slice(0, 4), checksum), "Bad checkum"); + return getchecksum(data).then(function(realchecksum) { + assert(bufferEqual(checksum, realchecksum), "Bad checkum"); var decoded = varint.decode(data); var version = decoded.value; - assert(version <= 4, "Version too high"); - assert(version >= 1, "Version too low"); + assertversion(version); data = decoded.rest; decoded = varint.decode(data); var stream = decoded.value; + assertstream(stream); var ripe = decoded.rest; var ripelen = ripe.length; - switch (version) { - case 1: - assert(ripelen === 20); - break; - case 2: - case 3: - assert(ripelen >= 18, "Ripe too short"); - assert(ripelen <= 20, "Ripe too long"); - break; - case 4: - assert(ripelen >= 4, "Ripe too short"); - assert(ripelen <= 20, "Ripe too long"); - assert(ripe[0] !== 0, "Ripe encode error"); - break; + assertripelen(ripelen, version); + if (version === 4) { + assert(ripe[0] !== 0, "Ripe decode error"); } // Prevent extra allocation. God, kill me please for premature @@ -70,3 +61,211 @@ exports.decode = function(str) { return {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); + }); +} + +// Get RIPEMD160(SHA512(SIGN_PUBLIC_KEY || ENC_PUBLIC_KEY)) +// Arguments could be either private or public keys. Private keys are +// **always** 32 bytes in length. +function keys2ripe(signKey, encKey) { + var signPublicKey, encPublicKey; + if (signKey.length === 32) { + signPublicKey = bmcrypto.getPublic(signKey); + } else { + signPublicKey = signKey; + } + if (encKey.length === 32) { + encPublicKey = bmcrypto.getPublic(encKey); + } else { + encPublicKey = encKey; + } + var concat = Buffer.concat([signPublicKey, encPublicKey]); + return bmcrypto.sha512(concat).then(bmcrypto.ripemd160); +} + +/** + * Get Ripe hash for the given address object. + * @param {Address} addr - Address object + * @param {?Object} opts - Options + * @return {Buffer} Resulting Ripe hash. + */ +function getRipe(addr, opts) { + var signKey = addr.signPrivateKey || addr.signPublicKey; + assert(signKey, "No signing key"); + var encKey = addr.encPrivateKey || addr.encPublicKey; + assert(encKey, "No encryption key"); + opts = opts || {}; + return keys2ripe(signKey, encKey).then(function(ripe) { + if (opts.short) { + var ripelen = getripelen(ripe); + return ripe.slice(20 - ripelen); + } else { + return ripe; + } + }); +} +exports.getRipe = getRipe; + +// Do neccessary checkings of the address version. +function assertversion(version) { + assert(version <= 4, "Version too high"); + assert(version >= 1, "Version too low"); +} + +// Do neccessary checkings of the stream number. +function assertstream(stream) { + assert(stream, "No stream"); +} + +// Get truncated ripe hash length. +function getripelen(ripe) { + var zeroes = 0; + for (var i = 0; i < 20, ripe[i] === 0; i++) { + zeroes++; + } + return 20 - zeroes; +} + +// Do neccessary checkings of the truncated ripe hash length depending +// on the address version. +function assertripelen(ripelen, version) { + 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"); + break; + case 4: + assert(ripelen >= 4, "Ripe too short"); + assert(ripelen <= 20, "Ripe too long"); + break; + default: + throw new Error("Wrong version"); + } +} + +// The same as `assertripelen` but return true/false instead of throw an +// Error. +function checkripelen(ripelen, version) { + try { + assertripelen(ripelen, version); + return true; + } catch(e) { + return false; + } +} + +/** + * Encode Bitmessage address object into address string. + * @param {Address} addr - Address object + * @return {Promise.} Address string + */ +exports.encode = function(addr) { + var version, stream, ripepromise; + try { + version = addr.version; + assertversion(version); + stream = addr.stream; + assertstream(stream); + + if (addr.ripe) { + ripepromise = Promise.resolve(addr.ripe); + } else { + ripepromise = getRipe(addr); + } + } catch (e) { + return Promise.reject(e); + } + + return ripepromise.then(function(ripe) { + var ripelen = getripelen(ripe); + assertripelen(ripelen, version); + // Skip leading zeroes. + ripe = ripe.slice(20 - ripelen); + var data = Buffer.concat([ + varint.encode(version), + varint.encode(stream), + ripe, + ]); + return getchecksum(data).then(function(checksum) { + var addr = Buffer.concat([data, checksum]); + return "BM-" + bs58.encode(addr); + }); + }); +}; + +/** + * Create new Bitmessage address using random encryption and signing + * private keys. + * @param {?Object} opts - Address options + * @return {Promise.} Generated address object + */ +exports.getRandom = function(opts) { + var version, stream, ripelen, signPrivateKey; + try { + opts = opts || {}; + version = opts.version || 4; + assertversion(version); + stream = opts.stream || 1; + assertstream(version); + ripelen = opts.ripelen || 19; + assertripelen(ripelen, version); + // Place it to try-catch since there might be not enough entropy to + // generate the key and the function will fail. + signPrivateKey = bmcrypto.getPrivate(); + } catch(e) { + return Promise.reject(e); + } + 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 = !!opts.strictripelen; + 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 heavilty 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 + // ) but is + // available in Chrome (at least in 39.0+). + return new Promise(function(resolve, reject) { + function tryKey() { + var encPrivateKey; + try { + encPrivateKey = bmcrypto.getPrivate(); + } catch(e) { + reject(e); + } + return keys2ripe(signPublicKey, encPrivateKey).then(function(ripe) { + console.log(ripe); + var len = getripelen(ripe); + if ( + (strictripelen && len === ripelen) || + (!strictripelen && len <= ripelen && checkripelen(ripelen, version)) + ) { + resolve({ + version: version, + stream: stream, + signPrivateKey: signPrivateKey, + encPrivateKey: encPrivateKey, + }); + } else { + nextTick(tryKey); + } + }).catch(reject); + } + tryKey(); + }); +}; diff --git a/lib/crypto-platform.browser.js b/lib/crypto-platform.browser.js index 3edc3f0..a43d41c 100644 --- a/lib/crypto-platform.browser.js +++ b/lib/crypto-platform.browser.js @@ -7,8 +7,8 @@ "use strict"; -var assert = require("assert"); require("es6-promise").polyfill(); +var assert = require("assert"); var ripemd160 = require("ripemd160"); // Support `webkit` prefix for Safari (not tested yet). diff --git a/lib/crypto.js b/lib/crypto.js index cbda353..6d52c76 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -6,8 +6,19 @@ "use strict"; +var eccrypto = require("eccrypto"); var cryptoPlatform = require("./crypto-platform"); Object.keys(cryptoPlatform).forEach(function(key) { exports[key] = cryptoPlatform[key]; }); + +/** + * Generate new random private key. + * @return {Buffer} New private key. + */ +exports.getPrivate = function() { + return cryptoPlatform.randomBytes(32); +}; + +exports.getPublic = eccrypto.getPublic; diff --git a/package.json b/package.json index bb13879..54f9015 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "./lib/crypto-platform.js": "./lib/crypto-platform.browser.js" }, "scripts": { - "test": "mocha && xvfb-run -a karma start && jshint .", + "test": "ALL_TESTS=1 mocha && xvfb-run -a karma start && jshint .", "m": "mocha", "k": "xvfb-run -a karma start", "kc": "xvfb-run -a karma start --browsers Chromium", @@ -46,6 +46,7 @@ "dependencies": { "bs58": "^2.0.0", "buffer-equal": "~0.0.1", + "eccrypto": "^0.1.1", "es6-promise": "^2.0.1", "int64-native": "^0.3.2", "node-int64": "^0.3.2", diff --git a/test.js b/test.js index 0c2ef41..5b8ff50 100644 --- a/test.js +++ b/test.js @@ -1,4 +1,8 @@ var expect = require("chai").expect; +var allTests = typeof window === "undefined" ? + !!process.env.ALL_TESTS : + window.ALL_TESTS; + var bitmessage = require("./lib"); var Int64 = bitmessage.Int64; var Address = bitmessage.Address; @@ -112,4 +116,33 @@ describe("Address", function() { expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56"); }); }); + + it("should allow to generate new Bitmessage address", function() { + return Address.getRandom().then(function(addr) { + 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 Address.encode(addr).then(function(str) { + expect(str.slice(0, 3)).to.equal("BM-"); + return Address.decode(str).then(function(addr2) { + 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.getRandom({ripelen: 18}).then(function(addr) { + return Address.getRipe(addr, {short: true}).then(function(ripe) { + expect(ripe.length).to.be.at.most(18); + }); + }); + }); + } });