Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
d204214
feat(gh-9345): add E2E initialization state tracking
Caffe1neAdd1ct Jan 20, 2026
0a9e82e
fix(gh-9345): prevent E2E folders from being blacklisted during startup
Caffe1neAdd1ct Jan 20, 2026
b64a9ed
fix(gh-9345): clear E2E restoration list on encryption reset
Caffe1neAdd1ct Jan 20, 2026
30ea5ff
fix(gh-9345): ensure E2E state correctly transitions to Initialized
Caffe1neAdd1ct Jan 21, 2026
4e8b46e
fix(gh-9345): defer E2E folders during initialization, distinguish fr…
Caffe1neAdd1ct Jan 21, 2026
3b3847c
feat(gh-9345): add E2EFolderManager bridge for startup restoration
Caffe1neAdd1ct Jan 21, 2026
66cca74
test(gh-9345): add E2EFolderManager tests and improve folder restorat…
Caffe1neAdd1ct Jan 21, 2026
44623ac
fix(gh-9345): address all review feedback from PR 9379
Caffe1neAdd1ct Mar 19, 2026
3ef9a14
fix(gh-9345): address PR feedback, clean test logs, handle capability…
Caffe1neAdd1ct Mar 19, 2026
89e89d4
fix(gh-9345): clean up comments
Caffe1neAdd1ct Mar 19, 2026
9067ac9
fix(gh-9345): remove maybe_unused and improved qverify asserts
Caffe1neAdd1ct Mar 19, 2026
743aeb4
refactor(gh-9345): consolidate initialization state management
Caffe1neAdd1ct Mar 20, 2026
6186298
fix(gh-9345): enhance E2E folder restoration tests and improve blackl…
Caffe1neAdd1ct Mar 20, 2026
4321f21
fix(gh-9345): set initialization state to failed on unsuccessful init…
Caffe1neAdd1ct Mar 20, 2026
ed5abe1
fix(gh-9345): improve E2E folder restoration by handling sync termina…
Caffe1neAdd1ct Mar 21, 2026
b554564
fix(gh-9345): add initialization logging for E2E folder manager tests
Caffe1neAdd1ct Mar 21, 2026
2c96643
Merge branch 'master' into bugfix/gh-9345-e2e-accountsettings
Caffe1neAdd1ct Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ set(client_SRCS
conflictsolver.cpp
connectionvalidator.h
connectionvalidator.cpp
e2eefoldermanager.h
e2eefoldermanager.cpp
editlocallyjob.h
editlocallyjob.cpp
editlocallymanager.h
Expand Down
44 changes: 6 additions & 38 deletions src/gui/accountsettings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1495,41 +1495,6 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft,
}
}

void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync()
{
if (!_accountState->account()->e2e()->isInitialized()) {
return;
}

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()
{
const auto actionMigrateCertificate = addActionToEncryptionMessage(tr("Migrate certificate to a new one"), e2EeUiActionMigrateCertificateId);
Expand Down Expand Up @@ -1681,8 +1646,6 @@ void AccountSettings::customizeStyle()

void AccountSettings::setupE2eEncryption()
{
connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync);

if (_accountState->account()->e2e()->isInitialized()) {
slotE2eEncryptionMnemonicReady();
} else {
Expand All @@ -1695,7 +1658,6 @@ void AccountSettings::setupE2eEncryption()
"Enter the unique mnemonic to have the encrypted folders synchronize on this device as well."));
}
});
_accountState->account()->setE2eEncryptionKeysGenerationAllowed(false);
_accountState->account()->e2e()->initialize(this);
}
}
Expand All @@ -1714,6 +1676,12 @@ void AccountSettings::forgetE2eEncryption()
const auto account = _accountState->account();
if (!account->e2e()->isInitialized()) {
FolderMan::instance()->removeE2eFiles(account);

for (const auto folder : FolderMan::instance()->map()) {
if (folder->accountState()->account() == account) {
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {});
}
}
}
}

Expand Down
1 change: 0 additions & 1 deletion src/gui/accountsettings.h
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,6 @@ protected slots:

void slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight,
const QVector<int> &roles);
void slotPossiblyUnblacklistE2EeFoldersAndRestartSync();

void slotE2eEncryptionCertificateNeedMigration();

Expand Down
3 changes: 3 additions & 0 deletions src/gui/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "configfile.h"
#include "connectionvalidator.h"
#include "creds/abstractcredentials.h"
#include "e2eefoldermanager.h"
#include "editlocallymanager.h"
#include "folder.h"
#include "folderman.h"
Expand Down Expand Up @@ -504,6 +505,8 @@ void Application::setupAccountsAndFolders()
const auto foldersListSize = FolderMan::instance()->setupFolders();
FolderMan::instance()->setSyncEnabled(true);

