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