diff --git a/README.md b/README.md index ca41bdd..6d035f7 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [x] error - [x] object - [ ] Object types - - [ ] getpubkey + - [x] getpubkey - [ ] pubkey - [ ] msg - [ ] broadcast @@ -55,7 +55,6 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/ - [ ] Address - [x] encode - [x] decode - - [x] getRipe - [x] fromRandom - [ ] fromPassphrase - [x] UserAgent diff --git a/lib/address.js b/lib/address.js index d798497..e544eee 100644 --- a/lib/address.js +++ b/lib/address.js @@ -46,6 +46,10 @@ function Address(opts) { * @return {Address} Decoded address object. */ Address.decode = function(str) { + if (str instanceof Address) { + return str; + } + str = str.trim(); if (str.slice(0, 3) === "BM-") { str = str.slice(3); @@ -121,6 +125,21 @@ Address.prototype.getRipe = function(opts) { } }; +/** + * 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); +}; + // Get truncated ripe hash length. function getripelen(ripe) { var zeroes = 0; diff --git a/lib/messages.js b/lib/messages.js index 2b2e159..a5289ce 100644 --- a/lib/messages.js +++ b/lib/messages.js @@ -5,6 +5,7 @@ * @see {@link https://bitmessage.org/Bitmessage%20Technical%20Paper.pdf} * @module bitmessage/messages */ +// TODO(Kagami): Document object-like params. "use strict"; @@ -266,8 +267,8 @@ var error = exports.error = { * 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 encode/decode it with appropriate - * function from `objects` module. + * 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 */ @@ -285,20 +286,21 @@ exports.object = { * @return {?number} Object type. */ getType: function(buf) { - // Per v3 spec object starts with nonce (8 bytes), expiresTime (8 - // bytes) and objectType (4 bytes). So we need only first 20 bytes - // to read the type. - if (buf.length >= 20) { - return buf.readUInt32BE(16, true); - } + assert(buf.length >= 22, "object message payload is too small"); + return buf.readUInt32BE(16, true); }, /** * Decode `object` message payload. - * NOTE: `nonce` is copied, `payload` references input buffer. + * 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"); @@ -311,13 +313,15 @@ exports.object = { 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: decodedStream.rest, + payload: payload, }; }, @@ -326,6 +330,7 @@ exports.object = { * @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"); diff --git a/lib/objects.js b/lib/objects.js index d5bcc96..56efbe6 100644 --- a/lib/objects.js +++ b/lib/objects.js @@ -1,5 +1,68 @@ /** * Working with objects. + * NOTE: All operations with objects in this module are asynchronous and + * return promises. * @see {@link https://bitmessage.org/wiki/Protocol_specification#Object_types} * @module bitmessage/objects */ +// TODO(Kagami): Document object-like params. + +"use strict"; + +var objectAssign = Object.assign || require("object-assign"); +var assert = require("./util").assert; +var promise = require("./platform").promise; +var object = require("./messages").object; +var Address = require("./address"); + +/** + * `getpubkey` object. When a node has the hash of a public key (from an + * address) but not the public key itself, it must send out a request + * for the public key. + * @see {@link https://bitmessage.org/wiki/Protocol_specification#getpubkey} + * @namespace + */ +exports.getpubkey = { + /** + * Decode `getpubkey` object message payload. + * @param {Buffer} buf - Message payload + * @return {Promise.} A promise that contained decoded + * `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"); + var payload = decoded.payload; + delete decoded.payload; + if (decoded.version < 4) { + assert(payload.length === 20, "getpubkey ripe is too small"); + // Payload is copied so it's safe to return it right away. + decoded.ripe = payload; + } else { + assert(payload.length === 32, "getpubkey tag is too small"); + // Payload is copied so it's safe to return it right away. + decoded.tag = payload; + } + resolve(decoded); + }); + }, + + /** + * Encode `getpubkey` object message payload. + * @param {Object} opts - `getpubkey` object options + * @return {Promise.} A promise that contained encoded message + * payload when fulfilled. + */ + 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 = opts.version < 4 ? addr.getRipe() : addr.getTag(); + resolve(object.encode(opts)); + }); + }, +}; diff --git a/lib/structs.js b/lib/structs.js index 0c0b17b..8dffe58 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -3,7 +3,7 @@ * @see {@link https://bitmessage.org/wiki/Protocol_specification#Common_structures} * @module bitmessage/structs */ -// TODO(Kagami): Find a way how to document object params properly. +// TODO(Kagami): Document object-like params. "use strict"; diff --git a/test.js b/test.js index c3ecf9b..0227cbd 100644 --- a/test.js +++ b/test.js @@ -22,6 +22,8 @@ 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 WIF = bitmessage.WIF; var POW = bitmessage.POW; var Address = bitmessage.Address; @@ -414,6 +416,24 @@ describe("Message types", function() { }); }); +describe("Object types", function() { + describe("getpubkey", function() { + it("should encode and decode", function() { + return getpubkey.encodeAsync({ + nonce: Buffer(8), + ttl: 100, + to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z", + }).then(getpubkey.decodeAsync) + .then(function(res) { + expect(res.ttl).to.equal(100); + expect(res.version).to.equal(4); + expect(res.stream).to.equal(1); + expect(res.tag.toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); + }); + }); + }); +}); + describe("WIF", function() { var wifSign = "5JgQ79vTBusc61xYPtUEHYQ38AXKdDZgQ5rFp7Cbb4ZjXUKFZEV"; var wifEnc = "5K2aL8cnsEWHwHfHnUrPo8QdYyRfoYUBmhAnWY5GTpDLbeyusnE"; @@ -521,6 +541,17 @@ describe("High-level classes", function() { expect(addr2.ripe[0]).to.equal(0); }); + it("should calculate tag", function() { + var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); + expect(addr.getTag().toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); + }); + + it("should allow to decode Address instance", function() { + var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); + expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56"); + expect(Address.decode(addr)).to.equal(addr); + }); + // FIXME(Kagami): Don't run it in browser currently because it's // very slow. This need to be fixed. if (allTests && typeof window === "undefined") {