{"record":{"id":"09947381ebe2158c","repo":"actualbudget/actual","slug":"decrypt-failure","errorCode":"decrypt-failure","errorMessage":"decrypt-failure","messagePattern":"decrypt-failure","errorType":"error_code","errorClass":"SyncError","httpStatus":null,"severity":"critical","filePath":"packages/loot-core/src/server/sync/encoder.ts","lineNumber":120,"sourceCode":"  const messages = [];\n\n  for (const envelopePb of responsePb.messages) {\n    let msg;\n\n    if (envelopePb.isEncrypted) {\n      const binary = fromBinary(EncryptedDataSchema, envelopePb.content);\n\n      let decrypted;\n      try {\n        decrypted = await encryption.decrypt(coerceBuffer(binary.data), {\n          keyId: encryptKeyId,\n          algorithm: 'aes-256-gcm',\n          iv: coerceBuffer(binary.iv),\n          authTag: coerceBuffer(binary.authTag),\n        });\n      } catch (e) {\n        logger.log(e);\n        throw new SyncError('decrypt-failure', {\n          isMissingKey: e.message === 'missing-key',\n        });\n      }\n\n      msg = fromBinary(MessageSchema, decrypted);\n    } else {\n      msg = fromBinary(MessageSchema, envelopePb.content);\n    }\n\n    messages.push({\n      timestamp: Timestamp.parse(envelopePb.timestamp),\n      dataset: msg.dataset,\n      row: msg.row,\n      column: msg.column,\n      value: msg.value,\n    });\n  }\n","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/sync/encoder.ts#L102-L138","documentation":"A SyncError thrown by the sync encoder (decode) when encryption.decrypt() fails to decrypt an incoming sync message envelope marked isEncrypted. Like encrypt-failure it sets isMissingKey=true when the cause is a missing key. The received message is dropped and the whole sync fails, since the payload cannot be read.","triggerScenarios":"decode() processes a sync response whose envelope isEncrypted=true and encryption.decrypt(binary.data, { keyId: encryptKeyId, algorithm: 'aes-256-gcm', iv, authTag }) throws — key for encryptKeyId missing locally, wrong key, or corrupted/truncated ciphertext with bad authTag (GCM auth failure).","commonSituations":"Downloading messages from a budget whose key was rotated while this client still holds an old key; shared budget where another device encrypted with a key this device never received; tampered or partially truncated sync payloads failing AES-256-GCM authentication.","solutions":["Load the correct current encryption key on this client so 'missing-key' is resolved","If the key was rotated, re-fetch/enter the new key matching the server's encryptKeyId","Check sync-server logs for storage corruption; restore budget data from backup if payloads are corrupted","As a last resort, reset the budget's sync data (recreate the group) and re-sync from one authoritative device"],"exampleFix":"// before\nconst res = await syncApi.sync(...); // decrypt-failure\n// after: ensure key exists before syncing\nconst key = await encryption.getKey(encryptKeyId);\nif (!key) await promptUserForEncryptionKey(encryptKeyId);\nconst res = await syncApi.sync(...);","handlingStrategy":"try-catch","validationCode":"const { encryptKeyId } = prefs.getPrefs();\nif (envelope.isEncrypted && !encryptKeyId) {\n  throw new Error('Cannot decrypt: no encryption key configured');\n}","typeGuard":"function isDecryptFailure(e: unknown): e is SyncError & { reason: { isMissingKey: boolean } } {\n  return e instanceof SyncError && e.reason?.code === 'decrypt-failure';\n}","tryCatchPattern":"try {\n  const res = await fullSync();\n} catch (e) {\n  if (isDecryptFailure(e)) {\n    await promptForCorrectKey(); // then retry sync once\n  } else throw e;\n}","preventionTips":["Rotate keys on all devices at once, never partially","Verify AES-256-GCM authTag integrity — repeated auth failures indicate corruption","Keep the key store backed up alongside server data","Sync all devices after any key change"],"tags":["sync","encryption","e2ee","decryption"],"backgroundTag":"decryption-key-mismatch","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}