import type { CiphersImportPayload } from '@/lib/api/vault'; import { addFolder, addLoginUri, cardBrand, convertToNoteIfNeeded, extractTotpValue, isTotpFieldName, makeLoginCipher, normalizeUri, parseCardExpiry, parseCsv, parseEpochMaybe, processKvp, setLoginUris, txt, val, } from '@/lib/import-format-shared'; function onePasswordTypeHints(typeName: string): 1 | 2 | 3 | 4 { const t = txt(typeName).toLowerCase(); if (t.includes('creditcard') || t.includes('credit card')) return 3; if (t.includes('identity')) return 4; if (t.includes('securenote') || t.includes('secure note')) return 2; return 1; } function onePasswordCategoryType(categoryUuid: string): 1 | 2 | 3 | 4 | 5 { const c = txt(categoryUuid); if (['002', '101'].includes(c)) return 3; if (['004', '103', '104', '105', '106', '107', '108'].includes(c)) return 4; if (['003', '100', '111', '113'].includes(c)) return 2; if (c === '114') return 5; return 1; } function onePasswordCsvFieldLabel(property: string): string { return txt(property) .toLowerCase() .replace(/^.*?:\s*/, '') .replace(/\s+/g, ' ') .trim(); } function isOnePasswordUriField(property: string): boolean { const label = onePasswordCsvFieldLabel(property); return ['url', 'urls', 'website', 'web site'].includes(label); } function isOnePasswordUsernameField(property: string): boolean { return ['username', 'user name', 'email', 'e-mail', 'login'].includes(onePasswordCsvFieldLabel(property)); } function isOnePasswordPasswordField(property: string): boolean { return ['password', 'passphrase'].includes(onePasswordCsvFieldLabel(property)); } function readOnePasswordFieldValue(rawValue: unknown): { value: string; kind: string; raw: unknown } { if (!rawValue || typeof rawValue !== 'object' || Array.isArray(rawValue)) { return { value: txt(rawValue), kind: '', raw: rawValue }; } const obj = rawValue as Record; const keys = Object.keys(obj); const kind = keys.find((key) => obj[key] !== null && obj[key] !== undefined && txt(obj[key]) !== '') || keys[0] || ''; const raw = kind ? obj[kind] : undefined; if (kind === 'date') { const iso = parseEpochMaybe(raw); return { value: iso ? new Date(iso).toUTCString() : txt(raw), kind, raw }; } if (kind === 'monthYear') return { value: txt(raw), kind, raw }; if (kind === 'email' && raw && typeof raw === 'object') { return { value: txt((raw as Record).email_address), kind, raw }; } if (kind === 'address' || kind === 'sshKey') return { value: '', kind, raw }; return { value: txt(raw), kind, raw }; } export function parseOnePasswordCsv(textRaw: string, isMac: boolean): CiphersImportPayload { const rows = parseCsv(textRaw); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const ignored = new Set(['ainfo', 'autosubmit', 'notesplain', 'ps', 'scope', 'tags', 'title', 'uuid', 'notes', 'type']); for (const row of rows) { const title = txt(row.title || row.Title); if (!title) continue; const cipher = makeLoginCipher(); cipher.name = title || '--'; cipher.notes = `${txt(row.notesPlain)}\n${txt(row.notes)}`.trim() || null; let type: 1 | 2 | 3 | 4 = 1; if (isMac) { const t = txt(row.type).toLowerCase(); if (t === 'credit card') type = 3; else if (t === 'identity') type = 4; else if (t === 'secure note') type = 2; } else { const values = Object.keys(row).map((k) => `${k}:${txt(row[k])}`.toLowerCase()); const hasCard = values.some((x) => /number/i.test(x)) && values.some((x) => /expiry date/i.test(x)); const hasIdentity = values.some((x) => /first name|initial|last name|email/.test(x)); if (hasCard) type = 3; else if (hasIdentity) type = 4; } if (type === 2) { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; } else if (type === 3) { cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: null, number: null, brand: null, expMonth: null, expYear: null, code: null }; } else if (type === 4) { cipher.type = 4; cipher.login = null; cipher.identity = { firstName: null, middleName: null, lastName: null, username: null, email: null, phone: null, company: null, }; } let altUsername: string | null = null; for (const property of Object.keys(row)) { const rawVal = txt(row[property]); if (!rawVal) continue; const lower = property.toLowerCase(); if (Number(cipher.type) === 1) { const login = cipher.login as Record; if (!txt(login.username) && isOnePasswordUsernameField(lower)) { login.username = rawVal; continue; } if (!txt(login.password) && isOnePasswordPasswordField(lower)) { login.password = rawVal; continue; } if (isOnePasswordUriField(lower)) { addLoginUri(login, rawVal); continue; } if (!txt(login.totp) && isTotpFieldName(onePasswordCsvFieldLabel(lower))) { login.totp = rawVal; continue; } } else if (Number(cipher.type) === 3 && cipher.card) { const card = cipher.card as Record; if (!txt(card.number) && lower.includes('number')) { card.number = rawVal; card.brand = cardBrand(rawVal); continue; } if (!txt(card.code) && lower.includes('verification number')) { card.code = rawVal; continue; } if (!txt(card.cardholderName) && lower.includes('cardholder name')) { card.cardholderName = rawVal; continue; } if ((!txt(card.expMonth) || !txt(card.expYear)) && lower.includes('expiry date')) { const { month, year } = parseCardExpiry(rawVal); card.expMonth = month; card.expYear = year; continue; } } else if (Number(cipher.type) === 4 && cipher.identity) { const identity = cipher.identity as Record; if (!txt(identity.firstName) && lower.includes('first name')) { identity.firstName = rawVal; continue; } if (!txt(identity.middleName) && lower.includes('initial')) { identity.middleName = rawVal; continue; } if (!txt(identity.lastName) && lower.includes('last name')) { identity.lastName = rawVal; continue; } if (!txt(identity.username) && lower.includes('username')) { identity.username = rawVal; continue; } if (!txt(identity.email) && lower.includes('email')) { identity.email = rawVal; continue; } if (!txt(identity.phone) && lower.includes('default phone')) { identity.phone = rawVal; continue; } if (!txt(identity.company) && lower.includes('company')) { identity.company = rawVal; continue; } } if (!ignored.has(lower) && !isTotpFieldName(onePasswordCsvFieldLabel(lower)) && !lower.startsWith('section:') && !lower.startsWith('section ')) { if (!altUsername && lower === 'email') altUsername = rawVal; if (lower === 'created date' || lower === 'modified date') { const readable = parseEpochMaybe(rawVal); processKvp(cipher, `1Password ${property}`, readable || rawVal, false); } else { const hidden = lower.includes('password') || lower.includes('key') || lower.includes('secret'); processKvp(cipher, property, rawVal, hidden); } } } if (Number(cipher.type) === 1 && !txt((cipher.login as Record).username) && altUsername && !altUsername.includes('://')) { (cipher.login as Record).username = altUsername; } convertToNoteIfNeeded(cipher); result.ciphers.push(cipher); } return result; } function parseOnePasswordFieldsIntoCipher( cipher: Record, fields: any[], designationKey: string, valueKey: string, nameKey: string ): void { for (const field of fields || []) { const raw = field?.[valueKey]; if (raw === null || raw === undefined || txt(raw) === '') continue; const designation = txt(field?.[designationKey]).toLowerCase(); const k = txt(field?.k).toLowerCase(); const fieldName = txt(field?.[nameKey] ?? field?.t ?? field?.title) || 'no_name'; let value = txt(raw); if (k === 'date') { const asDate = parseEpochMaybe(raw); value = asDate ? new Date(asDate).toUTCString() : value; } if (Number(cipher.type) === 1) { const login = cipher.login as Record; if (!txt(login.username) && designation === 'username') { login.username = value; continue; } if (!txt(login.password) && designation === 'password') { login.password = value; continue; } if (!txt(login.totp) && (designation.startsWith('totp_') || isTotpFieldName(designation) || isTotpFieldName(fieldName))) { login.totp = extractTotpValue(raw) || value; continue; } } else if (Number(cipher.type) === 3 && cipher.card) { const card = cipher.card as Record; if (!txt(card.number) && designation === 'ccnum') { card.number = value; card.brand = cardBrand(value); continue; } if (!txt(card.code) && designation === 'cvv') { card.code = value; continue; } if (!txt(card.cardholderName) && designation === 'cardholder') { card.cardholderName = value; continue; } if ((!txt(card.expMonth) || !txt(card.expYear)) && designation === 'expiry') { const { month, year } = parseCardExpiry(value); card.expMonth = month; card.expYear = year; continue; } if (designation === 'type') continue; } else if (Number(cipher.type) === 4 && cipher.identity) { const identity = cipher.identity as Record; if (!txt(identity.firstName) && designation === 'firstname') { identity.firstName = value; continue; } if (!txt(identity.lastName) && designation === 'lastname') { identity.lastName = value; continue; } if (!txt(identity.middleName) && designation === 'initial') { identity.middleName = value; continue; } if (!txt(identity.phone) && designation === 'defphone') { identity.phone = value; continue; } if (!txt(identity.company) && designation === 'company') { identity.company = value; continue; } if (!txt(identity.email) && designation === 'email') { identity.email = value; continue; } if (!txt(identity.username) && designation === 'username') { identity.username = value; continue; } if (designation === 'address' && raw && typeof raw === 'object') { const addr = raw as Record; identity.address1 = val(addr.street); identity.city = val(addr.city); identity.country = txt(addr.country) ? txt(addr.country).toUpperCase() : null; identity.postalCode = val(addr.zip); identity.state = val(addr.state); continue; } } processKvp(cipher, fieldName, value, k === 'concealed'); } } function parseOnePasswordPasswordHistory(cipher: Record, history: any[]): void { const parsed = (history || []) .map((h) => ({ password: val(h?.value), lastUsedDate: parseEpochMaybe(h?.time) })) .filter((x) => !!x.password && !!x.lastUsedDate) .sort((a, b) => String(b.lastUsedDate).localeCompare(String(a.lastUsedDate))) .slice(0, 5); cipher.passwordHistory = parsed.length ? parsed : null; } export function parseOnePassword1Pif(textRaw: string): CiphersImportPayload { const lines = textRaw.split(/\r?\n/); const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || !trimmed.startsWith('{')) continue; let item: any; try { item = JSON.parse(trimmed); } catch { continue; } if (item?.trashed === true) continue; const cipher = makeLoginCipher(); cipher.name = val(item?.title || item?.overview?.title, '--'); cipher.favorite = !!item?.openContents?.faveIndex; let type = onePasswordTypeHints(item?.typeName); const details = item?.details || item?.secureContents || {}; if (details?.ccnum || details?.cvv) type = 3; if (details?.firstname || details?.address1) type = 4; if (type === 2) { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; } else if (type === 3) { cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: null, number: null, brand: null, expMonth: null, expYear: null, code: null }; } else if (type === 4) { cipher.type = 4; cipher.login = null; cipher.identity = { firstName: null, middleName: null, lastName: null, phone: null, email: null, username: null, company: null, }; } const uris: string[] = []; const locationUri = normalizeUri(item?.location || ''); if (locationUri) uris.push(locationUri); for (const u of item?.URLs || item?.secureContents?.URLs || item?.overview?.URLs || []) { const uri = normalizeUri(u?.url || u?.u || ''); if (uri) uris.push(uri); } if (Number(cipher.type) === 1) { setLoginUris(cipher.login as Record, uris); (cipher.login as Record).password = val(details?.password); } cipher.notes = val(details?.notesPlain); parseOnePasswordPasswordHistory(cipher, details?.passwordHistory || []); parseOnePasswordFieldsIntoCipher(cipher, details?.fields || [], 'designation', 'value', 'name'); for (const section of details?.sections || []) { parseOnePasswordFieldsIntoCipher(cipher, section?.fields || [], 'n', 'v', 't'); } convertToNoteIfNeeded(cipher); result.ciphers.push(cipher); } return result; } export function parseOnePassword1PuxJson(textRaw: string): CiphersImportPayload { const parsed = JSON.parse(textRaw) as { accounts?: any[] }; const result: CiphersImportPayload = { ciphers: [], folders: [], folderRelationships: [] }; const accounts = Array.isArray(parsed?.accounts) ? parsed.accounts : []; for (const account of accounts) { for (const vault of account?.vaults || []) { const vaultName = txt(vault?.attrs?.name); for (const item of vault?.items || []) { if (txt(item?.state) === 'archived') continue; const cipher = makeLoginCipher(); const categoryType = onePasswordCategoryType(item?.categoryUuid); if (categoryType === 2) { cipher.type = 2; cipher.login = null; cipher.secureNote = { type: 0 }; } else if (categoryType === 3) { cipher.type = 3; cipher.login = null; cipher.card = { cardholderName: null, number: null, brand: null, expMonth: null, expYear: null, code: null }; } else if (categoryType === 4) { cipher.type = 4; cipher.login = null; cipher.identity = { firstName: null, middleName: null, lastName: null, phone: null, email: null, username: null, company: null, address1: null, city: null, state: null, postalCode: null, country: null, passportNumber: null, ssn: null, licenseNumber: null, }; } else if (categoryType === 5) { cipher.type = 5; cipher.login = null; cipher.sshKey = { privateKey: null, publicKey: null, keyFingerprint: null, fingerprint: null, }; } cipher.favorite = Number(item?.favIndex) === 1; cipher.name = val(item?.overview?.title, '--'); cipher.notes = val(item?.details?.notesPlain); if (Number(cipher.type) === 1) { const urls: string[] = []; for (const u of item?.overview?.urls || []) { const uri = normalizeUri(u?.url || ''); if (uri) urls.push(uri); } if (!urls.length) { const fallbackUrl = normalizeUri(item?.overview?.url || ''); if (fallbackUrl) urls.push(fallbackUrl); } setLoginUris(cipher.login as Record, urls); if (txt(item?.categoryUuid) === '005' && !txt((cipher.login as Record).password)) { (cipher.login as Record).password = val(item?.details?.password); } } for (const loginField of item?.details?.loginFields || []) { const lv = txt(loginField?.value); if (!lv) continue; const designation = txt(loginField?.designation).toLowerCase(); const fieldName = txt(loginField?.name); const fieldLabel = onePasswordCsvFieldLabel(fieldName || designation); const fieldType = txt(loginField?.fieldType); if (Number(cipher.type) === 1) { const login = cipher.login as Record; if (designation === 'username' || isOnePasswordUsernameField(fieldLabel)) { login.username = lv; continue; } if (designation === 'password' || isOnePasswordPasswordField(fieldLabel)) { login.password = lv; continue; } if (designation.includes('totp') || isTotpFieldName(fieldName) || isTotpFieldName(fieldType)) { login.totp = lv; continue; } } processKvp(cipher, fieldName || designation || 'field', lv, fieldType === 'P'); } for (const section of item?.details?.sections || []) { const fieldTitle = txt(section?.title); for (const field of section?.fields || []) { const fieldId = txt(field?.id); const parsedField = readOnePasswordFieldValue(field?.value); const fieldType = parsedField.kind.toLowerCase(); const fieldTitleLocal = txt(field?.title) || fieldTitle; const fieldValueObj = parsedField.raw; const fieldValue = parsedField.value; if (!fieldValue && !(fieldValueObj && typeof fieldValueObj === 'object')) continue; if (Number(cipher.type) === 3 && cipher.card) { const card = cipher.card as Record; if (fieldId === 'creditCardNumber' || fieldType === 'creditcardnumber') { card.number = fieldValue; card.brand = cardBrand(fieldValue); continue; } if (fieldId === 'creditCardVerificationNumber') { card.code = fieldValue; continue; } if (fieldId === 'creditCardCardholder') { card.cardholderName = fieldValue; continue; } if (fieldId === 'creditCardExpiry') { const { month, year } = parseCardExpiry(fieldValue); card.expMonth = month; card.expYear = year; continue; } } else if (Number(cipher.type) === 4 && cipher.identity) { const identity = cipher.identity as Record; if (fieldId === 'firstName') { identity.firstName = fieldValue; continue; } if (fieldId === 'lastName') { identity.lastName = fieldValue; continue; } if (fieldId === 'initial') { identity.middleName = fieldValue; continue; } if (fieldId === 'company') { identity.company = fieldValue; continue; } if (fieldId === 'email') { identity.email = fieldValue; continue; } if (fieldId === 'phone') { identity.phone = fieldValue; continue; } if (fieldId === 'username') { identity.username = fieldValue; continue; } if (fieldId === 'address' && fieldValueObj && typeof fieldValueObj === 'object') { const addr = fieldValueObj as Record; identity.address1 = val(addr.street); identity.city = val(addr.city); identity.state = val(addr.state); identity.postalCode = val(addr.zip); identity.country = txt(addr.country) ? txt(addr.country).toUpperCase() : null; continue; } if (fieldId === 'socialSecurityNumber') { identity.ssn = fieldValue; continue; } if (fieldId === 'passportNumber') { identity.passportNumber = fieldValue; continue; } if (fieldId === 'licenseNumber') { identity.licenseNumber = fieldValue; continue; } } else if (Number(cipher.type) === 1) { const login = cipher.login as Record; if (fieldId === 'url' || fieldType === 'url') { addLoginUri(login, fieldValue); continue; } if ((fieldId === 'username' || onePasswordCsvFieldLabel(fieldTitleLocal) === 'username') && !txt(login.username)) { login.username = fieldValue; continue; } if ((fieldId === 'password' || onePasswordCsvFieldLabel(fieldTitleLocal) === 'password') && !txt(login.password)) { login.password = fieldValue; continue; } if (txt(item?.categoryUuid) === '112' && onePasswordCsvFieldLabel(fieldTitleLocal) === 'credential' && !txt(login.password)) { login.password = fieldValue; continue; } if (txt(item?.categoryUuid) === '112' && onePasswordCsvFieldLabel(fieldTitleLocal) === 'hostname') { addLoginUri(login, fieldValue); continue; } if ( (fieldId === 'oneTimePassword' || fieldId === 'totp' || fieldId.startsWith('TOTP_') || fieldType === 'totp' || fieldType === 'otp' || isTotpFieldName(fieldTitleLocal)) && !txt(login.totp) ) { login.totp = extractTotpValue(fieldValueObj) || fieldValue; continue; } } else if (Number(cipher.type) === 5 && cipher.sshKey && fieldType === 'sshkey' && fieldValueObj && typeof fieldValueObj === 'object') { const ssh = fieldValueObj as Record; const metadata = ssh.metadata && typeof ssh.metadata === 'object' ? ssh.metadata : {}; cipher.sshKey = { privateKey: val(metadata.privateKey ?? ssh.privateKey), publicKey: val(metadata.publicKey), keyFingerprint: val(metadata.fingerprint), fingerprint: val(metadata.fingerprint), }; continue; } const hidden = fieldType === 'concealed' || fieldType === 'totp' || fieldType === 'otp'; processKvp(cipher, fieldTitleLocal || fieldId || 'field', fieldValue, hidden); } } parseOnePasswordPasswordHistory(cipher, item?.details?.passwordHistory || []); convertToNoteIfNeeded(cipher); const idx = result.ciphers.push(cipher) - 1; const tagFolder = Array.isArray(item?.overview?.tags) ? txt(item.overview.tags[0]) : ''; if (tagFolder) addFolder(result, tagFolder, idx); else if (vaultName) addFolder(result, vaultName, idx); } } } return result; }