import { base64ToBytes, decryptBw, decryptStr } from './crypto'; import { deriveSendKeyParts } from './app-support'; import type { Cipher, Folder, Send } from './types'; export interface DecryptVaultCoreArgs { folders: Folder[]; ciphers: Cipher[]; symEncKeyB64: string; symMacKeyB64: string; } export interface DecryptVaultCoreResult { folders: Folder[]; ciphers: Cipher[]; } export interface DecryptSendsArgs { sends: Send[]; symEncKeyB64: string; symMacKeyB64: string; origin: string; } function sameBytes(a: Uint8Array, b: Uint8Array): boolean { if (a.byteLength !== b.byteLength) return false; for (let i = 0; i < a.byteLength; i += 1) { if (a[i] !== b[i]) return false; } return true; } async function decryptField( value: string | null | undefined, enc: Uint8Array, mac: Uint8Array ): Promise { if (!value || typeof value !== 'string') return ''; try { return await decryptStr(value, enc, mac); } catch { return value; } } async function decryptCipherField( value: string | null | undefined, itemEnc: Uint8Array, itemMac: Uint8Array, userEnc: Uint8Array, userMac: Uint8Array, canFallbackToUserKey: boolean ): Promise { if (!value || typeof value !== 'string') return ''; try { return await decryptStr(value, itemEnc, itemMac); } catch { // Try the legacy user-key path for mixed key/field ciphers. } if (canFallbackToUserKey) { try { return await decryptStr(value, userEnc, userMac); } catch { // Preserve the old raw fallback for fields that are genuinely unreadable. } } return value; } async function decryptFieldWithSource( value: string | null | undefined, itemEnc: Uint8Array, itemMac: Uint8Array, userEnc: Uint8Array, userMac: Uint8Array, canFallbackToUserKey: boolean ): Promise<{ text: string; source: 'item' | 'user' | 'plain' }> { const raw = String(value || '').trim(); if (!raw) return { text: '', source: 'plain' }; try { return { text: await decryptStr(raw, itemEnc, itemMac), source: 'item' }; } catch { // Try legacy user-key fallback below. } if (canFallbackToUserKey) { try { return { text: await decryptStr(raw, userEnc, userMac), source: 'user' }; } catch { // Keep plain fallback. } } return { text: raw, source: 'plain' }; } export async function decryptVaultCore(args: DecryptVaultCoreArgs): Promise { const userEnc = base64ToBytes(args.symEncKeyB64); const userMac = base64ToBytes(args.symMacKeyB64); const folders = await Promise.all( args.folders.map(async (folder) => ({ ...folder, decName: await decryptField(folder.name, userEnc, userMac), })) ); const ciphers = await Promise.all( args.ciphers.map(async (cipher) => { let itemEnc = userEnc; let itemMac = userMac; let usesItemKey = false; if (cipher.key) { try { const itemKey = await decryptBw(cipher.key, userEnc, userMac); if (itemKey.length >= 64) { itemEnc = itemKey.slice(0, 32); itemMac = itemKey.slice(32, 64); usesItemKey = true; } } catch { // Keep user key fallback. } } const itemUsesUserKey = sameBytes(itemEnc, userEnc) && sameBytes(itemMac, userMac); const canFallbackToUserKey = usesItemKey; const nextCipher: Cipher = { ...cipher, decName: await decryptCipherField(cipher.name || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decNotes: await decryptCipherField(cipher.notes || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), }; if (cipher.login) { nextCipher.login = { ...cipher.login, decUsername: await decryptCipherField(cipher.login.username || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decPassword: await decryptCipherField(cipher.login.password || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decTotp: await decryptCipherField(cipher.login.totp || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), uris: await Promise.all( (cipher.login.uris || []).map(async (uri) => ({ ...uri, decUri: await decryptCipherField(uri.uri || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), })) ), }; } if (Array.isArray(cipher.passwordHistory)) { nextCipher.passwordHistory = await Promise.all( cipher.passwordHistory.map(async (entry) => ({ ...entry, decPassword: await decryptCipherField(entry?.password || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), })) ); } if (cipher.card) { nextCipher.card = { ...cipher.card, decCardholderName: await decryptCipherField(cipher.card.cardholderName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decNumber: await decryptCipherField(cipher.card.number || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decBrand: await decryptCipherField(cipher.card.brand || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decExpMonth: await decryptCipherField(cipher.card.expMonth || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decExpYear: await decryptCipherField(cipher.card.expYear || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decCode: await decryptCipherField(cipher.card.code || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), }; } if (cipher.identity) { nextCipher.identity = { ...cipher.identity, decTitle: await decryptCipherField(cipher.identity.title || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decFirstName: await decryptCipherField(cipher.identity.firstName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decMiddleName: await decryptCipherField(cipher.identity.middleName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decLastName: await decryptCipherField(cipher.identity.lastName || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decUsername: await decryptCipherField(cipher.identity.username || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decCompany: await decryptCipherField(cipher.identity.company || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decSsn: await decryptCipherField(cipher.identity.ssn || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decPassportNumber: await decryptCipherField(cipher.identity.passportNumber || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decLicenseNumber: await decryptCipherField(cipher.identity.licenseNumber || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decEmail: await decryptCipherField(cipher.identity.email || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decPhone: await decryptCipherField(cipher.identity.phone || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decAddress1: await decryptCipherField(cipher.identity.address1 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decAddress2: await decryptCipherField(cipher.identity.address2 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decAddress3: await decryptCipherField(cipher.identity.address3 || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decCity: await decryptCipherField(cipher.identity.city || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decState: await decryptCipherField(cipher.identity.state || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decPostalCode: await decryptCipherField(cipher.identity.postalCode || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decCountry: await decryptCipherField(cipher.identity.country || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), }; } if (cipher.sshKey) { const encryptedFingerprint = cipher.sshKey.keyFingerprint || cipher.sshKey.fingerprint || ''; nextCipher.sshKey = { ...cipher.sshKey, decPrivateKey: await decryptCipherField(cipher.sshKey.privateKey || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decPublicKey: await decryptCipherField(cipher.sshKey.publicKey || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), keyFingerprint: encryptedFingerprint || null, fingerprint: encryptedFingerprint || null, decFingerprint: await decryptCipherField(encryptedFingerprint, itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), }; } if (cipher.fields) { nextCipher.fields = await Promise.all( cipher.fields.map(async (field) => ({ ...field, decName: await decryptCipherField(field.name || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), decValue: await decryptCipherField(field.value || '', itemEnc, itemMac, userEnc, userMac, canFallbackToUserKey), })) ); } if (Array.isArray(cipher.attachments)) { nextCipher.attachments = await Promise.all( cipher.attachments.map(async (attachment) => { const fileNameResult = await decryptFieldWithSource( attachment.fileName || '', itemEnc, itemMac, userEnc, userMac, !itemUsesUserKey ); return { ...attachment, decFileName: fileNameResult.text, }; }) ); } return nextCipher; }) ); return { folders, ciphers }; } export async function decryptSends(args: DecryptSendsArgs): Promise { const userEnc = base64ToBytes(args.symEncKeyB64); const userMac = base64ToBytes(args.symMacKeyB64); return Promise.all( args.sends.map(async (send) => { const nextSend: Send = { ...send }; try { if (send.key) { const sendKeyRaw = await decryptBw(send.key, userEnc, userMac); const derived = await deriveSendKeyParts(sendKeyRaw); nextSend.decName = await decryptField(send.name || '', derived.enc, derived.mac); nextSend.decNotes = await decryptField(send.notes || '', derived.enc, derived.mac); nextSend.decText = await decryptField(send.text?.text || '', derived.enc, derived.mac); if (send.file?.fileName) { const decFileName = await decryptField(send.file.fileName, derived.enc, derived.mac); nextSend.file = { ...(send.file || {}), fileName: decFileName || send.file.fileName, }; } nextSend.decShareKey = btoa(String.fromCharCode(...sendKeyRaw)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/g, ''); nextSend.shareUrl = `${args.origin}/#/send/${send.accessId}/${nextSend.decShareKey}`; } else { nextSend.decName = ''; nextSend.decNotes = ''; nextSend.decText = ''; } } catch { nextSend.decName = 'Decrypt failed'; } return nextSend; }) ); }