Ver código fonte

feat: 数据安全相关代码优化,firebase统计开关

BaiLuoYan 1 mês atrás
pai
commit
b06c43c189

+ 6 - 3
.env

@@ -15,10 +15,13 @@ VITE_STORAGE_NAME_SPACE="nomo-vpn-"
 
 # 安全配置
 VITE_ENABLE_REQUEST_ENCRYPTION="true"
-VITE_REQUEST_ENCRYPTION_KEY="1f475bd97898528a457663f60836d29f"
+VITE_REQUEST_ENCRYPTION_KEY="1f475bd97898528a457663f60836d29f" # 服务端指定
 VITE_ENABLE_STORAGE_ENCRYPTION="true"
-VITE_STORAGE_ENCRYPTION_KEY="NL-NOMO-VPN_StorageK"
-VITE_REQUEST_COMPRESSION="br"
+VITE_STORAGE_ENCRYPTION_KEY="2363ca53623c9ff2" # 生成算法:md5("NL-NOMO-VPN_StorageK").slice(0, 16)
+VITE_REQUEST_DATA_COMPRESSION="br"
+
+# 是否启用 Firebase
+VITE_ENABLE_FIREBASE="false"
 
 # API 配置
 VITE_API_BASE_URL="https://ow.clickto.dev/api/v1"

+ 7 - 4
src/config/index.ts

@@ -1,4 +1,4 @@
-import type { Config } from './types';
+import { CompressMethod, Config } from './types';
 
 const env = import.meta.env.VITE_APP_ENV || 'production';
 const isDev = env === 'development';
