From ea9e8e60ef27b8210cd2a8ac4420b0685f260703 Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Thu, 29 Jan 2015 21:18:07 +0300 Subject: [PATCH] objects.broadcast --- README.md | 4 +- lib/address.js | 19 +++ lib/objects.js | 327 ++++++++++++++++++++++++++++++++++++++++++++----- test.js | 97 ++++++++++++++- 4 files changed, 410 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index 3715b69..62513ec 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,11 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] inv - [x] getdata - [x] error -- [ ] Object types +- [x] Object types - [x] getpubkey - [x] pubkey - [x] msg - - [ ] broadcast + - [x] broadcast - [x] WIF - [x] POW - [x] High-level classes diff --git a/lib/address.js b/lib/address.js index e72fa9d..3b4b9b3 100644 --- a/lib/address.js +++ b/lib/address.js @@ -116,9 +116,19 @@ Address.prototype.getPubkeyPrivateKey = function() { return bmcrypto.sha512(getaddrhash(this)).slice(0, 32); }; +/** + * 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. + * @return {Buffer} A 32-byte private key. */ Address.prototype.getBroadcastPrivateKey = function() { if (this.version >= 4) { @@ -128,6 +138,15 @@ Address.prototype.getBroadcastPrivateKey = function() { } }; +/** + * 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()); +}; + /** * Calculate the address tag. * @return {Buffer} A 32-byte address tag. diff --git a/lib/objects.js b/lib/objects.js index 444c5ed..63dad70 100644 --- a/lib/objects.js +++ b/lib/objects.js @@ -183,26 +183,25 @@ function extractPubkeyV3(buf) { return decoded; } -function findPubkeyPrivateKey(neededPubkeys, tag) { - // `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. - neededPubkeys = neededPubkeys || {}; - var addr, addrs, i; - if (Address.isAddress(neededPubkeys)) { - addr = neededPubkeys; - if (bufferEqual(addr.getTag(), tag)) { - return addr.getPubkeyPrivateKey(); - } - } else if (Array.isArray(neededPubkeys)) { - addrs = neededPubkeys; +// Note that tag matching only works for address version >= 4. +function findAddrByTag(addrs, tag) { + var i, addr; + addrs = addrs || []; + if (Address.isAddress(addrs)) { + addrs = [addrs]; + } + if (Array.isArray(addrs)) { for (i = 0; i < addrs.length; i++) { - if (bufferEqual(addrs[i].getTag(), tag)) { - return addrs[i].getPubkeyPrivateKey(); + addr = addrs[i]; + if (addr.version >= 4 && bufferEqual(addr.getTag(), tag)) { + return addr; } } } else { - return neededPubkeys[tag]; + addr = addrs[tag]; + if (addr && addr.version >= 4) { + return addr; + } } } @@ -235,7 +234,7 @@ var pubkey = exports.pubkey = { * @param {?(Address[]|Address|Object)} opts.needed - Address objects * which represent pubkeys that we are interested in. This is used * only for pubkeys v4. `needed` is either single address or addresses - * array or Object key-by-tag. Time to match the tag is O(1), O(n), + * array or Object addr-by-tag. Time to match the key is O(1), O(n), * O(1) respectfully. * @return {Promise.} A promise that contains decoded `pubkey` * object structure when fulfilled. @@ -250,7 +249,7 @@ var pubkey = exports.pubkey = { assert(version <= 4, "Address version is too high"); var objectPayload = util.popkey(decoded, "objectPayload"); var siglen, pos, sig, dataToVerify, pubkeyp; - var tag, pubkeyPrivateKey, dataToDecrypt; + var tag, addr, pubkeyPrivateKey, dataToDecrypt; // v2 pubkey. if (version === 2) { @@ -284,8 +283,9 @@ var pubkey = exports.pubkey = { // v4 pubkey. assert(objectPayload.length >= 32, "Bad pubkey v4 object payload length"); tag = decoded.tag = objectPayload.slice(0, 32); - pubkeyPrivateKey = findPubkeyPrivateKey(opts.needed, tag); - assert(pubkeyPrivateKey, "You are not interested in this pubkey v4"); + addr = findAddrByTag(opts.needed, tag); + assert(addr, "You are not interested in this pubkey v4"); + pubkeyPrivateKey = addr.getPubkeyPrivateKey(); dataToDecrypt = objectPayload.slice(32); pubkeyp = bmcrypto .decrypt(pubkeyPrivateKey, dataToDecrypt) @@ -402,14 +402,12 @@ var pubkey = exports.pubkey = { opts.objectPayload = from.getTag(); obj = object.encodePayloadWithoutNonce(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); + return bmcrypto.encrypt(from.getPubkeyPublicKey(), dataToEnc); }).then(function(enc) { // POW calculation here. var nonce = new Buffer(8); @@ -425,7 +423,8 @@ var pubkey = exports.pubkey = { function tryDecryptMsg(identities, buf) { function inner(i) { if (i > last) { - return promise.reject("Failed to decrypt msg with given identities"); + var err = new Error("Failed to decrypt msg with given identities"); + return promise.reject(err); } return bmcrypto .decrypt(identities[i].encPrivateKey, buf) @@ -436,6 +435,9 @@ function tryDecryptMsg(identities, buf) { }); } + if (Address.isAddress(identities)) { + identities = [identities]; + } var last = identities.length - 1; return inner(0); } @@ -546,23 +548,20 @@ var msg = exports.msg = { */ decodePayloadAsync: function(buf, opts) { return new promise(function(resolve) { - var identities = opts.identities; - if (Address.isAddress(identities)) { - identities = [identities]; - } var decoded = object.decodePayload(buf); assert(decoded.type === object.MSG, "Bad object type"); assert(decoded.version === 1, "Bad msg version"); var objectPayload = util.popkey(decoded, "objectPayload"); - var msgp = tryDecryptMsg(identities, objectPayload) + var msgp = tryDecryptMsg(opts.identities, objectPayload) .then(function(decInfo) { var decrypted = decInfo.decrypted; // Version, stream. - // TODO(Kagami): Validate version range? var decodedVersion = var_int.decode(decrypted); - decoded.senderVersion = decodedVersion.value; + var senderVersion = decoded.senderVersion = decodedVersion.value; + assert(senderVersion >= 2, "Sender version is too low"); + assert(senderVersion <= 4, "Sender version is too high"); var decodedStream = var_int.decode(decodedVersion.rest); decoded.senderStream = decodedStream.value; @@ -588,9 +587,11 @@ var msg = exports.msg = { // Ripe, encoding. assert(rest.length >= 20, "Bad msg object payload length"); decoded.ripe = rest.slice(0, 20); + // TODO(Kagami): Also check against the calculatedRipe (see + // GH-6)? assert( bufferEqual(decoded.ripe, decInfo.addr.ripe), - "msg was decrypted but the destination ripe differs"); + "msg was decrypted but the destination ripe doesn't match"); decoded.length += 20; var decodedEncoding = var_int.decode(rest.slice(20)); var encoding = decoded.encoding = decodedEncoding.value; @@ -664,6 +665,8 @@ var msg = exports.msg = { opts.type = object.MSG; opts.version = 1; // The only known msg version var from = Address.decode(opts.from); + assert(from.version >= 2, "Address version is too low"); + assert(from.version <= 4, "Address version is too high"); var to = Address.decode(opts.to); opts.stream = to.stream; var nonceTrialsPerByte, payloadLengthExtraBytes; @@ -724,3 +727,263 @@ var msg = exports.msg = { }; var DEFAULT_ENCODING = msg.TRIVIAL; + +// Try to decrypt broadcast v4 with all provided subscription objects. +function tryDecryptBroadcastV4(subscriptions, buf) { + function inner(i) { + if (i > last) { + var err = new Error("Failed to decrypt msg with given identities"); + return promise.reject(err); + } + return bmcrypto + .decrypt(subscriptions[i].getBroadcastPrivateKey(), buf) + .then(function(decrypted) { + return {addr: subscriptions[i], decrypted: decrypted}; + }).catch(function() { + return inner(i + 1); + }); + } + + if (Address.isAddress(subscriptions)) { + subscriptions = [subscriptions]; + } else if (!Array.isArray(subscriptions)) { + subscriptions = Object.keys(subscriptions).map(function(k) { + return subscriptions[k]; + }); + } + // Only addresses with version < 4 may be used to encode broadcast v4. + subscriptions = subscriptions.filter(function(a) { + return a.version < 4; + }); + var last = subscriptions.length - 1; + return inner(0); +} + +/** + * `broadcast` object. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#broadcast} + * @namespace + * @static + */ +var broadcast = exports.broadcast = { + /** + * Decode `broadcast` object message. + * @param {Buffer} buf - Message + * @param {?Object} opts - Decoding options + * @return {Promise.} A promise that contains decoded + * `broadcast` object structure when fulfilled. + */ + decodeAsync: function(buf, opts) { + return new promise(function(resolve) { + var decoded = message.decode(buf); + assert(decoded.command === "object", "Bad command"); + resolve(broadcast.decodePayloadAsync(decoded.payload, opts)); + }); + }, + + /** + * Decode `broadcast` object message payload. + * @param {Buffer} buf - Message payload + * @param {Object} opts - Decoding options + * @param {(Address[]|Address|Object)} opts.subscriptions - Address + * objects which represent broadcast subscriptions. `subscriptions` is + * either single address or addresses array or Object + * addr-by-tag/addr-by-ripe. Time to match the key is O(1), O(n), O(1) + * respectfully. + * @return {Promise.} A promise that contains decoded `pubkey` + * object structure when fulfilled. + */ + decodePayloadAsync: function(buf, opts) { + return new promise(function(resolve) { + var decoded = object.decodePayload(buf); + assert(decoded.type === object.BROADCAST, "Bad object type"); + var version = decoded.version; + assert(version === 4 || version === 5, "Bad broadcast version"); + var objectPayload = util.popkey(decoded, "objectPayload"); + var tag, addr, broadPrivateKey, dataToDecrypt, broadp; + + if (version === 4) { + broadp = tryDecryptBroadcastV4(opts.subscriptions, objectPayload); + } else { + assert( + objectPayload.length >= 32, + "Bad broadcast v5 object payload length"); + tag = decoded.tag = objectPayload.slice(0, 32); + addr = findAddrByTag(opts.subscriptions, tag); + assert(addr, "You are not interested in this broadcast v5"); + broadPrivateKey = addr.getBroadcastPrivateKey(); + dataToDecrypt = objectPayload.slice(32); + broadp = bmcrypto + .decrypt(broadPrivateKey, dataToDecrypt) + .then(function(decrypted) { + return {addr: addr, decrypted: decrypted}; + }); + } + + broadp = broadp.then(function(decInfo) { + var decrypted = decInfo.decrypted; + + // Version, stream. + var decodedVersion = var_int.decode(decrypted); + var senderVersion = decoded.senderVersion = decodedVersion.value; + if (version === 4) { + assert(senderVersion >= 2, "Sender version is too low"); + assert(senderVersion <= 3, "Sender version is too high"); + } else { + assert(senderVersion === 4, "Bad sender version"); + } + var decodedStream = var_int.decode(decodedVersion.rest); + var senderStream = decoded.senderStream = decodedStream.value; + assert( + senderStream === decoded.stream, + "Cleartext broadcast object stream doesn't match encrypted"); + + // Behavior, keys. + assert( + decodedStream.rest.length >= 132, + "Bad broadcast object payload length"); + objectAssign(decoded, extractPubkey(decodedStream.rest)); + decoded.length += decodedVersion.length + decodedStream.length; + var rest = decrypted.slice(decoded.length); + var sender = new Address({ + version: senderVersion, + stream: senderStream, + signPublicKey: decoded.signPublicKey, + encPublicKey: decoded.encPublicKey, + }); + if (version === 4) { + assert( + bufferEqual(sender.ripe, decInfo.addr.ripe), + "The keys used to encrypt the broadcast doesn't match the keys "+ + "embedded into the object"); + } else { + assert( + bufferEqual(sender.getTag(), tag), + "The tag used to encrypt the broadcast doesn't match the keys "+ + "and version/stream embedded into the object"); + } + + // Pow extra. + if (senderVersion >= 3) { + var decodedTrials = var_int.decode(rest); + decoded.nonceTrialsPerByte = decodedTrials.value; + decoded.length += decodedTrials.length; + var decodedExtraBytes = var_int.decode(decodedTrials.rest); + decoded.payloadLengthExtraBytes = decodedExtraBytes.value; + decoded.length += decodedExtraBytes.length; + rest = decodedExtraBytes.rest; + } + + // Encoding, message + var decodedEncoding = var_int.decode(rest); + var encoding = decoded.encoding = decodedEncoding.value; + decoded.length += decodedEncoding.length; + var decodedMsgLength = var_int.decode(decodedEncoding.rest); + var msglen = decodedMsgLength.value; + rest = decodedMsgLength.rest; + assert(rest.length >= msglen, "Bad broadcast object payload length"); + decoded.length += decodedMsgLength.length + msglen; + var message = rest.slice(0, msglen); + objectAssign(decoded, decodeMessage(message, encoding)); + + // Signature. + var decodedSigLength = var_int.decode(rest.slice(msglen)); + var siglen = decodedSigLength.value; + rest = decodedSigLength.rest; + assert(rest.length >= siglen, "Bad broadcast object payload length"); + var sig = decoded.signature = rest.slice(0, siglen); + + // Verify signature. + var headerLength = decoded.headerLength; + if (version !== 4) { + // Compensate for tag. + headerLength += 32; + } + var dataToVerify = Buffer.concat([ + // Object header without nonce. + buf.slice(8, headerLength), + // Unencrypted pubkey data without signature. + decrypted.slice(0, decoded.length), + ]); + // Since data is encrypted, entire object payload is used. + decoded.length = objectPayload.length; + return bmcrypto.verify(decoded.signPublicKey, dataToVerify, sig); + }).then(function() { + return decoded; + }); + resolve(broadp); + }); + }, + + /** + * Encode `broadcast` object message. + * @param {Object} opts - `broadcast` object options + * @return {Promise.} A promise that contains encoded message + * when fulfilled. + */ + encodeAsync: function(opts) { + return broadcast.encodePayloadAsync(opts).then(function(payload) { + return message.encode("object", payload); + }); + }, + + /** + * Encode `broadcast` object message payload. + * @param {Object} opts - `broadcast` object options + * @return {Promise.} A promise that contains encoded message + * payload when fulfilled. + */ + // FIXME(Kagami): Do a POW. + encodePayloadAsync: function(opts) { + return new promise(function(resolve) { + // Deal with options. + opts = objectAssign({}, opts); + opts.type = object.BROADCAST; + var from = Address.decode(opts.from); + assert(from.version >= 2, "Address version is too low"); + assert(from.version <= 4, "Address version is too high"); + opts.version = from.version >= 4 ? 5 : 4; + opts.stream = from.stream; + var encoding = opts.encoding || DEFAULT_ENCODING; + var message = encodeMessage(opts); + + // Assemble the unencrypted message data. + var broadData = [ + var_int.encode(from.version), + var_int.encode(from.stream), + from.behavior.buffer, + from.signPublicKey.slice(1), + from.encPublicKey.slice(1), + ]; + if (from.version >= 3) { + broadData.push( + var_int.encode(util.getTrials(from)), + var_int.encode(util.getExtraBytes(from)) + ); + } + broadData.push( + var_int.encode(encoding), + var_int.encode(message.length), + message + ); + + // Sign and encrypt. + opts.objectPayload = from.version >= 4 ? from.getTag() : new Buffer(0); + var obj = object.encodePayloadWithoutNonce(opts); + var dataToSign = Buffer.concat([obj].concat(broadData)); + var broadp = bmcrypto + .sign(from.signPrivateKey, dataToSign) + .then(function(sig) { + var dataToEnc = broadData.concat(var_int.encode(sig.length), sig); + dataToEnc = Buffer.concat(dataToEnc); + return bmcrypto.encrypt(from.getBroadcastPublicKey(), 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(broadp); + }); + }, +}; diff --git a/test.js b/test.js index 05591bd..96674b0 100644 --- a/test.js +++ b/test.js @@ -27,6 +27,7 @@ var objects = bitmessage.objects; var getpubkey = objects.getpubkey; var pubkey = objects.pubkey; var msg = objects.msg; +var broadcast = objects.broadcast; var WIF = bitmessage.WIF; var POW = bitmessage.POW; var Address = bitmessage.Address; @@ -504,6 +505,11 @@ describe("Object types", function() { signPrivateKey: signPrivateKey, encPrivateKey: encPrivateKey, }); + var fromV3 = Address({ + version: 3, + signPrivateKey: signPrivateKey, + encPrivateKey: encPrivateKey, + }); it("should get type of the encoded object message", function() { var encoded = object.encode({ @@ -689,6 +695,74 @@ describe("Object types", function() { }); }); }); + + describe("broadcast", function() { + it("should encode and decode broadcast v4", function() { + return broadcast.encodeAsync({ + ttl: 987, + from: fromV3, + message: "test", + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return broadcast.decodeAsync(buf, {subscriptions: fromV3}); + }).then(function(res) { + expect(res.ttl).to.be.at.most(987); + expect(res.type).to.equal(object.BROADCAST); + expect(res.version).to.equal(4); + expect(res.stream).to.equal(1); + expect(res.senderVersion).to.equal(3); + expect(res.senderStream).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(res.encoding).to.equal(msg.TRIVIAL); + expect(res.message).to.equal("test"); + expect(res).to.not.have.property("subject"); + expect(Buffer.isBuffer(res.signature)).to.be.true; + }); + }); + + it("should encode and decode broadcast v5", function() { + return broadcast.encodeAsync({ + ttl: 101, + from: from, + message: "キタ━━━(゜∀゜)━━━!!!!!", + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return broadcast.decodeAsync(buf, {subscriptions: [from]}); + }).then(function(res) { + expect(res.ttl).to.be.at.most(987); + expect(res.type).to.equal(object.BROADCAST); + expect(res.version).to.equal(5); + expect(res.stream).to.equal(1); + expect(res.senderVersion).to.equal(4); + expect(res.senderStream).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(res.encoding).to.equal(msg.TRIVIAL); + expect(res.message).to.equal("キタ━━━(゜∀゜)━━━!!!!!"); + expect(res).to.not.have.property("subject"); + expect(Buffer.isBuffer(res.signature)).to.be.true; + }); + }); + + it("shouldn't decode broadcast without subscriptions", function(done) { + return broadcast.encodeAsync({ + ttl: 101, + from: from, + message: "test", + }).then(function(buf) { + return broadcast.decodeAsync(buf, {subscriptions: [fromV3]}); + }).catch(function() { + done(); + }); + }); + }); }); describe("WIF", function() { @@ -825,23 +899,40 @@ describe("High-level classes", function() { expect(addr.getTag().toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); }); - it("should calculate a private key to encrypt pubkey object", function() { + it("should calculate a private key to decrypt pubkey object", function() { var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); expect(addr.getPubkeyPrivateKey().toString("hex")).to.equal("15e516173769dc87d4a8e8ed90200362fa58c0228bb2b70b06f26c089a9823a4"); }); - it("should calculate a private key to encrypt broadcast v4", function() { + it("should calculate a public key to encrypt pubkey object", function() { + var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); + expect(addr.getPubkeyPublicKey().toString("hex")).to.equal("04ee196be97db61886beeec9ebc2c28b7d4cafbc407c31d8aac2f867068f727874e2d305ba970bd09a951aa2cde52b66061a5a8e709cda1125635a97e1c7b85ab4"); + }); + + it("should calculate a private key to decrypt broadcast v4", function() { var addr = Address.decode(" 2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU "); expect(addr.version).to.equal(3); expect(addr.getBroadcastPrivateKey().toString("hex")).to.equal("664420eaed1b6b3208fc04905c2f6ca758594c537eb5a08f2f0c2bbe6f07fb44"); }); - it("should calculate a private key to encrypt broadcast v5", function() { + it("should calculate a public key to encrypt broadcast v4", function() { + var addr = Address.decode(" 2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU "); + expect(addr.version).to.equal(3); + expect(addr.getBroadcastPublicKey().toString("hex")).to.equal("04da633350cf2ef8194b83ae028555971df56a64948940693e54b8b4c2597b8f9e833ac1285b37487121c271346fb29684e723a992aeb37b20962406ccade6c8d3"); + }); + + it("should calculate a private key to decrypt broadcast v5", function() { var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); expect(addr.version).to.equal(4); expect(addr.getBroadcastPrivateKey().toString("hex")).to.equal("15e516173769dc87d4a8e8ed90200362fa58c0228bb2b70b06f26c089a9823a4"); }); + it("should calculate a public key to encrypt broadcast v5", function() { + var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); + expect(addr.version).to.equal(4); + expect(addr.getBroadcastPublicKey().toString("hex")).to.equal("04ee196be97db61886beeec9ebc2c28b7d4cafbc407c31d8aac2f867068f727874e2d305ba970bd09a951aa2cde52b66061a5a8e709cda1125635a97e1c7b85ab4"); + }); + it("should allow to decode Address instance", function() { var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56");