diff --git a/lib/base32.js b/lib/base32.js index 8b002fa..362b21d 100644 --- a/lib/base32.js +++ b/lib/base32.js @@ -1,11 +1,14 @@ -'use strict'; +"use strict"; -var BigInteger = require('bigi'); +function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e) { throw _e; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e2) { didErr = true; err = _e2; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } -var ALPHABET = 'abcdefghjkmnprstuvwxyz0123456789'; // pre-compute lookup table +function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } -var SEPARATOR = ':'; -var CSLEN = 8; +function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } + +var JSBI = require('jsbi'); + +var ALPHABET = 'ABCDEFGHJKMNPRSTUVWXYZ0123456789'; var ALPHABET_MAP = {}; for (var z = 0; z < ALPHABET.length; z++) { @@ -16,195 +19,113 @@ for (var z = 0; z < ALPHABET.length; z++) { } ALPHABET_MAP[x] = z; -} - -function polymodStep(pre) { - var b = pre.shiftRight(35); - var mask = BigInteger.fromHex('07ffffffff'); - var v = pre.and(mask).shiftLeft(new BigInteger('5')); - - if (b.and(new BigInteger('1')).intValue() > 0) { - v = v.xor(BigInteger.fromHex('98f2bc8e61')); - } - - if (b.and(new BigInteger('2')).intValue()) { - v = v.xor(BigInteger.fromHex('79b76d99e2')); - } - - if (b.and(new BigInteger('4')).intValue()) { - v = v.xor(BigInteger.fromHex('f33e5fb3c4')); - } - - if (b.and(new BigInteger('8')).intValue()) { - v = v.xor(BigInteger.fromHex('ae2eabe2a8')); - } - - if (b.and(new BigInteger('16')).intValue()) { - v = v.xor(BigInteger.fromHex('1e4f43e470')); - } - - return v; -} - -function prefixChk(prefix) { - var chk = new BigInteger('1'); - - for (var i = 0; i < prefix.length; ++i) { - var c = prefix.charCodeAt(i); - var mixwith = new BigInteger('' + (c & 0x1f)); - chk = polymodStep(chk).xor(mixwith); - } - - chk = polymodStep(chk); - return chk; -} - -function encode(prefix, words) { - // too long? - if (prefix.length + CSLEN + 1 + words.length > 90) { - throw new TypeError('Exceeds Base32 maximum length'); - } - - prefix = prefix.toLowerCase(); // determine chk mod +} // pre defined BigInt could faster about 40 percent + + +var BIGINT_0 = JSBI.BigInt(0); +var BIGINT_1 = JSBI.BigInt(1); +var BIGINT_5 = JSBI.BigInt(5); +var BIGINT_35 = JSBI.BigInt(35); +var BIGINT_0B00001 = JSBI.BigInt(1); +var BIGINT_0B00010 = JSBI.BigInt(2); +var BIGINT_0B00100 = JSBI.BigInt(4); +var BIGINT_0B01000 = JSBI.BigInt(8); +var BIGINT_0B10000 = JSBI.BigInt(16); +var BIGINT_0X07FFFFFFFF = JSBI.BigInt(0x07ffffffff); +var BIGINT_0X98F2BC8E61 = JSBI.BigInt(0x98f2bc8e61); +var BIGINT_0X79B76D99E2 = JSBI.BigInt(0x79b76d99e2); +var BIGINT_0XF33E5FB3C4 = JSBI.BigInt(0xf33e5fb3c4); +var BIGINT_0XAE2EABE2A8 = JSBI.BigInt(0xae2eabe2a8); +var BIGINT_0X1E4F43E470 = JSBI.BigInt(0x1e4f43e470); + +function convertBit(buffer, inBits, outBits, pad) { + var mask = (1 << outBits) - 1; + var array = []; + var bits = 0; + var value = 0; - var chk = prefixChk(prefix); - var result = prefix + SEPARATOR; + var _iterator = _createForOfIteratorHelper(buffer), + _step; - for (var i = 0; i < words.length; ++i) { - var _x = words[i]; + try { + for (_iterator.s(); !(_step = _iterator.n()).done;) { + var _byte = _step.value; + bits += inBits; + value = value << inBits | _byte; - if (_x >>> 5 !== 0) { - throw new Error('Non 5-bit word'); + while (bits >= outBits) { + bits -= outBits; + array.push(value >>> bits & mask); + } } - - chk = polymodStep(chk).xor(new BigInteger('' + _x)); - result += ALPHABET.charAt(_x); - } - - for (var _i = 0; _i < CSLEN; ++_i) { - chk = polymodStep(chk); + } catch (err) { + _iterator.e(err); + } finally { + _iterator.f(); } - chk = chk.xor(new BigInteger('1')); + value = value << outBits - bits & mask; - for (var _i2 = 0; _i2 < CSLEN; ++_i2) { - var pos = 5 * (CSLEN - 1 - _i2); - var v2 = chk.shiftRight(new BigInteger('' + pos)).and(BigInteger.fromHex('1f')); - result += ALPHABET.charAt(v2.toString(10)); + if (bits && pad) { + array.push(value); + } else if (value && !pad) { + throw new Error('Excess padding'); + } else if (bits >= inBits && !pad) { + throw new Error('Non-zero padding'); } - return result; + return array; } -function decode(str) { - if (str.length < 8) { - throw new TypeError(str + ' too short'); - } +function polyMod(buffer) { + var checksumBigInt = BIGINT_1; - if (str.length > 90) { - throw new TypeError(str + ' too long'); - } // don't allow mixed case + var _iterator2 = _createForOfIteratorHelper(buffer), + _step2; + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _byte2 = _step2.value; + // c0 = c >> 35; + var high = JSBI.signedRightShift(checksumBigInt, BIGINT_35); // XXX: checksumBigInt must be positive, signedRightShift is ok + // c = ((c & 0x07ffffffff) << 5) ^ d; - var lowered = str.toLowerCase(); - var uppered = str.toUpperCase(); + checksumBigInt = JSBI.bitwiseAnd(checksumBigInt, BIGINT_0X07FFFFFFFF); + checksumBigInt = JSBI.leftShift(checksumBigInt, BIGINT_5); + checksumBigInt = _byte2 ? JSBI.bitwiseXor(checksumBigInt, JSBI.BigInt(_byte2)) : checksumBigInt; // bit ^ 0 = bit - if (str !== lowered && str !== uppered) { - throw new Error('Mixed-case string ' + str); - } + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00001), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X98F2BC8E61); + } - str = lowered; - var split = str.lastIndexOf(SEPARATOR); + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00010), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X79B76D99E2); + } - if (split === -1) { - throw new Error('No separator character for ' + str); - } + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00100), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0XF33E5FB3C4); + } - if (split === 0) { - throw new Error('Missing prefix for ' + str); - } - - var prefix = str.slice(0, split); - var wordChars = str.slice(split + 1); - - if (wordChars.length < 6) { - throw new Error('Data too short'); - } - - var chk = prefixChk(prefix); - var words = []; - - for (var i = 0; i < wordChars.length; ++i) { - var c = wordChars.charAt(i); - var v = ALPHABET_MAP[c]; - - if (v === undefined) { - throw new Error('Unknown character ' + c); - } + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B01000), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0XAE2EABE2A8); + } - chk = polymodStep(chk).xor(new BigInteger('' + v)); // not in the checksum? - - if (i + CSLEN >= wordChars.length) { - continue; + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B10000), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X1E4F43E470); + } } - - words.push(v); - } - - if (chk.toString(10) !== '1') { - throw new Error('Invalid checksum for ' + str); + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); } - return { - prefix: prefix, - words: words - }; -} - -function convert(data, inBits, outBits, pad) { - var value = 0; - var bits = 0; - var maxV = (1 << outBits) - 1; - var result = []; - - for (var i = 0; i < data.length; ++i) { - value = value << inBits | data[i]; - bits += inBits; - - while (bits >= outBits) { - bits -= outBits; - result.push(value >>> bits & maxV); - } - } - - if (pad) { - if (bits > 0) { - result.push(value << outBits - bits & maxV); - } - } else { - if (bits >= inBits) { - throw new Error('Excess padding'); - } - - if (value << outBits - bits & maxV) { - throw new Error('Non-zero padding'); - } - } - - return result; -} - -function toWords(bytes) { - return convert(bytes, 8, 5, true); -} - -function fromWords(words) { - return convert(words, 5, 8, false); + return JSBI.bitwiseXor(checksumBigInt, BIGINT_1); } module.exports = { - decode: decode, - encode: encode, - toWords: toWords, - fromWords: fromWords + convertBit: convertBit, + polyMod: polyMod, + ALPHABET: ALPHABET, + ALPHABET_MAP: ALPHABET_MAP }; \ No newline at end of file diff --git a/lib/index.js b/lib/index.js index 23da09d..1d9b910 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,5 +1,6 @@ -'use strict'; -/** global: Buffer */ +"use strict"; + +function _toArray(arr) { return _arrayWithHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableRest(); } function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest(); } @@ -9,17 +10,29 @@ function _iterableToArrayLimit(arr, i) { if (typeof Symbol === "undefined" || !( function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; } +function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread(); } + +function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } + +function _iterableToArray(iter) { if (typeof Symbol !== "undefined" && Symbol.iterator in Object(iter)) return Array.from(iter); } + +function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) return _arrayLikeToArray(arr); } + function _createForOfIteratorHelper(o, allowArrayLike) { var it; if (typeof Symbol === "undefined" || o[Symbol.iterator] == null) { if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") { if (it) o = it; var i = 0; var F = function F() {}; return { s: F, n: function n() { if (i >= o.length) return { done: true }; return { done: false, value: o[i++] }; }, e: function e(_e2) { throw _e2; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var normalCompletion = true, didErr = false, err; return { s: function s() { it = o[Symbol.iterator](); }, n: function n() { var step = it.next(); normalCompletion = step.done; return step; }, e: function e(_e3) { didErr = true; err = _e3; }, f: function f() { try { if (!normalCompletion && it["return"] != null) it["return"](); } finally { if (didErr) throw err; } } }; } function _unsupportedIterableToArray(o, minLen) { if (!o) return; if (typeof o === "string") return _arrayLikeToArray(o, minLen); var n = Object.prototype.toString.call(o).slice(8, -1); if (n === "Object" && o.constructor) n = o.constructor.name; if (n === "Map" || n === "Set") return Array.from(o); if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen); } function _arrayLikeToArray(arr, len) { if (len == null || len > arr.length) len = arr.length; for (var i = 0, arr2 = new Array(len); i < len; i++) { arr2[i] = arr[i]; } return arr2; } +var _require = require('./base32'), + ALPHABET = _require.ALPHABET, + ALPHABET_MAP = _require.ALPHABET_MAP, + polyMod = _require.polyMod, + convertBit = _require.convertBit; + var VERSION_BYTE = 0; var NET_ID_LIMIT = 0xFFFFFFFF; -var base32 = require('./base32'); - function encodeNetId(netId) { if (!Number.isInteger(netId)) { throw new Error('netId should be passed as an integer'); @@ -71,18 +84,6 @@ function decodeNetId(payload) { } } -function encodePayload(hexAddress) { - return Buffer.concat([Buffer.from([VERSION_BYTE]), hexAddress]); -} - -function decodePayload(payload) { - if (payload[0] !== VERSION_BYTE) { - throw new Error('Can not recognize version byte'); - } - - return Buffer.from(payload.slice(1)); -} - function getAddressType(hexAddress) { if (hexAddress.length < 1) { throw new Error('Empty payload in address'); @@ -131,19 +132,22 @@ function encode(hexAddress, netId) { throw new Error('hexAddress should be at least 20 bytes'); } - var addressType = getAddressType(hexAddress); - var encodedAddress = base32.encode(encodeNetId(netId), base32.toWords(encodePayload(hexAddress))); - - if (verbose) { - var _encodedAddress$split = encodedAddress.split(':'), - _encodedAddress$split2 = _slicedToArray(_encodedAddress$split, 2), - prefix = _encodedAddress$split2[0], - payload = _encodedAddress$split2[1]; - - encodedAddress = [prefix, "type.".concat(addressType), payload].join(':').toUpperCase(); - } - - return encodedAddress; + var addressType = getAddressType(hexAddress).toUpperCase(); + var netName = encodeNetId(netId).toUpperCase(); + var netName5Bits = Buffer.from(netName).map(function (_byte) { + return _byte & 31; + }); + var payload5Bits = convertBit([VERSION_BYTE].concat(_toConsumableArray(hexAddress)), 8, 5, true); + var checksumBigInt = polyMod([].concat(_toConsumableArray(netName5Bits), [0], _toConsumableArray(payload5Bits), [0, 0, 0, 0, 0, 0, 0, 0])); + var checksumBytes = Buffer.from(checksumBigInt.toString(16).padStart(10, '0'), 'hex'); + var checksum5Bits = convertBit(checksumBytes, 8, 5, true); + var payload = payload5Bits.map(function (_byte2) { + return ALPHABET[_byte2]; + }).join(''); + var checksum = checksum5Bits.map(function (_byte3) { + return ALPHABET[_byte3]; + }).join(''); + return verbose ? "".concat(netName, ":TYPE.").concat(addressType, ":").concat(payload).concat(checksum) : "".concat(netName, ":").concat(payload).concat(checksum).toLowerCase(); } function decode(address) { @@ -155,36 +159,79 @@ function decode(address) { throw new Error('Mixed-case address ' + address); } - var splits = address.split(':'); - var shouldHaveType = ''; - var reducedAddress = address; + var _address$toUpperCase$ = address.toUpperCase().match(/^([^:]+):(.+:)?(.{34})(.{8})$/), + _address$toUpperCase$2 = _slicedToArray(_address$toUpperCase$, 5), + netName = _address$toUpperCase$2[1], + shouldHaveType = _address$toUpperCase$2[2], + payload = _address$toUpperCase$2[3], + checksum = _address$toUpperCase$2[4]; + + var prefix5Bits = Buffer.from(netName).map(function (_byte4) { + return _byte4 & 31; + }); + var payload5Bits = []; + + var _iterator2 = _createForOfIteratorHelper(payload), + _step2; + + try { + for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) { + var _char = _step2.value; + payload5Bits.push(ALPHABET_MAP[_char]); + } + } catch (err) { + _iterator2.e(err); + } finally { + _iterator2.f(); + } + + var checksum5Bits = []; + + var _iterator3 = _createForOfIteratorHelper(checksum), + _step3; - if (splits.length === 3) { - shouldHaveType = splits[1]; - reducedAddress = [splits[0], splits[2]].join(':'); + try { + for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) { + var _char2 = _step3.value; + checksum5Bits.push(ALPHABET_MAP[_char2]); + } + } catch (err) { + _iterator3.e(err); + } finally { + _iterator3.f(); } - var result = base32.decode(reducedAddress); - var data = base32.fromWords(result.words); + var _convertBit = convertBit(payload5Bits, 5, 8), + _convertBit2 = _toArray(_convertBit), + version = _convertBit2[0], + addressBytes = _convertBit2.slice(1); - if (data.length < 1) { - throw new Error('Empty payload in address'); + if (version !== VERSION_BYTE) { + throw new Error('Can not recognize version byte'); } - var returnValue = { - hexAddress: decodePayload(data), - netId: decodeNetId(result.prefix), - type: getAddressType(decodePayload(data)) - }; + var hexAddress = Buffer.from(addressBytes); + var netId = decodeNetId(netName.toLowerCase()); + var type = getAddressType(hexAddress); - if (shouldHaveType !== '' && "type.".concat(returnValue.type) !== shouldHaveType.toLowerCase()) { + if (shouldHaveType && "type.".concat(type, ":") !== shouldHaveType.toLowerCase()) { throw new Error('Type of address doesn\'t match'); } - return returnValue; + var bigInt = polyMod([].concat(_toConsumableArray(prefix5Bits), [0], payload5Bits, checksum5Bits)); + + if (Number(bigInt)) { + throw new Error("Invalid checksum for ".concat(address)); + } + + return { + hexAddress: hexAddress, + netId: netId, + type: type + }; } module.exports = { - decode: decode, - encode: encode + encode: encode, + decode: decode }; \ No newline at end of file diff --git a/package.json b/package.json index 6d885af..58a7da4 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "ypliu ", "license": "MIT", "dependencies": { - "bigi": "^1.4.2" + "jsbi": "^3.1.4" }, "devDependencies": { "@babel/cli": "^7.12.10", diff --git a/src/base32.js b/src/base32.js index b4e4856..cd85c05 100644 --- a/src/base32.js +++ b/src/base32.js @@ -1,10 +1,6 @@ -'use strict' -const BigInteger = require('bigi') -const ALPHABET = 'abcdefghjkmnprstuvwxyz0123456789' +const JSBI = require('jsbi') +const ALPHABET = 'ABCDEFGHJKMNPRSTUVWXYZ0123456789' -// pre-compute lookup table -const SEPARATOR = ':' -const CSLEN = 8 const ALPHABET_MAP = {} for (let z = 0; z < ALPHABET.length; z++) { const x = ALPHABET.charAt(z) @@ -14,172 +10,85 @@ for (let z = 0; z < ALPHABET.length; z++) { ALPHABET_MAP[x] = z } -function polymodStep (pre) { - const b = pre.shiftRight(35) - const mask = BigInteger.fromHex('07ffffffff') +// pre defined BigInt could faster about 40 percent +const BIGINT_0 = JSBI.BigInt(0) +const BIGINT_1 = JSBI.BigInt(1) +const BIGINT_5 = JSBI.BigInt(5) +const BIGINT_35 = JSBI.BigInt(35) +const BIGINT_0B00001 = JSBI.BigInt(0b00001) +const BIGINT_0B00010 = JSBI.BigInt(0b00010) +const BIGINT_0B00100 = JSBI.BigInt(0b00100) +const BIGINT_0B01000 = JSBI.BigInt(0b01000) +const BIGINT_0B10000 = JSBI.BigInt(0b10000) +const BIGINT_0X07FFFFFFFF = JSBI.BigInt(0x07ffffffff) +const BIGINT_0X98F2BC8E61 = JSBI.BigInt(0x98f2bc8e61) +const BIGINT_0X79B76D99E2 = JSBI.BigInt(0x79b76d99e2) +const BIGINT_0XF33E5FB3C4 = JSBI.BigInt(0xf33e5fb3c4) +const BIGINT_0XAE2EABE2A8 = JSBI.BigInt(0xae2eabe2a8) +const BIGINT_0X1E4F43E470 = JSBI.BigInt(0x1e4f43e470) + +function convertBit (buffer, inBits, outBits, pad) { + const mask = (1 << outBits) - 1 + const array = [] - let v = pre.and(mask).shiftLeft(new BigInteger('5')) - - if (b.and(new BigInteger('1')).intValue() > 0) { - v = v.xor(BigInteger.fromHex('98f2bc8e61')) - } - if (b.and(new BigInteger('2')).intValue()) { - v = v.xor(BigInteger.fromHex('79b76d99e2')) - } - if (b.and(new BigInteger('4')).intValue()) { - v = v.xor(BigInteger.fromHex('f33e5fb3c4')) - } - if (b.and(new BigInteger('8')).intValue()) { - v = v.xor(BigInteger.fromHex('ae2eabe2a8')) - } - if (b.and(new BigInteger('16')).intValue()) { - v = v.xor(BigInteger.fromHex('1e4f43e470')) - } - - return v -} - -function prefixChk (prefix) { - let chk = new BigInteger('1') - for (let i = 0; i < prefix.length; ++i) { - const c = prefix.charCodeAt(i) - - const mixwith = new BigInteger('' + (c & 0x1f)) - chk = polymodStep(chk).xor(mixwith) - } - - chk = polymodStep(chk) - return chk -} - -function encode (prefix, words) { - // too long? - if ((prefix.length + CSLEN + 1 + words.length) > 90) { - throw new TypeError('Exceeds Base32 maximum length') - } - - prefix = prefix.toLowerCase() + let bits = 0 + let value = 0 + for (const byte of buffer) { + bits += inBits + value = (value << inBits) | byte - // determine chk mod - let chk = prefixChk(prefix) - let result = prefix + SEPARATOR - for (let i = 0; i < words.length; ++i) { - const x = words[i] - if ((x >>> 5) !== 0) { - throw new Error('Non 5-bit word') + while (bits >= outBits) { + bits -= outBits + array.push((value >>> bits) & mask) } - - chk = polymodStep(chk).xor(new BigInteger('' + x)) - result += ALPHABET.charAt(x) } + value = (value << (outBits - bits)) & mask - for (let i = 0; i < CSLEN; ++i) { - chk = polymodStep(chk) - } - chk = chk.xor(new BigInteger('1')) - for (let i = 0; i < CSLEN; ++i) { - const pos = 5 * (CSLEN - 1 - i) - const v2 = chk.shiftRight(new BigInteger('' + pos)).and(BigInteger.fromHex('1f')) - result += ALPHABET.charAt(v2.toString(10)) + if (bits && pad) { + array.push(value) + } else if (value && !pad) { + throw new Error('Excess padding') + } else if (bits >= inBits && !pad) { + throw new Error('Non-zero padding') } - return result + return array } -function decode (str) { - if (str.length < 8) { - throw new TypeError(str + ' too short') - } - if (str.length > 90) { - throw new TypeError(str + ' too long') - } - - // don't allow mixed case - const lowered = str.toLowerCase() - const uppered = str.toUpperCase() - if (str !== lowered && str !== uppered) { - throw new Error('Mixed-case string ' + str) - } +function polyMod (buffer) { + let checksumBigInt = BIGINT_1 + for (const byte of buffer) { + // c0 = c >> 35; + const high = JSBI.signedRightShift(checksumBigInt, BIGINT_35) // XXX: checksumBigInt must be positive, signedRightShift is ok - str = lowered + // c = ((c & 0x07ffffffff) << 5) ^ d; + checksumBigInt = JSBI.bitwiseAnd(checksumBigInt, BIGINT_0X07FFFFFFFF) + checksumBigInt = JSBI.leftShift(checksumBigInt, BIGINT_5) + checksumBigInt = byte ? JSBI.bitwiseXor(checksumBigInt, JSBI.BigInt(byte)) : checksumBigInt // bit ^ 0 = bit - const split = str.lastIndexOf(SEPARATOR) - if (split === -1) { - throw new Error('No separator character for ' + str) - } - - if (split === 0) { - throw new Error('Missing prefix for ' + str) - } - - const prefix = str.slice(0, split) - const wordChars = str.slice(split + 1) - if (wordChars.length < 6) { - throw new Error('Data too short') - } - - let chk = prefixChk(prefix) - const words = [] - for (let i = 0; i < wordChars.length; ++i) { - const c = wordChars.charAt(i) - const v = ALPHABET_MAP[c] - if (v === undefined) { - throw new Error('Unknown character ' + c) + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00001), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X98F2BC8E61) } - - chk = polymodStep(chk).xor(new BigInteger('' + v)) - // not in the checksum? - if (i + CSLEN >= wordChars.length) { - continue - } - words.push(v) - } - - if (chk.toString(10) !== '1') { - throw new Error('Invalid checksum for ' + str) - } - - return { prefix, words } -} - -function convert (data, inBits, outBits, pad) { - let value = 0 - let bits = 0 - const maxV = (1 << outBits) - 1 - - const result = [] - for (let i = 0; i < data.length; ++i) { - value = (value << inBits) | data[i] - bits += inBits - - while (bits >= outBits) { - bits -= outBits - result.push((value >>> bits) & maxV) + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00010), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X79B76D99E2) } - } - - if (pad) { - if (bits > 0) { - result.push((value << (outBits - bits)) & maxV) + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B00100), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0XF33E5FB3C4) } - } else { - if (bits >= inBits) { - throw new Error('Excess padding') + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B01000), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0XAE2EABE2A8) } - if ((value << (outBits - bits)) & maxV) { - throw new Error('Non-zero padding') + if (JSBI.notEqual(JSBI.bitwiseAnd(high, BIGINT_0B10000), BIGINT_0)) { + checksumBigInt = JSBI.bitwiseXor(checksumBigInt, BIGINT_0X1E4F43E470) } } - return result + return JSBI.bitwiseXor(checksumBigInt, BIGINT_1) } -function toWords (bytes) { - return convert(bytes, 8, 5, true) +module.exports = { + convertBit, + polyMod, + ALPHABET, + ALPHABET_MAP } - -function fromWords (words) { - return convert(words, 5, 8, false) -} - -module.exports = { decode, encode, toWords, fromWords } diff --git a/src/index.js b/src/index.js index aa18eff..2609b59 100644 --- a/src/index.js +++ b/src/index.js @@ -1,11 +1,13 @@ -'use strict' -/** global: Buffer */ +const { + ALPHABET, + ALPHABET_MAP, + polyMod, + convertBit +} = require('./base32') const VERSION_BYTE = 0 const NET_ID_LIMIT = 0xFFFFFFFF -const base32 = require('./base32') - function encodeNetId (netId) { if (!Number.isInteger(netId)) { throw new Error('netId should be passed as an integer') @@ -48,17 +50,6 @@ function decodeNetId (payload) { } } -function encodePayload (hexAddress) { - return Buffer.concat([Buffer.from([VERSION_BYTE]), hexAddress]) -} - -function decodePayload (payload) { - if (payload[0] !== VERSION_BYTE) { - throw new Error('Can not recognize version byte') - } - return Buffer.from(payload.slice(1)) -} - function getAddressType (hexAddress) { if (hexAddress.length < 1) { throw new Error('Empty payload in address') @@ -90,18 +81,22 @@ function encode (hexAddress, netId, verbose = false) { throw new Error('hexAddress should be at least 20 bytes') } - const addressType = getAddressType(hexAddress) + const addressType = getAddressType(hexAddress).toUpperCase() + const netName = encodeNetId(netId).toUpperCase() - let encodedAddress = base32.encode( - encodeNetId(netId), - base32.toWords(encodePayload(hexAddress)) - ) + const netName5Bits = Buffer.from(netName).map(byte => byte & 0b11111) + const payload5Bits = convertBit([VERSION_BYTE, ...hexAddress], 8, 5, true) - if (verbose) { - const [prefix, payload] = encodedAddress.split(':') - encodedAddress = [prefix, `type.${addressType}`, payload].join(':').toUpperCase() - } - return encodedAddress + const checksumBigInt = polyMod([...netName5Bits, 0, ...payload5Bits, 0, 0, 0, 0, 0, 0, 0, 0]) + const checksumBytes = Buffer.from(checksumBigInt.toString(16).padStart(10, '0'), 'hex') + const checksum5Bits = convertBit(checksumBytes, 8, 5, true) + + const payload = payload5Bits.map(byte => ALPHABET[byte]).join('') + const checksum = checksum5Bits.map(byte => ALPHABET[byte]).join('') + + return verbose + ? `${netName}:TYPE.${addressType}:${payload}${checksum}` + : `${netName}:${payload}${checksum}`.toLowerCase() } function decode (address) { @@ -112,32 +107,37 @@ function decode (address) { throw new Error('Mixed-case address ' + address) } - const splits = address.split(':') - let shouldHaveType = '' + const [, netName, shouldHaveType, payload, checksum] = address.toUpperCase().match(/^([^:]+):(.+:)?(.{34})(.{8})$/) - let reducedAddress = address - if (splits.length === 3) { - shouldHaveType = splits[1] - reducedAddress = [splits[0], splits[2]].join(':') + const prefix5Bits = Buffer.from(netName).map(byte => byte & 0b11111) + const payload5Bits = [] + for (const char of payload) { + payload5Bits.push(ALPHABET_MAP[char]) } - - const result = base32.decode(reducedAddress) - const data = base32.fromWords(result.words) - if (data.length < 1) { - throw new Error('Empty payload in address') + const checksum5Bits = [] + for (const char of checksum) { + checksum5Bits.push(ALPHABET_MAP[char]) } - const returnValue = { - hexAddress: decodePayload(data), - netId: decodeNetId(result.prefix), - type: getAddressType(decodePayload(data)) + const [version, ...addressBytes] = convertBit(payload5Bits, 5, 8) + if (version !== VERSION_BYTE) { + throw new Error('Can not recognize version byte') } - if (shouldHaveType !== '' && `type.${returnValue.type}` !== shouldHaveType.toLowerCase()) { + const hexAddress = Buffer.from(addressBytes) + const netId = decodeNetId(netName.toLowerCase()) + const type = getAddressType(hexAddress) + + if (shouldHaveType && `type.${type}:` !== shouldHaveType.toLowerCase()) { throw new Error('Type of address doesn\'t match') } - return returnValue + const bigInt = polyMod([...prefix5Bits, 0, ...payload5Bits, ...checksum5Bits]) + if (Number(bigInt)) { + throw new Error(`Invalid checksum for ${address}`) + } + + return { hexAddress, netId, type } } -module.exports = { decode: decode, encode: encode } +module.exports = { encode, decode } diff --git a/yarn.lock b/yarn.lock index 5d0af0d..9919db5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1475,11 +1475,6 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" -bigi@^1.4.2: - version "1.4.2" - resolved "https://registry.npmjs.org/bigi/-/bigi-1.4.2.tgz#9c665a95f88b8b08fc05cfd731f561859d725825" - integrity sha1-nGZalfiLiwj8Bc/XMfVhhZ1yWCU= - binary-extensions@^1.0.0: version "1.13.1" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" @@ -3432,6 +3427,11 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +jsbi@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/jsbi/-/jsbi-3.1.4.tgz#9654dd02207a66a4911b4e4bb74265bc2cbc9dd0" + integrity sha512-52QRRFSsi9impURE8ZUbzAMCLjPm4THO7H2fcuIvaaeFTbSysvkodbQQXIVsNgq/ypDbq6dJiuGKL0vZ/i9hUg== + jsbn@~0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"