| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- <?php
- namespace App\Libs;
- /**
- * 生图/视频提示词上下文(context_data)解密与校验
- *
- * 协议第一版:AES-256-GCM
- * 数据包:0x01 || IV(12字节) || 密文 || Tag(16字节),整体使用标准 Base64 编码
- * AAD:sound-ai:context_data:v1
- */
- class GenerationContext
- {
- /** 协议版本 */
- const VERSION = 1;
- /** 数据包最小长度:版本 1 字节 + IV 12 字节 + Tag 16 字节 */
- const MIN_PACKET_LENGTH = 29;
- /** 联调共享密钥(64 位十六进制字符串,解码后为 32 字节) */
- const KEY_HEX = 'f1b20a84c45fe3dbb270e867f9b1a181e9e5ffb3f06fce80341f8a35808f9b6a';
- /** 附加认证数据 */
- const AAD = 'sound-ai:context_data:v1';
- /** 校验通过 */
- const STATUS_OK = 'ok';
- /** 未携带 context_data */
- const STATUS_MISSING = 'missing';
- /** 编码/版本错误或认证/解密失败 */
- const STATUS_INVALID = 'invalid';
- /** 解密成功但与 prompt 不一致 */
- const STATUS_MISMATCH = 'mismatch';
- /**
- * 解密 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;
- }
- }
|