Ver Fonte

feat: 对接接口加密算法

BaiLuoYan há 1 mês atrás
pai
commit
24c55be6a0

+ 10 - 6
.env

@@ -1,20 +1,24 @@
 VITE_APP_ENV="production"
 
+# 产品代码
+VITE_API_PRODUCT_CODE="nomo"
+
 # 构建配置
 VITE_BUILD_PUBLIC_PATH="/"
 VITE_BUILD_COMPRESSION="gzip"
 
 # 应用基础配置
-VITE_APP_TITLE="Visa Card H5"
+VITE_APP_TITLE="NOMO VPN"
 VITE_APP_VERSION="1.0.0"
 VITE_ROUTER_MODE="history"
-VITE_STORAGE_NAME_SPACE="visa-card-h5-"
+VITE_STORAGE_NAME_SPACE="nomo-vpn-"
 
 # 安全配置
-VITE_ENABLE_REQUEST_ENCRYPTION="false"
-VITE_REQUEST_ENCRYPTION_KEY="NL-VisaCard-H5_RequestK"
+VITE_ENABLE_REQUEST_ENCRYPTION="true"
+VITE_REQUEST_ENCRYPTION_KEY="pOK71G7e6ICahmhmnSbrEY/bcQZCOtW4Yi3xOQJ1i4k="
 VITE_ENABLE_STORAGE_ENCRYPTION="true"
-VITE_STORAGE_ENCRYPTION_KEY="NL-VisaCard-H5_StorageK"
+VITE_STORAGE_ENCRYPTION_KEY="NL-NOMO-VPN_StorageK"
+VITE_REQUEST_COMPRESSION="br"
 
 # API 配置
-VITE_API_BASE_URL="/api/v1"
+VITE_API_BASE_URL="https://api.nomo.com"

+ 5 - 1
package.json

@@ -36,9 +36,11 @@
     "antd": "^5.24.4",
     "async-validator": "^4.2.5",
     "axios": "^1.8.4",
+    "brotli-wasm": "3.0.1",
     "copy-to-clipboard": "^3.3.3",
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
+    "fflate": "0.8.2",
     "file-saver": "^2.0.5",
     "firebase": "^11.5.0",
     "husky": "^9.1.7",
@@ -101,7 +103,9 @@
     "vite": "^6.2.2",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-remove-console": "^2.2.0",
-    "vite-plugin-style-import": "^2.0.0"
+    "vite-plugin-style-import": "^2.0.0",
+    "vite-plugin-top-level-await": "1.4.4",
+    "vite-plugin-wasm": "3.5.0"
   },
   "packageManager": "[email protected]",
   "engines": {

+ 274 - 68
pnpm-lock.yaml

@@ -44,6 +44,9 @@ importers:
       axios:
         specifier: ^1.8.4
         version: 1.8.4
+      brotli-wasm:
+        specifier: 3.0.1
+        version: 3.0.1
       copy-to-clipboard:
         specifier: ^3.3.3
         version: 3.3.3
@@ -53,6 +56,9 @@ importers:
       dayjs:
         specifier: ^1.11.13
         version: 1.11.13
+      fflate:
+        specifier: 0.8.2
+        version: 0.8.2
       file-saver:
         specifier: ^2.0.5
         version: 2.0.5
@@ -237,6 +243,12 @@ importers:
       vite-plugin-style-import:
         specifier: ^2.0.0
         version: 2.0.0([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
+      vite-plugin-top-level-await:
+        specifier: 1.4.4
+        version: 1.4.4([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
+      vite-plugin-wasm:
+        specifier: 3.5.0
+        version: 3.5.0([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]))
 
 packages:
 
@@ -359,6 +371,10 @@ packages:
     resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==}
     engines: {node: '>=6.9.0'}
 
+  '@babel/[email protected]':
+    resolution: {integrity: sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==}
+    engines: {node: '>=6.9.0'}
+
   '@babel/[email protected]':
     resolution: {integrity: sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==}
     engines: {node: '>=6.9.0'}
@@ -986,86 +1002,86 @@ packages:
     resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
     engines: {node: '>= 8'}
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [android]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [darwin]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [darwin]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [freebsd]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [linux]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==}
     engines: {node: '>= 10.0.0'}
     cpu: [arm64]
     os: [win32]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==}
     engines: {node: '>= 10.0.0'}
     cpu: [ia32]
     os: [win32]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==}
     engines: {node: '>= 10.0.0'}
     cpu: [x64]
     os: [win32]
 
-  '@parcel/[email protected].1':
-    resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
+  '@parcel/[email protected].6':
+    resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
     engines: {node: '>= 10.0.0'}
 
   '@pkgjs/[email protected]':
@@ -1157,6 +1173,15 @@ packages:
       react: '>=16.9.0'
       react-dom: '>=16.9.0'
 
+  '@rollup/[email protected]':
+    resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
+    engines: {node: '>=14.0.0'}
+    peerDependencies:
+      rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0
+    peerDependenciesMeta:
+      rollup:
+        optional: true
+
   '@rollup/[email protected]':
     resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==}
     engines: {node: '>= 8.0.0'}
@@ -1259,6 +1284,81 @@ packages:
   '@rtsao/[email protected]':
     resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
 
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-QoIupRWVH8AF1TgxYyeA5nS18dtqMuxNwchjBIwJo3RdwLEFiJq6onOx9JAxHtuPwUkIVuU2Xbp+jCJ7Vzmgtg==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-S52Gu1QtPSfBYDiejlcfp9GlN+NjTZBRRNsz8PNwBgSE626/FUf2PcllVUix7jqkoMC+t0rS8t+2/aSWlMuQtA==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-lXJs8oXo6Z4yCpimpQ8vPeCjkgoHu5NoMvmJZ8qxDyU99KVdg6KwU9H79vzrmB+HfH+dCZ7JGMqMF//f8Cfvdg==}
+    engines: {node: '>=10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-chRsz1K52/vj8Mfq/QOugVphlKPWlMh10V99qfH41hbGvwAU6xSPd681upO4bKiOr9+mRIZZW+EfJqY42ZzRyA==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==}
+    engines: {node: '>=10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-6XnzORkZCQzvTQ6cPrU7iaT9+i145oLwnin8JrfsLG41wl26+5cNQ2XV3zcbrnFEV6esjOceom9YO1w9mGJByw==}
+    engines: {node: '>=10'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-IQ2n6af7XKLL6P1gIeZACskSxK8jWtoKpJWLZmdXTDj1MGzktUy4i+FvpdtxFmJWNavRWH1VmTr6kAubRDHeKw==}
+    engines: {node: '>=10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-iLmLTodbYxU39HhMPaMUooPwO/zqJWvsqkrXv1ZI38rMb048p6N7qtAtTp37sw9NzSrvH6oli8EdDygo09IZ/w==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@swc/helpers': '>=0.5.17'
+    peerDependenciesMeta:
+      '@swc/helpers':
+        optional: true
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==}
+
   '@trysound/[email protected]':
     resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
     engines: {node: '>=10.13.0'}
@@ -1626,6 +1726,10 @@ packages:
     resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
     engines: {node: '>=8'}
 
+  [email protected]:
+    resolution: {integrity: sha512-U3K72/JAi3jITpdhZBqzSUq+DUY697tLxOuFXB+FpAE/Ug+5C3VZrv4uA674EUZHxNAuQ9wETXNqQkxZD6oL4A==}
+    engines: {node: '>=v18.0.0'}
+
   [email protected]:
     resolution: {integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==}
     engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
@@ -1943,10 +2047,9 @@ packages:
     resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
     engines: {node: '>=0.4.0'}
 
-  [email protected]:
-    resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
-    engines: {node: '>=0.10'}
-    hasBin: true
+  [email protected]:
+    resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+    engines: {node: '>=8'}
 
   [email protected]:
     resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
@@ -2248,6 +2351,9 @@ packages:
       picomatch:
         optional: true
 
+  [email protected]:
+    resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
+
   [email protected]:
     resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==}
     engines: {node: '>=12.0.0'}
@@ -2522,8 +2628,8 @@ packages:
     engines: {node: '>=0.10.0'}
     hasBin: true
 
-  immutable@5.0.3:
-    resolution: {integrity: sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==}
+  immutable@5.1.4:
+    resolution: {integrity: sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==}
 
   [email protected]:
     resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
@@ -3222,6 +3328,10 @@ packages:
     resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
     engines: {node: '>=12'}
 
+  [email protected]:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
+
   [email protected]:
     resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
     engines: {node: '>=0.10'}
@@ -4220,6 +4330,10 @@ packages:
   [email protected]:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
 
+  [email protected]:
+    resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==}
+    hasBin: true
+
   [email protected]:
     resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
 
