objects.broadcast

This commit is contained in:
Kagami Hiiragi 2015-01-29 21:18:07 +03:00
parent 34868ff014
commit ea9e8e60ef
4 changed files with 410 additions and 37 deletions

View File

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

View File

@ -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.

View File

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

97
test.js
View File

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