actualbudget/actual · critical · SyncError

encrypt-failure

encrypt-failure

Error message

encrypt-failure

What it means

A SyncError thrown by the sync encoder (encode) when encryption.encrypt() throws while encrypting an outgoing sync message with the budget's encryption key. The error carries isMissingKey=true when the underlying failure was specifically a missing key. It means the client could not produce an encrypted payload, so the sync request is aborted before it is sent.

Source

Thrown at packages/loot-core/src/server/sync/encoder.ts:64

  for (const msg of messages) {
    const binaryMsg = toBinary(
      MessageSchema,
      create(MessageSchema, {
        dataset: msg.dataset,
        row: msg.row,
        column: msg.column,
        value: msg.value as string,
      }),
    );

    let content: Uint8Array;
    let isEncrypted: boolean;
    if (encryptKeyId) {
      let result;
      try {
        result = await encryption.encrypt(binaryMsg, encryptKeyId);
      } catch (e) {
        throw new SyncError('encrypt-failure', {
          isMissingKey: e.message === 'missing-key',
        });
      }

      content = toBinary(
        EncryptedDataSchema,
        create(EncryptedDataSchema, {
          data: result.value,
          iv: Buffer.from(result.meta.iv, 'base64'),
          authTag: Buffer.from(result.meta.authTag, 'base64'),
        }),
      );
      isEncrypted = true;
    } else {
      content = binaryMsg;
      isEncrypted = false;
    }

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Re-enter/re-download the budget encryption key on this client (Settings → encryption key) so 'missing-key' is resolved
  2. Verify the budget's encryptKeyId preference matches an existing key id in the server key store
  3. If end-to-end encryption is not intended, disable encryption for the budget so encode() runs without encryptKeyId
  4. Rebuild the sync data from a known-good encrypted backup if the key is unrecoverable

Example fix

// before: client missing key
await sync(); // SyncError('encrypt-failure', { isMissingKey: true })
// after: load the key first
import * as encryption from './encryption';
await encryption.loadKey(base64Key);
await sync(); // succeeds
Defensive patterns

Strategy: try-catch

Validate before calling

const { encryptKeyId } = prefs.getPrefs();
if (encryptKeyId && !(await encryption.getKey(encryptKeyId))) {
  throw new Error('Encryption key missing before sync');
}

Type guard

function isEncryptFailure(e: unknown): e is SyncError & { reason: { isMissingKey: boolean } } {
  return e instanceof SyncError && e.reason?.code === 'encrypt-failure';
}

Try / catch

try {
  await fullSync();
} catch (e) {
  if (isEncryptFailure(e)) {
    if (e.reason.isMissingKey) await promptForEncryptionKey();
    else throw e;
  } else throw e;
}

Prevention

When it happens

Trigger: encode() is called with a non-empty encryptKeyId (end-to-end encryption enabled on the budget) and encryption.encrypt(binaryMsg, encryptKeyId) throws — most commonly because the key for encryptKeyId is not loaded locally ('missing-key'), or the key data is corrupt/unavailable.

Common situations: Restoring a budget or syncing an encrypted budget on a device where the encryption key was never entered or was lost; switching the budget's encryption key on the server without updating the client; a fresh client install whose local key store does not yet have the key id referenced by budget prefs.

Related errors


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