import { db } from './db.js' import { getActiveMasterKey } from './auth.js' import { encryptBuffer, decryptBuffer, generateMasterKey, decryptJson } from './crypto.js' // In-memory cache of decrypted logbook keys (ArrayBuffer) const keyCache = new Map() /** * Retrieves the logbook-specific key for a given logbookId. * Falls back to the user's master key if no logbook-specific key exists (legacy logbooks). */ export async function getLogbookKey(logbookId: string): Promise { if (keyCache.has(logbookId)) { return keyCache.get(logbookId)! } const record = await db.logbookKeys.get(logbookId) if (!record) { return null // Caller will fall back to getActiveMasterKey() } const masterKeyBytes = getActiveMasterKey() if (!masterKeyBytes) { throw new Error('Master key not found. Please log in.') } try { // Derive CryptoKey from user master key const aesKey = await window.crypto.subtle.importKey( 'raw', masterKeyBytes, { name: 'AES-GCM' }, false, ['decrypt'] ) // Decrypt logbook key using User Master Key const decrypted = await decryptBuffer(record.encryptedKey, record.iv, record.tag, aesKey) keyCache.set(logbookId, decrypted) return decrypted } catch (err) { console.warn(`Failed to decrypt logbook key for ${logbookId}, returning null to allow fallback:`, err) return null } } /** * Encrypts and stores a logbook-specific key in the local IndexedDB. */ export async function saveLogbookKey(logbookId: string, logbookKeyBuffer: ArrayBuffer): Promise { const masterKeyBytes = getActiveMasterKey() if (!masterKeyBytes) { throw new Error('Master key not found. Please log in.') } // Derive CryptoKey from user master key const aesKey = await window.crypto.subtle.importKey( 'raw', masterKeyBytes, { name: 'AES-GCM' }, false, ['encrypt'] ) const encrypted = await encryptBuffer(logbookKeyBuffer, aesKey) await db.logbookKeys.put({ logbookId, encryptedKey: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag }) keyCache.set(logbookId, logbookKeyBuffer) } /** * Generates a new random 256-bit logbook key. */ export function generateLogbookKey(): ArrayBuffer { return generateMasterKey() // 32 random bytes } /** * Clears the in-memory logbook key cache (called on logout). */ export function clearLogbookKeysCache() { keyCache.clear() } export async function ensureLogbookKey(logbookId: string): Promise { const localLb = await db.logbooks.get(logbookId) const encryptedTitle = localLb ? localLb.encryptedTitle : '' const isShared = localLb?.isShared === 1 const masterKey = getActiveMasterKey() let key = await getLogbookKey(logbookId) // Self-healing migration for legacy logbooks that got a wrong key generated if (key && encryptedTitle && masterKey) { try { const parsed = JSON.parse(encryptedTitle) await decryptJson(parsed.ciphertext, parsed.iv, parsed.tag, key) // Key works, return it return key } catch (err) { if (isShared) { throw new Error( 'Shared logbook encryption key is missing or invalid. Please go online and refresh your logbooks.' ) } console.warn('Stored logbook key failed to decrypt title. Testing if master key works (legacy migration)...') try { const parsed = JSON.parse(encryptedTitle) await decryptJson(parsed.ciphertext, parsed.iv, parsed.tag, masterKey) // Decryption succeeded with master key! Update logbook key to master key. console.info('Legacy logbook detected. Repairing logbook key to match master key...') key = masterKey await saveLogbookKey(logbookId, key) // Sync the repaired key to the server const aesMasterKey = await window.crypto.subtle.importKey( 'raw', masterKey, { name: 'AES-GCM' }, false, ['encrypt'] ) const encrypted = await encryptBuffer(key, aesMasterKey) const payloadData = { encryptedTitle, encryptedKey: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag } await db.syncQueue.put({ action: 'create', type: 'logbook', payloadId: logbookId, logbookId, data: JSON.stringify(payloadData), updatedAt: new Date().toISOString() }) return key } catch (masterErr) { console.error('Neither logbook key nor master key can decrypt title.', masterErr) } } } // If no logbook key exists yet if (!key) { if (isShared) { throw new Error( 'Shared logbook encryption key not found. Please go online and refresh your logbooks.' ) } if (encryptedTitle && masterKey) { try { // Check if title is already decryptable using masterKey (meaning it is a legacy logbook) const parsed = JSON.parse(encryptedTitle) await decryptJson(parsed.ciphertext, parsed.iv, parsed.tag, masterKey) // Yes, legacy logbook! Re-use masterKey as the logbook key so existing data is decryptable. console.info('Using master key for legacy logbook key...') key = masterKey } catch (err) { // Not decryptable with masterKey, generate new random key key = generateLogbookKey() } } else { key = generateLogbookKey() } await saveLogbookKey(logbookId, key) if (masterKey) { const aesMasterKey = await window.crypto.subtle.importKey( 'raw', masterKey, { name: 'AES-GCM' }, false, ['encrypt'] ) const encrypted = await encryptBuffer(key, aesMasterKey) const payloadData = { encryptedTitle, encryptedKey: encrypted.ciphertext, iv: encrypted.iv, tag: encrypted.tag } await db.syncQueue.put({ action: 'create', type: 'logbook', payloadId: logbookId, logbookId, data: JSON.stringify(payloadData), updatedAt: new Date().toISOString() }) } } return key }