mirror of
https://github.com/nvms/prsm.git
synced 2025-12-16 08:00:53 +00:00
187 lines
7.8 KiB
TypeScript
187 lines
7.8 KiB
TypeScript
import * as base32 from "hi-base32";
|
|
import * as crypto from "node:crypto";
|
|
|
|
export class InvalidOtpLengthError extends Error {}
|
|
export class InvalidSecretError extends Error {}
|
|
export class InvalidHashFunctionError extends Error {}
|
|
export class InvalidSecretStrengthError extends Error {}
|
|
|
|
class Otp {
|
|
static OTP_LENGTH_MIN = 6;
|
|
static OTP_LENGTH_MAX = 8;
|
|
static OTP_LENGTH_DEFAULT = 6;
|
|
static INTERVAL_LENGTH_DEFAULT = 30;
|
|
static EPOCH_DEFAULT = 0;
|
|
static HASH_FUNCTION_SHA_1 = 1;
|
|
static HASH_FUNCTION_SHA_256 = 2;
|
|
static HASH_FUNCTION_SHA_512 = 3;
|
|
static HASH_FUNCTION_DEFAULT = Otp.HASH_FUNCTION_SHA_1;
|
|
static SHARED_SECRET_STRENGTH_LOW = 1;
|
|
static SHARED_SECRET_STRENGTH_MODERATE = 2;
|
|
static SHARED_SECRET_STRENGTH_HIGH = 3;
|
|
|
|
/**
|
|
* Generates a shared secret using a specified strength.
|
|
*
|
|
* @param {number} strength - The strength of the shared secret, defaulting to Otp.SHARED_SECRET_STRENGTH_HIGH.
|
|
* This determines the number of bits used to generate the secret.
|
|
* @returns {string} - A base32 encoded string representing the generated shared secret.
|
|
* @throws {Error} - If the strength parameter is invalid or if there is an issue generating random bytes.
|
|
*/
|
|
static createSecret(strength: number = Otp.SHARED_SECRET_STRENGTH_HIGH): string {
|
|
const bits = this.determineBitsForSharedSecretStrength(strength);
|
|
const bytes = Math.ceil(bits / 8);
|
|
const buffer = crypto.randomBytes(bytes);
|
|
return base32.encode(buffer).replace(/=+$/, "");
|
|
}
|
|
|
|
/**
|
|
* Generates a TOTP (Time-based One-Time Password) Key URI for use in QR code generation.
|
|
* This URI can be scanned by authenticator apps like Google Authenticator or Authy.
|
|
*
|
|
* @param {string} issuer - The name of the service or organization issuing the OTP.
|
|
* @param {string} accountName - The account name or email address associated with the OTP.
|
|
* @param {string} secret - The shared secret key used for generating the OTP.
|
|
* @returns {string} - A URI formatted according to the otpauth URI scheme.
|
|
* @throws {Error} - Throws an error if any of the parameters are invalid or missing.
|
|
*/
|
|
static createTotpKeyUriForQrCode(issuer: string, accountName: string, secret: string): string {
|
|
return `otpauth://totp/${issuer}:${encodeURIComponent(accountName)}?secret=${secret}&issuer=${issuer}`;
|
|
}
|
|
|
|
/**
|
|
* Generates a Time-based One-Time Password (TOTP) using the provided secret and parameters.
|
|
*
|
|
* @param {string} secret - The shared secret key used for generating the TOTP. Must be at least 16 characters long.
|
|
* @param {number} [t=Math.floor(Date.now() / 1000)] - The current Unix time in seconds. Defaults to the current time.
|
|
* @param {number} [otpLength=Otp.OTP_LENGTH_DEFAULT] - The desired length of the OTP. Must be between Otp.OTP_LENGTH_MIN and Otp.OTP_LENGTH_MAX.
|
|
* @param {number} [t_x=Otp.INTERVAL_LENGTH_DEFAULT] - The time step in seconds. Defaults to Otp.INTERVAL_LENGTH_DEFAULT.
|
|
* @param {number} [t_0=Otp.EPOCH_DEFAULT] - The Unix time to start counting time steps. Defaults to Otp.EPOCH_DEFAULT.
|
|
* @param {number} [hashFunction=Otp.HASH_FUNCTION_DEFAULT] - The hash function to use (e.g., Otp.HASH_FUNCTION_SHA_1, Otp.HASH_FUNCTION_SHA_256, Otp.HASH_FUNCTION_SHA_512). Defaults to Otp.HASH_FUNCTION_DEFAULT.
|
|
* @returns {string} - The generated TOTP as a string of digits, padded to the specified length.
|
|
* @throws {InvalidOtpLengthError} - If the specified OTP length is not within the valid range.
|
|
* @throws {InvalidSecretError} - If the provided secret is less than 16 characters long.
|
|
* @throws {InvalidHashFunctionError} - If the specified hash function is not supported.
|
|
*/
|
|
static generateTotp(
|
|
secret: string,
|
|
t: number = Math.floor(Date.now() / 1000),
|
|
otpLength: number = Otp.OTP_LENGTH_DEFAULT,
|
|
t_x: number = Otp.INTERVAL_LENGTH_DEFAULT,
|
|
t_0: number = Otp.EPOCH_DEFAULT,
|
|
hashFunction: number = Otp.HASH_FUNCTION_DEFAULT,
|
|
): string {
|
|
if (otpLength < Otp.OTP_LENGTH_MIN || otpLength > Otp.OTP_LENGTH_MAX) {
|
|
throw new InvalidOtpLengthError();
|
|
}
|
|
|
|
secret = secret ? secret : "";
|
|
t = t ? t : Math.floor(Date.now() / 1000);
|
|
t_x = t_x ? t_x : Otp.INTERVAL_LENGTH_DEFAULT;
|
|
t_0 = t_0 ? t_0 : Otp.EPOCH_DEFAULT;
|
|
|
|
const c_t = Math.max(0, Math.floor((t - t_0) / t_x)); // Ensure c_t is non-negative
|
|
|
|
secret = secret.replace(/[^A-Za-z2-7]/g, "").toUpperCase();
|
|
|
|
if (secret.length < 16) {
|
|
throw new InvalidSecretError();
|
|
}
|
|
|
|
const k = base32.decode.asBytes(secret);
|
|
|
|
const counter64BitBigEndian = Buffer.alloc(8);
|
|
counter64BitBigEndian.writeUInt32BE(Math.floor(c_t / Math.pow(2, 32)), 0);
|
|
counter64BitBigEndian.writeUInt32BE(c_t % Math.pow(2, 32), 4);
|
|
|
|
let hashFunctionNameForHmac: string;
|
|
switch (hashFunction) {
|
|
case Otp.HASH_FUNCTION_SHA_1:
|
|
hashFunctionNameForHmac = "sha1";
|
|
break;
|
|
case Otp.HASH_FUNCTION_SHA_256:
|
|
hashFunctionNameForHmac = "sha256";
|
|
break;
|
|
case Otp.HASH_FUNCTION_SHA_512:
|
|
hashFunctionNameForHmac = "sha512";
|
|
break;
|
|
default:
|
|
throw new InvalidHashFunctionError();
|
|
}
|
|
|
|
const hmac = crypto.createHmac(hashFunctionNameForHmac, Buffer.from(k));
|
|
hmac.update(counter64BitBigEndian);
|
|
const mac = hmac.digest();
|
|
|
|
const offset = mac[mac.length - 1] & 0x0f;
|
|
const macSubstring4Bytes = mac.slice(offset, offset + 4);
|
|
|
|
const integer32Bit = macSubstring4Bytes.readUInt32BE(0) & 0x7fffffff;
|
|
|
|
const hotp = integer32Bit % Math.pow(10, otpLength);
|
|
|
|
return hotp.toString().padStart(otpLength, "0");
|
|
}
|
|
|
|
/**
|
|
* Verifies a Time-based One-Time Password (TOTP) against a given secret.
|
|
*
|
|
* @param {string} secret - The shared secret key used to generate the TOTP.
|
|
* @param {string} otpValue - The TOTP value to be verified.
|
|
* @param {number} [lookBehindSteps=2] - The number of time steps to look behind for a valid TOTP.
|
|
* @param {number} [lookAheadSteps=2] - The number of time steps to look ahead for a valid TOTP.
|
|
* @param {number} [t=Math.floor(Date.now() / 1000)] - The current Unix time in seconds.
|
|
* @param {number} [otpLength=Otp.OTP_LENGTH_DEFAULT] - The expected length of the TOTP.
|
|
* @param {number} [t_x=Otp.INTERVAL_LENGTH_DEFAULT] - The time step interval in seconds.
|
|
* @param {number} [t_0=Otp.EPOCH_DEFAULT] - The Unix epoch to start counting time steps from.
|
|
* @param {number} [hashFunction=Otp.HASH_FUNCTION_DEFAULT] - The hash function to use for generating the TOTP.
|
|
* @returns {boolean} - Returns true if the TOTP is valid, false otherwise.
|
|
* @throws {Error} - Throws an error if the OTP value length is not within the valid range.
|
|
*/
|
|
static verifyTotp(
|
|
secret: string,
|
|
otpValue: string,
|
|
lookBehindSteps: number = 2,
|
|
lookAheadSteps: number = 2,
|
|
t: number = Math.floor(Date.now() / 1000),
|
|
otpLength: number = Otp.OTP_LENGTH_DEFAULT,
|
|
t_x: number = Otp.INTERVAL_LENGTH_DEFAULT,
|
|
t_0: number = Otp.EPOCH_DEFAULT,
|
|
hashFunction: number = Otp.HASH_FUNCTION_DEFAULT,
|
|
): boolean {
|
|
otpValue = otpValue.replace(/[^0-9]/g, "");
|
|
|
|
if (otpValue.length < Otp.OTP_LENGTH_MIN || otpValue.length > Otp.OTP_LENGTH_MAX) {
|
|
return false;
|
|
}
|
|
|
|
if (otpValue.length !== otpLength) {
|
|
return false;
|
|
}
|
|
|
|
for (let s = -lookBehindSteps; s <= lookAheadSteps; s++) {
|
|
const expectedOtpValue = this.generateTotp(secret, t + t_x * s, otpLength, t_x, t_0, hashFunction);
|
|
if (crypto.timingSafeEqual(Buffer.from(expectedOtpValue), Buffer.from(otpValue))) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static determineBitsForSharedSecretStrength(strength: number): number {
|
|
switch (strength) {
|
|
case 1:
|
|
return 80;
|
|
case 2:
|
|
return 128;
|
|
case 3:
|
|
return 160;
|
|
default:
|
|
throw new InvalidSecretStrengthError();
|
|
}
|
|
}
|
|
}
|
|
|
|
export default Otp;
|