129 lines
4.1 KiB
PHP
129 lines
4.1 KiB
PHP
|
|
<?php
|
|||
|
|
|
|||
|
|
namespace App\Core;
|
|||
|
|
|
|||
|
|
use Exception;
|
|||
|
|
use RuntimeException;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 库内敏感字段 AES-256-CBC 加解密
|
|||
|
|
* 密文格式:nl_ase_256_ + base64(iv + ciphertext)
|
|||
|
|
* 密钥派生:hash('sha256', ENCRYPT_KEY, true) → 32 字节
|
|||
|
|
* 读取时兼容历史前缀 ***(迁移期)
|
|||
|
|
*/
|
|||
|
|
class DatabaseEncryptor
|
|||
|
|
{
|
|||
|
|
/** 新密文前缀(入库统一用这个) */
|
|||
|
|
public const PREFIX = 'nl_ase_256_';
|
|||
|
|
|
|||
|
|
/** 历史密文前缀(仅解密兼容) */
|
|||
|
|
public const LEGACY_PREFIX = '***';
|
|||
|
|
|
|||
|
|
private string $cipherMethod = 'AES-256-CBC';
|
|||
|
|
|
|||
|
|
private string $secretKey;
|
|||
|
|
|
|||
|
|
private int $ivLength;
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* @param string $key 来自 config('nl.encrypt_key') / env ENCRYPT_KEY
|
|||
|
|
*/
|
|||
|
|
public function __construct(string $key)
|
|||
|
|
{
|
|||
|
|
$key = trim($key);
|
|||
|
|
if ($key === '') {
|
|||
|
|
throw new RuntimeException('AES 加密密钥未配置(请设置 ENCRYPT_KEY)');
|
|||
|
|
}
|
|||
|
|
$this->secretKey = hash('sha256', $key, true);
|
|||
|
|
$ivLength = openssl_cipher_iv_length($this->cipherMethod);
|
|||
|
|
if ($ivLength === false) {
|
|||
|
|
throw new RuntimeException('不支持的加密算法:' . $this->cipherMethod);
|
|||
|
|
}
|
|||
|
|
$this->ivLength = $ivLength;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 加密后入库;空串不加密,已是密文则原样返回避免二次加密
|
|||
|
|
*
|
|||
|
|
* @param string|null $data 明文
|
|||
|
|
* @throws Exception
|
|||
|
|
*/
|
|||
|
|
public function encrypt($data): string
|
|||
|
|
{
|
|||
|
|
if ($data === null || $data === '') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$data = (string) $data;
|
|||
|
|
if (self::isEncrypted($data)) {
|
|||
|
|
return $data;
|
|||
|
|
}
|
|||
|
|
$iv = openssl_random_pseudo_bytes($this->ivLength);
|
|||
|
|
if ($iv === false) {
|
|||
|
|
throw new RuntimeException('生成 IV 失败');
|
|||
|
|
}
|
|||
|
|
$encryptedData = openssl_encrypt($data, $this->cipherMethod, $this->secretKey, OPENSSL_RAW_DATA, $iv);
|
|||
|
|
if ($encryptedData === false) {
|
|||
|
|
throw new RuntimeException('加密失败:' . (openssl_error_string() ?: 'unknown'));
|
|||
|
|
}
|
|||
|
|
return self::PREFIX . base64_encode($iv . $encryptedData);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 从库内密文解密;无已知前缀视为明文原样返回
|
|||
|
|
*
|
|||
|
|
* @param string|null $base64EncodedEncryptedData 库内值
|
|||
|
|
* @throws Exception
|
|||
|
|
*/
|
|||
|
|
public function decrypt($base64EncodedEncryptedData): string
|
|||
|
|
{
|
|||
|
|
if ($base64EncodedEncryptedData === null || $base64EncodedEncryptedData === '') {
|
|||
|
|
return '';
|
|||
|
|
}
|
|||
|
|
$base64EncodedEncryptedData = (string) $base64EncodedEncryptedData;
|
|||
|
|
$prefix = self::detectPrefix($base64EncodedEncryptedData);
|
|||
|
|
if ($prefix === null) {
|
|||
|
|
return $base64EncodedEncryptedData;
|
|||
|
|
}
|
|||
|
|
$payload = substr($base64EncodedEncryptedData, strlen($prefix));
|
|||
|
|
$decodedData = base64_decode($payload, true);
|
|||
|
|
if ($decodedData === false) {
|
|||
|
|
throw new RuntimeException('Base64 解码失败');
|
|||
|
|
}
|
|||
|
|
if (strlen($decodedData) <= $this->ivLength) {
|
|||
|
|
throw new RuntimeException('密文长度非法');
|
|||
|
|
}
|
|||
|
|
$iv = substr($decodedData, 0, $this->ivLength);
|
|||
|
|
$encryptedData = substr($decodedData, $this->ivLength);
|
|||
|
|
$decryptedData = openssl_decrypt($encryptedData, $this->cipherMethod, $this->secretKey, OPENSSL_RAW_DATA, $iv);
|
|||
|
|
if ($decryptedData === false) {
|
|||
|
|
throw new RuntimeException('解密失败:' . (openssl_error_string() ?: 'unknown'));
|
|||
|
|
}
|
|||
|
|
return $decryptedData;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 是否已是库内密文(含历史前缀)
|
|||
|
|
*/
|
|||
|
|
public static function isEncrypted(?string $value): bool
|
|||
|
|
{
|
|||
|
|
return self::detectPrefix($value) !== null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/**
|
|||
|
|
* 识别密文前缀;新前缀优先
|
|||
|
|
*/
|
|||
|
|
public static function detectPrefix(?string $value): ?string
|
|||
|
|
{
|
|||
|
|
if (!is_string($value) || $value === '') {
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
if (str_starts_with($value, self::PREFIX)) {
|
|||
|
|
return self::PREFIX;
|
|||
|
|
}
|
|||
|
|
if (str_starts_with($value, self::LEGACY_PREFIX)) {
|
|||
|
|
return self::LEGACY_PREFIX;
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
}
|