diff --git a/src/handlers/ciphers.ts b/src/handlers/ciphers.ts index 84e3f97..4a01420 100644 --- a/src/handlers/ciphers.ts +++ b/src/handlers/ciphers.ts @@ -7,6 +7,9 @@ import { CipherResponse, CipherSecureNote, CipherSshKey, + CipherBankAccount, + CipherDriversLicense, + CipherPassport, Attachment, PasswordHistory, } from '../types'; @@ -254,6 +257,49 @@ function sanitizeEncryptedObject>( return next as T; } +const BANK_ACCOUNT_ENCRYPTED_KEYS = [ + 'bankName', + 'nameOnAccount', + 'accountType', + 'accountNumber', + 'routingNumber', + 'branchNumber', + 'pin', + 'swiftCode', + 'iban', + 'bankContactPhone', +] as const; + +const DRIVERS_LICENSE_ENCRYPTED_KEYS = [ + 'firstName', + 'middleName', + 'lastName', + 'dateOfBirth', + 'licenseNumber', + 'issuingCountry', + 'issuingState', + 'issueDate', + 'expirationDate', + 'issuingAuthority', + 'licenseClass', +] as const; + +const PASSPORT_ENCRYPTED_KEYS = [ + 'surname', + 'givenName', + 'dateOfBirth', + 'sex', + 'birthPlace', + 'nationality', + 'issuingCountry', + 'passportNumber', + 'passportType', + 'nationalIdentificationNumber', + 'issuingAuthority', + 'issueDate', + 'expirationDate', +] as const; + function normalizeCipherForStorage(cipher: Cipher): Cipher { cipher.login = normalizeCipherLoginForStorage(cipher.login); cipher.sshKey = normalizeCipherSshKeyForCompatibility(cipher.sshKey); @@ -376,6 +422,20 @@ export function validateCipherEncryptedFieldsForCompatibility(cipher: Cipher): s if (fingerprint != null && !isValidEncString(fingerprint)) return 'SSH key fingerprint must be an encrypted string.'; } + const typedEncryptedObjects: Array<[string, any, readonly string[]]> = [ + ['Bank account', (cipher as any).bankAccount, BANK_ACCOUNT_ENCRYPTED_KEYS], + ['Drivers license', (cipher as any).driversLicense, DRIVERS_LICENSE_ENCRYPTED_KEYS], + ['Passport', (cipher as any).passport, PASSPORT_ENCRYPTED_KEYS], + ]; + for (const [label, source, keys] of typedEncryptedObjects) { + if (!source || typeof source !== 'object') continue; + for (const key of keys) { + if (source[key] != null && !optionalEncStringWithin(source[key], 10000)) { + return `${label} ${key} must be an encrypted string.`; + } + } + } + // Validate password history — each password must be an encrypted string. if (Array.isArray(cipher.passwordHistory)) { for (const entry of cipher.passwordHistory) { @@ -752,7 +812,20 @@ export function cipherToResponse( 'licenseNumber', ]); const normalizedSshKey = normalizeCipherSshKeyForCompatibility((passthrough as any).sshKey ?? null); - const normalizedSecureNote = Number(cipher.type) === 2 + const normalizedBankAccount = sanitizeEncryptedObject( + (passthrough as any).bankAccount ?? null, + BANK_ACCOUNT_ENCRYPTED_KEYS + ); + const normalizedDriversLicense = sanitizeEncryptedObject( + (passthrough as any).driversLicense ?? null, + DRIVERS_LICENSE_ENCRYPTED_KEYS + ); + const normalizedPassport = sanitizeEncryptedObject( + (passthrough as any).passport ?? null, + PASSPORT_ENCRYPTED_KEYS + ); + const responseType = Number(cipher.type) || 1; + const normalizedSecureNote = responseType === 2 ? normalizeCipherSecureNoteForCompatibility((passthrough as any).secureNote ?? null) ?? { type: 0 } : null; const responseAttachments = applyCipherEmbeddedAttachmentMetadata(cipher, attachments); @@ -763,7 +836,7 @@ export function cipherToResponse( ...passthrough, // Server-computed / enforced fields (always override) folderId: normalizeResponseFolderId(cipher.folderId, options.validFolderIds), - type: Number(cipher.type) || 1, + type: responseType, organizationId: normalizeOptionalId((passthrough as any).organizationId ?? null), organizationUseTotp: !!((passthrough as any).organizationUseTotp ?? false), creationDate: createdAt, @@ -785,6 +858,9 @@ export function cipherToResponse( fields: normalizeCipherFieldsForCompatibility((passthrough as any).fields), passwordHistory: normalizePasswordHistoryForCompatibility((passthrough as any).passwordHistory), sshKey: normalizedSshKey, + bankAccount: responseType === 6 ? normalizedBankAccount : null, + driversLicense: responseType === 7 ? normalizedDriversLicense : null, + passport: responseType === 8 ? normalizedPassport : null, key: responseCipherKey, data: typeof (passthrough as any).data === 'string' ? (passthrough as any).data : null, encryptedFor: (passthrough as any).encryptedFor ?? null, @@ -880,6 +956,9 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str const createIdentity = readCipherProp(cipherData, ['identity', 'Identity']); const createSecureNote = readCipherProp(cipherData, ['secureNote', 'SecureNote']); const createSshKey = readCipherProp(cipherData, ['sshKey', 'SshKey']); + const createBankAccount = readCipherProp(cipherData, ['bankAccount', 'BankAccount']); + const createDriversLicense = readCipherProp(cipherData, ['driversLicense', 'DriversLicense']); + const createPassport = readCipherProp(cipherData, ['passport', 'Passport']); const createPasswordHistory = readCipherProp(cipherData, ['passwordHistory', 'PasswordHistory']); if (createKey.present && !shouldAcceptCipherKey(createKey.value)) { @@ -909,6 +988,9 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str cipher.identity = createIdentity.present ? (createIdentity.value ?? null) : (cipher.identity ?? null); cipher.secureNote = createSecureNote.present ? (createSecureNote.value ?? null) : (cipher.secureNote ?? null); cipher.sshKey = createSshKey.present ? (createSshKey.value ?? null) : (cipher.sshKey ?? null); + cipher.bankAccount = createBankAccount.present ? (createBankAccount.value ?? null) : ((cipher as any).bankAccount ?? null); + cipher.driversLicense = createDriversLicense.present ? (createDriversLicense.value ?? null) : ((cipher as any).driversLicense ?? null); + cipher.passport = createPassport.present ? (createPassport.value ?? null) : ((cipher as any).passport ?? null); cipher.passwordHistory = createPasswordHistory.present ? (createPasswordHistory.value ?? null) : (cipher.passwordHistory ?? null); const createFields = getAliasedProp(cipherData, ['fields', 'Fields']); cipher.fields = createFields.present ? (createFields.value ?? null) : (cipher.fields ?? null); @@ -960,6 +1042,9 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str const incomingIdentity = readCipherProp(cipherData, ['identity', 'Identity']); const incomingSecureNote = readCipherProp(cipherData, ['secureNote', 'SecureNote']); const incomingSshKey = readCipherProp(cipherData, ['sshKey', 'SshKey']); + const incomingBankAccount = readCipherProp(cipherData, ['bankAccount', 'BankAccount']); + const incomingDriversLicense = readCipherProp(cipherData, ['driversLicense', 'DriversLicense']); + const incomingPassport = readCipherProp(cipherData, ['passport', 'Passport']); const incomingPasswordHistory = readCipherProp(cipherData, ['passwordHistory', 'PasswordHistory']); const incomingRevisionDate = readCipherRevisionDate(cipherData); const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData); @@ -1008,6 +1093,9 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str cipher.card = nextType === 3 ? (incomingCard.present ? (incomingCard.value ?? null) : (existingCipher.card ?? null)) : null; cipher.identity = nextType === 4 ? (incomingIdentity.present ? (incomingIdentity.value ?? null) : (existingCipher.identity ?? null)) : null; cipher.sshKey = nextType === 5 ? (incomingSshKey.present ? (incomingSshKey.value ?? null) : (existingCipher.sshKey ?? null)) : null; + cipher.bankAccount = nextType === 6 ? (incomingBankAccount.present ? (incomingBankAccount.value ?? null) : ((existingCipher as any).bankAccount ?? null)) : null; + cipher.driversLicense = nextType === 7 ? (incomingDriversLicense.present ? (incomingDriversLicense.value ?? null) : ((existingCipher as any).driversLicense ?? null)) : null; + cipher.passport = nextType === 8 ? (incomingPassport.present ? (incomingPassport.value ?? null) : ((existingCipher as any).passport ?? null)) : null; if (incomingPasswordHistory.present) { cipher.passwordHistory = incomingPasswordHistory.value ?? null; } diff --git a/src/handlers/import.ts b/src/handlers/import.ts index feb0b73..5c36514 100644 --- a/src/handlers/import.ts +++ b/src/handlers/import.ts @@ -17,6 +17,9 @@ interface CiphersImportRequest { favorite?: boolean; reprompt?: number; sshKey?: any | null; + bankAccount?: any | null; + driversLicense?: any | null; + passport?: any | null; key?: string | null; login?: { uris?: Array<{ uri: string | null; uriChecksum?: string | null; match?: number | null }> | null; @@ -185,6 +188,10 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st const card = readAliasedImportProp(c, ['card', 'Card']); const identity = readAliasedImportProp(c, ['identity', 'Identity']); const secureNote = readAliasedImportProp(c, ['secureNote', 'SecureNote']); + const sshKey = readAliasedImportProp(c, ['sshKey', 'SshKey']); + const bankAccount = readAliasedImportProp(c, ['bankAccount', 'BankAccount']); + const driversLicense = readAliasedImportProp(c, ['driversLicense', 'DriversLicense']); + const passport = readAliasedImportProp(c, ['passport', 'Passport']); const fields = readAliasedImportProp(c, ['fields', 'Fields']); const passwordHistory = readAliasedImportProp(c, ['passwordHistory', 'PasswordHistory']); const key = readAliasedImportProp(c, ['key', 'Key']); @@ -254,7 +261,10 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st })) || null, passwordHistory: passwordHistory ?? null, reprompt: c.reprompt ?? 0, - sshKey: normalizeCipherSshKeyForCompatibility((c as any).sshKey ?? null), + sshKey: normalizeCipherSshKeyForCompatibility(sshKey ?? null), + bankAccount: bankAccount ?? null, + driversLicense: driversLicense ?? null, + passport: passport ?? null, key: key ?? null, createdAt: now, updatedAt: now, diff --git a/src/types/index.ts b/src/types/index.ts index e6d82bf..7865144 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -124,6 +124,10 @@ export enum CipherType { SecureNote = 2, Card = 3, Identity = 4, + SSHKey = 5, + BankAccount = 6, + DriversLicense = 7, + Passport = 8, } export interface CipherLoginUri { @@ -158,6 +162,52 @@ export interface CipherSshKey { keyFingerprint: string; } +export interface CipherBankAccount { + bankName: string | null; + nameOnAccount: string | null; + accountType: string | null; + accountNumber: string | null; + routingNumber: string | null; + branchNumber: string | null; + pin: string | null; + swiftCode: string | null; + iban: string | null; + bankContactPhone: string | null; + [key: string]: any; +} + +export interface CipherDriversLicense { + firstName: string | null; + middleName: string | null; + lastName: string | null; + dateOfBirth: string | null; + licenseNumber: string | null; + issuingCountry: string | null; + issuingState: string | null; + issueDate: string | null; + expirationDate: string | null; + issuingAuthority: string | null; + licenseClass: string | null; + [key: string]: any; +} + +export interface CipherPassport { + surname: string | null; + givenName: string | null; + dateOfBirth: string | null; + sex: string | null; + birthPlace: string | null; + nationality: string | null; + issuingCountry: string | null; + passportNumber: string | null; + passportType: string | null; + nationalIdentificationNumber: string | null; + issuingAuthority: string | null; + issueDate: string | null; + expirationDate: string | null; + [key: string]: any; +} + export interface CipherIdentity { title: string | null; firstName: string | null; @@ -208,6 +258,9 @@ export interface Cipher { identity: CipherIdentity | null; secureNote: CipherSecureNote | null; sshKey: CipherSshKey | null; + bankAccount?: CipherBankAccount | null; + driversLicense?: CipherDriversLicense | null; + passport?: CipherPassport | null; fields: CipherField[] | null; passwordHistory: PasswordHistory[] | null; reprompt: number; @@ -547,6 +600,9 @@ export interface CipherResponse { identity: CipherIdentity | null; secureNote: CipherSecureNote | null; sshKey: CipherSshKey | null; + bankAccount: CipherBankAccount | null; + driversLicense: CipherDriversLicense | null; + passport: CipherPassport | null; fields: CipherField[] | null; passwordHistory: PasswordHistory[] | null; reprompt: number; diff --git a/webapp/src/components/VaultPage.tsx b/webapp/src/components/VaultPage.tsx index 9615a0f..a00d07b 100644 --- a/webapp/src/components/VaultPage.tsx +++ b/webapp/src/components/VaultPage.tsx @@ -12,17 +12,20 @@ import { cardListSubtitle, FOLDER_SORT_STORAGE_KEY, VAULT_SORT_STORAGE_KEY, + bankAccountListSubtitle, cipherTypeKey, cipherTypeLabel, createEmptyDraft, creationTimeValue, draftFromCipher, + driversLicenseListSubtitle, buildCipherDuplicateSignatures, firstCipherUri, firstPasskeyCreationTime, isCipherVisibleInArchive, isCipherVisibleInNormalVault, isCipherVisibleInTrash, + passportListSubtitle, sortTimeValue, type DuplicateDetectionMode, type SidebarFilter, @@ -308,10 +311,21 @@ export default function VaultPage(props: VaultPageProps) { const name = String(cipher.decName || cipher.name || ''); const username = String(cipher.login?.decUsername || ''); const uri = firstCipherUri(cipher); + const typedText = [ + cipher.bankAccount?.decBankName, + cipher.bankAccount?.decNameOnAccount, + cipher.bankAccount?.decAccountNumber, + cipher.driversLicense?.decLicenseNumber, + cipher.driversLicense?.decFirstName, + cipher.driversLicense?.decLastName, + cipher.passport?.decPassportNumber, + cipher.passport?.decGivenName, + cipher.passport?.decSurname, + ].filter(Boolean).join('\n'); const cipherId = String(cipher.id || '').trim(); meta.set(cipher.id, { name, - searchText: `${cipherId}\n${cipherId.replace(/-/g, '')}\n${name}\n${username}\n${uri}`.toLowerCase(), + searchText: `${cipherId}\n${cipherId.replace(/-/g, '')}\n${name}\n${username}\n${uri}\n${typedText}`.toLowerCase(), firstUri: uri, typeKey: cipherTypeKey(Number(cipher.type || 1)), sortTime: sortTimeValue(cipher), @@ -542,6 +556,9 @@ const folderName = useCallback((id: string | null | undefined): string => { if (Number(cipher.type || 1) === 3) { return cardListSubtitle(cipher); } + if (Number(cipher.type || 1) === 6) return bankAccountListSubtitle(cipher); + if (Number(cipher.type || 1) === 7) return driversLicenseListSubtitle(cipher); + if (Number(cipher.type || 1) === 8) return passportListSubtitle(cipher); return cipherTypeLabel(Number(cipher.type || 1)); }, [cipherMetaById]); diff --git a/webapp/src/components/vault/VaultDetailView.tsx b/webapp/src/components/vault/VaultDetailView.tsx index 2a21a3a..c138f17 100644 --- a/webapp/src/components/vault/VaultDetailView.tsx +++ b/webapp/src/components/vault/VaultDetailView.tsx @@ -327,6 +327,55 @@ export default function VaultDetailView(props: VaultDetailViewProps) { )} + {props.selectedCipher.bankAccount && ( +
+

