chad hace 1 día
padre
commit
a54a81f90b
Se han modificado 5 ficheros con 300 adiciones y 197 borrados
  1. 10 7
      package-lock.json
  2. 1 1
      package.json
  3. 33 4
      src/pages/Tabbar/My/index.vue
  4. 16 5
      src/request/index.js
  5. 240 180
      src/utils/apiCrypto.js

+ 10 - 7
package-lock.json

@@ -33,8 +33,8 @@
         "@uni-helper/vite-plugin-uni-components": "^0.2.10",
         "@uni-helper/vite-plugin-uni-components": "^0.2.10",
         "alova": "~3.2.11",
         "alova": "~3.2.11",
         "color": "~5.0.0",
         "color": "~5.0.0",
-        "crypto-js": "^4.2.0",
         "es-toolkit": "~1.38.0",
         "es-toolkit": "~1.38.0",
+        "node-forge": "^1.3.1",
         "pinia": "~2.0.36",
         "pinia": "~2.0.36",
         "pinia-plugin-persistedstate": "~3.2.1",
         "pinia-plugin-persistedstate": "~3.2.1",
         "uniapp-router-next": "~1.2.7",
         "uniapp-router-next": "~1.2.7",
@@ -5945,12 +5945,6 @@
         "node": ">= 8"
         "node": ">= 8"
       }
       }
     },
     },
-    "node_modules/crypto-js": {
-      "version": "4.2.0",
-      "resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
-      "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
-      "license": "MIT"
-    },
     "node_modules/css-font-size-keywords": {
     "node_modules/css-font-size-keywords": {
       "version": "1.0.0",
       "version": "1.0.0",
       "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
       "resolved": "https://registry.npmmirror.com/css-font-size-keywords/-/css-font-size-keywords-1.0.0.tgz",
@@ -7651,6 +7645,15 @@
       "dev": true,
       "dev": true,
       "license": "MIT"
       "license": "MIT"
     },
     },
+    "node_modules/node-forge": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmmirror.com/node-forge/-/node-forge-1.3.1.tgz",
+      "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==",
+      "license": "(BSD-3-Clause OR GPL-2.0)",
+      "engines": {
+        "node": ">= 6.13.0"
+      }
+    },
     "node_modules/node-releases": {
     "node_modules/node-releases": {
       "version": "2.0.50",
       "version": "2.0.50",
       "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.50.tgz",
       "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.50.tgz",

+ 1 - 1
package.json

@@ -72,8 +72,8 @@
     "@uni-helper/vite-plugin-uni-components": "^0.2.10",
     "@uni-helper/vite-plugin-uni-components": "^0.2.10",
     "alova": "~3.2.11",
     "alova": "~3.2.11",
     "color": "~5.0.0",
     "color": "~5.0.0",
-    "crypto-js": "^4.2.0",
     "es-toolkit": "~1.38.0",
     "es-toolkit": "~1.38.0",
+    "node-forge": "^1.3.1",
     "pinia": "~2.0.36",
     "pinia": "~2.0.36",
     "pinia-plugin-persistedstate": "~3.2.1",
     "pinia-plugin-persistedstate": "~3.2.1",
     "uniapp-router-next": "~1.2.7",
     "uniapp-router-next": "~1.2.7",

+ 33 - 4
src/pages/Tabbar/My/index.vue

@@ -124,14 +124,14 @@
             </view>
             </view>
           </view>
           </view>
 
 
-          <view class="asset-section">
+          <view class="asset-sf">
             <view
             <view
-              class="asset-item"
+              class="sf-item"
               v-for="(item, index) in Object.keys(balancesList)"
               v-for="(item, index) in Object.keys(balancesList)"
               :key="index"
               :key="index"
             >
             >
-              <text class="asset-label">{{ item }}</text>
-              <text class="asset-value muted">¥{{ balancesList[item] }}</text>
+              <text class="sf-label">{{ item }}</text>
+              <text class="sf-value muted">¥{{ balancesList[item] }}</text>
             </view>
             </view>
           </view>
           </view>
         </view>
         </view>
@@ -558,6 +558,35 @@ onShow(() => {
         }
         }
       }
       }
     }
     }
