Huge commit with partial objects.getpubkey impl

This commit is contained in:
Kagami Hiiragi 2015-01-22 03:00:58 +03:00
parent 33432c6735
commit d50dcac4a2
14 changed files with 707 additions and 244 deletions

View File

@ -30,6 +30,7 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/
- [x] HMAC-SHA-256 - [x] HMAC-SHA-256
- [x] Common structures - [x] Common structures
- [x] message - [x] message
- [x] object
- [x] var_int - [x] var_int
- [x] var_str - [x] var_str
- [x] var_int_list - [x] var_int_list
@ -45,10 +46,9 @@ API documentation is available [here](https://bitchan.github.io/bitmessage/docs/
- [x] inv - [x] inv
- [x] getdata - [x] getdata
- [x] error - [x] error
- [x] object
- [ ] Object types - [ ] Object types
- [x] getpubkey - [x] getpubkey
- [ ] pubkey - [x] pubkey
- [ ] msg - [ ] msg
- [ ] broadcast - [ ] broadcast
- [x] WIF - [x] WIF

View File

@ -1,3 +1,6 @@
// NOTE(Kagami): End-users shouldn't use this module. While it exports
// some helper routines, its API is _not_ stable.
"use strict"; "use strict";
var assert = exports.assert = function(condition, message) { var assert = exports.assert = function(condition, message) {
@ -6,6 +9,8 @@ var assert = exports.assert = function(condition, message) {
} }
}; };
exports.PROTOCOL_VERSION = 3;
// Missing methods to read/write 64 bits integers from/to buffers. // Missing methods to read/write 64 bits integers from/to buffers.
// TODO(Kagami): Use this helpers in structs, pow, platform. // TODO(Kagami): Use this helpers in structs, pow, platform.
@ -61,3 +66,31 @@ exports.tnow = function() {
var time = new Date(); var time = new Date();
return Math.floor(time.getTime() / 1000); return Math.floor(time.getTime() / 1000);
}; };
var DEFAULT_TRIALS_PER_BYTE = 1000;
var DEFAULT_EXTRA_BYTES = 1000;
exports.getTrials = function(opts) {
var nonceTrialsPerByte = opts.nonceTrialsPerByte;
// Automatically raise lower values per spec.
if (!nonceTrialsPerByte || nonceTrialsPerByte < DEFAULT_TRIALS_PER_BYTE) {
nonceTrialsPerByte = DEFAULT_TRIALS_PER_BYTE;
}
return nonceTrialsPerByte;
};
exports.getExtraBytes = function(opts) {
var payloadLengthExtraBytes = opts.payloadLengthExtraBytes;
// Automatically raise lower values per spec.
if (!payloadLengthExtraBytes ||
payloadLengthExtraBytes < DEFAULT_EXTRA_BYTES) {
payloadLengthExtraBytes = DEFAULT_EXTRA_BYTES;
}
return payloadLengthExtraBytes;
};
exports.popkey = function(obj, key) {
var value = obj[key];
delete obj[key];
return value;
};

View File

