// SPDX-License-Identifier: Apache-2.0
// Copyright 2024-2026 Vorion LLC
/**
 * TOTP (Time-based One-Time Password) MFA Implementation for Vorion Platform
 *
 * Provides secure two-factor authentication using TOTP standard (RFC 6238).
 * Compatible with Google Authenticator, Authy, Microsoft Authenticator, and other TOTP apps.
 *
 * @module auth/mfa/totp
 */
import { createHash, randomBytes } from 'node:crypto';
import { authenticator } from 'otplib';
import { toDataURL } from 'qrcode';
import { createLogger } from '../../common/logger.js';
const logger = createLogger({ component: 'totp-service' });
/**
 * Default TOTP configuration optimized for security while maintaining broad compatibility.
 * SHA256 is recommended by NIST and supported by most modern authenticator apps.
 * Falls back to SHA1 only if explicitly configured for legacy compatibility.
 */
const DEFAULT_CONFIG = {
    issuer: 'Vorion',
    algorithm: 'SHA256',
    digits: 6,
    period: 30,
    window: 1,
};
/**
 * Service for managing TOTP-based multi-factor authentication.
 *
 * Provides methods for generating secrets, verifying tokens, managing backup codes,
 * and generating QR codes for authenticator app enrollment.
 *
 * @example
 * ```typescript
 * const totpService = getTOTPService();
 *
 * // Generate a new secret for user enrollment
 * const secret = totpService.generateTOTPSecret('user123', 'user@example.com');
 *
 * // Generate QR code for authenticator app scanning
 * const qrCodeDataUrl = await totpService.generateQRCode(secret.otpauthUrl);
 *
 * // Verify a token from the user's authenticator app
 * const isValid = await totpService.verifyToken(secret.secret, '123456');
 * ```
 */