{t('txt_bank_account_details')}

+
{t('txt_bank_name')}{props.selectedCipher.bankAccount.decBankName || ''}
+
{t('txt_name_on_account')}{props.selectedCipher.bankAccount.decNameOnAccount || ''}
+
{t('txt_account_type')}{props.selectedCipher.bankAccount.decAccountType || ''}
+
{t('txt_account_number')}{props.selectedCipher.bankAccount.decAccountNumber || ''}
+
{t('txt_routing_number')}{props.selectedCipher.bankAccount.decRoutingNumber || ''}
+
{t('txt_branch_number')}{props.selectedCipher.bankAccount.decBranchNumber || ''}
+
{t('txt_pin')}{props.selectedCipher.bankAccount.decPin || ''}
+
{t('txt_swift_code')}{props.selectedCipher.bankAccount.decSwiftCode || ''}
+
{t('txt_iban')}{props.selectedCipher.bankAccount.decIban || ''}
+
{t('txt_bank_contact_phone')}{props.selectedCipher.bankAccount.decBankContactPhone || ''}
+
+ )} + + {props.selectedCipher.driversLicense && ( +
+

{t('txt_drivers_license_details')}

+
{t('txt_name')}{[props.selectedCipher.driversLicense.decFirstName, props.selectedCipher.driversLicense.decMiddleName, props.selectedCipher.driversLicense.decLastName].filter(Boolean).join(' ')}
+
{t('txt_date_of_birth')}{props.selectedCipher.driversLicense.decDateOfBirth || ''}
+
{t('txt_license_number')}{props.selectedCipher.driversLicense.decLicenseNumber || ''}
+
{t('txt_issuing_country')}{props.selectedCipher.driversLicense.decIssuingCountry || ''}
+
{t('txt_issuing_state')}{props.selectedCipher.driversLicense.decIssuingState || ''}
+
{t('txt_issue_date')}{props.selectedCipher.driversLicense.decIssueDate || ''}
+
{t('txt_expiration_date')}{props.selectedCipher.driversLicense.decExpirationDate || ''}
+
{t('txt_issuing_authority')}{props.selectedCipher.driversLicense.decIssuingAuthority || ''}
+
{t('txt_license_class')}{props.selectedCipher.driversLicense.decLicenseClass || ''}
+
+ )} + + {props.selectedCipher.passport && ( +
+

{t('txt_passport_details')}

+
{t('txt_name')}{[props.selectedCipher.passport.decGivenName, props.selectedCipher.passport.decSurname].filter(Boolean).join(' ')}
+
{t('txt_date_of_birth')}{props.selectedCipher.passport.decDateOfBirth || ''}
+
{t('txt_sex')}{props.selectedCipher.passport.decSex || ''}
+
{t('txt_birth_place')}{props.selectedCipher.passport.decBirthPlace || ''}
+
{t('txt_nationality')}{props.selectedCipher.passport.decNationality || ''}
+
{t('txt_issuing_country')}{props.selectedCipher.passport.decIssuingCountry || ''}
+
{t('txt_passport_number')}{props.selectedCipher.passport.decPassportNumber || ''}
+
{t('txt_passport_type')}{props.selectedCipher.passport.decPassportType || ''}
+
{t('txt_national_id_number')}{props.selectedCipher.passport.decNationalIdentificationNumber || ''}
+
{t('txt_issuing_authority')}{props.selectedCipher.passport.decIssuingAuthority || ''}
+
{t('txt_issue_date')}{props.selectedCipher.passport.decIssueDate || ''}
+
{t('txt_expiration_date')}{props.selectedCipher.passport.decExpirationDate || ''}
+
+ )} + {!!(props.selectedCipher.decNotes || '').trim() && (

{t('txt_notes')}

diff --git a/webapp/src/components/vault/VaultEditor.tsx b/webapp/src/components/vault/VaultEditor.tsx index 9cfb00b..7bc6d1f 100644 --- a/webapp/src/components/vault/VaultEditor.tsx +++ b/webapp/src/components/vault/VaultEditor.tsx @@ -589,6 +589,64 @@ export default function VaultEditor(props: VaultEditorProps) {
)} + {props.draft.type === 6 && ( +
+

{t('txt_bank_account_details')}

+
+ + + + + + + + + + +
+
+ )} + + {props.draft.type === 7 && ( +
+

{t('txt_drivers_license_details')}

+
+ + + + + + + + + + + +
+
+ )} + + {props.draft.type === 8 && ( +
+

{t('txt_passport_details')}

+
+ + + + + + + + + + + + + +
+
+ )} +

{t('txt_attachments')}

diff --git a/webapp/src/components/vault/VaultSidebar.tsx b/webapp/src/components/vault/VaultSidebar.tsx index 06e19b7..59c5d63 100644 --- a/webapp/src/components/vault/VaultSidebar.tsx +++ b/webapp/src/components/vault/VaultSidebar.tsx @@ -117,9 +117,18 @@ export default function VaultSidebar(props: VaultSidebarProps) { + + + diff --git a/webapp/src/components/vault/vault-page-helpers.tsx b/webapp/src/components/vault/vault-page-helpers.tsx index 42e9151..336d16d 100644 --- a/webapp/src/components/vault/vault-page-helpers.tsx +++ b/webapp/src/components/vault/vault-page-helpers.tsx @@ -14,7 +14,7 @@ import { firstCipherUri, hostFromUri, websiteIconUrl } from '@/lib/website-utils import { normalizeEquivalentDomain } from '@shared/domain-normalize'; import WebsiteIcon from './WebsiteIcon'; -export type TypeFilter = 'login' | 'card' | 'identity' | 'note' | 'ssh'; +export type TypeFilter = 'login' | 'card' | 'identity' | 'note' | 'ssh' | 'bank' | 'license' | 'passport'; export type VaultSortMode = 'edited' | 'created' | 'name'; export type DuplicateDetectionMode = 'exact' | 'login-site' | 'login-credentials' | 'password'; export type SidebarFilter = @@ -98,6 +98,32 @@ export function cardListSubtitle(cipher: Cipher): string { return cipherTypeLabel(3); } +export function bankAccountListSubtitle(cipher: Cipher): string { + const bankName = valueOrFallback(cipher.bankAccount?.decBankName ?? cipher.bankAccount?.bankName).trim(); + const accountType = valueOrFallback(cipher.bankAccount?.decAccountType ?? cipher.bankAccount?.accountType).trim(); + const accountNumber = valueOrFallback(cipher.bankAccount?.decAccountNumber ?? cipher.bankAccount?.accountNumber).replace(/\D/g, ''); + const last4 = accountNumber.length >= 4 ? accountNumber.slice(-4) : ''; + return [bankName, accountType, last4 ? `*${last4}` : ''].filter(Boolean).join(', ') || cipherTypeLabel(6); +} + +export function driversLicenseListSubtitle(cipher: Cipher): string { + const licenseNumber = valueOrFallback(cipher.driversLicense?.decLicenseNumber ?? cipher.driversLicense?.licenseNumber).trim(); + const name = [ + valueOrFallback(cipher.driversLicense?.decFirstName ?? cipher.driversLicense?.firstName).trim(), + valueOrFallback(cipher.driversLicense?.decLastName ?? cipher.driversLicense?.lastName).trim(), + ].filter(Boolean).join(' '); + return licenseNumber || name || cipherTypeLabel(7); +} + +export function passportListSubtitle(cipher: Cipher): string { + const passportNumber = valueOrFallback(cipher.passport?.decPassportNumber ?? cipher.passport?.passportNumber).trim(); + const name = [ + valueOrFallback(cipher.passport?.decGivenName ?? cipher.passport?.givenName).trim(), + valueOrFallback(cipher.passport?.decSurname ?? cipher.passport?.surname).trim(), + ].filter(Boolean).join(' '); + return passportNumber || name || cipherTypeLabel(8); +} + export function CardBrandIcon({ brand }: { brand?: string | null }) { const display = displayCardBrand(brand); const key = display.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'generic'; @@ -118,7 +144,10 @@ export function getCreateTypeOptions(): TypeOption[] { return [ { type: 1, label: t('txt_login') }, { type: 3, label: t('txt_card') }, + { type: 6, label: t('txt_bank_account') }, { type: 4, label: t('txt_identity') }, + { type: 7, label: t('txt_drivers_license') }, + { type: 8, label: t('txt_passport') }, { type: 2, label: t('txt_note') }, { type: 5, label: t('txt_ssh_key') }, ]; @@ -185,6 +214,9 @@ export function CreateTypeIcon({ type }: { type: number }) { if (type === 4) return ; if (type === 2) return ; if (type === 5) return ; + if (type === 6) return ; + if (type === 7) return ; + if (type === 8) return ; return ; } @@ -193,7 +225,11 @@ export function cipherTypeKey(type: number): TypeFilter { if (type === 3) return 'card'; if (type === 4) return 'identity'; if (type === 2) return 'note'; - return 'ssh'; + if (type === 5) return 'ssh'; + if (type === 6) return 'bank'; + if (type === 7) return 'license'; + if (type === 8) return 'passport'; + return 'note'; } function cipherDeletedValue(cipher: Cipher): boolean { @@ -230,6 +266,9 @@ export function cipherTypeLabel(type: number): string { if (type === 4) return t('txt_identity'); if (type === 2) return t('txt_secure_note'); if (type === 5) return t('txt_ssh_key'); + if (type === 6) return t('txt_bank_account'); + if (type === 7) return t('txt_drivers_license'); + if (type === 8) return t('txt_passport'); return t('txt_item'); } @@ -239,6 +278,9 @@ export function TypeIcon({ type }: { type: number }) { if (type === 4) return ; if (type === 2) return ; if (type === 5) return ; + if (type === 6) return ; + if (type === 7) return ; + if (type === 8) return ; return ; } @@ -355,6 +397,52 @@ export function buildCipherDuplicateSignature(cipher: Cipher): string { fingerprint: valueOrFallback(cipher.sshKey.decFingerprint ?? cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint), } : null, + bankAccount: cipher.bankAccount + ? { + bankName: valueOrFallback(cipher.bankAccount.decBankName ?? cipher.bankAccount.bankName), + nameOnAccount: valueOrFallback(cipher.bankAccount.decNameOnAccount ?? cipher.bankAccount.nameOnAccount), + accountType: valueOrFallback(cipher.bankAccount.decAccountType ?? cipher.bankAccount.accountType), + accountNumber: valueOrFallback(cipher.bankAccount.decAccountNumber ?? cipher.bankAccount.accountNumber), + routingNumber: valueOrFallback(cipher.bankAccount.decRoutingNumber ?? cipher.bankAccount.routingNumber), + branchNumber: valueOrFallback(cipher.bankAccount.decBranchNumber ?? cipher.bankAccount.branchNumber), + pin: valueOrFallback(cipher.bankAccount.decPin ?? cipher.bankAccount.pin), + swiftCode: valueOrFallback(cipher.bankAccount.decSwiftCode ?? cipher.bankAccount.swiftCode), + iban: valueOrFallback(cipher.bankAccount.decIban ?? cipher.bankAccount.iban), + bankContactPhone: valueOrFallback(cipher.bankAccount.decBankContactPhone ?? cipher.bankAccount.bankContactPhone), + } + : null, + driversLicense: cipher.driversLicense + ? { + firstName: valueOrFallback(cipher.driversLicense.decFirstName ?? cipher.driversLicense.firstName), + middleName: valueOrFallback(cipher.driversLicense.decMiddleName ?? cipher.driversLicense.middleName), + lastName: valueOrFallback(cipher.driversLicense.decLastName ?? cipher.driversLicense.lastName), + dateOfBirth: valueOrFallback(cipher.driversLicense.decDateOfBirth ?? cipher.driversLicense.dateOfBirth), + licenseNumber: valueOrFallback(cipher.driversLicense.decLicenseNumber ?? cipher.driversLicense.licenseNumber), + issuingCountry: valueOrFallback(cipher.driversLicense.decIssuingCountry ?? cipher.driversLicense.issuingCountry), + issuingState: valueOrFallback(cipher.driversLicense.decIssuingState ?? cipher.driversLicense.issuingState), + issueDate: valueOrFallback(cipher.driversLicense.decIssueDate ?? cipher.driversLicense.issueDate), + expirationDate: valueOrFallback(cipher.driversLicense.decExpirationDate ?? cipher.driversLicense.expirationDate), + issuingAuthority: valueOrFallback(cipher.driversLicense.decIssuingAuthority ?? cipher.driversLicense.issuingAuthority), + licenseClass: valueOrFallback(cipher.driversLicense.decLicenseClass ?? cipher.driversLicense.licenseClass), + } + : null, + passport: cipher.passport + ? { + surname: valueOrFallback(cipher.passport.decSurname ?? cipher.passport.surname), + givenName: valueOrFallback(cipher.passport.decGivenName ?? cipher.passport.givenName), + dateOfBirth: valueOrFallback(cipher.passport.decDateOfBirth ?? cipher.passport.dateOfBirth), + sex: valueOrFallback(cipher.passport.decSex ?? cipher.passport.sex), + birthPlace: valueOrFallback(cipher.passport.decBirthPlace ?? cipher.passport.birthPlace), + nationality: valueOrFallback(cipher.passport.decNationality ?? cipher.passport.nationality), + issuingCountry: valueOrFallback(cipher.passport.decIssuingCountry ?? cipher.passport.issuingCountry), + passportNumber: valueOrFallback(cipher.passport.decPassportNumber ?? cipher.passport.passportNumber), + passportType: valueOrFallback(cipher.passport.decPassportType ?? cipher.passport.passportType), + nationalIdentificationNumber: valueOrFallback(cipher.passport.decNationalIdentificationNumber ?? cipher.passport.nationalIdentificationNumber), + issuingAuthority: valueOrFallback(cipher.passport.decIssuingAuthority ?? cipher.passport.issuingAuthority), + issueDate: valueOrFallback(cipher.passport.decIssueDate ?? cipher.passport.issueDate), + expirationDate: valueOrFallback(cipher.passport.decExpirationDate ?? cipher.passport.expirationDate), + } + : null, secureNoteType: cipher.secureNote?.type ?? null, fields: (cipher.fields || []).map((field) => ({ type: field.type ?? null, @@ -427,6 +515,40 @@ export function createEmptyDraft(type: number): VaultDraft { sshPrivateKey: '', sshPublicKey: '', sshFingerprint: '', + bankName: '', + bankNameOnAccount: '', + bankAccountType: '', + bankAccountNumber: '', + bankRoutingNumber: '', + bankBranchNumber: '', + bankPin: '', + bankSwiftCode: '', + bankIban: '', + bankContactPhone: '', + licenseFirstName: '', + licenseMiddleName: '', + licenseLastName: '', + licenseDateOfBirth: '', + licenseNumber: '', + licenseIssuingCountry: '', + licenseIssuingState: '', + licenseIssueDate: '', + licenseExpirationDate: '', + licenseIssuingAuthority: '', + licenseClass: '', + passportSurname: '', + passportGivenName: '', + passportDateOfBirth: '', + passportSex: '', + passportBirthPlace: '', + passportNationality: '', + passportIssuingCountry: '', + passportNumber: '', + passportType: '', + passportNationalIdentificationNumber: '', + passportIssuingAuthority: '', + passportIssueDate: '', + passportExpirationDate: '', customFields: [], }; } @@ -490,6 +612,46 @@ export function draftFromCipher(cipher: Cipher): VaultDraft { draft.sshPublicKey = cipher.sshKey.decPublicKey || ''; draft.sshFingerprint = cipher.sshKey.decFingerprint || ''; } + if (cipher.bankAccount) { + draft.bankName = cipher.bankAccount.decBankName || ''; + draft.bankNameOnAccount = cipher.bankAccount.decNameOnAccount || ''; + draft.bankAccountType = cipher.bankAccount.decAccountType || ''; + draft.bankAccountNumber = cipher.bankAccount.decAccountNumber || ''; + draft.bankRoutingNumber = cipher.bankAccount.decRoutingNumber || ''; + draft.bankBranchNumber = cipher.bankAccount.decBranchNumber || ''; + draft.bankPin = cipher.bankAccount.decPin || ''; + draft.bankSwiftCode = cipher.bankAccount.decSwiftCode || ''; + draft.bankIban = cipher.bankAccount.decIban || ''; + draft.bankContactPhone = cipher.bankAccount.decBankContactPhone || ''; + } + if (cipher.driversLicense) { + draft.licenseFirstName = cipher.driversLicense.decFirstName || ''; + draft.licenseMiddleName = cipher.driversLicense.decMiddleName || ''; + draft.licenseLastName = cipher.driversLicense.decLastName || ''; + draft.licenseDateOfBirth = cipher.driversLicense.decDateOfBirth || ''; + draft.licenseNumber = cipher.driversLicense.decLicenseNumber || ''; + draft.licenseIssuingCountry = cipher.driversLicense.decIssuingCountry || ''; + draft.licenseIssuingState = cipher.driversLicense.decIssuingState || ''; + draft.licenseIssueDate = cipher.driversLicense.decIssueDate || ''; + draft.licenseExpirationDate = cipher.driversLicense.decExpirationDate || ''; + draft.licenseIssuingAuthority = cipher.driversLicense.decIssuingAuthority || ''; + draft.licenseClass = cipher.driversLicense.decLicenseClass || ''; + } + if (cipher.passport) { + draft.passportSurname = cipher.passport.decSurname || ''; + draft.passportGivenName = cipher.passport.decGivenName || ''; + draft.passportDateOfBirth = cipher.passport.decDateOfBirth || ''; + draft.passportSex = cipher.passport.decSex || ''; + draft.passportBirthPlace = cipher.passport.decBirthPlace || ''; + draft.passportNationality = cipher.passport.decNationality || ''; + draft.passportIssuingCountry = cipher.passport.decIssuingCountry || ''; + draft.passportNumber = cipher.passport.decPassportNumber || ''; + draft.passportType = cipher.passport.decPassportType || ''; + draft.passportNationalIdentificationNumber = cipher.passport.decNationalIdentificationNumber || ''; + draft.passportIssuingAuthority = cipher.passport.decIssuingAuthority || ''; + draft.passportIssueDate = cipher.passport.decIssueDate || ''; + draft.passportExpirationDate = cipher.passport.decExpirationDate || ''; + } draft.customFields = (cipher.fields || []).map((field) => ({ type: parseFieldType(field.type), label: field.decName || '', diff --git a/webapp/src/lib/api/vault.ts b/webapp/src/lib/api/vault.ts index 0ff375e..d5e2613 100644 --- a/webapp/src/lib/api/vault.ts +++ b/webapp/src/lib/api/vault.ts @@ -513,6 +513,30 @@ async function encryptTextValue(value: string, enc: Uint8Array, mac: Uint8Array) return encryptBw(new TextEncoder().encode(s), enc, mac); } +function stripDecodedObjectFields(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) return {}; + const out: Record = {}; + for (const [key, item] of Object.entries(value)) { + if (/^dec[A-Z]/.test(key)) continue; + out[key] = item; + } + return out; +} + +async function encryptObjectFields( + existing: unknown, + entries: Array<[string, string]>, + draft: VaultDraft, + enc: Uint8Array, + mac: Uint8Array +): Promise> { + const out = stripDecodedObjectFields(existing); + for (const [fieldName, draftKey] of entries) { + out[fieldName] = await encryptTextValue(String((draft as unknown as Record)[draftKey] || ''), enc, mac); + } + return out; +} + async function encryptPasswordHistory( entries: CipherPasswordHistoryEntry[] | null | undefined, enc: Uint8Array, @@ -587,6 +611,40 @@ function draftFromDecryptedCipher(cipher: Cipher): VaultDraft { sshPrivateKey: '', sshPublicKey: '', sshFingerprint: '', + bankName: '', + bankNameOnAccount: '', + bankAccountType: '', + bankAccountNumber: '', + bankRoutingNumber: '', + bankBranchNumber: '', + bankPin: '', + bankSwiftCode: '', + bankIban: '', + bankContactPhone: '', + licenseFirstName: '', + licenseMiddleName: '', + licenseLastName: '', + licenseDateOfBirth: '', + licenseNumber: '', + licenseIssuingCountry: '', + licenseIssuingState: '', + licenseIssueDate: '', + licenseExpirationDate: '', + licenseIssuingAuthority: '', + licenseClass: '', + passportSurname: '', + passportGivenName: '', + passportDateOfBirth: '', + passportSex: '', + passportBirthPlace: '', + passportNationality: '', + passportIssuingCountry: '', + passportNumber: '', + passportType: '', + passportNationalIdentificationNumber: '', + passportIssuingAuthority: '', + passportIssueDate: '', + passportExpirationDate: '', customFields: [], }; @@ -662,6 +720,43 @@ function draftFromDecryptedCipher(cipher: Cipher): VaultDraft { cipher.sshKey.decFingerprint, cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint ); + } else if (type === 6 && cipher.bankAccount) { + draft.bankName = plainCipherValue(cipher.bankAccount.decBankName, cipher.bankAccount.bankName); + draft.bankNameOnAccount = plainCipherValue(cipher.bankAccount.decNameOnAccount, cipher.bankAccount.nameOnAccount); + draft.bankAccountType = plainCipherValue(cipher.bankAccount.decAccountType, cipher.bankAccount.accountType); + draft.bankAccountNumber = plainCipherValue(cipher.bankAccount.decAccountNumber, cipher.bankAccount.accountNumber); + draft.bankRoutingNumber = plainCipherValue(cipher.bankAccount.decRoutingNumber, cipher.bankAccount.routingNumber); + draft.bankBranchNumber = plainCipherValue(cipher.bankAccount.decBranchNumber, cipher.bankAccount.branchNumber); + draft.bankPin = plainCipherValue(cipher.bankAccount.decPin, cipher.bankAccount.pin); + draft.bankSwiftCode = plainCipherValue(cipher.bankAccount.decSwiftCode, cipher.bankAccount.swiftCode); + draft.bankIban = plainCipherValue(cipher.bankAccount.decIban, cipher.bankAccount.iban); + draft.bankContactPhone = plainCipherValue(cipher.bankAccount.decBankContactPhone, cipher.bankAccount.bankContactPhone); + } else if (type === 7 && cipher.driversLicense) { + draft.licenseFirstName = plainCipherValue(cipher.driversLicense.decFirstName, cipher.driversLicense.firstName); + draft.licenseMiddleName = plainCipherValue(cipher.driversLicense.decMiddleName, cipher.driversLicense.middleName); + draft.licenseLastName = plainCipherValue(cipher.driversLicense.decLastName, cipher.driversLicense.lastName); + draft.licenseDateOfBirth = plainCipherValue(cipher.driversLicense.decDateOfBirth, cipher.driversLicense.dateOfBirth); + draft.licenseNumber = plainCipherValue(cipher.driversLicense.decLicenseNumber, cipher.driversLicense.licenseNumber); + draft.licenseIssuingCountry = plainCipherValue(cipher.driversLicense.decIssuingCountry, cipher.driversLicense.issuingCountry); + draft.licenseIssuingState = plainCipherValue(cipher.driversLicense.decIssuingState, cipher.driversLicense.issuingState); + draft.licenseIssueDate = plainCipherValue(cipher.driversLicense.decIssueDate, cipher.driversLicense.issueDate); + draft.licenseExpirationDate = plainCipherValue(cipher.driversLicense.decExpirationDate, cipher.driversLicense.expirationDate); + draft.licenseIssuingAuthority = plainCipherValue(cipher.driversLicense.decIssuingAuthority, cipher.driversLicense.issuingAuthority); + draft.licenseClass = plainCipherValue(cipher.driversLicense.decLicenseClass, cipher.driversLicense.licenseClass); + } else if (type === 8 && cipher.passport) { + draft.passportSurname = plainCipherValue(cipher.passport.decSurname, cipher.passport.surname); + draft.passportGivenName = plainCipherValue(cipher.passport.decGivenName, cipher.passport.givenName); + draft.passportDateOfBirth = plainCipherValue(cipher.passport.decDateOfBirth, cipher.passport.dateOfBirth); + draft.passportSex = plainCipherValue(cipher.passport.decSex, cipher.passport.sex); + draft.passportBirthPlace = plainCipherValue(cipher.passport.decBirthPlace, cipher.passport.birthPlace); + draft.passportNationality = plainCipherValue(cipher.passport.decNationality, cipher.passport.nationality); + draft.passportIssuingCountry = plainCipherValue(cipher.passport.decIssuingCountry, cipher.passport.issuingCountry); + draft.passportNumber = plainCipherValue(cipher.passport.decPassportNumber, cipher.passport.passportNumber); + draft.passportType = plainCipherValue(cipher.passport.decPassportType, cipher.passport.passportType); + draft.passportNationalIdentificationNumber = plainCipherValue(cipher.passport.decNationalIdentificationNumber, cipher.passport.nationalIdentificationNumber); + draft.passportIssuingAuthority = plainCipherValue(cipher.passport.decIssuingAuthority, cipher.passport.issuingAuthority); + draft.passportIssueDate = plainCipherValue(cipher.passport.decIssueDate, cipher.passport.issueDate); + draft.passportExpirationDate = plainCipherValue(cipher.passport.decExpirationDate, cipher.passport.expirationDate); } return draft; @@ -983,6 +1078,10 @@ function getCipherKeyMismatchProbes(cipher: Cipher): string[] { cipher.identity?.title, cipher.identity?.firstName, cipher.sshKey?.privateKey, + cipher.bankAccount?.bankName, + cipher.bankAccount?.accountNumber, + cipher.driversLicense?.licenseNumber, + cipher.passport?.passportNumber, ...(cipher.fields || []).flatMap((field) => [field.name, field.value]), ]; const probes: string[] = []; @@ -1053,6 +1152,40 @@ function hasUnresolvedEncryptedFields(cipher: Cipher): boolean { [cipher.sshKey?.privateKey, cipher.sshKey?.decPrivateKey], [cipher.sshKey?.publicKey, cipher.sshKey?.decPublicKey], [cipher.sshKey?.keyFingerprint || cipher.sshKey?.fingerprint, cipher.sshKey?.decFingerprint], + [cipher.bankAccount?.bankName, cipher.bankAccount?.decBankName], + [cipher.bankAccount?.nameOnAccount, cipher.bankAccount?.decNameOnAccount], + [cipher.bankAccount?.accountType, cipher.bankAccount?.decAccountType], + [cipher.bankAccount?.accountNumber, cipher.bankAccount?.decAccountNumber], + [cipher.bankAccount?.routingNumber, cipher.bankAccount?.decRoutingNumber], + [cipher.bankAccount?.branchNumber, cipher.bankAccount?.decBranchNumber], + [cipher.bankAccount?.pin, cipher.bankAccount?.decPin], + [cipher.bankAccount?.swiftCode, cipher.bankAccount?.decSwiftCode], + [cipher.bankAccount?.iban, cipher.bankAccount?.decIban], + [cipher.bankAccount?.bankContactPhone, cipher.bankAccount?.decBankContactPhone], + [cipher.driversLicense?.firstName, cipher.driversLicense?.decFirstName], + [cipher.driversLicense?.middleName, cipher.driversLicense?.decMiddleName], + [cipher.driversLicense?.lastName, cipher.driversLicense?.decLastName], + [cipher.driversLicense?.dateOfBirth, cipher.driversLicense?.decDateOfBirth], + [cipher.driversLicense?.licenseNumber, cipher.driversLicense?.decLicenseNumber], + [cipher.driversLicense?.issuingCountry, cipher.driversLicense?.decIssuingCountry], + [cipher.driversLicense?.issuingState, cipher.driversLicense?.decIssuingState], + [cipher.driversLicense?.issueDate, cipher.driversLicense?.decIssueDate], + [cipher.driversLicense?.expirationDate, cipher.driversLicense?.decExpirationDate], + [cipher.driversLicense?.issuingAuthority, cipher.driversLicense?.decIssuingAuthority], + [cipher.driversLicense?.licenseClass, cipher.driversLicense?.decLicenseClass], + [cipher.passport?.surname, cipher.passport?.decSurname], + [cipher.passport?.givenName, cipher.passport?.decGivenName], + [cipher.passport?.dateOfBirth, cipher.passport?.decDateOfBirth], + [cipher.passport?.sex, cipher.passport?.decSex], + [cipher.passport?.birthPlace, cipher.passport?.decBirthPlace], + [cipher.passport?.nationality, cipher.passport?.decNationality], + [cipher.passport?.issuingCountry, cipher.passport?.decIssuingCountry], + [cipher.passport?.passportNumber, cipher.passport?.decPassportNumber], + [cipher.passport?.passportType, cipher.passport?.decPassportType], + [cipher.passport?.nationalIdentificationNumber, cipher.passport?.decNationalIdentificationNumber], + [cipher.passport?.issuingAuthority, cipher.passport?.decIssuingAuthority], + [cipher.passport?.issueDate, cipher.passport?.decIssueDate], + [cipher.passport?.expirationDate, cipher.passport?.decExpirationDate], ...(cipher.fields || []).flatMap((field) => [ [field.name, field.decName] as [unknown, unknown], [field.value, field.decValue] as [unknown, unknown], @@ -1157,6 +1290,9 @@ async function buildCipherPayload( identity: null, secureNote: null, sshKey: null, + bankAccount: null, + driversLicense: null, + passport: null, fields: await encryptCustomFields(draft.customFields || [], keys.enc, keys.mac), passwordHistory: await encryptPasswordHistory(cipher?.passwordHistory, keys.enc, keys.mac), }; @@ -1222,11 +1358,73 @@ async function buildCipherPayload( } else if (type === 5) { const encryptedFingerprint = await encryptTextValue(draft.sshFingerprint, keys.enc, keys.mac); payload.sshKey = { + ...stripDecodedObjectFields(cipher?.sshKey), privateKey: await encryptTextValue(draft.sshPrivateKey, keys.enc, keys.mac), publicKey: await encryptTextValue(draft.sshPublicKey, keys.enc, keys.mac), keyFingerprint: encryptedFingerprint, fingerprint: encryptedFingerprint, }; + } else if (type === 6) { + payload.bankAccount = await encryptObjectFields( + cipher?.bankAccount, + [ + ['bankName', 'bankName'], + ['nameOnAccount', 'bankNameOnAccount'], + ['accountType', 'bankAccountType'], + ['accountNumber', 'bankAccountNumber'], + ['routingNumber', 'bankRoutingNumber'], + ['branchNumber', 'bankBranchNumber'], + ['pin', 'bankPin'], + ['swiftCode', 'bankSwiftCode'], + ['iban', 'bankIban'], + ['bankContactPhone', 'bankContactPhone'], + ], + draft, + keys.enc, + keys.mac + ); + } else if (type === 7) { + payload.driversLicense = await encryptObjectFields( + cipher?.driversLicense, + [ + ['firstName', 'licenseFirstName'], + ['middleName', 'licenseMiddleName'], + ['lastName', 'licenseLastName'], + ['dateOfBirth', 'licenseDateOfBirth'], + ['licenseNumber', 'licenseNumber'], + ['issuingCountry', 'licenseIssuingCountry'], + ['issuingState', 'licenseIssuingState'], + ['issueDate', 'licenseIssueDate'], + ['expirationDate', 'licenseExpirationDate'], + ['issuingAuthority', 'licenseIssuingAuthority'], + ['licenseClass', 'licenseClass'], + ], + draft, + keys.enc, + keys.mac + ); + } else if (type === 8) { + payload.passport = await encryptObjectFields( + cipher?.passport, + [ + ['surname', 'passportSurname'], + ['givenName', 'passportGivenName'], + ['dateOfBirth', 'passportDateOfBirth'], + ['sex', 'passportSex'], + ['birthPlace', 'passportBirthPlace'], + ['nationality', 'passportNationality'], + ['issuingCountry', 'passportIssuingCountry'], + ['passportNumber', 'passportNumber'], + ['passportType', 'passportType'], + ['nationalIdentificationNumber', 'passportNationalIdentificationNumber'], + ['issuingAuthority', 'passportIssuingAuthority'], + ['issueDate', 'passportIssueDate'], + ['expirationDate', 'passportExpirationDate'], + ], + draft, + keys.enc, + keys.mac + ); } else if (type === 2) { payload.secureNote = { type: 0 }; } diff --git a/webapp/src/lib/export-formats.ts b/webapp/src/lib/export-formats.ts index 26c02f0..8b9827c 100644 --- a/webapp/src/lib/export-formats.ts +++ b/webapp/src/lib/export-formats.ts @@ -215,13 +215,13 @@ function mapCipherEncrypted(cipher: Cipher): Record { const login = cipher.login; out.login = login ? { - ...cloneValue(login), + ...(cloneWithoutDecodedFields(login) || {}), username: login.username ?? null, password: login.password ?? null, totp: login.totp ?? null, uris: Array.isArray(login.uris) ? login.uris.map((uri) => ({ - ...cloneValue(uri), + ...(cloneWithoutDecodedFields(uri) || {}), uri: uri?.uri ?? null, uriChecksum: uri?.uriChecksum ?? null, match: (uri as { match?: unknown })?.match ?? null, @@ -280,6 +280,7 @@ function mapCipherEncrypted(cipher: Cipher): Record { out.sshKey = cipher.sshKey ? { + ...(cloneWithoutDecodedFields(cipher.sshKey) || {}), privateKey: cipher.sshKey.privateKey ?? null, publicKey: cipher.sshKey.publicKey ?? null, keyFingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, @@ -287,6 +288,9 @@ function mapCipherEncrypted(cipher: Cipher): Record { fingerprint: cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, } : null; + out.bankAccount = cloneWithoutDecodedFields(cipher.bankAccount) ?? null; + out.driversLicense = cloneWithoutDecodedFields(cipher.driversLicense) ?? null; + out.passport = cloneWithoutDecodedFields(cipher.passport) ?? null; return out; } @@ -331,8 +335,8 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint out.login = null; } - out.card = cipher.card ? await deepDecryptUnknown(cipher.card, keyParts.enc, keyParts.mac) : null; - out.identity = cipher.identity ? await deepDecryptUnknown(cipher.identity, keyParts.enc, keyParts.mac) : null; + out.card = cipher.card ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.card), keyParts.enc, keyParts.mac) : null; + out.identity = cipher.identity ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.identity), keyParts.enc, keyParts.mac) : null; if (cipher.sshKey) { const fingerprint = await decryptMaybe( cipher.sshKey.keyFingerprint ?? cipher.sshKey.fingerprint ?? null, @@ -340,6 +344,7 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint keyParts.mac ); out.sshKey = { + ...((await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.sshKey), keyParts.enc, keyParts.mac)) as Record), privateKey: await decryptMaybe(cipher.sshKey.privateKey ?? null, keyParts.enc, keyParts.mac), publicKey: await decryptMaybe(cipher.sshKey.publicKey ?? null, keyParts.enc, keyParts.mac), keyFingerprint: fingerprint, @@ -349,6 +354,15 @@ async function mapCipherPlain(cipher: Cipher, userEnc: Uint8Array, userMac: Uint } else { out.sshKey = null; } + out.bankAccount = cipher.bankAccount + ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.bankAccount), keyParts.enc, keyParts.mac) + : null; + out.driversLicense = cipher.driversLicense + ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.driversLicense), keyParts.enc, keyParts.mac) + : null; + out.passport = cipher.passport + ? await deepDecryptUnknown(cloneWithoutDecodedFields(cipher.passport), keyParts.enc, keyParts.mac) + : null; out.secureNote = cipher.secureNote ? { type: normalizeNumber((cipher.secureNote as { type?: unknown }).type, 0), @@ -431,6 +445,9 @@ function sourceTypeLabel(type: number): string { if (type === 3) return 'card'; if (type === 4) return 'identity'; if (type === 5) return 'sshKey'; + if (type === 6) return 'bankAccount'; + if (type === 7) return 'driversLicense'; + if (type === 8) return 'passport'; if (type === 2) return 'note'; return `type ${type}`; } @@ -449,6 +466,16 @@ function appendRecordFieldLines(lines: string[], prefix: string, value: unknown) } } +function cloneWithoutDecodedFields(value: unknown): Record | null { + if (!isRecord(value)) return null; + const out: Record = {}; + for (const [key, item] of Object.entries(value)) { + if (/^dec[A-Z]/.test(key)) continue; + out[key] = cloneValue(item); + } + return out; +} + const BITWARDEN_CSV_OBJECT_FIELDS: Record = { card: ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code'], identity: [ @@ -472,6 +499,9 @@ const BITWARDEN_CSV_OBJECT_FIELDS: Record = { 'country', ], sshKey: ['privateKey', 'publicKey', 'keyFingerprint', 'fingerprint'], + bankAccount: ['bankName', 'nameOnAccount', 'accountType', 'accountNumber', 'routingNumber', 'branchNumber', 'pin', 'swiftCode', 'iban', 'bankContactPhone'], + driversLicense: ['firstName', 'middleName', 'lastName', 'dateOfBirth', 'licenseNumber', 'issuingCountry', 'issuingState', 'issueDate', 'expirationDate', 'issuingAuthority', 'licenseClass'], + passport: ['surname', 'givenName', 'dateOfBirth', 'sex', 'birthPlace', 'nationality', 'issuingCountry', 'passportNumber', 'passportType', 'nationalIdentificationNumber', 'issuingAuthority', 'issueDate', 'expirationDate'], }; function appendKnownRecordFieldLines(lines: string[], prefix: string, value: unknown): void { diff --git a/webapp/src/lib/i18n/locales/en.ts b/webapp/src/lib/i18n/locales/en.ts index 457aef6..eb0eff8 100644 --- a/webapp/src/lib/i18n/locales/en.ts +++ b/webapp/src/lib/i18n/locales/en.ts @@ -688,6 +688,35 @@ const en: Record = { "txt_last_name": "Last Name", "txt_last_seen": "Last Seen", "txt_license_number": "License Number", + "txt_bank_account": "Bank Account", + "txt_bank_account_details": "Bank Account Details", + "txt_bank_name": "Bank Name", + "txt_name_on_account": "Name on Account", + "txt_account_type": "Account Type", + "txt_account_number": "Account Number", + "txt_routing_number": "Routing Number", + "txt_branch_number": "Branch Number", + "txt_pin": "PIN", + "txt_swift_code": "SWIFT Code", + "txt_iban": "IBAN", + "txt_bank_contact_phone": "Bank Contact Phone", + "txt_drivers_license": "Driver License", + "txt_drivers_license_details": "Driver License Details", + "txt_date_of_birth": "Date of Birth", + "txt_issuing_country": "Issuing Country", + "txt_issuing_state": "Issuing State", + "txt_issue_date": "Issue Date", + "txt_issuing_authority": "Issuing Authority", + "txt_license_class": "License Class", + "txt_passport": "Passport", + "txt_passport_details": "Passport Details", + "txt_surname": "Surname", + "txt_given_name": "Given Name", + "txt_sex": "Sex", + "txt_birth_place": "Place of Birth", + "txt_nationality": "Nationality", + "txt_passport_type": "Passport Type", + "txt_national_id_number": "National ID Number", "txt_link_copied": "Link copied", "txt_linked": "Linked", "txt_linux_desktop": "Linux Desktop", diff --git a/webapp/src/lib/i18n/locales/es.ts b/webapp/src/lib/i18n/locales/es.ts index ea5091b..cf5f129 100644 --- a/webapp/src/lib/i18n/locales/es.ts +++ b/webapp/src/lib/i18n/locales/es.ts @@ -688,6 +688,35 @@ const es: Record = { "txt_last_name": "Apellido", "txt_last_seen": "Visto por última vez", "txt_license_number": "Número de licencia", + "txt_bank_account": "Cuenta bancaria", + "txt_bank_account_details": "Detalles de cuenta bancaria", + "txt_bank_name": "Nombre del banco", + "txt_name_on_account": "Nombre en la cuenta", + "txt_account_type": "Tipo de cuenta", + "txt_account_number": "Número de cuenta", + "txt_routing_number": "Número de ruta", + "txt_branch_number": "Número de sucursal", + "txt_pin": "PIN", + "txt_swift_code": "Código SWIFT", + "txt_iban": "IBAN", + "txt_bank_contact_phone": "Teléfono del banco", + "txt_drivers_license": "Licencia de conducir", + "txt_drivers_license_details": "Detalles de licencia de conducir", + "txt_date_of_birth": "Fecha de nacimiento", + "txt_issuing_country": "País emisor", + "txt_issuing_state": "Estado emisor", + "txt_issue_date": "Fecha de emisión", + "txt_issuing_authority": "Autoridad emisora", + "txt_license_class": "Clase de licencia", + "txt_passport": "Pasaporte", + "txt_passport_details": "Detalles del pasaporte", + "txt_surname": "Apellido", + "txt_given_name": "Nombre", + "txt_sex": "Sexo", + "txt_birth_place": "Lugar de nacimiento", + "txt_nationality": "Nacionalidad", + "txt_passport_type": "Tipo de pasaporte", + "txt_national_id_number": "Número de ID nacional", "txt_link_copied": "Enlace copiado", "txt_linked": "Vinculado", "txt_linux_desktop": "Escritorio Linux", diff --git a/webapp/src/lib/i18n/locales/ru.ts b/webapp/src/lib/i18n/locales/ru.ts index aa07e7f..90e3df6 100644 --- a/webapp/src/lib/i18n/locales/ru.ts +++ b/webapp/src/lib/i18n/locales/ru.ts @@ -688,6 +688,35 @@ const ru: Record = { "txt_last_name": "Фамилия", "txt_last_seen": "Последний визит", "txt_license_number": "Номер лицензии", + "txt_bank_account": "Банковский счет", + "txt_bank_account_details": "Данные банковского счета", + "txt_bank_name": "Название банка", + "txt_name_on_account": "Имя владельца счета", + "txt_account_type": "Тип счета", + "txt_account_number": "Номер счета", + "txt_routing_number": "Маршрутный номер", + "txt_branch_number": "Номер отделения", + "txt_pin": "PIN", + "txt_swift_code": "SWIFT-код", + "txt_iban": "IBAN", + "txt_bank_contact_phone": "Телефон банка", + "txt_drivers_license": "Водительское удостоверение", + "txt_drivers_license_details": "Данные водительского удостоверения", + "txt_date_of_birth": "Дата рождения", + "txt_issuing_country": "Страна выдачи", + "txt_issuing_state": "Регион выдачи", + "txt_issue_date": "Дата выдачи", + "txt_issuing_authority": "Орган выдачи", + "txt_license_class": "Категория", + "txt_passport": "Паспорт", + "txt_passport_details": "Данные паспорта", + "txt_surname": "Фамилия", + "txt_given_name": "Имя", + "txt_sex": "Пол", + "txt_birth_place": "Место рождения", + "txt_nationality": "Гражданство", + "txt_passport_type": "Тип паспорта", + "txt_national_id_number": "Национальный ID", "txt_link_copied": "Ссылка скопирована", "txt_linked": "Связано", "txt_linux_desktop": "Рабочий стол Linux", diff --git a/webapp/src/lib/i18n/locales/zh-CN.ts b/webapp/src/lib/i18n/locales/zh-CN.ts index 893b5dc..36ab760 100644 --- a/webapp/src/lib/i18n/locales/zh-CN.ts +++ b/webapp/src/lib/i18n/locales/zh-CN.ts @@ -688,6 +688,35 @@ const zhCN: Record = { "txt_last_name": "姓", "txt_last_seen": "最后在线", "txt_license_number": "证件号", + "txt_bank_account": "银行账户", + "txt_bank_account_details": "银行账户详情", + "txt_bank_name": "银行名称", + "txt_name_on_account": "账户姓名", + "txt_account_type": "账户类型", + "txt_account_number": "账户号码", + "txt_routing_number": "路由号码", + "txt_branch_number": "分行号码", + "txt_pin": "PIN", + "txt_swift_code": "SWIFT 代码", + "txt_iban": "IBAN", + "txt_bank_contact_phone": "银行联系电话", + "txt_drivers_license": "驾照", + "txt_drivers_license_details": "驾照详情", + "txt_date_of_birth": "出生日期", + "txt_issuing_country": "签发国家/地区", + "txt_issuing_state": "签发州/省", + "txt_issue_date": "签发日期", + "txt_issuing_authority": "签发机构", + "txt_license_class": "驾照等级", + "txt_passport": "护照", + "txt_passport_details": "护照详情", + "txt_surname": "姓", + "txt_given_name": "名", + "txt_sex": "性别", + "txt_birth_place": "出生地", + "txt_nationality": "国籍", + "txt_passport_type": "护照类型", + "txt_national_id_number": "国家身份证号", "txt_link_copied": "链接已复制", "txt_linked": "已关联", "txt_linux_desktop": "Linux 桌面端", diff --git a/webapp/src/lib/i18n/locales/zh-TW.ts b/webapp/src/lib/i18n/locales/zh-TW.ts index 4cd6b76..5c0c3f9 100644 --- a/webapp/src/lib/i18n/locales/zh-TW.ts +++ b/webapp/src/lib/i18n/locales/zh-TW.ts @@ -688,6 +688,35 @@ const zhTW: Record = { "txt_last_name": "姓", "txt_last_seen": "最後在線", "txt_license_number": "證件號", + "txt_bank_account": "銀行帳戶", + "txt_bank_account_details": "銀行帳戶詳情", + "txt_bank_name": "銀行名稱", + "txt_name_on_account": "帳戶姓名", + "txt_account_type": "帳戶類型", + "txt_account_number": "帳戶號碼", + "txt_routing_number": "路由號碼", + "txt_branch_number": "分行號碼", + "txt_pin": "PIN", + "txt_swift_code": "SWIFT 代碼", + "txt_iban": "IBAN", + "txt_bank_contact_phone": "銀行聯絡電話", + "txt_drivers_license": "駕照", + "txt_drivers_license_details": "駕照詳情", + "txt_date_of_birth": "出生日期", + "txt_issuing_country": "簽發國家/地區", + "txt_issuing_state": "簽發州/省", + "txt_issue_date": "簽發日期", + "txt_issuing_authority": "簽發機構", + "txt_license_class": "駕照等級", + "txt_passport": "護照", + "txt_passport_details": "護照詳情", + "txt_surname": "姓", + "txt_given_name": "名", + "txt_sex": "性別", + "txt_birth_place": "出生地", + "txt_nationality": "國籍", + "txt_passport_type": "護照類型", + "txt_national_id_number": "國家身分證號", "txt_link_copied": "鏈接已複製", "txt_linked": "已關聯", "txt_linux_desktop": "Linux 桌面端", diff --git a/webapp/src/lib/import-formats-bitwarden.ts b/webapp/src/lib/import-formats-bitwarden.ts index 9e677b7..6263368 100644 --- a/webapp/src/lib/import-formats-bitwarden.ts +++ b/webapp/src/lib/import-formats-bitwarden.ts @@ -40,6 +40,10 @@ export interface BitwardenCipherInput { fields?: BitwardenFieldInput[] | null; passwordHistory?: Array<{ password?: string | null; lastUsedDate?: string | null }> | null; sshKey?: Record | null; + bankAccount?: Record | null; + driversLicense?: Record | null; + passport?: Record | null; + [key: string]: unknown; } export interface BitwardenJsonInput { @@ -79,6 +83,7 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload { let hasAnyExplicitFolderLink = false; for (const item of itemsRaw) { ciphers.push({ + ...(item && typeof item === 'object' ? item as Record : {}), id: item?.id ?? null, type: Number(item?.type || 1) || 1, name: item?.name ?? 'Untitled', @@ -93,7 +98,7 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload { totp: item.login.totp ?? null, fido2Credentials: Array.isArray(item.login.fido2Credentials) ? item.login.fido2Credentials : null, uris: Array.isArray(item.login.uris) - ? item.login.uris.map((u) => ({ uri: u?.uri ?? null, match: u?.match ?? null })) + ? item.login.uris.map((u) => ({ ...u, uri: u?.uri ?? null, uriChecksum: u?.uriChecksum ?? null, match: u?.match ?? null })) : null, } : null, @@ -114,6 +119,9 @@ export function normalizeBitwardenImport(raw: unknown): CiphersImportPayload { .filter((x) => !!x.password) : null, sshKey: item?.sshKey ?? null, + bankAccount: item?.bankAccount ?? null, + driversLicense: item?.driversLicense ?? null, + passport: item?.passport ?? null, }); const folderId = txt(item?.folderId); if (!folderId) continue; diff --git a/webapp/src/lib/types.ts b/webapp/src/lib/types.ts index bc05402..d97f6be 100644 --- a/webapp/src/lib/types.ts +++ b/webapp/src/lib/types.ts @@ -142,6 +142,86 @@ export interface CipherSshKey { decFingerprint?: string; } +export interface CipherBankAccount { + bankName?: string | null; + nameOnAccount?: string | null; + accountType?: string | null; + accountNumber?: string | null; + routingNumber?: string | null; + branchNumber?: string | null; + pin?: string | null; + swiftCode?: string | null; + iban?: string | null; + bankContactPhone?: string | null; + decBankName?: string; + decNameOnAccount?: string; + decAccountType?: string; + decAccountNumber?: string; + decRoutingNumber?: string; + decBranchNumber?: string; + decPin?: string; + decSwiftCode?: string; + decIban?: string; + decBankContactPhone?: string; + [key: string]: unknown; +} + +export interface CipherDriversLicense { + firstName?: string | null; + middleName?: string | null; + lastName?: string | null; + dateOfBirth?: string | null; + licenseNumber?: string | null; + issuingCountry?: string | null; + issuingState?: string | null; + issueDate?: string | null; + expirationDate?: string | null; + issuingAuthority?: string | null; + licenseClass?: string | null; + decFirstName?: string; + decMiddleName?: string; + decLastName?: string; + decDateOfBirth?: string; + decLicenseNumber?: string; + decIssuingCountry?: string; + decIssuingState?: string; + decIssueDate?: string; + decExpirationDate?: string; + decIssuingAuthority?: string; + decLicenseClass?: string; + [key: string]: unknown; +} + +export interface CipherPassport { + surname?: string | null; + givenName?: string | null; + dateOfBirth?: string | null; + sex?: string | null; + birthPlace?: string | null; + nationality?: string | null; + issuingCountry?: string | null; + passportNumber?: string | null; + passportType?: string | null; + nationalIdentificationNumber?: string | null; + issuingAuthority?: string | null; + issueDate?: string | null; + expirationDate?: string | null; + decSurname?: string; + decGivenName?: string; + decDateOfBirth?: string; + decSex?: string; + decBirthPlace?: string; + decNationality?: string; + decIssuingCountry?: string; + decPassportNumber?: string; + decPassportType?: string; + decNationalIdentificationNumber?: string; + decIssuingAuthority?: string; + decIssueDate?: string; + decExpirationDate?: string; + [key: string]: unknown; +} + export interface CipherField { type?: number | string | null; name?: string | null; @@ -175,6 +255,9 @@ export interface Cipher { card?: CipherCard | null; identity?: CipherIdentity | null; sshKey?: CipherSshKey | null; + bankAccount?: CipherBankAccount | null; + driversLicense?: CipherDriversLicense | null; + passport?: CipherPassport | null; secureNote?: { type?: number | null } | null; passwordHistory?: CipherPasswordHistoryEntry[] | null; fields?: CipherField[] | null; @@ -276,6 +359,40 @@ export interface VaultDraft { sshPrivateKey: string; sshPublicKey: string; sshFingerprint: string; + bankName: string; + bankNameOnAccount: string; + bankAccountType: string; + bankAccountNumber: string; + bankRoutingNumber: string; + bankBranchNumber: string; + bankPin: string; + bankSwiftCode: string; + bankIban: string; + bankContactPhone: string; + licenseFirstName: string; + licenseMiddleName: string; + licenseLastName: string; + licenseDateOfBirth: string; + licenseNumber: string; + licenseIssuingCountry: string; + licenseIssuingState: string; + licenseIssueDate: string; + licenseExpirationDate: string; + licenseIssuingAuthority: string; + licenseClass: string; + passportSurname: string; + passportGivenName: string; + passportDateOfBirth: string; + passportSex: string; + passportBirthPlace: string; + passportNationality: string; + passportIssuingCountry: string; + passportNumber: string; + passportType: string; + passportNationalIdentificationNumber: string; + passportIssuingAuthority: string; + passportIssueDate: string; + passportExpirationDate: string; customFields: VaultDraftField[]; } diff --git a/webapp/src/lib/vault-decrypt.ts b/webapp/src/lib/vault-decrypt.ts index 05ebe67..474e4e1 100644 --- a/webapp/src/lib/vault-decrypt.ts +++ b/webapp/src/lib/vault-decrypt.ts @@ -66,6 +66,31 @@ async function decryptCipherField( return looksLikeCipherString(value) ? '' : value; } +async function decryptCipherObjectFields>( + source: T | null | undefined, + fields: readonly string[], + itemEnc: Uint8Array, + itemMac: Uint8Array, + userEnc: Uint8Array, + userMac: Uint8Array, + canFallbackToUserKey: boolean +): Promise { + if (!source || typeof source !== 'object') return source; + const next: Record = { ...source }; + for (const field of fields) { + const decKey = `dec${field.charAt(0).toUpperCase()}${field.slice(1)}`; + next[decKey] = await decryptCipherField( + source[field] as string | null | undefined, + itemEnc, + itemMac, + userEnc, + userMac, + canFallbackToUserKey + ); + } + return next as T; +} + async function decryptFieldWithSource( value: string | null | undefined, itemEnc: Uint8Array, @@ -200,6 +225,42 @@ export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise ({