objects.msg

This commit is contained in:
Kagami Hiiragi 2015-01-28 23:37:20 +03:00
parent 4610701a3e
commit 2e4c91f49b
5 changed files with 402 additions and 65 deletions

View File

@ -49,7 +49,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/
- [ ] Object types
- [x] getpubkey
- [x] pubkey
- [ ] msg
- [x] msg
- [ ] broadcast
- [x] WIF
- [x] POW

View File

@ -1,5 +1,5 @@
// NOTE(Kagami): End-users shouldn't use this module. While it exports
// some helper routines, its API is _not_ stable.
// NOTE(Kagami): End-users shouldn't import this module. While it
// exports some helper routines, its API is _not_ stable.
"use strict";
@ -47,15 +47,15 @@ exports.readTime64BE = function(buf, offset) {
return new Date(timestamp * 1000);
};
exports.writeUInt64BE = function(buf, value, offset, noAssert) {
function writeUInt64BE(buf, value, offset, noAssert) {
buf = buf || new Buffer(8);
offset = offset || 0;
assert(noAssert || value <= MAX_SAFE_INTEGER, "Unsafe integer");
buf.writeUInt32BE(Math.floor(value / 4294967296), offset, noAssert);
buf.writeUInt32BE(value % 4294967296, offset + 4, noAssert);
return buf;
};
var writeUInt64BE = exports.writeUInt64BE;
}
exports.writeUInt64BE = writeUInt64BE;
exports.writeTime64BE = function(buf, time, offset, noAssert) {
var timestamp = Math.floor(time.getTime() / 1000);
@ -67,8 +67,8 @@ exports.tnow = function() {
return Math.floor(time.getTime() / 1000);
};
var DEFAULT_TRIALS_PER_BYTE = 1000;
var DEFAULT_EXTRA_BYTES = 1000;
var DEFAULT_TRIALS_PER_BYTE = exports.DEFAULT_TRIALS_PER_BYTE = 1000;
var DEFAULT_EXTRA_BYTES = exports.DEFAULT_EXTRA_BYTES = 1000;
exports.getTrials = function(opts) {
var nonceTrialsPerByte = opts.nonceTrialsPerByte;

View File

@ -13,6 +13,7 @@
"use strict";
var objectAssign = Object.assign || require("object-assign");
var bufferEqual = require("buffer-equal");
var assert = require("./_util").assert;
var promise = require("./platform").promise;
var bmcrypto = require("./crypto");
@ -95,8 +96,7 @@ var getpubkey = exports.getpubkey = {
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 objectPayload = decoded.objectPayload;
delete decoded.objectPayload;
var objectPayload = util.popkey(decoded, "objectPayload");
if (decoded.version < 4) {
assert(objectPayload.length === 20, "getpubkey ripe is too small");
// Object payload is copied so it's safe to return it right away.
@ -148,11 +148,11 @@ var getpubkey = exports.getpubkey = {
},
};
// Helper function for `pubkey.decode`.
// Extract pubkey data from decrypted object payload.
function extractPubkeyV2(buf) {
var decoded = {};
// Object payload is copied so it's safe to return it right away.
function extractPubkey(buf) {
var decoded = {length: 132};
// We assume here that input buffer was copied before so it's safe to
// return reference to it.
decoded.behavior = PubkeyBitfield(buf.slice(0, 4));
var signPublicKey = decoded.signPublicKey = new Buffer(65);
signPublicKey[0] = 4;
@ -163,23 +163,23 @@ function extractPubkeyV2(buf) {
return decoded;
}
// Helper function for `pubkey.decode`.
// Extract pubkey data from decrypted object payload.
// Extract pubkey version 3 data from decrypted object payload.
function extractPubkeyV3(buf) {
var decoded = {};
var length = 0;
var decodedTrials = var_int.decode(buf);
var decoded = extractPubkey(buf);
var decodedTrials = var_int.decode(buf.slice(132));
decoded.nonceTrialsPerByte = decodedTrials.value;
length += decodedTrials.length;
decoded.length += decodedTrials.length;
var decodedExtraBytes = var_int.decode(decodedTrials.rest);
decoded.payloadLengthExtraBytes = decodedExtraBytes.value;
length += decodedExtraBytes.length;
decoded.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;
var siglen = decodedSigLength.value;
var rest = decodedSigLength.rest;
assert(rest.length >= siglen, "Bad pubkey object payload length");
decoded.signature = rest.slice(0, siglen);
siglen += decodedSigLength.length;
decoded._siglen = siglen; // Internal value
decoded.length = length;
decoded.length += siglen;
return decoded;
}
@ -213,19 +213,17 @@ var pubkey = exports.pubkey = {
* object structure when fulfilled.
*/
decodePayloadAsync: function(buf, opts) {
return new promise(function(resolve, reject) {
return new promise(function(resolve) {
opts = opts || {};
var neededPubkeys = opts.neededPubkeys || {};
var decoded = object.decodePayload(buf);
assert(decoded.type === object.PUBKEY, "Wrong object type");
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");
var siglen, sig, dataToVerify, pubkeyp;
var objectPayload = util.popkey(decoded, "objectPayload");
var siglen, pos, sig, dataToVerify, pubkeyp;
var addr, addrs, tag, pubkeyEncPrivateKey, dataToDecrypt;
var length = 132;
// v2 pubkey.
if (version === 2) {
@ -233,9 +231,7 @@ var pubkey = exports.pubkey = {
assert(
objectPayload.length === 132,
"Bad pubkey v2 object payload length");
objectAssign(decoded, extractPubkeyV2(objectPayload));
// Real data length.
decoded.length = length;
objectAssign(decoded, extractPubkey(objectPayload));
return resolve(decoded);
}
@ -245,14 +241,11 @@ var pubkey = exports.pubkey = {
assert(
objectPayload.length >= 135,
"Bad pubkey v3 object payload length");
objectAssign(decoded, extractPubkeyV2(objectPayload));
objectAssign(decoded, extractPubkeyV3(objectPayload.slice(132)));
objectAssign(decoded, extractPubkeyV3(objectPayload));
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);
pos = decoded.headerLength + decoded.length - siglen;
// Object message payload from `expiresTime` up to `sig_length`.
dataToVerify = buf.slice(8, pos);
sig = decoded.signature;
pubkeyp = bmcrypto.verify(decoded.signPublicKey, dataToVerify, sig)
.then(function() {
@ -281,30 +274,26 @@ var pubkey = exports.pubkey = {
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"));
}
assert(pubkeyEncPrivateKey, "You are not interested in this pubkey v4");
dataToDecrypt = objectPayload.slice(32);
pubkeyp = bmcrypto.decrypt(pubkeyEncPrivateKey, dataToDecrypt)
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)));
objectAssign(decoded, extractPubkeyV3(decrypted));
siglen = util.popkey(decoded, "_siglen");
length += decoded.length;
// Real data 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),
// Unencrypted pubkey data without signature.
decrypted.slice(0, length - siglen),
decrypted.slice(0, decoded.length - siglen),
]);
sig = decoded.signature;
// Since data is encrypted, entire object payload is used.
decoded.length = objectPayload.length;
return bmcrypto.verify(decoded.signPublicKey, dataToVerify, sig);
}).then(function() {
return decoded;
@ -420,3 +409,289 @@ var pubkey = exports.pubkey = {
});
},
};
// Try to decrypt message with all provided identities.
function tryDecrypt(identities, buf) {
function inner(i) {
if (i > last) {
return promise.reject("Failed to decrypt msg with given identities");
}
return bmcrypto
.decrypt(identities[i].encPrivateKey, buf)
.then(function(decrypted) {
return {addr: identities[i], decrypted: decrypted};
}).catch(function() {
return inner(i + 1);
});
}
var last = identities.length - 1;
return inner(0);
}
// Loosely decode message in SIMPLE encoding.
function decodeSimple(buf) {
var decoded = {};
var message = buf.toString("utf8");
var subject, index;
if (message.slice(0, 8) === "Subject:") {
subject = message.slice(8);
index = subject.indexOf("\nBody:");
if (index !== -1) {
message = subject.slice(index + 6);
subject = subject.slice(0, index);
} else {
message = "";
}
decoded.subject = subject;
decoded.message = message;
} else {
decoded.subject = "";
decoded.message = message;
}
return decoded;
}
/**
* `msg` object.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#msg}
* @namespace
* @static
*/
var msg = exports.msg = {
/**
* Any data with this number may be ignored. The sending node might
* simply be sharing its public key with you.
*/
IGNORE: 0,
/**
* UTF-8. No 'Subject' or 'Body' sections. Useful for simple strings
* of data, like URIs or magnet links.
*/
TRIVIAL: 1,
/**
* UTF-8. Uses 'Subject' and 'Body' sections. No MIME is used.
*/
SIMPLE: 2,
/**
* Decode `msg` object message.
* @param {Buffer} buf - Message
* @param {?Object} opts - Decoding options
* @return {Promise.<Object>} A promise that contains decoded `msg`
* 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(msg.decodePayloadAsync(decoded.payload, opts));
});
},
/**
* Decode `msg` object message payload.
* @param {Buffer} buf - Message payload
* @param {Object} opts - Decoding options
* @return {Promise.<Object>} A promise that contains decoded `msg`
* object structure when fulfilled.
*/
decodePayloadAsync: function(buf, opts) {
return new promise(function(resolve) {
var decoded = object.decodePayload(buf);
assert(decoded.type === object.MSG, "Wrong object type");
assert(decoded.version === 1, "Wrong msg version");
var objectPayload = util.popkey(decoded, "objectPayload");
var msgp = tryDecrypt(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 decodedStream = var_int.decode(decodedVersion.rest);
decoded.senderStream = decodedStream.value;
// Behavior, keys.
assert(
decodedStream.rest.length >= 132,
"Bad msg object payload length");
objectAssign(decoded, extractPubkey(decodedStream.rest));
decoded.length += decodedVersion.length + decodedStream.length;
var rest = decrypted.slice(decoded.length);
// Pow extra.
if (decoded.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;
}
// Ripe, encoding.
assert(rest.length >= 20, "Bad msg object payload length");
decoded.ripe = rest.slice(0, 20);
assert(
bufferEqual(decoded.ripe, decInfo.addr.ripe),
"Message was decrypted but the destination ripe differs");
decoded.length += 20;
var decodedEncoding = var_int.decode(rest.slice(20));
var encoding = decoded.encoding = decodedEncoding.value;
decoded.length += decodedEncoding.length;
// Message.
var decodedMsgLength = var_int.decode(decodedEncoding.rest);
var msglen = decodedMsgLength.value;
rest = decodedMsgLength.rest;
assert(rest.length >= msglen, "Bad msg object payload length");
decoded.length += decodedMsgLength.length + msglen;
var message = rest.slice(0, msglen);
if (encoding === msg.TRIVIAL) {
decoded.message = message.toString("utf8");
} else if (encoding === msg.SIMPLE) {
objectAssign(decoded, decodeSimple(message));
} else {
decoded.message = message;
}
// Acknowledgement data.
// TODO(Kagami): Validate ack, check a POW.
var decodedAckLength = var_int.decode(rest.slice(msglen));
var acklen = decodedAckLength.value;
rest = decodedAckLength.rest;
assert(rest.length >= acklen, "Bad msg object payload length");
decoded.length += decodedAckLength.length + acklen;
decoded.ack = rest.slice(0, acklen);
// Signature.
var decodedSigLength = var_int.decode(rest.slice(acklen));
var siglen = decodedSigLength.value;
rest = decodedSigLength.rest;
assert(rest.length >= siglen, "Bad msg object payload length");
var sig = decoded.signature = rest.slice(0, siglen);
// Verify signature.
var dataToVerify = Buffer.concat([
// Object header without nonce.
buf.slice(8, decoded.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(msgp);
});
},
/**
* Encode `msg` object message.
* @param {Object} opts - `msg` object options
* @return {Promise.<Buffer>} A promise that contains encoded message
* when fulfilled.
*/
encodeAsync: function(opts) {
return msg.encodePayloadAsync(opts).then(function(payload) {
return message.encode("object", payload);
});
},
/**
* Encode `msg` object message payload.
* @param {Object} opts - `msg` 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.MSG;
opts.version = 1; // The only known msg version
var from = Address.decode(opts.from);
var to = Address.decode(opts.to);
opts.stream = to.stream;
var nonceTrialsPerByte, payloadLengthExtraBytes;
if (from.version >= 3) {
if (opts.friend) {
nonceTrialsPerByte = util.DEFAULT_TRIALS_PER_BYTE;
payloadLengthExtraBytes = util.DEFAULT_EXTRA_BYTES;
} else {
nonceTrialsPerByte = util.getTrials(from);
payloadLengthExtraBytes = util.getExtraBytes(from);
}
}
var encoding = opts.encoding || msg.TRIVIAL;
var message = opts.message;
var subject = opts.subject;
if (encoding === msg.IGNORE && !message) {
// User may omit message for IGNORE encoding.
message = new Buffer(0);
} else if (!Buffer.isBuffer(message)) {
// User may specify message as a string.
message = new Buffer(message, "utf8");
}
if (encoding === msg.SIMPLE && subject) {
// User may specify subject for SIMPLE encoding.
if (!Buffer.isBuffer(subject)) {
subject = new Buffer(subject, "utf8");
}
message = Buffer.concat([
new Buffer("Subject:"),
subject,
new Buffer("\nBody:"),
message,
]);
}
// Assembling the unencrypted message data.
var msgData = [
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) {
msgData.push(
var_int.encode(nonceTrialsPerByte),
var_int.encode(payloadLengthExtraBytes)
);
}
msgData.push(
to.ripe,
var_int.encode(encoding),
var_int.encode(message.length),
message
);
// TODO(Kagami): Calculate ACK.
msgData.push(var_int.encode(0));
opts.objectPayload = new Buffer(0);
var obj = object.encodePayloadWithoutNonce(opts);
var dataToSign = Buffer.concat([obj].concat(msgData));
var msgp = bmcrypto
.sign(from.signPrivateKey, dataToSign)
.then(function(sig) {
var dataToEnc = msgData.concat(var_int.encode(sig.length), sig);
dataToEnc = Buffer.concat(dataToEnc);
return bmcrypto.encrypt(to.encPublicKey, 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(msgp);
});
},
};

View File

@ -23,8 +23,8 @@ exports.decode = var_str.decode;
/**
* Parse raw user agent into software stack list. Most underlying
* software comes first.
* NOTE: Decoding is rather loose and non-strict, it won't fail on bad
* user agent format because it's not that important.
* NOTE: Decoding is rather loose, it won't fail on bad user agent
* format because it's not that important.
* @param {string} str - Raw user agent string
* @return {Object[]} Parsed user agent.
*/

86
test.js
View File

@ -26,6 +26,7 @@ var error = messages.error;
var objects = bitmessage.objects;
var getpubkey = objects.getpubkey;
var pubkey = objects.pubkey;
var msg = objects.msg;
var WIF = bitmessage.WIF;
var POW = bitmessage.POW;
var Address = bitmessage.Address;
@ -91,11 +92,11 @@ describe("Crypto", function() {
var privateKey = Buffer(32);
privateKey.fill(1);
var publicKey = bmcrypto.getPublic(privateKey);
var msg = Buffer("test");
return bmcrypto.sign(privateKey, msg).then(function(sig) {
var message = Buffer("test");
return bmcrypto.sign(privateKey, message).then(function(sig) {
expect(Buffer.isBuffer(sig)).to.be.true;
expect(sig.toString("hex")).to.equal("304402204737396b697e5a3400e3aedd203d8be89879f97708647252bd0c17752ff4c8f302201d52ef234de82ce0719679fa220334c83b80e21b8505a781d32d94a27d9310aa");
return bmcrypto.verify(publicKey, msg, sig);
return bmcrypto.verify(publicKey, message, sig);
});
});
@ -495,6 +496,15 @@ describe("Message types", function() {
// TODO(Kagami): Add tests for encodePayloadAsync/decodePayloadAsync as well.
describe("Object types", function() {
var signPrivateKey = Buffer("71c95d26c716a5e85e9af9efe26fb5f744dc98005a13d05d23ee92c77e038d9f", "hex");
var signPublicKey = bmcrypto.getPublic(signPrivateKey);
var encPrivateKey = Buffer("9f9969c93c2d186787a7653f70e49be34c03c4a853e6ad0c867db0946bc433c6", "hex");
var encPublicKey = bmcrypto.getPublic(encPrivateKey);
var from = Address({
signPrivateKey: signPrivateKey,
encPrivateKey: encPrivateKey,
});
it("should get type of the encoded object message", function() {
var encoded = object.encode({
nonce: Buffer(8),
@ -556,15 +566,6 @@ describe("Object types", function() {
});
describe("pubkey", function() {
var signPrivateKey = Buffer("71c95d26c716a5e85e9af9efe26fb5f744dc98005a13d05d23ee92c77e038d9f", "hex");
var signPublicKey = bmcrypto.getPublic(signPrivateKey);
var encPrivateKey = Buffer("9f9969c93c2d186787a7653f70e49be34c03c4a853e6ad0c867db0946bc433c6", "hex");
var encPublicKey = bmcrypto.getPublic(encPrivateKey);
var from = Address({
signPrivateKey: signPrivateKey,
encPrivateKey: encPrivateKey,
});
it("should encode and decode pubkey v2", function() {
return pubkey.encodeAsync({
ttl: 123,
@ -627,6 +628,67 @@ describe("Object types", function() {
});
});
});
describe("msg", function() {
it("should encode and decode msg", function() {
return msg.encodeAsync({
ttl: 111,
from: from,
to: from,
message: "test",
}).then(function(buf) {
expect(message.decode(buf).command).to.equal("object");
return msg.decodeAsync(buf, {identities: [from]});
}).then(function(res) {
expect(res.ttl).to.be.at.most(111);
expect(res.type).to.equal(object.MSG);
expect(res.version).to.equal(1);
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(bufferEqual(res.ripe, from.ripe)).to.be.true;
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("shouldn't decode msg without identities", function(done) {
return msg.encodeAsync({
ttl: 111,
from: from,
to: from,
message: "test",
}).then(function(buf) {
return msg.decodeAsync(buf, {identities: []});
}).catch(function() {
done();
});
});
it("should encode and decode SIMPLE msg", function() {
return msg.encodeAsync({
ttl: 111,
from: from,
to: from,
encoding: msg.SIMPLE,
subject: "Тема",
message: "Сообщение",
}).then(function(buf) {
return msg.decodeAsync(buf, {identities: [from]});
}).then(function(res) {
expect(res.encoding).to.equal(msg.SIMPLE);
expect(res.subject).to.equal("Тема");
expect(res.message).to.equal("Сообщение");
});
});
});
});
describe("WIF", function() {