UNPKG

bstring

Version:

String encodings for javascript

546 lines (424 loc) 12.1 kB
/*! * cashaddr.js - cashaddr for bcash * Copyright (c) 2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin * * Implementation of CashAddr * https://github.com/bitcoincashorg/spec/blob/master/cashaddr.md * * Parts of this software are based on "bitcoin-abc". * https://github.com/Bitcoin-ABC/bitcoin-abc * * Parts of this software are based on "bech32". * https://github.com/sipa/bech32 * * Copyright (c) 2017 Pieter Wuille * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ 'use strict'; const assert = require('bsert'); /** * U64 */ class U64 { constructor(hi, lo) { this.hi = hi | 0; this.lo = lo | 0; } ushrn32(bits) { bits &= 63; let lo = this.lo; if (bits === 0) return lo; if (bits < 32) { lo >>>= bits; lo |= this.hi << (32 - bits); } else { lo = this.hi >>> (bits - 32); } return lo; } } /** * Constants */ const POOL105 = Buffer.allocUnsafe(105); const CHARSET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; const TABLE = [ -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 ]; const CHECKSUM_MASK = new U64(0x00000007, 0xffffffff); const GENERATOR = [ new U64(0x00000098, 0xf2bc8e61), new U64(0x00000079, 0xb76d99e2), new U64(0x000000f3, 0x3e5fb3c4), new U64(0x000000ae, 0x2eabe2a8), new U64(0x0000001e, 0x4f43e470) ]; /** * Update checksum * @ignore * @param {U64} chk * @param {Number} x * @returns {U64} -- new checksum */ function polymod(pre, x) { const c = pre; // b = c >> 35 const b = c.hi >>> 3; // c = (c & CHECKSUM_MASK) << 5 c.hi &= CHECKSUM_MASK.hi; c.lo &= CHECKSUM_MASK.lo; c.hi <<= 5; c.hi |= c.lo >>> 27; c.lo <<= 5; for (let i = 0; i < GENERATOR.length; i++) { if ((b >>> i) & 1) { // c ^= GENERATOR[i] c.hi ^= GENERATOR[i].hi; c.lo ^= GENERATOR[i].lo; } } // c ^= x c.lo ^= x; return c; } /** * Serialize data to cashaddr. * @param {String} prefix * @param {Buffer} data - 5bit serialized * @returns {String} */ function serialize(prefix, data) { assert(typeof prefix === 'string'); assert(Buffer.isBuffer(data)); const chk = new U64(0, 1); let str = ''; let upper = false; let lower = false; for (let i = 0; i < prefix.length; i++) { let ch = prefix.charCodeAt(i); if ((ch & 0xff00) || (ch >>> 5) === 0) throw new Error('Invalid cashaddr character.'); if (ch >= 0x61 && ch <= 0x7a) { lower = true; } else if (ch >= 0x41 && ch <= 0x5a) { upper = true; ch = (ch - 0x41) + 0x61; } else if (ch >= 0x30 && ch <= 0x39) { throw new Error('Invalid cashaddr prefix.'); } polymod(chk, ch & 0x1f); str += String.fromCharCode(ch); } if (lower && upper) throw new Error('Invalid cashaddr prefix.'); polymod(chk, 0); str += ':'; for (let i = 0; i < data.length; i++) { const ch = data[i]; if ((ch >>> 5) !== 0) throw new Error('Invalid cashaddr value.'); polymod(chk, ch); str += CHARSET[ch]; } for (let i = 0; i < 8; i++) polymod(chk, 0); chk.lo ^= 1; for (let i = 0; i < 8; i++) { const v = chk.ushrn32((7 - i) * 5) & 0x1f; str += CHARSET[v]; } return str; } /** * Decode CashAddr string. * @param {String} str * @param {String} defaultPrefix (lowercase and w/o numbers) * @returns {Array} [prefix, data] */ function deserialize(str, defaultPrefix) { assert(typeof str === 'string'); if (str.length < 8 || str.length > 196) // 83 + 1 + 112 throw new Error('Invalid cashaddr data length.'); let lower = false; let upper = false; let number = false; let plen = 0; // Process lower/upper, make sure we have prefix. for (let i = 0; i < str.length; i++) { const ch = str.charCodeAt(i); if (ch >= 0x61 && ch <= 0x7a) { lower = true; continue; } if (ch >= 0x41 && ch <= 0x5a) { upper = true; continue; } if (ch >= 0x30 && ch <= 0x39) { number = true; continue; } if (ch === 0x3a) { // : if (number || i === 0 || i > 83) throw new Error('Invalid cashaddr prefix.'); if (plen !== 0) throw new Error('Invalid cashaddr separators.'); plen = i; continue; } throw new Error('Invalid cashaddr character.'); } if (upper && lower) throw new Error('Invalid cashaddr casing.'); // Process checksum. const chk = new U64(0, 1); let prefix; if (plen === 0) { prefix = defaultPrefix.toLowerCase(); } else { prefix = str.substring(0, plen).toLowerCase(); plen += 1; } // Process prefix. for (let i = 0; i < prefix.length; i++) { const ch = prefix.charCodeAt(i); polymod(chk, (ch | 0x20) & 0x1f); } polymod(chk, 0); const dlen = str.length - plen; if (dlen <= 8 || dlen > 112) throw new Error('Invalid cashaddr data length.'); const data = Buffer.allocUnsafe(dlen); for (let i = plen; i < str.length; i++) { const ch = str.charCodeAt(i); const v = (ch & 0xff80) ? -1 : TABLE[ch]; if (v === -1) throw new Error('Invalid cashaddr character.'); polymod(chk, v); if (i + 8 < str.length) data[i - plen] = v; } const valid = chk.hi === 0 && chk.lo === 1 && prefix === defaultPrefix; if (!valid) throw new Error('Invalid cashaddr checksum.'); return [prefix, data.slice(0, -8)]; } /** * Convert serialized data to another base. * @param {Buffer} input * @param {Number} i * @param {Buffer} output * @param {Number} j * @param {Number} frombits * @param {Number} tobits * @param {Boolean} pad * @returns {Buffer} */ function convert(input, i, output, j, frombits, tobits, pad) { assert(Buffer.isBuffer(input)); assert((i >>> 0) === i); assert(Buffer.isBuffer(output)); assert((j >>> 0) === j); assert((frombits & 0xff) === frombits); assert((tobits & 0xff) === tobits); assert(typeof pad === 'boolean'); const maxv = (1 << tobits) - 1; let acc = 0; let bits = 0; for (; i < input.length; i++) { const value = input[i]; if ((value >>> frombits) !== 0) throw new Error('Invalid bits.'); acc = (acc << frombits) | value; bits += frombits; while (bits >= tobits) { bits -= tobits; output[j++] = (acc >>> bits) & maxv; } } if (pad) { if (bits) output[j++] = (acc << (tobits - bits)) & maxv; } else { if (bits >= frombits || ((acc << (tobits - bits)) & maxv)) throw new Error('Invalid bits.'); } assert(j <= output.length); return output.slice(0, j); } /** * Calculate size required for bit conversion. * @param {Number} len * @param {Number} frombits * @param {Number} tobits * @param {Boolean} pad * @returns {Number} */ function convertSize(len, frombits, tobits, pad) { assert((len >>> 0) === len); assert((frombits & 0xff) === frombits); assert((tobits & 0xff) === tobits); assert(typeof pad === 'boolean'); assert(tobits !== 0); let size = (len * frombits + (tobits - 1)) / tobits; size >>>= 0; if (pad) size += 1; return size; } /** * Convert serialized data to another base. * @param {Buffer} data * @param {Number} frombits * @param {Number} tobits * @param {Boolean} pad * @returns {Buffer} */ function convertBits(data, frombits, tobits, pad) { assert(Buffer.isBuffer(data)); assert((frombits & 0xff) === frombits); assert((tobits & 0xff) === tobits); assert(typeof pad === 'boolean'); const size = convertSize(data.length, frombits, tobits, pad); const out = Buffer.allocUnsafe(size); return convert(data, 0, out, 0, frombits, tobits, pad); } /** * Get cashaddr encoded size. * @param {Number} size * @returns {Number} */ function encodedSize(size) { assert((size >>> 0) === size); switch (size) { case 20: return 0; case 24: return 1; case 28: return 2; case 32: return 3; case 40: return 4; case 48: return 5; case 56: return 6; case 64: return 7; default: throw new Error('Non standard length.'); } } /** * Serialize data to cashaddr * @param {String} prefix * @param {Number} type - (0 = P2PKH, 1 = P2SH) * @param {Buffer} hash * @returns {String} */ function encode(prefix, type, hash) { assert(typeof prefix === 'string'); // There are 4 bits available for the version (2 ^ 4 = 16) assert((type & 0x0f) === type, 'Invalid cashaddr type.'); assert(Buffer.isBuffer(hash)); if (prefix.length === 0 || prefix.length > 83) throw new Error('Invalid cashaddr prefix.'); const size = encodedSize(hash.length); const data = Buffer.allocUnsafe(hash.length + 1); data[0] = (type << 3) | size; hash.copy(data, 1); const output = POOL105; const converted = convert(data, 0, output, 0, 8, 5, true); return serialize(prefix, converted); } /** * Deserialize data from CashAddr address. * @param {String} str * @param {String} defaultPrefix (lowercase and w/o numbers) * @returns {Object} */ function decode(str, defaultPrefix = 'bitcoincash') { assert(typeof str === 'string'); assert(typeof defaultPrefix === 'string'); const [prefix, data] = deserialize(str, defaultPrefix); const extrabits = (data.length * 5) & 7; if (extrabits >= 5) throw new Error('Invalid padding in data.'); const last = data[data.length - 1]; const mask = (1 << extrabits) - 1; if (last & mask) throw new Error('Non zero padding.'); const output = data; const converted = convert(data, 0, output, 0, 5, 8, false); const type = (converted[0] >>> 3) & 0x1f; const hash = converted.slice(1); let size = 20 + 4 * (converted[0] & 0x03); if (converted[0] & 0x04) size *= 2; if (size !== hash.length) throw new Error('Invalid cashaddr data length.'); return new AddrResult(prefix, type, hash); } /** * Test whether a string is a cashaddr string. * @param {String} str * @param {String} defaultPrefix (lowercase and w/o numbers) * @returns {Boolean} */ function test(str, defaultPrefix = 'bitcoincash') { try { decode(str, defaultPrefix); } catch (e) { return false; } return true; } /** * AddrResult * @private * @property {String} prefix * @property {Number} type (0 = P2PKH, 1 = P2SH) * @property {Buffer} hash */ class AddrResult { constructor(prefix, type, hash) { this.prefix = prefix; this.type = type; this.hash = hash; } } /* * Expose */ convertBits; exports.encode = encode; exports.decode = decode; exports.test = test;