E2EFolderManager::instance();

const auto prettyNamesList = [](const QList<AccountStatePtr> &accounts) {
QStringList list;
for (const auto &account : accounts) {
Expand Down
139 changes: 139 additions & 0 deletions src/gui/e2eefoldermanager.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later

#include "e2eefoldermanager.h"
#include "accountmanager.h"
#include "clientsideencryption.h"
#include "folderman.h"
#include "folder.h"

#include <QLoggingCategory>

namespace OCC {

Q_LOGGING_CATEGORY(lcE2eFolderManager, "nextcloud.gui.e2efoldermanager", QtInfoMsg)

E2EFolderManager *E2EFolderManager::_instance = nullptr;

E2EFolderManager *E2EFolderManager::instance()
{
if (!_instance) {
_instance = new E2EFolderManager();
}
return _instance;
}

E2EFolderManager::E2EFolderManager(QObject *parent)
: QObject(parent)
{
const auto accounts = AccountManager::instance()->accounts();
for (const auto &accountState : accounts) {
if (accountState && accountState->account() && accountState->account()->e2e()) {
connectE2eSignals(accountState->account());
}
}

connect(AccountManager::instance(), &AccountManager::accountAdded,
this, &E2EFolderManager::slotAccountAdded);
}

E2EFolderManager::~E2EFolderManager()
{
_instance = nullptr;
}

void E2EFolderManager::slotAccountAdded(AccountState *accountState)
{
if (accountState && accountState->account() && accountState->account()->e2e()) {
connectE2eSignals(accountState->account());
}
}

void E2EFolderManager::connectE2eSignals(const AccountPtr &account)
{
if (!account || !account->e2e()) {
return;
}

connect(account->e2e(), &ClientSideEncryption::initializationFinished,
this, &E2EFolderManager::slotE2eInitializationFinished, Qt::UniqueConnection);

if (account->e2e()->isInitialized()) {
restoreE2eFoldersForAccount(account);
}
}

void E2EFolderManager::slotE2eInitializationFinished()
{
qCDebug(lcE2eFolderManager) << "E2E initialization finished, restoring blacklisted E2E folders";

auto *e2e = qobject_cast<ClientSideEncryption *>(sender());
if (!e2e) {
qCWarning(lcE2eFolderManager) << "slotE2eInitializationFinished called but sender is not ClientSideEncryption";
return;
}

const auto accounts = AccountManager::instance()->accounts();
for (const auto &accountState : accounts) {
if (accountState->account()->e2e() == e2e) {
restoreE2eFoldersForAccount(accountState->account());
break;
}
}
}

void E2EFolderManager::restoreE2eFoldersForAccount(const AccountPtr &account)
{
if (!account || !account->e2e() || !account->e2e()->isInitialized()) {
qCDebug(lcE2eFolderManager) << "Cannot restore folders - account or E2E not ready";
return;
}

qCDebug(lcE2eFolderManager) << "Restoring E2E folders for account:" << account->displayName();

auto *folderMan = FolderMan::instance();
const auto folders = folderMan->map();

int foldersProcessed = 0;
for (const auto &folder : folders) {
if (folder->accountState()->account() != account) {
continue;
}

bool ok = false;
const auto foldersToRemoveFromBlacklist = folder->journalDb()->getSelectiveSyncList(
SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok);

if (foldersToRemoveFromBlacklist.isEmpty()) {
continue;
}

qCDebug(lcE2eFolderManager) << "Found E2E folders to restore for" << folder->alias()
<< ":" << foldersToRemoveFromBlacklist;

auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok);
qCDebug(lcE2eFolderManager) << "Current blacklist:" << blackList;

for (const auto &pathToRemoveFromBlackList : foldersToRemoveFromBlacklist) {
blackList.removeAll(pathToRemoveFromBlackList);
}

folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList);
folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {});

for (const auto &pathToRemoteDiscover : foldersToRemoveFromBlacklist) {
folder->journalDb()->schedulePathForRemoteDiscovery(pathToRemoteDiscover);
}

folderMan->scheduleFolder(folder);
++foldersProcessed;
}

if (foldersProcessed > 0) {
qCDebug(lcE2eFolderManager) << "Restored E2E folders for" << foldersProcessed << "sync folders";
} else {
qCDebug(lcE2eFolderManager) << "No E2E folders needed restoration";
}
}

} // namespace OCC
43 changes: 43 additions & 0 deletions src/gui/e2eefoldermanager.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
// SPDX-License-Identifier: GPL-2.0-or-later

