diff --git a/README.md b/README.md index e0733fe..85e7f15 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] HMAC-SHA-256 - [x] Common structures - [x] message + - [x] object - [x] var_int - [x] var_str - [x] var_int_list @@ -45,10 +46,9 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] inv - [x] getdata - [x] error - - [x] object - [ ] Object types - [x] getpubkey - - [ ] pubkey + - [x] pubkey - [ ] msg - [ ] broadcast - [x] WIF diff --git a/lib/util.js b/lib/_util.js similarity index 69% rename from lib/util.js rename to lib/_util.js index 0ee87a8..f891a87 100644 --- a/lib/util.js +++ b/lib/_util.js @@ -1,3 +1,6 @@ +// NOTE(Kagami): End-users shouldn't use this module. While it exports +// some helper routines, its API is _not_ stable. + "use strict"; var assert = exports.assert = function(condition, message) { @@ -6,6 +9,8 @@ var assert = exports.assert = function(condition, message) { } }; +exports.PROTOCOL_VERSION = 3; + // Missing methods to read/write 64 bits integers from/to buffers. // TODO(Kagami): Use this helpers in structs, pow, platform. @@ -61,3 +66,31 @@ exports.tnow = function() { var time = new Date(); return Math.floor(time.getTime() / 1000); }; + +var DEFAULT_TRIALS_PER_BYTE = 1000; +var DEFAULT_EXTRA_BYTES = 1000; + +exports.getTrials = function(opts) { + var nonceTrialsPerByte = opts.nonceTrialsPerByte; + // Automatically raise lower values per spec. + if (!nonceTrialsPerByte || nonceTrialsPerByte < DEFAULT_TRIALS_PER_BYTE) { + nonceTrialsPerByte = DEFAULT_TRIALS_PER_BYTE; + } + return nonceTrialsPerByte; +}; + +exports.getExtraBytes = function(opts) { + var payloadLengthExtraBytes = opts.payloadLengthExtraBytes; + // Automatically raise lower values per spec. + if (!payloadLengthExtraBytes || + payloadLengthExtraBytes < DEFAULT_EXTRA_BYTES) { + payloadLengthExtraBytes = DEFAULT_EXTRA_BYTES; + } + return payloadLengthExtraBytes; +}; + +exports.popkey = function(obj, key) { + var value = obj[key]; + delete obj[key]; + return value; +}; diff --git a/lib/address.js b/lib/address.js index f399536..598b010 100644 --- a/lib/address.js +++ b/lib/address.js @@ -9,10 +9,11 @@ var objectAssign = Object.assign || require("object-assign"); var bufferEqual = require("buffer-equal"); var bs58 = require("bs58"); -var assert = require("./util").assert; +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. @@ -103,21 +104,40 @@ Address.prototype.getRipe = function(opts) { } }; +function getdoublehash(addr) { + var ripe = addr.getRipe(); + var dataToHash = Buffer.concat([ + var_int.encode(addr.version), + var_int.encode(addr.stream), + ripe, + ]); + return bmcrypto.sha512(bmcrypto.sha512(dataToHash)); +} + +/** + * Calculate the encryption key used to encrypt/decrypt {@link pubkey} + * and {@link broadcast} objects. + * @return {Buffer} A 32-byte private key. + * @static + */ +var getPubkeyPrivateKey = Address.prototype.getPubkeyPrivateKey = function() { + return getdoublehash(this).slice(0, 32); +}; + +/** + * Just an alias to {@link Address#getPubkeyPrivateKey} for convinience. + */ +Address.prototype.getBroadcastPrivateKey = getPubkeyPrivateKey; + /** * Calculate the address tag. * @return {Buffer} A 32-byte address tag. */ Address.prototype.getTag = function() { - var ripe = this.getRipe(); - var dataToHash = Buffer.concat([ - var_int.encode(this.version), - var_int.encode(this.stream), - ripe, - ]); - var hash = bmcrypto.sha512(bmcrypto.sha512(dataToHash)); - return hash.slice(32); + return getdoublehash(this).slice(32); }; + // Get truncated ripe hash length. function getripelen(ripe) { var zeroes = 0; @@ -177,12 +197,6 @@ Address.prototype.encode = function() { 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. diff --git a/lib/crypto.js b/lib/crypto.js index 4993f8d..654a145 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -7,15 +7,19 @@ "use strict"; var eccrypto = require("eccrypto"); +var assert = require("./_util").assert; var platform = require("./platform"); +var promise = platform.promise; + /** * Calculate SHA-1 hash. * @param {Buffer} buf - Input data * @return {Buffer} Resulting hash. * @function + * @static */ -exports.sha1 = platform.sha1; +var sha1 = exports.sha1 = platform.sha1; /** * Calculate SHA-256 hash. @@ -64,3 +68,107 @@ exports.getPrivate = function() { * @function */ exports.getPublic = eccrypto.getPublic; + +/** + * Sign message using ecdsa-with-sha1 scheme. + * @param {Buffer} privateKey - A 32-byte private key + * @msg {Buffer} msg - The message being signed + * @return {Promise.} A promise that contains signature in DER + * format when fulfilled. + */ +exports.sign = function(privateKey, msg) { + var hash = sha1(msg); + return eccrypto.sign(privateKey, hash); +}; + +/** + * Verify signature using ecdsa-with-sha1 scheme. + * @param {Buffer} publicKey - A 65-byte public key + * @msg {Buffer} msg - The message being verified + * @param {Buffer} sig - The signature in DER format + * @return {Promise.} A promise that resolves on correct + * signature and rejects on bad key or signature. + */ +exports.verify = function(publicKey, msg, sig) { + var hash = sha1(msg); + return eccrypto.verify(publicKey, hash, sig); +}; + +var SECP256K1_TYPE = 714; + +// We define this structure here to avoid circular imports. However we +// rexport and document it in `structs` module for consistency. +var encrypted = exports.encrypted = { + decode: function(buf) { + assert(buf.length >= 118, "Buffer is too small"); + assert(buf.readUInt16BE(16, true) === SECP256K1_TYPE, "Bad curve type"); + assert(buf.readUInt16BE(18, true) === 32, "Bad Rx length"); + assert(buf.readUInt16BE(52, true) === 32, "Bad Ry length"); + var iv = new Buffer(16); + buf.copy(iv, 0, 0, 16); + var ephemPublicKey = new Buffer(65); + ephemPublicKey[0] = 0x04; + buf.copy(ephemPublicKey, 1, 20, 52); + buf.copy(ephemPublicKey, 33, 54, 86); + // NOTE(Kagami): We do copy instead of slice to protect against + // possible source buffer modification by user. + var ciphertext = new Buffer(buf.length - 118); + buf.copy(ciphertext, 0, 86, buf.length - 32); + var mac = new Buffer(32); + buf.copy(mac, 0, buf.length - 32); + return { + iv: iv, + ephemPublicKey: ephemPublicKey, + ciphertext: ciphertext, + mac: mac, + }; + }, + + encode: function(opts) { + assert(opts.iv.length === 16, "Bad IV"); + assert(opts.ephemPublicKey.length === 65, "Bad public key"); + assert(opts.mac.length === 32, "Bad MAC"); + // 16 + 2 + 2 + 32 + 2 + 32 + ? + 32 + var buf = new Buffer(118 + opts.ciphertext.length); + opts.iv.copy(buf); + buf.writeUInt16BE(SECP256K1_TYPE, 16, true); // Curve type + buf.writeUInt16BE(32, 18, true); // Rx length + opts.ephemPublicKey.copy(buf, 20, 1, 33); // Rx + buf.writeUInt16BE(32, 52, true); // Ry length + opts.ephemPublicKey.copy(buf, 54, 33); // Ry + opts.ciphertext.copy(buf, 86); + opts.mac.copy(buf, 86 + opts.ciphertext.length); + return buf; + }, +}; + +/** + * Encrypt message for given recepient's public key. + * @param {Buffer} publicKeyTo - Recipient's public key (65 bytes) + * @param {Buffer} msg - The message being encrypted + * @param {?{?iv: Buffer, ?ephemPrivateKey: Buffer}} opts - You may also + * specify initialization vector (16 bytes) and ephemeral private key + * (32 bytes) to get deterministic results. + * @return {Promise.} - A promise that resolves with the buffer + * in `encrypted` format successful encryption and rejects on failure. + */ +exports.encrypt = function(publicKeyTo, msg, opts) { + return eccrypto.encrypt(publicKeyTo, msg, opts).then(function(encObj) { + return encrypted.encode(encObj); + }); +}; + +/** + * Decrypt message using given private key. + * @param {Buffer} privateKey - A 32-byte private key of recepient of + * the mesage + * @param {Buffer} buf - encrypted data + * @return {Promise.} - A promise that resolves with the + * plaintext on successful decryption and rejects on failure. + */ +exports.decrypt = function(privateKey, buf) { + return new promise(function(resolve) { + var encObj = encrypted.decode(buf); + resolve(eccrypto.decrypt(privateKey, encObj)); + }); +}; diff --git a/lib/index.js b/lib/index.js index 5598022..03c72f1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,7 @@ "use strict"; /** Current protocol version. */ -exports.PROTOCOL_VERSION = 3; +exports.PROTOCOL_VERSION = require("./_util").PROTOCOL_VERSION; /** [Common structures.]{@link module:bitmessage/structs} */ exports.structs = require("./structs"); diff --git a/lib/messages.js b/lib/messages.js index de99091..78b4f4c 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -9,11 +9,12 @@ "use strict"; -var assert = require("./util").assert; +var assert = require("./_util").assert; var structs = require("./structs"); -var ServicesBitfield = structs.ServicesBitfield; var UserAgent = require("./user-agent"); -var util = require("./util"); +var util = require("./_util"); + +var ServicesBitfield = structs.ServicesBitfield; /** * `version` message. @@ -77,7 +78,7 @@ exports.version = { var streamNumbers = opts.streamNumbers || [1]; // Start encoding. var protoVersion = new Buffer(4); - protoVersion.writeUInt32BE(require("./").PROTOCOL_VERSION, 0); + protoVersion.writeUInt32BE(util.PROTOCOL_VERSION, 0); var addrRecv = structs.net_addr.encode({ services: services, host: opts.remoteHost, @@ -263,91 +264,3 @@ var error = exports.error = { ]); }, }; - -/** - * `object` message. An `object` is a message which is shared throughout - * a stream. It is the only message which propagates; all others are - * only between two nodes. - * NOTE: You shouldn't use `encode` and `decode` methods directly. - * Instead, get type of the object and process it using appropriate - * namespace from `objects` module. - * @see {@link https://bitmessage.org/wiki/Protocol_specification#object} - * @namespace - */ -exports.object = { - // Known types. - GETPUBKEY: 0, - PUBKEY: 1, - MSG: 2, - BROADCAST: 3, - - /** - * Returns type of the `object` message if we can parse it. Per spec - * we should still relay unknown objects. - * @param {Buffer} buf - Message payload - * @return {?number} Object type. - */ - getType: function(buf) { - assert(buf.length >= 22, "object message payload is too small"); - return buf.readUInt32BE(16, true); - }, - - /** - * Decode `object` message payload. - * NOTE: `nonce` and `payload` are copied. - * @param {Buffer} buf - Message payload - * @return {Object} Decoded `object` structure. - */ - // FIXME(Kagami): Check for POW. - // TODO(Kagami): Option to not fail on bad POW (may be useful for - // bitchan). - // TODO(Kagami): Option to not fail on expired object (would be useful - // for bitchan). - decode: function(buf) { - // 8 + 8 + 4 + (1+) + (1+) - assert(buf.length >= 22, "object message payload is too small"); - var nonce = new Buffer(8); - buf.copy(nonce, 0, 0, 8); - var expiresTime = util.readTimestamp64BE(buf.slice(8, 16)); - var ttl = expiresTime - util.tnow(); - assert(ttl >= -3600, "Object expired more than a hour ago"); - assert(ttl <= 2430000, "expiresTime is too far in the future"); - var type = buf.readUInt32BE(16, true); - var decodedVersion = structs.var_int.decode(buf.slice(20)); - var decodedStream = structs.var_int.decode(decodedVersion.rest); - var payload = new Buffer(decodedStream.rest.length); - decodedStream.rest.copy(payload); - return { - nonce: nonce, - ttl: ttl, - type: type, - version: decodedVersion.value, - stream: decodedStream.value, - payload: payload, - }; - }, - - /** - * Encode `object` message payload. - * @param {Object} opts - Object options - * @return {Buffer} Encoded payload. - */ - // TODO(Kagami): Do a POW if nonce is not provided. - encode: function(opts) { - assert(opts.nonce.length === 8, "Bad nonce"); - assert(opts.ttl > 0, "Bad TTL"); - assert(opts.ttl <= 2430000, "TTL may not be larger than 28 days + 3 hours"); - var expiresTime = util.tnow() + opts.ttl; - var type = new Buffer(4); - type.writeUInt32BE(opts.type, 0); - var stream = opts.stream || 1; - return Buffer.concat([ - opts.nonce, - util.writeUInt64BE(null, expiresTime), - type, - structs.var_int.encode(opts.version), - structs.var_int.encode(stream), - opts.payload, - ]); - }, -}; diff --git a/lib/objects.js b/lib/objects.js index 1e18e7c..e40fdf1 100644 --- a/lib/objects.js +++ b/lib/objects.js @@ -6,14 +6,21 @@ * @module bitmessage/objects */ // TODO(Kagami): Document object-like params. +// FIXME(Kagami): Think through API, we may want to get decoded +// structure even if it contains unsupported version. Also error +// handling may need some refactoring. "use strict"; var objectAssign = Object.assign || require("object-assign"); -var assert = require("./util").assert; +var assert = require("./_util").assert; var promise = require("./platform").promise; -var object = require("./messages").object; +var bmcrypto = require("./crypto"); var Address = require("./address"); +var var_int = require("./structs").var_int; +var PubkeyBitfield = require("./structs").PubkeyBitfield; +var object = require("./structs").object; +var util = require("./_util"); /** * `getpubkey` object. When a node has the hash of a public key (from an @@ -26,13 +33,15 @@ exports.getpubkey = { /** * Decode `getpubkey` object message payload. * @param {Buffer} buf - Message payload - * @return {Promise.} A promise that contained decoded + * @return {Promise.} A promise that contains decoded * `getpubkey` object structure when fulfilled. */ decodeAsync: function(buf) { return new promise(function(resolve) { var decoded = object.decode(buf); assert(decoded.type === object.GETPUBKEY, "Wrong object type"); + assert(decoded.version >= 2, "getpubkey version is too low"); + assert(decoded.version <= 4, "getpubkey version is too high"); var payload = decoded.payload; delete decoded.payload; if (decoded.version < 4) { @@ -51,18 +60,265 @@ exports.getpubkey = { /** * Encode `getpubkey` object message payload. * @param {Object} opts - `getpubkey` object options - * @return {Promise.} A promise that contained encoded message + * @return {Promise.} A promise that contains encoded message * payload when fulfilled. */ + // FIXME(Kagami): Do a POW. encodeAsync: function(opts) { return new promise(function(resolve) { opts = objectAssign({}, opts); opts.type = object.GETPUBKEY; - var addr = Address.decode(opts.to); - opts.version = addr.version; - opts.stream = addr.stream; - opts.payload = addr.version < 4 ? addr.getRipe() : addr.getTag(); + // Bitmessage address of recepeint of `getpubkey` message. + var to = Address.decode(opts.to); + assert(to.version >= 2, "Address version is too low"); + assert(to.version <= 4, "Address version is too high"); + opts.version = to.version; + opts.stream = to.stream; + opts.payload = to.version < 4 ? to.getRipe() : to.getTag(); + // POW calculation here. + var nonce = new Buffer(8); + opts.nonce = nonce; resolve(object.encode(opts)); }); }, }; + +// Helper function for `pubkey.decode`. +function extractPubkeyV2(payload) { + var decoded = {}; + // Payload is copied so it's safe to return it right away. + decoded.behavior = PubkeyBitfield(payload.slice(0, 4)); + var signPublicKey = decoded.signPublicKey = new Buffer(65); + signPublicKey[0] = 4; + payload.copy(signPublicKey, 1, 4, 68); + var encPublicKey = decoded.encPublicKey = new Buffer(65); + encPublicKey[0] = 4; + payload.copy(encPublicKey, 1, 68, 132); + return decoded; +} + +// Helper function for `pubkey.decode`. +function extractPubkeyV3(payload) { + var decoded = {}; + var length = 0; + var decodedTrials = var_int.decode(payload); + decoded.nonceTrialsPerByte = decodedTrials.value; + length += decodedTrials.length; + var decodedExtraBytes = var_int.decode(decodedTrials.rest); + decoded.payloadLengthExtraBytes = decodedExtraBytes.value; + length += decodedExtraBytes.length; + var decodedSigLength = var_int.decode(decodedExtraBytes.rest); + decoded.signature = decodedSigLength.rest.slice(0, decodedSigLength.value); + var siglen = decodedSigLength.length + decodedSigLength.value; + length += siglen; + // Internal value. + decoded._siglen = siglen; + decoded.length = length; + return decoded; +} + +/** + * `pubkey` object. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#pubkey} + * @namespace + */ +exports.pubkey = { + /** + * Decode `pubkey` object message payload. + * @param {Buffer} buf - Message payload + * @param {?Object} opts - Decoding options + * @return {Promise.} A promise that contains decoded `pubkey` + * object structure when fulfilled. + */ + decodeAsync: function(buf, opts) { + return new promise(function(resolve, reject) { + opts = opts || {}; + var neededPubkeys = opts.neededPubkeys || {}; + var decoded = object.decode(buf); + assert(decoded.type === object.PUBKEY, "Wrong object type"); + var payload = decoded.payload; + delete decoded.payload; + var version = decoded.version; + assert(version >= 2, "Address version is too low"); + assert(version <= 4, "Address version is too high"); + var siglen, sig, dataToVerify, pubkeyp; + var addr, addrs, tag, pubkeyEncPrivateKey, dataToDecrypt; + var length = 132; + + // v2 pubkey. + if (version === 2) { + // 4 + 64 + 64 + assert(payload.length === 132, "Bad pubkey v2 object payload length"); + objectAssign(decoded, extractPubkeyV2(payload)); + // Real data length. + decoded.length = length; + return resolve(decoded); + } + + // v3 pubkey. + if (version === 3) { + // 4 + 64 + 64 + (1+) + (1+) + (1+) + assert(payload.length >= 135, "Bad pubkey v3 object payload length"); + objectAssign(decoded, extractPubkeyV2(payload)); + objectAssign(decoded, extractPubkeyV3(payload.slice(132))); + siglen = util.popkey(decoded, "_siglen"); + length += decoded.length; + // Real data length. + decoded.length = length; + // Object message payload without nonce up to sigLength. + dataToVerify = buf.slice(8, decoded.headerLength + length - siglen); + sig = decoded.signature; + pubkeyp = bmcrypto.verify(decoded.signPublicKey, dataToVerify, sig) + .then(function() { + return decoded; + }); + return resolve(pubkeyp); + } + + // v4 pubkey. + + // `neededPubkeys` is either single address or addresses array or + // Object key-by-tag. Time to match the tag is O(1), O(n), O(1) + // respectfully. + if (Address.isAddress(neededPubkeys)) { + addr = neededPubkeys; + neededPubkeys = {}; + neededPubkeys[addr.getTag()] = addr.getPubkeyPrivateKey(); + } else if (Array.isArray(neededPubkeys)) { + addrs = neededPubkeys; + neededPubkeys = {}; + addrs.forEach(function(a) { + neededPubkeys[a.getTag()] = a.getPubkeyPrivateKey(); + }); + } + + assert(payload.length >= 32, "Bad pubkey v4 object payload length"); + tag = decoded.tag = payload.slice(0, 32); + pubkeyEncPrivateKey = neededPubkeys[tag]; + if (!pubkeyEncPrivateKey) { + return reject(new Error("You are not interested in this pubkey v4")); + } + dataToDecrypt = payload.slice(32); + pubkeyp = bmcrypto.decrypt(pubkeyEncPrivateKey, dataToDecrypt) + .then(function(decrypted) { + // 4 + 64 + 64 + (1+) + (1+) + (1+) + assert( + decrypted.length >= 135, + "Bad pubkey v4 object payload length"); + objectAssign(decoded, extractPubkeyV2(decrypted)); + objectAssign(decoded, extractPubkeyV3(decrypted.slice(132))); + siglen = util.popkey(decoded, "_siglen"); + length += decoded.length; + // Real data length. + // Since data is encrypted, entire payload is used. + decoded.length = payload.length; + dataToVerify = Buffer.concat([ + // Object header without nonce + tag. + buf.slice(8, decoded.headerLength + 32), + // Unencrypted pubkey data without signature. + decrypted.slice(0, length - siglen), + ]); + sig = decoded.signature; + return bmcrypto.verify(decoded.signPublicKey, dataToVerify, sig); + }).then(function() { + return decoded; + }); + resolve(pubkeyp); + }); + }, + + /** + * Encode `pubkey` object message payload. + * @param {Object} opts - `pubkey` object options + * @return {Promise.} A promise that contains encoded message + * payload when fulfilled. + */ + // FIXME(Kagami): Do a POW. + encodeAsync: function(opts) { + return new promise(function(resolve) { + opts = objectAssign({}, opts); + opts.type = object.PUBKEY; + // Originator of `pubkey` message. + var from = Address.decode(opts.from); + var nonceTrialsPerByte = util.getTrials(from); + var payloadLengthExtraBytes = util.getExtraBytes(from); + // Bitmessage address of recepient of `pubkey` message. + var to, version, stream; + if (opts.to) { + to = Address.decode(opts.to); + version = to.version; + stream = to.stream; + } else { + version = opts.version || 4; + stream = opts.stream || 1; + } + assert(version >= 2, "Address version is too low"); + assert(version <= 4, "Address version is too high"); + opts.version = version; + opts.stream = stream; + var obj, pubkeyp; + + // v2 pubkey. + if (version === 2) { + opts.payload = Buffer.concat([ + from.behavior.buffer, + from.signPublicKey.slice(1), + from.encPublicKey.slice(1), + ]); + obj = object.encodeWithoutNonce(opts); + // POW calculation here. + var nonce = new Buffer(8); + obj = Buffer.concat([nonce, obj]); + return resolve(obj); + } + + var pubkeyData = [ + from.behavior.buffer, + from.signPublicKey.slice(1), + from.encPublicKey.slice(1), + var_int.encode(nonceTrialsPerByte), + var_int.encode(payloadLengthExtraBytes), + ]; + + // v3 pubkey. + if (version === 3) { + opts.payload = Buffer.concat(pubkeyData); + obj = object.encodeWithoutNonce(opts); + pubkeyp = bmcrypto + .sign(from.signPrivateKey, obj) + .then(function(sig) { + // POW calculation here. + var nonce = new Buffer(8); + // Append signature to the encoded object and we are done. + return Buffer.concat([ + nonce, + obj, + var_int.encode(sig.length), + sig, + ]); + }); + return resolve(pubkeyp); + } + + // v4 pubkey. + opts.payload = from.getTag(); + obj = object.encodeWithoutNonce(opts); + var dataToSign = Buffer.concat([obj].concat(pubkeyData)); + var pubkeyEncPrivateKey = from.getPubkeyPrivateKey(); + var pubkeyEncPublicKey = bmcrypto.getPublic(pubkeyEncPrivateKey); + pubkeyp = bmcrypto + .sign(from.signPrivateKey, dataToSign) + .then(function(sig) { + var dataToEnc = pubkeyData.concat(var_int.encode(sig.length), sig); + dataToEnc = Buffer.concat(dataToEnc); + return bmcrypto.encrypt(pubkeyEncPublicKey, dataToEnc); + }).then(function(enc) { + // POW calculation here. + var nonce = new Buffer(8); + // Concat object header with ecnrypted data and we are done. + return Buffer.concat([nonce, obj, enc]); + }); + resolve(pubkeyp); + }); + }, +}; diff --git a/lib/platform.browser.js b/lib/platform.browser.js index 90b7bd1..7b54d3e 100644 --- a/lib/platform.browser.js +++ b/lib/platform.browser.js @@ -14,7 +14,9 @@ var hash = require("hash.js"); var Sha512 = require("sha.js/sha512"); var BN = require("bn.js"); var work = require("webworkify"); -var assert = require("./util").assert; +var assert = require("./_util").assert; + +var cryptoObj = window.crypto || window.msCrypto; exports.sha1 = function(buf) { return new Buffer(hash.sha1().update(buf).digest()); @@ -34,7 +36,7 @@ exports.ripemd160 = function(buf) { exports.randomBytes = function(size) { var arr = new Uint8Array(size); - window.crypto.getRandomValues(arr); + cryptoObj.getRandomValues(arr); return new Buffer(arr); }; @@ -139,4 +141,4 @@ exports.pow = function(opts) { return powp; }; -exports.promise = Promise; +exports.promise = window.Promise; diff --git a/lib/platform.js b/lib/platform.js index 38b885d..366597d 100644 --- a/lib/platform.js +++ b/lib/platform.js @@ -10,7 +10,7 @@ var promise = typeof Promise === "undefined" ? require("es6-promise").Promise : Promise; var bignum = require("bignum"); -var assert = require("./util").assert; +var assert = require("./_util").assert; var worker = require("./worker"); var createHash = crypto.createHash; diff --git a/lib/pow.js b/lib/pow.js index ffac708..74f2cdd 100644 --- a/lib/pow.js +++ b/lib/pow.js @@ -10,9 +10,7 @@ var objectAssign = Object.assign || require("object-assign"); var bmcrypto = require("./crypto"); var platform = require("./platform"); - -var DEFAULT_TRIALS_PER_BYTE = 1000; -var DEFAULT_EXTRA_BYTES = 1000; +var util = require("./_util"); /** * Calculate target @@ -22,20 +20,11 @@ var DEFAULT_EXTRA_BYTES = 1000; // Just a wrapper around platform-specific implementation. exports.getTarget = function(opts) { var payloadLength = opts.payloadLength || opts.payload.length; - var nonceTrialsPerByte = opts.nonceTrialsPerByte; - // Automatically raise lower values per spec. - if (!nonceTrialsPerByte || nonceTrialsPerByte < DEFAULT_TRIALS_PER_BYTE) { - nonceTrialsPerByte = DEFAULT_TRIALS_PER_BYTE; - } - var payloadLengthExtraBytes = opts.payloadLengthExtraBytes; - if (!payloadLengthExtraBytes || payloadLengthExtraBytes < DEFAULT_EXTRA_BYTES) { - payloadLengthExtraBytes = DEFAULT_EXTRA_BYTES; - } return platform.getTarget({ ttl: opts.ttl, payloadLength: payloadLength, - nonceTrialsPerByte: nonceTrialsPerByte, - payloadLengthExtraBytes: payloadLengthExtraBytes, + nonceTrialsPerByte: util.getTrials(opts), + payloadLengthExtraBytes: util.getExtraBytes(opts), }); }; diff --git a/lib/structs.js b/lib/structs.js index fa63ffc..c0991cc 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -9,8 +9,9 @@ var objectAssign = Object.assign || require("object-assign"); var bufferEqual = require("buffer-equal"); -var assert = require("./util").assert; +var assert = require("./_util").assert; var bmcrypto = require("./crypto"); +var util = require("./_util"); function isAscii(str) { for (var i = 0; i < str.length; i++) { @@ -91,6 +92,95 @@ var message = exports.message = { }, }; +/** + * `object` message. An `object` is a message which is shared throughout + * a stream. It is the only message which propagates; all others are + * only between two nodes. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#object} + * @namespace + * @static + */ +var object = exports.object = { + // Known types. + GETPUBKEY: 0, + PUBKEY: 1, + MSG: 2, + BROADCAST: 3, + + /** + * Decode `object` message payload. + * NOTE: `nonce` and `payload` are copied. + * @param {Buffer} buf - Message payload + * @return {Object} Decoded `object` structure. + */ + // FIXME(Kagami): Check a POW. + // TODO(Kagami): Option to not fail on bad POW (may be useful for + // bitchan). + // TODO(Kagami): Option to not fail on expired objects (would be + // useful for bitchan). + decode: function(buf) { + // 8 + 8 + 4 + (1+) + (1+) + assert(buf.length >= 22, "object message payload is too small"); + var nonce = new Buffer(8); + buf.copy(nonce, 0, 0, 8); + var expiresTime = util.readTimestamp64BE(buf.slice(8, 16)); + var ttl = expiresTime - util.tnow(); + assert(ttl >= -3600, "Object expired more than a hour ago"); + assert(ttl <= 2430000, "expiresTime is too far in the future"); + var type = buf.readUInt32BE(16, true); + var decodedVersion = var_int.decode(buf.slice(20)); + var decodedStream = var_int.decode(decodedVersion.rest); + var headerLength = 20 + decodedVersion.length + decodedStream.length; + var payload = new Buffer(decodedStream.rest.length); + decodedStream.rest.copy(payload); + return { + nonce: nonce, + ttl: ttl, + type: type, + version: decodedVersion.value, + stream: decodedStream.value, + headerLength: headerLength, + payload: payload, + }; + }, + + /** + * Encode `object` message payload without leading nonce field (may be + * useful if you are going to calculate it later). + * @param {Object} opts - Object options + * @return {Buffer} Encoded payload. + */ + encodeWithoutNonce: function(opts) { + assert(opts.ttl > 0, "Bad TTL"); + assert(opts.ttl <= 2430000, "TTL may not be larger than 28 days + 3 hours"); + var expiresTime = util.tnow() + opts.ttl; + var type = new Buffer(4); + type.writeUInt32BE(opts.type, 0); + var stream = opts.stream || 1; + return Buffer.concat([ + util.writeUInt64BE(null, expiresTime), + type, + var_int.encode(opts.version), + var_int.encode(stream), + opts.payload, + ]); + }, + + /** + * Encode `object` message payload. + * @param {Object} opts - Object options + * @return {Buffer} Encoded payload. + */ + encode: function(opts) { + assert(opts.nonce.length === 8, "Bad nonce"); + // NOTE(Kagami): This may be a bit inefficient since we allocate + // twice. + return Buffer.concat([ + opts.nonce, + object.encodeWithoutNonce(opts), + ]); + }, +}; /** * Variable length integer. @@ -435,67 +525,26 @@ exports.inv_vect = { }, }; -var SECP256K1_TYPE = 714; - /** * Encrypted payload. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Encrypted_payload} - * @namespace + * @namespace encrypted */ -exports.encrypted = { - /** - * Decode encrypted payload. - * NOTE: all structure members are copied. - * @param {Buffer} buf - A buffer that contains encrypted payload - * @return {Object} Decoded encrypted structure. - */ - decode: function(buf) { - assert(buf.length >= 118, "Buffer is too small"); - assert(buf.readUInt16BE(16, true) === SECP256K1_TYPE, "Bad curve type"); - assert(buf.readUInt16BE(18, true) === 32, "Bad Rx length"); - assert(buf.readUInt16BE(52, true) === 32, "Bad Ry length"); - var iv = new Buffer(16); - buf.copy(iv, 0, 0, 16); - var ephemPublicKey = new Buffer(65); - ephemPublicKey[0] = 0x04; - buf.copy(ephemPublicKey, 1, 20, 52); - buf.copy(ephemPublicKey, 33, 54, 86); - // NOTE(Kagami): We do copy instead of slice to protect against - // possible source buffer modification by user. - var ciphertext = new Buffer(buf.length - 118); - buf.copy(ciphertext, 0, 86, buf.length - 32); - var mac = new Buffer(32); - buf.copy(mac, 0, buf.length - 32); - return { - iv: iv, - ephemPublicKey: ephemPublicKey, - ciphertext: ciphertext, - mac: mac, - }; - }, - - /** - * Encode `encrypted`. - * @param {Object} opts - Encode options - * @return {Buffer} Encoded encrypted payload. - */ - encode: function(opts) { - assert(opts.iv.length === 16, "Bad IV"); - assert(opts.ephemPublicKey.length === 65, "Bad public key"); - assert(opts.mac.length === 32, "Bad MAC"); - // 16 + 2 + 2 + 32 + 2 + 32 + ? + 32 - var buf = new Buffer(118 + opts.ciphertext.length); - opts.iv.copy(buf); - buf.writeUInt16BE(SECP256K1_TYPE, 16, true); // Curve type - buf.writeUInt16BE(32, 18, true); // Rx length - opts.ephemPublicKey.copy(buf, 20, 1, 33); // Rx - buf.writeUInt16BE(32, 52, true); // Ry length - opts.ephemPublicKey.copy(buf, 54, 33); // Ry - opts.ciphertext.copy(buf, 86); - opts.mac.copy(buf, 86 + opts.ciphertext.length); - return buf; - }, -}; +/** + * Decode encrypted payload. + * NOTE: all structure members are copied. + * @param {Buffer} buf - A buffer that contains encrypted payload + * @return {Object} Decoded encrypted structure. + * @function encrypted.decode + */ +/** + * Encode `encrypted`. + * @param {Object} opts - Encode options + * @return {Buffer} Encoded encrypted payload. + * @function encrypted.encode + */ +// Reexport struct. +exports.encrypted = bmcrypto.encrypted; // Creates bitfield (MSB 0) class of the specified size. var Bitfield = function(size) { diff --git a/lib/wif.js b/lib/wif.js index 8e77931..7bb589e 100644 --- a/lib/wif.js +++ b/lib/wif.js @@ -8,7 +8,7 @@ var bufferEqual = require("buffer-equal"); var bs58 = require("bs58"); -var assert = require("./util").assert; +var assert = require("./_util").assert; var bmcrypto = require("./crypto"); // Compute the WIF checksum for the given data. diff --git a/package.json b/package.json index 78425d1..bd2133c 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "bn.js": "^1.0.0", "bs58": "^2.0.0", "buffer-equal": "~0.0.1", - "eccrypto": "^0.9.0", + "eccrypto": "^0.9.3", "es6-promise": "^2.0.1", "hash.js": "^1.0.2", "nan": "^1.4.1", diff --git a/test.js b/test.js index a276e9d..784ef94 100644 --- a/test.js +++ b/test.js @@ -8,6 +8,7 @@ var bitmessage = require("./lib"); var bmcrypto = require("./lib/crypto"); var structs = bitmessage.structs; var message = structs.message; +var object = structs.object; var var_int = structs.var_int; var var_str = structs.var_str; var var_int_list = structs.var_int_list; @@ -21,9 +22,9 @@ var version = messages.version; var addr = messages.addr; var inv = messages.inv; var error = messages.error; -var object = messages.object; var objects = bitmessage.objects; var getpubkey = objects.getpubkey; +var pubkey = objects.pubkey; var WIF = bitmessage.WIF; var POW = bitmessage.POW; var Address = bitmessage.Address; @@ -65,8 +66,48 @@ describe("Crypto", function() { expect(bytes[value]).to.be.below(7); } // Ideal sum = (255 / 2) * size = 12750 - expect(sum).to.be.above(10000); - expect(sum).to.be.below(15000); + expect(sum).to.be.above(5000); + expect(sum).to.be.below(20000); + }); + + it("should generate private keys", function() { + var privateKey = bmcrypto.getPrivate(); + expect(Buffer.isBuffer(privateKey)).to.be.true; + expect(privateKey.length).to.equal(32); + var sum = 0; + for (var i = 0; i < 32; i++) { sum += privateKey[i]; } + expect(sum).to.be.above(0); + expect(sum).to.be.below(8160); + }); + + it("should allow to convert private key to public", function() { + var privateKey = Buffer(32); + privateKey.fill(1); + expect(bmcrypto.getPublic(privateKey).toString("hex")).to.equal("041b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f70beaf8f588b541507fed6a642c5ab42dfdf8120a7f639de5122d47a69a8e8d1"); + }); + + it("should allow to sign and verify message", function() { + var privateKey = Buffer(32); + privateKey.fill(1); + var publicKey = bmcrypto.getPublic(privateKey); + var msg = Buffer("test"); + return bmcrypto.sign(privateKey, msg).then(function(sig) { + expect(Buffer.isBuffer(sig)).to.be.true; + expect(sig.toString("hex")).to.equal("304402204737396b697e5a3400e3aedd203d8be89879f97708647252bd0c17752ff4c8f302201d52ef234de82ce0719679fa220334c83b80e21b8505a781d32d94a27d9310aa"); + return bmcrypto.verify(publicKey, msg, sig); + }); + }); + + it("should allow to encrypt and decrypt message", function() { + var privateKeyA = bmcrypto.getPrivate(); + var publicKeyA = bmcrypto.getPublic(privateKeyA); + return bmcrypto.encrypt(publicKeyA, Buffer("msg to a")).then(function(buf) { + expect(Buffer.isBuffer(buf)).to.be.true; + return bmcrypto.decrypt(privateKeyA, buf).then(function(plaintext) { + expect(Buffer.isBuffer(plaintext)).to.be.true; + expect(plaintext.toString()).to.equal("msg to a"); + }); + }); }); }); @@ -93,6 +134,37 @@ describe("Common structures", function() { }); }); + describe("object", function() { + it("should encode and decode", function() { + var nonce = Buffer(8); + var res = object.decode(object.encode({ + nonce: nonce, + ttl: 100, + type: 2, + version: 1, + payload: Buffer("test"), + })); + + expect(bufferEqual(nonce, res.nonce)).to.be.true; + expect(res.ttl).to.be.at.least(100); + expect(res.type).to.equal(2); + expect(res.version).to.equal(1); + expect(res.stream).to.equal(1); + expect(res.headerLength).to.equal(22); + expect(res.payload.toString()).to.equal("test"); + }); + + it("shouldn't encode too big TTL", function() { + expect(object.encode.bind(null, { + nonce: Buffer(8), + ttl: 10000000, + type: 2, + version: 1, + payload: Buffer("test"), + })).to.throw(Error); + }); + }); + describe("var_int", function() { it("should decode", function() { var res; @@ -372,55 +444,12 @@ describe("Message types", function() { expect(res.length).to.equal(18); }); }); - - describe("object", function() { - it("should encode and decode", function() { - var nonce = Buffer(8); - var res = object.decode(object.encode({ - nonce: nonce, - ttl: 100, - type: 2, - version: 1, - payload: Buffer("test"), - })); - - expect(bufferEqual(nonce, res.nonce)).to.be.true; - expect(res.ttl).to.be.at.least(100); - expect(res.type).to.equal(2); - expect(res.version).to.equal(1); - expect(res.stream).to.equal(1); - expect(res.payload.toString()).to.equal("test"); - }); - - it("shouldn't encode too big TTL", function() { - expect(object.encode.bind(null, { - nonce: Buffer(8), - ttl: 10000000, - type: 2, - version: 1, - payload: Buffer("test"), - })).to.throw(Error); - }); - - it("should return type of the object", function() { - var encoded = object.encode({ - nonce: Buffer(8), - ttl: 100, - type: object.BROADCAST, - version: 1, - payload: Buffer("test"), - }); - expect(object.getType(encoded)).to.equal(3); - expect(object.decode(encoded).type).to.equal(3); - }); - }); }); describe("Object types", function() { describe("getpubkey", function() { it("should encode and decode getpubkey v3", function() { return getpubkey.encodeAsync({ - nonce: Buffer(8), ttl: 100, to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", }).then(getpubkey.decodeAsync) @@ -436,7 +465,6 @@ describe("Object types", function() { it("should encode and decode getpubkey v4", function() { return getpubkey.encodeAsync({ - nonce: Buffer(8), ttl: 100, to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z", }).then(getpubkey.decodeAsync) @@ -450,6 +478,72 @@ describe("Object types", function() { }); }); }); + + describe("pubkey", function() { + var signPrivateKey = Buffer("71c95d26c716a5e85e9af9efe26fb5f744dc98005a13d05d23ee92c77e038d9f", "hex"); + var signPublicKey = bmcrypto.getPublic(signPrivateKey); + var encPrivateKey = Buffer("9f9969c93c2d186787a7653f70e49be34c03c4a853e6ad0c867db0946bc433c6", "hex"); + var encPublicKey = bmcrypto.getPublic(encPrivateKey); + var from = Address({ + signPrivateKey: signPrivateKey, + encPrivateKey: encPrivateKey, + }); + + it("should encode and decode pubkey v2", function() { + return pubkey.encodeAsync({ + ttl: 123, + from: from, + to: "BM-onhypnh1UMhbQpmvdiPuG6soLLytYJAfH", + }).then(pubkey.decodeAsync) + .then(function(res) { + expect(res.ttl).to.equal(123); + expect(res.type).to.equal(object.PUBKEY); + expect(res.version).to.equal(2); + expect(res.stream).to.equal(1); + expect(res.behavior.get(PubkeyBitfield.DOES_ACK)).to.be.true; + expect(bufferEqual(res.signPublicKey, signPublicKey)).to.be.true; + expect(bufferEqual(res.encPublicKey, encPublicKey)).to.be.true; + expect(res.length).to.equal(132); + }); + }); + + it("should encode and decode pubkey v3", function() { + return pubkey.encodeAsync({ + ttl: 456, + from: from, + to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", + }).then(pubkey.decodeAsync) + .then(function(res) { + expect(res.ttl).to.equal(456); + expect(res.type).to.equal(object.PUBKEY); + expect(res.version).to.equal(3); + expect(res.stream).to.equal(1); + expect(res.behavior.get(PubkeyBitfield.DOES_ACK)).to.be.true; + expect(bufferEqual(res.signPublicKey, signPublicKey)).to.be.true; + expect(bufferEqual(res.encPublicKey, encPublicKey)).to.be.true; + expect(res.nonceTrialsPerByte).to.equal(1000); + expect(res.payloadLengthExtraBytes).to.equal(1000); + }); + }); + + it("should encode and decode pubkey v4", function() { + return pubkey.encodeAsync({ttl: 789, from: from, to: from}) + .then(function(buf) { + return pubkey.decodeAsync(buf, {neededPubkeys: from}); + }).then(function(res) { + expect(res.ttl).to.equal(789); + expect(res.type).to.equal(object.PUBKEY); + expect(res.version).to.equal(4); + expect(res.stream).to.equal(1); + expect(res.behavior.get(PubkeyBitfield.DOES_ACK)).to.be.true; + expect(bufferEqual(res.signPublicKey, signPublicKey)).to.be.true; + expect(bufferEqual(res.encPublicKey, encPublicKey)).to.be.true; + expect(res.nonceTrialsPerByte).to.equal(1000); + expect(res.payloadLengthExtraBytes).to.equal(1000); + expect(bufferEqual(res.tag, from.getTag())).to.be.true; + }); + }); + }); }); describe("WIF", function() { @@ -586,6 +680,11 @@ describe("High-level classes", function() { expect(addr.getTag().toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); }); + it("should calculate a private key to encrypt pubkey object", function() { + var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); + expect(addr.getPubkeyPrivateKey().toString("hex")).to.equal("15e516173769dc87d4a8e8ed90200362fa58c0228bb2b70b06f26c089a9823a4"); + }); + it("should allow to decode Address instance", function() { var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56");