UNPKG

@scirexs/srp6a

Version:

SRP-6a (Secure Remote Password) implementation in TypeScript for browser and server.

322 lines (321 loc) 10.4 kB
var _a; export { computeHash, CryptoNumber, generateSecureRandom, getDefaultConfig, SRPConfig }; import { GROUP_2048, SHA_256 } from "./constants.js"; // Works with Web Crypto API like brower, Node.js, Deno. const crypto = globalThis?.crypto; if (!crypto || !crypto.subtle) throw new Error("Could not find `globalThis.crypto` with Web Crypto API."); /** * Configuration class for SRP6a authentication protocol * Combines security group and hash settings to provide various parameters required for authentication */ class SRPConfig { #group; #hash; #prime; #generator; /** * Gets the prime number from the security group * @returns {CryptoNumber} Prime number value */ get prime() { return this.#prime; } /** * Gets the generator from the security group * @returns {CryptoNumber} Generator value */ get generator() { return this.#generator; } /** * Gets the bit length of the security group * @returns {number} Bit length */ get length() { return this.#group.length; } /** * Gets the multiplier from the security group * @returns {string} Multiplier value */ get multiplier() { return this.#group.multiplier; } /** * Gets the hash algorithm * @returns {HashAlgorithm} Hash algorithm */ get algorithm() { return this.#hash.algorithm; } /** * Gets the number of bytes in the hash value * @returns {number} Number of bytes in hash value */ get hashBytes() { return this.#hash.bytes; } /** * Gets the salt bit length * @returns {number} Salt bit length */ get salt() { return this.#hash.salt; } /** * Creates an instance of SRPConfig * @param {SRPSecurityGroup} group - Security group configuration * @param {SRPHashConfig} hash - Hash configuration */ constructor(group, hash) { this.#group = group; this.#hash = hash; CryptoNumber.PAD_LEN = Math.ceil(group.length / 4); // pad length as hex string this.#prime = new CryptoNumber(group.prime); this.#generator = new CryptoNumber(group.generator); } } /** * Gets the default SRP configuration * Returns default configuration using GROUP_2048 and SHA_256 * @returns {SRPConfig} Default SRP configuration */ function getDefaultConfig() { return new SRPConfig(GROUP_2048, SHA_256); } async function computeHash(num, config) { num = num instanceof CryptoNumber ? num.buf : num; return new CryptoNumber(new Uint8Array(await crypto.subtle.digest(config.algorithm, num))); } function generateSecureRandom(bytes) { const result = new Uint8Array(bytes); crypto.getRandomValues(result); return new CryptoNumber(result); } /** * Class representing numbers used in cryptographic operations * Manages numbers in three formats: bigint, hex string, and Uint8Array, * with lazy conversion performed as needed */ class CryptoNumber { /** Padding length for hex strings (no need to use) */ static PAD_LEN = 0; #int; #hex = ""; #buf; /** * Gets the number in bigint format (lazy evaluation) * @returns {bigint} Number in bigint format */ get int() { if (this.#int === undefined) this.#int = this.#deriveInt(); return this.#int; } /** * Gets the number in hex string format (lazy evaluation) * @returns {string} Number in hex string format */ get hex() { if (!this.#hex) this.#hex = this.#deriveHex(); return this.#hex; } /** * Gets the number in Uint8Array format (lazy evaluation) * @returns {Uint8Array} Number in Uint8Array format */ get buf() { if (!this.#buf) this.#buf = this.#deriveBuf(); return this.#buf; } /** * Creates an instance of CryptoNumber * @param {bigint | string | Uint8Array} value - Initial value (bigint, hex string, or Uint8Array) * @throws {Error} If PAD_LEN is not initialized */ constructor(value) { if (_a.PAD_LEN <= 0) throw new Error("PAD_BYTES must be initialized before use."); switch (typeof value) { case "bigint": this.#int = value; break; case "string": this.#hex = _a.#guardHex(value); break; case "object": this.#buf = value; break; } } /** * Returns a new CryptoNumber with hex string left-padded with zeros to the specified length * @param {number} [len] - Padding length (uses PAD_LEN if omitted) * @returns {CryptoNumber} New CryptoNumber with padded value */ pad(len) { return new _a(this.hex.padStart(len ?? _a.PAD_LEN, "0")); } /** * Clears the internal Uint8Array buffer by filling it with zeros * Used to securely erase sensitive data */ clear() { if (!this.#buf) return; this.#buf.fill(0); this.#buf = undefined; } /** Derives bigint from internal state */ #deriveInt() { return this.#hex ? this.#castHex2Int() : this.#castBuf2Int(); } /** Derives hex string from internal state */ #deriveHex() { const str = this.#buf ? this.#castBuf2Hex() : this.#castInt2Hex(); return _a.#guardHex(str); } /** Derives Uint8Array from internal state */ #deriveBuf() { return this.#hex ? this.#castHex2Buf() : this.#castInt2Buf(); } /** Converts bigint to hex string */ #castInt2Hex() { if (this.#int === undefined) _a.#castError(); return _a.#padHexString(this.#int.toString(16)); } /** Converts bigint to Uint8Array */ #castInt2Buf() { if (this.#int === undefined) _a.#castError(); return new Uint8Array(this.hex.match(/.{2}/g).map((x) => parseInt(x, 16))); } /** Converts hex string to bigint */ #castHex2Int() { if (!this.#hex) _a.#castError(); return BigInt(`0x${this.#hex}`); } /** Converts hex string to Uint8Array */ #castHex2Buf() { if (!this.#hex) _a.#castError(); return new Uint8Array(this.#hex.match(/.{2}/g).map((x) => parseInt(x, 16))); } /** Converts Uint8Array to bigint */ #castBuf2Int() { if (!this.#buf) _a.#castError(); return BigInt(`0x${this.hex}`); } /** Converts Uint8Array to hex string */ #castBuf2Hex() { if (!this.#buf) _a.#castError(); return Array.from(this.#buf) .map((x) => x.toString(16).padStart(2, "0")) .join(""); } /** Throws a conversion error */ static #castError() { throw new Error("Can't cast from empty."); } /** Validates and pads hex string */ static #guardHex(str) { if (!_a.#isValidHexString(str)) throw new Error("Contains invalid characters as hexadecimal."); return _a.#padHexString(str); } /** Checks if string is a valid hex string */ static #isValidHexString(str) { return /^[0-9a-fA-F]+$/.test(str); } /** Pads hex string to even length */ static #padHexString(str) { return str.length % 2 === 0 ? str : "0" + str; } /** * Efficiently calculates modular exponentiation (base^pow mod mod) * @param {CryptoNumber | bigint} base - Base value * @param {CryptoNumber | bigint} pow - Exponent value * @param {CryptoNumber} mod - Modulus value * @returns {CryptoNumber} Result of modular exponentiation * @throws {Error} If arguments are invalid */ static modPow(base, pow, mod) { base = typeof base === "object" ? base.int : base; pow = typeof pow === "object" ? pow.int : pow; if (base < 0n) throw new Error(`Invalid base: ${base.toString()}`); if (pow < 0n) throw new Error(`Invalid power: ${pow.toString()}`); if (mod.int < 1n) throw new Error(`Invalid modulo: ${mod.int.toString()}`); let result = 1n; base = base % mod.int; while (pow > 0n) { if (pow % 2n === 1n) result = (result * base) % mod.int; base = (base * base) % mod.int; pow /= 2n; } return new _a(result); } /** * Concatenates multiple CryptoNumber buffers * @param {...CryptoNumber} nums - CryptoNumbers to concatenate * @returns {CryptoNumber} Concatenated CryptoNumber */ static concat(...nums) { const len = nums.reduce((sum, num) => sum + num.buf.length, 0); const result = new Uint8Array(len); let offset = 0; for (const num of nums) { result.set(num.buf, offset); offset += num.buf.length; } return new _a(result); } /** * Performs XOR operation on two CryptoNumbers * @param {CryptoNumber} a - First operand * @param {CryptoNumber} b - Second operand * @returns {CryptoNumber} XOR operation result * @throws {Error} If buffer lengths differ */ static xor(a, b) { if (a.buf.length !== b.buf.length) throw new Error("Uint8Array length must be same."); const aBuf = a.buf; const bBuf = b.buf; const result = new Uint8Array(aBuf.length); for (let i = 0; i < aBuf.length; i++) { result[i] = aBuf[i] ^ bBuf[i]; } return new _a(result); } /** * Compares two CryptoNumbers in constant time * Used to prevent timing attacks * @param {CryptoNumber} a - First comparison target * @param {CryptoNumber} b - Second comparison target * @returns {boolean} True if values are equal */ static compare(a, b) { const aBuf = a.buf; const bBuf = b.buf; const len = Math.max(aBuf.length, bBuf.length); let result = aBuf.length ^ bBuf.length; for (let i = 0; i < len; i++) { const aVal = i < aBuf.length ? aBuf[i] : 0; const bVal = i < bBuf.length ? bBuf[i] : 0; result |= aVal ^ bVal; } return result === 0; } } _a = CryptoNumber;