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

  1. Prompt for the password and run key-load/key-make to load the key into memory before encrypt/decrypt
  2. Check hasKey(keyId) before calling encrypt/decrypt
  3. If the key id doesn't match any known key, re-sync or restore the file's key metadata
  4. 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

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


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/e1547e1d93887f1a. Report an issue: GitHub.