export class TOTPService {
    config;
    /**
     * Creates a new TOTPService instance.
     *
     * @param config - Partial configuration to override defaults
     */
    constructor(config = {}) {
        this.config = { ...DEFAULT_CONFIG, ...config };
        logger.info('TOTPService initialized', { issuer: this.config.issuer });
    }
    /**
     * Generates a new TOTP secret for user enrollment.
     *
     * Creates a cryptographically secure random secret and formats it as a
     * base32 string along with an otpauth:// URL for QR code generation.
     *
     * @param userId - Unique identifier for the user
     * @param email - User's email address (used as account name in authenticator apps)
     * @returns TOTPSecret containing the secret and otpauth URL
     *
     * @example
     * ```typescript
     * const secret = totpService.generateTOTPSecret('user123', 'user@example.com');
     * // secret.secret: 'JBSWY3DPEHPK3PXP...'
     * // secret.otpauthUrl: 'otpauth://totp/Vorion:user@example.com?secret=...'
     * ```
     */
    generateTOTPSecret(userId, email) {
        // Generate a cryptographically secure random secret
        const secret = authenticator.generateSecret();
        // Build the otpauth URL for QR code generation
        // keyuri(accountName, issuer, secret) - returns otpauth:// URL
        const otpauthUrl = authenticator.keyuri(email, this.config.issuer, secret);
        logger.debug('Generated TOTP secret', { userId, email });
        return {
            secret,
            otpauthUrl,
        };
    }
    /**
     * Generates a QR code image as a data URL for authenticator app enrollment.
     *
     * The returned data URL can be directly used as an image src attribute
     * or embedded in HTML/email for user scanning.
     *
     * @param otpauthUrl - The otpauth:// URL to encode in the QR code
     * @returns Promise resolving to a data URL (base64 PNG image)
     *
     * @example
     * ```typescript
     * const dataUrl = await totpService.generateQRCode(secret.otpauthUrl);
     * // Returns: 'data:image/png;base64,iVBORw0KGgo...'
     * ```
     */
    async generateQRCode(otpauthUrl) {
        try {
            const dataUrl = await toDataURL(otpauthUrl, {
                errorCorrectionLevel: 'M',
                type: 'image/png',
                margin: 2,
                width: 256,
            });
            logger.debug('Generated QR code for TOTP enrollment');
            return dataUrl;
        }
        catch (error) {
            logger.error('Failed to generate QR code', { error });
            throw new Error('Failed to generate QR code for TOTP enrollment');
        }
    }
    /**
     * Verifies a TOTP token against the stored secret.
     *
     * Accounts for clock drift by allowing tokens within the configured
     * time window (default: 1 period before/after current time).
     *
     * @param secret - The base32 encoded secret associated with the user
     * @param token - The 6 or 8 digit token from the authenticator app
     * @returns Promise resolving to true if the token is valid, false otherwise
     *
     * @example
     * ```typescript
     * const isValid = await totpService.verifyToken(userSecret, '123456');
     * if (isValid) {
     *   // Grant access
     * }
     * ```
     */
    async verifyToken(secret, token) {
        try {
            // Ensure token is the expected length
            if (token.length !== this.config.digits) {
                logger.debug('Token verification failed: invalid length', {
                    expected: this.config.digits,
                    received: token.length,
                });
                return false;
            }
            // Ensure token contains only digits
            if (!/^\d+$/.test(token)) {
                logger.debug('Token verification failed: non-numeric characters');
                return false;
            }
            // Configure authenticator options before verification
            authenticator.options = {
                digits: this.config.digits,
                step: this.config.period,
                window: this.config.window,
            };
            // check() returns boolean directly in otplib v12
            const isValid = authenticator.check(token, secret);
            logger.debug('Token verification completed', { valid: isValid });
            return isValid;
        }
        catch (error) {
            logger.error('Token verification error', { error });
            return false;
        }
    }
    /**
     * Generates cryptographically secure backup codes for account recovery.
     *
     * Backup codes are formatted as readable groups (e.g., 'XXXX-XXXX')
     * and should be hashed before storage using hashBackupCode().
     *
     * @param count - Number of backup codes to generate (default: 10)
     * @returns Array of plain-text backup codes to show to the user once
     *
     * @example
     * ```typescript
     * const codes = totpService.generateBackupCodes(10);
     * // Returns: ['A1B2-C3D4', 'E5F6-G7H8', ...]
     *
     * // Hash codes before storing
     * const hashedCodes = codes.map(code => totpService.hashBackupCode(code));
     * ```
     */
    generateBackupCodes(count = 10) {
        const codes = [];
        const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
        for (let i = 0; i < count; i++) {
            // Generate 8 random bytes for each code
            const bytes = randomBytes(8);
            let code = '';
            // Convert bytes to alphanumeric characters
            for (let j = 0; j < 8; j++) {
                code += characters[bytes[j] % characters.length];
            }
            // Format as XXXX-XXXX for readability
            codes.push(`${code.slice(0, 4)}-${code.slice(4, 8)}`);
        }
        logger.debug('Generated backup codes', { count });
        return codes;
    }
    /**
     * Hashes a backup code for secure storage.
     *
     * Uses SHA-256 to create a one-way hash of the backup code.
     * The plain-text code should never be stored.
     *
     * @param code - The plain-text backup code to hash
     * @returns SHA-256 hash of the backup code (hex encoded)
     *
     * @example
     * ```typescript
     * const hashed = totpService.hashBackupCode('A1B2-C3D4');
     * // Store 'hashed' in database, never store plain-text code
     * ```
     */
    hashBackupCode(code) {
        // Normalize the code (uppercase, remove formatting)
        const normalizedCode = code.toUpperCase().replace(/[^A-Z0-9]/g, '');
        return createHash('sha256').update(normalizedCode).digest('hex');
    }
    /**
     * Verifies a backup code against a list of hashed codes.
     *
     * If valid, returns the index of the used code so it can be removed
     * from the stored list (backup codes are single-use).
     *
     * @param code - The plain-text backup code entered by the user
     * @param hashedCodes - Array of SHA-256 hashed backup codes from storage
     * @returns Object indicating validity and the index of the matched code
     *
     * @example
     * ```typescript
     * const result = totpService.verifyBackupCode('A1B2-C3D4', storedHashedCodes);
     * if (result.valid) {
     *   // Remove used code from storage
     *   storedHashedCodes.splice(result.usedIndex!, 1);
     *   // Grant access
     * }
     * ```
     */
    verifyBackupCode(code, hashedCodes) {
        const hashedInput = this.hashBackupCode(code);
        // Use timing-safe comparison to prevent timing attacks
        for (let i = 0; i < hashedCodes.length; i++) {
            const storedHash = hashedCodes[i];
            // Compare hashes using constant-time comparison
            if (this.timingSafeEqual(hashedInput, storedHash)) {
                logger.debug('Backup code verified successfully', { codeIndex: i });
                return { valid: true, usedIndex: i };
            }
        }
        logger.debug('Backup code verification failed: no match');
        return { valid: false };
    }
    /**
     * Performs a timing-safe string comparison to prevent timing attacks.
     *
     * @param a - First string to compare
     * @param b - Second string to compare
     * @returns true if strings are equal, false otherwise
     */
    timingSafeEqual(a, b) {
        if (a.length !== b.length) {
            return false;
        }
        const bufferA = Buffer.from(a, 'utf8');
        const bufferB = Buffer.from(b, 'utf8');
        try {
            const crypto = require('node:crypto');
            return crypto.timingSafeEqual(bufferA, bufferB);
        }
        catch {
            // Fallback for environments without timingSafeEqual
            let result = 0;
            for (let i = 0; i < bufferA.length; i++) {
                result |= bufferA[i] ^ bufferB[i];
            }
            return result === 0;
        }
    }
    /**
     * Gets the current configuration.
     *
     * @returns The active TOTP configuration
     */
    getConfig() {
        return { ...this.config };
    }
}
// Singleton instance
let totpServiceInstance = null;
/**
 * Gets the singleton TOTPService instance.
 *
 * Creates a new instance on first call with default configuration.
 * Subsequent calls return the same instance.
 *
 * @param config - Optional configuration for the service (only used on first call)
 * @returns The TOTPService singleton instance
 *
 * @example
 * ```typescript
 * // Get default instance
 * const totpService = getTOTPService();
 *
 * // Or initialize with custom config on first call
 * const totpService = getTOTPService({ issuer: 'MyApp', digits: 8 });
 * ```
 */
export function getTOTPService(config) {
    if (!totpServiceInstance) {
        totpServiceInstance = new TOTPService(config);
    }
    return totpServiceInstance;
}
/**
 * Resets the singleton instance (primarily for testing).
 *
 * @internal
 */
export function resetTOTPService() {
    totpServiceInstance = null;
}
//# sourceMappingURL=totp.js.map