Journal sync: a transient object-storage read error is treated as "parameters not found" and regenerates pbkdf2salt, locking out every other device

Author: federicobechiniCreated Aug 29, 2026Updated Sep 6, 2026
Labelsawaiting confirmation

Abstract

On the journal sync (S3-compatible object storage) path, getSyncParameters() cannot tell "the parameters object is genuinely absent" from "I could not read the parameters object". Both end up as SyncParamsNotFoundError, so SyncParamsHandler creates fresh parameters and generates a new pbkdf2salt, which is then uploaded to the remote. Every other device keeps the old salt and can no longer decrypt anything until it re-downloads the whole vault.

This is the same class of problem as #1040, but on the journal/object-storage path rather than CouchDB.

Environment

  • Self-hosted LiveSync 1.0.16 (behaviour still present at commit 4b29f44, 24 Aug 2026, which covers 1.0.17 to 1.0.21)
  • Journal sync with an S3-compatible object storage backend
  • macOS desktop (the device that regenerates) plus iOS (the device that gets locked out)
  • End-to-end encryption enabled

Observed behaviour

Three occurrences in one week on an otherwise healthy setup. The machine did not sleep during any of them, and replication was working normally before and after.

27/08/2026, 17:34:54 -> Synchronisation parameters not found, creating new ones
27/08/2026, 17:34:58 -> Synchronisation parameters not found, creating new ones
27/08/2026, 17:35:03 -> Failed to obtain PBKDF2 salt (Security Seed) for replication
27/08/2026, 17:35:12 -> Failed to obtain PBKDF2 salt (Security Seed) for replication
27/08/2026, 17:36:03 -> Synchronisation parameters not found, creating new ones
27/08/2026, 17:37:03 -> Synchronisation parameters not found, creating new ones
27/08/2026, 17:38:03 -> Synchronisation parameters not found, creating new ones
27/08/2026, 17:38:13 -> Synchronisation parameters do not have PBKDF2 salt, generating a new salt

Earlier occurrences: 21 Aug 2026 around 14:18 and 25 Aug 2026 at 06:13, with the same sequence.

The retries spread over several minutes suggest a transient connectivity problem rather than the object actually disappearing from the bucket three times. I cannot prove which it was from the log alone, and that is itself part of the problem: both cases emit the same line.

Analysis

SyncParamsHandler is careful and only regenerates on SyncParamsNotFoundError, rethrowing anything else (src/replication/SyncParamsHandler.ts:78-95). The information is lost one layer below.

MinioStorageAdapter.downloadWithResult() already distinguishes three outcomes correctly, returning NOT_FOUND only when isMissingObjectError(ex) and UNAVAILABLE for everything else (src/replication/journal/objectstore/MinioStorageAdapter.ts:143-175).

But the older download() wrapper collapses them into one falsy value:

typescript
async download(key: string, ignoreCache: boolean = false): Promise<Uint8Array | false> {
    const result = await this.downloadWithResult(key, ignoreCache);
    return result.status === JournalStorageReadStatuses.AVAILABLE ? result.value : false;
}

and getSyncParameters() still uses that wrapper, turning any non-AVAILABLE result into "not found":

typescript
const downloadedData = await this.storage.download(DOCID_JOURNAL_SYNC_PARAMETERS, true);
if (!downloadedData) {
    throw new SyncParamsNotFoundError(`Missing sync parameters`);
}

(src/replication/journal/JournalSyncCore.ts:92-96)

This looks like an incomplete migration rather than a design choice: downloadJsonWithResult() a few lines below, at JournalSyncCore.ts:154, already uses downloadWithResult.

Suggested fix

Have getSyncParameters() use downloadWithResult and branch on the status: raise SyncParamsNotFoundError only on NOT_FOUND, and raise a retryable/fatal fetch error on UNAVAILABLE so that SyncParamsHandler rethrows instead of regenerating.

Why it matters

The failure is silent and asymmetric. The device that regenerates keeps working perfectly, so nothing looks wrong there; the damage lands entirely on the other devices, which simply stop receiving. Recovery requires a full re-download on each of them, which is painful on mobile data. A transient network blip should not be able to invalidate a shared encryption parameter.

Source: vrtmrz/obsidian-livesync