Found while implementing #298 (tenant-scoped vault identifiers). Pre-existing on main; not introduced by #298 and not made worse by it — devices join succeeds and the first sync pull fails identically with and without that branch.
Summary
ldgr devices join <token> completes pairing and installs the paired device's vault key K_A as the session key, but it never adopts K_A into the joining device's own vault file. The joining device's vault.db remains SQLCipher-encrypted under derive_db_key(K_local), so the first ldgr sync pull after a successful join fails with:
failed to open the encrypted working store — wrong key or an unmigrated plaintext database
Root cause predates the SQLCipher work
This is important for triage — it would be easy to misfile this as "SQLCipher broke device pairing".
So SQLCipher changed when it fails, not why.
The error message is actively destructive
The failure tells the user to run ldgr unlock, which overwrites the keystore entry holding K_A — the only copy. The pairing code is single-use (set_relay_response updates only WHERE response_data IS NULL), so recovery requires generating a fresh code on the other device.
What a real fix requires
- Re-wrap
K_A under the joining device's password into its vault file, and
- Re-key or recreate
vault.db under derive_db_key(K_A).
Note there is currently no PRAGMA rekey / cipher_migrate anywhere in crates/ (verified), so this needs new machinery. It also forces an explicit decision about any local data an earlier ldgr init already created on the joining device (merge, discard, or refuse to join a non-empty vault).
Suggested triage
Plausibly M0-adjacent given #300 covers iOS QR-scan device onboarding — a fix here should land before or alongside that, since onboarding a second device is exactly the flow this breaks.
Repro
- Device A: set up a vault and sync.
- Device B:
ldgr init, then ldgr sync setup.
- Device A:
ldgr devices add → note the pairing code.
- Device B:
ldgr devices join <code> → reports success.
- Device B:
ldgr sync pull → fails with the working-store error above.
Summary
ldgr devices join <token>completes pairing and installs the paired device's vault keyK_Aas the session key, but it never adoptsK_Ainto the joining device's own vault file. The joining device'svault.dbremains SQLCipher-encrypted underderive_db_key(K_local), so the firstldgr sync pullafter a successful join fails with:Root cause predates the SQLCipher work
This is important for triage — it would be easy to misfile this as "SQLCipher broke device pairing".
run_join(crates/ldgr-cli/src/commands/devices.rs) callssession::create_session(..., &vault_key)with the received key, but nothing re-wrapsK_Ainto the joining device's vault file.unlockor the 15-minute session expiry — after whichunlockre-derivesK_localand sync payloads sealed underK_Astop decrypting.6d122f6, at-rest encryption) added the forced probe read inrequire_unlocked_db_with_key, which moved the failure earlier: from "next unlock" to "first pull".So SQLCipher changed when it fails, not why.
The error message is actively destructive
The failure tells the user to run
ldgr unlock, which overwrites the keystore entry holdingK_A— the only copy. The pairing code is single-use (set_relay_responseupdates onlyWHERE response_data IS NULL), so recovery requires generating a fresh code on the other device.What a real fix requires
K_Aunder the joining device's password into its vault file, andvault.dbunderderive_db_key(K_A).Note there is currently no
PRAGMA rekey/cipher_migrateanywhere incrates/(verified), so this needs new machinery. It also forces an explicit decision about any local data an earlierldgr initalready created on the joining device (merge, discard, or refuse to join a non-empty vault).Suggested triage
Plausibly M0-adjacent given #300 covers iOS QR-scan device onboarding — a fix here should land before or alongside that, since onboarding a second device is exactly the flow this breaks.
Repro
ldgr init, thenldgr sync setup.ldgr devices add→ note the pairing code.ldgr devices join <code>→ reports success.ldgr sync pull→ fails with the working-store error above.