bitmessage-js/lib/address.js

386 lines
10 KiB
JavaScript
Raw Normal View History

2014-12-18 17:47:18 +01:00
/**
* Working with Bitmessage addresses.
2014-12-30 18:00:28 +01:00
* @see {@link https://bitmessage.org/wiki/Address}
2014-12-18 17:47:18 +01:00
* @module bitmessage/address
*/
"use strict";
2015-01-06 12:06:15 +01:00
var objectAssign = Object.assign || require("object-assign");
2014-12-18 17:47:18 +01:00
var bufferEqual = require("buffer-equal");
var bs58 = require("bs58");
var assert = require("./_util").assert;
2015-01-03 11:14:39 +01:00
var var_int = require("./structs").var_int;
2015-01-19 00:05:53 +01:00
var PubkeyBitfield = require("./structs").PubkeyBitfield;
2014-12-18 17:47:18 +01:00
var bmcrypto = require("./crypto");
var popkey = require("./_util").popkey;
2014-12-18 17:47:18 +01:00
2014-12-29 23:16:51 +01:00
/**
* Create a new Bitmessage address object.
* @param {?Object} opts - Address options
* @constructor
2015-01-03 16:58:41 +01:00
* @static
2014-12-29 23:16:51 +01:00
*/
function Address(opts) {
if (!(this instanceof Address)) {
return new Address(opts);
}
2015-01-18 17:38:23 +01:00
opts = objectAssign({}, opts);
// Pull out version right away because it may be needed in setters.
this.version = popkey(opts, "version") || 4;
2014-12-29 23:16:51 +01:00
assert(this.version <= 4, "Version too high");
assert(this.version >= 1, "Version too low");
2015-01-19 13:08:56 +01:00
// Set defaults.
opts.stream = opts.stream || 1;
opts.behavior = opts.behavior ||
PubkeyBitfield().set(PubkeyBitfield.DOES_ACK);
2015-01-18 17:38:23 +01:00
// Merge remained values.
objectAssign(this, opts);
2014-12-29 23:16:51 +01:00
}
2015-01-29 19:47:01 +01:00
/**
* Create a copy of the address object.
* @return {Address} Cloned address.
*/
Address.prototype.clone = function() {
return new Address(this);
};
2015-01-22 00:39:51 +01:00
/**
* Test if 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;
};
2014-12-18 17:47:18 +01:00
/**
* Parse Bitmessage address into address object.
* @param {String} str - Address string (with or without `BM-` prefix)
* @return {Address} Decoded address object.
2014-12-18 17:47:18 +01:00
*/
2014-12-29 23:16:51 +01:00
Address.decode = function(str) {
2015-01-22 00:39:51 +01:00
if (Address.isAddress(str)) {
2015-01-18 12:37:09 +01:00
return str;
}
2014-12-18 17:47:18 +01:00
str = str.trim();
if (str.slice(0, 3) === "BM-") {
str = str.slice(3);
}
var bytes = bs58.decode(str);
2014-12-18 17:47:18 +01:00
var data = new Buffer(bytes.slice(0, -4));
var checksum = new Buffer(bytes.slice(-4));
2015-01-08 03:00:19 +01:00
assert(bufferEqual(checksum, getaddrchecksum(data)), "Bad checkum");
2014-12-18 17:47:18 +01:00
var decoded = var_int.decode(data);
var version = decoded.value;
2014-12-18 17:47:18 +01:00
data = decoded.rest;
decoded = var_int.decode(data);
var stream = decoded.value;
2014-12-18 17:47:18 +01:00
var ripe = decoded.rest;
if (version === 4) {
assert(ripe[0] !== 0, "Ripe encode error");
}
2014-12-18 17:47:18 +01:00
return new Address({version: version, stream: stream, ripe: ripe});
2014-12-18 17:47:18 +01:00
};
// Compute the Bitmessage checksum for the given data.
2015-01-08 03:00:19 +01:00
function getaddrchecksum(data) {
return bmcrypto.sha512(bmcrypto.sha512(data)).slice(0, 4);
}
/**
* Get the ripe hash of the address without prefix zeroes.
* @return {Buffer} A short ripe hash.
*/
Address.prototype.getShortRipe = function() {
2015-01-18 17:38:23 +01:00
var ripe = this.ripe;
return ripe.slice(20 - getripelen(ripe));
2014-12-29 23:16:51 +01:00
};
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 {@link pubkey}
* objects.
* @return {Buffer} A 32-byte private key.
*/
Address.prototype.getPubkeyPrivateKey = function() {
return bmcrypto.sha512(getaddrhash(this)).slice(0, 32);
};
2015-01-29 19:18:07 +01:00
/**
* Calculate the corresponding public key for encryption key used to
* encrypt/decrypt {@link 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
* {@link broadcast} objects.
2015-01-29 19:18:07 +01:00
* @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);
}
};
2015-01-29 19:18:07 +01:00
/**
* Calculate the corresponding public key for encryption key used to
* encrypt/decrypt {@link broadcast} objects.
* @return {Buffer} A 65-byte public key.
*/
Address.prototype.getBroadcastPublicKey = function() {
return bmcrypto.getPublic(this.getBroadcastPrivateKey());
};
2015-01-18 12:37:09 +01:00
/**
* Calculate the address tag.
* @return {Buffer} A 32-byte address tag.
*/
Address.prototype.getTag = function() {
return bmcrypto.sha512(getaddrhash(this)).slice(32);
2015-01-18 12:37:09 +01:00
};
// 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:
2014-12-29 23:16:51 +01:00
throw new Error("Bad version");
}
}
2014-12-27 22:04:23 +01:00
// 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.
*/
2014-12-29 23:16:51 +01:00
Address.prototype.encode = function() {
var data = Buffer.concat([
var_int.encode(this.version),
var_int.encode(this.stream),
this.getShortRipe(),
]);
2015-01-08 03:00:19 +01:00
var addr = Buffer.concat([data, getaddrchecksum(data)]);
return "BM-" + bs58.encode(addr);
};
/**
2014-12-29 23:16:51 +01:00
* Create new Bitmessage address from random encryption and signing
* private keys.
* @param {?Object} opts - Address options
* @return {Address} Generated address object.
*/
2014-12-29 23:16:51 +01:00
Address.fromRandom = function(opts) {
2015-01-06 12:06:15 +01:00
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.
2015-01-18 13:34:02 +01:00
var encPrivateKey, encPublicKey, ripe, len;
2014-12-29 23:16:51 +01:00
var signPrivateKey = bmcrypto.getPrivate();
var signPublicKey = bmcrypto.getPublic(signPrivateKey);
2015-01-18 13:34:02 +01:00
var keysbuf = new Buffer(130);
signPublicKey.copy(keysbuf);
while (true) {
encPrivateKey = bmcrypto.getPrivate();
encPublicKey = bmcrypto.getPublic(encPrivateKey);
encPublicKey.copy(keysbuf, 65);
ripe = bmcrypto.ripemd160(bmcrypto.sha512(keysbuf));
2015-01-18 13:34:02 +01:00
len = getripelen(ripe);
if (len <= ripelen && checkripelen(len, version)) {
opts.signPrivateKey = signPrivateKey;
opts.encPrivateKey = encPrivateKey;
return new Address(opts);
}
}
};
2014-12-29 23:16:51 +01:00
2015-01-18 13:34:02 +01:00
/**
* Create new Bitmessage address from passphrase.
* @param {?Object} opts - Address options
* @return {Address} Generated address object.
*/
Address.fromPassphrase = function(opts) {
2015-01-19 13:08:56 +01:00
if (typeof opts === "string") {
opts = {passphrase: opts};
} else {
opts = objectAssign({}, opts);
}
2015-01-18 13:34:02 +01:00
var version = opts.version = opts.version || 4;
var ripelen = popkey(opts, "ripeLength") || 19;
2015-01-18 13:34:02 +01:00
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 = new Buffer(130);
// XXX(Kagami): Spec doesn't mention encoding, using UTF-8.
var phrasebuf = new Buffer(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;
2015-01-18 16:27:09 +01:00
}
2015-01-18 13:34:02 +01:00
};
2015-01-18 17:38:23 +01:00
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 = new Buffer(20);
fullripe.fill(0);
ripe.copy(fullripe, 20 - ripe.length);
ripe = fullripe;
}
this._ripe = ripe;
},
});
2014-12-29 23:16:51 +01:00
module.exports = Address;