requestCrypto.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. import globalConfig from '@/config';
  2. import { CompressMethod } from '@/config/types';
  3. import {
  4. bytesToNumber,
  5. numberToBytesAt,
  6. Endian,
  7. bytesToString,
  8. stringToBytes,
  9. } from '@/utils/bytesUtils';
  10. import {
  11. aesCbcDecryptBytes,
  12. aesCbcEncryptBytes,
  13. bytesBase64decode,
  14. bytesBase64encode,
  15. } from '@/utils/crypto';
  16. import { compressBytes, decompressBytes, CompressFormat } from './compress';
  17. import { currentUnixTimestamp } from './timeUtils';
  18. export type JsonPrimitive = string | number | boolean | null;
  19. export type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue | undefined };
  20. const TIMESTAMP_BYTES = 8;
  21. function buildTimestampBytes(timestamp: number): Uint8Array {
  22. const arr = new Uint8Array(TIMESTAMP_BYTES);
  23. numberToBytesAt(0, arr, 0, Endian.BE);
  24. numberToBytesAt(timestamp, arr, 4, Endian.BE);
  25. return arr;
  26. }
  27. function parseTimestampBytes(bytes: Uint8Array): number {
  28. const high = bytesToNumber(bytes, 0, Endian.BE);
  29. const low = bytesToNumber(bytes, 4, Endian.BE);
  30. return high * 0x100000000 + low;
  31. }
  32. function compressFormatFromMethod(method: CompressMethod): CompressFormat {
  33. return method === CompressMethod.BR ? CompressFormat.BROTLI : CompressFormat.GZIP;
  34. }
  35. /**
  36. * 请求体加密(与后端 GoDataEncrypt 对应)
  37. * 仅接收字符串:8字节时间戳(BE)+数据 → 压缩() → AES-CBC 加密 → IV + 密文
  38. * @param plaintext 明文字符串
  39. * @param key 密钥(由 crypto 内部补齐/截断为 32 字节)
  40. * @param compressMethod 压缩算法,空表示不压缩
  41. * @param timestamp 时间戳(秒级数字),内部转为 8 字节大端后拼在明文前
  42. * @returns 二进制密文(IV 16 字节 + 密文)
  43. */
  44. export async function encryptRequestPayload(
  45. dataBytes: Uint8Array,
  46. timestamp: number,
  47. key: Uint8Array,
  48. compressMethod: CompressMethod = CompressMethod.NO_ZIP
  49. ): Promise<Uint8Array> {
  50. const compressed =
  51. compressMethod && compressMethod !== CompressMethod.NO_ZIP
  52. ? await compressBytes(dataBytes, compressFormatFromMethod(compressMethod))
  53. : dataBytes;
  54. const timestampBytes = buildTimestampBytes(timestamp);
  55. const timeAndData = new Uint8Array(TIMESTAMP_BYTES + compressed.length);
  56. timeAndData.set(timestampBytes, 0);
  57. timeAndData.set(compressed, TIMESTAMP_BYTES);
  58. return aesCbcEncryptBytes(timeAndData, key);
  59. }
  60. /**
  61. * 响应体解密(与后端 GoDataDecrypt 对应)
  62. * 仅接收二进制:IV + 密文 → AES-CBC 解密 → 去掉 8 字节时间戳 → 可选解压
  63. * @param encryptedBytes 二进制密文(IV 16 字节 + 密文)
  64. * @param key 密钥(由 crypto 内部补齐/截断为 32 字节)
  65. * @param compressMethod 解压算法,空表示未压缩
  66. * @returns 去掉时间戳并解压后的数据
  67. */
  68. export async function decryptResponsePayload(
  69. encryptedBytes: Uint8Array,
  70. key: Uint8Array,
  71. compressMethod: CompressMethod = CompressMethod.NO_ZIP
  72. ): Promise<{ timestamp: number; data: Uint8Array }> {
  73. const decrypted = aesCbcDecryptBytes(encryptedBytes, key);
  74. if (decrypted.length < TIMESTAMP_BYTES) return { timestamp: 0, data: new Uint8Array(0) };
  75. const timestampBytes = decrypted.subarray(0, TIMESTAMP_BYTES);
  76. const timestamp = parseTimestampBytes(timestampBytes);
  77. const dataBytes = decrypted.subarray(TIMESTAMP_BYTES);
  78. const rawBytes =
  79. compressMethod && compressMethod !== CompressMethod.NO_ZIP
  80. ? await decompressBytes(dataBytes, compressFormatFromMethod(compressMethod))
  81. : dataBytes;
  82. return { timestamp, data: rawBytes };
  83. }
  84. /**
  85. * 加密 URL 参数
  86. * @param params 参数对象
  87. * @returns 加密后的 URL 参数
  88. */
  89. export async function encryptUrlParams(params: JsonValue): Promise<string> {
  90. const jsonString = typeof params === 'string' ? params : JSON.stringify(params);
  91. const key = stringToBytes(globalConfig.security.requestEncryptionKey);
  92. const dataBytes = stringToBytes(jsonString);
  93. const encrypted = await encryptRequestPayload(
  94. dataBytes,
  95. currentUnixTimestamp(),
  96. key,
  97. globalConfig.security.compressMethod
  98. );
  99. return bytesBase64encode(encrypted, true);
  100. }
  101. /**
  102. * 解密 URL 参数
  103. * @param params 加密后的 URL 参数
  104. * @returns 解密后的参数对象
  105. */
  106. export async function decryptUrlParams<T = any>(params: string): Promise<T | null> {
  107. // const key = bytesBase64decode(globalConfig.security.requestEncryptionKey);
  108. const key = stringToBytes(globalConfig.security.requestEncryptionKey);
  109. const dataBytes = bytesBase64decode(params, true);
  110. const compressMethod = globalConfig.security.compressMethod;
  111. try {
  112. const { data } = await decryptResponsePayload(dataBytes, key, compressMethod);
  113. const result = bytesToString(data);
  114. return JSON.parse(result) as T;
  115. } catch {
  116. return null;
  117. }
  118. }