Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254/** * MFA Service * * Handles TOTP (Time-based One-Time Password) setup, verification, * and backup code management */
import { TOTP, Secret } from 'otpauth';import crypto from 'crypto';import * as QRCode from 'qrcode';import type { DatabaseService } from './database.js';import type { TOTPSetupResponse, MFAStatus } from '../types/mfa.js';
export interface MFAConfig { issuer: string; encryptionKey: string; // 32-byte hex string for AES-256-GCM backupCodesCount?: number;}
export class MFAService { private issuer: string; private encryptionKey: Buffer; private backupCodesCount: number;
constructor( private db: DatabaseService, config: MFAConfig ) { this.issuer = config.issuer; this.encryptionKey = Buffer.from(config.encryptionKey, 'hex'); this.backupCodesCount = config.backupCodesCount || 10;
if (this.encryptionKey.length !== 32) { throw new Error('MFA encryption key must be 32 bytes (64 hex characters)'); } }
/** * Start TOTP setup for a user * Returns the secret and QR code for authenticator app */ async setupTOTP(did: string, handle: string): Promise<TOTPSetupResponse> { // Generate a new secret const secret = new Secret({ size: 20 }); // 160-bit secret
// Create TOTP instance const totp = new TOTP({ issuer: this.issuer, label: handle || did, algorithm: 'SHA1', digits: 6, period: 30, secret, });
// Generate provisioning URI const uri = totp.toString();
// Generate QR code const qrCode = await QRCode.toDataURL(uri);
// Encrypt and store the secret (not yet enabled) const encryptedSecret = this.encryptSecret(secret.base32); this.db.saveMFATOTP({ did, secret_encrypted: encryptedSecret, enabled: false, });
return { secret: secret.base32, qr_code: qrCode, manual_entry_key: secret.base32, issuer: this.issuer, account_name: handle || did, }; }
/** * Verify TOTP code during setup and enable TOTP */ verifyAndEnableTOTP(did: string, code: string): boolean { const config = this.db.getMFATOTP(did); if (!config) { return false; }
// Decrypt the secret const secretBase32 = this.decryptSecret(config.secret_encrypted);
// Verify the code const totp = new TOTP({ issuer: this.issuer, algorithm: 'SHA1', digits: 6, period: 30, secret: Secret.fromBase32(secretBase32), });
const delta = totp.validate({ token: code, window: 1 });
if (delta !== null) { // Enable TOTP this.db.enableMFATOTP(did); return true; }
return false; }
/** * Verify TOTP code during login */ verifyTOTP(did: string, code: string): boolean { const config = this.db.getMFATOTP(did); if (!config || !config.enabled) { return false; }
// Decrypt the secret const secretBase32 = this.decryptSecret(config.secret_encrypted);
// Verify the code const totp = new TOTP({ issuer: this.issuer, algorithm: 'SHA1', digits: 6, period: 30, secret: Secret.fromBase32(secretBase32), });
const delta = totp.validate({ token: code, window: 1 }); return delta !== null; }
/** * Disable TOTP for a user */ disableTOTP(did: string): void { this.db.disableMFATOTP(did); }
/** * Check if user has TOTP enabled */ isTOTPEnabled(did: string): boolean { const config = this.db.getMFATOTP(did); return config?.enabled ?? false; }
/** * Generate backup codes for a user */ generateBackupCodes(did: string): string[] { const codes: string[] = []; const codeHashes: string[] = [];
for (let i = 0; i < this.backupCodesCount; i++) { // Generate 8-character alphanumeric code (split into 4-4 for readability) const part1 = crypto.randomBytes(2).toString('hex').toUpperCase(); const part2 = crypto.randomBytes(2).toString('hex').toUpperCase(); const code = `${part1}-${part2}`; codes.push(code);
// Hash the code for storage const hash = crypto.createHash('sha256').update(code).digest('hex'); codeHashes.push(hash); }
// Store hashed codes this.db.saveBackupCodes(did, codeHashes);
return codes; }
/** * Verify a backup code */ verifyBackupCode(did: string, code: string): boolean { // Hash the provided code const codeHash = crypto.createHash('sha256').update(code.toUpperCase()).digest('hex');
// Look up unused backup code const backupCode = this.db.getUnusedBackupCode(did, codeHash); if (!backupCode) { return false; }
// Mark as used this.db.markBackupCodeUsed(backupCode.id); return true; }
/** * Get MFA status for a user */ getMFAStatus(did: string, passkeyCount: number): MFAStatus { const totpConfig = this.db.getMFATOTP(did); const unusedBackupCodes = this.db.countUnusedBackupCodes(did);
return { totp_enabled: totpConfig?.enabled ?? false, totp_verified_at: totpConfig?.verified_at?.toISOString(), passkey_count: passkeyCount, backup_codes_remaining: unusedBackupCodes, }; }
/** * Verify MFA (TOTP or backup code) */ verifyMFA(did: string, code: string, type: 'totp' | 'backup_code'): boolean { if (type === 'totp') { return this.verifyTOTP(did, code); } else if (type === 'backup_code') { return this.verifyBackupCode(did, code); } return false; }
/** * Encrypt a TOTP secret using AES-256-GCM */ private encryptSecret(secret: string): string { const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', this.encryptionKey, iv);
let encrypted = cipher.update(secret, 'utf8', 'hex'); encrypted += cipher.final('hex'); const authTag = cipher.getAuthTag();
// Return iv:authTag:encrypted return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`; }
/** * Decrypt a TOTP secret */ private decryptSecret(encryptedData: string): string { const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
const iv = Buffer.from(ivHex, 'hex'); const authTag = Buffer.from(authTagHex, 'hex');
const decipher = crypto.createDecipheriv('aes-256-gcm', this.encryptionKey, iv); decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8');
return decrypted; }}