encodePayloadAsync/decodePayloadAsync for objects

This commit is contained in:
Kagami Hiiragi 2015-01-27 16:07:49 +03:00
parent 514265b7cd
commit a25bf41036
4 changed files with 179 additions and 82 deletions

View File

@ -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.<Object>} 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.<Object>} 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.<Buffer>} 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.<Object>} 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.<Object>} 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.<Buffer>} 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);

View File

@ -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,
]);
},
};

View File

@ -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",

34
test.js
View File

@ -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);