import type { CiphersImportPayload } from '@/lib/api/vault'; import { addFolder, cardBrand, makeLoginCipher, normalizeUri, parseCardExpiry, parseCsv, parseCsvRows, processKvp, splitFullName, txt, val, } from '@/lib/import-format-shared'; export function parseEnpassCsv(textRaw: string): CiphersImportPayload { const rows = parseCsvRows(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; let first = true; for (const r of rows) { if (r.length < 2 || (first && (r[0] === 'Title' || r[0] === 'title'))) { first = false; continue; } const cipher = makeLoginCipher(); cipher.name = val(r[0], '--'); cipher.notes = val(r[r.length - 1]); const hasLoginHints = r.some((x) => ['username', 'password', 'email', 'url'].includes(txt(x).toLowerCase())); const hasCardHints = r.some((x) => ['cardholder', 'number', 'expiry date'].includes(txt(x).toLowerCase())); if (r.length === 2 || !hasLoginHints) { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; } if (hasCardHints) { cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: null, number: null, brand: null, expMonth: null, expYear: null, code: null }; } if (r.length > 2 && r.length % 2 === 0) { for (let i = 0; i < r.length - 2; i += 2) { const fieldName = txt(r[i + 1]); const fieldValue = txt(r[i + 2]); if (!fieldValue) continue; const low = fieldName.toLowerCase(); if (cipher.type === 1) { const login = cipher.login as Record; if (low === 'url' && !Array.isArray(login.uris)) { const uri = normalizeUri(fieldValue); login.uris = uri ? [{ uri, match: null }] : null; continue; } if ((low === 'username' || low === 'email') && !txt(login.username)) { login.username = fieldValue; continue; } if (low === 'password' && !txt(login.password)) { login.password = fieldValue; continue; } if (low === 'totp' && !txt(login.totp)) { login.totp = fieldValue; continue; } } else if (cipher.type === 3 && cipher.card) { const card = cipher.card as Record; if (low === 'cardholder' && !txt(card.cardholderName)) { card.cardholderName = fieldValue; continue; } if (low === 'number' && !txt(card.number)) { card.number = fieldValue; card.brand = cardBrand(fieldValue); continue; } if (low === 'cvc' && !txt(card.code)) { card.code = fieldValue; continue; } if (low === 'expiry date' && !txt(card.expMonth) && !txt(card.expYear)) { const m = fieldValue.match(/^0?([1-9]|1[0-2])\/((?:[1-2][0-9])?[0-9]{2})$/); if (m) { card.expMonth = m[1]; card.expYear = m[2].length === 2 ? `20${m[2]}` : m[2]; continue; } } if (low === 'type') continue; } processKvp(cipher, fieldName, fieldValue, false); } } result.ciphers.push(cipher); } return result; } export function parseEnpassJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { folders?: any[]; items?: any[] }; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const folderTitleById = new Map(); for (const f of parsed.folders || []) { if (f?.uuid && f?.title) folderTitleById.set(String(f.uuid), String(f.title).trim()); } for (const item of parsed.items || []) { const cipher = makeLoginCipher(); cipher.name = val(item?.title, '--'); cipher.favorite = Number(item?.favorite || 0) > 0; cipher.notes = val(item?.note); const templateType = txt(item?.template_type); const fields = Array.isArray(item?.fields) ? item.fields : []; if (templateType.startsWith('creditcard.')) { cipher.type = 3; cipher.login = null; const card: Record = { cardholderName: null, number: null, code: null, expMonth: null, expYear: null, brand: null, }; for (const field of fields) { const t = txt(field?.type); const v = txt(field?.value); if (!v || t === 'section' || t === 'ccType') continue; if (t === 'ccName' && !txt(card.cardholderName)) card.cardholderName = v; else if (t === 'ccNumber' && !txt(card.number)) { card.number = v; card.brand = cardBrand(v); } else if (t === 'ccCvc' && !txt(card.code)) card.code = v; else if (t === 'ccExpiry' && !txt(card.expYear)) { const m = v.match(/^0?([1-9]|1[0-2])\/((?:[1-2][0-9])?[0-9]{2})$/); if (m) { card.expMonth = m[1]; card.expYear = m[2].length === 2 ? `20${m[2]}` : m[2]; } else { processKvp(cipher, txt(field?.label), v, Number(field?.sensitive || 0) === 1); } } else { processKvp(cipher, txt(field?.label), v, Number(field?.sensitive || 0) === 1); } } cipher.card = card; } else if ( templateType.startsWith('login.') || templateType.startsWith('password.') || fields.some((f: any) => txt(f?.type) === 'password' && txt(f?.value)) ) { const login = cipher.login as Record; const urls: string[] = []; for (const field of fields) { const t = txt(field?.type); const v = txt(field?.value); if (!v || t === 'section') continue; if ((t === 'username' || t === 'email') && !txt(login.username)) login.username = v; else if (t === 'password' && !txt(login.password)) login.password = v; else if (t === 'totp' && !txt(login.totp)) login.totp = v; else if (t === 'url') { const n = normalizeUri(v); if (n) urls.push(n); } else if (t === '.Android#') { let cleaned = v.startsWith('androidapp://') ? v : `androidapp://${v}`; cleaned = cleaned.replace('android://', '').replace(/androidapp:\/\/.*==@/g, 'androidapp://'); const n = normalizeUri(cleaned) || cleaned; urls.push(n); } else { processKvp(cipher, txt(field?.label), v, Number(field?.sensitive || 0) === 1); } } login.uris = urls.length ? urls.map((u) => ({ uri: u, match: null })) : null; } else { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; for (const field of fields) { const v = txt(field?.value); if (!v || txt(field?.type) === 'section') continue; processKvp(cipher, txt(field?.label), v, Number(field?.sensitive || 0) === 1); } } const idx = result.ciphers.push(cipher) - 1; const folderId = Array.isArray(item?.folders) && item.folders.length ? String(item.folders[0]) : ''; if (folderId && folderTitleById.has(folderId)) addFolder(result, folderTitleById.get(folderId) || '', idx); } return result; } export function parseKeeperCsv(textRaw: string): CiphersImportPayload { const rows = parseCsvRows(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { if (row.length < 6) continue; const cipher = makeLoginCipher(); cipher.name = val(row[1], '--'); const login = cipher.login as Record; login.username = val(row[2]); login.password = val(row[3]); const uri = normalizeUri(row[4] || ''); login.uris = uri ? [{ uri, match: null }] : null; cipher.notes = val(row[5]); if (row.length > 7) { for (let i = 7; i < row.length; i += 2) { const k = txt(row[i]); const v = txt(row[i + 1]); if (!k) continue; if (k === 'TFC:Keeper') (cipher.login as Record).totp = val(v); else processKvp(cipher, k, v, false); } } const idx = result.ciphers.push(cipher) - 1; addFolder(result, row[0], idx); } return result; } export function parseKeeperJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { records?: any[] }; const records = Array.isArray(parsed.records) ? parsed.records : []; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const record of records) { const cipher = makeLoginCipher(); cipher.name = val(record.title, '--'); const login = cipher.login as Record; login.username = val(record.login); login.password = val(record.password); const uri = normalizeUri(record.login_url || ''); login.uris = uri ? [{ uri, match: null }] : null; cipher.notes = val(record.notes); const cf = record.custom_fields || {}; if (cf['TFC:Keeper']) login.totp = val(cf['TFC:Keeper']); for (const key of Object.keys(cf)) { if (key === 'TFC:Keeper') continue; processKvp(cipher, key, String(cf[key] ?? ''), false); } if (Array.isArray(record.folders)) { const idx = result.ciphers.push(cipher) - 1; for (const f of record.folders) { const folderName = f?.folder || f?.shared_folder; if (folderName) addFolder(result, String(folderName), idx); } } else { result.ciphers.push(cipher); } } return result; } export function parseLogMeOnceCsv(textRaw: string): CiphersImportPayload { const rows = parseCsvRows(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { if (row.length < 4) continue; const cipher = makeLoginCipher(); cipher.name = val(row[0], '--'); const login = cipher.login as Record; login.username = val(row[2]); login.password = val(row[3]); const uri = normalizeUri(row[1] || ''); login.uris = uri ? [{ uri, match: null }] : null; result.ciphers.push(cipher); } return result; } export function parseMeldiumCsv(textRaw: string): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const row of rows) { const cipher = makeLoginCipher(); cipher.name = val(row.DisplayName, '--'); cipher.notes = val(row.Notes); const login = cipher.login as Record; login.username = val(row.UserName); login.password = val(row.Password); const uri = normalizeUri(row.Url || ''); login.uris = uri ? [{ uri, match: null }] : null; result.ciphers.push(cipher); } return result; } export function parseProtonPassJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { encrypted?: boolean; vaults?: Record }; if (parsed?.encrypted) throw new Error('Unable to import an encrypted Proton Pass export.'); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const vaults = parsed?.vaults && typeof parsed.vaults === 'object' ? parsed.vaults : {}; for (const vault of Object.values(vaults)) { const vaultName = txt((vault as Record).name); const items = Array.isArray((vault as Record).items) ? ((vault as Record).items as any[]) : []; for (const item of items) { if (Number(item?.state) === 2) continue; const itemType = txt(item?.data?.type); const cipher = makeLoginCipher(); cipher.name = val(item?.data?.metadata?.name, '--'); cipher.notes = val(item?.data?.metadata?.note); cipher.favorite = !!item?.pinned; if (itemType === 'login') { const content = item?.data?.content || {}; const login = cipher.login as Record; const urls: string[] = []; for (const u of content?.urls || []) { const uri = normalizeUri(u || ''); if (uri) urls.push(uri); } login.uris = urls.length ? urls.map((uri) => ({ uri, match: null })) : null; const username = val(content?.itemUsername); const email = val(content?.itemEmail); login.username = username || email; if (username && email) processKvp(cipher, 'email', email, false); login.password = val(content?.password); login.totp = val(content?.totpUri); for (const extra of item?.data?.extraFields || []) { const t = txt(extra?.type); const fieldValue = t === 'totp' ? val(extra?.data?.totpUri) : val(extra?.data?.content); processKvp(cipher, txt(extra?.fieldName), fieldValue || '', t !== 'text'); } } else if (itemType === 'note') { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; } else if (itemType === 'creditCard') { const content = item?.data?.content || {}; const { month, year } = parseCardExpiry(txt(content?.expirationDate)); cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: val(content?.cardholderName), number: val(content?.number), brand: cardBrand(val(content?.number)), code: val(content?.verificationNumber), expMonth: month, expYear: year, }; if (txt(content?.pin)) processKvp(cipher, 'PIN', txt(content.pin), true); } else if (itemType === 'identity') { const content = item?.data?.content || {}; const name = splitFullName(val(content?.fullName)); cipher.type = 4; cipher.login = null; cipher.identity = { firstName: val(content?.firstName) || name.firstName, middleName: val(content?.middleName) || name.middleName, lastName: val(content?.lastName) || name.lastName, email: val(content?.email), phone: val(content?.phoneNumber), company: val(content?.company), ssn: val(content?.socialSecurityNumber), passportNumber: val(content?.passportNumber), licenseNumber: val(content?.licenseNumber), address1: val(content?.organization), address2: val(content?.streetAddress), address3: `${txt(content?.floor)} ${txt(content?.county)}`.trim() || null, city: val(content?.city), state: val(content?.stateOrProvince), postalCode: val(content?.zipOrPostalCode), country: val(content?.countryOrRegion), }; for (const key of Object.keys(content || {})) { if ( [ 'fullName', 'firstName', 'middleName', 'lastName', 'email', 'phoneNumber', 'company', 'socialSecurityNumber', 'passportNumber', 'licenseNumber', 'organization', 'streetAddress', 'floor', 'county', 'city', 'stateOrProvince', 'zipOrPostalCode', 'countryOrRegion', ].includes(key) ) { continue; } if (key === 'extraSections' && Array.isArray(content[key])) { for (const section of content[key]) { for (const extra of section?.sectionFields || []) { processKvp(cipher, txt(extra?.fieldName), txt(extra?.data?.content), txt(extra?.type) === 'hidden'); } } continue; } if (Array.isArray(content[key])) { for (const extra of content[key]) { processKvp(cipher, txt(extra?.fieldName), txt(extra?.data?.content), txt(extra?.type) === 'hidden'); } continue; } processKvp(cipher, key, txt(content[key]), false); } for (const extra of item?.data?.extraFields || []) { processKvp(cipher, txt(extra?.fieldName), txt(extra?.data?.content), txt(extra?.type) === 'hidden'); } } else { continue; } const idx = result.ciphers.push(cipher) - 1; if (vaultName) addFolder(result, vaultName, idx); } } return result; }