更新
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\prescriptionai;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
/** Authenticated at-rest encryption; key is never stored in the database. */
|
||||
final class PrescriptionAiCipher
|
||||
{
|
||||
private ?string $secret;
|
||||
|
||||
public function __construct(?string $secret = null)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
}
|
||||
|
||||
public function encrypt(array $value, string $purpose): string
|
||||
{
|
||||
$iv = random_bytes(12);
|
||||
$tag = '';
|
||||
$cipher = openssl_encrypt(PrescriptionAiPolicy::canonical($value), 'aes-256-gcm', $this->key(),
|
||||
OPENSSL_RAW_DATA, $iv, $tag, $purpose);
|
||||
if ($cipher === false) {
|
||||
throw new RuntimeException('AI_ANALYSIS_ENCRYPTION_FAILED');
|
||||
}
|
||||
return 'v1:' . base64_encode($iv . $tag . $cipher);
|
||||
}
|
||||
|
||||
public function decrypt(string $value, string $purpose): array
|
||||
{
|
||||
$bytes = str_starts_with($value, 'v1:') ? base64_decode(substr($value, 3), true) : false;
|
||||
if ($bytes === false || strlen($bytes) < 30) {
|
||||
throw new RuntimeException('AI_ANALYSIS_CIPHER_INVALID');
|
||||
}
|
||||
$plain = openssl_decrypt(substr($bytes, 28), 'aes-256-gcm', $this->key(), OPENSSL_RAW_DATA,
|
||||
substr($bytes, 0, 12), substr($bytes, 12, 16), $purpose);
|
||||
if ($plain === false) {
|
||||
throw new RuntimeException('AI_ANALYSIS_CIPHER_INVALID');
|
||||
}
|
||||
$decoded = json_decode($plain, true, 512, JSON_THROW_ON_ERROR);
|
||||
if (!is_array($decoded)) {
|
||||
throw new RuntimeException('AI_ANALYSIS_CIPHER_INVALID');
|
||||
}
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function key(): string
|
||||
{
|
||||
if ($this->secret === null) {
|
||||
$this->secret = (string) config('prescription_analysis.encryption_key', '');
|
||||
if ($this->secret === '') {
|
||||
$dir = root_path('runtime') . 'prescription_ai_private';
|
||||
if (!is_dir($dir) && !@mkdir($dir, 0700, true) && !is_dir($dir)) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_UNAVAILABLE');
|
||||
}
|
||||
$path = $dir . DIRECTORY_SEPARATOR . 'snapshot.key';
|
||||
$stream = @fopen($path, 'c+b');
|
||||
if ($stream === false) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_UNAVAILABLE');
|
||||
}
|
||||
try {
|
||||
if (!flock($stream, LOCK_EX)) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_UNAVAILABLE');
|
||||
}
|
||||
@chmod($path, 0600);
|
||||
$key = trim((string) stream_get_contents($stream));
|
||||
if ($key === '') {
|
||||
$key = bin2hex(random_bytes(32));
|
||||
rewind($stream);
|
||||
if (fwrite($stream, $key) !== strlen($key) || !fflush($stream)) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_UNAVAILABLE');
|
||||
}
|
||||
}
|
||||
if (!preg_match('/^[a-f0-9]{64}$/', $key)) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_INVALID');
|
||||
}
|
||||
$this->secret = $key;
|
||||
} finally {
|
||||
flock($stream, LOCK_UN);
|
||||
fclose($stream);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (strlen($this->secret) < 32) {
|
||||
throw new RuntimeException('AI_ANALYSIS_KEY_INVALID');
|
||||
}
|
||||
return hash('sha256', $this->secret, true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user