Implement self-healing migration/repair for legacy logbook keys to fix guest decryption failures
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { db } from './db.js'
|
import { db } from './db.js'
|
||||||
import { getActiveMasterKey } from './auth.js'
|
import { getActiveMasterKey } from './auth.js'
|
||||||
import { encryptBuffer, decryptBuffer, generateMasterKey } from './crypto.js'
|
import { encryptBuffer, decryptBuffer, generateMasterKey, decryptJson } from './crypto.js'
|
||||||
|
|
||||||
// In-memory cache of decrypted logbook keys (ArrayBuffer)
|
// In-memory cache of decrypted logbook keys (ArrayBuffer)
|
||||||
const keyCache = new Map<string, ArrayBuffer>()
|
const keyCache = new Map<string, ArrayBuffer>()
|
||||||
@@ -83,22 +83,32 @@ export function clearLogbookKeysCache() {
|
|||||||
keyCache.clear()
|
keyCache.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures a logbook-specific key exists for a given logbookId.
|
|
||||||
* If not, it generates a key, encrypts it with the user's master key, saves it locally and in the sync queue.
|
|
||||||
*/
|
|
||||||
export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer> {
|
export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer> {
|
||||||
let key = await getLogbookKey(logbookId)
|
const localLb = await db.logbooks.get(logbookId)
|
||||||
if (key) return key
|
const encryptedTitle = localLb ? localLb.encryptedTitle : ''
|
||||||
|
const masterKey = getActiveMasterKey()
|
||||||
|
|
||||||
// Generate new key
|
let key = await getLogbookKey(logbookId)
|
||||||
key = generateLogbookKey()
|
|
||||||
|
// 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) {
|
||||||
|
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)
|
await saveLogbookKey(logbookId, key)
|
||||||
|
|
||||||
// Encrypt it with user master key
|
// Sync the repaired key to the server
|
||||||
const masterKey = getActiveMasterKey()
|
|
||||||
if (!masterKey) throw new Error('Master key not found')
|
|
||||||
|
|
||||||
const aesMasterKey = await window.crypto.subtle.importKey(
|
const aesMasterKey = await window.crypto.subtle.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
masterKey,
|
masterKey,
|
||||||
@@ -107,27 +117,73 @@ export async function ensureLogbookKey(logbookId: string): Promise<ArrayBuffer>
|
|||||||
['encrypt']
|
['encrypt']
|
||||||
)
|
)
|
||||||
const encrypted = await encryptBuffer(key, aesMasterKey)
|
const encrypted = await encryptBuffer(key, aesMasterKey)
|
||||||
|
|
||||||
// Retrieve local logbook details to preserve encryptedTitle
|
|
||||||
const localLb = await db.logbooks.get(logbookId)
|
|
||||||
const encryptedTitle = localLb ? localLb.encryptedTitle : ''
|
|
||||||
|
|
||||||
const payloadData = {
|
const payloadData = {
|
||||||
encryptedTitle,
|
encryptedTitle,
|
||||||
encryptedKey: encrypted.ciphertext,
|
encryptedKey: encrypted.ciphertext,
|
||||||
iv: encrypted.iv,
|
iv: encrypted.iv,
|
||||||
tag: encrypted.tag
|
tag: encrypted.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Put in sync queue to update the server logbook record with the key
|
|
||||||
await db.syncQueue.put({
|
await db.syncQueue.put({
|
||||||
action: 'create', // Server sync treats create as upsert
|
action: 'create',
|
||||||
type: 'logbook',
|
type: 'logbook',
|
||||||
payloadId: logbookId,
|
payloadId: logbookId,
|
||||||
logbookId,
|
logbookId,
|
||||||
data: JSON.stringify(payloadData),
|
data: JSON.stringify(payloadData),
|
||||||
updatedAt: new Date().toISOString()
|
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 (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
|
return key
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user