99 lines
2.6 KiB
PHP
99 lines
2.6 KiB
PHP
<?php
|
|
|
|
/**
|
|
* AES-256-GCM encryption helper for sensitive data at rest (e.g., TOTP secrets).
|
|
*
|
|
* Requires TOTP_ENCRYPTION_KEY in .env (64 hex chars = 32 bytes).
|
|
* Format: base64(nonce + ciphertext + tag)
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
class Encryption
|
|
{
|
|
private const CIPHER = 'aes-256-gcm';
|
|
private const NONCE_LENGTH = 12;
|
|
private const TAG_LENGTH = 16;
|
|
|
|
private static ?string $key = null;
|
|
|
|
private static function getKey(): string
|
|
{
|
|
if (self::$key === null) {
|
|
$hex = env('TOTP_ENCRYPTION_KEY', '');
|
|
if (strlen($hex) !== 64 || !ctype_xdigit($hex)) {
|
|
throw new RuntimeException('TOTP_ENCRYPTION_KEY must be 64 hex chars (32 bytes)');
|
|
}
|
|
self::$key = hex2bin($hex);
|
|
}
|
|
return self::$key;
|
|
}
|
|
|
|
public static function encrypt(string $plaintext): string
|
|
{
|
|
$key = self::getKey();
|
|
$nonce = random_bytes(self::NONCE_LENGTH);
|
|
$tag = '';
|
|
|
|
$ciphertext = openssl_encrypt(
|
|
$plaintext,
|
|
self::CIPHER,
|
|
$key,
|
|
OPENSSL_RAW_DATA,
|
|
$nonce,
|
|
$tag,
|
|
'',
|
|
self::TAG_LENGTH
|
|
);
|
|
|
|
if ($ciphertext === false) {
|
|
throw new RuntimeException('Encryption failed');
|
|
}
|
|
|
|
return base64_encode($nonce . $ciphertext . $tag);
|
|
}
|
|
|
|
public static function decrypt(string $encoded): string
|
|
{
|
|
$key = self::getKey();
|
|
$raw = base64_decode($encoded, true);
|
|
|
|
if ($raw === false || strlen($raw) < self::NONCE_LENGTH + self::TAG_LENGTH + 1) {
|
|
throw new RuntimeException('Invalid encrypted data');
|
|
}
|
|
|
|
$nonce = substr($raw, 0, self::NONCE_LENGTH);
|
|
$tag = substr($raw, -self::TAG_LENGTH);
|
|
$ciphertext = substr($raw, self::NONCE_LENGTH, -self::TAG_LENGTH);
|
|
|
|
$plaintext = openssl_decrypt(
|
|
$ciphertext,
|
|
self::CIPHER,
|
|
$key,
|
|
OPENSSL_RAW_DATA,
|
|
$nonce,
|
|
$tag
|
|
);
|
|
|
|
if ($plaintext === false) {
|
|
throw new RuntimeException('Decryption failed');
|
|
}
|
|
|
|
return $plaintext;
|
|
}
|
|
|
|
/**
|
|
* Zjisti, zda je hodnota sifrovana (base64 s ocekavanou delkou).
|
|
* TOTP secret je vzdy 16-32 ASCII znaku, sifrovany je base64 s nonce+tag.
|
|
*/
|
|
public static function isEncrypted(string $value): bool
|
|
{
|
|
if (strlen($value) < 40) {
|
|
return false;
|
|
}
|
|
$decoded = base64_decode($value, true);
|
|
return $decoded !== false
|
|
&& strlen($decoded) > self::NONCE_LENGTH + self::TAG_LENGTH;
|
|
}
|
|
}
|