feat: 数据导入新增支持 Proton Auth
This commit is contained in:
1 parent
5ab24ee862
commit
3e5e1afcfe
11 files changed
+158
-13
No files matched your search
@@ -6,6 +6,7 @@ logs
|
||||
.actrc
|
||||
.vscode/*
|
||||
.devcontainer/*
|
||||
.test/*
|
||||
|
||||
backend/.dev.vars
|
||||
backend/.wrangler
|
||||
|
||||
@@ -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"
|
||||
|
||||
Executable
BIN
Binary file not shown.
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
<el-tag type="info" effect="light"><icon2FAS /> 2FAS (.2fas)</el-tag>
|
||||
<el-tag type="info" effect="light"><iconAegis /> Aegis (.json/.txt)</el-tag>
|
||||
<el-tag type="info" effect="light"><iconBitwarden /> Bitwarden Auth (.json/.csv)</el-tag>
|
||||
<el-tag type="info" effect="light"><iconProtonAuth /> Proton Auth (.json)</el-tag>
|
||||
<el-tag type="info" effect="light"><iconGoogleAuth /> Google Auth (.png/.jpg)</el-tag>
|
||||
<el-tag type="info" effect="light"><iconMicrosoftAuth /> Microsoft Auth (PhoneFactor)</el-tag>
|
||||
</div>
|
||||
@@ -97,6 +98,7 @@
|
||||
<!-- 加密文件密码输入弹窗 -->
|
||||
<el-dialog v-model="showDecryptDialog" title="🔓 解密备份文件" width="400px" @close="handleDecryptDialogClose" destroy-on-close>
|
||||
<el-alert v-if="currentImportType === 'aegis_encrypted'" title="检测到 Aegis 加密备份" type="warning" :closable="false" style="margin-bottom: 15px;" />
|
||||
<el-alert v-else-if="currentImportType === 'proton'" title="检测到 Proton Pass 加密备份" type="warning" :closable="false" style="margin-bottom: 15px;" />
|
||||
<el-alert v-else title="检测到本系统加密备份" type="success" :closable="false" style="margin-bottom: 15px;" />
|
||||
<el-form label-position="top" v-loading="isDecrypting" :element-loading-text="loadingText">
|
||||
<el-form-item label="请输入该备份的解密密码:">
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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 已复制')
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
// 操作成功后:跳回账户列表并刷新数据
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<svg fill="none" width="1em" height="1em" viewBox="0 0 482 482" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="currentColor" d="M282.973 219.186c0 16.827-9.934 31.28-24.2 37.973v29.894c0 9.8-7.934 17.733-17.774 17.733-9.826 0-17.72-7.933-17.72-17.733v-29.894c-14.306-6.693-24.186-21.146-24.186-37.973 0-23.186 18.773-41.96 41.906-41.96 23.147 0 41.974 18.774 41.974 41.96m76.267 72.054c3.892-9.227 6.706-18.774 8.372-28.467-1.626 9.733-4.426 19.24-8.373 28.466zm10.266-50.28c0 1.426-.04 2.893-.094 4.32-.093 3.893-.426 7.8-.946 11.64.48-3.947.8-7.84.906-11.787a61 61 0 0 0 .134-4.174z"></path>
|
||||
<path fill="currentColor" d="M296.8 56.8c0 1.04-.054 2.04-.107 3.093 0 .52-.04 1.04-.134 1.56-.053 1.094-.2 2.147-.333 3.24a38 38 0 0 1-.573 3.32c0 .387-.147.72-.24 1a32 32 0 0 1-.707 2.853c-.333 1.374-.813 2.707-1.293 3.987-.28.907-.654 1.76-.987 2.613-.2.48-.387.907-.573 1.294-.48 1.04-.947 2.04-1.48 2.986-.52 1-1.094 2-1.654 2.947a80 80 0 0 1-3 4.427 57 57 0 0 1-4.04 4.8c-.853.893-1.706 1.8-2.613 2.613-.947.853-1.853 1.707-2.853 2.467a56 56 0 0 1-9.36 6.226h-.054c-14.68-9.413-30.12-17.013-46.146-22.906a250 250 0 0 0-16.107-5.28c-2.76-.8-5.466-1.52-8.213-2.187-37.84-9.453-77.52-9.546-115.44-.373-3.093.76-6.227 1.613-9.32 2.466A296 296 0 0 0 62.36 80.8a206 206 0 0 1 8.933-9.507c1.613-1.573 3.173-3.093 4.747-4.613 2.906-2.747 5.8-5.413 8.84-7.933 1.573-1.374 3.133-2.654 4.706-3.987a202 202 0 0 1 9.027-6.987c.187-.146.333-.24.48-.386 1.667-1.134 3.28-2.374 4.893-3.467 11.12-7.747 22.76-14.493 34.787-20.147 3.427-1.613 6.893-3.093 10.413-4.56 1.8-.76 3.654-1.48 5.507-2.186 1.76-.72 3.56-1.334 5.32-2 1.813-.614 3.667-1.24 5.467-1.854 1.813-.613 3.666-1.186 5.426-1.76 1.8-.52 3.654-1.04 5.454-1.573a246 246 0 0 1 22.24-5.08c1.866-.333 3.72-.613 5.613-.907 11.213-1.76 22.533-2.653 33.893-2.8.987 0 1.947-.053 2.893-.053.947 0 1.947.053 2.894.053C273.413 2.52 296.839 26.907 296.8 56.8"></path>
|
||||
<path fill="currentColor" d="M296.8 56.8c0 1.04-.054 2.04-.107 3.093a9 9 0 0 1-.133 1.56c-.054 1.094-.2 2.147-.334 3.24a38 38 0 0 1-.573 3.32c0 .387-.147.72-.24 1a32 32 0 0 1-.707 2.853 35 35 0 0 1-1.293 3.987c-.28.907-.653 1.76-.987 2.613a42 42 0 0 1-2.053 4.28c-.52 1-1.093 2-1.653 2.947a57.4 57.4 0 0 1-7.04 9.227c-.854.893-1.707 1.8-2.614 2.613-.946.853-1.853 1.707-2.853 2.467a56 56 0 0 1-9.36 6.226h-.053a236.5 236.5 0 0 0-70.467-30.373 242 242 0 0 0-124.76 2.093A296 296 0 0 0 62.36 80.8a206 206 0 0 1 13.68-14.12c2.907-2.747 5.8-5.413 8.84-7.933 1.574-1.374 3.134-2.654 4.707-3.987a202 202 0 0 1 9.027-6.987c.186-.146.333-.24.48-.386 1.666-1.134 3.28-2.374 4.893-3.467a238 238 0 0 1 34.786-20.147 251 251 0 0 1 15.92-6.746c1.76-.72 3.56-1.334 5.32-2l5.467-1.854c1.813-.613 3.667-1.186 5.427-1.76 1.8-.52 3.653-1.04 5.453-1.573a246 246 0 0 1 22.24-5.08c1.867-.333 3.72-.613 5.613-.907 11.214-1.76 22.534-2.653 33.894-2.8.986 0 1.946-.053 2.893-.053s1.947.053 2.893.053C273.576 2.547 296.859 27.08 296.8 56.8Z" stroke="currentColor"></path>
|
||||
<path fill="currentColor" d="M480.999 238.106v5.8c-.147 11.36-1.04 22.667-2.8 33.88-.24 1.907-.574 3.76-.907 5.613-1.333 7.454-3 14.867-5.08 22.24-.533 1.8-1 3.654-1.573 5.467-.574 1.76-1.147 3.613-1.76 5.413-.574 1.854-1.24 3.667-1.853 5.467-.667 1.76-1.28 3.56-2 5.32q-1.048 2.778-2.187 5.52a206 206 0 0 1-4.56 10.4 234.6 234.6 0 0 1-20.147 34.786c-1.093 1.627-2.28 3.24-3.466 4.907-.147.133-.24.28-.334.467-2.28 3-4.613 6.08-7.04 9.026-1.333 1.574-2.613 3.147-3.946 4.707-2.56 3.04-5.227 5.947-7.974 8.84-1.52 1.573-3.053 3.133-4.613 4.76-27.373 27.373-59.787 46.813-94.2 58.16 56.133-62.44 73.76-147.226 52.893-223.6 0-.04-.04-.093-.04-.146.094-1.374.147-2.747.147-4.174 0-30.8 24.947-55.746 55.747-55.746 29.84-.053 54.213 23.386 55.693 52.893"></path>
|
||||
<path fill="currentColor" stroke="currentColor" d="M480.999 238.106v5.8c-.146 11.36-1.04 22.667-2.8 33.88-.24 1.907-.573 3.76-.906 5.613-1.334 7.454-3 14.867-5.08 22.24-.534 1.8-1 3.654-1.574 5.467-.573 1.76-1.146 3.613-1.76 5.413-.573 1.854-1.24 3.667-1.853 5.467-.667 1.76-1.28 3.56-2 5.32a183 183 0 0 1-2.187 5.52 234.6 234.6 0 0 1-24.706 45.186c-1.094 1.627-2.28 3.24-3.467 4.907q-.211.201-.333.467c-2.28 3-4.614 6.08-7.04 9.026-1.334 1.574-2.614 3.147-3.947 4.707-2.56 3.04-5.227 5.947-7.973 8.84l-4.614 4.76a238.85 238.85 0 0 1-94.2 58.16c56.134-62.44 73.76-147.226 52.894-223.6 0-.04-.04-.093-.04-.146.093-1.374.147-2.747.147-4.174a55.733 55.733 0 0 1 55.746-55.746c29.7-.044 54.207 23.23 55.693 52.893Z"></path>
|
||||
<path fill="currentColor" d="M316.52 468.879c-83.414 27.52-178.88 8.133-245.227-58.16-90.626-90.64-93.626-235.72-8.933-329.92 3.04-1 6.133-1.946 9.213-2.853 3.093-.853 6.227-1.706 9.32-2.466 37.92-9.227 77.6-9.133 115.44.373 2.747.667 5.466 1.387 8.213 2.187a250 250 0 0 1 16.12 5.28c16.013 5.893 31.453 13.493 46.147 22.906-.96.52-1.96 1-2.947 1.467-1 .387-2 .813-3 1.187-1 .386-2.04.773-3.093 1.093-.04 0-.094 0-.134.053a56.6 56.6 0 0 1-13.12 2.374 74 74 0 0 1-3.466.093c-32.894 0-65.773 12.547-90.867 37.64-50.146 50.187-50.186 131.506 0 181.693 50.187 50.186 131.547 50.133 181.68 0 1.573-1.573 3.093-3.133 4.56-4.8 2.76-3.04 5.333-6.187 7.746-9.453a275 275 0 0 0 3.574-5.04c1.133-1.667 2.226-3.427 3.28-5.134 1.04-1.76 2.04-3.52 2.986-5.28 1-1.8 1.907-3.613 2.76-5.413.854-1.813 1.707-3.613 2.467-5.467a129 129 0 0 0 8.373-28.466c.32-1.947.614-3.894.854-5.854.466-3.933.8-7.84.893-11.773 0 .04.053.093.053.133 20.867 76.374 3.227 161.16-52.893 223.6z"></path>
|
||||
<path stroke="currentColor" d="M316.52 468.879c-83.413 27.52-178.88 8.133-245.226-58.16-90.627-90.64-93.627-235.72-8.934-329.92 3.04-1 6.134-1.946 9.214-2.853a240.64 240.64 0 0 1 149.092 5.374 237.3 237.3 0 0 1 46.146 22.906 61 61 0 0 1-2.947 1.467c-1 .387-2 .813-3 1.187a45 45 0 0 1-3.093 1.093c-.04 0-.093 0-.133.053a56.6 56.6 0 0 1-16.587 2.467 128.1 128.1 0 0 0-90.866 37.64c-50.147 50.187-50.187 131.506 0 181.693 50.186 50.186 131.546 50.133 181.68 0a121 121 0 0 0 12.306-14.253 139 139 0 0 0 6.853-10.174 151 151 0 0 0 5.747-10.693c.853-1.813 1.707-3.613 2.467-5.467a129 129 0 0 0 9.226-34.32c.467-3.933.8-7.84.894-11.773 0 .04.053.093.053.133 20.867 76.374 3.227 161.16-52.893 223.6Z"></path>
|
||||
<path stroke="currentColor" d="M282.973 219.186c0 16.827-9.934 31.28-24.2 37.973v29.894c0 9.8-7.934 17.733-17.774 17.733-9.826 0-17.72-7.933-17.72-17.733v-29.894c-14.306-6.693-24.186-21.146-24.186-37.973 0-23.186 18.773-41.96 41.906-41.96 23.147 0 41.974 18.774 41.974 41.96Z"></path>
|
||||
<path stroke="currentColor" d="M282.973 219.186c0 16.827-9.934 31.28-24.2 37.973v29.894c0 9.8-7.934 17.733-17.774 17.733-9.826 0-17.72-7.933-17.72-17.733v-29.894c-14.306-6.693-24.186-21.146-24.186-37.973 0-23.186 18.773-41.96 41.906-41.96 23.147 0 41.974 18.774 41.974 41.96Z"></path>
|
||||
</svg>
|
||||
</template>
|
||||
<script>
|
||||
export default { name: 'iconProtonAuth' }
|
||||
</script>
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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 })],
|
||||
|
||||
Reference in new issue
Block a user