@scirexs/srp6a
Version:
SRP-6a (Secure Remote Password) implementation in TypeScript for browser and server.
106 lines (105 loc) • 4.97 kB
JavaScript
export { addRandomDelay, calculateVerifier, computeClientEvidence, computeClientKey, computeIdentity, computeMultiplier, computeScramblingParameter, computeSecret, computeServerEvidence, computeServerKey, generateKeyPair, generateRandomKey, generateSalt, generateServerKeyPair, isValidPublic, };
import { computeHash, CryptoNumber, generateSecureRandom } from "./crypto.js";
/** s = RAND() */
function generateSalt(config) {
return generateSecureRandom(config.salt);
}
/** I = H(U | ":" | p) */
async function computeIdentity(username, password, config) {
if (!isValidIdentitySource(username, password))
throw new Error("Username and password must have length.");
return await computeHash(new TextEncoder().encode(`${username}:${password}`), config);
}
/** x = H(s | I) */
async function computeSecret(salt, identity, config) {
return await computeHash(CryptoNumber.concat(salt, identity), config);
}
/** v = MP(g, x, N) */
function calculateVerifier(secret, config) {
return CryptoNumber.modPow(config.generator, secret, config.prime);
}
/** k = H(N | PAD(g)) */
async function computeMultiplier(config) {
const num = config.multiplier ? new CryptoNumber(config.multiplier) : CryptoNumber.concat(config.prime, config.generator.pad());
return await computeHash(num, config);
}
/** b, B = k * v + MP(g, b, N) */
function generateServerKeyPair(multiplier, verifier, config) {
const pair = generateKeyPair(config);
pair.public = new CryptoNumber((multiplier.int * verifier.int + pair.public.int) % config.prime.int); // to prevent exceed prime
return pair;
}
/** a, A = MP(g, a, N) */
function generateKeyPair(config) {
const pvt = generateRandomKey(config);
return {
private: pvt,
public: CryptoNumber.modPow(config.generator, pvt, config.prime),
};
}
/** a = RAND(), b = RAND() */
function generateRandomKey(config) {
let result;
do {
const array = generateSecureRandom(config.hashBytes);
result = array.int % config.prime.int;
} while (result === 0n);
return new CryptoNumber(result);
}
/** u = H(PAD(A) | PAD(B)) */
async function computeScramblingParameter(client, server, config) {
return await computeHash(CryptoNumber.concat(client.pad(), server.pad()), config);
}
/** Kc = H(Sc) */
async function computeClientKey(server, multiplier, secret, pvt, scrambling, config) {
return await computeHash(calculateClientSession(server, multiplier, secret, pvt, scrambling, config), config);
}
/** Sc = MP(B - (k * MP(g, x, N)), a + (u * x), N) */
function calculateClientSession(server, multiplier, secret, pvt, scrambling, config) {
const kgx = (multiplier.int * CryptoNumber.modPow(config.generator, secret, config.prime).int) % config.prime.int;
const base = (server.int - kgx + config.prime.int) % config.prime.int; // to prevent negative, add prime
const pow = (pvt.int + (scrambling.int * secret.int)) % config.prime.int; // to prevent exceed prime
return CryptoNumber.modPow(base, pow, config.prime);
}
/** Mc = H(H(N) xor H(g), H(U), s, A, B, K) */
async function computeClientEvidence(username, salt, client, server, key, config) {
const primeHash = await computeHash(config.prime, config);
const generatorHash = await computeHash(config.generator, config);
const xor = CryptoNumber.xor(primeHash, generatorHash);
const usernameHash = await computeHash(new TextEncoder().encode(username), config);
return await computeHash(CryptoNumber.concat(xor, usernameHash, salt, client, server, key), config);
}
/** Ks = H(Ss) */
async function computeServerKey(client, verifier, scrambling, pvt, config) {
return await computeHash(calculateServerSession(client, verifier, scrambling, pvt, config), config);
}
/** Ss = MP(A * MP(v, u, N), b, N) */
function calculateServerSession(client, verifier, scrambling, pvt, config) {
const base = (client.int * CryptoNumber.modPow(verifier, scrambling, config.prime).int) % config.prime.int;
return CryptoNumber.modPow(base, pvt, config.prime);
}
/** Ms = H(A, Mc, K) */
async function computeServerEvidence(client, evidence, key, config) {
return await computeHash(CryptoNumber.concat(client, evidence, key), config);
}
/** Confirm username and password is not empty. */
function isValidIdentitySource(username, password) {
return Boolean(username) && Boolean(password);
}
/** Confirm public key is valid or not. */
function isValidPublic(pub, config) {
return pub.int % config.prime.int !== 0n && 1n <= pub.int && pub.int < config.prime.int;
}
/** Add random delay to fail authentication. */
async function addRandomDelay(ms = 5) {
ms = Math.ceil(Math.abs(ms));
ms = ms <= 1 ? 5 : ms;
const delay = (Math.random() * (ms - 1)) + 1;
await new Promise((resolve) => setTimeout(resolve, delay));
}
// export const __internal = Deno?.args?.includes("test")
// ? {
// calculateClientSession,
// calculateServerSession,
// }
// : {};