/** * Working with Bitmessage addresses. * @see {@link https://bitmessage.org/wiki/Address} * @module bitmessage/address */ "use strict"; require("object.assign").shim(); var assert = require("assert"); var bufferEqual = require("buffer-equal"); var bs58 = require("bs58"); var var_int = require("./structs").var_int; var bmcrypto = require("./crypto"); /** * Create a new Bitmessage address object. * @param {?Object} opts - Address options * @constructor */ function Address(opts) { if (!(this instanceof Address)) { return new Address(opts); } opts = opts || {}; Object.assign(this, opts); this.version = this.version || 4; assert(this.version <= 4, "Version too high"); assert(this.version >= 1, "Version too low"); this.stream = this.stream || 1; if (this.ripe) { 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 {Address} Decoded address object. */ Address.decode = function(str) { str = str.trim(); if (str.slice(0, 3) === "BM-") { str = str.slice(3); } var bytes = bs58.decode(str); var data = new Buffer(bytes.slice(0, -4)); var checksum = new Buffer(bytes.slice(-4)); assert(bufferEqual(checksum, getchecksum(data)), "Bad checkum"); var decoded = var_int.decode(data); var version = decoded.value; data = decoded.rest; decoded = var_int.decode(data); var stream = decoded.value; var ripe = decoded.rest; if (version === 4) { assert(ripe[0] !== 0, "Ripe encode error"); } return new Address({version: version, stream: stream, ripe: ripe}); }; // Compute the Bitmessage checksum for the given data. function getchecksum(data) { return bmcrypto.sha512(bmcrypto.sha512(data)).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.ripemd160(bmcrypto.sha512(concat)); } /** * Calculate the ripe hash of the address. * @param {?Object} opts - Options * @return {Buffer} Resulting ripe hash. */ Address.prototype.getRipe = function(opts) { var ripe; opts = opts || {}; if (this.ripe) { ripe = this.ripe; } else { var signKey = this.signPrivateKey || this.signPublicKey; assert(signKey, "No signing key"); var encKey = this.encPrivateKey || this.encPublicKey; assert(encKey, "No encryption key"); ripe = keys2ripe(signKey, encKey); } var ripelen = getripelen(ripe); assertripelen(ripelen, this.version, ripe); if (opts.short) { return ripe.slice(20 - ripelen); } else { return ripe; } }; // 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, 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 is too short"); assert(ripelen <= 20, "Ripe is too long"); break; case 4: assert(ripelen >= 4, "Ripe is too short"); assert(ripelen <= 20, "Ripe is too long"); break; default: throw new Error("Bad version"); } } // The same as `assertripelen` but return boolean instead of thrown an // Error. function checkripelen(ripelen, version) { try { assertripelen(ripelen, version); return true; } catch(e) { return false; } } /** * Encode Bitmessage address object into address string. * @return {string} Address string. */ Address.prototype.encode = function() { var ripe = this.getRipe({short: true}); var data = Buffer.concat([ var_int.encode(this.version), var_int.encode(this.stream), ripe, ]); 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 {Address} Generated address object. */ Address.fromRandom = function(opts) { opts = Object.assign({}, opts); var version = opts.version = opts.version || 4; var ripelen = popkey(opts, "ripelen") || 19; assertripelen(ripelen, version); // 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); 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) || (!strictripelen && len <= ripelen && checkripelen(ripelen, version)) ) { // TODO(Kagami): Do we need to put all these properties or compute // them manually via ECMA5 getters/setters instead? opts.signPrivateKey = signPrivateKey; opts.signPublicKey = signPublicKey; opts.encPrivateKey = encPrivateKey; opts.encPublicKey = encPublicKey; opts.ripe = ripe; return new Address(opts); } } }; module.exports = Address;