@@ -14,6 +14,7 @@ if (isDev) {
 
 const config: Config = {
     app: {
+        code: import.meta.env.VITE_API_PRODUCT_CODE!,
         title,
         version: import.meta.env.VITE_APP_VERSION!,
         routerMode: import.meta.env.VITE_ROUTER_MODE!,
@@ -21,10 +22,12 @@ const config: Config = {
     },
     security: {
         enableRequestEncryption: import.meta.env.VITE_ENABLE_REQUEST_ENCRYPTION === 'true',
-        requestEncryptionKey: import.meta.env.VITE_REQUEST_ENCRYPTION_KEY ?? 'unknown',
+        requestEncryptionKey: import.meta.env.VITE_REQUEST_ENCRYPTION_KEY ?? '',
         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',
+        storageEncryptionKey: import.meta.env.VITE_STORAGE_ENCRYPTION_KEY ?? '',
+        compressMethod:
+            (import.meta.env.VITE_REQUEST_DATA_COMPRESSION as CompressMethod) ??
+            CompressMethod.NO_ZIP,
     },
     api: {
         baseURL: import.meta.env.VITE_API_BASE_URL!,

+ 1 - 1
src/config/request/encryptionInterceptors.ts

@@ -1,9 +1,9 @@
 import isNil from 'ramda/es/isNil';
 
 import globalConfig from '@/config';
+import { CompressMethod } from '@/config/types';
 // import { bytesBase64decode } from '@/utils/crypto';
 import {
-    CompressMethod,
     decryptResponsePayload,
     encryptRequestPayload,
 } from '@/utils/requestCrypto';

+ 7 - 1
src/config/types.ts

@@ -1,7 +1,13 @@
-import { CompressMethod } from "@/utils/requestCrypto";
+export enum CompressMethod {
+    NO_ZIP = 'nozip',
+    BR = 'br',
+    GZIP = 'gzip',
+}
 
 export interface Config {
     app: {
+        /**应用代码 */
+        code: string;
         /**应用标题 */
         title: string;
         /**应用版本 */

+ 46 - 16
src/firebase.tsx

@@ -1,22 +1,30 @@
-import { getAnalytics, logEvent, setUserProperties } from 'firebase/analytics';
-import { initializeApp } from 'firebase/app';
+import { Analytics, getAnalytics, logEvent, setUserProperties } from 'firebase/analytics';
+import { FirebaseApp, initializeApp } from 'firebase/app';
 
-const firebaseConfig = {
-    apiKey: 'AIzaSyCxwmQ0oUKyUi-FVdqtNZx7SE_a0Mg7xHM',
-    authDomain: 'jump-visacard.firebaseapp.com',
-    projectId: 'jump-visacard',
-    storageBucket: 'jump-visacard.firebasestorage.app',
-    messagingSenderId: '508907990485',
-    appId: '1:508907990485:web:2f3bddddfc1d431224a15c',
-    measurementId: 'G-G87QHZ6J4L',
-};
+let app: FirebaseApp;
+let analytics: Analytics;
 
 // Initialize Firebase
-console.log('Firebase 初始化开始');
-const app = initializeApp(firebaseConfig);
-console.log('Firebase app 已创建');
-const analytics = getAnalytics(app);
-console.log('Firebase analytics 已创建');
+if (import.meta.env.VITE_ENABLE_FIREBASE === 'true') {
+    console.log('Firebase 初始化开始');
+
+    // Firebase 配置
+    const firebaseConfig = {
+        apiKey: 'AIzaSyCxwmQ0oUKyUi-FVdqtNZx7SE_a0Mg7xHM',
+        authDomain: 'jump-visacard.firebaseapp.com',
+        projectId: 'jump-visacard',
+        storageBucket: 'jump-visacard.firebasestorage.app',
+        messagingSenderId: '508907990485',
+        appId: '1:508907990485:web:2f3bddddfc1d431224a15c',
+        measurementId: 'G-G87QHZ6J4L',
+    };
+
+    app = initializeApp(firebaseConfig);
+    console.log('Firebase app 已创建');
+
+    analytics = getAnalytics(app);
+    console.log('Firebase analytics 已创建');
+}
 
 /**
  * 上报自定义事件
@@ -24,6 +32,15 @@ console.log('Firebase analytics 已创建');
  * @param eventParams 事件参数
  */
 export const reportEvent = (eventName: string, eventParams?: Record<string, any>) => {
+    if (import.meta.env.VITE_ENABLE_FIREBASE !== 'true') {
+        console.log(
+            '🚫 Firebase 未启用,不上报事件, eventName:',
+            eventName,
+            'eventParams:',
+            eventParams
+        );
+        return;
+    }
     try {
         logEvent(analytics, eventName, eventParams);
     } catch (error) {
@@ -37,6 +54,15 @@ export const reportEvent = (eventName: string, eventParams?: Record<string, any>
  * @param errorContext 错误上下文
  */
 export const reportError = (error: Error, errorContext?: Record<string, any>) => {
+    if (import.meta.env.VITE_ENABLE_FIREBASE !== 'true') {
+        console.log(
+            '🚫 Firebase 未启用,不上报错误事件, error:',
+            error,
+            'errorContext:',
+            errorContext
+        );
+        return;
+    }
     try {
         logEvent(analytics, 'error', {
             error_message: error.message,
@@ -54,6 +80,10 @@ export const reportError = (error: Error, errorContext?: Record<string, any>) =>
  * @param properties 用户属性对象
  */
 export const setUserPropertyValue = (properties: Record<string, string>) => {
+    if (import.meta.env.VITE_ENABLE_FIREBASE !== 'true') {
+        console.log('🚫 Firebase 未启用,不上报用户属性, properties:', properties);
+        return;
+    }
     try {
         setUserProperties(analytics, properties);
     } catch (error) {

+ 1 - 3
src/models/userConfigModel.ts

@@ -1,13 +1,11 @@
 import { useState, useCallback } from 'react';
 
 import { userKey } from '@/utils/authUtils';
-import { createLocalTools } from '@/utils/localUtils';
+import { secureLocalStorage as ls } from '@/utils/localUtils';
 import { currentUnixTimestamp } from '@/utils/timeUtils';
 
 import { createModel } from '../utils/model/createModel';
 
-const ls = createLocalTools();
-
 interface UserConfigState {
     userConfig: API.UserInfo | null;
 }

+ 4 - 3
src/pages/pricing/useService.ts

@@ -3,7 +3,8 @@ import { useEffect, useMemo, useState } from 'react';
 import { PlanTagType, PayMethodType } from '@/defines';
 import { userConfigModel } from '@/models/userConfigModel';
 import { fetchGetUserConfig, fetchPlanList } from '@/services/config';
-import { getToken, setToken } from '@/utils/authUtils';
+import { setToken, userKey } from '@/utils/authUtils';
+import { secureLocalStorage as ls } from '@/utils/localUtils';
 import { currentUnixTimestamp } from '@/utils/timeUtils';
 import * as stringUtils from '@/utils/stringUtils';
 
@@ -27,9 +28,9 @@ export function useService(): UseServiceReturn {
 
     const [plans, setPlans] = useState<Plan[]>([]);
     useEffect(() => {
-        const userinfo = getToken();
+        const userinfo = ls.getLocal<API.UserInfo>(userKey);
         const expired = (userinfo?.accessExpires ?? 0) - currentUnixTimestamp() <= 0;
-        if (expired) return;  // 如果 accessToken 过期,什么都不做
+        if (expired) return; // 如果 accessToken 过期,什么都不做
         if (userinfo?.account?.username) return; // 否则,如果用户信息存在,也什么都不做
         // 请求用户信息
         fetchGetUserConfig({})

+ 10 - 10
src/utils/authUtils.ts

@@ -1,19 +1,18 @@
 import Cookies from 'js-cookie';
 
-import { createLocalTools } from '@/utils/localUtils';
+import { secureLocalStorage as ls } from '@/utils/localUtils';
+import { encryptKey, encryptData, decryptData } from '@/utils/storage';
 
 import { currentJsTimestamp } from './timeUtils';
 
-const ls = createLocalTools();
-
 export const userKey = 'user-info';
 export const tokenKey = 'authorized-token';
 
 /** 获取`token` */
 export function getToken(): API.UserInfo {
-    return Cookies.get(tokenKey)
-        ? JSON.parse(Cookies.get(tokenKey)!)
-        : (ls.getLocal(userKey) as API.UserInfo);
+    const cookieData = Cookies.get(encryptKey(tokenKey));
+    const cookieDecrypted = cookieData ? decryptData(cookieData) : '';
+    return cookieDecrypted ? JSON.parse(cookieDecrypted) : (ls.getLocal(userKey) as API.UserInfo);
 }
 
 /**
@@ -24,14 +23,15 @@ export function getToken(): API.UserInfo {
  */
 export function setToken(data: API.UserInfo) {
     const { accessToken = '', accessExpires = 0, refreshToken = '' } = data;
-    const cookieString = JSON.stringify({ accessToken, accessExpires, refreshToken });
+    const cookieData = JSON.stringify({ accessToken, accessExpires, refreshToken });
+    const cookieEncrypted = encryptData(cookieData);
 
     if (accessExpires > 0) {
-        Cookies.set(tokenKey, cookieString, {
+        Cookies.set(encryptKey(tokenKey), cookieEncrypted, {
             expires: (accessExpires * 1000 - currentJsTimestamp()) / 86400000,
         });
     } else {
-        Cookies.set(tokenKey, cookieString);
+        Cookies.set(encryptKey(tokenKey), cookieEncrypted);
     }
 
     function setUserKey(data: API.UserInfo) {
@@ -55,7 +55,7 @@ export function setToken(data: API.UserInfo) {
 
 /** 删除`token`以及key值为`user-info`的session信息 */
 export function removeToken() {
-    Cookies.remove(tokenKey);
+    Cookies.remove(encryptKey(tokenKey));
     ls.removeLocal(userKey);
 }
 

+ 91 - 91
src/utils/crypto/index.ts

@@ -163,10 +163,12 @@ export function bytesBase64decode(str: string, urlSafe = false): Uint8Array {
  * @returns 随机密钥
  */
 export const generateKeyString = (length: number = 32): string => {
+    if (length <= 0) return '';
+
     const result: string[] = [];
     const bufferSize = Math.ceil((length * 256) / MAX_ALLOWED) + 16;
     const buffer = new Uint8Array(bufferSize);
-    let pos = 0;
+    let pos = buffer.length;
     while (result.length < length) {
         if (pos >= buffer.length) {
             crypto.getRandomValues(buffer);
@@ -206,7 +208,28 @@ export const aesCbcEncryptString = (data: string, key: string, iv?: string): str
     const keyHex = encUtf8.parse(paddedKey);
     const ivHex = encUtf8.parse(ivValue);
     const encrypted = AES.encrypt(data, keyHex, { iv: ivHex });
-    return iv ? encrypted.toString() : ivValue + encrypted.toString();
+    const ciphertextBase64 = encBase64.stringify(encrypted.ciphertext);
+    return iv ? ciphertextBase64 : ivValue + ciphertextBase64;
+};
+
+/**
+ * AES-CBC 解密(字符串),对应 aesCbcEncryptString
+ */
+export const aesCbcDecryptString = (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 ciphertextBase64 = iv ? encryptedData : encryptedData.slice(16);
+        const keyHex = encUtf8.parse(paddedKey);
+        const ivHex = encUtf8.parse(ivValue);
+        const cp = CipherParams.create({ ciphertext: encBase64.parse(ciphertextBase64) });
+        const decrypted = AES.decrypt(cp, keyHex, { iv: ivHex });
+        return decrypted.toString(encUtf8);
+    } catch (error) {
+        console.error('AES decrypt error:', error);
+        return '';
+    }
 };
 
 /**
@@ -233,26 +256,7 @@ export function aesCbcEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8
 }
 
 /**
- * AES-CBC 解密(字符串)
- */
-export const aesCbcDecryptString = (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 });
-        return decrypted.toString(encUtf8);
-    } catch (error) {
-        console.error('AES decrypt error:', error);
-        return '';
-    }
-};
-
-/**
- * AES-CBC 解密(二进制)
+ * AES-CBC 解密(二进制)对应 aesCbcEncryptBytes
  */
 export function aesCbcDecryptBytes(
     encryptedData: Uint8Array,
@@ -265,10 +269,7 @@ export function aesCbcDecryptBytes(
         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 cp = CipherParams.create({ ciphertext: bytesToWordArray(ctBytes), iv: ivWa });
         const decrypted = AES.decrypt(cp, keyWa, { iv: ivWa });
         return wordArrayToBytes(decrypted);
     } catch (error) {
@@ -292,7 +293,28 @@ export const aesCtrEncryptString = (data: string, key: string, iv?: string): str
     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();
+    const ciphertextBase64 = encBase64.stringify(encrypted.ciphertext);
+    return iv ? ciphertextBase64 : ivValue + ciphertextBase64;
+};
+
+/**
+ * AES-CTR 解密(字符串)对应 aesCtrEncryptString
+ */
+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 ciphertextBase64 = iv ? encryptedData : encryptedData.slice(16);
+        const keyHex = encUtf8.parse(paddedKey);
+        const ivHex = encUtf8.parse(ivValue);
+        const cp = CipherParams.create({ ciphertext: encBase64.parse(ciphertextBase64) });
+        const decrypted = AES.decrypt(cp, keyHex, { iv: ivHex, mode: modeCtr });
+        return decrypted.toString(encUtf8);
+    } catch (error) {
+        console.error('AES-CTR decrypt error:', error);
+        return '';
+    }
 };
 
 /**
@@ -318,25 +340,6 @@ export function aesCtrEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8
     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 解密(二进制)
  */
@@ -351,10 +354,7 @@ export function aesCtrDecryptBytes(
         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 cp = CipherParams.create({ ciphertext: bytesToWordArray(ctBytes), iv: ivWa });
         const decrypted = AES.decrypt(cp, keyWa, { iv: ivWa, mode: modeCtr });
         return wordArrayToBytes(decrypted);
     } catch (error) {
@@ -374,22 +374,9 @@ export const aesEcbEncryptString = (data: string, key: string): string => {
     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();
+    return encBase64.stringify(encrypted.ciphertext);
 };
 
-/**
- * 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 解密(字符串)
  */
@@ -398,7 +385,8 @@ export const aesEcbDecryptString = (encryptedData: string, key: string): string
     try {
         const paddedKey = key.padEnd(32, '0').slice(0, 32);
         const keyHex = encUtf8.parse(paddedKey);
-        const decrypted = AES.decrypt(encryptedData, keyHex, { mode: modeEcb });
+        const cp = CipherParams.create({ ciphertext: encBase64.parse(encryptedData) });
+        const decrypted = AES.decrypt(cp, keyHex, { mode: modeEcb });
         return decrypted.toString(encUtf8);
     } catch (error) {
         console.error('AES-ECB decrypt error:', error);
@@ -406,6 +394,19 @@ export const aesEcbDecryptString = (encryptedData: string, key: string): string
     }
 };
 
+/**
+ * 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 解密(二进制)
  */
@@ -427,8 +428,8 @@ export function aesEcbDecryptBytes(encryptedData: Uint8Array, key: Uint8Array):
  *
  * 注意:
  *   - 如果如果 iv 为空,则返回的加密结果的头部会包含 iv,否则不包含 iv。
- *   - 如果 key 的长度不足 32 位,则会在末尾补齐 字符'0'。
- *   - 如果 iv 的长度不足 16 位,则会在末尾补齐 字符'0'。
+ *   - 如果 key 的长度不足 16 位,则会在末尾补齐 字符'0'。
+ *   - 如果 iv 的长度不足 8 位,则会在末尾补齐 字符'0'。
  */
 export const rabbitEncryptString = (data: string, key: string, iv?: string): string => {
     if (!data || !key) return '';
@@ -437,7 +438,28 @@ export const rabbitEncryptString = (data: string, key: string, iv?: string): str
     const keyHex = encUtf8.parse(paddedKey);
     const ivHex = encUtf8.parse(ivValue);
     const encrypted = Rabbit.encrypt(data, keyHex, { iv: ivHex });
-    return iv ? encrypted.toString() : ivValue + encrypted.toString();
+    const ciphertextBase64 = encBase64.stringify(encrypted.ciphertext);
+    return iv ? ciphertextBase64 : ivValue + ciphertextBase64;
+};
+
+/**
+ * Rabbit 解密(字符串)对应 rabbitEncryptString
+ */
+export const rabbitDecryptString = (encryptedData: string, key: string, iv?: string): string => {
+    if (!encryptedData || !key) return '';
+    try {
+        const paddedKey = key.padEnd(16, '0').slice(0, 16);
+        const ivValue = iv ? iv.padEnd(8, '0').slice(0, 8) : encryptedData.slice(0, 8);
+        const ciphertextBase64 = iv ? encryptedData : encryptedData.slice(8);
+        const keyHex = encUtf8.parse(paddedKey);
+        const ivHex = encUtf8.parse(ivValue);
+        const cp = CipherParams.create({ ciphertext: encBase64.parse(ciphertextBase64) });
+        const decrypted = Rabbit.decrypt(cp, keyHex, { iv: ivHex });
+        return decrypted.toString(encUtf8);
+    } catch (error) {
+        console.error('Rabbit decrypt error:', error);
+        return '';
+    }
 };
 
 /**
@@ -463,25 +485,6 @@ export function rabbitEncryptBytes(data: Uint8Array, key: Uint8Array, iv?: Uint8
     return out;
 }
 
-/**
- * Rabbit 解密(字符串)
- */
-export const rabbitDecryptString = (encryptedData: string, key: string, iv?: string): string => {
-    if (!encryptedData || !key) return '';
-    try {
-        const paddedKey = key.padEnd(16, '0').slice(0, 16);
-        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);
-        const ivHex = encUtf8.parse(ivValue);
-        const decrypted = Rabbit.decrypt(data, keyHex, { iv: ivHex });
-        return decrypted.toString(encUtf8);
-    } catch (error) {
-        console.error('Rabbit decrypt error:', error);
-        return '';
-    }
-};
-
 /**
  * Rabbit 解密(二进制)
  */
@@ -498,10 +501,7 @@ export function rabbitDecryptBytes(
         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 cp = CipherParams.create({ ciphertext: bytesToWordArray(ctBytes), iv: ivWa });
         const decrypted = Rabbit.decrypt(cp, keyWa, { iv: ivWa });
         return wordArrayToBytes(decrypted);
     } catch (error) {

+ 3 - 1
src/utils/localUtils.ts

@@ -71,4 +71,6 @@ function createLocalTools(storageOptions: StorageOptions = {}) {
     return { setLocal, getLocal, removeLocal, clearLocal };
 }
 
-export { createLocalTools };
+const secureLocalStorage = createLocalTools({ encryptKey: true, encryptData: true });
+
+export { createLocalTools, secureLocalStorage };

+ 1 - 1
src/utils/request/types.ts

@@ -1,6 +1,6 @@
 import { AxiosError, AxiosRequestConfig, AxiosResponse } from 'axios';
 
-import { CompressMethod } from '@/utils/requestCrypto';
+import { CompressMethod } from '@/config/types';
 
 // request 方法 opts 参数的接口
 export interface IRequestOptions extends AxiosRequestConfig {

+ 12 - 17
src/utils/requestCrypto.ts

@@ -1,17 +1,16 @@
 import { aesCbcDecryptBytes, aesCbcEncryptBytes, bytesBase64decode } from '@/utils/crypto';
-import { bytesToNumber, numberToBytesAt, Endian, bytesToString, stringToBytes } from '@/utils/bytesUtils';
+import {
+    bytesToNumber,
+    numberToBytesAt,
+    Endian,
+    bytesToString,
+    stringToBytes,
+} from '@/utils/bytesUtils';
 
 import globalConfig from '@/config';
+import { CompressMethod } from '@/config/types';
 
 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;
 
@@ -45,12 +44,10 @@ export async function encryptRequestPayload(
     dataBytes: Uint8Array,
     timestamp: number,
     key: Uint8Array,
-    compressMethod: CompressMethod = CompressMethod.NONE
+    compressMethod: CompressMethod = CompressMethod.NO_ZIP
 ): Promise<Uint8Array> {
     const compressed =
-        !isNil(compressMethod) &&
-        compressMethod !== CompressMethod.NONE &&
-        compressMethod !== CompressMethod.NO_ZIP
+        compressMethod && compressMethod !== CompressMethod.NO_ZIP
             ? await compressBytes(dataBytes, compressFormatFromMethod(compressMethod))
             : dataBytes;
 
@@ -73,7 +70,7 @@ export async function encryptRequestPayload(
 export async function decryptResponsePayload(
     encryptedBytes: Uint8Array,
     key: Uint8Array,
-    compressMethod: CompressMethod = CompressMethod.NONE
+    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) };
@@ -84,9 +81,7 @@ export async function decryptResponsePayload(
     const dataBytes = decrypted.subarray(TIMESTAMP_BYTES);
 
     const rawBytes =
-        !isNil(compressMethod) &&
-        compressMethod !== CompressMethod.NONE &&
-        compressMethod !== CompressMethod.NO_ZIP
+        compressMethod && compressMethod !== CompressMethod.NO_ZIP
             ? await decompressBytes(dataBytes, compressFormatFromMethod(compressMethod))
             : dataBytes;
 

+ 3 - 1
src/utils/sessionUtils.ts

@@ -71,4 +71,6 @@ function createSessionTools(storageOptions: StorageOptions = {}) {
     return { setSession, getSession, removeSession, clearSession };
 }
 
-export { createSessionTools };
+const secureSessionStorage = createSessionTools({ encryptKey: true, encryptData: true });
+
+export { createSessionTools, secureSessionStorage };

+ 4 - 9
src/utils/storage/index.ts

@@ -1,12 +1,7 @@
 import globalConfig from '@/config';
-import {
-    stringMd5,
-    aesCbcEncryptString,
-    aesCbcDecryptString,
-    rabbitEncryptString,
-    rabbitDecryptString,
-} from '@/utils/crypto';
+import { stringMd5, rabbitEncryptString, rabbitDecryptString } from '@/utils/crypto';
 
+// 用一样的 key 和 iv 来加密 storage 的 key, 保证每次加密后的 storage 的 key 的结果一样,否则每次获取数据都需要把 storage 中的所有 key 解密一遍才知道哪个是我们需要的 key
 const storageKeyIV = stringMd5(`${globalConfig.app.title}_${globalConfig.app.version}`).slice(0, 8);
 
 export function encryptKey(key: string) {
@@ -18,11 +13,11 @@ export function decryptKey(key: string) {
 }
 
 export function encryptData(data: string) {
-    return aesCbcEncryptString(data, globalConfig.security.storageEncryptionKey);
+    return rabbitEncryptString(data, globalConfig.security.storageEncryptionKey);
 }
 
 export function decryptData(data: string) {
-    return aesCbcDecryptString(data, globalConfig.security.storageEncryptionKey);
+    return rabbitDecryptString(data, globalConfig.security.storageEncryptionKey);
 }
 
 export interface StorageOptions {

+ 4 - 0
types/vite-env.d.ts

@@ -48,6 +48,8 @@ interface ImportMetaEnv {
     /**压缩选项 */
     VITE_BUILD_COMPRESSION?: BuildCompression;
 
+    /**应用代码 */
+    VITE_API_PRODUCT_CODE?: string;
     /**应用标题 */
     VITE_APP_TITLE?: string;
     /**应用版本 */
@@ -65,6 +67,8 @@ interface ImportMetaEnv {
     VITE_ENABLE_STORAGE_ENCRYPTION?: string;
     /**存储加密密钥 */
     VITE_STORAGE_ENCRYPTION_KEY?: string;
+    /**请求数据压缩方法 */
+    VITE_REQUEST_DATA_COMPRESSION?: 'nozip' | 'br' | 'gzip'; // 不压缩、br压缩、gzip压缩
 
     /**API基础URL */
     VITE_API_BASE_URL?: string;