/** * Working with Bitmessage addresses. * **NOTE**: `Address` is exported as a module. * @example * var Address = require("bitmessage").Address; * * // Generate a new random Bitmessage identity. * var addr1 = Address.fromRandom(); * console.log("New random Bitmessage address:", addr1.encode()); * * // Or create it from passphrase. * var addr2 = Address.fromPassphrase("test"); * console.log("Deterministic Bitmessage address:", addr2.encode()); * @see {@link https://pybitmessage.rtfd.io/en/v0.6/address.html} * @module bitmessage/address */ // TODO(Kagami): Document getters/setters. "use strict"; var objectAssign = Object.assign || require("object-assign"); var bufferEqual = require("buffer-equal"); var bs58 = require("bs58"); var assert = require("./_util").assert; var var_int = require("./structs").var_int; var PubkeyBitfield = require("./structs").PubkeyBitfield; var bmcrypto = require("./crypto"); var popkey = require("./_util").popkey; /** * Create a new Bitmessage address object. * @param {Object=} opts - Address options * @param {number} opts.version - Version number (4 by default) * @param {number} opts.stream - Stream number (1 by default) * @param {Object} opts.behavior - [Pubkey features]{@link * module:bitmessage/structs.PubkeyBitfield} (`DOES_ACK` by default) * @param {Buffer} opts.signPrivateKey - Signing private key * @param {Buffer} opts.signPublicKey - Signing public key * @param {Buffer} opts.encPrivateKey - Encryption private key * @param {Buffer} opts.encPublicKey - Encryption public key * @param {Buffer} opts.ripe - Keys RIPEMD hash * @constructor * @static */ function Address(opts) { if (!(this instanceof Address)) { return new Address(opts); } opts = objectAssign({}, opts); // Pull out version right away because it may be needed in setters. this.version = popkey(opts, "version") || 4; assert(this.version <= 4, "Version too high"); assert(this.version >= 1, "Version too low"); // Set defaults. opts.stream = opts.stream || 1; opts.behavior = opts.behavior || PubkeyBitfield().set(PubkeyBitfield.DOES_ACK); // Merge remained values. objectAssign(this, opts); } /** * Create a copy of the address object. * @return {Address} Cloned address. */ Address.prototype.clone = function() { return new Address(this); }; /** * Test if the given object is an Address instance. * NOTE: Implementation is just simple `instanceof` but it improves * readability and consistent with `isArray`, `isBuffer`, etc. * @param {Object} obj - Given object * @return {boolean} */ Address.isAddress = function(obj) { return obj instanceof Address; }; /** * 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) { if (Address.isAddress(str)) { return str; } str = str.trim(); if (str.slice(0, 3) === "BM-") { str = str.slice(3); } else if (str.slice(0, 11) === "bitmessage:") { str = str.slice(11); } var bytes = bs58.decode(str); var data = Buffer.from(bytes.slice(0, -4)); var checksum = Buffer.from(bytes.slice(-4)); assert(bufferEqual(checksum, getaddrchecksum(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 getaddrchecksum(data) { return bmcrypto.sha512(bmcrypto.sha512(data)).slice(0, 4); } /** * Get the RIPEMD hash of the address keys without prefix nulls. * @return {Buffer} A short RIPEMD hash. */ Address.prototype.getShortRipe = function() { var ripe = this.ripe; return ripe.slice(20 - getripelen(ripe)); }; function getaddrhash(addr) { var dataToHash = Buffer.concat([ var_int.encode(addr.version), var_int.encode(addr.stream), addr.ripe, ]); return bmcrypto.sha512(dataToHash); } /** * Calculate the encryption key used to encrypt/decrypt * [pubkey]{@link module:bitmessage/objects.pubkey} objects. * @return {Buffer} A 32-byte private key. */ Address.prototype.getPubkeyPrivateKey = function() { return bmcrypto.sha512(getaddrhash(this)).slice(0, 32); }; /** * Calculate the corresponding public key for encryption key used to * encrypt/decrypt * [pubkey]{@link module:bitmessage/objects.pubkey} objects. * @return {Buffer} A 65-byte public key. */ Address.prototype.getPubkeyPublicKey = function() { return bmcrypto.getPublic(this.getPubkeyPrivateKey()); }; /** * Calculate the encryption key used to encrypt/decrypt * [broadcast]{@link module:bitmessage/objects.broadcast} objects. * @return {Buffer} A 32-byte private key. */ Address.prototype.getBroadcastPrivateKey = function() { if (this.version >= 4) { return bmcrypto.sha512(getaddrhash(this)).slice(0, 32); } else { return getaddrhash(this).slice(0, 32); } }; /** * Calculate the corresponding public key for encryption key used to * encrypt/decrypt * [broadcast]{@link module:bitmessage/objects.broadcast} objects. * @return {Buffer} A 65-byte public key. */ Address.prototype.getBroadcastPublicKey = function() { return bmcrypto.getPublic(this.getBroadcastPrivateKey()); }; /** * Calculate the address tag. * @return {Buffer} A 32-byte address tag. */ Address.prototype.getTag = function() { return bmcrypto.sha512(getaddrhash(this)).slice(32); }; // 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 data = Buffer.concat([ var_int.encode(this.version), var_int.encode(this.stream), this.getShortRipe(), ]); var addr = Buffer.concat([data, getaddrchecksum(data)]); return "BM-" + bs58.encode(addr); }; /** * Create a new Bitmessage address with random encryption and signing * private keys. * @param {Object=} opts - Address options * @param {number} opts.ripeLength - Required length of the short RIPEMD * hash (19 by default) * @param {number} opts.version - Version number (4 by default) * @param {number} opts.stream - Stream number (1 by default) * @param {Object} opts.behavior - [Pubkey features]{@link module:bitmessage/structs.PubkeyBitfield} (`DOES_ACK` by default) * @return {Address} New address object. */ Address.fromRandom = function(opts) { opts = objectAssign({}, opts); var version = opts.version = opts.version || 4; var ripelen = popkey(opts, "ripeLength") || 19; assertripelen(ripelen, version); // 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, len; var signPrivateKey = bmcrypto.getPrivate(); var signPublicKey = bmcrypto.getPublic(signPrivateKey); var keysbuf = Buffer.alloc(130); signPublicKey.copy(keysbuf); while (true) { encPrivateKey = bmcrypto.getPrivate(); encPublicKey = bmcrypto.getPublic(encPrivateKey); encPublicKey.copy(keysbuf, 65); ripe = bmcrypto.ripemd160(bmcrypto.sha512(keysbuf)); len = getripelen(ripe); if (len <= ripelen && checkripelen(len, version)) { opts.signPrivateKey = signPrivateKey; opts.encPrivateKey = encPrivateKey; return new Address(opts); } } }; /** * Create a new Bitmessage address from passphrase. * @param {(string|Object)} opts - Passphrase or address options * @param {string} opts.passphrase - Passphrase to generate address from * @param {number=} opts.ripeLength - Required length of the short * RIPEMD hash (19 by default) * @param {number=} opts.version - Version number (4 by default) * @param {number=} opts.stream - Stream number (1 by default) * @param {Object=} opts.behavior - [Pubkey features]{@link module:bitmessage/structs.PubkeyBitfield} (`DOES_ACK` by default) * @return {Address} New address object. */ Address.fromPassphrase = function(opts) { if (typeof opts === "string") { opts = {passphrase: opts}; } else { opts = objectAssign({}, opts); } var version = opts.version = opts.version || 4; var ripelen = popkey(opts, "ripeLength") || 19; assertripelen(ripelen, version); var passphrase = popkey(opts, "passphrase"); // TODO(Kagami): Speed it up using web workers in Browser. // TODO(Kagami): Bind to C++ version of this code in Node. var signPrivateKey, signPublicKey, encPrivateKey, encPublicKey; var ripe, len, tmp; var signnonce = 0; var encnonce = 1; var keysbuf = Buffer.alloc(130); // XXX(Kagami): Spec doesn't mention encoding, using UTF-8. var phrasebuf = Buffer.from(passphrase, "utf8"); while (true) { // TODO(Kagami): We may slightly optimize it and pre-create tmp // buffers based on the encoded nonce size (1, 3, 5 and 9 bytes). tmp = Buffer.concat([phrasebuf, var_int.encode(signnonce)]); signPrivateKey = bmcrypto.sha512(tmp).slice(0, 32); signPublicKey = bmcrypto.getPublic(signPrivateKey); signPublicKey.copy(keysbuf); tmp = Buffer.concat([phrasebuf, var_int.encode(encnonce)]); encPrivateKey = bmcrypto.sha512(tmp).slice(0, 32); encPublicKey = bmcrypto.getPublic(encPrivateKey); encPublicKey.copy(keysbuf, 65); ripe = bmcrypto.ripemd160(bmcrypto.sha512(keysbuf)); len = getripelen(ripe); if (len <= ripelen && checkripelen(len, version)) { opts.signPrivateKey = signPrivateKey; opts.encPrivateKey = encPrivateKey; return new Address(opts); } signnonce += 2; encnonce += 2; } }; Object.defineProperty(Address.prototype, "signPrivateKey", { get: function() { return this._signPrivateKey; }, set: function(signPrivateKey) { this._signPrivateKey = signPrivateKey; // Invalidate cached values; delete this._signPublicKey; delete this._ripe; }, }); Object.defineProperty(Address.prototype, "signPublicKey", { get: function() { if (this._signPublicKey) { return this._signPublicKey; } else if (this.signPrivateKey) { this._signPublicKey = bmcrypto.getPublic(this.signPrivateKey); return this._signPublicKey; } else { throw new Error("No signing key"); } }, set: function(signPublicKey) { this._signPublicKey = signPublicKey; }, }); Object.defineProperty(Address.prototype, "encPrivateKey", { get: function() { return this._encPrivateKey; }, set: function(encPrivateKey) { this._encPrivateKey = encPrivateKey; // Invalidate cached values; delete this._encPublicKey; delete this._ripe; }, }); Object.defineProperty(Address.prototype, "encPublicKey", { get: function() { if (this._encPublicKey) { return this._encPublicKey; } else if (this.encPrivateKey) { this._encPublicKey = bmcrypto.getPublic(this.encPrivateKey); return this._encPublicKey; } else { throw new Error("No encryption key"); } }, set: function(encPublicKey) { this._encPublicKey = encPublicKey; }, }); Object.defineProperty(Address.prototype, "ripe", { get: function() { if (this._ripe) { return this._ripe; } var dataToHash = Buffer.concat([this.signPublicKey, this.encPublicKey]); this._ripe = bmcrypto.ripemd160(bmcrypto.sha512(dataToHash)); return this._ripe; }, set: function(ripe) { assertripelen(getripelen(ripe), this.version, ripe); if (ripe.length < 20) { var fullripe = Buffer.alloc(20); fullripe.fill(0); ripe.copy(fullripe, 20 - ripe.length); ripe = fullripe; } this._ripe = ripe; }, }); module.exports = Address;