feat: 支持更多密码管理器csv格式的备份数据导入
This commit is contained in:
1 parent
1b809cfc24
commit
d8daff4f0c
10 files changed
+130
-25
No files matched your search
@@ -63,6 +63,12 @@
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconBitwardenPass /></el-icon> Bitwarden Pass (.json/.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><icon1Password /></el-icon> 1Password (.1pux/.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconProtonPass /></el-icon> Proton Pass (.pgp/.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconKeeper /></el-icon> Keeper (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconNordPass /></el-icon> NordPass (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconEnpass /></el-icon> Enpass (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconRoboForm /></el-icon> Roboform (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconKeePass /></el-icon> KeePassXC (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconApple /></el-icon> iCloud Keychain (.csv)</el-button>
|
||||
<el-button plain class="migration-button-with-icon" @click="triggerUpload"><el-icon><iconDashlanePass /></el-icon> Dashlane (.csv)</el-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg viewBox="6 6 36 36" xmlns="http://www.w3.org/2000/svg" height="24" width="24">
|
||||
<path d="M36.1,26.5c0-4.9,4-7.3,4.2-7.5c-2.3-3.3-5.8-3.8-7-3.8c-2.9-0.3-5.7,1.7-7.2,1.7c-1.5,0-3.8-1.7-6.2-1.7 c-3.1,0-6,1.8-7.6,4.6c-3.2,5.6-0.8,13.9,2.3,18.4c1.5,2.2,3.3,4.6,5.7,4.5c2.3-0.1,3.2-1.5,6-1.5c2.8,0,3.6,1.5,6,1.4 c2.5-0.1,4-2.2,5.5-4.4c1.8-2.6,2.5-5.1,2.6-5.2C40.3,33.1,36.1,31.5,36.1,26.5z M30.8,11.3c1.3-1.6,2.2-3.8,1.9-6.1 c-2,0.1-4.4,1.3-5.8,3c-1.3,1.5-2.4,3.7-2.1,5.9C27.1,14.2,29.4,12.9,30.8,11.3z" fill="currentColor"/>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconApple' }
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg" fill="none">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M45.264 36.76c-10.63 0-18.583 9.76-16.435 20.173l15.646 75.828c2.701 13.089 14.226 22.479 27.59 22.479h47.87c13.364 0 24.889-9.39 27.59-22.479l15.646-75.828c2.148-10.413-5.804-20.174-16.435-20.174H45.264ZM17.077 59.357C13.392 41.5 27.03 24.76 45.264 24.76h101.472c18.234 0 31.872 16.741 28.187 34.6l-15.646 75.827c-3.851 18.664-20.285 32.054-39.342 32.054h-47.87c-19.057 0-35.491-13.39-39.342-32.054L17.077 59.358Zm43.927 21.108c0-18.859 15.54-33.96 34.478-33.96 18.938 0 34.478 15.101 34.478 33.96 0 11.459-5.756 21.546-14.498 27.679v25.135c0 6.745-5.468 12.214-12.213 12.214H88.751c-6.745 0-12.213-5.469-12.213-12.214v-24.436c-9.322-6.05-15.534-16.472-15.534-28.378Zm34.478-21.96c-12.518 0-22.478 9.935-22.478 21.96 0 8.467 4.914 15.88 12.225 19.549a6.001 6.001 0 0 1 3.309 5.363v27.902c0 .118.095.214.213.214h14.498a.213.213 0 0 0 .213-.214V104.83a6 6 0 0 1 3.092-5.247c6.859-3.802 11.406-10.97 11.406-19.118 0-12.025-9.959-21.96-22.478-21.96Z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconEnpass' }
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M12 0C5.378 0 0 5.378 0 12c0 6.623 5.378 12 12 12c6.623 0 12-5.377 12-12c0-6.622-5.377-12-12-12m0 .791a11.203 11.203 0 0 1 11.209 11.21A11.2 11.2 0 0 1 12 23.208A11.203 11.203 0 0 1 .791 12A11.203 11.203 0 0 1 12.001.791zm0 1.871a9.4 9.4 0 0 0-1.705.162A1.746 1.746 0 0 0 12 4.95a1.746 1.746 0 0 0 1.706-2.126A9.3 9.3 0 0 0 12 2.662M7.2 4a9.34 9.34 0 0 0-4.54 8a9.338 9.338 0 0 0 18.677 0a9.34 9.34 0 0 0-4.54-7.998a4.89 4.89 0 0 1-2.701 5.365l-.12 2.066l1.482 1.483l-1.483 1.482l.989.989l-.989.988l.12 1.857L12 20.328l-2.096-2.096V9.367A4.89 4.89 0 0 1 7.2 4m3.4 6.543v6.99h.7v-6.99z"/>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconKeePass' }
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg viewBox="250 250 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m739.1 548.2-44-36.2 43.9-36.2-25.2-30.5L633 512l80.9 66.8 25.2-30.6zm-31.3 74.3-109-87h-135v199.8h39.5V575H585l98.2 78.4 24.6-30.9zm-50.1 59.4-84.2-69.3-32.3.2v122.4l39.5-10.1v-55.4l44.2 36.4 32.8-24.2zM425.2 354.5l-39.5-34.4v387.3l39.5-34.4V354.5zm-77.5 50.8-39.5 36.3V592l39.5 33.7V405.3zm116.1 83.2h135l109-87-24.7-30.9L585 449h-81.6V288.7h-39.5v199.8h-.1zm77.3-71.9 32.3.2 84.2-69.3-32.8-24.2-44.2 36.4v-55.4l-39.5-10.1v122.4zM768 512c0 141.4-114.6 256-256 256S256 653.4 256 512s114.6-256 256-256 256 114.6 256 256z" fill="currentColor"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconKeeper' }
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" height="24" width="24">
|
||||
<path d="M2 6H22M2 12H22M2 18H22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconMenu' }
|
||||
</script>
|
||||
@@ -0,0 +1,12 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3" d="M6.59 36.89c-7.003-9.732-4.792-23.299 4.94-30.303A21.7 21.7 0 0 1 24 2.5c11.99.117 21.614 9.93 21.498 21.92a21.7 21.7 0 0 1-4.088 12.47L31.08 20l-1.86 3.17l1.88 3.23L24 14.17l-5.27 9l1.9 3.27L16.91 20z"/>
|
||||
<g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="3">
|
||||
<circle cx="20.605" cy="35" r="3.105"/>
|
||||
<path d="M30.5 35.011h-6.79m4.515 1.858v-1.858"/>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconNordPass' }
|
||||
</script>
|
||||
@@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<svg width="800px" height="800px" viewBox="200 200 600 600" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M308 656c-28.3 0-52 25.1-52 56s23.7 56 52 56h408c28.3 0 52-25.1 52-56s-23.7-56-52-56H308zm392-400H324c-37.2 0-68 30.8-68 68v232c0 37.1 30.9 68 68 68h376c37.2 0 68-30.8 68-68V324c0-37.2-30.8-68-68-68zm12 276c0 19.9-16.1 36-36 36H348c-19.9 0-36-16.1-36-36V348c0-19.9 16.1-36 36-36h328c19.9 0 36 16.1 36 36v184z" fill="currentColor"></path>
|
||||
<path fill="currentColor" d="M604.1 384.2c-30.2 0-54.6 24.5-54.6 54.6 0 30.2 24.5 54.6 54.6 54.6 30.2 0 54.6-24.5 54.6-54.6s-24.4-54.6-54.6-54.6m-181.4 0c-30.2 0-54.6 24.5-54.6 54.6 0 30.2 24.5 54.6 54.6 54.6 30.2 0 54.6-24.5 54.6-54.6s-24.4-54.6-54.6-54.6"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconRoboForm' }
|
||||
</script>
|
||||
@@ -32,11 +32,7 @@
|
||||
link
|
||||
class="header-menu-btn-refined"
|
||||
>
|
||||
<el-icon :size="20">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 6H20M4 12H20M4 18H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</el-icon>
|
||||
<el-icon :size="24"><iconMenu /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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: ''
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user