actualbudget/actual · critical
missing-key
missing-key
Error message
missing-key
What it means
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.
Source
Thrown at packages/loot-core/src/server/encryption/index.ts:44
getId() {
return this.id;
}
getValue() {
return this.value;
}
serialize() {
return {
id: this.id,
base64: this.value.base64,
};
}
}
export function getKey(keyId) {
if (keyId == null || keys[keyId] == null) {
throw new Error('missing-key');
}
return keys[keyId];
}
export function hasKey(keyId) {
return keyId in keys;
}
export async function encrypt(value, keyId) {
return internals.encrypt(getKey(keyId), value);
}
export async function decrypt(encrypted, meta) {
return internals.decrypt(getKey(meta.keyId), encrypted, meta);
}
export function randomBytes(n) {
return internals.randomBytes(n);View on GitHub (pinned to d4334cb6e6)
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
Example fix
// before
const data = decrypt(keyId, encrypted);
// after
if (!hasKey(keyId)) {
await loadKeyFromPassword(password); // populates the key map
}
const data = decrypt(keyId, encrypted); Defensive patterns
Strategy: try-catch
Validate before calling
import { hasKey } from '../server/encryption';
if (!hasKey(keyId)) {
await promptForPasswordAndLoadKey();
} Type guard
function isKeyAvailable(keyId: string | null | undefined): boolean {
return keyId != null && hasKey(keyId);
} Try / catch
try {
const plaintext = decrypt(keyId, blob);
} catch (e) {
if (e.message === 'missing-key') {
const ok = await promptForPasswordAndLoadKey();
if (ok) return decrypt(keyId, blob);
throw new Error('User cancelled unlock; data remains encrypted');
}
throw e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- key-make must be called with file loaded
- decrypt-failure
- encrypt-failure
- Account with ID ${accountId} does not exist.
- This function cannot track error data. It needs to accept th
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/e1547e1d93887f1a.
Report an issue: GitHub.