tryDecodeKey($encryptionKey); $this->key = $key ?? ''; $this->configured = null !== $key; } public function encrypt(string $plaintext): string { $this->assertConfigured(); $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = sodium_crypto_secretbox($plaintext, $nonce, $this->key); return sodium_bin2hex($nonce.$ciphertext); } public function decrypt(string $encrypted): string { $this->assertConfigured(); $decoded = sodium_hex2bin($encrypted); $nonce = mb_substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, '8bit'); $ciphertext = mb_substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES, null, '8bit'); $plaintext = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key); if (false === $plaintext) { throw new RuntimeException('Failed to decrypt token.'); } return $plaintext; } private function tryDecodeKey(string $encryptionKey): ?string { if ('' === $encryptionKey) { return null; } try { $key = sodium_hex2bin($encryptionKey); } catch (SodiumException) { return null; } if (SODIUM_CRYPTO_SECRETBOX_KEYBYTES !== mb_strlen($key, '8bit')) { return null; } return $key; } private function assertConfigured(): void { if (!$this->configured) { throw new RuntimeException('Encryption is not configured. Please set a valid ENCRYPTION_KEY.'); } } }