bitmessage-js/lib/objects.js

325 lines
11 KiB
JavaScript

/**
* 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.
// 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 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 util = require("./_util");
/**
* `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.<Object>} 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) {
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.<Buffer>} 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;
// 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.<Object>} 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.<Buffer>} 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);
});
},
};