#pragma once

#include <QObject>
#include "account.h"
#include "accountmanager.h"

namespace OCC {

/**
* @brief Stateless bridge between E2E encryption and folder management
*
* This class acts as a mediator that:
* - Listens to E2E initialization signals from all accounts
* - Coordinates folder restoration when E2E becomes ready
* - Keeps E2E concerns separate from FolderMan's core responsibilities
*
* @ingroup gui
*/
class E2EFolderManager : public QObject
{
Q_OBJECT

public:
static E2EFolderManager *instance();
~E2EFolderManager() override;

private slots:
void slotE2eInitializationFinished();
void slotAccountAdded(AccountState *accountState);

private:
E2EFolderManager(QObject *parent = nullptr);

void connectE2eSignals(const AccountPtr &account);
void restoreE2eFoldersForAccount(const AccountPtr &account);

static E2EFolderManager *_instance;
};

} // namespace OCC
7 changes: 4 additions & 3 deletions src/gui/folderwatcher.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,7 @@ void FolderWatcher::performSetPermissionsTest(const QString &path)

if (!QFile::exists(path)) {
QFile f(path);
f.open(QIODevice::WriteOnly);
if (!f.isOpen()) {
if (!f.open(QIODevice::WriteOnly)) {
qCWarning(lcFolderWatcher()) << "Failed to create test file: " << path;
return;
}
Expand Down Expand Up @@ -158,7 +157,9 @@ void FolderWatcher::startNotificationTestWhenReady()
FileSystem::setModTime(path, mtime + 1);
} else {
QFile f(path);
f.open(QIODevice::WriteOnly | QIODevice::Append);
if (!f.open(QIODevice::WriteOnly | QIODevice::Append)) {
qCWarning(lcFolderWatcher()) << "Failed to open test notification file for writing:" << path << f.errorString();
}
}
FileSystem::setFileHidden(path, true);

Expand Down
34 changes: 34 additions & 0 deletions src/libsync/clientsideencryption.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,16 @@ bool ClientSideEncryption::isInitialized() const
return useTokenBasedEncryption() || !getMnemonic().isEmpty();
}

bool ClientSideEncryption::isInitializing() const
{
return _initializationState == InitializationState::Initializing;
}
Comment thread
Caffe1neAdd1ct marked this conversation as resolved.
Outdated

ClientSideEncryption::InitializationState ClientSideEncryption::initializationState() const
{
return _initializationState;
}

QSslKey ClientSideEncryption::getPublicKey() const
{
return _encryptionCertificate.getSslPublicKey();
Expand Down Expand Up @@ -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);
Comment thread
Caffe1neAdd1ct marked this conversation as resolved.
Outdated

if (!_account->capabilities().clientSideEncryptionAvailable()) {
qCInfo(lcCse()) << "No Client side encryption available on server.";
_initializationState = InitializationState::Failed;
emit initializationStateChanged(_initializationState);
emit initializationFinished();
return;
}
Expand Down Expand Up @@ -1795,6 +1816,10 @@ void ClientSideEncryption::forgetSensitiveData()
_encryptionCertificate.clear();
_otherCertificates.clear();
_context.clear();

_initializationState = InitializationState::NotStarted;
emit initializationStateChanged(_initializationState);

Q_EMIT canDecryptChanged();
Q_EMIT canEncryptChanged();
Q_EMIT userCertificateNeedsMigrationChanged();
Expand Down Expand Up @@ -1901,6 +1926,8 @@ bool ClientSideEncryption::sensitiveDataRemaining() const
void ClientSideEncryption::failedToInitialize()
{
forgetSensitiveData();
_initializationState = InitializationState::Failed;
emit initializationStateChanged(_initializationState);
Q_EMIT initializationFinished();
}

Expand Down Expand Up @@ -2110,6 +2137,8 @@ void ClientSideEncryption::sendPublicKey()
case 200:
case 409:
saveCertificateIdentification();
_initializationState = InitializationState::Initialized;
emit initializationStateChanged(_initializationState);
emit initializationFinished();

break;
Expand Down Expand Up @@ -2189,6 +2218,11 @@ void ClientSideEncryption::checkServerHasSavedKeys()
};

const auto privateKeyOnServerIsValid = [this] () {
qCInfo(lcCse) << "Private key on server is valid, setting state to Initialized";
_initializationState = InitializationState::Initialized;
qCInfo(lcCse) << "State set to:" << static_cast<int>(_initializationState);
emit initializationStateChanged(_initializationState);
qCInfo(lcCse) << "Emitting initializationFinished signal";
Q_EMIT initializationFinished();
};

Expand Down
Loading