Initial commit
This commit is contained in:
98
api/includes/Encryption.php
Normal file
98
api/includes/Encryption.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user