@@ -4236,6 +4350,16 @@ packages:
     peerDependencies:
       vite: '>=2.0.0'
 
+  [email protected]:
+    resolution: {integrity: sha512-QyxQbvcMkgt+kDb12m2P8Ed35Sp6nXP+l8ptGrnHV9zgYDUpraO0CPdlqLSeBqvY2DToR52nutDG7mIHuysdiw==}
+    peerDependencies:
+      vite: '>=2.8'
+
+  [email protected]:
+    resolution: {integrity: sha512-X5VWgCnqiQEGb+omhlBVsvTfxikKtoOgAzQ95+BZ8gQ+VfMHIjSHr0wyvXFQCa0eKQ0fKyaL0kWcEnYqBac4lQ==}
+    peerDependencies:
+      vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7
+
   [email protected]:
     resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==}
     engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0}
@@ -4545,6 +4669,9 @@ snapshots:
     dependencies:
       regenerator-runtime: 0.14.1
 
+  '@babel/[email protected]':
+    optional: true
+
   '@babel/[email protected]':
     dependencies:
       '@babel/code-frame': 7.26.2
@@ -5289,65 +5416,65 @@ snapshots:
       '@nodelib/fs.scandir': 2.1.5
       fastq: 1.19.1
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     optional: true
 
-  '@parcel/[email protected].1':
+  '@parcel/[email protected].6':
     dependencies:
-      detect-libc: 1.0.3
+      detect-libc: 2.1.2
       is-glob: 4.0.3
-      micromatch: 4.0.8
       node-addon-api: 7.1.1
+      picomatch: 4.0.3
     optionalDependencies:
-      '@parcel/watcher-android-arm64': 2.5.1
-      '@parcel/watcher-darwin-arm64': 2.5.1
-      '@parcel/watcher-darwin-x64': 2.5.1
-      '@parcel/watcher-freebsd-x64': 2.5.1
-      '@parcel/watcher-linux-arm-glibc': 2.5.1
-      '@parcel/watcher-linux-arm-musl': 2.5.1
-      '@parcel/watcher-linux-arm64-glibc': 2.5.1
-      '@parcel/watcher-linux-arm64-musl': 2.5.1
-      '@parcel/watcher-linux-x64-glibc': 2.5.1
-      '@parcel/watcher-linux-x64-musl': 2.5.1
-      '@parcel/watcher-win32-arm64': 2.5.1
-      '@parcel/watcher-win32-ia32': 2.5.1
-      '@parcel/watcher-win32-x64': 2.5.1
+      '@parcel/watcher-android-arm64': 2.5.6
+      '@parcel/watcher-darwin-arm64': 2.5.6
+      '@parcel/watcher-darwin-x64': 2.5.6
+      '@parcel/watcher-freebsd-x64': 2.5.6
+      '@parcel/watcher-linux-arm-glibc': 2.5.6
+      '@parcel/watcher-linux-arm-musl': 2.5.6
+      '@parcel/watcher-linux-arm64-glibc': 2.5.6
+      '@parcel/watcher-linux-arm64-musl': 2.5.6
+      '@parcel/watcher-linux-x64-glibc': 2.5.6
+      '@parcel/watcher-linux-x64-musl': 2.5.6
+      '@parcel/watcher-win32-arm64': 2.5.6
+      '@parcel/watcher-win32-ia32': 2.5.6
+      '@parcel/watcher-win32-x64': 2.5.6
     optional: true
 
   '@pkgjs/[email protected]':
@@ -5445,6 +5572,10 @@ snapshots:
       react: 18.3.1
       react-dom: 18.3.1([email protected])
 
+  '@rollup/[email protected]([email protected])':
+    optionalDependencies:
+      rollup: 4.36.0
+
   '@rollup/[email protected]':
     dependencies:
       estree-walker: 2.0.2
@@ -5509,6 +5640,58 @@ snapshots:
 
   '@rtsao/[email protected]': {}
 
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    optional: true
+
+  '@swc/[email protected]':
+    dependencies:
+      '@swc/counter': 0.1.3
+      '@swc/types': 0.1.25
+    optionalDependencies:
+      '@swc/core-darwin-arm64': 1.15.11
+      '@swc/core-darwin-x64': 1.15.11
+      '@swc/core-linux-arm-gnueabihf': 1.15.11
+      '@swc/core-linux-arm64-gnu': 1.15.11
+      '@swc/core-linux-arm64-musl': 1.15.11
+      '@swc/core-linux-x64-gnu': 1.15.11
+      '@swc/core-linux-x64-musl': 1.15.11
+      '@swc/core-win32-arm64-msvc': 1.15.11
+      '@swc/core-win32-ia32-msvc': 1.15.11
+      '@swc/core-win32-x64-msvc': 1.15.11
+
+  '@swc/[email protected]': {}
+
+  '@swc/[email protected]':
+    dependencies:
+      '@swc/counter': 0.1.3
+
   '@trysound/[email protected]': {}
 
   '@tybys/[email protected]':
@@ -5974,6 +6157,8 @@ snapshots:
     dependencies:
       fill-range: 7.1.1
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       caniuse-lite: 1.0.30001706
@@ -6272,7 +6457,7 @@ snapshots:
 
   [email protected]:
     dependencies:
-      '@babel/runtime': 7.26.10
+      '@babel/runtime': 7.28.6
     optional: true
 
   [email protected]: {}
@@ -6310,7 +6495,7 @@ snapshots:
 
   [email protected]: {}
 
-  detect-libc@1.0.3:
+  detect-libc@2.1.2:
     optional: true
 
   [email protected]: {}
@@ -6767,6 +6952,8 @@ snapshots:
     optionalDependencies:
       picomatch: 4.0.2
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       flat-cache: 3.2.0
@@ -7076,7 +7263,7 @@ snapshots:
   [email protected]:
     optional: true
 
-  immutable@5.0.3:
+  immutable@5.1.4:
     optional: true
 
   [email protected]:
@@ -7764,6 +7951,9 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]:
+    optional: true
+
   [email protected]: {}
 
   [email protected]: {}
@@ -8434,10 +8624,10 @@ snapshots:
   [email protected]:
     dependencies:
       chokidar: 4.0.3
-      immutable: 5.0.3
+      immutable: 5.1.4
       source-map-js: 1.2.1
     optionalDependencies:
-      '@parcel/watcher': 2.5.1
+      '@parcel/watcher': 2.5.6
     optional: true
 
   [email protected]:
@@ -9004,6 +9194,8 @@ snapshots:
 
   [email protected]: {}
 
+  [email protected]: {}
+
   [email protected]:
     dependencies:
       spdx-correct: 3.2.0
@@ -9031,6 +9223,20 @@ snapshots:
       pathe: 0.2.0
       vite: 6.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
 
+  [email protected]([email protected])([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])):
+    dependencies:
+      '@rollup/plugin-virtual': 3.0.2([email protected])
+      '@swc/core': 1.15.11
+      uuid: 10.0.0
+      vite: 6.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
+    transitivePeerDependencies:
+      - '@swc/helpers'
+      - rollup
+
+  [email protected]([email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected])):
+    dependencies:
+      vite: 6.2.2(@types/[email protected])([email protected])([email protected])([email protected])([email protected])
+
   [email protected](@types/[email protected])([email protected])([email protected])([email protected])([email protected]):
     dependencies:
       esbuild: 0.25.1

+ 1 - 0
src/config/index.ts

@@ -24,6 +24,7 @@ const config: Config = {
         requestEncryptionKey: import.meta.env.VITE_REQUEST_ENCRYPTION_KEY ?? 'unknown',
         enableStorageEncryption: import.meta.env.VITE_ENABLE_STORAGE_ENCRYPTION === 'true',
         storageEncryptionKey: import.meta.env.VITE_STORAGE_ENCRYPTION_KEY ?? 'unknown',
+        compressMethod: import.meta.env.VITE_REQUEST_COMPRESSION ?? 'br',
     },
     api: {
         baseURL: import.meta.env.VITE_API_BASE_URL!,

+ 12 - 4
src/config/request/authHeaderInterceptor.ts

@@ -1,6 +1,6 @@
 import { fetchRefreshToken } from '@/services/login';
 import { formatToken, getToken, setToken } from '@/utils/authUtils';
-import { md5, sha1 } from '@/utils/crypto';
+import { stringMd5, stringSha1 } from '@/utils/crypto';
 import { IRequestInterceptorAxios, RequestConfig } from '@/utils/request/types';
 import { currentUnixTimestamp } from '@/utils/timeUtils';
 
@@ -51,10 +51,18 @@ class TokenRefresh {
 }
 
 export const authHeaderInterceptor: IRequestInterceptorAxios = (config: RequestConfig) => {
-    const ts = currentUnixTimestamp();
     config.headers = config.headers ?? {};
-    config.headers['X-Request-Sign'] = sha1(ts + md5(navigator.userAgent ?? ''));
-    config.headers['X-Request-Timestamp'] = ts;
+    const existingTs = config.headers['X-Request-Timestamp'];
+    let ts: string | number;
+    if (existingTs != null && existingTs !== '') {
+        ts = String(existingTs);
+    } else {
+        ts = currentUnixTimestamp();
+        config.headers['X-Request-Timestamp'] = ts;
+    }
+    config.headers['X-Request-Sign'] = stringSha1(
+        String(ts) + stringMd5(navigator.userAgent ?? '')
+    );
     if (config.requireToken !== true) return config;
 
     const data = getToken();

+ 84 - 20
src/config/request/encryptionInterceptors.ts

@@ -1,5 +1,12 @@
 import globalConfig from '@/config';
-import { aesEncrypt, aesDecrypt } from '@/utils/crypto';
+import { bytesBase64decode } from '@/utils/crypto';
+import {
+    CompressMethod,
+    decryptResponsePayload,
+    encryptRequestPayload,
+} from '@/utils/requestCrypto';
+import { bytesToString } from '@/utils/bytesUtils';
+import { currentUnixTimestamp } from '@/utils/timeUtils';
 import {
     IRequestInterceptorAxios,
     IResponseInterceptor,
@@ -8,26 +15,57 @@ import {
 
 /**
  * 请求数据加密拦截器
- * 对请求数据进行加密
+ * 将请求体加密为二进制(可选压缩 + 时间戳 + AES-CBC),以二进制发送
  */
-export const requestEncryptionInterceptor: IRequestInterceptorAxios = (config) => {
-    const { enabled: optEnableEncryption, key: optEncryptionKey } =
-        (config as RequestConfig)?.encryption ?? {};
+export const requestEncryptionInterceptor: IRequestInterceptorAxios = async (config) => {
+    const {
+        enabled: optEnableEncryption,
+        key: optEncryptionKey,
+        compressMethod: optCompressMethod,
+    } = (config as RequestConfig)?.encryption ?? {};
 
     const enableEncryption = optEnableEncryption ?? globalConfig.security.enableRequestEncryption;
     const encryptionKey = optEncryptionKey ?? globalConfig.security.requestEncryptionKey;
+    const compressMethod = optCompressMethod ?? globalConfig.security.compressMethod;
 
     if (!enableEncryption) {
         return config;
     }
 
     if (['post', 'put'].includes(config.method?.toLowerCase() || '') && config.data) {
-        config.headers = {
-            ...(config.headers ?? {}),
-            'X-Request-Encrypted': 'true',
-            'Content-Type': 'text/plain',
-        };
-        config.data = aesEncrypt(JSON.stringify(config.data), encryptionKey);
+        // 指定后端返回二进制数据
+        config.responseType = 'arraybuffer';
+
+        // 设置 Content-Type 为 application/octet-stream
+        config.headers = config.headers ?? {};
+        config.headers['Content-Type'] = 'application/octet-stream';
+
+        // 指定接口数据压缩方式
+        config.headers['X-NL-Content-Encoding'] = compressMethod;
+
+        // 设置 X-Request-Timestamp
+        const existingTs = config.headers['X-Request-Timestamp'];
+        let timestamp: number;
+        if (existingTs != null && existingTs !== '') {
+            timestamp = Number(existingTs);
+            if (Number.isNaN(timestamp)) timestamp = currentUnixTimestamp();
+        } else {
+            timestamp = currentUnixTimestamp();
+            config.headers['X-Request-Timestamp'] = timestamp;
+        }
+
+        // 加密请求体
+        const keyBytes = bytesBase64decode(encryptionKey);
+        const dataBytes = new TextEncoder().encode(JSON.stringify(config.data));
+        const encrypted = await encryptRequestPayload(
+            dataBytes,
+            timestamp,
+            keyBytes,
+            compressMethod as CompressMethod
+        );
+
+        // 设置请求体
+        config.data = encrypted.buffer;
     }
 
     return config;
@@ -35,27 +73,53 @@ export const requestEncryptionInterceptor: IRequestInterceptorAxios = (config) =
 
 /**
  * 响应数据解密拦截器
- * 对响应数据进行解密
+ * 将服务端返回的二进制密文解密(AES-CBC → 去时间戳 → 解压)并解析为 JSON
  */
-export const responseDecryptionInterceptor: IResponseInterceptor = (response) => {
-    const { enabled: optEnableEncryption, key: optEncryptionKey } =
-        (response.config as RequestConfig)?.encryption ?? {};
+export const responseDecryptionInterceptor: IResponseInterceptor = async (response) => {
+    const {
+        enabled: optEnableEncryption,
+        key: optEncryptionKey,
+        compressMethod: optCompressMethod,
+    } = (response.config as RequestConfig)?.encryption ?? {};
 
     const enableEncryption = optEnableEncryption ?? globalConfig.security.enableRequestEncryption;
     const encryptionKey = optEncryptionKey ?? globalConfig.security.requestEncryptionKey;
+    const compressMethod = optCompressMethod ?? globalConfig.security.compressMethod;
 
     if (!enableEncryption) {
         return response;
     }
 
-    const isEncrypted = response.headers?.['X-Response-Encrypted'] === 'true';
+    // 后端永远加密响应体,但不设置 X-Response-Encrypted,因此不用判断 header 中的 X-Response-Encrypted 字段
+    // const isEncrypted = response.headers?.['X-Response-Encrypted'] === 'true';
+    const isEncrypted = true;
     if (isEncrypted && response.data) {
-        const decryptedData = aesDecrypt(response.data as string, encryptionKey);
-        if (decryptedData) {
+        const raw = response.data as ArrayBuffer | unknown;
+        const encryptedBytes = new Uint8Array(
+            raw instanceof ArrayBuffer ? raw : (raw as ArrayBuffer)
+        );
+        const keyBytes = bytesBase64decode(encryptionKey);
+        const decrypted = await decryptResponsePayload(
+            encryptedBytes,
+            keyBytes,
+            compressMethod as CompressMethod
+        );
+
+        if (decrypted?.data?.length) {
             try {
-                response.data = JSON.parse(decryptedData);
+                const dataStr = bytesToString(decrypted.data);
+                console.log(
+                    '[responseDecryptionInterceptor] Payload parsed. Decrypted timestamp:',
+                    decrypted.timestamp,
+                    '| Body string:',
+                    dataStr
+                );
+                response.data = JSON.parse(dataStr);
             } catch (error) {
-                console.error('Failed to parse decrypted response data:', error);
+                console.error(
+                    '[responseDecryptionInterceptor] Failed to parse decrypted response data:',
+                    error
+                );
             }
         }
     }

+ 2 - 0
src/config/request/index.ts

@@ -17,6 +17,8 @@ const config: RequestConfig = {
         Accept: 'application/json, text/plain, */*',
         'Content-Type': 'application/json',
         'X-Requested-With': 'XMLHttpRequest',
+        'X-NL-Product-Code': import.meta.env.VITE_API_PRODUCT_CODE!, // 每次请求必须包含产品代码
+        'X-NL-Request-Type': "web", // 请求类型,用于区分请求来源
     },
     paramsSerializer: (params) => stringify(params),
     ...errorConfig,

+ 4 - 0
src/config/types.ts

@@ -1,3 +1,5 @@
+import { CompressMethod } from "@/utils/requestCrypto";
+
 export interface Config {
     app: {
         /**应用标题 */
@@ -18,6 +20,8 @@ export interface Config {
         enableStorageEncryption: boolean;
         /**存储加密密钥 */
         storageEncryptionKey: string;
+        /**压缩方法 */
+        compressMethod: CompressMethod;
     };
     api: {
         /**API基础URL */

+ 9 - 7
src/pages/redirect/index.tsx

@@ -3,6 +3,7 @@ import React, { useEffect } from 'react';
 import { useNavigate, useSearchParams } from 'react-router-dom';
 
 import { setToken } from '@/utils/authUtils';
+import { decryptUrlParams } from '@/utils/requestCrypto';
 
 /**
  * 解密重定向参数
@@ -11,10 +12,8 @@ import { setToken } from '@/utils/authUtils';
  */
 const decryptRedirectParams = async (
     _encryptedData: string
-): Promise<{ userInfo: API.UserInfo; redirectPath: string } | null> => {
-    // TODO: 实现解密逻辑
-    // 这里先返回 null,等待后续实现
-    return null;
+): Promise<{ userConfig: API.UserInfo; redirectPath: string } | null> => {
+    return decryptUrlParams<{ userConfig: API.UserInfo; redirectPath: string }>(_encryptedData);
 };
 
 const Redirect: React.FC = () => {
@@ -23,6 +22,7 @@ const Redirect: React.FC = () => {
 
     useEffect(() => {
         const redirectParam = searchParams.get('d');
+        console.log('redirectParam', redirectParam);
 
         // 如果没有重定向参数,默认跳转到 home 页
         if (!redirectParam) {
@@ -36,17 +36,19 @@ const Redirect: React.FC = () => {
                 // 解密重定向参数
                 const decryptedData = await decryptRedirectParams(redirectParam);
 
+                console.log('decryptedData', decryptedData);
+
                 if (!decryptedData) {
                     // 解密失败,跳转到 home 页
                     navigate('/home', { replace: true });
                     return;
                 }
 
-                const { userInfo, redirectPath } = decryptedData;
+                const { userConfig, redirectPath } = decryptedData;
 
                 // 保存用户信息到 localStorage
-                if (userInfo) {
-                    setToken(userInfo);
+                if (userConfig) {
+                    setToken(userConfig);
                 }
 
                 // 跳转到指定路由

+ 91 - 0
src/utils/bytesUtils.ts

@@ -0,0 +1,91 @@
+export enum Endian {
+    BE = 'be',
+    LE = 'le',
+}
+
+const UTF8_ENCODER = new TextEncoder();
+const UTF8_DECODER = new TextDecoder();
+
+function swapWord32Bytes(bytes: Uint8Array): Uint8Array {
+    const out = new Uint8Array(bytes.length);
+    let i = 0;
+    for (; i + 4 <= bytes.length; i += 4) {
+        out[i] = bytes[i + 3]!;
+        out[i + 1] = bytes[i + 2]!;
+        out[i + 2] = bytes[i + 1]!;
+        out[i + 3] = bytes[i]!;
+    }
+    for (; i < bytes.length; i++) out[i] = bytes[i]!;
+    return out;
+}
+
+/** 将小端数据转换为大端数据 */
+export function littleEndianToBigEndian(bytes: Uint8Array): Uint8Array {
+    return swapWord32Bytes(bytes);
+}
+
+/** 将大端数据转换为小端数据 */
+export function bigEndianToLittleEndian(bytes: Uint8Array): Uint8Array {
+    return swapWord32Bytes(bytes);
+}
+
+/** 将数字转为 4 字节二进制;endian 为 'be' 大端 或 'le' 小端 */
+export function numberToBytes(value: number, endian: Endian = Endian.BE): Uint8Array {
+    const buf = new Uint8Array(4);
+    numberToBytesAt(value, buf, 0, endian);
+    return buf;
+}
+
+/** 在 buffer 的 offset 处写入 4 字节无符号整数 */
+export function numberToBytesAt(
+    value: number,
+    buffer: Uint8Array,
+    offset: number,
+    endian: Endian = Endian.BE
+): void {
+    if (endian === Endian.BE) {
+        buffer[offset] = (value >>> 24) & 0xff;
+        buffer[offset + 1] = (value >>> 16) & 0xff;
+        buffer[offset + 2] = (value >>> 8) & 0xff;
+        buffer[offset + 3] = value & 0xff;
+    } else {
+        buffer[offset] = value & 0xff;
+        buffer[offset + 1] = (value >>> 8) & 0xff;
+        buffer[offset + 2] = (value >>> 16) & 0xff;
+        buffer[offset + 3] = (value >>> 24) & 0xff;
+    }
+}
+
+/** 从 buffer 的 offset 处读取 4 字节无符号整数 */
+export function bytesToNumber(
+    buffer: Uint8Array,
+    offset: number,
+    endian: Endian = Endian.BE
+): number {
+    if (endian === 'be') {
+        return (
+            ((buffer[offset]! << 24) |
+                (buffer[offset + 1]! << 16) |
+                (buffer[offset + 2]! << 8) |
+                buffer[offset + 3]!) >>>
+            0
+        );
+    }
+    return (
+        (buffer[offset]! |
+            (buffer[offset + 1]! << 8) |
+            (buffer[offset + 2]! << 16) |
+            (buffer[offset + 3]! << 24)) >>>
+        0
+    );
+}
+
+/** 将 UTF-8 字符串转为二进制 */
+export function stringToBytes(str: string): Uint8Array {
+    return UTF8_ENCODER.encode(str);
+}
+
+/** 将 UTF-8 二进制转为字符串 */
+export function bytesToString(bytes: Uint8Array): string {
+    return UTF8_DECODER.decode(bytes);
+}

+ 49 - 0
src/utils/compress.ts

@@ -0,0 +1,49 @@
+import brotliWasm from 'brotli-wasm';
+import { gzipSync, gunzipSync } from 'fflate';
+
+export enum CompressFormat {
+    GZIP = 'gzip',
+    BROTLI = 'brotli',
+}
+
+let brotliModule: Awaited<typeof brotliWasm> | null = null;
+
+async function getBrotli() {
+    if (brotliModule) return brotliModule;
+    brotliModule = await brotliWasm;
+    return brotliModule;
+}
+
+/**
+ * 使用 fflate(brotli-wasm) 压缩二进制数据
+ * @param bytes 原始二进制数据
+ * @param format 压缩格式
+ * @returns 压缩后的二进制数据
+ */
+export async function compressBytes(
+    bytes: Uint8Array,
+    format: CompressFormat
+): Promise<Uint8Array> {
+    if (format === CompressFormat.GZIP) {
+        return gzipSync(bytes);
+    }
+    const brotli = await getBrotli();
+    return brotli.compress(bytes);
+}
+
+/**
+ * 使用 fflate(brotli-wasm) 解压二进制数据
+ * @param bytes 压缩后的二进制数据
+ * @param format 压缩格式(需与压缩时一致)
+ * @returns 解压后的二进制数据
+ */
+export async function decompressBytes(
+    bytes: Uint8Array,
+    format: CompressFormat
+): Promise<Uint8Array> {
+    if (format === CompressFormat.GZIP) {
+        return gunzipSync(bytes);
+    }
+    const brotli = await getBrotli();
+    return brotli.decompress(bytes);
+}

+ 409 - 51
src/utils/crypto/index.ts

@@ -1,24 +1,115 @@
+import CryptoJS from 'crypto-js/core';
+import 'crypto-js/lib-typedarrays';
+import 'crypto-js/cipher-core';
 import AES from 'crypto-js/aes';
 import encBase64 from 'crypto-js/enc-base64';
 import encUtf8 from 'crypto-js/enc-utf8';
+import modeCtr from 'crypto-js/mode-ctr';
+import modeEcb from 'crypto-js/mode-ecb';
 import MD5 from 'crypto-js/md5';
 import Rabbit from 'crypto-js/rabbit';
 import SHA1 from 'crypto-js/sha1';
 import SHA256 from 'crypto-js/sha256';
 
-export function md5(str: string, lowerCase?: boolean): string {
+import { bigEndianToLittleEndian, littleEndianToBigEndian } from '@/utils/bytesUtils';
+
+// ---------------------------------------------------------------------------
+// 常量
+// ---------------------------------------------------------------------------
+
+const WordArray = CryptoJS.lib.WordArray as typeof CryptoJS.lib.WordArray & {
+    create(words?: number[] | Uint8Array, sigBytes?: number): CryptoJS.lib.WordArray;
+};
+const CipherParams = CryptoJS.lib.CipherParams as {
+    create(cfg: {
+        ciphertext: CryptoJS.lib.WordArray;
+        iv?: CryptoJS.lib.WordArray;
+    }): CryptoJS.lib.CipherParams;
+};
+
+const KEY_CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
+const KEY_CHARS_LEN = KEY_CHARS.length;
+const MAX_ALLOWED = 256 - (256 % KEY_CHARS_LEN);
+
+const AES_KEY_BYTES = 32;
+const AES_IV_BYTES = 16;
+const RABBIT_KEY_BYTES = 16;
+const RABBIT_IV_BYTES = 8;
+
+// ---------------------------------------------------------------------------
+// 内部工具函数
+// ---------------------------------------------------------------------------
+
+function wordsToBytes(words: number[], sigBytes: number, littleEndian?: boolean): Uint8Array {
+    const out = new Uint8Array(sigBytes);
+    for (let i = 0; i < sigBytes; i++) {
+        out[i] = (words[i >>> 2]! >>> (24 - (i % 4) * 8)) & 0xff;
+    }
+    return littleEndian ? bigEndianToLittleEndian(out) : out;
+}
+
+function bytesToWordArray(bytes: Uint8Array, littleEndian?: boolean): CryptoJS.lib.WordArray {
+    const input = littleEndian ? littleEndianToBigEndian(bytes) : bytes;
+    return WordArray.create(input);
+}
+
+function wordArrayToBytes(wa: CryptoJS.lib.WordArray, littleEndian?: boolean): Uint8Array {
+    return wordsToBytes(wa.words, wa.sigBytes, littleEndian);
+}
+
+function padBytes(buf: Uint8Array, len: number): Uint8Array {
+    if (buf.length >= len) return buf.subarray(0, len);
+    const out = new Uint8Array(len);
+    out.set(buf);
+    return out;
+}
+
+/**
+ * 计算字符串的 MD5
+ */
+export function stringMd5(str: string, lowerCase?: boolean): string {
     return lowerCase ? String(MD5(str)).toLowerCase() : String(MD5(str)).toUpperCase();
 }
 
-export function sha1(str: string, lowerCase?: boolean): string {
+/**
+ * 计算二进制数据的 MD5,返回 16 字节
+ */
+export function bytesMd5(bytes: Uint8Array): Uint8Array {
+    return wordArrayToBytes(MD5(bytesToWordArray(bytes)) as CryptoJS.lib.WordArray);
+}
+
+/**
+ * 计算字符串的 SHA-1
+ */
+export function stringSha1(str: string, lowerCase?: boolean): string {
     return lowerCase ? String(SHA1(str)).toLowerCase() : String(SHA1(str)).toUpperCase();
 }
 
-export function sha256(str: string, lowerCase?: boolean): string {
+/**
+ * 计算二进制数据的 SHA-1,返回 20 字节
+ */
+export function bytesSha1(bytes: Uint8Array): Uint8Array {
+    return wordArrayToBytes(SHA1(bytesToWordArray(bytes)) as CryptoJS.lib.WordArray);
+}
+
+/**
+ * 计算字符串的 SHA-256
+ */
+export function stringSha256(str: string, lowerCase?: boolean): string {
     return lowerCase ? String(SHA256(str)).toLowerCase() : String(SHA256(str)).toUpperCase();
 }
 
-export function base64encode(raw: string): string {
+/**
+ * 计算二进制数据的 SHA-256,返回 32 字节
+ */
+export function bytesSha256(bytes: Uint8Array): Uint8Array {
+    return wordArrayToBytes(SHA256(bytesToWordArray(bytes)) as CryptoJS.lib.WordArray);
+}
+
+/**
+ * 对字符串进行 Base64 编码
+ */
+export function stringBase64encode(raw: string): string {
     try {
         return encBase64.stringify(encUtf8.parse(raw));
     } catch {
@@ -26,7 +117,21 @@ export function base64encode(raw: string): string {
     }
 }
 
-export function base64decode(str: string): string {
+/**
+ * 对二进制数据进行 Base64 编码
+ */
+export function bytesBase64encode(bytes: Uint8Array): string {
+    try {
+        return encBase64.stringify(bytesToWordArray(bytes));
+    } catch {
+        return '';
+    }
+}
+
+/**
+ * 对字符串进行 Base64 解码
+ */
+export function stringBase64decode(str: string): string {
     try {
         return encBase64.parse(str).toString(encUtf8);
     } catch {
@@ -35,41 +140,62 @@ export function base64decode(str: string): string {
 }
 
 /**
- * 生成随机密钥
+ * 对 Base64 字符串解码为二进制数据
+ */
+export function bytesBase64decode(str: string): Uint8Array {
+    try {
+        return wordArrayToBytes(encBase64.parse(str));
+    } catch {
+        return new Uint8Array(0);
+    }
+}
+
+/**
+ * 生成随机密钥 (字符串)
  * @param length 密钥长度
  * @returns 随机密钥
  */
-export const generateKey = (length: number = 32): string => {
-    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
-    let result = '';
-    for (let i = 0; i < length; i++) {
-        result += chars.charAt(Math.floor(Math.random() * chars.length));
+export const generateKeyString = (length: number = 32): string => {
+    const result: string[] = [];
+    const bufferSize = Math.ceil((length * 256) / MAX_ALLOWED) + 16;
+    const buffer = new Uint8Array(bufferSize);
+    let pos = 0;
+    while (result.length < length) {
+        if (pos >= buffer.length) {
+            crypto.getRandomValues(buffer);
+            pos = 0;
+        }
+        const byte = buffer[pos++];
+        if (byte < MAX_ALLOWED) {
+            result.push(KEY_CHARS[byte % KEY_CHARS_LEN]);
+        }
     }
-    return result;
+    return result.join('');
 };
 
 /**
- * 生成随机 IV
- * @param length 初始化向量长度
- * @returns 16 字节的随机 IV
+ * 生成随机密钥 (二进制)
+ * @param length 密钥长度
+ * @returns 随机密钥
  */
-const generateIV = (length: number = 16): string => {
-    return generateKey(length);
+export const generateKeyBytes = (length: number): Uint8Array => {
+    const out = new Uint8Array(length);
+    crypto.getRandomValues(out);
+    return out;
 };
 
 /**
- * 加密数据
- * @param data 要加密的字符串数据
- * @param key 加密密钥
- * @param iv 可选的初始化向量,如果不提供则随机生成
- * @returns 加密后的字符串(如果未提供 IV,则包含 IV)
+ * AES-CBC 加密(字符串)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 字符'0'。
+ *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 字符'0'。
  */
-export const aesEncrypt = (data: string, key: string, iv?: string): string => {
+export const aesCbcEncryptString = (data: string, key: string, iv?: string): string => {
     if (!data || !key) return '';
-    // aes算法要求密钥长度为 16、24 或 32 字节,因此我们这里保证密钥长度为 32 个字符
     const paddedKey = key.padEnd(32, '0').slice(0, 32);
-    // aes算法要求 IV 长度为 16 字节,因此我们这里保证 IV 长度为 16 个字符
-    const ivValue = iv ? iv.padEnd(16, '0').slice(0, 16) : generateIV(16);
+    const ivValue = iv ? iv.padEnd(16, '0').slice(0, 16) : generateKeyString(16);
     const keyHex = encUtf8.parse(paddedKey);
     const ivHex = encUtf8.parse(ivValue);
     const encrypted = AES.encrypt(data, keyHex, { iv: ivHex });
@@ -77,18 +203,35 @@ export const aesEncrypt = (data: string, key: string, iv?: string): string => {
 };
 
 /**
- * 解密数据
- * @param encryptedData 加密的字符串(如果加密时未提供 IV,则包含 IV)
- * @param key 解密密钥
- * @param iv 可选的初始化向量,如果不提供则从加密数据中提取
- * @returns 解密后的字符串
+ * AES-CBC 加密(二进制)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 数字0。
+ *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 数字0。
  */
-export const aesDecrypt = (encryptedData: string, key: string, iv?: string): string => {
+export function aesCbcEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8Array): Uint8Array {
+    if (!data.length || !key.length) return new Uint8Array(0);
+    const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+    const ivWa = iv
+        ? bytesToWordArray(padBytes(iv, AES_IV_BYTES))
+        : bytesToWordArray(generateKeyBytes(AES_IV_BYTES));
+    const encrypted = AES.encrypt(bytesToWordArray(data), keyWa, { iv: ivWa });
+    const ct = wordArrayToBytes(encrypted.ciphertext);
+    if (iv) return ct;
+    const out = new Uint8Array(AES_IV_BYTES + ct.length);
+    out.set(wordArrayToBytes(ivWa), 0);
+    out.set(ct, AES_IV_BYTES);
+    return out;
+}
+
+/**
+ * AES-CBC 解密(字符串)
+ */
+export const aesCbcDecryptString = (encryptedData: string, key: string, iv?: string): string => {
     if (!encryptedData || !key) return '';
     try {
-        // aes算法要求密钥长度为 16、24 或 32 字节,因此我们这里保证密钥长度为 32 个字符
         const paddedKey = key.padEnd(32, '0').slice(0, 32);
-        // aes算法要求 IV 长度为 16 字节,因此我们这里保证 IV 长度为 16 个字符
         const ivValue = iv ? iv.padEnd(16, '0').slice(0, 16) : encryptedData.slice(0, 16);
         const data = iv ? encryptedData : encryptedData.slice(16);
         const keyHex = encUtf8.parse(paddedKey);
@@ -102,18 +245,188 @@ export const aesDecrypt = (encryptedData: string, key: string, iv?: string): str
 };
 
 /**
- * Rabbit 加密
- * @param data 要加密的数据
- * @param key 加密密钥
- * @param iv 可选的初始化向量,如果不提供则随机生成
- * @returns 加密后的数据(如果未提供 IV,则包含 IV)
+ * AES-CBC 解密(二进制)
+ */
+export function aesCbcDecryptBytes(
+    encryptedData: Uint8Array,
+    key: Uint8Array,
+    iv?: Uint8Array
+): Uint8Array {
+    if (!encryptedData.length || !key.length) return new Uint8Array(0);
+    try {
+        const ivBytes = iv ? padBytes(iv, AES_IV_BYTES) : encryptedData.subarray(0, AES_IV_BYTES);
+        const ctBytes = iv ? encryptedData : encryptedData.subarray(AES_IV_BYTES);
+        const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+        const ivWa = bytesToWordArray(ivBytes);
+        const cp = CipherParams.create({
+            ciphertext: bytesToWordArray(ctBytes),
+            iv: ivWa,
+        });
+        const decrypted = AES.decrypt(cp, keyWa, { iv: ivWa });
+        return wordArrayToBytes(decrypted);
+    } catch (error) {
+        console.error('AES-CBC decrypt bytes error:', error);
+        return new Uint8Array(0);
+    }
+}
+
+/**
+ * AES-CTR 加密(字符串)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 字符'0'。
+ *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 字符'0'。
  */
-export const rabbitEncrypt = (data: string, key: string, iv?: string): string => {
+export const aesCtrEncryptString = (data: string, key: string, iv?: string): string => {
+    if (!data || !key) return '';
+    const paddedKey = key.padEnd(32, '0').slice(0, 32);
+    const ivValue = iv ? iv.padEnd(16, '0').slice(0, 16) : generateKeyString(16);
+    const keyHex = encUtf8.parse(paddedKey);
+    const ivHex = encUtf8.parse(ivValue);
+    const encrypted = AES.encrypt(data, keyHex, { iv: ivHex, mode: modeCtr });
+    return iv ? encrypted.toString() : ivValue + encrypted.toString();
+};
+
+/**
+ * AES-CTR 加密(二进制)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 数字0。
+ *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 数字0。
+ */
+export function aesCtrEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8Array): Uint8Array {
+    if (!data.length || !key.length) return new Uint8Array(0);
+    const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+    const ivWa = iv
+        ? bytesToWordArray(padBytes(iv, AES_IV_BYTES))
+        : bytesToWordArray(generateKeyBytes(AES_IV_BYTES));
+    const encrypted = AES.encrypt(bytesToWordArray(data), keyWa, { iv: ivWa, mode: modeCtr });
+    const ct = wordArrayToBytes(encrypted.ciphertext);
+    if (iv) return ct;
+    const out = new Uint8Array(AES_IV_BYTES + ct.length);
+    out.set(wordArrayToBytes(ivWa), 0);
+    out.set(ct, AES_IV_BYTES);
+    return out;
+}
+
+/**
+ * AES-CTR 解密(字符串)
+ */
+export const aesCtrDecryptString = (encryptedData: string, key: string, iv?: string): string => {
+    if (!encryptedData || !key) return '';
+    try {
+        const paddedKey = key.padEnd(32, '0').slice(0, 32);
+        const ivValue = iv ? iv.padEnd(16, '0').slice(0, 16) : encryptedData.slice(0, 16);
+        const data = iv ? encryptedData : encryptedData.slice(16);
+        const keyHex = encUtf8.parse(paddedKey);
+        const ivHex = encUtf8.parse(ivValue);
+        const decrypted = AES.decrypt(data, keyHex, { iv: ivHex, mode: modeCtr });
+        return decrypted.toString(encUtf8);
+    } catch (error) {
+        console.error('AES-CTR decrypt error:', error);
+        return '';
+    }
+};
+
+/**
+ * AES-CTR 解密(二进制)
+ */
+export function aesCtrDecryptBytes(
+    encryptedData: Uint8Array,
+    key: Uint8Array,
+    iv?: Uint8Array
+): Uint8Array {
+    if (!encryptedData.length || !key.length) return new Uint8Array(0);
+    try {
+        const ivBytes = iv ? padBytes(iv, AES_IV_BYTES) : encryptedData.subarray(0, AES_IV_BYTES);
+        const ctBytes = iv ? encryptedData : encryptedData.subarray(AES_IV_BYTES);
+        const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+        const ivWa = bytesToWordArray(ivBytes);
+        const cp = CipherParams.create({
+            ciphertext: bytesToWordArray(ctBytes),
+            iv: ivWa,
+        });
+        const decrypted = AES.decrypt(cp, keyWa, { iv: ivWa, mode: modeCtr });
+        return wordArrayToBytes(decrypted);
+    } catch (error) {
+        console.error('AES-CTR decrypt bytes error:', error);
+        return new Uint8Array(0);
+    }
+}
+
+/**
+ * AES-ECB 加密(字符串,无 IV,不推荐用于敏感数据)
+ *
+ * 注意:
+ *   - 如果 key 的长度不足32位,则会在末尾补齐 字符'0'。
+ */
+export const aesEcbEncryptString = (data: string, key: string): string => {
+    if (!data || !key) return '';
+    const paddedKey = key.padEnd(32, '0').slice(0, 32);
+    const keyHex = encUtf8.parse(paddedKey);
+    const encrypted = AES.encrypt(data, keyHex, { mode: modeEcb });
+    return encrypted.toString();
+};
+
+/**
+ * AES-ECB 加密(二进制,无 IV)
+ *
+ * 注意:
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 数字0。
+ */
+export function aesEcbEncryptBytes(data: Uint8Array, key: Uint8Array): Uint8Array {
+    if (!data.length || !key.length) return new Uint8Array(0);
+    const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+    const encrypted = AES.encrypt(bytesToWordArray(data), keyWa, { mode: modeEcb });
+    return wordArrayToBytes(encrypted.ciphertext);
+}
+
+/**
+ * AES-ECB 解密(字符串)
+ */
+export const aesEcbDecryptString = (encryptedData: string, key: string): string => {
+    if (!encryptedData || !key) return '';
+    try {
+        const paddedKey = key.padEnd(32, '0').slice(0, 32);
+        const keyHex = encUtf8.parse(paddedKey);
+        const decrypted = AES.decrypt(encryptedData, keyHex, { mode: modeEcb });
+        return decrypted.toString(encUtf8);
+    } catch (error) {
+        console.error('AES-ECB decrypt error:', error);
+        return '';
+    }
+};
+
+/**
+ * AES-ECB 解密(二进制)
+ */
+export function aesEcbDecryptBytes(encryptedData: Uint8Array, key: Uint8Array): Uint8Array {
+    if (!encryptedData.length || !key.length) return new Uint8Array(0);
+    try {
+        const keyWa = bytesToWordArray(padBytes(key, AES_KEY_BYTES));
+        const cp = CipherParams.create({ ciphertext: bytesToWordArray(encryptedData) });
+        const decrypted = AES.decrypt(cp, keyWa, { mode: modeEcb });
+        return wordArrayToBytes(decrypted);
+    } catch (error) {
+        console.error('AES-ECB decrypt bytes error:', error);
+        return new Uint8Array(0);
+    }
+}
+
+/**
+ * Rabbit 加密(字符串)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 32 位,则会在末尾补齐 字符'0'。
+ *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 字符'0'。
+ */
+export const rabbitEncryptString = (data: string, key: string, iv?: string): string => {
     if (!data || !key) return '';
-    // Rabbit 算法要求密钥长度为 16 字节,因此我们这里保证密钥长度为 16 个字符
     const paddedKey = key.padEnd(16, '0').slice(0, 16);
-    // Rabbit 算法要求 IV 长度为 8 字节,因此我们这里保证 IV 长度为 8 个字符
-    const ivValue = iv ? iv.padEnd(8, '0').slice(0, 8) : generateIV(8);
+    const ivValue = iv ? iv.padEnd(8, '0').slice(0, 8) : generateKeyString(8);
     const keyHex = encUtf8.parse(paddedKey);
     const ivHex = encUtf8.parse(ivValue);
     const encrypted = Rabbit.encrypt(data, keyHex, { iv: ivHex });
@@ -121,18 +434,35 @@ export const rabbitEncrypt = (data: string, key: string, iv?: string): string =>
 };
 
 /**
- * Rabbit 解密
- * @param encryptedData 加密的数据(如果加密时未提供 IV,则包含 IV)
- * @param key 解密密钥
- * @param iv 可选的初始化向量,如果不提供则从加密数据中提取
- * @returns 解密后的数据
+ * Rabbit 加密(二进制)
+ *
+ * 注意:
+ *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
+ *   - 如果 key 的长度不足 16 位,则会在末尾补齐 数字0。
+ *   - 如果 iv 的长度不足 8 位,则会在末尾补齐 数字0。
  */
-export const rabbitDecrypt = (encryptedData: string, key: string, iv?: string): string => {
+export function rabbitEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8Array): Uint8Array {
+    if (!data.length || !key.length) return new Uint8Array(0);
+    const keyWa = bytesToWordArray(padBytes(key, RABBIT_KEY_BYTES));
+    const ivWa = iv
+        ? bytesToWordArray(padBytes(iv, RABBIT_IV_BYTES))
+        : bytesToWordArray(generateKeyBytes(RABBIT_IV_BYTES));
+    const encrypted = Rabbit.encrypt(bytesToWordArray(data), keyWa, { iv: ivWa });
+    const ct = wordArrayToBytes(encrypted.ciphertext);
+    if (iv) return ct;
+    const out = new Uint8Array(RABBIT_IV_BYTES + ct.length);
+    out.set(wordArrayToBytes(ivWa), 0);
+    out.set(ct, RABBIT_IV_BYTES);
+    return out;
+}
+
+/**
+ * Rabbit 解密(字符串)
+ */
+export const rabbitDecryptString = (encryptedData: string, key: string, iv?: string): string => {
     if (!encryptedData || !key) return '';
     try {
-        // Rabbit 算法要求密钥长度为 16 字节,因此我们这里保证密钥长度为 16 个字符
         const paddedKey = key.padEnd(16, '0').slice(0, 16);
-        // Rabbit 算法要求 IV 长度为 8 字节,因此我们这里保证 IV 长度为 8 个字符
         const ivValue = iv ? iv.padEnd(8, '0').slice(0, 8) : encryptedData.slice(0, 8);
         const data = iv ? encryptedData : encryptedData.slice(8);
         const keyHex = encUtf8.parse(paddedKey);
@@ -144,3 +474,31 @@ export const rabbitDecrypt = (encryptedData: string, key: string, iv?: string):
         return '';
     }
 };
+
+/**
+ * Rabbit 解密(二进制)
+ */
+export function rabbitDecryptBytes(
+    encryptedData: Uint8Array,
+    key: Uint8Array,
+    iv?: Uint8Array
+): Uint8Array {
+    if (!encryptedData.length || !key.length) return new Uint8Array(0);
+    try {
+        const ivBytes = iv
+            ? padBytes(iv, RABBIT_IV_BYTES)
+            : encryptedData.subarray(0, RABBIT_IV_BYTES);
+        const ctBytes = iv ? encryptedData : encryptedData.subarray(RABBIT_IV_BYTES);
+        const keyWa = bytesToWordArray(padBytes(key, RABBIT_KEY_BYTES));
+        const ivWa = bytesToWordArray(ivBytes);
+        const cp = CipherParams.create({
+            ciphertext: bytesToWordArray(ctBytes),
+            iv: ivWa,
+        });
+        const decrypted = Rabbit.decrypt(cp, keyWa, { iv: ivWa });
+        return wordArrayToBytes(decrypted);
+    } catch (error) {
+        console.error('Rabbit decrypt bytes error:', error);
+        return new Uint8Array(0);
+    }
+}

+ 3 - 0
src/utils/request/types.ts

@@ -1,5 +1,7 @@
 import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
 
+import { CompressMethod } from '@/utils/requestCrypto';
+
 // request 方法 opts 参数的接口
 export interface IRequestOptions extends AxiosRequestConfig {
     requestInterceptors?: IRequestInterceptorTuple[];
@@ -52,6 +54,7 @@ export interface RequestConfig<T = any> extends IRequestOptions {
     encryption?: {
         enabled: boolean;
         key: string;
+        compressMethod?: CompressMethod;
     };
     errorConfig?: {
         errorHandler?: IErrorHandler;

+ 107 - 0
src/utils/requestCrypto.ts

@@ -0,0 +1,107 @@
+import { aesCbcDecryptBytes, aesCbcEncryptBytes, bytesBase64decode } from '@/utils/crypto';
+import { bytesToNumber, numberToBytesAt, Endian, bytesToString } from '@/utils/bytesUtils';
+
+import globalConfig from '@/config';
+
+import { compressBytes, decompressBytes, CompressFormat } from './compress';
+import isNil from 'ramda/es/isNil';
+
+export enum CompressMethod {
+    NONE = '',
+    NO_ZIP = 'nozip',
+    BR = 'br',
+    GZIP = 'gzip',
+}
+
+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.NONE
+): Promise<Uint8Array> {
+    const compressed =
+        !isNil(compressMethod) &&
+        compressMethod !== CompressMethod.NONE &&
+        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.NONE
+): 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 =
+        !isNil(compressMethod) &&
+        compressMethod !== CompressMethod.NONE &&
+        compressMethod !== CompressMethod.NO_ZIP
+            ? await decompressBytes(dataBytes, compressFormatFromMethod(compressMethod))
+            : dataBytes;
+
+    return { timestamp, data: rawBytes };
+}
+
+export async function decryptUrlParams<T = any>(params: string): Promise<T | null> {
+    const key = bytesBase64decode(globalConfig.security.requestEncryptionKey);
+    const dataBytes = bytesBase64decode(params);
+    const compressMethod = globalConfig.security.compressMethod;
+    try {
+        const { data } = await decryptResponsePayload(dataBytes, key, compressMethod);
+        const result = bytesToString(data);
+        return JSON.parse(result) as T;
+    } catch (error) {
+        return null;
+    }
+}

+ 12 - 7
src/utils/storage/index.ts

@@ -1,23 +1,28 @@
 import globalConfig from '@/config';
-import { md5, aesEncrypt, aesDecrypt, rabbitEncrypt, rabbitDecrypt } from '@/utils/crypto';
+import {
+    stringMd5,
+    aesCbcEncryptString,
+    aesCbcDecryptString,
+    rabbitEncryptString,
+    rabbitDecryptString,
+} from '@/utils/crypto';
 
-const storageKeyIV = md5(`${globalConfig.app.title}_${globalConfig.app.version}`).slice(0, 8);
-console.log('storageKeyIV:', storageKeyIV);
+const storageKeyIV = stringMd5(`${globalConfig.app.title}_${globalConfig.app.version}`).slice(0, 8);
 
 export function encryptKey(key: string) {
-    return rabbitEncrypt(key, globalConfig.security.storageEncryptionKey, storageKeyIV);
+    return rabbitEncryptString(key, globalConfig.security.storageEncryptionKey, storageKeyIV);
 }
 
 export function decryptKey(key: string) {
-    return rabbitDecrypt(key, globalConfig.security.storageEncryptionKey, storageKeyIV);
+    return rabbitDecryptString(key, globalConfig.security.storageEncryptionKey, storageKeyIV);
 }
 
 export function encryptData(data: string) {
-    return aesEncrypt(data, globalConfig.security.storageEncryptionKey);
+    return aesCbcEncryptString(data, globalConfig.security.storageEncryptionKey);
 }
 
 export function decryptData(data: string) {
-    return aesDecrypt(data, globalConfig.security.storageEncryptionKey);
+    return aesCbcDecryptString(data, globalConfig.security.storageEncryptionKey);
 }
 
 export interface StorageOptions {

+ 7 - 0
vite.config.ts

@@ -1,6 +1,8 @@
 import react from '@vitejs/plugin-react';
 import { defineConfig, loadEnv } from 'vite';
 import removeConsole from 'vite-plugin-remove-console';
+import topLevelAwait from 'vite-plugin-top-level-await';
+import wasm from 'vite-plugin-wasm';
 
 import { viteBuildInfo } from './build/buildInfo';
 import { configCompressPlugin } from './build/compress';
@@ -16,6 +18,8 @@ export default defineConfig(({ mode }) => {
         base: env.VITE_BUILD_PUBLIC_PATH,
         plugins: [
             react(),
+            wasm(),
+            topLevelAwait(),
             isProd &&
                 removeConsole({
                     includes: ['log', 'warn'],
@@ -26,6 +30,9 @@ export default defineConfig(({ mode }) => {
             configCompressPlugin(env.VITE_BUILD_COMPRESSION!),
         ].filter(Boolean),
         resolve: { alias },
+        optimizeDeps: {
+            exclude: ['brotli-wasm'],
+        },
         css: {
             preprocessorOptions: {
                 less: {