Browse Source

接口加密测试

Michael Wang 3 years ago
parent
commit
e82af10def

+ 2 - 0
package.json

@@ -21,6 +21,7 @@
     "axios": "^0.26.1",
     "axios-progress-bar": "^1.2.0",
     "axios-retry": "^3.2.4",
+    "crypto-js": "^4.1.1",
     "face-api.js": "^0.22.2",
     "js-md5": "^0.7.3",
     "js-sls-logger": "^2.0.1",
@@ -38,6 +39,7 @@
     "vuedraggable": "4.1.0"
   },
   "devDependencies": {
+    "@types/crypto-js": "^4.1.1",
     "@types/js-md5": "^0.4.3",
     "@types/lodash-es": "^4.17.6",
     "@types/node": "^17.0.24",

+ 12 - 0
pnpm-lock.yaml

@@ -2,6 +2,7 @@ lockfileVersion: 5.3
 
 specifiers:
   '@chenfengyuan/vue-qrcode': ^2.0.0
+  '@types/crypto-js': ^4.1.1
   '@types/js-md5': ^0.4.3
   '@types/lodash-es': ^4.17.6
   '@types/node': ^17.0.24
@@ -16,6 +17,7 @@ specifiers:
   axios: ^0.26.1
   axios-progress-bar: ^1.2.0
   axios-retry: ^3.2.4
+  crypto-js: ^4.1.1
   electron: 1.7.16
   eslint: ^8.13.0
   eslint-config-prettier: ^8.5.0
@@ -53,6 +55,7 @@ dependencies:
   axios: 0.26.1
   axios-progress-bar: 1.2.0_axios@0.26.1
   axios-retry: 3.2.4
+  crypto-js: 4.1.1
   face-api.js: 0.22.2
   js-md5: 0.7.3
   js-sls-logger: 2.0.1
@@ -70,6 +73,7 @@ dependencies:
   vuedraggable: 4.1.0_vue@3.2.33
 
 devDependencies:
+  '@types/crypto-js': 4.1.1
   '@types/js-md5': 0.4.3
   '@types/lodash-es': 4.17.6
   '@types/node': 17.0.24
@@ -537,6 +541,10 @@ packages:
       '@types/node': 17.0.24
     dev: true
 
+  /@types/crypto-js/4.1.1:
+    resolution: {integrity: sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==}
+    dev: true
+
   /@types/form-data/0.0.33:
     resolution: {integrity: sha512-8BSvG1kGm83cyJITQMZSulnl6QV8jqAGreJsc5tPu1Jq0vTSOiY/k24Wx82JRpWwZSqrala6sd5rWi6aNXvqcw==}
     dependencies:
@@ -1284,6 +1292,10 @@ packages:
       which: 2.0.2
     dev: true
 
+  /crypto-js/4.1.1:
+    resolution: {integrity: sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==}
+    dev: false
+
   /css-render/0.15.9:
     resolution: {integrity: sha512-FMVcWsVipKEBR/mVf1+pIjCRQdztILVKxbp8TN5/Vf0Q/fdTq0OIb8JRW/pk7PP1eeWnB/ejQ0MNBe7ELjLblg==}
     dependencies:

+ 4 - 3
src/api/login.ts

@@ -8,10 +8,11 @@ export async function loginApi(
   password: string,
   domain: string,
   rootOrgId: number,
-  geeParams: object
+  geeParams: object,
+  timestamp: number
 ) {
   return httpApp.post<{ code: string; desc: string; content: any }>(
-    "/api/ecs_core/verifyCode/gt/login",
+    "/api/ecs_core/login",
     {
       accountType,
       accountValue,
@@ -21,7 +22,7 @@ export async function loginApi(
       alwaysOK: true,
       ...geeParams,
     },
-    { setGlobalMask: true }
+    { setGlobalMask: true, headers: { timestamp } }
   );
 }
 

+ 25 - 12
src/features/OnlineExam/OnlineExamOverview/OnlineExamOverview.vue

@@ -12,6 +12,10 @@ const route = useRoute();
 const examId = route.params.examId;
 const examStudentId = route.query.examStudentId;
 
+// 没传的话,转为0,判断时0,则相当于为每传
+let examRecordDataId: number = +route.query.examRecordDataId!;
+const courseName = <string>route.query.courseName;
+
 const TOTAL_READ_TIME = 120;
 const FORCE_READ_TIME = import.meta.env.DEV ? 1 : 10;
 
@@ -21,7 +25,6 @@ let beforeExamRemark = $ref("");
 let startInfo: { courseName: string } | null = $ref(null);
 let paperStruct: PaperStruct | null = $ref(null);
 let paperTotalScore = $ref(0);
-let examRecordDataId = -1;
 
 const { addInterval } = useTimers();
 let remainTime = $ref(Number.MAX_SAFE_INTEGER);
@@ -74,19 +77,29 @@ onMounted(async () => {
       { "axios-retry": { retries: 4 }, noErrorMessage: true }
     );
     beforeExamRemark = exam.data.BEFORE_EXAM_REMARK || "";
-    const res = await httpApp.get<{
-      courseName: string;
-      examRecordDataId: number;
-    }>(
-      "/api/ecs_oe_student/examControl/startExam?examStudentId=" +
-        examStudentId,
-      { "axios-retry": { retries: 4 }, noErrorMessage: true }
-    );
 
-    // const res = { data: { examRecordDataId: 8066868, courseName: "test" } };
+    if (examRecordDataId) {
+      startInfo = { courseName };
+    } else {
+      const timestamp = Date.now();
+      const res = await httpApp.post<{
+        courseName: string;
+        examRecordDataId: number;
+      }>(
+        "/api/ecs_oe_student/examControl/unonline/startExam?examStudentId=" +
+          examStudentId,
+        {
+          "axios-retry": { retries: 4 },
+          noErrorMessage: true,
+          headers: { timestamp },
+        }
+      );
+
+      // const res = { data: { examRecordDataId: 8066868, courseName: "test" } };
 
-    startInfo = res.data;
-    examRecordDataId = res.data.examRecordDataId;
+      startInfo = res.data;
+      examRecordDataId = res.data.examRecordDataId;
+    }
 
     const paperStructRes = await httpApp.get(
       "/api/ecs_oe_student/examRecordPaperStruct/getExamRecordPaperStruct?examRecordDataId=" +

+ 72 - 3
src/features/OnlineExam/StartExamModal.vue

@@ -9,6 +9,8 @@ import router from "@/router";
 import { checkExamInProgress } from "../UserLogin/useExamInProgress";
 import { store } from "@/store/store";
 import { closeMediaStream } from "@/utils/camera";
+import { decryptB, decryptC, encryptB, encryptC, getKey } from "@/utils/utils";
+import { AxiosError } from "axios";
 
 const { course } = defineProps<{ course: OnlineExam }>();
 
@@ -215,9 +217,76 @@ function disagreeCommittment() {
 let passedAllChecks = false;
 async function enterExam() {
   passedAllChecks = true;
-  await router.push(
-    `/online-exam/exam/${course.examId}/overview?examStudentId=${course.examStudentId}`
-  );
+
+  // decode utf-8 base64 string
+  // const s = decryptB(
+  //   "eyJleGFtUmVjb3JkRGF0YUlkIjo4MDY2ODcyLCJjb3Vyc2VDb2RlIjpudWxsLCJjb3Vyc2VOYW1lIjoi57u85ZCI5Y23Iiwic3R1ZGVudENvZGUiOm51bGwsInN0dWRlbnROYW1lIjpudWxsLCJmYWNlVmVyaWZ5TWludXRlIjpudWxsfQ=="
+  // );
+  // console.log(s);
+  // return;
+
+  let hstate = { examStudentId: course.examStudentId };
+  if (course.examType === "ONLINE") {
+    const timestamp = Date.now();
+    const rawStr = JSON.stringify({
+      examStudentId: course.examStudentId,
+      timestamp,
+    });
+
+    const key = getKey(timestamp);
+    // console.log({ rawStr, key });
+
+    let encryptParams = "";
+    // if (store.user.salt === "E") {
+    encryptParams = encryptC(encryptB(rawStr), key);
+    // }
+
+    try {
+      let res = await httpApp.post<string>(
+        "/api/ecs_oe_student/examControl/online/startExam",
+        encryptParams,
+        {
+          "axios-retry": { retries: 4 },
+          noErrorMessage: true,
+          headers: { timestamp, "Content-Type": "text/plain" },
+        }
+      );
+
+      res.data = decryptB(decryptC(res.data, key));
+      // console.log(res.data);
+      const newRes: {
+        courseName: string;
+        examRecordDataId: number;
+      } = JSON.parse(res.data);
+
+      Object.assign(hstate, {
+        examRecordDataId: newRes.examRecordDataId,
+        courseName: encodeURIComponent(newRes.courseName),
+      });
+    } catch (error) {
+      console.log(error);
+      if ("isAxiosError" in <any>error) {
+        const ne = <AxiosError>error;
+        const desc: string = ne.response?.data?.desc;
+        if (desc) {
+          $message.error(desc);
+          logger({ cnl: ["server"], act: "开考失败", dtl: desc });
+        }
+      } else {
+        logger({
+          cnl: ["server"],
+          act: "开考失败",
+          dtl: "非接口失败",
+          possibleError: error,
+        });
+      }
+    }
+  }
+  await router.push({
+    name: "OnlineExamOverview",
+    params: { examId: course.examId },
+    query: hstate,
+  });
 }
 
 onUnmounted(() => {

+ 22 - 1
src/features/UserLogin/UserLogin.vue

@@ -12,8 +12,10 @@ import {
 import { useTimers } from "@/setups/useTimers";
 import { store } from "@/store/store";
 import { createEncryptLog, createUserDetailLog } from "@/utils/logger";
+import { MD5 } from "@/utils/md5";
 import { getScreenShot, isElectron } from "@/utils/nativeMethods";
 import ua from "@/utils/ua";
+import { decryptLogin, setSalt } from "@/utils/utils";
 import { CloseCircleOutline, LockClosed, Person } from "@vicons/ionicons5";
 import { FormItemInst, FormRules } from "naive-ui";
 import { onMounted, watch } from "vue";
@@ -248,13 +250,26 @@ async function loginForuser() {
   }
 
   try {
+    const timestamp = Date.now();
     const res = await loginApi(
       formValue.accountType,
       formValue.accountValue,
       formValue.password,
       domain,
       QECSConfig.ROOT_ORG_ID,
-      geeParams
+      geeParams,
+      timestamp
+    );
+
+    const key = MD5(
+      "accountValue=" +
+        formValue.accountValue +
+        "&" +
+        "password=" +
+        formValue.password +
+        "&" +
+        "timestamp=" +
+        timestamp
     );
 
     logger({
@@ -271,6 +286,12 @@ async function loginForuser() {
 
     if (res.data.code === "200") {
       errorInfo = "";
+      // FIXME: 插入多余代码?
+      const str = decryptLogin(res.data.content as string, key);
+      res.data.content = JSON.parse(str);
+      console.log(res.data.content);
+      setSalt(res.data.content.salt as string);
+      // delete res.data.content.salt;
       // 准备下面的登录token
       store.user = res.data.content;
     } else {

+ 2 - 1
src/setups/useWebSocket.ts

@@ -10,7 +10,7 @@ export function useWebSocket() {
   let heartbeatIds: number[] = [];
   const RECONNECT_INTERVAL = 6 * 1000;
   let reconnectIds: number[] = [];
-  const HEARTBEAT_INTERVAL = 50 * 1000;
+  const HEARTBEAT_INTERVAL = 10 * 1000;
   let reconnectNumber = 0;
 
   let closeExplicitly = false;
@@ -26,6 +26,7 @@ export function useWebSocket() {
     _onMessage: (e: MessageEvent) => void,
     _byWho: string
   ) {
+    closeExplicitly = false;
     url = _url;
     onMessage = _onMessage;
     byWho = _byWho;

+ 2 - 0
src/types/student-client.d.ts

@@ -28,6 +28,8 @@ export type Store = {
     token: string | null;
     /** 专业 */
     specialty: string;
+    /** 加密方式ABCDEFG */
+    salt: string;
   };
   /** 学生端配置。 Q代表qmth */
   QECSConfig: {

+ 129 - 0
src/utils/utils.ts

@@ -1,4 +1,7 @@
 import router from "@/router";
+import { store } from "@/store/store";
+import CryptoJS from "crypto-js";
+import { MD5 } from "./md5";
 
 export function setUUID() {
   if (!localStorage.getItem("uuidForEcs")) {
@@ -51,3 +54,129 @@ export function toChineseNumber(num: number) {
 
   return ret;
 }
+
+let salt: string;
+export function setSalt(_salt: string) {
+  salt = _salt;
+}
+
+export function getSalt() {
+  return salt;
+}
+
+export function getKey(timestamp: number) {
+  const { key, token } = store.user;
+  if (!key || !token) return "";
+
+  return MD5(`key=${key}&token=${token}&timestamp=${timestamp}`);
+}
+
+function toHex(str: string) {
+  if (!str) {
+    return "";
+  }
+
+  const chars = [];
+  for (let n = 0; n < str.length; n++) {
+    chars.push(str.charCodeAt(n).toString(16));
+  }
+  return chars.join("");
+}
+
+const ivStr = "@M#A$G%A^2&0*2(1"; // 偏移量
+const iv = toHex(ivStr); // 16进制偏移量
+export function decryptLogin(encStr: string, key: string): string {
+  return CryptoJS.enc.Utf8.stringify(
+    CryptoJS.AES.decrypt(
+      CryptoJS.format.Hex.parse(encStr),
+      CryptoJS.enc.Hex.parse(toHex(key)),
+      {
+        iv: CryptoJS.enc.Hex.parse(iv),
+        mode: CryptoJS.mode.CBC,
+        padding: CryptoJS.pad.Pkcs7,
+      }
+    )
+  );
+}
+
+/** AES(带向量) 简写 A */
+export function encryptA(str: string, key: string) {
+  return CryptoJS.AES.encrypt(str, CryptoJS.enc.Hex.parse(toHex(key)), {
+    // iv: CryptoJS.enc.Hex.parse(iv),
+    mode: CryptoJS.mode.ECB,
+    padding: CryptoJS.pad.Pkcs7,
+  }).ciphertext.toString();
+}
+
+export function decryptA(encStr: string, key: string): string {
+  return CryptoJS.enc.Utf8.stringify(
+    CryptoJS.AES.decrypt(
+      CryptoJS.format.Hex.parse(encStr),
+      CryptoJS.enc.Hex.parse(toHex(key)),
+      {
+        // iv: CryptoJS.enc.Hex.parse(iv),
+        mode: CryptoJS.mode.ECB,
+        padding: CryptoJS.pad.Pkcs7,
+      }
+    )
+  );
+}
+
+/** base64加密 简写B */
+export function encryptB(str: string): string {
+  // return window.btoa(str);
+  return CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(str));
+}
+
+/** base64加密 简写B */
+export function decryptB(str: string): string {
+  return CryptoJS.enc.Utf8.stringify(CryptoJS.enc.Base64.parse(str));
+}
+
+/** RC4(无向量) 简写 C */
+export function encryptC(str: string, key: string) {
+  return CryptoJS.RC4.encrypt(
+    str,
+    CryptoJS.enc.Hex.parse(toHex(key))
+  ).toString();
+}
+
+export function decryptC(encStr: string, key: string): string {
+  return CryptoJS.enc.Utf8.stringify(
+    CryptoJS.RC4.decrypt(encStr, CryptoJS.enc.Hex.parse(toHex(key)))
+  );
+}
+
+/** DES(无向量) 简写 D */
+export function encryptD(str: string, key: string) {
+  return CryptoJS.DES.encrypt(str, CryptoJS.enc.Hex.parse(toHex(key)), {
+    mode: CryptoJS.mode.ECB,
+    padding: CryptoJS.pad.Pkcs7,
+  }).toString();
+}
+
+export function decryptD(encStr: string, key: string): string {
+  return CryptoJS.enc.Utf8.stringify(
+    CryptoJS.DES.decrypt(encStr, CryptoJS.enc.Hex.parse(toHex(key)), {
+      mode: CryptoJS.mode.ECB,
+      padding: CryptoJS.pad.Pkcs7,
+    })
+  );
+}
+
+/** TripleDES(无向量) 简写 E */
+export function encryptE(str: string, key: string) {
+  return CryptoJS.TripleDES.encrypt(str, CryptoJS.enc.Hex.parse(toHex(key)), {
+    mode: CryptoJS.mode.ECB,
+    padding: CryptoJS.pad.Pkcs7,
+  }).toString();
+}
+
+export function decryptE(encStr: string, key: string): string {
+  return CryptoJS.enc.Utf8.stringify(
+    CryptoJS.TripleDES.decrypt(encStr, CryptoJS.enc.Hex.parse(toHex(key)), {
+      mode: CryptoJS.mode.ECB,
+      padding: CryptoJS.pad.Pkcs7,
+    })
+  );
+}