From d8daff4f0c8ad4abbecae92a2c3158d16e95a577 Mon Sep 17 00:00:00 2001 From: nap0o Date: Tue, 17 Mar 2026 17:41:46 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E6=9B=B4=E5=A4=9A?= =?UTF-8?q?=E5=AF=86=E7=A0=81=E7=AE=A1=E7=90=86=E5=99=A8csv=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E5=A4=87=E4=BB=BD=E6=95=B0=E6=8D=AE=E5=AF=BC?= =?UTF-8?q?=E5=85=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../features/migration/views/dataImport.vue | 12 +++ .../src/shared/components/icons/iconApple.vue | 8 ++ .../shared/components/icons/iconEnpass.vue | 8 ++ .../shared/components/icons/iconKeePass.vue | 8 ++ .../shared/components/icons/iconKeeper.vue | 8 ++ .../src/shared/components/icons/iconMenu.vue | 8 ++ .../shared/components/icons/iconNordPass.vue | 12 +++ .../shared/components/icons/iconRoboForm.vue | 9 +++ frontend/src/shared/components/theHeader.vue | 7 +- .../shared/utils/serializers/csvStrategy.js | 75 ++++++++++++++----- 10 files changed, 130 insertions(+), 25 deletions(-) create mode 100644 frontend/src/shared/components/icons/iconApple.vue create mode 100644 frontend/src/shared/components/icons/iconEnpass.vue create mode 100644 frontend/src/shared/components/icons/iconKeePass.vue create mode 100644 frontend/src/shared/components/icons/iconKeeper.vue create mode 100644 frontend/src/shared/components/icons/iconMenu.vue create mode 100644 frontend/src/shared/components/icons/iconNordPass.vue create mode 100644 frontend/src/shared/components/icons/iconRoboForm.vue diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index 10ac4e0..05c64f4 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -63,6 +63,12 @@ Bitwarden Pass (.json/.csv) 1Password (.1pux/.csv) Proton Pass (.pgp/.csv) + Keeper (.csv) + NordPass (.csv) + Enpass (.csv) + Roboform (.csv) + KeePassXC (.csv) + iCloud Keychain (.csv) Dashlane (.csv) @@ -197,6 +203,12 @@ import iconProtonPass from '@/shared/components/icons/iconProtonPass.vue' import iconEnte from '@/shared/components/icons/iconEnte.vue' import icon1Password from '@/shared/components/icons/icon1Password.vue' import iconLastPassAuth from '@/shared/components/icons/iconLastPassAuth.vue' +import iconKeeper from '@/shared/components/icons/iconKeeper.vue' +import iconNordPass from '@/shared/components/icons/iconNordPass.vue' +import iconRoboForm from '@/shared/components/icons/iconRoboForm.vue' +import iconEnpass from '@/shared/components/icons/iconEnpass.vue' +import iconKeePass from '@/shared/components/icons/iconKeePass.vue' +import iconApple from '@/shared/components/icons/iconApple.vue' const emit = defineEmits(['success']) diff --git a/frontend/src/shared/components/icons/iconApple.vue b/frontend/src/shared/components/icons/iconApple.vue new file mode 100644 index 0000000..7cad3ac --- /dev/null +++ b/frontend/src/shared/components/icons/iconApple.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/shared/components/icons/iconEnpass.vue b/frontend/src/shared/components/icons/iconEnpass.vue new file mode 100644 index 0000000..c16ecf0 --- /dev/null +++ b/frontend/src/shared/components/icons/iconEnpass.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/shared/components/icons/iconKeePass.vue b/frontend/src/shared/components/icons/iconKeePass.vue new file mode 100644 index 0000000..db9ced5 --- /dev/null +++ b/frontend/src/shared/components/icons/iconKeePass.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/shared/components/icons/iconKeeper.vue b/frontend/src/shared/components/icons/iconKeeper.vue new file mode 100644 index 0000000..25083b9 --- /dev/null +++ b/frontend/src/shared/components/icons/iconKeeper.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/shared/components/icons/iconMenu.vue b/frontend/src/shared/components/icons/iconMenu.vue new file mode 100644 index 0000000..3c98b5c --- /dev/null +++ b/frontend/src/shared/components/icons/iconMenu.vue @@ -0,0 +1,8 @@ + + diff --git a/frontend/src/shared/components/icons/iconNordPass.vue b/frontend/src/shared/components/icons/iconNordPass.vue new file mode 100644 index 0000000..115dd82 --- /dev/null +++ b/frontend/src/shared/components/icons/iconNordPass.vue @@ -0,0 +1,12 @@ + + diff --git a/frontend/src/shared/components/icons/iconRoboForm.vue b/frontend/src/shared/components/icons/iconRoboForm.vue new file mode 100644 index 0000000..1db666d --- /dev/null +++ b/frontend/src/shared/components/icons/iconRoboForm.vue @@ -0,0 +1,9 @@ + + diff --git a/frontend/src/shared/components/theHeader.vue b/frontend/src/shared/components/theHeader.vue index b3558d6..bf69381 100644 --- a/frontend/src/shared/components/theHeader.vue +++ b/frontend/src/shared/components/theHeader.vue @@ -32,11 +32,7 @@ link class="header-menu-btn-refined" > - - - - - + @@ -50,6 +46,7 @@ import { useAuthUserStore } from '@/features/auth/store/authUserStore' import { useThemeStore } from '@/shared/stores/themeStore' import { Sunny, Moon } from '@element-plus/icons-vue' import iconLocales from '@/shared/components/icons/iconLocales.vue' +import iconMenu from '@/shared/components/icons/iconMenu.vue' import { useI18n } from 'vue-i18n' import { setLanguage } from '@/locales' diff --git a/frontend/src/shared/utils/serializers/csvStrategy.js b/frontend/src/shared/utils/serializers/csvStrategy.js index a4f6c1b..290baa9 100644 --- a/frontend/src/shared/utils/serializers/csvStrategy.js +++ b/frontend/src/shared/utils/serializers/csvStrategy.js @@ -63,6 +63,7 @@ export const csvStrategy = { const headers = this._splitCsvLine(lines[0]).map(h => h.toLowerCase()) const rawVault = [] + // 1. 定义已知格式的列名特征 const isBitwardenPass = headers.includes('login_totp') const isBitwardenAuth = headers.includes('otpauth') && !headers.includes('title') const is1Password = headers.includes('otpauth') && headers.includes('title') @@ -70,13 +71,28 @@ export const csvStrategy = { const isDashlane = headers.includes('otpurl') && headers.includes('title') && headers.includes('username') const isGeneric = headers.includes('issuer') || headers.includes('secret') || headers.includes('name') - if (!isBitwardenPass && !isBitwardenAuth && !is1Password && !isProtonPass && !isDashlane && !isGeneric) return [] + // 2. 增强型同义词库(用于识别未知 App 导出的文件) + const TOTP_SYNONYMS = ['otpauth', 'login_totp', 'totp', 'mfa', 'two_factor_code', 'secret', 'otpurl', '2fa', 'authenticator'] + const NAME_SYNONYMS = ['name', 'title', 'item name', 'issuer', 'label'] + const USER_SYNONYMS = ['username', 'login', 'login_username', 'account', 'email'] + + // 3. 辅助函数:清洗密钥内容 + const cleanSecret = (str) => { + if (!str) return '' + return str.toString().trim().replace(/[\s-]/g, '').toUpperCase() + } + + // 如果连通用识别都不过,且没有同义词表头,则返回 + const hasAnyTotpHeader = headers.some(h => TOTP_SYNONYMS.includes(h)) + if (!isBitwardenPass && !isBitwardenAuth && !is1Password && !isProtonPass && !isDashlane && !isGeneric && !hasAnyTotpHeader) return [] for (let i = 1; i < lines.length; i++) { const row = this._splitCsvLine(lines[i]) const rowData = {} headers.forEach((h, index) => { rowData[h] = row[index] || '' }) + // --- 优先级路径:已知 App 格式 --- + if (is1Password) { const totpStr = (rowData['otpauth'] || '').trim() if (totpStr && totpStr.startsWith('otpauth://')) { @@ -94,8 +110,7 @@ export const csvStrategy = { if (totpStr.startsWith('otpauth://')) { accData = parseOtpUri(totpStr) } else { - // Check if it's a plain Base32 secret - const secret = totpStr.replace(/\s/g, '').toUpperCase() + const secret = cleanSecret(totpStr) const base32Re = /^[A-Z2-7]+=*$/ if (base32Re.test(secret)) { accData = { @@ -109,7 +124,6 @@ export const csvStrategy = { } } } - if (accData) { accData.service = rowData['name'] || accData.service accData.account = rowData['login_username'] || accData.account @@ -138,22 +152,43 @@ export const csvStrategy = { rawVault.push(accData) } } - } else { - const secret = rowData['secret'] || '' - if (secret) { - // 本系统导出的 generic csv: name=account, issuer=service - // 兼容其他工具: 若有独立的 account 列则优先 - const service = rowData['issuer'] || rowData['name'] || 'Unknown' - const account = rowData['account'] || rowData['name'] || '' - rawVault.push({ - service, - account, - secret: secret.replace(/\s/g, '').toUpperCase(), - algorithm: (rowData['algorithm'] || 'SHA1').toUpperCase().replace(/^SHA-?1$/, 'SHA-1').replace(/^SHA-?256$/, 'SHA-256').replace(/^SHA-?512$/, 'SHA-512'), - digits: parseInt(rowData['digits'] || '6', 10), - period: parseInt(rowData['period'] || '30', 10), - category: '' - }) + } + // --- 兜底路径:智能同义词识别 (支持 Keeper, NordPass, RoboForm, KeePassXC 等) --- + else { + // 查找包含密钥数据的列 + const totpHeader = headers.find(h => TOTP_SYNONYMS.includes(h)) + const totpVal = totpHeader ? (rowData[totpHeader] || '').trim() : '' + + if (totpVal) { + // 情况 A: 包含 otpauth:// 的 URI + if (totpVal.toLowerCase().startsWith('otpauth://')) { + const accData = parseOtpUri(totpVal) + if (accData) { + const serviceHeader = headers.find(h => NAME_SYNONYMS.includes(h)) + const userHeader = headers.find(h => USER_SYNONYMS.includes(h)) + accData.service = rowData[serviceHeader] || accData.service + accData.account = rowData[userHeader] || accData.account + rawVault.push(accData) + } + } + // 情况 B: 纯 Base32 密钥内容 + else { + const secret = cleanSecret(totpVal) + const base32Re = /^[A-Z2-7]+=*$/ + if (base32Re.test(secret)) { + const serviceHeader = headers.find(h => NAME_SYNONYMS.includes(h)) + const userHeader = headers.find(h => USER_SYNONYMS.includes(h)) + rawVault.push({ + service: rowData[serviceHeader] || 'Unknown', + account: rowData[userHeader] || 'Unknown Account', + secret: secret, + algorithm: (rowData['algorithm'] || 'SHA1').toUpperCase().replace(/^SHA-?1$/, 'SHA-1').replace(/^SHA-?256$/, 'SHA-256').replace(/^SHA-?512$/, 'SHA-512'), + digits: parseInt(rowData['digits'] || '6', 10), + period: parseInt(rowData['period'] || '30', 10), + category: '' + }) + } + } } } }