# TOTP MFA Security Design Documentation

This document summarizes the design choices and security considerations for implementing Google Authenticator–compatible **TOTP (Time-based One-Time Password)** in PHP.

---

## 1. TOTP Overview
- **TOTP (RFC 6238)** generates 6-digit codes based on:
  - Shared secret (Base32 encoded)
  - Current Unix time (default 30-second window)
- Both client (e.g., Google Authenticator) and server must hold the same **shared secret**.
- Codes expire quickly (30s), limiting replay attacks.

---

## 2. Storage of TOTP Secrets
Secrets must be stored securely to prevent disclosure. Unlike passwords, **secrets cannot be hashed** (one-way) since they must be used to generate OTPs.

### ❌ Bad Practice
- Storing secrets in plaintext in the database.
- Hashing secrets (you cannot regenerate OTPs).

### ✅ Good Practice
- **Encrypt** secrets before storing.
- Use **AES-256-GCM** or **XChaCha20-Poly1305** for authenticated encryption.
- Store **ciphertext + nonce + auth tag** in DB.

---

## 3. Encryption Key Strategies

### Option 1: Password-Derived Keys (Not Recommended for MFA Login)
- Derive key from user password (PBKDF2, Argon2).
- Only works after password entry.
- **Problem:** cannot verify TOTP before login is fully complete.
- Requires re-encryption on password change.
- Suitable for **end-to-end encrypted apps** but not general MFA.

### Option 2: Server Master Key (Recommended)
- Use a single AES-256 or XChaCha20 key for encrypting all user TOTP secrets.
- Store key in a secure location accessible only to the app.
- Works for MFA at login.
- Easy to rotate (re-encrypt all records).

### Option 3: Hybrid (Per-User Salt + Master Key)
- Generate per-user salt.
- Encryption key = HMAC(master_key, user_salt).
- Limits blast radius if DB leaks.

---

## 4. Where to Store the Master Key

### Basic Approach (Good Enough for Now)
- Store in `.env` file.
- Lock down permissions: `chmod 600`, owned by root.
- Do not commit `.env` to version control.

### Advanced Approaches
- **KMS / Vault:** AWS KMS, Azure Key Vault, GCP KMS, HashiCorp Vault.
- **OS Secret Stores:** Linux keyring, systemd credentials.
- **Split Key:** Half in `.env`, half in DB or Vault.

---

## 5. Threat Model & Trade-Offs

- **Compromise of DB only:** Encrypted secrets are safe.
- **Compromise of app server + .env:** Attacker can decrypt secrets.
- **Full root compromise:** Nothing is safe (same as DB password, SSL keys).

Conclusion: `.env` with strong permissions is *sufficient for now*. Later migration to Vault/KMS increases protection against server compromise.

---

## 6. Implementation Notes

### 6.1 Repository Implementation Snapshot (May 2025)

The repo already contains scaffolding to support these design choices:

- **Database Layer** (`TAFDB.pgsql`, `TAFDB.sql`)
  - `mfa_type` enum plus `user_mfa_factors`, `user_mfa_totp`, and `user_mfa_webauthn` tables store enrolled factors.
  - Legacy `user_2fa` tables remain for backward compatibility but are superseded by the new UUID-driven schema.
  - `login_audit` includes `mfa_completed_at` for tracking successful MFA challenges.
- **PHP Models** under `models/` mirror the tables (`user_mfa_factors.php`, `user_mfa_totp.php`, `user_mfa_webauthn.php`, `user_2fa.php`).
- **Controller Logic** (`api/controllers/UsersController.php`)
  - `sendMFAChallenge`, `verifyTOTP`, and `enableTOTP` drive enrollment and verification.
  - Secrets are encrypted with `encryptAESGCM` using the master key described above and decrypted on demand for verification.
  - Login flow now prompts for a second factor whenever a TOTP or passkey is registered; users without any enrolled factor proceed with password-only login, while TOTP challenges are re-validated server-side before the session is created and successful challenges update `last_used_at` while returning the (still Base32) secret for offline caching.
  - WebAuthn / passkey assertions still return a TODO response until the verification handler is implemented.
- **Front-End Flows**
  - `FrontEnd/js/login.js` surfaces MFA prompts after a password challenge, resubmits the code, and mirrors the login response to cache the shared secret locally (encrypted with the user's password) for offline access.
  - `FrontEnd/js/modules/Profile/UserProfile.js` exposes enable/reset buttons and QR generation for Google Authenticator.
  - Offline helpers (`FrontEnd/js/utils/offlineLogin.js`, `FrontEnd/js/utils/MFACheck.js`) reuse the same TOTP expectations.

**Open Gaps:** we still need tenant-configurable UI forms/modules for MFA enrollment, and the security gaps doc tracks the TODO to wire the new tables into production login flows.

### Key Generation
#### PHP
```php
$key = random_bytes(32); // 32 bytes = 256-bit key
$base64Key = base64_encode($key);
echo "AES-256 Key (Base64): $base64Key\n";
```

#### Linux CLI
```bash
# Base64 encoded key (safe for .env)
openssl rand -base64 32
```

Store in `.env`:
```env
MASTER_KEY=hYZ9w9zixOZCxEexjS96kpXyo9p2iRYi2VxF4GyO6qk=
```

---

### AES-256-GCM (OpenSSL)
```php
function encryptAESGCM(string $plaintext, string $key): array {
    $iv = random_bytes(12); // 96-bit nonce
    $tag = '';
    $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
    return [
        'iv' => base64_encode($iv),
        'tag' => base64_encode($tag),
        'ciphertext' => base64_encode($ciphertext)
    ];
}

function decryptAESGCM(array $data, string $key): string|false {
    $iv = base64_decode($data['iv']);
    $tag = base64_decode($data['tag']);
    $ciphertext = base64_decode($data['ciphertext']);
    return openssl_decrypt($ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag);
}
```

---

### XChaCha20-Poly1305 (Libsodium)
```php
function encryptXChaCha(string $plaintext, string $key): array {
    $nonce = random_bytes(SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
    $ciphertext = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($plaintext, '', $nonce, $key);
    return [
        'nonce' => base64_encode($nonce),
        'ciphertext' => base64_encode($ciphertext)
    ];
}

function decryptXChaCha(array $data, string $key): string|false {
    $nonce = base64_decode($data['nonce']);
    $ciphertext = base64_decode($data['ciphertext']);
    return sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key);
}
```

---

## 7. Future Improvements
- Migrate secrets from `.env` → Vault/KMS.
- Add per-user salts.
- Implement key rotation.
- Audit logs for secret access.

---

## 8. Summary
- ✅ Secrets are **encrypted**, not hashed.
- ✅ Start with **.env key** (good enough).
- ✅ Recommended encryption: **AES-256-GCM** (compatibility) or **XChaCha20-Poly1305** (modern, safer).
- ✅ MFA flow: password → TOTP challenge → verify with decrypted secret.

---
