diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 0c0c62dbc9f90..ceedd8bc1d679 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -1497,37 +1497,9 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() { - if (!_accountState->account()->e2e()->isInitialized()) { - return; - } - + // FolderMan handles E2E folder restoration globally via slotE2eInitializationStateChanged + // Just disconnect this signal to avoid duplicate handling disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); - - for (const auto folder : FolderMan::instance()->map()) { - if (folder->accountState() != _accountState) { - continue; - } - bool ok = false; - const auto foldersToRemoveFromBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); - if (foldersToRemoveFromBlacklist.isEmpty()) { - continue; - } - auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); - const auto blackListSize = blackList.size(); - if (blackListSize == 0) { - continue; - } - for (const auto &pathToRemoveFromBlackList : foldersToRemoveFromBlacklist) { - blackList.removeAll(pathToRemoveFromBlackList); - } - if (blackList.size() != blackListSize) { - if (folder->isSyncRunning()) { - folderTerminateSyncAndUpdateBlackList(blackList, folder, foldersToRemoveFromBlacklist); - return; - } - updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); - } - } } void AccountSettings::slotE2eEncryptionCertificateNeedMigration() @@ -1538,35 +1510,6 @@ void AccountSettings::slotE2eEncryptionCertificateNeedMigration() }); } -void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const -{ - folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); - folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); - for (const auto &pathToRemoteDiscover : foldersToRemoveFromBlacklist) { - folder->journalDb()->schedulePathForRemoteDiscovery(pathToRemoteDiscover); - } - FolderMan::instance()->scheduleFolder(folder); -} - -void AccountSettings::folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) -{ - if (_folderConnections.contains(folder->alias())) { - qCWarning(lcAccountSettings) << "Folder " << folder->alias() << "is already terminating the sync."; - return; - } - // in case sync is already running - terminate it and start a new one - const QMetaObject::Connection syncTerminatedConnection = connect(folder, &Folder::syncFinished, this, [this, blackList, folder, foldersToRemoveFromBlacklist]() { - const auto foundConnectionIt = _folderConnections.find(folder->alias()); - if (foundConnectionIt != _folderConnections.end()) { - disconnect(*foundConnectionIt); - _folderConnections.erase(foundConnectionIt); - } - updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); - }); - _folderConnections.insert(folder->alias(), syncTerminatedConnection); - folder->slotTerminateSync(); -} - void AccountSettings::refreshSelectiveSyncStatus() { QString unsyncedFoldersString; @@ -1685,6 +1628,8 @@ void AccountSettings::setupE2eEncryption() if (_accountState->account()->e2e()->isInitialized()) { slotE2eEncryptionMnemonicReady(); + // Ensure we restore any E2E folders that may have been blacklisted during startup + QTimer::singleShot(100, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); } else { setupE2eEncryptionMessage(); @@ -1714,6 +1659,17 @@ void AccountSettings::forgetE2eEncryption() const auto account = _accountState->account(); if (!account->e2e()->isInitialized()) { FolderMan::instance()->removeE2eFiles(account); + + // Clear E2E restoration tracking list for all folders + for (const auto folder : FolderMan::instance()->map()) { + if (folder->accountState()->account() == account) { + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); + } + } + + // Reset E2E initialization state to allow re-setup + account->setE2eEncryptionKeysGenerationAllowed(false); + account->setAskUserForMnemonic(false); } } diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 6db64c3f7e229..43d13f6fd18b2 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -110,10 +110,6 @@ protected slots: void slotE2eEncryptionCertificateNeedMigration(); -private slots: - void updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const; - void folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist); - private slots: void displayMnemonic(const QString &mnemonic); void forgetEncryptionOnDeviceForAccount(const OCC::AccountPtr &account) const; @@ -147,8 +143,6 @@ private slots: QAction *_addAccountAction = nullptr; bool _menuShown = false; - - QHash _folderConnections; }; } // namespace OCC diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 54eb15e53b24d..7131eae7d3fd7 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -20,6 +20,7 @@ #include #include #include "updatee2eefolderusersmetadatajob.h" +#include "libsync/clientsideencryption.h" #ifdef Q_OS_MACOS #include @@ -861,6 +862,23 @@ void FolderMan::slotAccountStateChanged() if (accountState->isConnected()) { qCInfo(lcFolderMan) << "Account" << accountName << "connected, scheduling its folders"; + + // Set up E2E initialization tracking for this account + if (accountState->account()->capabilities().clientSideEncryptionAvailable()) { + if (accountState->account()->e2e()) { + // Connect to E2E initialization signals to restore blacklisted folders when ready + connect(accountState->account()->e2e(), &ClientSideEncryption::initializationStateChanged, + this, &FolderMan::slotE2eInitializationStateChanged, Qt::UniqueConnection); + + // If E2E is already initialized, trigger immediate folder restoration + if (accountState->account()->e2e()->isInitialized()) { + qCInfo(lcFolderMan) << "E2E already initialized for account" << accountName << "- checking for folders to restore"; + QTimer::singleShot(500, [this, accountState]() { + restoreE2eFoldersForAccount(accountState); + }); + } + } + } const auto folderMapValues = _folderMap.values(); for (const auto f : folderMapValues) { @@ -1364,6 +1382,72 @@ void FolderMan::addFolderToSelectiveSyncList(const QString &path, const SyncJour } } +void FolderMan::restoreE2eFoldersForAccount(AccountState *accountState) +{ + if (!accountState || !accountState->account()->e2e() || !accountState->account()->e2e()->isInitialized()) { + qCWarning(lcFolderMan) << "Cannot restore E2E folders - E2E not properly initialized"; + return; + } + + qCInfo(lcFolderMan) << "Restoring E2E encrypted folders for account" << accountState->account()->displayName(); + + for (const auto &folder : _folderMap) { + if (folder->accountState() != accountState) { + continue; + } + + bool ok = false; + const auto foldersToRemoveFromBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); + if (!ok || foldersToRemoveFromBlacklist.isEmpty()) { + continue; + } + + auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); + if (!ok) { + qCWarning(lcFolderMan) << "Failed to read blacklist for folder" << folder->alias(); + continue; + } + + bool folderModified = false; + for (const auto &pathToRemove : foldersToRemoveFromBlacklist) { + if (blackList.removeAll(pathToRemove) > 0) { + folderModified = true; + qCInfo(lcFolderMan) << "Restored E2E folder from blacklist:" << pathToRemove << "in folder" << folder->alias(); + } + } + + if (folderModified) { + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); + + // Schedule the folder for sync to pick up the restored folders + if (folder->canSync()) { + scheduleFolder(folder); + } + } else { + // Clear the restoration list even if no changes were made + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); + } + } +} + +void FolderMan::slotE2eInitializationStateChanged(ClientSideEncryption::InitializationState state) +{ + qCInfo(lcFolderMan) << "E2E initialization state changed to:" << static_cast(state); + + if (state == ClientSideEncryption::InitializationState::Initialized) { + // E2E is now initialized - restore blacklisted folders and schedule resync + qCInfo(lcFolderMan) << "E2E initialized - restoring encrypted folders"; + + const auto accountStates = AccountManager::instance()->accounts(); + for (const auto &accountState : accountStates) { + if (accountState && accountState->account() && accountState->account()->e2e() && accountState->account()->e2e()->isInitialized()) { + restoreE2eFoldersForAccount(accountState.data()); + } + } + } +} + void FolderMan::whitelistFolderPath(const QString &path) { addFolderToSelectiveSyncList(path, SyncJournalDb::SelectiveSyncListType::SelectiveSyncWhiteList); diff --git a/src/gui/folderman.h b/src/gui/folderman.h index e8918a6779180..8f7b356eb3aad 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -18,6 +18,7 @@ #include "navigationpanehelper.h" #endif #include "syncfileitem.h" +#include "libsync/clientsideencryption.h" class TestFolderMan; class TestCfApiShellExtensionsIPC; @@ -294,6 +295,7 @@ private slots: void slotFolderCanSyncChanged(); void slotFolderSyncStarted(); void slotFolderSyncFinished(const OCC::SyncResult &); + void slotE2eInitializationStateChanged(OCC::ClientSideEncryption::InitializationState state); void slotRunOneEtagJob(); void slotEtagJobDestroyed(QObject *); @@ -368,6 +370,8 @@ private slots: [[nodiscard]] bool isSwitchToVfsNeeded(const FolderDefinition &folderDefinition) const; void addFolderToSelectiveSyncList(const QString &path, const SyncJournalDb::SelectiveSyncListType list); + + void restoreE2eFoldersForAccount(AccountState *accountState); QSet _disabledFolders; Folder::Map _folderMap; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 174b81727f04c..0e3ed57f03050 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -899,6 +899,16 @@ bool ClientSideEncryption::isInitialized() const return useTokenBasedEncryption() || !getMnemonic().isEmpty(); } +bool ClientSideEncryption::isInitializing() const +{ + return _initializationState == InitializationState::Initializing; +} + +ClientSideEncryption::InitializationState ClientSideEncryption::initializationState() const +{ + return _initializationState; +} + QSslKey ClientSideEncryption::getPublicKey() const { return _encryptionCertificate.getSslPublicKey(); @@ -1051,8 +1061,19 @@ void ClientSideEncryption::initialize(QWidget *settingsDialog) Q_ASSERT(_account); qCInfo(lcCse()) << "Initializing"; + + if (_initializationState == InitializationState::Initializing) { + qCWarning(lcCse()) << "E2E encryption already initializing, ignoring duplicate request"; + return; + } + + _initializationState = InitializationState::Initializing; + emit initializationStateChanged(_initializationState); + if (!_account->capabilities().clientSideEncryptionAvailable()) { qCInfo(lcCse()) << "No Client side encryption available on server."; + _initializationState = InitializationState::Failed; + emit initializationStateChanged(_initializationState); emit initializationFinished(); return; } @@ -1795,6 +1816,11 @@ void ClientSideEncryption::forgetSensitiveData() _encryptionCertificate.clear(); _otherCertificates.clear(); _context.clear(); + + // Reset initialization state so E2E can be re-initialized + _initializationState = InitializationState::NotStarted; + emit initializationStateChanged(_initializationState); + Q_EMIT canDecryptChanged(); Q_EMIT canEncryptChanged(); Q_EMIT userCertificateNeedsMigrationChanged(); @@ -1901,6 +1927,8 @@ bool ClientSideEncryption::sensitiveDataRemaining() const void ClientSideEncryption::failedToInitialize() { forgetSensitiveData(); + _initializationState = InitializationState::Failed; + emit initializationStateChanged(_initializationState); Q_EMIT initializationFinished(); } @@ -2110,6 +2138,8 @@ void ClientSideEncryption::sendPublicKey() case 200: case 409: saveCertificateIdentification(); + _initializationState = InitializationState::Initialized; + emit initializationStateChanged(_initializationState); emit initializationFinished(); break; diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 545a7dd0ba8d0..bc20621de0681 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -245,12 +245,25 @@ class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { FatalError, }; + enum class InitializationState + { + NotStarted, + Initializing, + Initialized, + Failed + }; + Q_ENUM(EncryptionErrorType) + Q_ENUM(InitializationState) explicit ClientSideEncryption(); [[nodiscard]] bool isInitialized() const; + [[nodiscard]] bool isInitializing() const; + + [[nodiscard]] InitializationState initializationState() const; + [[nodiscard]] bool tokenIsSetup() const; [[nodiscard]] QSslKey getPublicKey() const; @@ -297,6 +310,7 @@ class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { signals: void initializationFinished(bool isNewMnemonicGenerated = false); + void initializationStateChanged(InitializationState state); void sensitiveDataForgotten(); void privateKeyDeleted(); void certificateDeleted(); @@ -400,6 +414,8 @@ private slots: AccountPtr _account; + InitializationState _initializationState = InitializationState::NotStarted; + QString _mnemonic; bool _newMnemonicGenerated = false; diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index c6f320c6826df..5595c617d3feb 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -221,8 +221,39 @@ void ProcessDirectoryJob::process() // Recall file shall not be ignored (#4420) const auto isHidden = e.localEntry.isHidden || (!f.first.isEmpty() && f.first[0] == '.' && f.first != QLatin1String(".sys.admin#recall#")); - const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() && - _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->isInitialized(); + // Check E2E folder encryption status with state-aware handling + const auto account = _discoveryData->_account; + const bool isE2eEncryptedFolder = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted(); + const bool hasE2eCapability = account->e2e() != nullptr; + + bool isEncryptedFolderButE2eIsNotSetup = false; + bool shouldDeferE2eFolder = false; + + if (isE2eEncryptedFolder && hasE2eCapability) { + const auto e2eState = account->e2e()->initializationState(); + + if (e2eState == OCC::ClientSideEncryption::InitializationState::NotStarted || + e2eState == OCC::ClientSideEncryption::InitializationState::Initializing) { + shouldDeferE2eFolder = true; + } else if (e2eState == OCC::ClientSideEncryption::InitializationState::Failed) { + isEncryptedFolderButE2eIsNotSetup = true; + } + } else if (isE2eEncryptedFolder && !hasE2eCapability) { + isEncryptedFolderButE2eIsNotSetup = true; + } + + if (shouldDeferE2eFolder) { + qCInfo(lcDisco) << "E2E encrypted folder found but E2E still initializing:" << path._server + << "- E2E state:" << static_cast(account->e2e()->initializationState()) + << "- allowing sync to proceed (E2E will handle when ready)"; + } + + if (isEncryptedFolderButE2eIsNotSetup) { + qCDebug(lcDisco) << "Found E2E encrypted folder but E2E setup failed:" << path._server + << "- E2E available:" << hasE2eCapability + << "- E2E state:" << (hasE2eCapability ? static_cast(account->e2e()->initializationState()) : -1) + << "- E2E initialized:" << (hasE2eCapability ? account->e2e()->isInitialized() : false); + } if (isEncryptedFolderButE2eIsNotSetup) { checkAndUpdateSelectiveSyncListsForE2eeFolders(path._server + "/"); @@ -535,20 +566,28 @@ void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const Q const auto pathWithTrailingSlash = Utility::trailingSlashPath(path); + // Check if folder was previously blacklisted to avoid overriding user choices const auto blackListList = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); auto blackListSet = QSet{blackListList.begin(), blackListList.end()}; - blackListSet.insert(pathWithTrailingSlash); - auto blackList = blackListSet.values(); - blackList.sort(); - _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); - - const auto toRemoveFromBlacklistList = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); - auto toRemoveFromBlacklistSet = QSet{toRemoveFromBlacklistList.begin(), toRemoveFromBlacklistList.end()}; - toRemoveFromBlacklistSet.insert(pathWithTrailingSlash); - // record it into a separate list to automatically remove from blacklist once the e2EE gets set up - auto toRemoveFromBlacklist = toRemoveFromBlacklistSet.values(); - toRemoveFromBlacklist.sort(); - _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, toRemoveFromBlacklist); + + if (!blackListSet.contains(pathWithTrailingSlash)) { + qCInfo(lcDisco) << "Blacklisting E2E folder until initialization:" << pathWithTrailingSlash; + + blackListSet.insert(pathWithTrailingSlash); + auto blackList = blackListSet.values(); + blackList.sort(); + _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); + + // Track for automatic restoration when E2E initializes + const auto toRemoveFromBlacklistList = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); + auto toRemoveFromBlacklistSet = QSet{toRemoveFromBlacklistList.begin(), toRemoveFromBlacklistList.end()}; + toRemoveFromBlacklistSet.insert(pathWithTrailingSlash); + auto toRemoveFromBlacklist = toRemoveFromBlacklistSet.values(); + toRemoveFromBlacklist.sort(); + _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, toRemoveFromBlacklist); + } else { + qCDebug(lcDisco) << "E2E folder already blacklisted, skipping restoration tracking:" << pathWithTrailingSlash; + } } void ProcessDirectoryJob::processFile(PathTuple path,