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
- Re-enter/re-download the budget encryption key on this client (Settings → encryption key) so 'missing-key' is resolved
- Verify the budget's encryptKeyId preference matches an existing key id in the server key store
- If end-to-end encryption is not intended, disable encryption for the budget so encode() runs without encryptKeyId
- 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
- Enter the budget encryption key on every device before enabling sync
- Keep a safe record of the key id and key material
- Disable e2ee on budgets that don't require it
- Test sync after key rotation on all devices
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
- decrypt-failure
- decrypt-failure
- Sync ID is required for sync ${flag}. Set --sync-id or ACTUA
- Could not resolve on-disk budget id for syncId ${syncId} aft
- TrieNode for key ${k} could not be found
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/2b570f0c0d7fc6cc.
Report an issue: GitHub.