From bfea5d0a1c7b6a30f972cad26a6b2e7278983dc2 Mon Sep 17 00:00:00 2001 From: shuaiplus <2327005759@qq.com> Date: Sun, 7 Jun 2026 19:18:17 +0800 Subject: [PATCH] fix: add support for KeePass CSV import format and enhance import parsing logic --- .codegraph/.gitignore | 17 +++++++++++++ webapp/src/components/ImportPage.tsx | 1 + webapp/src/lib/import-format-sources.ts | 1 + webapp/src/lib/import-formats-csv-misc.ts | 31 ++++++++++++++++++++++- webapp/src/lib/import-formats.ts | 2 ++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 .codegraph/.gitignore diff --git a/.codegraph/.gitignore b/.codegraph/.gitignore new file mode 100644 index 0000000..2c9c938 --- /dev/null +++ b/.codegraph/.gitignore @@ -0,0 +1,17 @@ +# CodeGraph data files +# These are local to each machine and should not be committed + +# Database +*.db +*.db-wal +*.db-shm + +# Cache +cache/ + +# Logs +*.log + +# Hook markers +.dirty +*.pid diff --git a/webapp/src/components/ImportPage.tsx b/webapp/src/components/ImportPage.tsx index 2c66bf6..71b212a 100644 --- a/webapp/src/components/ImportPage.tsx +++ b/webapp/src/components/ImportPage.tsx @@ -91,6 +91,7 @@ const COMMON_IMPORT_SOURCE_IDS: ImportSourceId[] = [ 'lastpass', 'dashlane_csv', 'dashlane_json', + 'keepass_csv', 'keepass_xml', 'keepassx_csv', ]; diff --git a/webapp/src/lib/import-format-sources.ts b/webapp/src/lib/import-format-sources.ts index 5bb4995..1c13958 100644 --- a/webapp/src/lib/import-format-sources.ts +++ b/webapp/src/lib/import-format-sources.ts @@ -23,6 +23,7 @@ export const IMPORT_SOURCES = [ { id: 'lastpass', label: 'LastPass (csv)' }, { id: 'dashlane_csv', label: 'Dashlane (csv)' }, { id: 'dashlane_json', label: 'Dashlane (json)' }, + { id: 'keepass_csv', label: 'KeePass 1.x (csv)' }, { id: 'keepass_xml', label: 'KeePass 2 (xml)' }, { id: 'keepassx_csv', label: 'KeePassX (csv)' }, { id: 'arc_csv', label: 'Arc (csv)' }, diff --git a/webapp/src/lib/import-formats-csv-misc.ts b/webapp/src/lib/import-formats-csv-misc.ts index 83d30db..0e063a5 100644 --- a/webapp/src/lib/import-formats-csv-misc.ts +++ b/webapp/src/lib/import-formats-csv-misc.ts @@ -198,6 +198,7 @@ export function parseEncryptrCsv(textRaw: string): CiphersImportPayload { export function parseKeePassXCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; + const standardColumns = new Set(['Group', 'Title', 'Username', 'Password', 'URL', 'Notes', 'TOTP']); for (const row of rows) { if (!txt(row.Title)) continue; const cipher = makeLoginCipher(); @@ -209,12 +210,34 @@ export function parseKeePassXCsv(textRaw: string): CiphersImportPayload { login.totp = val(row.TOTP); const uri = normalizeUri(row.URL || ''); login.uris = uri ? [{ uri, match: null }] : null; + for (const [key, value] of Object.entries(row)) { + if (standardColumns.has(key)) continue; + processKvp(cipher, key, value, false); + } const idx = result.ciphers.push(cipher) - 1; addFolder(result, txt(row.Group).replace(/^Root\//, ''), idx); } return result; } +export function parseKeePassCsv(textRaw: string): CiphersImportPayload { + const rows = parseCsv(textRaw); + const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; + for (const row of rows) { + if (!txt(row.Account)) continue; + const cipher = makeLoginCipher(); + cipher.name = val(row.Account, '--'); + cipher.notes = val(row.Comments); + const login = cipher.login as Record; + login.username = val(row['Login Name']); + login.password = val(row.Password); + const uri = normalizeUri(row['Web Site'] || ''); + login.uris = uri ? [{ uri, match: null }] : null; + result.ciphers.push(cipher); + } + return result; +} + export function parseLastPassCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; @@ -350,7 +373,8 @@ export function parseKeePassXml(textRaw: string): CiphersImportPayload { const cipher = makeLoginCipher(); for (const s of qd(entry, 'String')) { const key = txt(qd(s, 'Key')[0]?.textContent); - const value = txt(qd(s, 'Value')[0]?.textContent); + const valueNode = qd(s, 'Value')[0]; + const value = txt(valueNode?.textContent); if (!value) continue; const login = cipher.login as Record; if (key === 'Title') cipher.name = value; @@ -361,6 +385,11 @@ export function parseKeePassXml(textRaw: string): CiphersImportPayload { login.uris = uri ? [{ uri, match: null }] : null; } else if (key === 'otp') login.totp = value.replace('key=', ''); else if (key === 'Notes') cipher.notes = `${txt(cipher.notes)}${txt(cipher.notes) ? '\n' : ''}${value}`; + else { + const hidden = ['True', 'true', '1'].includes(valueNode?.getAttribute('ProtectInMemory') || '') + || ['True', 'true', '1'].includes(valueNode?.getAttribute('Protected') || ''); + processKvp(cipher, key, value, hidden); + } } const idx = result.ciphers.push(cipher) - 1; if (!isRoot && folder >= 0) result.folderRelationships.push({ key: idx, value: folder }); diff --git a/webapp/src/lib/import-formats.ts b/webapp/src/lib/import-formats.ts index 8d494d6..ad12258 100644 --- a/webapp/src/lib/import-formats.ts +++ b/webapp/src/lib/import-formats.ts @@ -10,6 +10,7 @@ import { parseDashlaneCsv, parseDashlaneJson, parseEncryptrCsv, + parseKeePassCsv, parseKeePassXCsv, parseKeePassXml, parseLastPassCsv, @@ -75,6 +76,7 @@ const IMPORT_SOURCE_PARSERS: Record Ciphers lastpass: parseLastPassCsv, dashlane_csv: parseDashlaneCsv, dashlane_json: parseDashlaneJson, + keepass_csv: parseKeePassCsv, keepass_xml: parseKeePassXml, keepassx_csv: parseKeePassXCsv, arc_csv: parseArcCsv,