Files
zyt/server/app/common/service/qywx/QywxPromotionCodeCipher.php
T
2026-09-01 11:26:47 +08:00

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();
}
}