<?php
declare(strict_types=1);

/**
 * Symmetric encryption for sensitive DB values (e.g. TOTP secrets).
 * Key is stored in data/app.key – never in the database.
 * Format: enc1:<base64(iv[12] . ciphertext . tag[16])>
 */
class Crypto
{
    private static ?string $key = null;

    private static function key(): string
    {
        if (self::$key !== null) return self::$key;

        $keyFile = DATA_PATH . '/app.key';
        if (!file_exists($keyFile)) {
            // Open with 'c' and exclusive lock BEFORE writing. A second concurrent
            // first-init waits on the lock, then re-reads the now-populated file
            // — preventing the race where two callers each generate a fresh key
            // and one wins, silently invalidating values encrypted by the other.
            $prevUmask = umask(0177); // 0600 perms on create
            $fh = fopen($keyFile, 'c+');
            if ($fh === false) {
                umask($prevUmask);
                throw new \RuntimeException('Could not open app.key');
            }
            flock($fh, LOCK_EX);
            $existing = trim((string)stream_get_contents($fh));
            if ($existing === '') {
                $hex = bin2hex(random_bytes(32));
                ftruncate($fh, 0);
                rewind($fh);
                fwrite($fh, $hex);
                fflush($fh);
            }
            flock($fh, LOCK_UN);
            fclose($fh);
            @chmod($keyFile, 0600); // belt-and-braces in case umask was overridden
            umask($prevUmask);
        }
        $hex = trim((string)file_get_contents($keyFile));
        return self::$key = hex2bin($hex);
    }

    public static function encrypt(string $plain): string
    {
        $iv  = random_bytes(12);
        $tag = '';
        $ct  = openssl_encrypt($plain, 'aes-256-gcm', self::key(), OPENSSL_RAW_DATA, $iv, $tag, '', 16);
        return 'enc1:' . base64_encode($iv . $ct . $tag);
    }

    public static function decrypt(string $value): string
    {
        if (!str_starts_with($value, 'enc1:')) return $value; // legacy plaintext – return as-is
        $raw  = base64_decode(substr($value, 5));
        $iv   = substr($raw, 0, 12);
        $tag  = substr($raw, -16);
        $ct   = substr($raw, 12, -16);
        $plain = openssl_decrypt($ct, 'aes-256-gcm', self::key(), OPENSSL_RAW_DATA, $iv, $tag);
        if ($plain === false) {
            // Tag mismatch or wrong key — surface the failure instead of silently
            // returning "" (security-audit #16). A user whose TOTP secret cannot
            // be decrypted otherwise gets stuck in MFA-loop with no log trace.
            error_log('[Plenvo Crypto] decrypt failed: tag mismatch or wrong key');
            if (class_exists('SecurityLog')) {
                SecurityLog::log('crypto_decrypt_failed', SecurityLog::SEV_CRITICAL);
            }
            return '';
        }
        return $plain;
    }

    public static function isEncrypted(string $value): bool
    {
        return str_starts_with($value, 'enc1:');
    }
}
