E2EE Desync folders on restart of application, computer or logout and back in to user account#9653
Conversation
Add comprehensive E2E initialization state tracking with enum-based states to prevent race conditions during client startup. This allows better handling of E2E encrypted folders during the initialization phase. Changes: - Add InitializationState enum (NotStarted, Initializing, Initialized, Failed) - Add isInitializing() and initializationState() query methods - Add initializationStateChanged() signal for state monitoring - Emit state changes during initialization lifecycle - Reset state properly in forgetSensitiveData() for re-initialization Files modified: - src/libsync/clientsideencryption.h - src/libsync/clientsideencryption.cpp Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…ing startup Improve E2E encrypted folder discovery logic to avoid premature blacklisting during client startup race conditions. Only blacklist when E2E truly fails, not while initializing. Changes: - Check E2E initialization state before blacklisting folders - Allow E2E folders to proceed when state is NotStarted or Initializing - Only blacklist when E2E state is Failed or unavailable - Track only NEW blacklisted folders for restoration - Preserve user-chosen blacklisted folders (don't add to restoration list) Files modified: - src/libsync/discovery.cpp Signed-off-by: Kevin Andrews <kevin@nforced.uk>
Fix issue where E2E folders would be incorrectly restored after encryption reset. Clear the restoration tracking list and reset initialization flags when user resets E2E encryption. Changes: - Clear SelectiveSyncE2eFoldersToRemoveFromBlacklist in forgetE2eEncryption() - Reset _e2eAskUserForMnemonic flag to allow re-initialization - Trigger E2E folder restoration after manual E2E setup in setupE2eEncryption()" Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…ialized Fix E2E initialization state not properly transitioning to Initialized state during normal startup when keys are loaded from keychain. This caused folders to remain permanently deferred even though E2E was fully functional. The `privateKeyOnServerIsValid` callback path (used when loading existing keys from keychain) was emitting the `initializationFinished` signal but not setting the internal state to Initialized. Only the `sendPublicKey` path (for new key generation) was correctly updating the state. Changes: - Set `_initializationState = InitializationState::Initialized` in privateKeyOnServerIsValid callback - Emit `initializationStateChanged` signal with updated state - Add debug logging to track state transitions during keychain loading Files modified: - src/libsync/clientsideencryption.cpp Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…nguish from permanent blacklisting Improve E2E encrypted folder discovery logic to properly defer folders during initialization while distinguishing temporary deferral from permanent blacklisting. During client startup, E2E folders are now: - Temporarily deferred when E2E state is NotStarted or Initializing (tracked for restoration) - Permanently blacklisted when E2E state is Failed or capability unavailable (not tracked) This prevents folders with failed E2E from being indefinitely tracked for restoration while still allowing proper restoration when E2E successfully initializes. Changes: - Add `shouldTrackForRestoration` parameter to distinguish temporary vs permanent blacklisting - Defer folders with debug logging when E2E is initializing (tracked for restoration) - Blacklist folders without tracking when E2E failed or unavailable - Preserve user-chosen blacklisted folders (don't override user preferences) Files modified: - src/libsync/discovery.h - src/libsync/discovery.cpp Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…ration Implement E2EFolderManager as a stateless bridge coordinator between E2E encryption and folder management to reliably restore blacklisted E2E folders when encryption becomes available. E2EFolderManager provides: - Initialization at application startup (before first sync) - Connection to all account E2E initialization signals - Automatic restoration of deferred E2E folders when encryption ready - Separation of concerns between encryption and folder management Changes: - Add E2EFolderManager singleton class - Implement `initialize()` to connect to existing accounts and listen for new accounts - Implement `slotE2eInitializationFinished()` to handle E2E ready events - Implement `restoreE2eFoldersForAccount()` to remove folders from blacklist and schedule sync - Initialize E2EFolderManager in Application after FolderMan setup - Add comprehensive debug logging with lcE2eFolderManager category Files added: - src/gui/e2efoldermanager.h - src/gui/e2efoldermanager.cpp Files modified: - src/gui/CMakeLists.txt - src/gui/application.cpp - src/gui/accountsettings.cpp Signed-off-by: Kevin Andrews <kevin@nforced.uk>
… restoration logic Signed-off-by: Kevin Andrews <kevin@nforced.uk>
Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…apability early returns Signed-off-by: Kevin Andrews <kevin@nforced.uk>
Signed-off-by: Kevin Andrews <kevin@nforced.uk>
Signed-off-by: Kevin Andrews <kevin@nforced.uk>
nilsding
left a comment
There was a problem hiding this comment.
Thanks again for your contribution, the overall implementation looks fine to me :)
I am mainly concerned about the test cases -- I think most of them are good to have (might be worth to introduce new test files in general, e.g. for AccountManager), however to me they do not seem to really test the behaviour of the new E2eFolderManager class.
test/testfolderman.cpp
Outdated
| void testE2EFolderBlacklistRestoration() | ||
| { | ||
| // Test that E2E folders can be tracked in the database for restoration | ||
| // This test verifies the database operations work without requiring full FolderMan setup |
There was a problem hiding this comment.
These tests should be moved to testsyncjournaldb.cpp then as they are not specific to FolderMan at all and really only testing SyncJournalDb
There was a problem hiding this comment.
Tests relocated from testfolderman.cpp :
testE2EFolderBlacklistRestoration
testE2EFolderNotTrackedIfUserBlacklisted
testE2ERestorationClearsTrackingList
have all been completely removed .
to testsyncjournaldb.cpp for testing SyncJournalDb schema without FolderMan mockups
@nilsding can you review this please?
| account->setCapabilities(capabilities); | ||
|
|
||
| auto accountState = new AccountState(account); | ||
| accountState->setParent(this); | ||
| AccountManager::instance()->addAccount(account); | ||
|
|
||
| // Simulate folders blacklisted during startup (before E2E initialized) | ||
| // This mimics what happens when client restarts and E2E isn't ready yet | ||
| QTemporaryDir syncDir; | ||
| QString dbPath = syncDir.path() + "/.sync_test.db"; | ||
| SyncJournalDb db(dbPath); | ||
|
|
||
| QStringList e2eFoldersToRestore = {"/encrypted1/", "/encrypted2/"}; | ||
| db.setSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, | ||
| e2eFoldersToRestore); | ||
|
|
||
| // Verify folders are marked for restoration | ||
| bool ok = false; | ||
| auto restorationList = db.getSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); | ||
| QVERIFY(ok); | ||
| QCOMPARE(restorationList.size(), 2); | ||
|
|
||
| // E2EFolderManager is ready to restore folders when E2E signal fires | ||
| auto *manager = E2EFolderManager::instance(); | ||
| QVERIFY(manager); | ||
| QVERIFY(account->e2e()); | ||
| } | ||
|
|
||
| void testScenario5_MultipleFoldersTrackedForRestoration() | ||
| { | ||
| // Verify multiple E2E folders can be tracked and restored | ||
| QTemporaryDir dir; | ||
| QString dbPath = dir.path() + "/.sync_test.db"; | ||
| SyncJournalDb db(dbPath); | ||
|
|
||
| // Simulate multiple E2E folders being blacklisted during startup | ||
| QStringList multipleFolders = { | ||
| "/Documents/Private/", | ||
| "/Photos/Encrypted/", | ||
| "/Work/Confidential/", | ||
| "/Personal/Secrets/" | ||
| }; | ||
|
|
||
| db.setSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, | ||
| multipleFolders); | ||
|
|
||
| // Verify all folders are tracked | ||
| bool ok = false; | ||
| auto restorationList = db.getSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); | ||
| QVERIFY(ok); | ||
| QCOMPARE(restorationList.size(), 4); | ||
|
|
||
| for (const auto &folder : multipleFolders) { | ||
| QVERIFY(restorationList.contains(folder)); | ||
| } | ||
|
|
||
| // After restoration, list should be clearable | ||
| db.setSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, | ||
| {}); | ||
|
|
||
| restorationList = db.getSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); | ||
| QVERIFY(ok); | ||
| QVERIFY(restorationList.isEmpty()); | ||
| } | ||
|
|
||
| void testScenario6_UserBlacklistPreserved() | ||
| { | ||
| // Verify user-blacklisted folders are NOT added to restoration list | ||
| QTemporaryDir dir; | ||
| QString dbPath = dir.path() + "/.sync_test.db"; | ||
| SyncJournalDb db(dbPath); | ||
|
|
||
| // User manually blacklists an E2E folder via selective sync | ||
| QStringList userBlacklist = {"/User/Excluded/"}; | ||
| db.setSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncBlackList, | ||
| userBlacklist); | ||
|
|
||
| // Verify it's blacklisted | ||
| bool ok = false; | ||
| auto blacklist = db.getSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncBlackList, &ok); | ||
| QVERIFY(ok); | ||
| QCOMPARE(blacklist.size(), 1); | ||
| QVERIFY(blacklist.contains("/User/Excluded/")); | ||
|
|
||
| // Verify it's NOT in restoration list (would be added only during E2E init) | ||
| auto restorationList = db.getSelectiveSyncList( | ||
| SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); | ||
| QVERIFY(ok); | ||
| QVERIFY(restorationList.isEmpty()); | ||
|
|
||
| // This ensures user preferences are preserved across restarts | ||
| } |
There was a problem hiding this comment.
just as in the previous PR #9379, most of these tests do not actually verify any specific behaviour from E2EFolderManager, but from the AccountManager and SyncJournalDb classes. The tests in here that make use of SyncJournalDb are directly modifying a sync state database which are not part of any folder connection.
You would need to try to properly set up an account with a sync connection (maybe using the FakeFolder test helper class could be a starting point for that -- this one sets up a fake folder for testing complete with a folder-specific sync journal but does not register it with FolderMan, see https://github.com/nextcloud/desktop/blob/master/test/testfolderman.cpp?plain=1#L123-L126 for an example on how this would be achieved), update the capabilities of the account to indicate support for end-to-end-encryption, and e.g. verify the folder's journal db before and after adding the account to AccountManager.
The current state of the testScenario1_FoldersRestoreAfterRestart test is looking really promising, but it should be creating and updating a sync journal DB by itself (other than preparing it to a state where you can verify the behaviour of E2EFolderManager afterwards of course).
There was a problem hiding this comment.
I've had a go at improving the e2ee folder manager tests:
- removed QTemporaryDir and SyncJournalDb instantiations
- creating a FakeFolder folder object
- creating an account with the folder and setup e2ee capabilities for it
- query the fake folder sync journals rather than seeding and relying on a db instance
Let me know your thoughts @nilsding
Thanks @nilsding I'll have a look at the tests as soon as possible, life is on the busy front at the moment with a personal P1 deadline looming 👶 and outstanding DIY renovations needing urgent attention 😳 I've improved on the other areas mentioned and cleaned up the implementation quite a bit, tests definitely needing improvements. Hopefully there will be some time next week or the following week. Thanks for the pointers 🙏 |
16595d6 to
689fcbf
Compare
Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…ve blacklist handling Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…sful initialization using hardware token Signed-off-by: Kevin Andrews <kevin@nforced.uk>
689fcbf to
4321f21
Compare
|
Looking into a couple of regressions these changes have introduced:
had these issues before, race conditions where the e2ee isn't ready but the sync had started |
|
Added back in the continue's inside Removed the || isEncryptedFolderButE2eIsNotSetup check
Client no longer re-downloads all files each client startup for e2ee folders. |
…c termination and blacklist updates Signed-off-by: Kevin Andrews <kevin@nforced.uk>
…ger tests Signed-off-by: Kevin Andrews <kevin@nforced.uk>
be25c2b to
b554564
Compare
Continuation from #9379
Bug report: #9345
I feel quite strongly about this issue as it's directly affecting the ability to keep files synced to a device and introducing risks of data loss.
So on this front, please feel free to contribute to this PR and help to progress it.
Two user acceptance tests ran:
https://github.com/user-attachments/assets/82766180-edc3-4aca-a9bc-947365474381 - Before PR (master branch)
https://github.com/user-attachments/assets/23475baf-4386-41eb-877e-d00459fa520c - After PR (this branch at HEAD today)
I've run some other tests including selectively keeping or unchecking multiple e2ee folders and "normal" folders.
This PR does resolve a (in my opinion) serious issue and completely prevents using the e2ee feature. I've been using the original PR branch for a good few weeks now and i've not found any issues with it.
Testing
Sandboxing testing command used:
Run this from inside the build output folder and modify paths as needed. Just helped me test without affecting the main user sync settings and folders.
To Do