diff --git a/lib/objects.js b/lib/objects.js index e40fdf1..c3e6252 100644 --- a/lib/objects.js +++ b/lib/objects.js @@ -17,46 +17,76 @@ var assert = require("./_util").assert; var promise = require("./platform").promise; 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 structs = require("./structs"); var util = require("./_util"); +var var_int = structs.var_int; +var PubkeyBitfield = structs.PubkeyBitfield; +var message = structs.message; +var object = structs.object; + /** * `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 + * @static */ -exports.getpubkey = { +var getpubkey = exports.getpubkey = { + /** + * Decode `getpubkey` object message. + * @param {Buffer} buf - Message + * @return {Promise.} A promise that contains decoded + * `getpubkey` object structure when fulfilled. + */ + decodeAsync: function(buf) { + return new promise(function(resolve) { + var decoded = message.decode(buf); + assert(decoded.command === "object", "Bad command"); + resolve(getpubkey.decodePayloadAsync(decoded.payload)); + }); + }, + /** * Decode `getpubkey` object message payload. * @param {Buffer} buf - Message payload * @return {Promise.} A promise that contains decoded * `getpubkey` object structure when fulfilled. */ - decodeAsync: function(buf) { + decodePayloadAsync: function(buf) { return new promise(function(resolve) { - var decoded = object.decode(buf); + var decoded = object.decodePayload(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; + var objectPayload = decoded.objectPayload; + delete decoded.objectPayload; 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; + assert(objectPayload.length === 20, "getpubkey ripe is too small"); + // Object payload is copied so it's safe to return it right away. + decoded.ripe = objectPayload; } 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; + assert(objectPayload.length === 32, "getpubkey tag is too small"); + // Object payload is copied so it's safe to return it right away. + decoded.tag = objectPayload; } resolve(decoded); }); }, + /** + * Encode `getpubkey` object message. + * @param {Object} opts - `getpubkey` object options + * @return {Promise.} A promise that contains encoded message + * when fulfilled. + */ + encodeAsync: function(opts) { + return getpubkey.encodePayloadAsync(opts).then(function(payload) { + return message.encode("object", payload); + }); + }, + /** * Encode `getpubkey` object message payload. * @param {Object} opts - `getpubkey` object options @@ -64,7 +94,7 @@ exports.getpubkey = { * payload when fulfilled. */ // FIXME(Kagami): Do a POW. - encodeAsync: function(opts) { + encodePayloadAsync: function(opts) { return new promise(function(resolve) { opts = objectAssign({}, opts); opts.type = object.GETPUBKEY; @@ -74,34 +104,36 @@ exports.getpubkey = { 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(); + opts.objectPayload = to.version < 4 ? to.getRipe() : to.getTag(); // POW calculation here. var nonce = new Buffer(8); opts.nonce = nonce; - resolve(object.encode(opts)); + resolve(object.encodePayload(opts)); }); }, }; // Helper function for `pubkey.decode`. -function extractPubkeyV2(payload) { +// Extract pubkey data from decrypted object payload. +function extractPubkeyV2(buf) { var decoded = {}; - // Payload is copied so it's safe to return it right away. - decoded.behavior = PubkeyBitfield(payload.slice(0, 4)); + // Object payload is copied so it's safe to return it right away. + decoded.behavior = PubkeyBitfield(buf.slice(0, 4)); var signPublicKey = decoded.signPublicKey = new Buffer(65); signPublicKey[0] = 4; - payload.copy(signPublicKey, 1, 4, 68); + buf.copy(signPublicKey, 1, 4, 68); var encPublicKey = decoded.encPublicKey = new Buffer(65); encPublicKey[0] = 4; - payload.copy(encPublicKey, 1, 68, 132); + buf.copy(encPublicKey, 1, 68, 132); return decoded; } // Helper function for `pubkey.decode`. -function extractPubkeyV3(payload) { +// Extract pubkey data from decrypted object payload. +function extractPubkeyV3(buf) { var decoded = {}; var length = 0; - var decodedTrials = var_int.decode(payload); + var decodedTrials = var_int.decode(buf); decoded.nonceTrialsPerByte = decodedTrials.value; length += decodedTrials.length; var decodedExtraBytes = var_int.decode(decodedTrials.rest); @@ -111,8 +143,7 @@ function extractPubkeyV3(payload) { decoded.signature = decodedSigLength.rest.slice(0, decodedSigLength.value); var siglen = decodedSigLength.length + decodedSigLength.value; length += siglen; - // Internal value. - decoded._siglen = siglen; + decoded._siglen = siglen; // Internal value decoded.length = length; return decoded; } @@ -121,8 +152,24 @@ function extractPubkeyV3(payload) { * `pubkey` object. * @see {@link https://bitmessage.org/wiki/Protocol_specification#pubkey} * @namespace + * @static */ -exports.pubkey = { +var pubkey = exports.pubkey = { + /** + * Decode `pubkey` object message. + * @param {Buffer} buf - Message + * @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) { + var decoded = message.decode(buf); + assert(decoded.command === "object", "Bad command"); + resolve(pubkey.decodePayloadAsync(decoded.payload, opts)); + }); + }, + /** * Decode `pubkey` object message payload. * @param {Buffer} buf - Message payload @@ -130,14 +177,14 @@ exports.pubkey = { * @return {Promise.} A promise that contains decoded `pubkey` * object structure when fulfilled. */ - decodeAsync: function(buf, opts) { + decodePayloadAsync: function(buf, opts) { return new promise(function(resolve, reject) { opts = opts || {}; var neededPubkeys = opts.neededPubkeys || {}; - var decoded = object.decode(buf); + var decoded = object.decodePayload(buf); assert(decoded.type === object.PUBKEY, "Wrong object type"); - var payload = decoded.payload; - delete decoded.payload; + var objectPayload = decoded.objectPayload; + delete decoded.objectPayload; var version = decoded.version; assert(version >= 2, "Address version is too low"); assert(version <= 4, "Address version is too high"); @@ -148,8 +195,10 @@ exports.pubkey = { // v2 pubkey. if (version === 2) { // 4 + 64 + 64 - assert(payload.length === 132, "Bad pubkey v2 object payload length"); - objectAssign(decoded, extractPubkeyV2(payload)); + assert( + objectPayload.length === 132, + "Bad pubkey v2 object payload length"); + objectAssign(decoded, extractPubkeyV2(objectPayload)); // Real data length. decoded.length = length; return resolve(decoded); @@ -158,9 +207,11 @@ exports.pubkey = { // 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))); + assert( + objectPayload.length >= 135, + "Bad pubkey v3 object payload length"); + objectAssign(decoded, extractPubkeyV2(objectPayload)); + objectAssign(decoded, extractPubkeyV3(objectPayload.slice(132))); siglen = util.popkey(decoded, "_siglen"); length += decoded.length; // Real data length. @@ -192,13 +243,13 @@ exports.pubkey = { }); } - assert(payload.length >= 32, "Bad pubkey v4 object payload length"); - tag = decoded.tag = payload.slice(0, 32); + assert(objectPayload.length >= 32, "Bad pubkey v4 object payload length"); + tag = decoded.tag = objectPayload.slice(0, 32); pubkeyEncPrivateKey = neededPubkeys[tag]; if (!pubkeyEncPrivateKey) { return reject(new Error("You are not interested in this pubkey v4")); } - dataToDecrypt = payload.slice(32); + dataToDecrypt = objectPayload.slice(32); pubkeyp = bmcrypto.decrypt(pubkeyEncPrivateKey, dataToDecrypt) .then(function(decrypted) { // 4 + 64 + 64 + (1+) + (1+) + (1+) @@ -210,8 +261,8 @@ exports.pubkey = { siglen = util.popkey(decoded, "_siglen"); length += decoded.length; // Real data length. - // Since data is encrypted, entire payload is used. - decoded.length = payload.length; + // Since data is encrypted, entire object payload is used. + decoded.length = objectPayload.length; dataToVerify = Buffer.concat([ // Object header without nonce + tag. buf.slice(8, decoded.headerLength + 32), @@ -227,6 +278,18 @@ exports.pubkey = { }); }, + /** + * Encode `pubkey` object message. + * @param {Object} opts - `pubkey` object options + * @return {Promise.} A promise that contains encoded message + * when fulfilled. + */ + encodeAsync: function(opts) { + return pubkey.encodePayloadAsync(opts).then(function(payload) { + return message.encode("object", payload); + }); + }, + /** * Encode `pubkey` object message payload. * @param {Object} opts - `pubkey` object options @@ -234,7 +297,7 @@ exports.pubkey = { * payload when fulfilled. */ // FIXME(Kagami): Do a POW. - encodeAsync: function(opts) { + encodePayloadAsync: function(opts) { return new promise(function(resolve) { opts = objectAssign({}, opts); opts.type = object.PUBKEY; @@ -260,12 +323,12 @@ exports.pubkey = { // v2 pubkey. if (version === 2) { - opts.payload = Buffer.concat([ + opts.objectPayload = Buffer.concat([ from.behavior.buffer, from.signPublicKey.slice(1), from.encPublicKey.slice(1), ]); - obj = object.encodeWithoutNonce(opts); + obj = object.encodePayloadWithoutNonce(opts); // POW calculation here. var nonce = new Buffer(8); obj = Buffer.concat([nonce, obj]); @@ -282,8 +345,8 @@ exports.pubkey = { // v3 pubkey. if (version === 3) { - opts.payload = Buffer.concat(pubkeyData); - obj = object.encodeWithoutNonce(opts); + opts.objectPayload = Buffer.concat(pubkeyData); + obj = object.encodePayloadWithoutNonce(opts); pubkeyp = bmcrypto .sign(from.signPrivateKey, obj) .then(function(sig) { @@ -301,8 +364,8 @@ exports.pubkey = { } // v4 pubkey. - opts.payload = from.getTag(); - obj = object.encodeWithoutNonce(opts); + opts.objectPayload = from.getTag(); + obj = object.encodePayloadWithoutNonce(opts); var dataToSign = Buffer.concat([obj].concat(pubkeyData)); var pubkeyEncPrivateKey = from.getPubkeyPrivateKey(); var pubkeyEncPublicKey = bmcrypto.getPublic(pubkeyEncPrivateKey); diff --git a/lib/structs.js b/lib/structs.js index 3786139..bf3387d 100644 --- a/lib/structs.js +++ b/lib/structs.js @@ -108,9 +108,21 @@ var object = exports.object = { MSG: 2, BROADCAST: 3, + /** + * Decode `object` message. + * NOTE: `nonce` and `objectPayload` are copied. + * @param {Buffer} buf - Message + * @return {Object} Decoded `object` structure. + */ + decode: function(buf) { + var decoded = message.decode(buf); + assert(decoded.command === "object", "Bad command"); + return object.decodePayload(decoded.payload); + }, + /** * Decode `object` message payload. - * NOTE: `nonce` and `payload` are copied. + * NOTE: `nonce` and `objectPayload` are copied. * @param {Buffer} buf - Message payload * @return {Object} Decoded `object` structure. */ @@ -119,7 +131,7 @@ var object = exports.object = { // bitchan). // TODO(Kagami): Option to not fail on expired objects (would be // useful for bitchan). - decode: function(buf) { + decodePayload: function(buf) { // 8 + 8 + 4 + (1+) + (1+) assert(buf.length >= 22, "object message payload is too small"); var nonce = new Buffer(8); @@ -132,8 +144,8 @@ var object = exports.object = { 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); + var objectPayload = new Buffer(decodedStream.rest.length); + decodedStream.rest.copy(objectPayload); return { nonce: nonce, ttl: ttl, @@ -141,17 +153,42 @@ var object = exports.object = { version: decodedVersion.value, stream: decodedStream.value, headerLength: headerLength, - payload: payload, + objectPayload: objectPayload, }; }, + /** + * Encode `object` message. + * @param {Object} opts - Object options + * @return {Buffer} Encoded message. + */ + encode: function(opts) { + var payload = object.encodePayload(opts); + return message.encode("object", payload); + }, + + /** + * Encode `object` message payload. + * @param {Object} opts - Object options + * @return {Buffer} Encoded payload. + */ + encodePayload: 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.encodePayloadWithoutNonce(opts), + ]); + }, + /** * 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) { + encodePayloadWithoutNonce: 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; @@ -163,22 +200,7 @@ var object = exports.object = { 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), + opts.objectPayload, ]); }, }; diff --git a/package.json b/package.json index bd2133c..bf69f66 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.3", + "eccrypto": "^0.9.4", "es6-promise": "^2.0.1", "hash.js": "^1.0.2", "nan": "^1.4.1", diff --git a/test.js b/test.js index 9d28b6c..53e6b07 100644 --- a/test.js +++ b/test.js @@ -112,6 +112,7 @@ describe("Crypto", function() { }); }); +// TODO(Kagami): Add tests for encodePayload/decodePayload as well. describe("Common structures", function() { describe("message", function() { it("should decode", function() { @@ -143,7 +144,7 @@ describe("Common structures", function() { ttl: 100, type: 2, version: 1, - payload: Buffer("test"), + objectPayload: Buffer("test"), })); expect(bufferEqual(nonce, res.nonce)).to.be.true; @@ -152,7 +153,7 @@ describe("Common structures", function() { 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"); + expect(res.objectPayload.toString()).to.equal("test"); }); it("shouldn't encode too big TTL", function() { @@ -161,7 +162,7 @@ describe("Common structures", function() { ttl: 10000000, type: 2, version: 1, - payload: Buffer("test"), + objectPayload: Buffer("test"), })).to.throw(Error); }); }); @@ -360,6 +361,7 @@ describe("Common structures", function() { }); }); +// TODO(Kagami): Add tests for encodePayload/decodePayload as well. describe("Message types", function() { describe("version", function() { it("should encode and decode", function() { @@ -485,14 +487,17 @@ describe("Message types", function() { }); }); +// TODO(Kagami): Add tests for encodePayloadAsync/decodePayloadAsync as well. describe("Object types", function() { describe("getpubkey", function() { it("should encode and decode getpubkey v3", function() { return getpubkey.encodeAsync({ ttl: 100, to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", - }).then(getpubkey.decodeAsync) - .then(function(res) { + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return getpubkey.decodeAsync(buf); + }).then(function(res) { expect(res.ttl).to.be.at.most(100); expect(res.type).to.equal(object.GETPUBKEY); expect(res.version).to.equal(3); @@ -506,8 +511,10 @@ describe("Object types", function() { return getpubkey.encodeAsync({ ttl: 100, to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z", - }).then(getpubkey.decodeAsync) - .then(function(res) { + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return getpubkey.decodeAsync(buf); + }).then(function(res) { expect(res.ttl).to.be.at.most(100); expect(res.type).to.equal(object.GETPUBKEY); expect(res.version).to.equal(4); @@ -533,8 +540,10 @@ describe("Object types", function() { ttl: 123, from: from, to: "BM-onhypnh1UMhbQpmvdiPuG6soLLytYJAfH", - }).then(pubkey.decodeAsync) - .then(function(res) { + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return pubkey.decodeAsync(buf); + }).then(function(res) { expect(res.ttl).to.be.at.most(123); expect(res.type).to.equal(object.PUBKEY); expect(res.version).to.equal(2); @@ -551,8 +560,10 @@ describe("Object types", function() { ttl: 456, from: from, to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", - }).then(pubkey.decodeAsync) - .then(function(res) { + }).then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); + return pubkey.decodeAsync(buf); + }).then(function(res) { expect(res.ttl).to.be.at.most(456); expect(res.type).to.equal(object.PUBKEY); expect(res.version).to.equal(3); @@ -569,6 +580,7 @@ describe("Object types", function() { it("should encode and decode pubkey v4", function() { return pubkey.encodeAsync({ttl: 789, from: from, to: from}) .then(function(buf) { + expect(message.decode(buf).command).to.equal("object"); return pubkey.decodeAsync(buf, {neededPubkeys: from}); }).then(function(res) { expect(res.ttl).to.be.at.most(789);