{"record":{"id":"e1547e1d93887f1a","repo":"actualbudget/actual","slug":"missing-key","errorCode":"missing-key","errorMessage":"missing-key","messagePattern":"missing-key","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"packages/loot-core/src/server/encryption/index.ts","lineNumber":44,"sourceCode":"  getId() {\n    return this.id;\n  }\n\n  getValue() {\n    return this.value;\n  }\n\n  serialize() {\n    return {\n      id: this.id,\n      base64: this.value.base64,\n    };\n  }\n}\n\nexport function getKey(keyId) {\n  if (keyId == null || keys[keyId] == null) {\n    throw new Error('missing-key');\n  }\n  return keys[keyId];\n}\n\nexport function hasKey(keyId) {\n  return keyId in keys;\n}\n\nexport async function encrypt(value, keyId) {\n  return internals.encrypt(getKey(keyId), value);\n}\n\nexport async function decrypt(encrypted, meta) {\n  return internals.decrypt(getKey(meta.keyId), encrypted, meta);\n}\n\nexport function randomBytes(n) {\n  return internals.randomBytes(n);","sourceCodeStart":26,"sourceCodeEnd":62,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/encryption/index.ts#L26-L62","documentation":"getKey looks up a decrypted key by id in the in-memory key map; if keyId is null or no such key is loaded it throws an Error with message (and code) 'missing-key'. It is used by encrypt/decrypt, so data operations cannot proceed when the required key has not been loaded into memory.","triggerScenarios":"Calling encrypt/decrypt for data whose keyId was never loaded (user hasn't entered the password), after an app restart without re-running key-load, or with a keyId referencing a key deleted from the file.","commonSituations":"Opening an encrypted budget and skipping the password prompt (cancelled dialog); background jobs touching encrypted data before the user unlocks; a corrupt/renamed key id in the file's key metadata.","solutions":["Prompt for the password and run key-load/key-make to load the key into memory before encrypt/decrypt","Check hasKey(keyId) before calling encrypt/decrypt","If the key id doesn't match any known key, re-sync or restore the file's key metadata","Ensure the unlock flow completes (not cancelled) before proceeding with data operations"],"exampleFix":"// before\nconst data = decrypt(keyId, encrypted);\n// after\nif (!hasKey(keyId)) {\n  await loadKeyFromPassword(password); // populates the key map\n}\nconst data = decrypt(keyId, encrypted);","handlingStrategy":"try-catch","validationCode":"import { hasKey } from '../server/encryption';\nif (!hasKey(keyId)) {\n  await promptForPasswordAndLoadKey();\n}","typeGuard":"function isKeyAvailable(keyId: string | null | undefined): boolean {\n  return keyId != null && hasKey(keyId);\n}","tryCatchPattern":"try {\n  const plaintext = decrypt(keyId, blob);\n} catch (e) {\n  if (e.message === 'missing-key') {\n    const ok = await promptForPasswordAndLoadKey();\n    if (ok) return decrypt(keyId, blob);\n    throw new Error('User cancelled unlock; data remains encrypted');\n  }\n  throw e;\n}","preventionTips":["Check hasKey(keyId) before any encrypt/decrypt call","Complete (don't cancel) the password unlock flow before data operations","After app restart, reload keys before background jobs touch encrypted data","Detect key-id mismatch after sync and re-fetch key metadata"],"tags":["encryption","missing-key","state","decryption"],"backgroundTag":"encryption-key-missing","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}