actualbudget/actual · error · FileDownloadError

decrypt-failure

decrypt-failure

Error message

decrypt-failure

What it means

Thrown by the download handler in packages/loot-core/src/server/cloud-storage.ts when an encrypted budget file downloaded from cloud storage cannot be decrypted locally with the loaded encryption key. The app wraps the underlying encryption.decrypt() failure (including the 'missing-key' case) into a FileDownloadError with code 'decrypt-failure' and an isMissingKey flag. This prevents a corrupt or keyless decrypt attempt from silently producing garbage budget data.

Source

Thrown at packages/loot-core/src/server/cloud-storage.ts:499

  if (userFileInfoRes.status !== 'ok') {
    logger.log(
      'Could not download file from the server. Are you sure you have the right file ID?',
      userFileInfoRes,
    );
    throw FileDownloadError('internal', { fileId: cloudFileId });
  }

  const fileData = userFileInfoRes.data;
  let buffer = userFileRes;

  // The download process checks if the server gave us decrypt
  // information. It is assumed that this key has already been loaded
  // in, which is done in a previous step
  if (fileData.encryptMeta) {
    try {
      buffer = await encryption.decrypt(buffer, fileData.encryptMeta);
    } catch (e) {
      throw FileDownloadError('decrypt-failure', {
        isMissingKey: e.message === 'missing-key',
      });
    }
  }

  return importBuffer(fileData, buffer);
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Obtain the correct password/key for the file and re-run key creation/key-test (handlers['key-test']) before downloading
  2. Ensure the budget's key is loaded via the sync server key endpoints (download-budget with the password parameter)
  3. If isMissingKey is true, prompt the user for the encryption password rather than retrying
  4. Verify the cloud file integrity (re-download); if the ciphertext is corrupt, restore from another device's copy

Example fix

// before
const { data } = await api.downloadBudget(cloudFileId);
// after
const { data, error } = await api.downloadBudget(cloudFileId, { password });
if (error?.reason === 'decrypt-failure') {
  throw new Error(error.isMissingKey ? 'Password required to decrypt budget' : 'Wrong decryption key/password');
}
Defensive patterns

Strategy: try-catch

Validate before calling

const files = await api.getBudgetFiles();
const file = files.find(f => f.cloudFileId === cloudFileId);
if (file?.encryptMeta) console.warn('Encrypted budget: ensure key/password is available before download');

Type guard

function isFileDownloadError(e: unknown): e is { type: 'decrypt-failure'; isMissingKey?: boolean } {
  return typeof e === 'object' && e !== null && (e as any).type === 'decrypt-failure';
}

Try / catch

try {
  await api.downloadBudget(cloudFileId);
} catch (e) {
  if (isFileDownloadError(e)) {
    if (e.isMissingKey) promptForPassword();
    else throw new Error('Cannot decrypt budget: wrong or corrupt key');
  } else throw e;
}

Prevention

When it happens

Trigger: Calling download-budget (or the cloud download path in @actual-app/api) for a budget file whose fileData.encryptMeta is set, when the correct end-to-end encryption key is not loaded in memory or the supplied password/key is wrong; also when the stored ciphertext is corrupted.

Common situations: Downloading an end-to-end-encrypted budget on a fresh device/keyring where the key was never fetched; restoring data after clearing local storage; password changed on another device so local key is stale; server data corrupted or tampered.

Related errors


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