Skip to main content
Glama
totp-generator.ts10.2 kB
import { randomBytes, createHmac, timingSafeEqual } from 'crypto'; import { authenticator } from 'otplib'; import { logger } from '../../utils/logger'; export interface TOTPConfig { secretLength: number; window: number; step: number; digits: number; algorithm: string; encoding: string; } export interface BackupCodeConfig { length: number; count: number; charset: string; minEntropy: number; } export interface SecretValidation { isValid: boolean; entropy: number; strength: 'weak' | 'moderate' | 'strong' | 'very-strong'; issues: string[]; } export class SecureTOTPGenerator { private readonly config: TOTPConfig = { secretLength: 32, // 160 bits base32 encoded (RFC 4648) window: 1, // Allow 1 time step tolerance step: 30, // 30 second time steps digits: 6, // 6 digit codes algorithm: 'sha256', // Use SHA-256 instead of SHA-1 encoding: 'base32' }; private readonly backupConfig: BackupCodeConfig = { length: 8, // 8 character backup codes count: 10, // Generate 10 backup codes charset: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ', // Base36 for readability minEntropy: 40 // Minimum bits of entropy required }; constructor() { // Configure otplib with secure parameters authenticator.options = { window: this.config.window, step: this.config.step, digits: this.config.digits, algorithm: this.config.algorithm as any, encoding: this.config.encoding as any, }; } /** * Generate cryptographically secure TOTP secret */ public generateSecret(): string { try { // Generate cryptographically secure random bytes const secretBytes = randomBytes(this.config.secretLength); // Convert to base32 for TOTP compatibility const secret = this.bytesToBase32(secretBytes); // Validate the generated secret const validation = this.validateSecretEntropy(secret); if (!validation.isValid) { logger.warn('Generated secret failed validation, regenerating', { issues: validation.issues, entropy: validation.entropy }); // Recursively generate until we get a valid secret return this.generateSecret(); } // Clear sensitive data from memory secretBytes.fill(0); logger.debug('TOTP secret generated successfully', { secretLength: secret.length, entropy: validation.entropy, strength: validation.strength }); return secret; } catch (error) { logger.error('Failed to generate TOTP secret', { error }); throw new Error('TOTP secret generation failed'); } } /** * Generate cryptographically secure backup codes */ public generateBackupCodes(count: number = this.backupConfig.count): string[] { try { const codes: string[] = []; const usedCodes = new Set<string>(); while (codes.length < count) { // Generate cryptographically secure random bytes const randomBytes = this.generateSecureRandomBytes(this.backupConfig.length); // Convert to backup code format const code = this.bytesToBackupCode(randomBytes); // Ensure uniqueness and entropy requirements if (!usedCodes.has(code) && this.validateBackupCodeEntropy(code)) { codes.push(code); usedCodes.add(code); } // Clear sensitive data randomBytes.fill(0); } logger.debug('Backup codes generated successfully', { count: codes.length, codeLength: this.backupConfig.length }); return codes; } catch (error) { logger.error('Failed to generate backup codes', { error }); throw new Error('Backup code generation failed'); } } /** * Validate TOTP secret entropy and strength */ public validateSecretEntropy(secret: string): SecretValidation { const issues: string[] = []; let entropy = 0; try { // Validate secret length if (secret.length < 16) { issues.push('Secret too short (minimum 16 characters)'); } // Validate base32 encoding if (!/^[A-Z2-7]+$/.test(secret)) { issues.push('Invalid base32 encoding'); } // Calculate entropy entropy = this.calculateEntropy(secret); // Validate minimum entropy if (entropy < 80) { issues.push(`Insufficient entropy (${entropy} bits, minimum 80)`); } // Check for patterns and repetition if (this.hasRepeatedPatterns(secret)) { issues.push('Contains repeated patterns'); } // Determine strength level let strength: 'weak' | 'moderate' | 'strong' | 'very-strong'; if (entropy < 60) { strength = 'weak'; } else if (entropy < 80) { strength = 'moderate'; } else if (entropy < 120) { strength = 'strong'; } else { strength = 'very-strong'; } return { isValid: issues.length === 0, entropy, strength, issues }; } catch (error) { logger.error('Secret validation failed', { error }); return { isValid: false, entropy: 0, strength: 'weak', issues: ['Validation failed'] }; } } /** * Validate backup code entropy */ public validateBackupCodeEntropy(code: string): boolean { try { const entropy = this.calculateEntropy(code); return entropy >= this.backupConfig.minEntropy; } catch (error) { logger.error('Backup code validation failed', { error }); return false; } } /** * Generate TOTP token for testing and validation */ public generateToken(secret: string, timestamp?: number): string { try { const token = authenticator.generate(secret); logger.debug('TOTP token generated for validation', { secretLength: secret.length, timestamp: timestamp || Date.now() }); return token; } catch (error) { logger.error('Failed to generate TOTP token', { error }); throw new Error('TOTP token generation failed'); } } /** * Verify TOTP token with timing attack protection */ public verifyToken(token: string, secret: string, window?: number): boolean { try { // Use constant-time comparison for token verification const expectedToken = this.generateToken(secret); // Verify using otplib with specified window const isValid = authenticator.check(token, secret); // Additional constant-time verification for security const tokenBuffer = Buffer.from(token, 'utf8'); const expectedBuffer = Buffer.from(expectedToken, 'utf8'); const constantTimeResult = tokenBuffer.length === expectedBuffer.length && timingSafeEqual(tokenBuffer, expectedBuffer); logger.debug('TOTP token verification completed', { tokenLength: token.length, isValid: isValid, constantTimeMatch: constantTimeResult }); return isValid; } catch (error) { logger.error('TOTP token verification failed', { error }); return false; } } /** * Convert bytes to base32 encoding for TOTP compatibility */ private bytesToBase32(bytes: Buffer): string { const base32Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; let result = ''; let buffer = 0; let bitsLeft = 0; for (let i = 0; i < bytes.length; i++) { buffer = (buffer << 8) | bytes[i]; bitsLeft += 8; while (bitsLeft >= 5) { result += base32Chars[(buffer >> (bitsLeft - 5)) & 31]; bitsLeft -= 5; } } if (bitsLeft > 0) { result += base32Chars[(buffer << (5 - bitsLeft)) & 31]; } return result; } /** * Convert bytes to backup code format */ private bytesToBackupCode(bytes: Buffer): string { let result = ''; const charset = this.backupConfig.charset; for (let i = 0; i < this.backupConfig.length; i++) { const randomIndex = bytes[i % bytes.length] % charset.length; result += charset[randomIndex]; } return result; } /** * Generate cryptographically secure random bytes */ private generateSecureRandomBytes(length: number): Buffer { return randomBytes(length); } /** * Calculate Shannon entropy of a string */ private calculateEntropy(str: string): number { const charCounts: { [key: string]: number } = {}; // Count character frequencies for (const char of str) { charCounts[char] = (charCounts[char] || 0) + 1; } // Calculate entropy let entropy = 0; const length = str.length; for (const count of Object.values(charCounts)) { const probability = count / length; entropy -= probability * Math.log2(probability); } // Return entropy in bits return entropy * length; } /** * Check for repeated patterns in secret */ private hasRepeatedPatterns(secret: string): boolean { // Check for repeated substrings for (let len = 2; len <= secret.length / 2; len++) { for (let i = 0; i <= secret.length - len * 2; i++) { const pattern = secret.substr(i, len); const nextOccurrence = secret.indexOf(pattern, i + len); if (nextOccurrence !== -1) { return true; } } } return false; } /** * Get current configuration */ public getConfig(): TOTPConfig { return { ...this.config }; } /** * Get backup code configuration */ public getBackupConfig(): BackupCodeConfig { return { ...this.backupConfig }; } /** * Generate OTP URL for QR code generation */ public generateOTPURL(secret: string, label: string, issuer: string): string { try { const url = authenticator.keyuri(label, issuer, secret); logger.debug('OTP URL generated for QR code', { label, issuer, secretLength: secret.length }); return url; } catch (error) { logger.error('Failed to generate OTP URL', { error }); throw new Error('OTP URL generation failed'); } } } export const secureTOTPGenerator = new SecureTOTPGenerator();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/perfecxion-ai/secure-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server