90 lines
3.2 KiB
PHP
90 lines
3.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace app\common\service\qywx;
|
|
|
|
use RuntimeException;
|
|
|
|
/** 一次性欢迎码仅加密短存;密钥不写数据库。多节点须显式共享环境密钥。 */
|
|
class QywxPromotionCodeCipher
|
|
{
|
|
private ?string $key;
|
|
|
|
public function __construct(?string $key = null)
|
|
{
|
|
$this->key = $key;
|
|
}
|
|
|
|
public function encrypt(string $code): string
|
|
{
|
|
$iv = random_bytes(12);
|
|
$tag = '';
|
|
$encrypted = openssl_encrypt($code, 'aes-256-gcm', $this->key(), OPENSSL_RAW_DATA, $iv, $tag);
|
|
if ($encrypted === false) {
|
|
throw new RuntimeException('无法加密欢迎码');
|
|
}
|
|
return base64_encode($iv . $tag . $encrypted);
|
|
}
|
|
|
|
public function decrypt(string $cipher): string
|
|
{
|
|
$value = base64_decode($cipher, true);
|
|
if ($value === false || strlen($value) <= 28) {
|
|
throw new RuntimeException('欢迎码密文无效');
|
|
}
|
|
$code = openssl_decrypt(substr($value, 28), 'aes-256-gcm', $this->key(), OPENSSL_RAW_DATA, substr($value, 0, 12), substr($value, 12, 16));
|
|
if ($code === false) {
|
|
throw new RuntimeException('欢迎码解密失败,请核对工作进程密钥');
|
|
}
|
|
return $code;
|
|
}
|
|
|
|
private function key(): string
|
|
{
|
|
if ($this->key !== null) {
|
|
if (strlen($this->key) < 32) {
|
|
throw new RuntimeException('欢迎码加密密钥至少32字符');
|
|
}
|
|
return hash('sha256', $this->key, true);
|
|
}
|
|
$configured = (string) config('qywx_promotion_automation.encryption_key', '');
|
|
if ($configured !== '') {
|
|
$this->key = $configured;
|
|
return $this->key();
|
|
}
|
|
$directory = root_path('runtime') . 'qywx_promotion_private';
|
|
if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
|
|
throw new RuntimeException('无法创建欢迎码私有密钥目录');
|
|
}
|
|
$path = $directory . DIRECTORY_SEPARATOR . 'welcome.key';
|
|
$stream = @fopen($path, 'c+b');
|
|
if ($stream === false) {
|
|
throw new RuntimeException('无法读取欢迎码私有密钥');
|
|
}
|
|
try {
|
|
// 首次回调和多个worker可能同时启动;读写均持锁,避免读取尚未写完的密钥。
|
|
if (!flock($stream, LOCK_EX)) {
|
|
throw new RuntimeException('无法锁定欢迎码私有密钥');
|
|
}
|
|
@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('无法保存欢迎码私有密钥');
|
|
}
|
|
}
|
|
if (!preg_match('/^[0-9a-f]{64}$/', $key)) {
|
|
throw new RuntimeException('欢迎码私有密钥损坏,请恢复原密钥');
|
|
}
|
|
$this->key = $key;
|
|
} finally {
|
|
flock($stream, LOCK_UN);
|
|
fclose($stream);
|
|
}
|
|
return $this->key();
|
|
}
|
|
}
|