@scirexs/srp6a
Version:
SRP-6a (Secure Remote Password) implementation in TypeScript for browser and server.
166 lines (165 loc) • 6.76 kB
JavaScript
export { createEvidence, createLoginHello, createUserCredentials, extractLoginResult, extractServerHello, verifyServer };
import { CryptoNumber } from "../shared/crypto.js";
import { calculateVerifier, computeClientEvidence, computeClientKey, computeIdentity, computeMultiplier, computeScramblingParameter, computeSecret, computeServerEvidence, generateKeyPair, generateSalt, isValidPublic, } from "../shared/functions.js";
/**
* Creates user credentials for SRP6a signup process.
* Generates salt, computes identity and secret, then calculates verifier.
*
* @param username - The username for the account
* @param password - The password for the account
* @param config - SRP configuration object
* @returns Promise that resolves to signup credentials containing username, salt, and verifier
*
* @example
* ```ts
* const credentials = await createUserCredentials("user123", "password123", config);
* console.log(credentials); // { username: "user123", salt: "...", verifier: "..." }
* ```
*/
async function createUserCredentials(username, password, config) {
const salt = generateSalt(config);
const identity = await computeIdentity(username, password, config);
const secret = await computeSecret(salt, identity, config);
const verifier = calculateVerifier(secret, config);
return {
username,
salt: salt.hex,
verifier: verifier.hex,
};
}
/**
* Creates the initial client hello message for SRP6a login process.
* Generates a key pair and returns both the hello message and the key pair.
*
* @param username - The username attempting to login
* @param config - SRP configuration object
* @returns Tuple containing [ClientHello message, KeyPair for the session]
*
* @example
* ```ts
* const [hello, keyPair] = createLoginHello("user123", config);
* // Send hello to server, keep keyPair for evidence creation
* ```
*/
function createLoginHello(username, config) {
const pair = generateKeyPair(config);
return [
{ username, client: pair.public.hex },
{ private: pair.private.hex, public: pair.public.hex },
];
}
/**
* Creates login evidence after receiving server hello response.
* Computes client evidence and expected server evidence for mutual authentication.
*
* @param username - The username attempting to login
* @param password - The password for authentication
* @param salt - Salt value from server
* @param server - Server's public key
* @param pair - Client's key pair from login hello
* @param config - SRP configuration object
* @returns Promise that resolves to tuple containing [LoginEvidence, expected server evidence]
* @throws Error if server's public key is invalid
*
* @example
* ```ts
* const [evidence, expected] = await createEvidence(
* "user123",
* "password123",
* serverSalt,
* serverPublicKey,
* clientKeyPair,
* config
* );
* ```
*/
async function createEvidence(username, password, salt, server, pair, config) {
salt = new CryptoNumber(salt);
server = new CryptoNumber(server);
const pubClient = new CryptoNumber(pair.public);
const pvtClient = new CryptoNumber(pair.private);
if (!isValidPublic(server, config))
throw new Error("Random public key from server is invalid.");
const identity = await computeIdentity(username, password, config);
const secret = await computeSecret(salt, identity, config);
const scrambling = await computeScramblingParameter(pubClient, server, config);
const multiplier = await computeMultiplier(config);
const key = await computeClientKey(server, multiplier, secret, pvtClient, scrambling, config);
const evidence = await computeClientEvidence(username, salt, pubClient, server, key, config);
const expected = await computeServerEvidence(pubClient, evidence, key, config);
return [
{ evidence: evidence.hex },
expected.hex,
];
}
/**
* Verifies server evidence against expected value for mutual authentication.
* Ensures that the server has knowledge of the shared secret by comparing evidence values.
*
* @param expected - The expected server evidence computed by client
* @param evidence - The actual server evidence received from server
* @returns True if server evidence matches expected value, false otherwise
*
* @example
* ```ts
* const isServerValid = verifyServer(expectedEvidence, serverEvidence);
* if (!isServerValid) {
* throw new Error("Server authentication failed");
* }
* ```
*/
function verifyServer(expected, evidence) {
return expected === evidence;
}
/**
* Extracts server hello information from HTTP response, if server used this library.
* Parses the response and validates that it contains required salt and server properties.
*
* @param response - HTTP response from server hello endpoint
* @returns Promise that resolves to ServerHello object containing salt and server public key
* @throws Error if response is not OK, not JSON, or missing required properties
*
* @example
* ```ts
* const response = await fetch("/api/login/hello", { method: "POST", ... });
* const { salt, server } = await extractServerHello(response);
* ```
*/
async function extractServerHello(response) {
return await getTypedObjectFromResponse(response, "salt", "server");
}
/**
* Extracts login result from HTTP response after evidence submission, if server used this library.
* Parses the response and validates that it contains required result and evidence properties.
*
* @param response - HTTP response from login evidence endpoint
* @returns Promise that resolves to AuthResult object containing result status and server evidence
* @throws Error if response is not OK, not JSON, or missing required properties
*
* @example
* ```ts
* const response = await fetch("/api/login/evidence", { method: "POST", ... });
* const { result, evidence } = await extractLoginResult(response);
* ```
*/
async function extractLoginResult(response) {
return await getTypedObjectFromResponse(response, "success", "evidence");
}
// deno-lint-ignore no-explicit-any
async function getTypedObjectFromResponse(response, ...props) {
if (!response.ok)
throw new Error("Request is failed.");
if (response.headers.get("Content-Type") !== "application/json")
throw new Error("Response is not json type.");
const data = await response.json();
if (typeof data !== "object" || Array.isArray(data))
throw new Error("Response has invalid data.");
checkRequiredProperties(data, ...props);
return data;
}
function checkRequiredProperties(obj, ...props) {
for (const prop of props) {
if (!Object.hasOwn(obj, prop))
throw new Error("Required properties are not exist in response.");
}
}