/** * CryptoUtils - Standalone cryptocurrency utilities library * * STANDALONE USAGE (outside Bitrequest): * ---------------------------------------- * * * * * FEATURES: * - Base58 / Base58Check encoding * - Bech32 / Bech32m encoding * - Secp256k1 elliptic curve operations * - SHA256, RIPEMD160, Hash160, Keccak-256 * - Bitcoin/Litecoin/Ethereum address generation * - Bitcoin Cash CashAddr support * - BIP39 mnemonic utilities * - AES encryption/decryption * - LNURL decoding * * @version 1.1.0 * @license AGPL-3.0 * @see https://github.com/bitrequest/bitrequest.github.io * secp256k1 implementation based onhttps://github.com/paulmillr/noble-secp256k1 */ // ============================================ // CONSTANTS // ============================================ const crypto = window.crypto, b58ab = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz", b32ab = "qpzry9x8gf2tvdw0s3jn54khce6mua7l", generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3], bytestring = "0000000000000000000000000000000000000000000000000000000000000000", utf8_encoder = new TextEncoder(), utf8_decoder = new TextDecoder("utf-8", { "fatal": true }), BECH32_CONST = 1, BECH32M_CONST = 0x2bc830a3; // Secp256k1 curve parameters const secp = {}, CURVE = { "a": 0n, "b": 7n, "P": (2n ** 256n) - (2n ** 32n) - 977n, "n": (2n ** 256n) - 432420386565659656852420866394968145599n, "Gx": 55066263022277343669578718895168534326250603453777594175500187360389116729240n, "Gy": 32670510020758816978083085130507043184471273380659243275938904335757337482424n }; secp.CURVE = CURVE; // ============================================ // TEST CONSTANTS // bip39 (All addresses / xpubs in this app are test addresses derived from the following testphrase, taken from https://github.com/bitcoinbook/bitcoinbook/blob/f8b883dcd4e3d1b9adf40fed59b7e898fbd9241f/ch05.asciidoc) // "army van defense carry jealous true garbage claim echo media make crunch" // ============================================ const crypto_utils_const = { "version": "1.1.0", // secp256k1 test: private key 1 = generator point G "test_privkey": "0000000000000000000000000000000000000000000000000000000000000001", "test_pubkey": "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", // Derived from test phrase "army van defense carry jealous true garbage claim echo media make crunch" "test_pubkey_bech32": "03bb4a626f63436a64d7cf1e441713cc964c0d53289a5b17acb1b9c262be57cb17", "test_address_bech32": "bc1qg0azlj4w2lrq8jssrrz6eprt2fe7f7edm4vpd5", "test_pubkey_eth": "03c026c4b041059c84a187252682b6f80cbbe64eb81497111ab6914b050a8936fd", "test_address_eth": "0x2161DedC3Be05B7Bb5aa16154BcbD254E9e9eb68", "test_legacy_address": "1AVPurYZinnctgGPiXziwU6PuyZKX5rYZU", "test_address_cashaddr": "qp5p0eur784pk8wxy2kzlz3ctnq5whfnuqqpp78u22" }; // ============================================ // CORE HELPERS // ============================================ // Creates a typed array with 8-bit unsigned integers from a byte array function uint_8array(bytes) { return new Uint8Array(bytes); } // Encodes string to UTF-8 using TextEncoder function buffer(enc) { return utf8_encoder.encode(enc); } // Decodes UTF-8 encoded data using TextDecoder function unbuffer(enc, encoding) { return utf8_decoder.decode(enc); } // Converts ArrayBuffer to zero-padded hexadecimal string function buf2hex(buffer) { return Array.prototype.map.call(uint_8array(buffer), x => ("00" + x.toString(16)).slice(-2)).join(""); } // Validates string contains only hexadecimal characters [0-9a-fA-F] function is_hex(str) { return new RegExp("^[a-fA-F0-9]+$").test(str); } // Left-pads string with zeros to specified byte length with truncation function str_pad(val, bytes) { return (bytestring.slice(0, bytes) + val).substr(-bytes); } // Converts integer to base-16 string representation function dec_to_hex(val) { return val.toString(16); } // Parses hexadecimal string to BigInt with 0x prefix function hex_to_dec(val) { return BigInt("0x" + val); } // Converts a hexadecimal string to a decimal string function hex_to_number_string(val) { return hex_to_int(val).toString(); } // Converts a hexadecimal string to a number function hex_to_int(val) { return parseInt(val, 16); } // Pads binary strings with leading zeros function pad_binary(binary_str, target_length) { let padded_str = binary_str.toString(); while (padded_str.length < target_length) { padded_str = "0" + padded_str; } return padded_str; } // Concatenates multiple Uint8Arrays into one function concat_bytes(...arrays) { const sizes = arrays.reduce((acc, a) => acc + a.length, 0), result = uint_8array(sizes); let offset = 0; for (const array of arrays) { result.set(array, offset); offset += array.length; } return result; } // Encodes integer as Bitcoin-style variable-length integer (LEB128) function encode_varint(n) { const bytes = []; while (n >= 0x80) { bytes.push((n & 0x7f) | 0x80); n >>= 7; } bytes.push(n); return new Uint8Array(bytes); } // ============================================ // SJCL BIT OPERATIONS // ============================================ // Converts UTF-8 string to SJCL bit array function to_bits(val) { return sjcl.codec.utf8String.toBits(val); } // Converts hex string to SJCL bit array function hex_to_bits(val) { return sjcl.codec.hex.toBits(val); } // Converts SJCL bit array to hex string function from_bits(val) { return sjcl.codec.hex.fromBits(val); } // Returns the bit length of SJCL bit array function bit_length(val) { return sjcl.bitArray.bitLength(val); } // Concatenates two SJCL bit arrays function concat_array(arr1, arr2) { return sjcl.bitArray.concat(arr1, arr2); } // ============================================ // BASE CONVERSION // ============================================ // Converts binary string to SJCL word array function binary_string_to_word_array(binary) { const bit_len = binary.length; let words = []; for (let i = 0; i < bit_len; i += 32) { const str_chunk = binary.substring(i, i + 32), int_word = parseInt(str_chunk, 2); words.push(int_word | 0); } return sjcl.bitArray.clamp(words, bit_len); } // Converts byte array to SJCL word array function byte_array_to_word_array(data) { let words = [], i, word = 0; for (i = 0; i < data.length; i++) { word = (word << 8) | data[i]; if ((i + 1) % 4 === 0) { words.push(word); word = 0; } } if (i % 4 !== 0) { word <<= (4 - (i % 4)) * 8; words.push(word); } return sjcl.bitArray.clamp(words, data.length * 8); } // Converts byte array to binary string function byte_array_to_binary_string(data) { let bin_str = ""; for (let i = 0; i < data.length; i++) { bin_str += pad_binary(data[i].toString(2), 8); } return bin_str; } // Converts hex string to binary string function hex_string_to_binary_string(hexString) { let bin_str = ""; for (let i = 0; i < hexString.length; i++) { const hexChar = hexString[i], hexInt = parseInt(hexChar, 16), bin_frag = hexInt.toString(2); bin_str += pad_binary(bin_frag, 4); } return bin_str; } // ============================================ // BASE58 ENCODING // ============================================ // Encodes data to Base58 string from hex or UTF-8 input function b58enc(enc, encode = "hex") { const bytestring = (encode === "hex") ? hex_to_bytes(enc) : buffer(enc); return b58enc_uint_array(bytestring); } // Converts Uint8Array to Base58 string using custom alphabet function b58enc_uint_array(u) { let d = [], s = "", i, j, c, n; for (i in u) { j = 0, c = u[i]; s += c || s.length ^ i ? "" : 1; while (j in d || c) { n = d[j]; n = n ? n * 256 + c : c; c = n / 58 | 0; d[j] = n % 58; j++ } } while (j--) s += b58ab[d[j]]; return s; } // Decodes Base58 string to UTF-8 or hexadecimal output function b58dec(dec, decode) { const buffer = b58dec_uint_array(dec); return (decode === "hex") ? buf2hex(buffer) : unbuffer(buffer, "utf-8"); } // Converts Base58 string to Uint8Array using custom alphabet function b58dec_uint_array(dec) { let d = [], b = [], i, j, c, n; for (i in dec) { j = 0, c = b58ab.indexOf(dec[i]); if (c < 0) return undefined; c || b.length ^ i ? i : b.push(0); while (j in d || c) { n = d[j]; n = n ? n * 58 + c : c; c = n >> 8; d[j] = n % 256; j++ } } while (j--) b.push(d[j]); return uint_8array(b); } // Implements Base58Check encoding with double SHA256 checksum function b58check_encode(payload) { const full_bytes = payload + hmacsha(hmacsha(payload, "sha256", "hex"), "sha256", "hex").slice(0, 8); return b58enc(full_bytes, "hex"); } // Decodes Base58Check string and removes 4-byte checksum function b58check_decode(val) { const full_bytes = b58dec(val, "hex"), bytes = full_bytes.substring(0, full_bytes.length - 8); return bytes; } // ============================================ // BECH32 ENCODING // ============================================ // Converts input byte array to 5-bit word representation for bech32 encoding function to_words(bytes) { const res = convert_bits(bytes, 8, 5, true); if (Array.isArray(res)) { return res } throw new Error(res) } // Converts 5-bit word array back to byte representation for bech32 decoding function from_words(bytes) { const res = convert_bits(bytes, 5, 8, true); if (Array.isArray(res)) { return res } throw new Error(res) } // Transforms data between different bit-length representations with optional padding function convert_bits(data, inBits, outBits, pad) { let value = 0, bits = 0, maxV = (1 << outBits) - 1, 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 (pad) { if (bits > 0) { result.push((value << (outBits - bits)) & maxV); } } else { if (bits >= inBits) { return "Excess padding" } if ((value << (outBits - bits)) & maxV) { return "Non-zero padding" } } return result } // Computes the Bech32 checksum function polymod(values) { let chk = 1; for (let p = 0; p < values.length; ++p) { let top = chk >> 25; chk = (chk & 0x1ffffff) << 5 ^ values[p]; for (let i = 0; i < 5; ++i) { if ((top >> i) & 1) { chk ^= generator[i]; } } } return chk; } // Expands the human-readable part for Bech32 encoding function hrp_expand(hrp) { const ret = []; let p; for (p = 0; p < hrp.length; ++p) { ret.push(hrp.charCodeAt(p) >> 5); } ret.push(0); for (p = 0; p < hrp.length; ++p) { ret.push(hrp.charCodeAt(p) & 31); } return ret; } // Verifies the checksum in a Bech32 address function verify_checksum(hrp, data) { return polymod(hrp_expand(hrp).concat(data)) === 1; } // Creates a checksum for Bech32 encoding function create_checksum(hrp, data) { const values = hrp_expand(hrp).concat(data).concat([0, 0, 0, 0, 0, 0]), mod = polymod(values) ^ 1, ret = []; for (let p = 0; p < 6; ++p) { ret.push((mod >> 5 * (5 - p)) & 31); } return ret; } // Encodes data into a Bech32 address function bech32_encode(hrp, data) { let combined = data.concat(create_checksum(hrp, data)), ret = hrp + "1"; for (let p = 0; p < combined.length; ++p) { ret += b32ab.charAt(combined[p]); } return ret; } // Decodes a Bech32 encoded string function bech32_decode(bechString) { let p, has_lower = false, has_upper = false; for (p = 0; p < bechString.length; ++p) { if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) { return null; } if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) { has_lower = true; } if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) { has_upper = true; } } if (has_lower && has_upper) { return null; } bechString = bechString.toLowerCase(); const pos = bechString.lastIndexOf("1"); if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) { return null; } const hrp = bechString.substring(0, pos), data = []; for (p = pos + 1; p < bechString.length; ++p) { const d = b32ab.indexOf(bechString.charAt(p)); if (d === -1) { return null; } data.push(d); } const encoding = verify_checksum_with_type(hrp, data); if (!encoding) { return null; } if (data[0] === 1 && encoding !== "bech32m") { return null; } if (data[0] === 0 && encoding !== "bech32") { return null; } return { "hrp": hrp, "data": data.slice(0, data.length - 6), "encoding": encoding }; } // Modified polymod function to support both bech32 and bech32m function verify_checksum_with_type(hrp, data) { const modulo = polymod(hrp_expand(hrp).concat(data)); if (modulo === BECH32_CONST) return "bech32"; if (modulo === BECH32M_CONST) return "bech32m"; return null; } // Converts a binary array to decimal array for Bech32 encoding function bech32_dec_array(bitarr) { const hexstr = [0]; bitarr.forEach(bits => { hexstr.push(parseInt(bits, 2)); }); return hexstr; } // Converts a public key to a Bech32 address function pub_to_address_bech32(hrp, pubkey) { const step1 = hash160(pubkey), step2 = hex_string_to_binary_string(step1), step3 = step2.match(/.{1,5}/g), step4 = bech32_dec_array(step3); return bech32_encode(hrp, step4); } // ============================================ // SECP256K1 ELLIPTIC CURVE // ============================================ // Computes modular reduction with positive result function mod(a, m = CURVE.P) { const r = a % m; return r >= 0n ? r : m + r; } // Evaluates the secp256k1 curve equation y² = x³ + 7 for a given x coordinate function weierstrass(x) { return mod(x ** 3n + CURVE.b); } // Implements Extended Euclidean Algorithm to find GCD and Bézout's identity coefficients function egcd(a, b) { if (typeof a === "number") a = BigInt(a); if (typeof b === "number") b = BigInt(b); let [x, y, u, v] = [0n, 1n, 1n, 0n]; while (a !== 0n) { const q = b / a, r = b % a; let m = x - u * q, n = y - v * q; [b, a] = [a, r]; [x, y] = [u, v]; [u, v] = [m, n]; } return [b, x, y]; } // Calculates the modular multiplicative inverse using extended Euclidean algorithm function invert(number, modulo = CURVE.P) { if (number === 0n || modulo <= 0n) { throw new Error("invert: invalid number"); } const [g, x] = egcd(mod(number, modulo), modulo); if (g !== 1n) throw new Error("invert: does not exist"); return mod(x, modulo); } // Converts hexadecimal string to Uint8Array with zero-padding for odd length function hex_to_bytes(hex) { if (typeof hex !== "string") throw new TypeError("hexToBytes: expected string"); if (hex.length % 2) hex = "0" + hex; const len = hex.length / 2, array = uint_8array(len); for (let i = 0; i < len; i++) { const j = i * 2; array[i] = parseInt(hex.slice(j, j + 2), 16); } return array; } // Converts Uint8Array to hexadecimal string function bytes_to_hex(uint8a) { let hex = ""; for (let i = 0; i < uint8a.length; i++) { hex += uint8a[i].toString(16).padStart(2, "0"); } return hex; } // Parses hexadecimal string to BigInt function hex_to_number(hex) { if (typeof hex !== "string") throw new TypeError("hexToNumber: expected string"); return hex.length ? BigInt("0x" + hex) : 0n; } // Converts Uint8Array to BigInt (big-endian) function bytes_to_number(bytes) { return hex_to_number(bytes_to_hex(bytes)); } // Zero-pads BigInt to 64-character hex string function pad64(num) { return num.toString(16).padStart(64, "0"); } // Implements point addition and doubling in Jacobian projective coordinates class JacobianPoint { constructor(x, y, z) { this.x = x; this.y = y; this.z = z; } static fromAffine(p) { return new JacobianPoint(p.x, p.y, 1n); } double() { const { "x": X1, "y": Y1, "z": Z1 } = this; if (!Y1) return new JacobianPoint(0n, 0n, 0n); const A = mod(X1 ** 2n), B = mod(Y1 ** 2n), C = mod(B ** 2n), D = mod(2n * (mod((X1 + B) ** 2n) - A - C)), E = mod(3n * A), F = mod(E ** 2n), X3 = mod(F - 2n * D), Y3 = mod(E * (D - X3) - 8n * C), Z3 = mod(2n * Y1 * Z1); return new JacobianPoint(X3, Y3, Z3); } add(other) { if (!other.x && !other.y) return this; if (!this.x && !this.y) return other; const { "x": X1, "y": Y1, "z": Z1 } = this, { "x": X2, "y": Y2, "z": Z2 } = other, Z1Z1 = mod(Z1 ** 2n), Z2Z2 = mod(Z2 ** 2n), U1 = mod(X1 * Z2Z2), U2 = mod(X2 * Z1Z1), S1 = mod(Y1 * Z2 * Z2Z2), S2 = mod(Y2 * Z1 * Z1Z1), H = mod(U2 - U1), r = mod(S2 - S1); if (H === 0n) { if (r === 0n) { return this.double(); } else { return new JacobianPoint(0n, 0n, 0n); } } const HH = mod(H ** 2n), HHH = mod(H * HH), V = mod(U1 * HH), X3 = mod(r ** 2n - HHH - 2n * V), Y3 = mod(r * (V - X3) - S1 * HHH), Z3 = mod(Z1 * Z2 * H); return new JacobianPoint(X3, Y3, Z3); } multiplyUnsafe(scalar) { let n = scalar; if (typeof n !== "bigint") n = BigInt(n); n = n % CURVE.n; if (n === 0n) return new JacobianPoint(0n, 0n, 0n); let p = new JacobianPoint(0n, 0n, 0n), d = this; while (n > 0n) { if (n & 1n) p = p.add(d); d = d.double(); n >>= 1n; } return p; } toAffine() { if (this.z === 0n) { return new Point(0n, 0n); } const iz = invert(this.z, CURVE.P), iz2 = mod(iz ** 2n), x = mod(this.x * iz2), y = mod(this.y * iz2 * iz); return new Point(x, y); } } // Represents a point on secp256k1 curve in affine coordinates (x,y) class Point { constructor(x, y) { this.x = x; this.y = y; } static fromPrivateKey(privateKey) { const key = normalize_privatekey(privateKey); return Point.BASE.multiply(key); } static fromHex(hex) { const bytes = hex instanceof Uint8Array ? hex : hex_to_bytes(hex); if (bytes.length === 32) { return this.fromX(bytes); } const header = bytes[0]; if (header === 0x02 || header === 0x03) { return this.fromCompressedHex(bytes); } if (header === 0x04) { return this.fromUncompressedHex(bytes); } throw new Error("Point.fromHex: invalid format"); } static fromX(bytes) { const x = bytes_to_number(bytes), y2 = weierstrass(x); let y = sqrt_mod(y2); if ((y & 1n) === 1n) { y = mod(-y); } const p = new Point(x, y); p.assertValidity(); return p; } static fromCompressedHex(bytes) { if (bytes.length !== 33) { throw new Error("Compressed pubkey must be 33 bytes"); } const x = bytes_to_number(bytes.slice(1)), y2 = weierstrass(x); let y = sqrt_mod(y2); const odd = (y & 1n) === 1n, isFirstByteOdd = (bytes[0] & 1) === 1; if (odd !== isFirstByteOdd) { y = mod(-y); } const p = new Point(x, y); p.assertValidity(); return p; } static fromUncompressedHex(bytes) { if (bytes.length !== 65) { throw new Error("Uncompressed pubkey must be 65 bytes"); } const x = bytes_to_number(bytes.slice(1, 33)), y = bytes_to_number(bytes.slice(33)), p = new Point(x, y); p.assertValidity(); return p; } assertValidity() { const { x, y } = this; if (x < 0n || x >= CURVE.P || y < 0n || y >= CURVE.P) { throw new Error("Point is not on curve (coordinates out of range)"); } const left = mod(y * y), right = weierstrass(x); if (left !== right) { throw new Error("Point is not on curve (y^2 != x^3 + 7)"); } } multiply(scalar) { return JacobianPoint.fromAffine(this).multiplyUnsafe(scalar).toAffine(); } add(other) { const pA = JacobianPoint.fromAffine(this), pB = JacobianPoint.fromAffine(other); return pA.add(pB).toAffine(); } negate() { return new Point(this.x, mod(-this.y)); } toHex(compressed = false) { const xHex = pad64(this.x); if (compressed) { const prefix = (this.y & 1n) === 1n ? "03" : "02"; return prefix + xHex; } else { const yHex = pad64(this.y); return "04" + xHex + yHex; } } static get BASE() { return new Point(CURVE.Gx, CURVE.Gy); } static get ZERO() { return new Point(0n, 0n); } } // Precomputed constant for sqrt_mod const P_1_4 = (CURVE.P + 1n) >> 2n; // Calculates modular square root using simplified Tonelli-Shanks for p ≡ 3 (mod 4) function sqrt_mod(x) { return pow_mod(x, P_1_4, CURVE.P); } // Computes modular exponentiation using square-and-multiply algorithm function pow_mod(base, exponent, modulus) { let result = 1n, b = mod(base, modulus), e = exponent; while (e > 0n) { if (e & 1n) result = mod(result * b, modulus); b = mod(b * b, modulus); e >>= 1n; } return result; } // Validates and normalizes private key to BigInt within curve order range function normalize_privatekey(privateKey) { let key = null; if (typeof privateKey === "bigint") { key = privateKey; } else if (typeof privateKey === "string") { key = hex_to_number(privateKey); } else if (privateKey instanceof Uint8Array) { key = bytes_to_number(privateKey); } else { throw new Error("Invalid private key type"); } key = key % CURVE.n; if (key <= 0n || key >= CURVE.n) { throw new Error("Invalid private key range"); } return key; } // Derives compressed or uncompressed public key from private key scalar function get_publickey(privateKey, isCompressed = true) { const P = Point.fromPrivateKey(privateKey); return P.toHex(isCompressed); } // Export the main Point class secp.Point = Point; // ============================================ // HASH FUNCTIONS // ============================================ // Generates HMAC using SJCL with optional hex encoding function hmac_bits(message, key, encode) { const enc_msg = (encode == "hex") ? hex_to_bits(message) : message, hmac = new sjcl.misc.hmac(key, sjcl.hash.sha512); return from_bits(hmac.encrypt(enc_msg)); } // Computes HMAC-SHA hash with optional key encoding function hmacsha(key, hash, encode) { const enc_key = (encode == "hex") ? hex_to_bits(key) : key; return from_bits(hmacsha_bits(enc_key, hash)); } // Performs HMAC-SHA hash computation on input key function hmacsha_bits(key, hash) { return sjcl.hash[hash].hash(key); } // Computes double hash: RIPEMD160(SHA256(input)) function hash160(pub) { return hmacsha(hmacsha(pub, "sha256", "hex"), "ripemd160", "hex"); } // Computes a substring of SHA256 hash function sha_sub(val, lim) { return hmacsha(val, "sha256").slice(0, lim); } // Keccak-256 hash function (used for Ethereum addresses) function keccak_256(input) { const rc = [ 1, 0, 32898, 0, 32906, 2147483648, 2147516416, 2147483648, 32907, 0, 2147483649, 0, 2147516545, 2147483648, 32777, 2147483648, 138, 0, 136, 0, 2147516425, 0, 2147483658, 0, 2147516555, 0, 139, 2147483648, 32905, 2147483648, 32771, 2147483648, 32770, 2147483648, 128, 2147483648, 32778, 0, 2147483658, 2147483648, 2147516545, 2147483648, 32896, 2147483648, 2147483649, 0, 2147516424, 2147483648 ]; function keccak_f(s) { let c = [], b = [], h, l; for (let n = 0; n < 48; n += 2) { for (let x = 0; x < 10; x++) { c[x] = s[x] ^ s[x + 10] ^ s[x + 20] ^ s[x + 30] ^ s[x + 40]; } for (let x = 0; x < 10; x += 2) { h = c[(x + 8) % 10] ^ (c[(x + 2) % 10] << 1 | c[(x + 3) % 10] >>> 31); l = c[(x + 9) % 10] ^ (c[(x + 3) % 10] << 1 | c[(x + 2) % 10] >>> 31); for (let y = 0; y < 50; y += 10) { s[x + y] ^= h; s[x + y + 1] ^= l; } } b[0] = s[0]; b[1] = s[1]; b[32] = s[11] << 4 | s[10] >>> 28; b[33] = s[10] << 4 | s[11] >>> 28; b[14] = s[20] << 3 | s[21] >>> 29; b[15] = s[21] << 3 | s[20] >>> 29; b[46] = s[31] << 9 | s[30] >>> 23; b[47] = s[30] << 9 | s[31] >>> 23; b[28] = s[40] << 18 | s[41] >>> 14; b[29] = s[41] << 18 | s[40] >>> 14; b[20] = s[2] << 1 | s[3] >>> 31; b[21] = s[3] << 1 | s[2] >>> 31; b[2] = s[13] << 12 | s[12] >>> 20; b[3] = s[12] << 12 | s[13] >>> 20; b[34] = s[22] << 10 | s[23] >>> 22; b[35] = s[23] << 10 | s[22] >>> 22; b[16] = s[33] << 13 | s[32] >>> 19; b[17] = s[32] << 13 | s[33] >>> 19; b[48] = s[42] << 2 | s[43] >>> 30; b[49] = s[43] << 2 | s[42] >>> 30; b[40] = s[5] << 30 | s[4] >>> 2; b[41] = s[4] << 30 | s[5] >>> 2; b[22] = s[14] << 6 | s[15] >>> 26; b[23] = s[15] << 6 | s[14] >>> 26; b[4] = s[25] << 11 | s[24] >>> 21; b[5] = s[24] << 11 | s[25] >>> 21; b[36] = s[34] << 15 | s[35] >>> 17; b[37] = s[35] << 15 | s[34] >>> 17; b[18] = s[45] << 29 | s[44] >>> 3; b[19] = s[44] << 29 | s[45] >>> 3; b[10] = s[6] << 28 | s[7] >>> 4; b[11] = s[7] << 28 | s[6] >>> 4; b[42] = s[17] << 23 | s[16] >>> 9; b[43] = s[16] << 23 | s[17] >>> 9; b[24] = s[26] << 25 | s[27] >>> 7; b[25] = s[27] << 25 | s[26] >>> 7; b[6] = s[36] << 21 | s[37] >>> 11; b[7] = s[37] << 21 | s[36] >>> 11; b[38] = s[47] << 24 | s[46] >>> 8; b[39] = s[46] << 24 | s[47] >>> 8; b[30] = s[8] << 27 | s[9] >>> 5; b[31] = s[9] << 27 | s[8] >>> 5; b[12] = s[18] << 20 | s[19] >>> 12; b[13] = s[19] << 20 | s[18] >>> 12; b[44] = s[29] << 7 | s[28] >>> 25; b[45] = s[28] << 7 | s[29] >>> 25; b[26] = s[38] << 8 | s[39] >>> 24; b[27] = s[39] << 8 | s[38] >>> 24; b[8] = s[48] << 14 | s[49] >>> 18; b[9] = s[49] << 14 | s[48] >>> 18; for (let y = 0; y < 50; y += 10) { for (let x = 0; x < 10; x += 2) { s[y + x] = b[y + x] ^ (~b[y + (x + 2) % 10] & b[y + (x + 4) % 10]); s[y + x + 1] = b[y + x + 1] ^ (~b[y + (x + 3) % 10] & b[y + (x + 5) % 10]); } } s[0] ^= rc[n]; s[1] ^= rc[n + 1]; } } let bytes; if (typeof input === "string") { bytes = new Uint8Array(input.length); for (let i = 0; i < input.length; i++) bytes[i] = input.charCodeAt(i); } else if (input instanceof Uint8Array) { bytes = input; } else if (Array.isArray(input)) { bytes = new Uint8Array(input); } else { throw new Error("Invalid input type for keccak256"); } const rate = 136, block_count = 34, s = new Array(50).fill(0), blocks = new Array(35).fill(0); let i = 0; for (let pos = 0; pos < bytes.length; pos++) { blocks[i >> 2] |= bytes[pos] << ((i & 3) << 3); if (++i >= rate) { for (let j = 0; j < block_count; j++) { s[j] ^= blocks[j]; blocks[j] = 0; } keccak_f(s); i = 0; } } blocks[i >> 2] |= 1 << ((i & 3) << 3); blocks[block_count - 1] |= 0x80000000; for (let j = 0; j < block_count; j++) s[j] ^= blocks[j]; keccak_f(s); let hex = ""; for (let i = 0; i < 32; i++) { const byte = (s[i >> 2] >> ((i & 3) << 3)) & 0xff; hex += (byte >> 4).toString(16) + (byte & 0xf).toString(16); } return hex; } // ============================================ // KEY & ADDRESS GENERATION // ============================================ // Encodes private key to Wallet Import Format (WIF) with optional compression function privkey_wif(versionbytes, hexkey, comp) { const compressed = (comp) ? "01" : ""; return b58check_encode(versionbytes + hexkey + compressed); } // Generates corresponding public key from a private key function priv_to_pub(priv) { return get_publickey(priv, true); } // Converts compressed public key to full uncompressed format function expand_pub(pub) { return secp.Point.fromHex(pub).toHex(false); } // Generates standard cryptocurrency address from public key function pub_to_address(versionbytes, pub) { return hash160_to_address(versionbytes, hash160(pub)); } // Derives Ethereum-specific address from public key function pub_to_eth_address(pub) { const xp_pub = expand_pub(pub), keccak = "0x" + keccak_256(hex_to_bytes(xp_pub.slice(2))), addr = "0x" + keccak.slice(26); return to_checksum_address(addr); } // Converts RIPEMD160 hash to a cryptocurrency address function hash160_to_address(versionbytes, h160) { return b58check_encode(versionbytes + h160); } // Converts an Ethereum address to checksum format function to_checksum_address(e) { if (void 0 === e) { return ""; } if (!/^(0x)?[0-9a-f]{40}$/i.test(e)) { throw new Error("Given address " + e + " is not a valid Ethereum address."); return } e = e.toLowerCase().replace(/^0x/i, ""); for (var t = keccak_256(e).replace(/^0x/i, ""), r = "0x", n = 0; n < e.length; n++) 7 < parseInt(t[n], 16) ? r += e[n].toUpperCase() : r += e[n]; return r; } // ============================================ // BITCOIN CASH (CASHADDR) // ============================================ const cashaddr = (function() { const CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; const CHARSET_MAP = {}; for (let i = 0; i < CHARSET.length; i++) { CHARSET_MAP[CHARSET[i]] = i; } function polymod(values) { const GENERATORS = [ 0x98f2bc8e61n, 0x79b76d99e2n, 0xf33e5fb3c4n, 0xae2eabe2a8n, 0x1e4f43e470n ]; let chk = 1n; for (let i = 0; i < values.length; i++) { const top = chk >> 35n; chk = ((chk & 0x07ffffffffn) << 5n) ^ BigInt(values[i]); for (let j = 0; j < 5; j++) { if ((top >> BigInt(j)) & 1n) { chk ^= GENERATORS[j]; } } } return chk ^ 1n; } function prefixToArray(prefix) { const result = new Uint8Array(prefix.length + 1); for (let i = 0; i < prefix.length; i++) { result[i] = prefix.charCodeAt(i) & 31; } result[prefix.length] = 0; return result; } function convertBits(data, fromBits, toBits, pad) { let acc = 0, bits = 0; const result = [], maxv = (1 << toBits) - 1; for (let i = 0; i < data.length; i++) { const value = data[i]; acc = (acc << fromBits) | value; bits += fromBits; while (bits >= toBits) { bits -= toBits; result.push((acc >> bits) & maxv); } } if (pad) { if (bits > 0) result.push((acc << (toBits - bits)) & maxv); } else if (bits >= fromBits || ((acc << (toBits - bits)) & maxv)) { throw new Error("Invalid padding"); } return new Uint8Array(result); } function createChecksum(prefix, payload) { const prefixArray = prefixToArray(prefix), combined = new Uint8Array(prefixArray.length + payload.length + 8); combined.set(prefixArray); combined.set(payload, prefixArray.length); const mod = polymod(combined), checksum = new Uint8Array(8); for (let i = 0; i < 8; i++) { checksum[7 - i] = Number((mod >> BigInt(i * 5)) & 31n); } return checksum; } function verifyChecksum(prefix, payload) { const prefixArray = prefixToArray(prefix), combined = new Uint8Array(prefixArray.length + payload.length); combined.set(prefixArray); combined.set(payload, prefixArray.length); return polymod(combined) === 0n; } function getHashSize(versionByte) { return [160, 192, 224, 256, 320, 384, 448, 512][versionByte & 7]; } function getType(versionByte) { const typeValue = versionByte & 120; if (typeValue === 0) return "P2PKH"; if (typeValue === 8) return "P2SH"; throw new Error("Invalid address type"); } return { "encode": function(prefix, type, hash) { let versionByte = (type === "P2PKH") ? 0 : (type === "P2SH") ? 8 : null; if (versionByte === null) throw new Error("Invalid type: " + type); const hashBits = hash.length * 8, sizeMap = { 160: 0, 192: 1, 224: 2, 256: 3, 320: 4, 384: 5, 448: 6, 512: 7 }; if (!(hashBits in sizeMap)) throw new Error("Invalid hash size"); versionByte |= sizeMap[hashBits]; const versionAndHash = new Uint8Array(hash.length + 1); versionAndHash[0] = versionByte; versionAndHash.set(hash, 1); const payload = convertBits(versionAndHash, 8, 5, true), checksum = createChecksum(prefix, payload), combined = new Uint8Array(payload.length + checksum.length); combined.set(payload); combined.set(checksum, payload.length); let result = prefix + ":"; for (let i = 0; i < combined.length; i++) { result += CHARSET[combined[i]]; } return result; }, "decode": function(address) { const lower = address.toLowerCase(); if (address !== lower && address !== address.toUpperCase()) { throw new Error("Mixed case address"); } const parts = lower.split(":"); if (parts.length !== 2) throw new Error("Missing prefix"); const prefix = parts[0], payloadStr = parts[1], payload = new Uint8Array(payloadStr.length); for (let i = 0; i < payloadStr.length; i++) { const char = payloadStr[i]; if (!(char in CHARSET_MAP)) throw new Error("Invalid character: " + char); payload[i] = CHARSET_MAP[char]; } if (!verifyChecksum(prefix, payload)) throw new Error("Invalid checksum"); const data = payload.slice(0, -8), converted = convertBits(data, 5, 8, false), versionByte = converted[0], hash = converted.slice(1); if (hash.length * 8 !== getHashSize(versionByte)) { throw new Error("Invalid hash size"); } return { prefix, type: getType(versionByte), hash }; } }; })(); // Converts a legacy Bitcoin Cash address to CashAddr format function pub_to_cashaddr(legacy) { const c_addr = bch_cashaddr("bitcoincash", "P2PKH", legacy); return c_addr.split(":")[1]; } // Converts a CashAddr format address to legacy Bitcoin Cash address function bch_legacy(cadr) { try { const address = (cadr.indexOf(":") === -1) ? "bitcoincash:" + cadr : cadr, version = 0, dec = cashaddr.decode(address), bytes = dec.hash, bytesarr = Array.from(bytes), conc = concat_array([0], bytesarr), unbuf = buf2hex(conc); return b58check_encode(unbuf); } catch (e) { return cadr } } // Converts a legacy Bitcoin Cash address to CashAddr format function bch_cashaddr(prefix, type, legacy) { try { const lbytes = b58dec_uint_array(legacy), lbslice = lbytes.slice(1, 21); return cashaddr.encode(prefix, type, lbslice); } catch (e) { console.error(e.name, e.message); return legacy } } // ============================================ // LNURL & LIGHTNING // ============================================ // Decodes a Bech32 encoded LNURL function lnurl_decodeb32(lnurl) { let p, has_lower = false, has_upper = false; for (p = 0; p < lnurl.length; ++p) { if (lnurl.charCodeAt(p) < 33 || lnurl.charCodeAt(p) > 126) { return null; } if (lnurl.charCodeAt(p) >= 97 && lnurl.charCodeAt(p) <= 122) { has_lower = true; } if (lnurl.charCodeAt(p) >= 65 && lnurl.charCodeAt(p) <= 90) { has_upper = true; } } if (has_lower && has_upper) { return null; } const lnurlow = lnurl.toLowerCase(), pos = lnurlow.lastIndexOf("1"), hrp = lnurlow.substring(0, pos), data = []; for (p = pos + 1; p < lnurlow.length; ++p) { const d = b32ab.indexOf(lnurlow.charAt(p)); if (d === -1) { return null; } data.push(d); } if (!verify_checksum(hrp, data)) { return null; } return { "hrp": hrp, "data": data.slice(0, data.length - 6) }; } // ============================================ // MNEMONIC FUNCTIONS // ============================================ // Cleans and normalizes mnemonic string function clean_string(words) { return normalize_string(join_words(split_words(words))); } // Concatenates word array with single space delimiter function join_words(words) { return words.join(" "); } // Splits string on whitespace and removes empty elements function split_words(mnemonic) { return mnemonic.split(/\s/g).filter(function(x) { return x.length; }); } // Applies Unicode NFKD normalization to string function normalize_string(str) { return str.normalize("NFKD"); } // Converts BIP39 mnemonic to binary string with 11-bit word indices function mnemonic_to_binary_string(mnemonic) { const mm = split_words(mnemonic); if (mm.length == 0 || mm.length % 3 > 0) { return null; } const idx = []; for (let i = 0; i < mm.length; i++) { const word = mm[i], wordIndex = wordlist.indexOf(word); if (wordIndex == -1) { return null; } const binaryIndex = pad_binary(wordIndex.toString(2), 11); idx.push(binaryIndex); } return idx.join(""); } // ============================================ // ENCRYPTION // ============================================ // Encrypts data using AES-GCM function aes_enc(params, keyString) { const buffer = uint_8array(16), iv = byte_array_to_word_array(crypto.getRandomValues(buffer)), key = sjcl.codec.base64.toBits(keyString), cipher = new sjcl.cipher.aes(key), data = to_bits(params), enc = sjcl.mode.gcm.encrypt(cipher, data, iv, {}, 128), concatbitArray = concat_array(iv, enc), conString = sjcl.codec.base64.fromBits(concatbitArray); return conString; } // Decrypts AES-GCM encrypted data function aes_dec(content, keyst) { const bitArray = sjcl.codec.base64.toBits(content), bitArrayCopy = bitArray.slice(0), ivdec = bitArrayCopy.slice(0, 4), encryptedBitArray = bitArray.slice(4), key = sjcl.codec.base64.toBits(keyst), cipher = new sjcl.cipher.aes(key); try { const data = sjcl.mode.gcm.decrypt(cipher, encryptedBitArray, ivdec, {}, 128); return sjcl.codec.utf8String.fromBits(data); } catch (err) { console.error(err.name, err.message); return false } } // ============================================ // MISCELLANEOUS // ============================================ // Encodes a Nimiq transaction hash for use with Nimiq.watch function nimiq_hash(tx) { return encodeURIComponent(btoa(tx.match(/\w{2}/g).map(function(a) { return String.fromCharCode(parseInt(a, 16)); }).join(""))); } // ============================================ // SCRIPTHASH // ============================================ // Convert address to scripthash, with support for Bitcoin Cash addresses function address_to_scripthash(addr, currency) { const address = (currency === "bitcoin-cash") ? bch_legacy(addr) : addr; let script_pub_key; if (address.startsWith("bc1") || address.startsWith("tb1") || address.startsWith("ltc1")) { try { const decoded = bech32_decode(address); if (!decoded) throw new Error("Invalid bech32 address"); const program = convert5to8(decoded.data.slice(1)); if (!program) throw new Error("Invalid witness program"); if (decoded.data[0] === 1) { if (program.length !== 32) { throw new Error("Invalid Taproot program length: " + program.length); } script_pub_key = "5120" + program.map(function(b) { return b.toString(16).padStart(2, "0"); }).join(""); } else if (decoded.data[0] === 0) { if (program.length === 20) { script_pub_key = "0014" + program.map(function(b) { return b.toString(16).padStart(2, "0"); }).join(""); } else if (program.length === 32) { script_pub_key = "0020" + program.map(function(b) { return b.toString(16).padStart(2, "0"); }).join(""); } else { throw new Error("Invalid witness program length: " + program.length); } } else { throw new Error("Unsupported witness version"); } } catch (error) { throw new Error("Invalid bech32 address: " + error.message); } } else { try { const decoded = b58check_decode(address), version = decoded.slice(0, 2), hash = decoded.slice(2); if (version === "00" || version === "30") { script_pub_key = "76a914" + hash + "88ac"; } else if (version === "05" || version === "32") { script_pub_key = "a914" + hash + "87"; } else { throw new Error("Unsupported address version: " + version); } } catch (error) { throw new Error("Invalid base58 address: " + error.message); } } const script_hash = hmacsha(script_pub_key, "sha256", "hex"); return { "script_pub_key": script_pub_key, "hash": script_hash.match(/.{2}/g).reverse().join("") } } // Helper function for converting groups of 5 bits to 8 bits function convert5to8(data) { const acc = new Array(Math.floor(data.length * 5 / 8)); let index = 0, bits = 0, value = 0; for (let i = 0; i < data.length; ++i) { value = (value << 5) | data[i]; bits += 5; while (bits >= 8) { bits -= 8; acc[index] = (value >> bits) & 0xff; index += 1; } } if (bits >= 5 || ((value << (8 - bits)) & 0xff)) { return null; } return acc; } // ============================================ // COMPATIBILITY TESTING // ============================================ // Tests crypto.getRandomValues availability function test_crypto_api() { try { return !!(crypto && crypto.getRandomValues); } catch (e) { console.error("CryptoUtils test_crypto_api:", e.message); return false; } } // Tests BigInt functionality function test_bigint() { try { return typeof BigInt === "function" && BigInt("9007199254740991") + BigInt(1) === BigInt("9007199254740992"); } catch (e) { console.error("CryptoUtils test_bigint:", e.message); return false; } } // Tests secp256k1 private key to public key derivation function test_secp256k1() { try { return get_publickey(crypto_utils_const.test_privkey) === crypto_utils_const.test_pubkey; } catch (e) { console.error("CryptoUtils test_secp256k1:", e.message); return false; } } // Tests bech32 address encoding function test_bech32() { try { return pub_to_address_bech32("bc", crypto_utils_const.test_pubkey_bech32) === crypto_utils_const.test_address_bech32; } catch (e) { console.error("CryptoUtils test_bech32:", e.message); return false; } } // Tests Bitcoin Cash cashaddr encoding function test_cashaddr() { try { return pub_to_cashaddr(crypto_utils_const.test_legacy_address) === crypto_utils_const.test_address_cashaddr; } catch (e) { console.error("CryptoUtils test_cashaddr:", e.message); return false; } } // Tests keccak256 / Ethereum address derivation function test_keccak256() { try { return pub_to_eth_address(crypto_utils_const.test_pubkey_eth) === crypto_utils_const.test_address_eth; } catch (e) { console.error("CryptoUtils test_keccak256:", e.message); return false; } } // Tests AES encryption round-trip function test_aes() { try { const test_data = "crypto_utils_test", test_key = "0123456789abcdef0123456789abcdef", encrypted = aes_enc(test_data, test_key); return aes_dec(encrypted, test_key) === test_data; } catch (e) { console.error("CryptoUtils test_aes:", e.message); return false; } } // ============================================ // MODULE EXPORT // ============================================ const CryptoUtils = { // Library info VERSION: "1.1.0", // Curve parameters secp, CURVE: CURVE, // === Core Helpers === uint_8array, buffer, unbuffer, buf2hex, is_hex, str_pad, dec_to_hex, hex_to_dec, hex_to_number_string, hex_to_int, pad_binary, pad64, concat_bytes, encode_varint, // === SJCL Bit Operations === to_bits, hex_to_bits, from_bits, bit_length, concat_array, // === String Utilities === clean_string, normalize_string, split_words, join_words, // === Base Conversion === binary_string_to_word_array, byte_array_to_word_array, byte_array_to_binary_string, hex_string_to_binary_string, mnemonic_to_binary_string, // === Base58 === b58enc, b58enc_uint_array, b58dec, b58dec_uint_array, b58check_encode, b58check_decode, // === Bech32 === to_words, from_words, convert_bits, polymod, hrp_expand, verify_checksum, verify_checksum_with_type, create_checksum, bech32_encode, bech32_decode, bech32_dec_array, lnurl_decodeb32, // === Byte/Hex Conversion === hex_to_bytes, bytes_to_hex, hex_to_number, bytes_to_number, // === Elliptic Curve (secp256k1) === mod, weierstrass, egcd, invert, pow_mod, sqrt_mod, // === Key Operations === normalize_privatekey, get_publickey, priv_to_pub, privkey_wif, expand_pub, // === Hashing === hmacsha, hmac_bits, hmacsha_bits, sha_sub, keccak_256, hash160, nimiq_hash, // === Address Generation === pub_to_address, pub_to_address_bech32, hash160_to_address, pub_to_eth_address, to_checksum_address, // === Bitcoin Cash === pub_to_cashaddr, bch_cashaddr, bch_legacy, // === Encryption === aes_enc, aes_dec, // === Validation === address_to_scripthash, convert5to8, // === Constants === crypto_utils_const, // === Testing === test_crypto_api, test_bigint, test_secp256k1, test_bech32, test_cashaddr, test_keccak256, test_aes };