@scirexs/srp6a
Version:
SRP-6a (Secure Remote Password) implementation in TypeScript for browser and server.
193 lines (192 loc) • 8.04 kB
JavaScript
export { addRandomDelay, authenticate, createDummyHello, createServerHello, extractClientHello, extractLoginEvidence, extractSignupCredentials, };
import { CryptoNumber } from "../shared/crypto.js";
import { addRandomDelay, computeClientEvidence, computeMultiplier, computeScramblingParameter, computeServerEvidence, computeServerKey, generateRandomKey, generateSalt, generateServerKeyPair, isValidPublic, } from "../shared/functions.js";
/**
* Creates server hello response for SRP6a authentication.
* Generates server key pair and returns hello message along with the key pair for session use.
*
* @param salt - Salt value from user registration (string or CryptoNumber)
* @param verifier - Password verifier from user registration (string or CryptoNumber)
* @param config - SRP configuration object
* @returns Promise that resolves to tuple containing [ServerHello message, KeyPair for the session]
*
* @example
* ```ts
* const userRecord = getUserFromDatabase(username);
* const [hello, keyPair] = await createServerHello(userRecord.salt, userRecord.verifier, config);
* // Send hello to client, store keyPair for authentication
* ```
*/
async function createServerHello(salt, verifier, config) {
salt = typeof salt === "string" ? salt : salt.hex;
verifier = typeof verifier === "string" ? new CryptoNumber(verifier) : verifier;
const multiplier = await computeMultiplier(config);
const pair = generateServerKeyPair(multiplier, verifier, config);
return [
{ salt, server: pair.public.hex },
{ private: pair.private.hex, public: pair.public.hex },
];
}
/**
* Creates a dummy server hello response.
* Generates a dummy salt and server key to prevent user enumeration attacks.
*
* @param config - SRP configuration object
* @returns Dummy ServerHello message
*
* @example
* ```ts
* const userRecord = getUserFromDatabase(username);
* if (!userRecord) {
* const dummy = await createDummyHello(config);
* addRandomDelay();
* // Send dummy response to client
* }
* ```
*/
function createDummyHello(config) {
return {
salt: generateSalt(config).hex,
server: generateRandomKey(config).hex,
};
}
/**
* Authenticates client evidence and generates server evidence for mutual authentication.
* Verifies client's knowledge of password and computes server evidence if authentication succeeds.
*
* @param username - The username attempting to authenticate
* @param salt - Salt value from user registration (string or CryptoNumber)
* @param verifier - Password verifier from user registration (string or CryptoNumber)
* @param pair - Server's key pair from hello phase
* @param client - Client's public key from hello phase (string or CryptoNumber)
* @param evidence - Client evidence to verify (string or CryptoNumber)
* @param config - SRP configuration object
* @returns Promise that resolves to AuthResult with result status and server evidence
* @throws Error if client's public key is invalid
*
* @example
* ```ts
* const result = await authenticate(
* username,
* userRecord.salt,
* userRecord.verifier,
* serverKeyPair,
* clientPublicKey,
* clientEvidence,
* config
* );
*
* if (result.success) {
* // Authentication successful, send server evidence
* return { success: true, evidence: result.evidence };
* } else {
* // Authentication failed
* return { success: false, evidence: "" };
* }
* ```
*/
async function authenticate(username, salt, verifier, pair, client, evidence, config) {
salt = typeof salt === "string" ? new CryptoNumber(salt) : salt;
verifier = typeof verifier === "string" ? new CryptoNumber(verifier) : verifier;
client = typeof client === "string" ? new CryptoNumber(client) : client;
evidence = typeof evidence === "string" ? new CryptoNumber(evidence) : evidence;
const pubServer = typeof pair.public === "string" ? new CryptoNumber(pair.public) : pair.public;
const pvtServer = typeof pair.private === "string" ? new CryptoNumber(pair.private) : pair.private;
if (!isValidPublic(client, config))
throw new Error("Random public key from client is invalid.");
const scrambling = await computeScramblingParameter(client, pubServer, config);
const key = await computeServerKey(client, verifier, scrambling, pvtServer, config);
const authEvidence = await computeClientEvidence(username, salt, client, pubServer, key, config);
const success = CryptoNumber.compare(authEvidence, evidence);
if (!success) {
await addRandomDelay();
return { success, evidence: "" };
}
const serverEvidence = await computeServerEvidence(client, evidence, key, config);
return {
success,
evidence: serverEvidence.hex,
};
}
/**
* Extracts client signup information from HTTP request, if client used this library.
* Parses the request body and validates that it contains required username and credential properties.
*
* @param request - HTTP request containing signup credential data
* @returns Promise that resolves to SignupCredentials object containing username and credential data
* @throws Error if request is not POST, not JSON, or missing required properties
*
* @example
* ```ts
* // In your HTTP handler
* const { username, salt, verifier } = await extractSignupCredentials(request);
* storeDatabase(username, salt, verifier);
* ```
*/
async function extractSignupCredentials(request) {
return await getTypedObjectFromResponse(request, "username", "salt", "verifier");
}
/**
* Extracts client hello information from HTTP request, if client used this library.
* Parses the request body and validates that it contains required username and client properties.
*
* @param request - HTTP request containing client hello data
* @returns Promise that resolves to ClientHello object containing username and client public key
* @throws Error if request is not POST, not JSON, or missing required properties
*
* @example
* ```ts
* // In your HTTP handler
* const { username, client } = await extractClientHello(request);
* const userRecord = getUserFromDatabase(username);
* const [hello, keyPair] = await createServerHello(userRecord.salt, userRecord.verifier, config);
* ```
*/
async function extractClientHello(request) {
return await getTypedObjectFromResponse(request, "username", "client");
}
/**
* Extracts login evidence from HTTP request, if client used this library.
* Parses the request body and validates that it contains required username and evidence properties.
*
* @param request - HTTP request containing login evidence data
* @returns Promise that resolves to LoginEvidence object containing username and client evidence
* @throws Error if request is not POST, not JSON, or missing required properties
*
* @example
* ```ts
* // In your HTTP handler
* const { username, evidence } = await extractLoginEvidence(request);
* const userRecord = getUserFromDatabase(username);
* const result = await authenticate(
* username,
* userRecord.salt,
* userRecord.verifier,
* storedKeyPair,
* storedClientPublicKey,
* evidence,
* config
* );
* ```
*/
async function extractLoginEvidence(request) {
return await getTypedObjectFromResponse(request, "username", "evidence");
}
// deno-lint-ignore no-explicit-any
async function getTypedObjectFromResponse(request, ...props) {
if (request.method !== "POST")
throw new Error("Request must be post method.");
if (request.headers.get("Content-Type") !== "application/json")
throw new Error("Request is not json type.");
const data = await request.json();
if (typeof data !== "object" || Array.isArray(data))
throw new Error("Request 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 request.");
}
}