@scirexs/srp6a
Version:
SRP-6a (Secure Remote Password) implementation in TypeScript for browser and server.
322 lines (321 loc) • 10.4 kB
JavaScript
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;