@ -9,10 +9,11 @@
var objectAssign = Object.assign || require("object-assign"); var objectAssign = Object.assign || require("object-assign");
var bufferEqual = require("buffer-equal"); var bufferEqual = require("buffer-equal");
var bs58 = require("bs58"); var bs58 = require("bs58");
var assert = require("./util").assert; var assert = require("./_util").assert;
var var_int = require("./structs").var_int; var var_int = require("./structs").var_int;
var PubkeyBitfield = require("./structs").PubkeyBitfield; var PubkeyBitfield = require("./structs").PubkeyBitfield;
var bmcrypto = require("./crypto"); var bmcrypto = require("./crypto");
var popkey = require("./_util").popkey;
/** /**
* Create a new Bitmessage address object. * Create a new Bitmessage address object.
@ -103,21 +104,40 @@ Address.prototype.getRipe = function(opts) {
} }
}; };
function getdoublehash(addr) {
var ripe = addr.getRipe();
var dataToHash = Buffer.concat([
var_int.encode(addr.version),
var_int.encode(addr.stream),
ripe,
]);
return bmcrypto.sha512(bmcrypto.sha512(dataToHash));
}
/**
* Calculate the encryption key used to encrypt/decrypt {@link pubkey}
* and {@link broadcast} objects.
* @return {Buffer} A 32-byte private key.
* @static
*/
var getPubkeyPrivateKey = Address.prototype.getPubkeyPrivateKey = function() {
return getdoublehash(this).slice(0, 32);
};
/**
* Just an alias to {@link Address#getPubkeyPrivateKey} for convinience.
*/
Address.prototype.getBroadcastPrivateKey = getPubkeyPrivateKey;
/** /**
* Calculate the address tag. * Calculate the address tag.
* @return {Buffer} A 32-byte address tag. * @return {Buffer} A 32-byte address tag.
*/ */
Address.prototype.getTag = function() { Address.prototype.getTag = function() {
var ripe = this.getRipe(); return getdoublehash(this).slice(32);
var dataToHash = Buffer.concat([
var_int.encode(this.version),
var_int.encode(this.stream),
ripe,
]);
var hash = bmcrypto.sha512(bmcrypto.sha512(dataToHash));
return hash.slice(32);
}; };
// Get truncated ripe hash length. // Get truncated ripe hash length.
function getripelen(ripe) { function getripelen(ripe) {
var zeroes = 0; var zeroes = 0;
@ -177,12 +197,6 @@ Address.prototype.encode = function() {
return "BM-" + bs58.encode(addr); return "BM-" + bs58.encode(addr);
}; };
function popkey(obj, key) {
var value = obj[key];
delete obj[key];
return value;
}
/** /**
* Create new Bitmessage address from random encryption and signing * Create new Bitmessage address from random encryption and signing
* private keys. * private keys.

View File

@ -7,15 +7,19 @@
"use strict"; "use strict";
var eccrypto = require("eccrypto"); var eccrypto = require("eccrypto");
var assert = require("./_util").assert;
var platform = require("./platform"); var platform = require("./platform");
var promise = platform.promise;
/** /**
* Calculate SHA-1 hash. * Calculate SHA-1 hash.
* @param {Buffer} buf - Input data * @param {Buffer} buf - Input data
* @return {Buffer} Resulting hash. * @return {Buffer} Resulting hash.
* @function * @function
* @static
*/ */
exports.sha1 = platform.sha1; var sha1 = exports.sha1 = platform.sha1;
/** /**
* Calculate SHA-256 hash. * Calculate SHA-256 hash.
@ -64,3 +68,107 @@ exports.getPrivate = function() {
* @function * @function
*/ */
exports.getPublic = eccrypto.getPublic; exports.getPublic = eccrypto.getPublic;
/**
* Sign message using ecdsa-with-sha1 scheme.
* @param {Buffer} privateKey - A 32-byte private key
* @msg {Buffer} msg - The message being signed
* @return {Promise.<Buffer>} A promise that contains signature in DER
* format when fulfilled.
*/
exports.sign = function(privateKey, msg) {
var hash = sha1(msg);
return eccrypto.sign(privateKey, hash);
};
/**
* Verify signature using ecdsa-with-sha1 scheme.
* @param {Buffer} publicKey - A 65-byte public key
* @msg {Buffer} msg - The message being verified
* @param {Buffer} sig - The signature in DER format
* @return {Promise.<undefined>} A promise that resolves on correct
* signature and rejects on bad key or signature.
*/
exports.verify = function(publicKey, msg, sig) {
var hash = sha1(msg);
return eccrypto.verify(publicKey, hash, sig);
};
var SECP256K1_TYPE = 714;
// We define this structure here to avoid circular imports. However we
// rexport and document it in `structs` module for consistency.
var encrypted = exports.encrypted = {
decode: function(buf) {
assert(buf.length >= 118, "Buffer is too small");
assert(buf.readUInt16BE(16, true) === SECP256K1_TYPE, "Bad curve type");
assert(buf.readUInt16BE(18, true) === 32, "Bad Rx length");
assert(buf.readUInt16BE(52, true) === 32, "Bad Ry length");
var iv = new Buffer(16);
buf.copy(iv, 0, 0, 16);
var ephemPublicKey = new Buffer(65);
ephemPublicKey[0] = 0x04;
buf.copy(ephemPublicKey, 1, 20, 52);
buf.copy(ephemPublicKey, 33, 54, 86);
// NOTE(Kagami): We do copy instead of slice to protect against
// possible source buffer modification by user.
var ciphertext = new Buffer(buf.length - 118);
buf.copy(ciphertext, 0, 86, buf.length - 32);
var mac = new Buffer(32);
buf.copy(mac, 0, buf.length - 32);
return {
iv: iv,
ephemPublicKey: ephemPublicKey,
ciphertext: ciphertext,
mac: mac,
};
},
encode: function(opts) {
assert(opts.iv.length === 16, "Bad IV");
assert(opts.ephemPublicKey.length === 65, "Bad public key");
assert(opts.mac.length === 32, "Bad MAC");
// 16 + 2 + 2 + 32 + 2 + 32 + ? + 32
var buf = new Buffer(118 + opts.ciphertext.length);
opts.iv.copy(buf);
buf.writeUInt16BE(SECP256K1_TYPE, 16, true); // Curve type
buf.writeUInt16BE(32, 18, true); // Rx length
opts.ephemPublicKey.copy(buf, 20, 1, 33); // Rx
buf.writeUInt16BE(32, 52, true); // Ry length
opts.ephemPublicKey.copy(buf, 54, 33); // Ry
opts.ciphertext.copy(buf, 86);
opts.mac.copy(buf, 86 + opts.ciphertext.length);
return buf;
},
};
/**
* Encrypt message for given recepient's public key.
* @param {Buffer} publicKeyTo - Recipient's public key (65 bytes)
* @param {Buffer} msg - The message being encrypted
* @param {?{?iv: Buffer, ?ephemPrivateKey: Buffer}} opts - You may also
* specify initialization vector (16 bytes) and ephemeral private key
* (32 bytes) to get deterministic results.
* @return {Promise.<Buffer>} - A promise that resolves with the buffer
* in `encrypted` format successful encryption and rejects on failure.
*/
exports.encrypt = function(publicKeyTo, msg, opts) {
return eccrypto.encrypt(publicKeyTo, msg, opts).then(function(encObj) {
return encrypted.encode(encObj);
});
};
/**
* Decrypt message using given private key.
* @param {Buffer} privateKey - A 32-byte private key of recepient of
* the mesage
* @param {Buffer} buf - encrypted data
* @return {Promise.<Buffer>} - A promise that resolves with the
* plaintext on successful decryption and rejects on failure.
*/
exports.decrypt = function(privateKey, buf) {
return new promise(function(resolve) {
var encObj = encrypted.decode(buf);
resolve(eccrypto.decrypt(privateKey, encObj));
});
};

View File

@ -6,7 +6,7 @@
"use strict"; "use strict";
/** Current protocol version. */ /** Current protocol version. */
exports.PROTOCOL_VERSION = 3; exports.PROTOCOL_VERSION = require("./_util").PROTOCOL_VERSION;
/** [Common structures.]{@link module:bitmessage/structs} */ /** [Common structures.]{@link module:bitmessage/structs} */
exports.structs = require("./structs"); exports.structs = require("./structs");

View File

@ -9,11 +9,12 @@
"use strict"; "use strict";
var assert = require("./util").assert; var assert = require("./_util").assert;
var structs = require("./structs"); var structs = require("./structs");
var ServicesBitfield = structs.ServicesBitfield;
var UserAgent = require("./user-agent"); var UserAgent = require("./user-agent");
var util = require("./util"); var util = require("./_util");
var ServicesBitfield = structs.ServicesBitfield;
/** /**
* `version` message. * `version` message.
@ -77,7 +78,7 @@ exports.version = {
var streamNumbers = opts.streamNumbers || [1]; var streamNumbers = opts.streamNumbers || [1];
// Start encoding. // Start encoding.
var protoVersion = new Buffer(4); var protoVersion = new Buffer(4);
protoVersion.writeUInt32BE(require("./").PROTOCOL_VERSION, 0); protoVersion.writeUInt32BE(util.PROTOCOL_VERSION, 0);
var addrRecv = structs.net_addr.encode({ var addrRecv = structs.net_addr.encode({
services: services, services: services,
host: opts.remoteHost, host: opts.remoteHost,
@ -263,91 +264,3 @@ var error = exports.error = {
]); ]);
}, },
}; };
/**
* `object` message. An `object` is a message which is shared throughout
* a stream. It is the only message which propagates; all others are
* only between two nodes.
* NOTE: You shouldn't use `encode` and `decode` methods directly.
* Instead, get type of the object and process it using appropriate
* namespace from `objects` module.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#object}
* @namespace
*/
exports.object = {
// Known types.
GETPUBKEY: 0,
PUBKEY: 1,
MSG: 2,
BROADCAST: 3,
/**
* Returns type of the `object` message if we can parse it. Per spec
* we should still relay unknown objects.
* @param {Buffer} buf - Message payload
* @return {?number} Object type.
*/
getType: function(buf) {
assert(buf.length >= 22, "object message payload is too small");
return buf.readUInt32BE(16, true);
},
/**
* Decode `object` message payload.
* NOTE: `nonce` and `payload` are copied.
* @param {Buffer} buf - Message payload
* @return {Object} Decoded `object` structure.
*/
// FIXME(Kagami): Check for POW.
// TODO(Kagami): Option to not fail on bad POW (may be useful for
// bitchan).
// TODO(Kagami): Option to not fail on expired object (would be useful
// for bitchan).
decode: function(buf) {
// 8 + 8 + 4 + (1+) + (1+)
assert(buf.length >= 22, "object message payload is too small");
var nonce = new Buffer(8);
buf.copy(nonce, 0, 0, 8);
var expiresTime = util.readTimestamp64BE(buf.slice(8, 16));
var ttl = expiresTime - util.tnow();
assert(ttl >= -3600, "Object expired more than a hour ago");
assert(ttl <= 2430000, "expiresTime is too far in the future");
var type = buf.readUInt32BE(16, true);
var decodedVersion = structs.var_int.decode(buf.slice(20));
var decodedStream = structs.var_int.decode(decodedVersion.rest);
var payload = new Buffer(decodedStream.rest.length);
decodedStream.rest.copy(payload);
return {
nonce: nonce,
ttl: ttl,
type: type,
version: decodedVersion.value,
stream: decodedStream.value,
payload: payload,
};
},
/**
* Encode `object` message payload.
* @param {Object} opts - Object options
* @return {Buffer} Encoded payload.
*/
// TODO(Kagami): Do a POW if nonce is not provided.
encode: function(opts) {
assert(opts.nonce.length === 8, "Bad nonce");
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;
var type = new Buffer(4);
type.writeUInt32BE(opts.type, 0);
var stream = opts.stream || 1;
return Buffer.concat([
opts.nonce,
util.writeUInt64BE(null, expiresTime),
type,
structs.var_int.encode(opts.version),
structs.var_int.encode(stream),
opts.payload,
]);
},
};

View File

@ -6,14 +6,21 @@
* @module bitmessage/objects * @module bitmessage/objects
*/ */
// TODO(Kagami): Document object-like params. // 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"; "use strict";
var objectAssign = Object.assign || require("object-assign"); var objectAssign = Object.assign || require("object-assign");
var assert = require("./util").assert; var assert = require("./_util").assert;
var promise = require("./platform").promise; var promise = require("./platform").promise;
var object = require("./messages").object; var bmcrypto = require("./crypto");
var Address = require("./address"); 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 * `getpubkey` object. When a node has the hash of a public key (from an
@ -26,13 +33,15 @@ exports.getpubkey = {
/** /**
* Decode `getpubkey` object message payload. * Decode `getpubkey` object message payload.
* @param {Buffer} buf - Message payload * @param {Buffer} buf - Message payload
* @return {Promise.<Object>} A promise that contained decoded * @return {Promise.<Object>} A promise that contains decoded
* `getpubkey` object structure when fulfilled. * `getpubkey` object structure when fulfilled.
*/ */
decodeAsync: function(buf) { decodeAsync: function(buf) {
return new promise(function(resolve) { return new promise(function(resolve) {
var decoded = object.decode(buf); var decoded = object.decode(buf);
assert(decoded.type === object.GETPUBKEY, "Wrong object type"); 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; var payload = decoded.payload;
delete decoded.payload; delete decoded.payload;
if (decoded.version < 4) { if (decoded.version < 4) {
@ -51,18 +60,265 @@ exports.getpubkey = {
/** /**
* Encode `getpubkey` object message payload. * Encode `getpubkey` object message payload.
* @param {Object} opts - `getpubkey` object options * @param {Object} opts - `getpubkey` object options
* @return {Promise.<Buffer>} A promise that contained encoded message * @return {Promise.<Buffer>} A promise that contains encoded message
* payload when fulfilled. * payload when fulfilled.
*/ */
// FIXME(Kagami): Do a POW.
encodeAsync: function(opts) { encodeAsync: function(opts) {
return new promise(function(resolve) { return new promise(function(resolve) {
opts = objectAssign({}, opts); opts = objectAssign({}, opts);
opts.type = object.GETPUBKEY; opts.type = object.GETPUBKEY;
var addr = Address.decode(opts.to); // Bitmessage address of recepeint of `getpubkey` message.
opts.version = addr.version; var to = Address.decode(opts.to);
opts.stream = addr.stream; assert(to.version >= 2, "Address version is too low");
opts.payload = addr.version < 4 ? addr.getRipe() : addr.getTag(); 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)); 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);
});
},
};

View File

@ -14,7 +14,9 @@ var hash = require("hash.js");
var Sha512 = require("sha.js/sha512"); var Sha512 = require("sha.js/sha512");
var BN = require("bn.js"); var BN = require("bn.js");
var work = require("webworkify"); var work = require("webworkify");
var assert = require("./util").assert; var assert = require("./_util").assert;
var cryptoObj = window.crypto || window.msCrypto;
exports.sha1 = function(buf) { exports.sha1 = function(buf) {
return new Buffer(hash.sha1().update(buf).digest()); return new Buffer(hash.sha1().update(buf).digest());
@ -34,7 +36,7 @@ exports.ripemd160 = function(buf) {
exports.randomBytes = function(size) { exports.randomBytes = function(size) {
var arr = new Uint8Array(size); var arr = new Uint8Array(size);
window.crypto.getRandomValues(arr); cryptoObj.getRandomValues(arr);
return new Buffer(arr); return new Buffer(arr);
}; };
@ -139,4 +141,4 @@ exports.pow = function(opts) {
return powp; return powp;
}; };
exports.promise = Promise; exports.promise = window.Promise;

View File

@ -10,7 +10,7 @@ var promise = typeof Promise === "undefined" ?
require("es6-promise").Promise : require("es6-promise").Promise :
Promise; Promise;
var bignum = require("bignum"); var bignum = require("bignum");
var assert = require("./util").assert; var assert = require("./_util").assert;
var worker = require("./worker"); var worker = require("./worker");
var createHash = crypto.createHash; var createHash = crypto.createHash;

View File

@ -10,9 +10,7 @@
var objectAssign = Object.assign || require("object-assign"); var objectAssign = Object.assign || require("object-assign");
var bmcrypto = require("./crypto"); var bmcrypto = require("./crypto");
var platform = require("./platform"); var platform = require("./platform");
var util = require("./_util");
var DEFAULT_TRIALS_PER_BYTE = 1000;
var DEFAULT_EXTRA_BYTES = 1000;
/** /**
* Calculate target * Calculate target
@ -22,20 +20,11 @@ var DEFAULT_EXTRA_BYTES = 1000;
// Just a wrapper around platform-specific implementation. // Just a wrapper around platform-specific implementation.
exports.getTarget = function(opts) { exports.getTarget = function(opts) {
var payloadLength = opts.payloadLength || opts.payload.length; var payloadLength = opts.payloadLength || opts.payload.length;
var nonceTrialsPerByte = opts.nonceTrialsPerByte;
// Automatically raise lower values per spec.
if (!nonceTrialsPerByte || nonceTrialsPerByte < DEFAULT_TRIALS_PER_BYTE) {
nonceTrialsPerByte = DEFAULT_TRIALS_PER_BYTE;
}
var payloadLengthExtraBytes = opts.payloadLengthExtraBytes;
if (!payloadLengthExtraBytes || payloadLengthExtraBytes < DEFAULT_EXTRA_BYTES) {
payloadLengthExtraBytes = DEFAULT_EXTRA_BYTES;
}
return platform.getTarget({ return platform.getTarget({
ttl: opts.ttl, ttl: opts.ttl,
payloadLength: payloadLength, payloadLength: payloadLength,
nonceTrialsPerByte: nonceTrialsPerByte, nonceTrialsPerByte: util.getTrials(opts),
payloadLengthExtraBytes: payloadLengthExtraBytes, payloadLengthExtraBytes: util.getExtraBytes(opts),
}); });
}; };

View File

@ -9,8 +9,9 @@
var objectAssign = Object.assign || require("object-assign"); var objectAssign = Object.assign || require("object-assign");
var bufferEqual = require("buffer-equal"); var bufferEqual = require("buffer-equal");
var assert = require("./util").assert; var assert = require("./_util").assert;
var bmcrypto = require("./crypto"); var bmcrypto = require("./crypto");
var util = require("./_util");
function isAscii(str) { function isAscii(str) {
for (var i = 0; i < str.length; i++) { for (var i = 0; i < str.length; i++) {
@ -91,6 +92,95 @@ var message = exports.message = {
}, },
}; };
/**
* `object` message. An `object` is a message which is shared throughout
* a stream. It is the only message which propagates; all others are
* only between two nodes.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#object}
* @namespace
* @static
*/
var object = exports.object = {
// Known types.
GETPUBKEY: 0,
PUBKEY: 1,
MSG: 2,
BROADCAST: 3,
/**
* Decode `object` message payload.
* NOTE: `nonce` and `payload` are copied.
* @param {Buffer} buf - Message payload
* @return {Object} Decoded `object` structure.
*/
// FIXME(Kagami): Check a POW.
// TODO(Kagami): Option to not fail on bad POW (may be useful for
// bitchan).
// TODO(Kagami): Option to not fail on expired objects (would be
// useful for bitchan).
decode: function(buf) {
// 8 + 8 + 4 + (1+) + (1+)
assert(buf.length >= 22, "object message payload is too small");
var nonce = new Buffer(8);
buf.copy(nonce, 0, 0, 8);
var expiresTime = util.readTimestamp64BE(buf.slice(8, 16));
var ttl = expiresTime - util.tnow();
assert(ttl >= -3600, "Object expired more than a hour ago");
assert(ttl <= 2430000, "expiresTime is too far in the future");
var type = buf.readUInt32BE(16, true);
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);
return {
nonce: nonce,
ttl: ttl,
type: type,
version: decodedVersion.value,
stream: decodedStream.value,
headerLength: headerLength,
payload: payload,
};
},
/**
* 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) {
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;
var type = new Buffer(4);
type.writeUInt32BE(opts.type, 0);
var stream = opts.stream || 1;
return Buffer.concat([
util.writeUInt64BE(null, expiresTime),
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),
]);
},
};
/** /**
* Variable length integer. * Variable length integer.
@ -435,67 +525,26 @@ exports.inv_vect = {
}, },
}; };
var SECP256K1_TYPE = 714;
/** /**
* Encrypted payload. * Encrypted payload.
* @see {@link https://bitmessage.org/wiki/Protocol_specification#Encrypted_payload} * @see {@link https://bitmessage.org/wiki/Protocol_specification#Encrypted_payload}
* @namespace * @namespace encrypted
*/ */
exports.encrypted = { /**
/**
* Decode encrypted payload. * Decode encrypted payload.
* NOTE: all structure members are copied. * NOTE: all structure members are copied.
* @param {Buffer} buf - A buffer that contains encrypted payload * @param {Buffer} buf - A buffer that contains encrypted payload
* @return {Object} Decoded encrypted structure. * @return {Object} Decoded encrypted structure.
* @function encrypted.decode
*/ */
decode: function(buf) { /**
assert(buf.length >= 118, "Buffer is too small");
assert(buf.readUInt16BE(16, true) === SECP256K1_TYPE, "Bad curve type");
assert(buf.readUInt16BE(18, true) === 32, "Bad Rx length");
assert(buf.readUInt16BE(52, true) === 32, "Bad Ry length");
var iv = new Buffer(16);
buf.copy(iv, 0, 0, 16);
var ephemPublicKey = new Buffer(65);
ephemPublicKey[0] = 0x04;
buf.copy(ephemPublicKey, 1, 20, 52);
buf.copy(ephemPublicKey, 33, 54, 86);
// NOTE(Kagami): We do copy instead of slice to protect against
// possible source buffer modification by user.
var ciphertext = new Buffer(buf.length - 118);
buf.copy(ciphertext, 0, 86, buf.length - 32);
var mac = new Buffer(32);
buf.copy(mac, 0, buf.length - 32);
return {
iv: iv,
ephemPublicKey: ephemPublicKey,
ciphertext: ciphertext,
mac: mac,
};
},
/**
* Encode `encrypted`. * Encode `encrypted`.
* @param {Object} opts - Encode options * @param {Object} opts - Encode options
* @return {Buffer} Encoded encrypted payload. * @return {Buffer} Encoded encrypted payload.
* @function encrypted.encode
*/ */
encode: function(opts) { // Reexport struct.
assert(opts.iv.length === 16, "Bad IV"); exports.encrypted = bmcrypto.encrypted;
assert(opts.ephemPublicKey.length === 65, "Bad public key");
assert(opts.mac.length === 32, "Bad MAC");
// 16 + 2 + 2 + 32 + 2 + 32 + ? + 32
var buf = new Buffer(118 + opts.ciphertext.length);
opts.iv.copy(buf);
buf.writeUInt16BE(SECP256K1_TYPE, 16, true); // Curve type
buf.writeUInt16BE(32, 18, true); // Rx length
opts.ephemPublicKey.copy(buf, 20, 1, 33); // Rx
buf.writeUInt16BE(32, 52, true); // Ry length
opts.ephemPublicKey.copy(buf, 54, 33); // Ry
opts.ciphertext.copy(buf, 86);
opts.mac.copy(buf, 86 + opts.ciphertext.length);
return buf;
},
};
// Creates bitfield (MSB 0) class of the specified size. // Creates bitfield (MSB 0) class of the specified size.
var Bitfield = function(size) { var Bitfield = function(size) {

View File

@ -8,7 +8,7 @@
var bufferEqual = require("buffer-equal"); var bufferEqual = require("buffer-equal");
var bs58 = require("bs58"); var bs58 = require("bs58");
var assert = require("./util").assert; var assert = require("./_util").assert;
var bmcrypto = require("./crypto"); var bmcrypto = require("./crypto");
// Compute the WIF checksum for the given data. // Compute the WIF checksum for the given data.

View File

@ -50,7 +50,7 @@
"bn.js": "^1.0.0", "bn.js": "^1.0.0",
"bs58": "^2.0.0", "bs58": "^2.0.0",
"buffer-equal": "~0.0.1", "buffer-equal": "~0.0.1",
"eccrypto": "^0.9.0", "eccrypto": "^0.9.3",
"es6-promise": "^2.0.1", "es6-promise": "^2.0.1",
"hash.js": "^1.0.2", "hash.js": "^1.0.2",
"nan": "^1.4.1", "nan": "^1.4.1",

193
test.js
View File

@ -8,6 +8,7 @@ var bitmessage = require("./lib");
var bmcrypto = require("./lib/crypto"); var bmcrypto = require("./lib/crypto");
var structs = bitmessage.structs; var structs = bitmessage.structs;
var message = structs.message; var message = structs.message;
var object = structs.object;
var var_int = structs.var_int; var var_int = structs.var_int;
var var_str = structs.var_str; var var_str = structs.var_str;
var var_int_list = structs.var_int_list; var var_int_list = structs.var_int_list;
@ -21,9 +22,9 @@ var version = messages.version;
var addr = messages.addr; var addr = messages.addr;
var inv = messages.inv; var inv = messages.inv;
var error = messages.error; var error = messages.error;
var object = messages.object;
var objects = bitmessage.objects; var objects = bitmessage.objects;
var getpubkey = objects.getpubkey; var getpubkey = objects.getpubkey;
var pubkey = objects.pubkey;
var WIF = bitmessage.WIF; var WIF = bitmessage.WIF;
var POW = bitmessage.POW; var POW = bitmessage.POW;
var Address = bitmessage.Address; var Address = bitmessage.Address;
@ -65,8 +66,48 @@ describe("Crypto", function() {
expect(bytes[value]).to.be.below(7); expect(bytes[value]).to.be.below(7);
} }
// Ideal sum = (255 / 2) * size = 12750 // Ideal sum = (255 / 2) * size = 12750
expect(sum).to.be.above(10000); expect(sum).to.be.above(5000);
expect(sum).to.be.below(15000); expect(sum).to.be.below(20000);
});
it("should generate private keys", function() {
var privateKey = bmcrypto.getPrivate();
expect(Buffer.isBuffer(privateKey)).to.be.true;
expect(privateKey.length).to.equal(32);
var sum = 0;
for (var i = 0; i < 32; i++) { sum += privateKey[i]; }
expect(sum).to.be.above(0);
expect(sum).to.be.below(8160);
});
it("should allow to convert private key to public", function() {
var privateKey = Buffer(32);
privateKey.fill(1);
expect(bmcrypto.getPublic(privateKey).toString("hex")).to.equal("041b84c5567b126440995d3ed5aaba0565d71e1834604819ff9c17f5e9d5dd078f70beaf8f588b541507fed6a642c5ab42dfdf8120a7f639de5122d47a69a8e8d1");
});
it("should allow to sign and verify message", 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) {
expect(Buffer.isBuffer(sig)).to.be.true;
expect(sig.toString("hex")).to.equal("304402204737396b697e5a3400e3aedd203d8be89879f97708647252bd0c17752ff4c8f302201d52ef234de82ce0719679fa220334c83b80e21b8505a781d32d94a27d9310aa");
return bmcrypto.verify(publicKey, msg, sig);
});
});
it("should allow to encrypt and decrypt message", function() {
var privateKeyA = bmcrypto.getPrivate();
var publicKeyA = bmcrypto.getPublic(privateKeyA);
return bmcrypto.encrypt(publicKeyA, Buffer("msg to a")).then(function(buf) {
expect(Buffer.isBuffer(buf)).to.be.true;
return bmcrypto.decrypt(privateKeyA, buf).then(function(plaintext) {
expect(Buffer.isBuffer(plaintext)).to.be.true;
expect(plaintext.toString()).to.equal("msg to a");
});
});
}); });
}); });
@ -93,6 +134,37 @@ describe("Common structures", function() {
}); });
}); });
describe("object", function() {
it("should encode and decode", function() {
var nonce = Buffer(8);
var res = object.decode(object.encode({
nonce: nonce,
ttl: 100,
type: 2,
version: 1,
payload: Buffer("test"),
}));
expect(bufferEqual(nonce, res.nonce)).to.be.true;
expect(res.ttl).to.be.at.least(100);
expect(res.type).to.equal(2);
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");
});
it("shouldn't encode too big TTL", function() {
expect(object.encode.bind(null, {
nonce: Buffer(8),
ttl: 10000000,
type: 2,
version: 1,
payload: Buffer("test"),
})).to.throw(Error);
});
});
describe("var_int", function() { describe("var_int", function() {
it("should decode", function() { it("should decode", function() {
var res; var res;
@ -372,55 +444,12 @@ describe("Message types", function() {
expect(res.length).to.equal(18); expect(res.length).to.equal(18);
}); });
}); });
describe("object", function() {
it("should encode and decode", function() {
var nonce = Buffer(8);
var res = object.decode(object.encode({
nonce: nonce,
ttl: 100,
type: 2,
version: 1,
payload: Buffer("test"),
}));
expect(bufferEqual(nonce, res.nonce)).to.be.true;
expect(res.ttl).to.be.at.least(100);
expect(res.type).to.equal(2);
expect(res.version).to.equal(1);
expect(res.stream).to.equal(1);
expect(res.payload.toString()).to.equal("test");
});
it("shouldn't encode too big TTL", function() {
expect(object.encode.bind(null, {
nonce: Buffer(8),
ttl: 10000000,
type: 2,
version: 1,
payload: Buffer("test"),
})).to.throw(Error);
});
it("should return type of the object", function() {
var encoded = object.encode({
nonce: Buffer(8),
ttl: 100,
type: object.BROADCAST,
version: 1,
payload: Buffer("test"),
});
expect(object.getType(encoded)).to.equal(3);
expect(object.decode(encoded).type).to.equal(3);
});
});
}); });
describe("Object types", function() { describe("Object types", function() {
describe("getpubkey", function() { describe("getpubkey", function() {
it("should encode and decode getpubkey v3", function() { it("should encode and decode getpubkey v3", function() {
return getpubkey.encodeAsync({ return getpubkey.encodeAsync({
nonce: Buffer(8),
ttl: 100, ttl: 100,
to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU", to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU",
}).then(getpubkey.decodeAsync) }).then(getpubkey.decodeAsync)
@ -436,7 +465,6 @@ describe("Object types", function() {
it("should encode and decode getpubkey v4", function() { it("should encode and decode getpubkey v4", function() {
return getpubkey.encodeAsync({ return getpubkey.encodeAsync({
nonce: Buffer(8),
ttl: 100, ttl: 100,
to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z", to: "2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z",
}).then(getpubkey.decodeAsync) }).then(getpubkey.decodeAsync)
@ -450,6 +478,72 @@ 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,
from: from,
to: "BM-onhypnh1UMhbQpmvdiPuG6soLLytYJAfH",
}).then(pubkey.decodeAsync)
.then(function(res) {
expect(res.ttl).to.equal(123);
expect(res.type).to.equal(object.PUBKEY);
expect(res.version).to.equal(2);
expect(res.stream).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.length).to.equal(132);
});
});
it("should encode and decode pubkey v3", function() {
return pubkey.encodeAsync({
ttl: 456,
from: from,
to: "BM-2D8Jxw5yiepaQqxrx43iPPNfRqbvWoJLoU",
}).then(pubkey.decodeAsync)
.then(function(res) {
expect(res.ttl).to.equal(456);
expect(res.type).to.equal(object.PUBKEY);
expect(res.version).to.equal(3);
expect(res.stream).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);
});
});
it("should encode and decode pubkey v4", function() {
return pubkey.encodeAsync({ttl: 789, from: from, to: from})
.then(function(buf) {
return pubkey.decodeAsync(buf, {neededPubkeys: from});
}).then(function(res) {
expect(res.ttl).to.equal(789);
expect(res.type).to.equal(object.PUBKEY);
expect(res.version).to.equal(4);
expect(res.stream).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.tag, from.getTag())).to.be.true;
});
});
});
}); });
describe("WIF", function() { describe("WIF", function() {
@ -586,6 +680,11 @@ describe("High-level classes", function() {
expect(addr.getTag().toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a"); expect(addr.getTag().toString("hex")).to.equal("facf1e3e6c74916203b7f714ca100d4d60604f0917696d0f09330f82f52bed1a");
}); });
it("should calculate a private key to encrypt pubkey object", function() {
var addr = Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z");
expect(addr.getPubkeyPrivateKey().toString("hex")).to.equal("15e516173769dc87d4a8e8ed90200362fa58c0228bb2b70b06f26c089a9823a4");
});
it("should allow to decode Address instance", function() { it("should allow to decode Address instance", function() {
var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z"); var addr = Address.decode("2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z");
expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56"); expect(addr.ripe.toString("hex")).to.equal("003ab6655de4bd8c603eba9b00dd5970725fdd56");