CaiWuXiTong/app/Utilities/UpstreamCredentialCipher.php

144 lines
4.4 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Utilities;
use Illuminate\Contracts\Encryption\DecryptException;
use RuntimeException;
/**
* 使用系统数据库登录密码加密共享的上游数据库凭据。
*
* 系统数据库密码是开发机唯一需要手工提供的启动秘密;云数据库密码不能
* 再依赖每台机器各自不同的 APP_KEY,否则共享记录无法跨开发环境解密。
*/
class UpstreamCredentialCipher
{
private const PREFIX = 'caiwu-system-db-v1:';
private const CIPHER = 'aes-256-gcm';
private const AAD = 'CaiWuXiTong upstream credential v1';
public function encrypt(string $value): string
{
return $this->encryptWithSecret($value, $this->systemDatabasePassword());
}
public function encryptWithSecret(string $value, string $secret): string
{
if ($value === '') {
throw new RuntimeException('云数据库密码不能为空。');
}
$this->assertSecret($secret);
$salt = random_bytes(16);
$iv = random_bytes(12);
$tag = '';
$encrypted = openssl_encrypt(
$value,
self::CIPHER,
$this->deriveKey($salt, $secret),
OPENSSL_RAW_DATA,
$iv,
$tag,
self::AAD,
16
);
if (! is_string($encrypted) || strlen($tag) !== 16) {
throw new RuntimeException('云数据库密码加密失败。');
}
$payload = json_encode([
'salt' => base64_encode($salt),
'iv' => base64_encode($iv),
'tag' => base64_encode($tag),
'value' => base64_encode($encrypted),
], JSON_THROW_ON_ERROR);
return self::PREFIX . base64_encode($payload);
}
public function decrypt(string $payload): string
{
return $this->decryptWithSecret($payload, $this->systemDatabasePassword());
}
public function decryptWithSecret(string $payload, string $secret): string
{
if (! str_starts_with($payload, self::PREFIX)) {
throw new DecryptException('共享云数据库密码格式不受支持。');
}
$this->assertSecret($secret);
try {
$encoded = substr($payload, strlen(self::PREFIX));
$json = base64_decode($encoded, true);
$data = is_string($json)
? json_decode($json, true, 16, JSON_THROW_ON_ERROR)
: null;
if (! is_array($data)) {
throw new RuntimeException('invalid payload');
}
$salt = base64_decode((string) ($data['salt'] ?? ''), true);
$iv = base64_decode((string) ($data['iv'] ?? ''), true);
$tag = base64_decode((string) ($data['tag'] ?? ''), true);
$encrypted = base64_decode((string) ($data['value'] ?? ''), true);
if (
! is_string($salt) || strlen($salt) !== 16
|| ! is_string($iv) || strlen($iv) !== 12
|| ! is_string($tag) || strlen($tag) !== 16
|| ! is_string($encrypted) || $encrypted === ''
) {
throw new RuntimeException('invalid payload');
}
$decrypted = openssl_decrypt(
$encrypted,
self::CIPHER,
$this->deriveKey($salt, $secret),
OPENSSL_RAW_DATA,
$iv,
$tag,
self::AAD
);
if (! is_string($decrypted) || $decrypted === '') {
throw new RuntimeException('invalid credential');
}
return $decrypted;
} catch (DecryptException $exception) {
throw $exception;
} catch (\Throwable) {
throw new DecryptException('系统数据库凭据无法解锁共享云数据库密码。');
}
}
private function deriveKey(string $salt, string $secret): string
{
return hash_hkdf('sha256', $secret, 32, self::AAD, $salt);
}
private function systemDatabasePassword(): string
{
$connection = (string) config('database.default', 'mysql');
$password = (string) config('database.connections.' . $connection . '.password', '');
$this->assertSecret($password);
return $password;
}
private function assertSecret(string $secret): void
{
if ($secret === '') {
throw new RuntimeException('系统数据库密码不可用,无法处理共享云数据库密码。');
}
}
}