error('生成接口 context_data 解密失败', [ 'scene' => $scene, 'detail' => $e->getMessage(), ]); Utils::throwError('20003:' . $e->getMessage()); } $data['prompt'] = $decoded; dLog($logChannel)->info('生成接口使用 context_data 解密结果作为提示词', [ 'scene' => $scene, 'decoded_bytes' => strlen($decoded), ]); return; } dLog($logChannel)->warning('生成接口未提供 prompt 与 context_data', [ 'scene' => $scene, ]); } /** * 解密 context_data * * @param mixed $value 前端传入的 context_data * @return string prompt 原文 * @throws \RuntimeException 编码/版本错误、密钥错误或认证/解密失败 */ public static function decode($value) { if (!is_string($value) || $value === '') { throw new \RuntimeException('context_data 编码或版本错误'); } $packet = base64_decode($value, true); // 回写比对用于排除空白字符、URL-safe 变体等非标准 Base64 编码 if ($packet === false || base64_encode($packet) !== $value || strlen($packet) < self::MIN_PACKET_LENGTH || ord($packet[0]) !== self::VERSION) { throw new \RuntimeException('context_data 编码或版本错误'); } $key = hex2bin(self::KEY_HEX); if ($key === false || strlen($key) !== 32) { throw new \RuntimeException('context_data 密钥配置错误'); } $iv = substr($packet, 1, 12); $tag = substr($packet, -16); $ciphertext = substr($packet, 13, -16); $plain = openssl_decrypt( $ciphertext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, self::AAD ); if ($plain === false) { throw new \RuntimeException('context_data 认证或解密失败'); } return $plain; } /** * 校验 prompt 与 context_data 是否逐字节一致 * * 不对 prompt 做 trim、换行转换、Unicode 归一化、模板替换等任何处理。 * * @param mixed $prompt * @param mixed $contextData * @return array */ public static function verify($prompt, $contextData) { $result = [ 'status' => self::STATUS_MISSING, 'message' => '', 'prompt_bytes' => is_string($prompt) ? strlen($prompt) : 0, 'decoded_bytes' => 0, 'decoded_sha256' => '', ]; if ($contextData === null || $contextData === '') { return $result; } if (!is_string($contextData)) { $result['status'] = self::STATUS_INVALID; $result['message'] = 'context_data 编码或版本错误'; return $result; } try { $decoded = self::decode($contextData); } catch (\RuntimeException $e) { $result['status'] = self::STATUS_INVALID; $result['message'] = $e->getMessage(); return $result; } $result['decoded_bytes'] = strlen($decoded); $result['decoded_sha256'] = substr(hash('sha256', $decoded), 0, 16); if (!is_string($prompt) || !hash_equals($prompt, $decoded)) { $result['status'] = self::STATUS_MISMATCH; $result['message'] = 'context_data 与 prompt 不一致'; return $result; } $result['status'] = self::STATUS_OK; return $result; } }