From 3e5e1afcfec83eeca7702ab8b254c6bd58dc8591 Mon Sep 17 00:00:00 2001 From: nap0o Date: Tue, 3 Mar 2026 14:02:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=94=AF=E6=8C=81=20Proton=20Auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + frontend/package.json | 6 +- frontend/public/argon2.wasm | Bin 0 -> 25725 bytes .../migration/composables/useDataImport.js | 6 +- .../migration/service/dataMigrationService.js | 9 ++ .../features/migration/views/dataImport.vue | 3 + .../vault/composables/useVaultActions.js | 13 +-- frontend/src/features/vault/views/home.vue | 8 +- .../components/icons/iconProtonAuth.vue | 16 +++ .../utils/serializers/protonStrategy.js | 105 ++++++++++++++++++ frontend/vite.config.js | 4 + 11 files changed, 158 insertions(+), 13 deletions(-) create mode 100755 frontend/public/argon2.wasm create mode 100644 frontend/src/shared/components/icons/iconProtonAuth.vue create mode 100644 frontend/src/shared/utils/serializers/protonStrategy.js diff --git a/.gitignore b/.gitignore index 4ce6b66..9a05614 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ logs .actrc .vscode/* .devcontainer/* +.test/* backend/.dev.vars backend/.wrangler diff --git a/frontend/package.json b/frontend/package.json index bc0a68e..bad1552 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,8 @@ "dependencies": { "@element-plus/icons-vue": "^2.3.2", "@tanstack/vue-query": "^5.66.0", + "argon2-browser": "^1.18.0", + "bcryptjs": "^3.0.3", "element-plus": "^2.9.5", "jsqr": "^1.4.0", "pinia": "^3.0.4", @@ -27,7 +29,9 @@ "unplugin-auto-import": "^0.19.0", "unplugin-vue-components": "^0.28.0", "vite": "^6.2.0", - "vite-plugin-pwa": "^0.21.1" + "vite-plugin-pwa": "^0.21.1", + "vite-plugin-top-level-await": "^1.6.0", + "vite-plugin-wasm": "^3.5.0" }, "overrides": { "serialize-javascript": "^7.0.3" diff --git a/frontend/public/argon2.wasm b/frontend/public/argon2.wasm new file mode 100755 index 0000000000000000000000000000000000000000..75c31114efa423c31c023d4df59bae4876cfb3f7 GIT binary patch literal 25725 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 B1NZ;{ literal 0 HcmV?d00001 diff --git a/frontend/src/features/migration/composables/useDataImport.js b/frontend/src/features/migration/composables/useDataImport.js index 523545c..b52db20 100644 --- a/frontend/src/features/migration/composables/useDataImport.js +++ b/frontend/src/features/migration/composables/useDataImport.js @@ -98,7 +98,7 @@ export function useDataImport(emitFn) { try { currentFile.value = file const reader = new FileReader() - + // 无论文件名为何,都先读取成二进制,再由 detectFileType 判断类型 reader.onload = async (event) => { const buffer = event.target.result @@ -124,7 +124,7 @@ export function useDataImport(emitFn) { } } - if (detectedType === 'encrypted' || detectedType === 'aegis_encrypted') { + if (detectedType === 'encrypted' || detectedType === 'aegis_encrypted' || detectedType === 'proton') { importPassword.value = '' isDialogHandled.value = false showDecryptDialog.value = true @@ -218,7 +218,7 @@ export function useDataImport(emitFn) { } // 先在前端做简单的去重处理(大小写/空格不敏感),避免重复发送相同条目 - const normalize = (s, a) => `${(s||'').toString().trim().toLowerCase()}:${(a||'').toString().trim().toLowerCase()}`; + const normalize = (s, a) => `${(s || '').toString().trim().toLowerCase()}:${(a || '').toString().trim().toLowerCase()}`; const seen = new Set(); const filteredVault = []; for (const acc of batchAccumulatedVault.value) { diff --git a/frontend/src/features/migration/service/dataMigrationService.js b/frontend/src/features/migration/service/dataMigrationService.js index 9c5d781..790bcfe 100644 --- a/frontend/src/features/migration/service/dataMigrationService.js +++ b/frontend/src/features/migration/service/dataMigrationService.js @@ -5,6 +5,7 @@ import { tryParseJSON, base64ToBytes } from '@/shared/utils/encoding' import { gaMigrationStrategy } from '@/shared/utils/serializers/gauthStrategy' import { csvStrategy } from '@/shared/utils/serializers/csvStrategy' import { aegisStrategy } from '@/shared/utils/serializers/aegisStrategy' +import protonStrategy from '@/shared/utils/serializers/protonStrategy' import { migrationError } from '@/shared/utils/errors/migrationError' import jsQR from 'jsqr' import initSqlJs from 'sql.js' @@ -74,6 +75,7 @@ export const dataMigrationService = { if (json.schemaVersion && Array.isArray(json.services)) return '2fas' if (json.version === 1 && json.db && typeof json.db === 'object' && Array.isArray(json.db.entries)) return 'aegis' // Aegis unencrypted if (json.version === 1 && json.header && json.db && typeof json.db === 'string') return 'aegis_encrypted' + if (json.version === 1 && typeof json.salt === 'string' && typeof json.content === 'string') return 'proton' } } @@ -578,6 +580,13 @@ export const dataMigrationService = { type = 'raw' } + if (type === 'proton') { + rawVault = await protonStrategy.parse(content, password) + type = 'raw' + content = JSON.stringify(rawVault) + password = undefined + } + if (type === 'aegis_encrypted') { const parsedAegis = tryParseJSON(content) const decryptedDb = await aegisStrategy.decryptDatabase(parsedAegis, password) diff --git a/frontend/src/features/migration/views/dataImport.vue b/frontend/src/features/migration/views/dataImport.vue index 35fc803..2451bc5 100644 --- a/frontend/src/features/migration/views/dataImport.vue +++ b/frontend/src/features/migration/views/dataImport.vue @@ -41,6 +41,7 @@ 2FAS (.2fas) Aegis (.json/.txt) Bitwarden Auth (.json/.csv) + Proton Auth (.json) Google Auth (.png/.jpg) Microsoft Auth (PhoneFactor) @@ -97,6 +98,7 @@ + @@ -121,6 +123,7 @@ import iconAegis from '@/shared/components/icons/iconAegis.vue' import iconGoogleAuth from '@/shared/components/icons/iconGoogleAuth.vue' import iconBitwarden from '@/shared/components/icons/iconBitwarden.vue' import iconMicrosoftAuth from '@/shared/components/icons/iconMicrosoftAuth.vue' +import iconProtonAuth from '@/shared/components/icons/iconProtonAuth.vue' const emit = defineEmits(['success']) diff --git a/frontend/src/features/vault/composables/useVaultActions.js b/frontend/src/features/vault/composables/useVaultActions.js index 2c57820..5b35308 100644 --- a/frontend/src/features/vault/composables/useVaultActions.js +++ b/frontend/src/features/vault/composables/useVaultActions.js @@ -127,10 +127,9 @@ export function useVaultActions(fetchVault, vault) { showSecret.value = false showQrDialog.value = true - const algorithm = vaultItem.algorithm - ? vaultItem.algorithm.replace('SHA', 'SHA-').replace('SHA--', 'SHA-') - : 'SHA-1' - const uri = `otpauth://totp/${encodeURIComponent(vaultItem.service)}:${encodeURIComponent(vaultItem.account)}?secret=${vaultItem.secret}&issuer=${encodeURIComponent(vaultItem.service)}&algorithm=${algorithm}&digits=${vaultItem.digits || 6}&period=${vaultItem.period || 30}` + // otpauth URI spec: algorithm 参数不带横线,即 SHA1/SHA256/SHA512 + const algorithm = (vaultItem.algorithm || 'SHA1').replace(/-/g, '').toUpperCase() + const uri = `otpauth://totp/${encodeURIComponent(vaultItem.service)}:${encodeURIComponent(vaultItem.account)}?secret=${encodeURIComponent(vaultItem.secret)}&issuer=${encodeURIComponent(vaultItem.service)}&algorithm=${algorithm}&digits=${vaultItem.digits || 6}&period=${vaultItem.period || 30}` qrCodeUrl.value = await QRCode.toDataURL(uri, { width: 240, margin: 1 }) } @@ -144,10 +143,8 @@ export function useVaultActions(fetchVault, vault) { const copyOtpUrl = () => { if (currentQrItem.value) { const item = currentQrItem.value - const algorithm = item.algorithm - ? item.algorithm.replace('SHA', 'SHA-').replace('SHA--', 'SHA-') - : 'SHA-1' - const uri = `otpauth://totp/${encodeURIComponent(item.service)}:${encodeURIComponent(item.account)}?secret=${item.secret}&issuer=${encodeURIComponent(item.service)}&algorithm=${algorithm}&digits=${item.digits || 6}&period=${item.period || 30}` + const algorithm = (item.algorithm || 'SHA1').replace(/-/g, '').toUpperCase() + const uri = `otpauth://totp/${encodeURIComponent(item.service)}:${encodeURIComponent(item.account)}?secret=${encodeURIComponent(item.secret)}&issuer=${encodeURIComponent(item.service)}&algorithm=${algorithm}&digits=${item.digits || 6}&period=${item.period || 30}` copyToClipboard(uri) ElMessage.success('OTP URI 已复制') } diff --git a/frontend/src/features/vault/views/home.vue b/frontend/src/features/vault/views/home.vue index 2ae7050..0a75bab 100644 --- a/frontend/src/features/vault/views/home.vue +++ b/frontend/src/features/vault/views/home.vue @@ -59,7 +59,13 @@ const DataImport = defineAsyncComponent(() => import('@/features/migration/vi const DataBackup = defineAsyncComponent(() => import('@/features/backup/views/dataBackup.vue')) const UtilityTools = defineAsyncComponent(() => import('@/features/tools/views/utilityTools.vue')) -const activeTab = ref('vault') +const sessionKey = 'activeTab' +const activeTab = ref(sessionStorage.getItem(sessionKey) || 'vault') + +watch(activeTab, (newVal) => { + sessionStorage.setItem(sessionKey, newVal) +}) + const vaultListRef = ref(null) // 操作成功后:跳回账户列表并刷新数据 diff --git a/frontend/src/shared/components/icons/iconProtonAuth.vue b/frontend/src/shared/components/icons/iconProtonAuth.vue new file mode 100644 index 0000000..7b10428 --- /dev/null +++ b/frontend/src/shared/components/icons/iconProtonAuth.vue @@ -0,0 +1,16 @@ + + \ No newline at end of file diff --git a/frontend/src/shared/utils/serializers/protonStrategy.js b/frontend/src/shared/utils/serializers/protonStrategy.js new file mode 100644 index 0000000..e8230f7 --- /dev/null +++ b/frontend/src/shared/utils/serializers/protonStrategy.js @@ -0,0 +1,105 @@ +import argon2 from 'argon2-browser/dist/argon2-bundled.min.js'; +import { parseOtpUri } from '../totp'; + +export default { + name: 'Proton Pass (.json)', + fileType: 'application/json, text/plain', + + async parse(fileContent, password) { + let jsonPayload; + try { + jsonPayload = JSON.parse(fileContent); + } catch (e) { + throw new Error('INVALID_FORMAT_OR_PASSWORD'); + } + + if (jsonPayload.version !== 1 || !jsonPayload.salt || !jsonPayload.content) { + throw new Error('INVALID_FORMAT_OR_PASSWORD'); + } + + if (!password) { + throw new Error('PASSWORD_REQUIRED'); + } + + try { + // Decode base64 in browser + const saltStr = atob(jsonPayload.salt); + const salt = new Uint8Array(saltStr.length); + for (let i = 0; i < saltStr.length; i++) { + salt[i] = saltStr.charCodeAt(i); + } + + const contentStr = atob(jsonPayload.content); + const content = new Uint8Array(contentStr.length); + for (let i = 0; i < contentStr.length; i++) { + content[i] = contentStr.charCodeAt(i); + } + + // Extract AES-GCM components + const iv = content.slice(0, 12); + // WebCrypto decrypter expects ciphertext followed by auth tag in one buffer + const cipherAndTag = content.slice(12); + + // 1. Derive key via Argon2id + const argon2Result = await argon2.hash({ + pass: password, + salt: salt, + time: 2, + mem: 19 * 1024, + hashLen: 32, + parallelism: 1, + type: argon2.ArgonType.Argon2id, + distPath: '/' + }); + + // 2. Decrypt via Web Crypto API + // argon2-browser returns Uint8Array in argon2Result.hash + const keyMaterial = argon2Result.hash; + + const cryptoKey = await window.crypto.subtle.importKey( + 'raw', + keyMaterial, + { name: 'AES-GCM' }, + false, + ['decrypt'] + ); + + // AAD defined in Rust + const aad = new TextEncoder().encode('proton.authenticator.export.v1'); + + const decryptedBuffer = await window.crypto.subtle.decrypt( + { + name: 'AES-GCM', + iv: iv, + additionalData: aad, + tagLength: 128 + }, + cryptoKey, + cipherAndTag + ); + + const decryptedString = new TextDecoder().decode(decryptedBuffer); + const decryptedJson = JSON.parse(decryptedString); + + // 3. Parse items + const entries = decryptedJson.entries || []; + const results = []; + + for (const entry of entries) { + if (entry.content && entry.content.uri) { + const parsed = parseOtpUri(entry.content.uri); + if (parsed) { + results.push(parsed); + } + } + } + + return results; + + } catch (err) { + console.error('Proton decryption failed:', err); + // Must throw this precise message so UI prompts for password again if wrong + throw new Error('INVALID_FORMAT_OR_PASSWORD'); + } + } +}; diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 116913d..149503a 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -7,6 +7,8 @@ import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' import { execSync } from 'child_process' +import wasm from 'vite-plugin-wasm' +import topLevelAwait from 'vite-plugin-top-level-await' let commitHash = '' try { @@ -18,6 +20,8 @@ try { export default defineConfig({ plugins: [ vue(), + wasm(), + topLevelAwait(), AutoImport({ imports: ['vue', 'vue-router'], resolvers: [ElementPlusResolver({ importStyle: false })],