144 lines
4.4 KiB
PHP
144 lines
4.4 KiB
PHP
<?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('系统数据库密码不可用,无法处理共享云数据库密码。');
|
||
}
|
||
}
|
||
}
|