import globalConfig from '@/config'; import { CompressMethod } from '@/config/types'; import { bytesToNumber, numberToBytesAt, Endian, bytesToString, stringToBytes, } from '@/utils/bytesUtils'; import { aesCbcDecryptBytes, aesCbcEncryptBytes, bytesBase64decode, bytesBase64encode, } from '@/utils/crypto'; import { compressBytes, decompressBytes, CompressFormat } from './compress'; import { currentUnixTimestamp } from './timeUtils'; export type JsonPrimitive = string | number | boolean | null; export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue | undefined }; const TIMESTAMP_BYTES = 8; function buildTimestampBytes(timestamp: number): Uint8Array { const arr = new Uint8Array(TIMESTAMP_BYTES); numberToBytesAt(0, arr, 0, Endian.BE); numberToBytesAt(timestamp, arr, 4, Endian.BE); return arr; } function parseTimestampBytes(bytes: Uint8Array): number { const high = bytesToNumber(bytes, 0, Endian.BE); const low = bytesToNumber(bytes, 4, Endian.BE); return high * 0x100000000 + low; } function compressFormatFromMethod(method: CompressMethod): CompressFormat { return method === CompressMethod.BR ? CompressFormat.BROTLI : CompressFormat.GZIP; } /** * 请求体加密(与后端 GoDataEncrypt 对应) * 仅接收字符串:8字节时间戳(BE)+数据 → 压缩() → AES-CBC 加密 → IV + 密文 * @param plaintext 明文字符串 * @param key 密钥(由 crypto 内部补齐/截断为 32 字节) * @param compressMethod 压缩算法,空表示不压缩 * @param timestamp 时间戳(秒级数字),内部转为 8 字节大端后拼在明文前 * @returns 二进制密文(IV 16 字节 + 密文) */ export async function encryptRequestPayload( dataBytes: Uint8Array, timestamp: number, key: Uint8Array, compressMethod: CompressMethod = CompressMethod.NO_ZIP ): Promise { const compressed = compressMethod && compressMethod !== CompressMethod.NO_ZIP ? await compressBytes(dataBytes, compressFormatFromMethod(compressMethod)) : dataBytes; const timestampBytes = buildTimestampBytes(timestamp); const timeAndData = new Uint8Array(TIMESTAMP_BYTES + compressed.length); timeAndData.set(timestampBytes, 0); timeAndData.set(compressed, TIMESTAMP_BYTES); return aesCbcEncryptBytes(timeAndData, key); } /** * 响应体解密(与后端 GoDataDecrypt 对应) * 仅接收二进制:IV + 密文 → AES-CBC 解密 → 去掉 8 字节时间戳 → 可选解压 * @param encryptedBytes 二进制密文(IV 16 字节 + 密文) * @param key 密钥(由 crypto 内部补齐/截断为 32 字节) * @param compressMethod 解压算法,空表示未压缩 * @returns 去掉时间戳并解压后的数据 */ export async function decryptResponsePayload( encryptedBytes: Uint8Array, key: Uint8Array, compressMethod: CompressMethod = CompressMethod.NO_ZIP ): Promise<{ timestamp: number; data: Uint8Array }> { const decrypted = aesCbcDecryptBytes(encryptedBytes, key); if (decrypted.length < TIMESTAMP_BYTES) return { timestamp: 0, data: new Uint8Array(0) }; const timestampBytes = decrypted.subarray(0, TIMESTAMP_BYTES); const timestamp = parseTimestampBytes(timestampBytes); const dataBytes = decrypted.subarray(TIMESTAMP_BYTES); const rawBytes = compressMethod && compressMethod !== CompressMethod.NO_ZIP ? await decompressBytes(dataBytes, compressFormatFromMethod(compressMethod)) : dataBytes; return { timestamp, data: rawBytes }; } /** * 加密 URL 参数 * @param params 参数对象 * @returns 加密后的 URL 参数 */ export async function encryptUrlParams(params: JsonValue): Promise { const jsonString = typeof params === 'string' ? params : JSON.stringify(params); const key = stringToBytes(globalConfig.security.requestEncryptionKey); const dataBytes = stringToBytes(jsonString); const encrypted = await encryptRequestPayload( dataBytes, currentUnixTimestamp(), key, globalConfig.security.compressMethod ); return bytesBase64encode(encrypted, true); } /** * 解密 URL 参数 * @param params 加密后的 URL 参数 * @returns 解密后的参数对象 */ export async function decryptUrlParams(params: string): Promise { // const key = bytesBase64decode(globalConfig.security.requestEncryptionKey); const key = stringToBytes(globalConfig.security.requestEncryptionKey); const dataBytes = bytesBase64decode(params, true); const compressMethod = globalConfig.security.compressMethod; try { const { data } = await decryptResponsePayload(dataBytes, key, compressMethod); const result = bytesToString(data); return JSON.parse(result) as T; } catch { return null; } }