From 1ccd56f17a2d69cf072d01a2fab4edabedbe6abe Mon Sep 17 00:00:00 2001 From: Kagami Hiiragi Date: Thu, 18 Dec 2014 19:47:18 +0300 Subject: [PATCH] Implement address decoding --- .jshintrc | 2 +- lib/address.js | 72 +++++++++++++++++++++++++++++++++++++++++++ lib/crypto.browser.js | 18 ++++++++++- lib/crypto.js | 12 +++++--- lib/index.js | 8 +++++ lib/utils.js | 7 +++++ lib/varint.js | 39 +++++++++++++++++++++++ package.json | 8 ++++- test.js | 63 +++++++++++++++++++++++++++++++++++-- 9 files changed, 219 insertions(+), 10 deletions(-) create mode 100644 lib/address.js create mode 100644 lib/utils.js create mode 100644 lib/varint.js diff --git a/.jshintrc b/.jshintrc index 411eb42..dafdeb9 100644 --- a/.jshintrc +++ b/.jshintrc @@ -7,7 +7,7 @@ // Enforcing "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) "camelcase" : false, // true: Identifiers must be in camelCase - "curly" : false, // true: Require {} for every new block or scope + "curly" : true, // true: Require {} for every new block or scope "eqeqeq" : true, // true: Require triple equals (===) for comparison "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() diff --git a/lib/address.js b/lib/address.js new file mode 100644 index 0000000..1d28490 --- /dev/null +++ b/lib/address.js @@ -0,0 +1,72 @@ +/** + * Working with Bitmessage addresses. + * @module bitmessage/address + */ + +"use strict"; + +var bufferEqual = require("buffer-equal"); +var bs58 = require("bs58"); +var varint = require("./varint"); +var bmcrypto = require("./crypto"); +var assert = require("./utils").assert; + +/** + * Parse Bitmessage Base58 encoded address (with or without `BM-` + * prefix) into address object. + */ +exports.decode = function(str) { + str = str.trim(); + if (str.slice(0, 3) === "BM-") { + str = str.slice(3); + } + + var bytes; + try { + bytes = bs58.decode(str); + } catch(e) { + return Promise.reject(e); + } + + // Checksum validating. + var data = new Buffer(bytes.slice(0, -4)); + var checksum = new Buffer(bytes.slice(-4)); + return bmcrypto.sha512(data).then(bmcrypto.sha512).then(function(dhash) { + assert(bufferEqual(dhash.slice(0, 4), checksum), "Bad checkum"); + + var decoded = varint.decode(data); + var version = decoded.value; + assert(version <= 4, "Version too high"); + assert(version >= 1, "Version too low"); + + data = decoded.rest; + decoded = varint.decode(data); + var stream = decoded.value; + + var ripe = decoded.rest; + var ripelen = ripe.length; + switch (version) { + case 1: + assert(ripelen === 20); + break; + case 2: + case 3: + assert(ripelen >= 18, "Ripe too short"); + assert(ripelen <= 20, "Ripe too long"); + break; + case 4: + assert(ripelen >= 4, "Ripe too short"); + assert(ripelen <= 20, "Ripe too long"); + assert(ripe[0] !== 0, "Ripe encode error"); + break; + } + + // Prevent extra allocation. God, kill me please for premature + // optimizations. + if (ripelen < 20) { + var zeroes = new Buffer(Array(20 - ripelen)); + ripe = Buffer.concat([zeroes, ripe]); + } + return {version: version, stream: stream, ripe: ripe}; + }); +}; diff --git a/lib/crypto.browser.js b/lib/crypto.browser.js index 91f3c69..d2fd33f 100644 --- a/lib/crypto.browser.js +++ b/lib/crypto.browser.js @@ -1,12 +1,28 @@ /** * Browser version of the crypto for Bitmessage JS implementation. - * @module bitmessage/lib/crypto.browser + * + * Documentation: + * Browsers support: + * Blink implementation details: + * + * @module bitmessage/crypto.browser */ +// FIXME(Kagami): Support webkit subtle prefix! +// TODO(Kagami): Try to support IE11. "use strict"; +require("es6-promise").polyfill(); +var ripemd160 = require("ripemd160"); + exports.sha512 = function(buf) { return window.crypto.subtle.digest({name: "SHA-512"}, buf).then(function(arr) { return new Buffer(new Uint8Array(arr)); }); }; + +exports.ripemd160 = function(buf) { + // XXX(Kagami): No support in browsers via Web Crypto API currently, + // so use module. + return Promise.resolve(ripemd160(buf)); +}; diff --git a/lib/crypto.js b/lib/crypto.js index debc832..c341da4 100644 --- a/lib/crypto.js +++ b/lib/crypto.js @@ -1,13 +1,11 @@ /** * Node.js version of the crypto for Bitmessage JS implementation. - * Wrap all crypto functions with promises because WebCryptoAPI uses it - * throughout. - * @module bitmessage/lib/crypto + * @module bitmessage/crypto */ "use strict"; -var Promise = require("es6-promise").Promise; // jshint ignore:line +require("es6-promise").polyfill(); var crypto = require("crypto"); exports.sha512 = function(buf) { @@ -15,3 +13,9 @@ exports.sha512 = function(buf) { hash.update(buf); return Promise.resolve(hash.digest()); }; + +exports.ripemd160 = function(buf) { + var hash = crypto.createHash("ripemd160"); + hash.update(buf); + return Promise.resolve(hash.digest()); +}; diff --git a/lib/index.js b/lib/index.js index e69de29..c360e9d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -0,0 +1,8 @@ +/** + * Main Bitmessage module. Just reexports all public submodules. + * @module bitmessage + */ + +"use strict"; + +exports.Address = require("./address"); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..ffe85a2 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,7 @@ +"use strict"; + +exports.assert = function(condition, message) { + if (!condition) { + throw new Error(message || "Assertion failed"); + } +}; diff --git a/lib/varint.js b/lib/varint.js new file mode 100644 index 0000000..48da310 --- /dev/null +++ b/lib/varint.js @@ -0,0 +1,39 @@ +/** + * Implement `var_int` encoding/decoding. + * @module bitmessage/varint + */ + +"use strict"; + +// TODO(Kagami): Since `node-int64` and `int64-native` APIs are slightly +// differ, there might be need in platform-dependent wrapper. Also think +// that to do with 64bit arithmetic since `node-int64` doesn't implement +// it. +var Int64 = require("int64-native"); +var assert = require("./utils").assert; + +exports.decode = function(buf) { + assert(buf.length > 0, "Empty buffer"); + var value, length; + switch (buf[0]) { + case 253: + value = buf.readUInt16BE(1); + length = 3; + break; + case 254: + value = buf.readUInt32BE(1); + length = 5; + break; + case 255: + var hi = buf.readUInt32BE(1); + var lo = buf.readUInt32BE(5); + value = new Int64(hi, lo); + length = 9; + break; + default: + value = buf[0]; + length = 1; + } + var rest = buf.slice(length); + return {value: value, length: length, rest: rest}; +}; diff --git a/package.json b/package.json index 122dbac..5e0ae99 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "JavaScript Bitmessage library", "main": "./lib/index.js", "browser": { + "int64-native": "node-int64", "./lib/crypto.js": "./lib/crypto.browser.js" }, "scripts": { @@ -38,6 +39,11 @@ "mocha": "*" }, "dependencies": { - "es6-promise": "^2.0.1" + "bs58": "^2.0.0", + "buffer-equal": "~0.0.1", + "es6-promise": "^2.0.1", + "int64-native": "^0.3.2", + "node-int64": "^0.3.2", + "ripemd160": "^0.2.0" } } diff --git a/test.js b/test.js index 2683bf3..8ac6dac 100644 --- a/test.js +++ b/test.js @@ -1,11 +1,68 @@ var expect = require("chai").expect; +var bufferEqual = require("buffer-equal"); +var bitmessage = require("./lib"); +var Address = bitmessage.Address; +var varint = require("./lib/varint"); var bmcrypto = require("./lib/crypto"); -describe("Bitmessage crypto", function() { - it("should calculate sha512 hash", function() { - return bmcrypto.sha512(new Buffer("test")).then(function(res) { +describe("var_int", function() { + it("should decode", function() { + var res; + expect(varint.decode.bind(Buffer([]))).to.throw(Error); + + res = varint.decode(Buffer([123])); + expect(res.value).to.equal(123); + expect(res.length).to.equal(1); + expect(bufferEqual(res.rest, Buffer([]))).to.be.true; + + res = varint.decode(Buffer("fd123456", "hex")); + expect(res.value).to.equal(0x1234); + expect(res.length).to.equal(3); + expect(bufferEqual(res.rest, Buffer("56", "hex"))).to.be.true; + + res = varint.decode(Buffer("fe1234567890", "hex")); + expect(res.value).to.equal(0x12345678); + expect(res.length).to.equal(5); + expect(bufferEqual(res.rest, Buffer("90", "hex"))).to.be.true; + + res = varint.decode(Buffer("ff0000001234567890", "hex")); + expect(res.value == 0x1234567890).to.be.true; + expect(res.length).to.equal(9); + expect(res.rest.length).to.equal(0); + }); +}); + +describe("Crypto", function() { + it("should implement SHA-512 hash", function() { + return bmcrypto.sha512(Buffer("test")).then(function(res) { expect(res.toString("hex")).to.equal("ee26b0dd4af7e749aa1a8ee3c10ae9923f618980772e473f8819a5d4940e0db27ac185f8a0e1d5f84f88bc887fd67b143732c304cc5fa9ad8e6f57f50028a8ff"); }); }); + + it("should implement RIPEMD-160 hash", function() { + return bmcrypto.ripemd160(Buffer("test")).then(function(res) { + expect(res.toString("hex")).to.equal("5e52fee47e6b070565f74372468cdc699de89107"); + }); + }); +}); + +describe("Address", function() { + it("should decode Bitmessage address", function() { + return Address.decode("BM-2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z") + .then(function(addr) { + expect(addr.version).to.equal(4); + expect(addr.stream).to.equal(1); + expect(bufferEqual(addr.ripe, Buffer("003ab6655de4bd8c603eba9b00dd5970725fdd56", "hex"))).to.be.true; + }); + }); + + it("should decode Bitmessage address badly formatted", function() { + return Address.decode(" 2cTux3PGRqHTEH6wyUP2sWeT4LrsGgy63z ") + .then(function(addr) { + expect(addr.version).to.equal(4); + expect(addr.stream).to.equal(1); + expect(bufferEqual(addr.ripe, Buffer("003ab6655de4bd8c603eba9b00dd5970725fdd56", "hex"))).to.be.true; + }); + }); });