UNPKG

@scure/base

Version:

Secure, audited & 0-dep implementation of base64, bech32, base58, base32 & base16

704 lines 24.4 kB
/*! scure-base - MIT License (c) 2022 Paul Miller (paulmillr.com) */ function isBytes(a) { return a instanceof Uint8Array || (ArrayBuffer.isView(a) && a.constructor.name === 'Uint8Array'); } /** Asserts something is Uint8Array. */ function abytes(b) { if (!isBytes(b)) throw new Error('Uint8Array expected'); } function isArrayOf(isString, arr) { if (!Array.isArray(arr)) return false; if (arr.length === 0) return true; if (isString) { return arr.every((item) => typeof item === 'string'); } else { return arr.every((item) => Number.isSafeInteger(item)); } } function afn(input) { if (typeof input !== 'function') throw new Error('function expected'); return true; } function astr(label, input) { if (typeof input !== 'string') throw new Error(`${label}: string expected`); return true; } function anumber(n) { if (!Number.isSafeInteger(n)) throw new Error(`invalid integer: ${n}`); } function aArr(input) { if (!Array.isArray(input)) throw new Error('array expected'); } function astrArr(label, input) { if (!isArrayOf(true, input)) throw new Error(`${label}: array of strings expected`); } function anumArr(label, input) { if (!isArrayOf(false, input)) throw new Error(`${label}: array of numbers expected`); } /** * @__NO_SIDE_EFFECTS__ */ function chain(...args) { const id = (a) => a; // Wrap call in closure so JIT can inline calls const wrap = (a, b) => (c) => a(b(c)); // Construct chain of args[-1].encode(args[-2].encode([...])) const encode = args.map((x) => x.encode).reduceRight(wrap, id); // Construct chain of args[0].decode(args[1].decode(...)) const decode = args.map((x) => x.decode).reduce(wrap, id); return { encode, decode }; } /** * Encodes integer radix representation to array of strings using alphabet and back. * Could also be array of strings. * @__NO_SIDE_EFFECTS__ */ function alphabet(letters) { // mapping 1 to "b" const lettersA = typeof letters === 'string' ? letters.split('') : letters; const len = lettersA.length; astrArr('alphabet', lettersA); // mapping "b" to 1 const indexes = new Map(lettersA.map((l, i) => [l, i])); return { encode: (digits) => { aArr(digits); return digits.map((i) => { if (!Number.isSafeInteger(i) || i < 0 || i >= len) throw new Error(`alphabet.encode: digit index outside alphabet "${i}". Allowed: ${letters}`); return lettersA[i]; }); }, decode: (input) => { aArr(input); return input.map((letter) => { astr('alphabet.decode', letter); const i = indexes.get(letter); if (i === undefined) throw new Error(`Unknown letter: "${letter}". Allowed: ${letters}`); return i; }); }, }; } /** * @__NO_SIDE_EFFECTS__ */ function join(separator = '') { astr('join', separator); return { encode: (from) => { astrArr('join.decode', from); return from.join(separator); }, decode: (to) => { astr('join.decode', to); return to.split(separator); }, }; } /** * Pad strings array so it has integer number of bits * @__NO_SIDE_EFFECTS__ */ function padding(bits, chr = '=') { anumber(bits); astr('padding', chr); return { encode(data) { astrArr('padding.encode', data); while ((data.length * bits) % 8) data.push(chr); return data; }, decode(input) { astrArr('padding.decode', input); let end = input.length; if ((end * bits) % 8) throw new Error('padding: invalid, string should have whole number of bytes'); for (; end > 0 && input[end - 1] === chr; end--) { const last = end - 1; const byte = last * bits; if (byte % 8 === 0) throw new Error('padding: invalid, string has too much padding'); } return input.slice(0, end); }, }; } /** * @__NO_SIDE_EFFECTS__ */ function normalize(fn) { afn(fn); return { encode: (from) => from, decode: (to) => fn(to) }; } /** * Slow: O(n^2) time complexity */ function convertRadix(data, from, to) { // base 1 is impossible if (from < 2) throw new Error(`convertRadix: invalid from=${from}, base cannot be less than 2`); if (to < 2) throw new Error(`convertRadix: invalid to=${to}, base cannot be less than 2`); aArr(data); if (!data.length) return []; let pos = 0; const res = []; const digits = Array.from(data, (d) => { anumber(d); if (d < 0 || d >= from) throw new Error(`invalid integer: ${d}`); return d; }); const dlen = digits.length; while (true) { let carry = 0; let done = true; for (let i = pos; i < dlen; i++) { const digit = digits[i]; const fromCarry = from * carry; const digitBase = fromCarry + digit; if (!Number.isSafeInteger(digitBase) || fromCarry / from !== carry || digitBase - digit !== fromCarry) { throw new Error('convertRadix: carry overflow'); } const div = digitBase / to; carry = digitBase % to; const rounded = Math.floor(div); digits[i] = rounded; if (!Number.isSafeInteger(rounded) || rounded * to + carry !== digitBase) throw new Error('convertRadix: carry overflow'); if (!done) continue; else if (!rounded) pos = i; else done = false; } res.push(carry); if (done) break; } for (let i = 0; i < data.length - 1 && data[i] === 0; i++) res.push(0); return res.reverse(); } const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b)); const radix2carry = /* @__NO_SIDE_EFFECTS__ */ (from, to) => from + (to - gcd(from, to)); const powers = /* @__PURE__ */ (() => { let res = []; for (let i = 0; i < 40; i++) res.push(2 ** i); return res; })(); /** * Implemented with numbers, because BigInt is 5x slower */ function convertRadix2(data, from, to, padding) { aArr(data); if (from <= 0 || from > 32) throw new Error(`convertRadix2: wrong from=${from}`); if (to <= 0 || to > 32) throw new Error(`convertRadix2: wrong to=${to}`); if (radix2carry(from, to) > 32) { throw new Error(`convertRadix2: carry overflow from=${from} to=${to} carryBits=${radix2carry(from, to)}`); } let carry = 0; let pos = 0; // bitwise position in current element const max = powers[from]; const mask = powers[to] - 1; const res = []; for (const n of data) { anumber(n); if (n >= max) throw new Error(`convertRadix2: invalid data word=${n} from=${from}`); carry = (carry << from) | n; if (pos + from > 32) throw new Error(`convertRadix2: carry overflow pos=${pos} from=${from}`); pos += from; for (; pos >= to; pos -= to) res.push(((carry >> (pos - to)) & mask) >>> 0); const pow = powers[pos]; if (pow === undefined) throw new Error('invalid carry'); carry &= pow - 1; // clean carry, otherwise it will cause overflow } carry = (carry << (to - pos)) & mask; if (!padding && pos >= from) throw new Error('Excess padding'); if (!padding && carry > 0) throw new Error(`Non-zero padding: ${carry}`); if (padding && pos > 0) res.push(carry >>> 0); return res; } /** * @__NO_SIDE_EFFECTS__ */ function radix(num) { anumber(num); const _256 = 2 ** 8; return { encode: (bytes) => { if (!isBytes(bytes)) throw new Error('radix.encode input should be Uint8Array'); return convertRadix(Array.from(bytes), _256, num); }, decode: (digits) => { anumArr('radix.decode', digits); return Uint8Array.from(convertRadix(digits, num, _256)); }, }; } /** * If both bases are power of same number (like `2**8 <-> 2**64`), * there is a linear algorithm. For now we have implementation for power-of-two bases only. * @__NO_SIDE_EFFECTS__ */ function radix2(bits, revPadding = false) { anumber(bits); if (bits <= 0 || bits > 32) throw new Error('radix2: bits should be in (0..32]'); if (radix2carry(8, bits) > 32 || radix2carry(bits, 8) > 32) throw new Error('radix2: carry overflow'); return { encode: (bytes) => { if (!isBytes(bytes)) throw new Error('radix2.encode input should be Uint8Array'); return convertRadix2(Array.from(bytes), 8, bits, !revPadding); }, decode: (digits) => { anumArr('radix2.decode', digits); return Uint8Array.from(convertRadix2(digits, bits, 8, revPadding)); }, }; } function unsafeWrapper(fn) { afn(fn); return function (...args) { try { return fn.apply(null, args); } catch (e) { } }; } function checksum(len, fn) { anumber(len); afn(fn); return { encode(data) { if (!isBytes(data)) throw new Error('checksum.encode: input should be Uint8Array'); const sum = fn(data).slice(0, len); const res = new Uint8Array(data.length + len); res.set(data); res.set(sum, data.length); return res; }, decode(data) { if (!isBytes(data)) throw new Error('checksum.decode: input should be Uint8Array'); const payload = data.slice(0, -len); const oldChecksum = data.slice(-len); const newChecksum = fn(payload).slice(0, len); for (let i = 0; i < len; i++) if (newChecksum[i] !== oldChecksum[i]) throw new Error('Invalid checksum'); return payload; }, }; } // prettier-ignore export const utils = { alphabet, chain, checksum, convertRadix, convertRadix2, radix, radix2, join, padding, }; // RFC 4648 aka RFC 3548 // --------------------- /** * base16 encoding from RFC 4648. * @example * ```js * base16.encode(Uint8Array.from([0x12, 0xab])); * // => '12AB' * ``` */ export const base16 = chain(radix2(4), alphabet('0123456789ABCDEF'), join('')); /** * base32 encoding from RFC 4648. Has padding. * Use `base32nopad` for unpadded version. * Also check out `base32hex`, `base32hexnopad`, `base32crockford`. * @example * ```js * base32.encode(Uint8Array.from([0x12, 0xab])); * // => 'CKVQ====' * base32.decode('CKVQ===='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32 = chain(radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), padding(5), join('')); /** * base32 encoding from RFC 4648. No padding. * Use `base32` for padded version. * Also check out `base32hex`, `base32hexnopad`, `base32crockford`. * @example * ```js * base32nopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'CKVQ' * base32nopad.decode('CKVQ'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32nopad = chain(radix2(5), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'), join('')); /** * base32 encoding from RFC 4648. Padded. Compared to ordinary `base32`, slightly different alphabet. * Use `base32hexnopad` for unpadded version. * @example * ```js * base32hex.encode(Uint8Array.from([0x12, 0xab])); * // => '2ALG====' * base32hex.decode('2ALG===='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32hex = chain(radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), padding(5), join('')); /** * base32 encoding from RFC 4648. No padding. Compared to ordinary `base32`, slightly different alphabet. * Use `base32hex` for padded version. * @example * ```js * base32hexnopad.encode(Uint8Array.from([0x12, 0xab])); * // => '2ALG' * base32hexnopad.decode('2ALG'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32hexnopad = chain(radix2(5), alphabet('0123456789ABCDEFGHIJKLMNOPQRSTUV'), join('')); /** * base32 encoding from RFC 4648. Doug Crockford's version. * https://www.crockford.com/base32.html * @example * ```js * base32crockford.encode(Uint8Array.from([0x12, 0xab])); * // => '2ANG' * base32crockford.decode('2ANG'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base32crockford = chain(radix2(5), alphabet('0123456789ABCDEFGHJKMNPQRSTVWXYZ'), join(''), normalize((s) => s.toUpperCase().replace(/O/g, '0').replace(/[IL]/g, '1'))); // Built-in base64 conversion https://caniuse.com/mdn-javascript_builtins_uint8array_frombase64 // prettier-ignore const hasBase64Builtin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toBase64 === 'function' && typeof Uint8Array.fromBase64 === 'function')(); const decodeBase64Builtin = (s, isUrl) => { astr('base64', s); const re = isUrl ? /^[A-Za-z0-9=_-]+$/ : /^[A-Za-z0-9=+/]+$/; const alphabet = isUrl ? 'base64url' : 'base64'; if (s.length > 0 && !re.test(s)) throw new Error('invalid base64'); return Uint8Array.fromBase64(s, { alphabet, lastChunkHandling: 'strict' }); }; /** * base64 from RFC 4648. Padded. * Use `base64nopad` for unpadded version. * Also check out `base64url`, `base64urlnopad`. * Falls back to built-in function, when available. * @example * ```js * base64.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs=' * base64.decode('Eqs='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ // prettier-ignore export const base64 = hasBase64Builtin ? { encode(b) { abytes(b); return b.toBase64(); }, decode(s) { return decodeBase64Builtin(s, false); }, } : chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), padding(6), join('')); /** * base64 from RFC 4648. No padding. * Use `base64` for padded version. * @example * ```js * base64nopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs' * base64nopad.decode('Eqs'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base64nopad = chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'), join('')); /** * base64 from RFC 4648, using URL-safe alphabet. Padded. * Use `base64urlnopad` for unpadded version. * Falls back to built-in function, when available. * @example * ```js * base64url.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs=' * base64url.decode('Eqs='); * // => Uint8Array.from([0x12, 0xab]) * ``` */ // prettier-ignore export const base64url = hasBase64Builtin ? { encode(b) { abytes(b); return b.toBase64({ alphabet: 'base64url' }); }, decode(s) { return decodeBase64Builtin(s, true); }, } : chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), padding(6), join('')); /** * base64 from RFC 4648, using URL-safe alphabet. No padding. * Use `base64url` for padded version. * @example * ```js * base64urlnopad.encode(Uint8Array.from([0x12, 0xab])); * // => 'Eqs' * base64urlnopad.decode('Eqs'); * // => Uint8Array.from([0x12, 0xab]) * ``` */ export const base64urlnopad = chain(radix2(6), alphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'), join('')); // base58 code // ----------- const genBase58 = /* @__NO_SIDE_EFFECTS__ */ (abc) => chain(radix(58), alphabet(abc), join('')); /** * base58: base64 without ambigous characters +, /, 0, O, I, l. * Quadratic (O(n^2)) - so, can't be used on large inputs. * @example * ```js * base58.decode('01abcdef'); * // => '3UhJW' * ``` */ export const base58 = genBase58('123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'); /** * base58: flickr version. Check out `base58`. */ export const base58flickr = genBase58('123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'); /** * base58: XRP version. Check out `base58`. */ export const base58xrp = genBase58('rpshnaf39wBUDNEGHJKLM4PQRST7VWXYZ2bcdeCg65jkm8oFqi1tuvAxyz'); // Data len (index) -> encoded block len const XMR_BLOCK_LEN = [0, 2, 3, 5, 6, 7, 9, 10, 11]; /** * base58: XMR version. Check out `base58`. * Done in 8-byte blocks (which equals 11 chars in decoding). Last (non-full) block padded with '1' to size in XMR_BLOCK_LEN. * Block encoding significantly reduces quadratic complexity of base58. */ export const base58xmr = { encode(data) { let res = ''; for (let i = 0; i < data.length; i += 8) { const block = data.subarray(i, i + 8); res += base58.encode(block).padStart(XMR_BLOCK_LEN[block.length], '1'); } return res; }, decode(str) { let res = []; for (let i = 0; i < str.length; i += 11) { const slice = str.slice(i, i + 11); const blockLen = XMR_BLOCK_LEN.indexOf(slice.length); const block = base58.decode(slice); for (let j = 0; j < block.length - blockLen; j++) { if (block[j] !== 0) throw new Error('base58xmr: wrong padding'); } res = res.concat(Array.from(block.slice(block.length - blockLen))); } return Uint8Array.from(res); }, }; /** * Method, which creates base58check encoder. * Requires function, calculating sha256. */ export const createBase58check = (sha256) => chain(checksum(4, (data) => sha256(sha256(data))), base58); /** * Use `createBase58check` instead. * @deprecated */ export const base58check = createBase58check; const BECH_ALPHABET = chain(alphabet('qpzry9x8gf2tvdw0s3jn54khce6mua7l'), join('')); const POLYMOD_GENERATORS = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3]; function bech32Polymod(pre) { const b = pre >> 25; let chk = (pre & 0x1ffffff) << 5; for (let i = 0; i < POLYMOD_GENERATORS.length; i++) { if (((b >> i) & 1) === 1) chk ^= POLYMOD_GENERATORS[i]; } return chk; } function bechChecksum(prefix, words, encodingConst = 1) { const len = prefix.length; let chk = 1; for (let i = 0; i < len; i++) { const c = prefix.charCodeAt(i); if (c < 33 || c > 126) throw new Error(`Invalid prefix (${prefix})`); chk = bech32Polymod(chk) ^ (c >> 5); } chk = bech32Polymod(chk); for (let i = 0; i < len; i++) chk = bech32Polymod(chk) ^ (prefix.charCodeAt(i) & 0x1f); for (let v of words) chk = bech32Polymod(chk) ^ v; for (let i = 0; i < 6; i++) chk = bech32Polymod(chk); chk ^= encodingConst; return BECH_ALPHABET.encode(convertRadix2([chk % powers[30]], 30, 5, false)); } /** * @__NO_SIDE_EFFECTS__ */ function genBech32(encoding) { const ENCODING_CONST = encoding === 'bech32' ? 1 : 0x2bc830a3; const _words = radix2(5); const fromWords = _words.decode; const toWords = _words.encode; const fromWordsUnsafe = unsafeWrapper(fromWords); function encode(prefix, words, limit = 90) { astr('bech32.encode prefix', prefix); if (isBytes(words)) words = Array.from(words); anumArr('bech32.encode', words); const plen = prefix.length; if (plen === 0) throw new TypeError(`Invalid prefix length ${plen}`); const actualLength = plen + 7 + words.length; if (limit !== false && actualLength > limit) throw new TypeError(`Length ${actualLength} exceeds limit ${limit}`); const lowered = prefix.toLowerCase(); const sum = bechChecksum(lowered, words, ENCODING_CONST); return `${lowered}1${BECH_ALPHABET.encode(words)}${sum}`; } function decode(str, limit = 90) { astr('bech32.decode input', str); const slen = str.length; if (slen < 8 || (limit !== false && slen > limit)) throw new TypeError(`invalid string length: ${slen} (${str}). Expected (8..${limit})`); // don't allow mixed case const lowered = str.toLowerCase(); if (str !== lowered && str !== str.toUpperCase()) throw new Error(`String must be lowercase or uppercase`); const sepIndex = lowered.lastIndexOf('1'); if (sepIndex === 0 || sepIndex === -1) throw new Error(`Letter "1" must be present between prefix and data only`); const prefix = lowered.slice(0, sepIndex); const data = lowered.slice(sepIndex + 1); if (data.length < 6) throw new Error('Data must be at least 6 characters long'); const words = BECH_ALPHABET.decode(data).slice(0, -6); const sum = bechChecksum(prefix, words, ENCODING_CONST); if (!data.endsWith(sum)) throw new Error(`Invalid checksum in ${str}: expected "${sum}"`); return { prefix, words }; } const decodeUnsafe = unsafeWrapper(decode); function decodeToBytes(str) { const { prefix, words } = decode(str, false); return { prefix, words, bytes: fromWords(words) }; } function encodeFromBytes(prefix, bytes) { return encode(prefix, toWords(bytes)); } return { encode, decode, encodeFromBytes, decodeToBytes, decodeUnsafe, fromWords, fromWordsUnsafe, toWords, }; } /** * bech32 from BIP 173. Operates on words. * For high-level, check out scure-btc-signer: * https://github.com/paulmillr/scure-btc-signer. */ export const bech32 = genBech32('bech32'); /** * bech32m from BIP 350. Operates on words. * It was to mitigate `bech32` weaknesses. * For high-level, check out scure-btc-signer: * https://github.com/paulmillr/scure-btc-signer. */ export const bech32m = genBech32('bech32m'); /** * UTF-8-to-byte decoder. Uses built-in TextDecoder / TextEncoder. * @example * ```js * const b = utf8.decode("hey"); // => new Uint8Array([ 104, 101, 121 ]) * const str = utf8.encode(b); // "hey" * ``` */ export const utf8 = { encode: (data) => new TextDecoder().decode(data), decode: (str) => new TextEncoder().encode(str), }; // Built-in hex conversion https://caniuse.com/mdn-javascript_builtins_uint8array_fromhex // prettier-ignore const hasHexBuiltin = /* @__PURE__ */ (() => typeof Uint8Array.from([]).toHex === 'function' && typeof Uint8Array.fromHex === 'function')(); // prettier-ignore const hexBuiltin = { encode(data) { abytes(data); return data.toHex(); }, decode(s) { astr('hex', s); return Uint8Array.fromHex(s); }, }; /** * hex string decoder. Uses built-in function, when available. * @example * ```js * const b = hex.decode("0102ff"); // => new Uint8Array([ 1, 2, 255 ]) * const str = hex.encode(b); // "0102ff" * ``` */ export const hex = hasHexBuiltin ? hexBuiltin : chain(radix2(4), alphabet('0123456789abcdef'), join(''), normalize((s) => { if (typeof s !== 'string' || s.length % 2 !== 0) throw new TypeError(`hex.decode: expected string, got ${typeof s} with length ${s.length}`); return s.toLowerCase(); })); // prettier-ignore const CODERS = { utf8, hex, base16, base32, base64, base64url, base58, base58xmr }; const coderTypeError = 'Invalid encoding type. Available types: utf8, hex, base16, base32, base64, base64url, base58, base58xmr'; /** @deprecated */ export const bytesToString = (type, bytes) => { if (typeof type !== 'string' || !CODERS.hasOwnProperty(type)) throw new TypeError(coderTypeError); if (!isBytes(bytes)) throw new TypeError('bytesToString() expects Uint8Array'); return CODERS[type].encode(bytes); }; /** @deprecated */ export const str = bytesToString; // as in python, but for bytes only /** @deprecated */ export const stringToBytes = (type, str) => { if (!CODERS.hasOwnProperty(type)) throw new TypeError(coderTypeError); if (typeof str !== 'string') throw new TypeError('stringToBytes() expects string'); return CODERS[type].decode(str); }; /** @deprecated */ export const bytes = stringToBytes; //# sourceMappingURL=index.js.map