+
+    .asset-sf {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 10px;
+      padding-top: 30rpx; /* 减小分割线上方内边距 */
+      border-top: 2rpx solid #f8f8f8;
+      .sf-item {
+        display: flex;
+        flex-direction: column;
+        flex: 1;
+        text-align: center;
+        .sf-label {
+          font-size: 26rpx;
+          color: #666;
+          margin-bottom: 8rpx;
+        }
+        .sf-value {
+          font-size: 40rpx; /* 调整字体 */
+          font-weight: 600;
+          color: #333;
+          &.muted {
+            font-size: 32rpx;
+            color: #999;
+            font-weight: 400;
+          }
+        }
+      }
+    }
   }
   }
 
 
   .quick-actions {
   .quick-actions {

+ 16 - 5
src/request/index.js

@@ -22,7 +22,7 @@ export var apiUrl = 'https://bot-api.f1bet888.com/api/'; // botapi的地址
 
 
 // 不需要加密的接口
 // 不需要加密的接口
 const encryptExcludeList = [
 const encryptExcludeList = [
-    '/upload/',
+    '/upload',
     '/egame/play'
     '/egame/play'
 ];
 ];
 function needEncrypt(url) {
 function needEncrypt(url) {
@@ -232,9 +232,19 @@ const service = (config) => {
 
 
                     header["X-Api-Encrypted"] = "1";
                     header["X-Api-Encrypted"] = "1";
 
 
-                    sendRequest({
-                        payload
-                    });
+                    if (method.toUpperCase() === "GET") {
+
+                        params.payload = payload;
+
+                        sendRequest(params);
+
+                    } else {
+
+                        sendRequest({
+                            payload
+                        });
+
+                    }
 
 
                 })
                 })
                 .catch(err => {
                 .catch(err => {
@@ -260,7 +270,8 @@ service.get = (url, config = {}) => {
     return service({
     return service({
         url,
         url,
         method: 'GET',
         method: 'GET',
-        data: config.params,
+        params: config.params,
+        data: {},
         hideLoading: config.hideLoading,
         hideLoading: config.hideLoading,
         showSuccessTip: config.showSuccessTip,
         showSuccessTip: config.showSuccessTip,
         showFailTip: config.showFailTip
         showFailTip: config.showFailTip

+ 240 - 180
src/utils/apiCrypto.js

@@ -1,324 +1,338 @@
-console.log("window.crypto =", window.crypto);
-console.log("window.crypto?.subtle =", window.crypto?.subtle);
-console.log("globalThis.crypto =", globalThis.crypto);
-console.log("globalThis.crypto?.subtle =", globalThis.crypto?.subtle);
+import forge from "node-forge";
+
+
+// =========================
+// AES-256-GCM KEY
+// 后端提供的32字节base64密钥
+// =========================
+
+const API_CRYPTO_KEY =
+    "3MRp/hTEz310NX3BJuMmIYAdlby1Q4K4XPeVSxXm5tE=";
+
+
+// =========================
+// 协议配置
+// =========================
+
 const VERSION = 1;
 const VERSION = 1;
 
 
 const IV_BYTES = 12;
 const IV_BYTES = 12;
 
 
 const TAG_BYTES = 16;
 const TAG_BYTES = 16;
 
 
+const TIMESTAMP_TOLERANCE_MS = 300 * 1000;
 
 
-const encoder = new TextEncoder();
 
 
-const decoder = new TextDecoder();
 
 
+// =========================
+// Base64URL
+// =========================
 
 
-// 你的后端生成的32字节base64密钥
-const API_KEY =
-    "3MRp/hTEz310NX3BJuMmIYAdlby1Q4K4XPeVSxXm5tE=";
-
+function bytesToBase64Url(bytes) {
 
 
+    return forge.util.encode64(bytes)
+        .replace(/\+/g, "-")
+        .replace(/\//g, "_")
+        .replace(/=+$/g, "");
 
 
-let cryptoKey = null;
+}
 
 
 
 
 
 
-// base64url -> Uint8Array
 function base64UrlToBytes(value) {
 function base64UrlToBytes(value) {
 
 
+    if (!value) {
+        throw new Error("base64url为空");
+    }
+
+
     const base64 =
     const base64 =
         value
         value
-            .replace(/-/g, '+')
-            .replace(/_/g, '/');
+            .replace(/-/g, "+")
+            .replace(/_/g, "/");
 
 
 
 
     const padded =
     const padded =
         base64 +
         base64 +
-        '='.repeat(
+        "=".repeat(
             (4 - base64.length % 4) % 4
             (4 - base64.length % 4) % 4
         );
         );
 
 
 
 
-    const binary =
-        atob(padded);
-
-
-    return Uint8Array.from(
-        binary,
-        c => c.charCodeAt(0)
+    return forge.util.decode64(
+        padded
     );
     );
 
 
 }
 }
 
 
 
 
 
 
-// Uint8Array -> base64url
-function bytesToBase64Url(bytes) {
-
-    let binary = '';
+// =========================
+// 获取AES KEY
+// =========================
 
 
+function getKey() {
 
 
-    bytes.forEach(
-        b => {
-            binary +=
-                String.fromCharCode(b);
-        }
-    );
-
+    const key =
+        forge.util.decode64(
+            API_CRYPTO_KEY
+        );
 
 
-    return btoa(binary)
-        .replace(/\+/g, '-')
-        .replace(/\//g, '_')
-        .replace(/=/g, '');
 
 
-}
+    if (key.length !== 32) {
 
 
+        throw new Error(
+            "AES KEY 必须为32字节"
+        );
 
 
+    }
 
 
-// 获取AES KEY
-async function getKey() {
 
 
+    return key;
 
 
-    if (cryptoKey) {
-        return cryptoKey;
-    }
+}
 
 
 
 
 
 
-    cryptoKey =
-        await crypto.subtle.importKey(
+// =========================
+// AAD
+// =========================
 
 
-            "raw",
+function getAAD(
+    direction,
+    ts
+) {
 
 
-            base64UrlToBytes(
-                API_KEY
-            ),
+    return `api-content:v1:${direction}:${ts}`;
 
 
-            {
-                name: "AES-GCM"
-            },
+}
 
 
-            false,
 
 
-            [
-                "encrypt",
-                "decrypt"
-            ]
 
 
-        );
+// =========================
+// 随机IV
+// =========================
 
 
+function createIV() {
 
 
-    return cryptoKey;
+    return forge.random.getBytesSync(
+        IV_BYTES
+    );
 
 
 }
 }
 
 
 
 
 
 
-// AAD
-function getAAD(
-    direction,
-    ts
+// =========================
+// 加密
+// =========================
+
+export function encryptApiPayload(
+    data,
+    direction = "request"
 ) {
 ) {
 
 
-    return encoder.encode(
+    return new Promise((resolve, reject) => {
 
 
-        `api-content:v1:${direction}:${ts}`
+        try {
 
 
-    );
 
 
-}
+            const ts =
+                Date.now();
 
 
 
 
+            const iv =
+                createIV();
 
 
-// 加密请求
-export async function encryptApiPayload(
-    data,
-    direction = "request"
-) {
 
 
 
 
-    const ts =
-        Date.now();
+            const cipher =
+                forge.cipher.createCipher(
+                    "AES-GCM",
+                    getKey()
+                );
 
 
 
 
 
 
-    const iv =
-        crypto.getRandomValues(
-            new Uint8Array(
-                IV_BYTES
-            )
-        );
+            cipher.start({
+
+                iv,
+
+                additionalData:
+                    getAAD(
+                        direction,
+                        ts
+                    ),
 
 
+                tagLength:
+                    TAG_BYTES * 8
 
 
+            });
 
 
-    const plaintext =
-        encoder.encode(
-            JSON.stringify(data)
-        );
 
 
 
 
+            cipher.update(
 
 
-    const encrypted =
-        new Uint8Array(
+                forge.util.createBuffer(
+                    JSON.stringify(data),
+                    "utf8"
+                )
 
 
-            await crypto.subtle.encrypt(
+            );
 
 
-                {
 
 
-                    name: "AES-GCM",
 
 
-                    iv,
+            const result =
+                cipher.finish();
 
 
-                    additionalData:
-                        getAAD(
-                            direction,
-                            ts
-                        ),
 
 
-                    tagLength:
-                        TAG_BYTES * 8
 
 
-                },
+            if (!result) {
 
 
+                throw new Error(
+                    "AES-GCM加密失败"
+                );
 
 
-                await getKey(),
+            }
 
 
 
 
-                plaintext
 
 
-            )
+            const envelope = {
 
 
-        );
+                v: VERSION,
 
 
+                ts,
 
 
 
 
-    // WebCrypto返回 ciphertext+tag
-    const tag =
-        encrypted.slice(
-            -TAG_BYTES
-        );
+                iv:
+                    bytesToBase64Url(
+                        iv
+                    ),
 
 
 
 
-    const cipherText =
-        encrypted.slice(
-            0,
-            -TAG_BYTES
-        );
+                data:
+                    bytesToBase64Url(
+                        cipher.output.getBytes()
+                    ),
 
 
 
 
+                tag:
+                    bytesToBase64Url(
+                        cipher.mode.tag.bytes()
+                    )
 
 
-    const envelope = {
+            };
 
 
-        v: VERSION,
 
 
-        ts,
 
 
-        iv:
-            bytesToBase64Url(iv),
+            const payload =
+                bytesToBase64Url(
 
 
+                    forge.util.encodeUtf8(
 
 
-        data:
-            bytesToBase64Url(
-                cipherText
-            ),
+                        JSON.stringify(
+                            envelope
+                        )
 
 
+                    )
 
 
-        tag:
-            bytesToBase64Url(
-                tag
-            )
+                );
 
 
-    };
 
 
 
 
+            resolve(payload);
 
 
-    return bytesToBase64Url(
 
 
-        encoder.encode(
+        } catch (e) {
 
 
-            JSON.stringify(
-                envelope
-            )
+            reject(e);
 
 
-        )
+        }
 
 
-    );
+    });
 
 
 }
 }
 
 
 
 
 
 
 
 
-// 解密响应
-export async function decryptApiPayload(
+// =========================
+// 解密
+// =========================
+
+export function decryptApiPayload(
     payload,
     payload,
     direction = "response"
     direction = "response"
 ) {
 ) {
 
 
+    return new Promise((resolve, reject) => {
 
 
-    const envelope =
-        JSON.parse(
+        try {
 
 
-            decoder.decode(
 
 
-                base64UrlToBytes(
-                    payload
-                )
+            const envelope =
+                JSON.parse(
 
 
-            )
+                    forge.util.decodeUtf8(
 
 
-        );
+                        base64UrlToBytes(
+                            payload
+                        )
 
 
+                    )
 
 
+                );
 
 
-    const iv =
-        base64UrlToBytes(
-            envelope.iv
-        );
 
 
 
 
-    const data =
-        base64UrlToBytes(
-            envelope.data
-        );
+            if (
+                envelope.v !== VERSION
+            ) {
 
 
+                throw new Error(
+                    "协议版本错误"
+                );
 
 
-    const tag =
-        base64UrlToBytes(
-            envelope.tag
-        );
+            }
 
 
 
 
 
 
-    const encrypted =
-        new Uint8Array(
+            const now =
+                Date.now();
 
 
-            data.length +
-            tag.length
 
 
-        );
 
 
+            if (
+                Math.abs(
+                    now - envelope.ts
+                )
+                >
+                TIMESTAMP_TOLERANCE_MS
+            ) {
 
 
+                throw new Error(
+                    "密文时间戳过期"
+                );
 
 
-    encrypted.set(data);
+            }
 
 
 
 
-    encrypted.set(
-        tag,
-        data.length
-    );
 
 
+            const decipher =
+                forge.cipher.createDecipher(
+                    "AES-GCM",
+                    getKey()
+                );
 
 
 
 
-    const plaintext =
-        await crypto.subtle.decrypt(
 
 
-            {
+            decipher.start({
 
 
-                name: "AES-GCM",
+                iv:
+                    base64UrlToBytes(
+                        envelope.iv
+                    ),
 
 
-                iv,
 
 
                 additionalData:
                 additionalData:
                     getAAD(
                     getAAD(
@@ -326,27 +340,73 @@ export async function decryptApiPayload(
                         envelope.ts
                         envelope.ts
                     ),
                     ),
 
 
+
                 tagLength:
                 tagLength:
-                    TAG_BYTES * 8
+                    TAG_BYTES * 8,
 
 
-            },
 
 
+                tag:
+                    forge.util.createBuffer(
 
 
-            await getKey(),
+                        base64UrlToBytes(
+                            envelope.tag
+                        )
 
 
+                    )
 
 
-            encrypted
+            });
 
 
-        );
 
 
 
 
+            decipher.update(
 
 
-    return JSON.parse(
+                forge.util.createBuffer(
 
 
-        decoder.decode(
-            plaintext
-        )
+                    base64UrlToBytes(
+                        envelope.data
+                    )
 
 
-    );
+                )
+
+            );
+
+
+
+            const result =
+                decipher.finish();
+
+
+
+            if (!result) {
+
+                throw new Error(
+                    "AES-GCM认证失败"
+                );
+
+            }
+
+
+
+            const plaintext =
+                decipher.output.toString(
+                    "utf8"
+                );
+
+
+
+            resolve(
+                JSON.parse(
+                    plaintext
+                )
+            );
+
+
+        } catch (e) {
+
+            reject(e);
+
+        }
+
+    });
 
 
 }
 }