"use strict"; const blockLimit = BigInt("0x10000000000000000"); /** * Pad the given buffer with zeroes so that it is a multiple of `blockSize` bytes. * @param {ArrayBuffer} buffer The buffer to pad. * @param {number} blockSize The target block size. * @returns {ArrayBuffer} The zero-padded buffer. */ function padBuffer(buffer, blockSize) { if (buffer.byteLength % blockSize === 0) { return buffer; } const padded = new ArrayBuffer(Math.ceil(buffer.byteLength / blockSize) * blockSize); new Uint8Array(padded).set(new Uint8Array(buffer)); return padded; } /** * Clamp the given buffer to the specified `size`, either truncating or padding * with zeroes as necessary. * @param {ArrayBuffer} buffer The source buffer. * @param {number} size The target size. * @returns {ArrayBuffer} The clamped buffer. */ function clampBuffer(buffer, size) { if (buffer.byteLength === size) { return buffer; } const clamped = new ArrayBuffer(size); new Uint8Array(clamped).set(new Uint8Array(buffer).slice(0, size)); return clamped; } /** * Convert raw binary data to a BigInt. * @param {DataView} data The binary data (must be a multiple of 8 bytes). * @returns {bigint} The equivalent BigInt value. */ function toBigInt(data) { let n = data.getBigUint64(data.byteLength - 8, true); for (let i = data.byteLength - 16; i >= 0; i -= 8) { n *= blockLimit; n += data.getBigUint64(i, true); } return n; } /** * Convert a numeric value to an ArrayBuffer. * @param {number | bigint} value The value to convert. * @returns {ArrayBuffer} The converted value. */ function toBuffer(value) { if (typeof value === "number") { value = BigInt(value); } const buffer = new ArrayBuffer(8); const view = new DataView(buffer); view.setBigUint64(0, value, true); return buffer; } const alpha41 = [ "B", "C", "D", "F", "G", "H", "J", "K", "L", "M", "N", "P", "Q", "R", "S", "T", "V", "W", "X", "Y", "Z", "b", "c", "d", "f", "g", "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z" ]; const alpha24 = [ "b", "c", "d", "f", "g", "h", "j", "k", "m", "n", "p", "q", "r", "s", "t", "v", "w", "x", "y", "z", "2", "5", "7", "9" ]; const factors = [ 0x055c3050, 0x53971500, 0x197b6830, 0x05c6b740, 0x098f6700, 0x02611500 ]; /** * Shuffle the encoding alphabat using the Fisher-Yates algorithm and the * provided seed. * @param {?ArrayBuffer} seed The seed value. * @param {?boolean} safe Use safe mode (base24). * @returns {string[]} The shuffled encoding alphabet. */ function shuffleAlphabet(seed, safe) { const chars = (safe ? alpha24 : alpha41).slice(0); if (seed) { const source = new DataView(seed); let mod = safe ? 24 : 41; let value; let n; let swap; (safe ? factors.slice(3) : factors).forEach((range, i) => { value = source.getUint32(i * 4, true) % range; while (range > 1) { range /= mod--; n = Math.floor(value / range); value %= range; swap = chars[n]; chars[n] = chars[mod]; chars[mod] = swap; } }); } return chars; } /** * Encode a numeric value (maximum 53 bits). * @param {number} base The encoding base (24 or 41). * @param {number} value The value to encode. * @param {string[]} chars The encoding characters. * @param {?number} length The target output length. * @returns {string} The encoded value. */ function encodeNumber(base, value, chars, length = 0) { let n; let result = ""; for (let i = 0; length === 0 && value > 0 || i < length; i++) { if (value) { n = value % base; value = Math.floor(value / base); } else { n = 0; } result += chars[n]; } return result; } /** * Encode a bigint value (maximum 64 bits). * @param {number} base The encoding base (24 or 41). * @param {bigint} value The value to encode. * @param {string[]} chars The encoding characters. * @param {?number} length The target output length. * @returns {string} The encoded value. */ function encodeBigInt(base, value, chars, length = 0) { const b = BigInt(base); let n; let result = ""; for (let i = 0; length === 0 && value > 0n || i < length; i++) { if (value) { n = Number(value % b); value /= b; } else { n = 0; } result += chars[n]; } return result; } /** * Restore a `number` value from raw decoded data. * @param {number} base The encoding base (24 or 41). * @param {Uint8Array} values Raw values to be restored. * @returns {number} Restored value. */ function restoreNumber(base, values) { let i = values.length - 1; let n = values[i--]; while (i >= 0) { n *= base; n += values[i--]; } return n; } /** * Restore a `bigint` value from raw decoded data. * @param {number} base The encoding base (24 or 41). * @param {Uint8Array} values Raw values to be restored. * @returns {bigint} Restored value. */ function restoreBigInt(base, values) { const b = BigInt(base); let i = values.length - 1; let n = BigInt(values[i--]); while (i >= 0) { n *= b; n += BigInt(values[i--]); } return n; } class KeymaskEncoder { chars; base; limit; block; /** * Create a new `KeymaskEncoder` using the provided encoding `base` and `seed`. * If provided, the `seed` will be used to shuffle the encoding alphabet. * @param {?ArrayBuffer} seed The seed value. * @param {?boolean} safe Use safe mode (base24). */ constructor(seed, safe) { if (safe) { this.base = 24; this.block = 14; this.limit = 11; } else { this.base = 41; this.block = 12; this.limit = 10; } this.chars = shuffleAlphabet(seed ? clampBuffer(seed, safe ? 12 : 24) : seed, safe); } /** * Encode the given value as base24 or base41. * @param {number | bigint | ArrayBuffer} value The value to encode. * @param {?number} length The target output length in characters. For inputs * greater than 64 bits, this applies to the final encoding block. * @returns {string} The encoded value. */ encode(value, length) { if (typeof value === "number") { return encodeNumber(this.base, value, this.chars, length); } else if (typeof value === "bigint") { if (value >= blockLimit) { let result = ""; let next; while (value > 0n) { next = value / blockLimit; result += encodeBigInt(this.base, value % blockLimit, this.chars, next > 0n ? this.block : length); value = next; } return result; } return encodeBigInt(this.base, value, this.chars, length); } value = padBuffer(value, 8); const data = new DataView(value); const last = Math.ceil(value.byteLength / 8) - 1; let result = ""; for (let i = 0; i <= last; i++) { result += encodeBigInt(this.base, data.getBigUint64(i * 8, true), this.chars, i < last ? this.block : length); } return result; } /** * Decode the provided value. * @param {string} value The value to be decoded. * @param {boolean} bigint Convert the result to a `BigInt`. * @returns {number | bigint | ArrayBuffer} The decoded value. */ decode(value, bigint = false) { const length = value.length; let values = new Uint8Array(length); for (let i = 0; i < length; i++) { values[i] = this.chars.indexOf(value.charAt(i)); } if (length <= this.limit && !bigint) { return restoreNumber(this.base, values); } else if (length <= this.block) { return restoreBigInt(this.base, values); } else { values = new Uint8Array(padBuffer(values.buffer, this.block)); const output = new ArrayBuffer(Math.round(values.byteLength * 8 / this.block)); const data = new DataView(output); for (let i = 0, j = 0; i < output.byteLength; i += 8, j += this.block) { data.setBigUint64(i, restoreBigInt(this.base, values.slice(j, j + this.block)), true); } if (bigint) { return toBigInt(data); } return data.buffer; } } } const lcgMap41 = [ [], [41, 22, 28], [1021, 65, 377], [65521, 17364, 32236], [2097143, 1043187, 1352851], [67108859, 19552116, 24409594], [4294967291n, 1588635695n, 3870709308n], [137438953447n, 31450092817n, 76886758244n], [4398046511093n, 2928603677866n, 3015630915308n], [281474976710597n, 59279420901007n, 163724808306782n], [9007199254740881n, 2082839274626558n, 3141627116318043n], [288230376151711717n, 56502943171806276n, 101565695086122187n], [18446744073709551557n, 9044836419713972268n, 13891176665706064842n] ]; const lcgMap24 = [ [], [23, 7, 10], [509, 110, 236], [8191, 1716, 5580], [262139, 92717, 166972], [4194301, 1731287, 2040406], [134217689, 45576512, 70391260], [4294967291n, 1588635695n, 3870709308n], [68719476731n, 40162435147n, 45453986995n], [2199023255531n, 717943173063n, 1319743354064n], [35184372088777n, 11850386302026n, 18586042069168n], [1125899906842597n, 605985299432352n, 791038363307311n], [36028797018963913n, 19708881949174686n, 32182684885571630n], [576460752303423433n, 287514719519235431n, 346764851511064641n], [18446744073709551557n, 9044836419713972268n, 13891176665706064842n] ]; class KeymaskGenerator { offsets; safe; /** * Create a Generator with custom offsets. * @param {?ArrayBuffer} seed 8-byte seed value. * @param {?boolean} safe Use safe mode (base24). */ constructor(seed, safe = false) { this.safe = safe; this.offsets = new Array(safe ? 15 : 13); if (seed) { const data = new DataView(clampBuffer(seed, 8)); const n32 = (data.getUint32(0, true) ^ data.getUint32(4, true)) >>> 0; const n64 = data.getBigUint64(0, true); if (safe) { for (let i = 1; i < 15; i++) { if (i < 7) { this.offsets[i] = n32 % (lcgMap24[i][0] - 1); } else if (i > 7) { this.offsets[i] = n64 % (lcgMap24[i][0] - 1n); } else { this.offsets[7] = BigInt(n32 % 4294967290); } } } else { for (let i = 1; i < 13; i++) { if (i < 6) { this.offsets[i] = n32 % (lcgMap41[i][0] - 1); } else if (i > 6) { this.offsets[i] = n64 % (lcgMap41[i][0] - 1n); } else { this.offsets[6] = BigInt(n32 % 4294967290); } } } } else if (safe) { for (let i = 1; i < 15; i++) { this.offsets[i] = i < 7 ? 0 : 0n; } } else { for (let i = 1; i < 13; i++) { this.offsets[i] = i < 6 ? 0 : 0n; } } } bigIntOutput(length) { return !this.safe && length > 10 || this.safe && length > 11; } /** * Calculate the next value in the given MLCG sequence. * @param {number | bigint} value The starting value. * @param {number} range The MLCG range (corresponds to encoded output length). * @param {?boolean} bigint Cast all outputs as BigInt. * @returns {number | bigint} The next value in the MLCG sequence. */ next(value, range, bigint = false) { if (!value) { return this.bigIntOutput(range) || bigint ? 0n : 0; } const map = this.safe ? lcgMap24 : lcgMap41; const mod = map[range][0]; const mult = map[range][1]; const offset = this.offsets[range]; if (!this.safe && range > 5 || this.safe && range > 6) { if (typeof value === "number") { value = BigInt(value); } value = value * mult % mod + offset; value = value < mod ? value : value - mod + 1n; return this.bigIntOutput(range) || bigint ? value : Number(value); } if (typeof value === "bigint") { value = Number(value); } value = value * mult % mod + offset; value = value < mod ? value : value - mod + 1; return bigint ? BigInt(value) : value; } /** * Calculate the previous value in the given MLCG sequence. * @param {number | bigint} value The starting value. * @param {number} range The MLCG range (corresponds to encoded output length). * @param {?boolean} bigint Cast all outputs as BigInt. * @returns {number | bigint} The previous value in the MLCG sequence. */ previous(value, range, bigint = false) { if (!value) { return this.bigIntOutput(range) || bigint ? 0n : 0; } const map = this.safe ? lcgMap24 : lcgMap41; const mod = map[range][0]; const mult = map[range][2]; const offset = this.offsets[range]; if (!this.safe && range > 5 || this.safe && range > 6) { if (typeof value === "number") { value = BigInt(value); } value -= offset; value = (value > 0 ? value : value + mod - 1n) * mult % mod; return this.bigIntOutput(range) || bigint ? value : Number(value); } if (typeof value === "bigint") { value = Number(value); } value -= offset; value = (value > 0 ? value : value + mod - 1) * mult % mod; return bigint ? BigInt(value) : value; } } const limits12 = [ 0n, 41n, 1021n, 65521n, 2097143n, 67108859n, 4294967291n, 137438953447n, 4398046511093n, 281474976710597n, 9007199254740881n, 288230376151711717n, 18446744073709551557n ]; const limits14 = [ 0n, 23n, 509n, 8191n, 262139n, 4194301n, 134217689n, 4294967291n, 68719476731n, 2199023255531n, 35184372088777n, 1125899906842597n, 36028797018963913n, 576460752303423433n, 18446744073709551557n ]; /** * Determine the encoding length of the provided value. * @param {number | bigint} value The value to be encoded. * @param {number[]} sizes The available encoding lengths. * @param {number} block The encoding block size. * @returns {number} The smallest available encoding length. */ function encodingLength(value, sizes, block) { const limits = block === 12 ? limits12 : limits14; let length = block; let size = 0; value = typeof value === "bigint" ? value : BigInt(value); for (let i = 0; i < sizes.length; i++) { size = sizes[i]; if (value < limits[size]) { length = size; break; } } if (length === block && value >= limits[size]) { for (let i = sizes[sizes.length - 1] + 1; i < block; i++) { if (value < limits[i]) { length = i; break; } } } return length; } class Keymask { encoder; generator; sizes; type; /** * @typedef {"number" | "bigint" | "integer" | "buffer" | undefined} KeymaskType */ /** * @typedef {object} KeymaskOptions * @property {?ArrayBuffer} seed The seed value. * @property {?(number | number[])} size The minimum encoding size or allowable sizes. * @property {?boolean} safe Use safe mode (Base24 encoding, no uppercase letters). * @property {?KeymaskType} type Optionally specify the `unmask()` return type. * @property {?KeymaskEncoder} encoder Custom/shared `KeymaskEncoder` instance. */ /** * Create a new `Keymask` instance using the provided options. * @param {?KeymaskOptions} options Keymask options. */ constructor(options) { options = options || {}; if (options.encoder) { this.encoder = options.encoder; this.generator = new KeymaskGenerator(options.seed, options.safe); } else if (options.seed) { this.encoder = new KeymaskEncoder(options.seed, options.safe); this.generator = new KeymaskGenerator(options.seed.byteLength < (options.safe ? 20 : 32) ? void 0 : options.seed.slice(options.safe ? 12 : 24), options.safe); } else { this.encoder = new KeymaskEncoder(void 0, options.safe); this.generator = new KeymaskGenerator(void 0, options.safe); } const sizes = options.size; const max = this.encoder.block; if (Array.isArray(sizes)) { this.sizes = sizes .filter((size) => size > 0 && size <= max) .sort((a, b) => a - b); } else { this.sizes = [sizes && sizes > 0 && sizes <= max ? sizes : 1]; } this.type = options.type; } /** * Mask the provided value. * @param {number | bigint | string | ArrayBuffer} value The value to mask. * @returns {string} The masked value. */ mask(value) { let length; if (value instanceof ArrayBuffer) { value = padBuffer(value.slice(0), 8); const data = new DataView(value); const final = value.byteLength - 8; length = encodingLength(data.getBigUint64(final, true), this.sizes, this.encoder.block); for (let i = 0; i < value.byteLength; i += 8) { data.setBigUint64(i, this.generator.next(data.getBigUint64(i, true), i < final ? this.encoder.block : length, true), true); } return this.encoder.encode(data.buffer, length); } else { if (typeof value === "string") { value = BigInt(value); } length = encodingLength(value, this.sizes, this.encoder.block); return this.encoder.encode(this.generator.next(value, length), length); } } /** * Unmask the provided value. * @param {string} value The encoded value to unmask. * @returns {number | bigint | string | ArrayBuffer} The unmasked value. */ unmask(value) { const result = this.encoder.decode(value); const block = this.encoder.block; if (result instanceof ArrayBuffer) { const data = new DataView(result); const length = value.length % block || block; const final = result.byteLength - 8; for (let i = 0; i <= final; i += 8) { data.setBigUint64(i, this.generator.previous(data.getBigUint64(i, true), i < final ? block : length, true), true); } if (this.type === "bigint" || this.type === "string" || this.type === "integer" && this.generator.bigIntOutput(value.length)) { const n = toBigInt(data); return (this.type === "string" ? n.toString() : n); } if (length < block) { const bytes = new Uint8Array(result); let end = result.byteLength - 1; while (end >= 0 && bytes[end] === 0) { end -= 1; } return result.slice(0, end + 1); } return result; } const n = this.generator.previous(result, value.length, this.type === "bigint"); return (this.type === "buffer" ? toBuffer(n) : this.type === "string" ? n.toString() : n); } } class StrictKeymask extends Keymask { constructor(options) { super({ ...options, safe: true }); } mask(value) { const result = super.mask(value); const first = result.charAt(0); return first === "5" ? "e" + result.substring(1) : first === "9" ? "i" + result.substring(1) : first === "7" ? "o" + result.substring(1) : first === "2" ? "u" + result.substring(1) : result; } unmask(value) { const first = value.charAt(0); return super.unmask(first === "e" ? "5" + value.substring(1) : first === "i" ? "9" + value.substring(1) : first === "o" ? "7" + value.substring(1) : first === "u" ? "2" + value.substring(1) : value); } } exports.Keymask = Keymask; exports.KeymaskEncoder = KeymaskEncoder; exports.KeymaskGenerator = KeymaskGenerator; exports.StrictKeymask = StrictKeymask;