diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9fbfef2..4d6d214 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.1", "axios": "^1.6.2", + "crypto-js": "^4.2.0", "element-plus": "^2.3.14", "pinia": "^2.1.7", "socket.io-client": "^4.7.5", @@ -736,6 +737,12 @@ "node": ">= 0.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/csstype": { "version": "3.2.3", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index fadd6a2..0f3a10e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,19 +8,20 @@ "preview": "vite preview" }, "dependencies": { + "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.6.2", + "crypto-js": "^4.2.0", + "element-plus": "^2.3.14", + "pinia": "^2.1.7", + "socket.io-client": "^4.7.5", "vue": "^3.3.4", "vue-router": "^4.2.5", - "pinia": "^2.1.7", - "element-plus": "^2.3.14", - "@element-plus/icons-vue": "^2.3.1", "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", - "xterm-addon-web-links": "^0.9.0", - "socket.io-client": "^4.7.5", - "axios": "^1.6.2" + "xterm-addon-web-links": "^0.9.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.4.0", "vite": "^4.5.0" } -} \ No newline at end of file +} diff --git a/frontend/src/views/QuickConnect.vue b/frontend/src/views/QuickConnect.vue index d183b74..ba2abd4 100644 --- a/frontend/src/views/QuickConnect.vue +++ b/frontend/src/views/QuickConnect.vue @@ -37,7 +37,7 @@ @@ -229,6 +229,7 @@ import { ElMessage, ElMessageBox } from 'element-plus' import { ArrowLeft, Connection, Delete, Close, Monitor, Lock, Folder, Upload, Download } from '@element-plus/icons-vue' import { useTerminalStore } from '@/stores/terminal' import { useAuthStore } from '@/stores/auth' +import CryptoJS from 'crypto-js' const HISTORY_KEY = 'webssh_quick_connect_history' @@ -458,18 +459,32 @@ const clearAllHistory = () => { }).catch(() => {}) } -// ========== 导出 / 导入 ========== +// ========== 导入与导出 ========== + +const getEncryptionKey = () => { + const email = authStore.user?.email || 'default' + return `${email}2026webssh` +} const exportHistory = () => { - const data = JSON.stringify(historyList.value, null, 2) - const blob = new Blob([data], { type: 'application/json' }) + if (historyList.value.length === 0) { + ElMessage.warning('没有历史记录可导出') + return + } + + const jsonStr = JSON.stringify(historyList.value, null, 2) + const key = getEncryptionKey() + // 对 JSON 字符串进行 AES 加密 + const encryptedData = CryptoJS.AES.encrypt(jsonStr, key).toString() + + const blob = new Blob([encryptedData], { type: 'text/plain' }) const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url - a.download = `webssh_quick_connect_${new Date().toISOString().slice(0, 10)}.json` + a.download = `webssh_quick_connect_${new Date().toISOString().slice(0, 10)}.enc` a.click() URL.revokeObjectURL(url) - ElMessage.success(`已导出 ${historyList.value.length} 条连接记录`) + ElMessage.success(`已加密导出 ${historyList.value.length} 条连接记录`) } const triggerImport = () => { @@ -481,9 +496,45 @@ const handleImportFile = (event) => { if (!file) return const reader = new FileReader() - reader.onload = (e) => { + reader.onload = async (e) => { try { - const imported = JSON.parse(e.target.result) + const fileContent = e.target.result + let key = getEncryptionKey() + + let jsonStr = '' + try { + const bytes = CryptoJS.AES.decrypt(fileContent, key) + jsonStr = bytes.toString(CryptoJS.enc.Utf8) + if (!jsonStr) throw new Error('Decryption empty') + } catch (err) { + // 第一轮使用当前用户邮箱解密失败,弹窗让用户输入导出时的邮箱 + try { + const { value: inputEmail } = await ElMessageBox.prompt( + '当前账号解密失败。如果这是其他账号导出的文件,请输入导出该文件时的账号邮箱:', + '需要提供原邮箱以解密', + { + confirmButtonText: '确定解密', + cancelButtonText: '取消', + inputPattern: /[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?/, + inputErrorMessage: '邮箱格式不正确' + } + ) + + key = `${inputEmail}2026webssh` + const newBytes = CryptoJS.AES.decrypt(fileContent, key) + jsonStr = newBytes.toString(CryptoJS.enc.Utf8) + + if (!jsonStr) throw new Error('Decryption empty second try') + } catch (promptErr) { + if (promptErr !== 'cancel') { + ElMessage.error('使用您提供的邮箱解密依然失败,文件可能已损坏或邮箱不正确') + } + event.target.value = '' + return + } + } + + const imported = JSON.parse(jsonStr) if (!Array.isArray(imported)) { ElMessage.error('文件格式错误:需要 JSON 数组')