From 4f35ed8f9171be62e5528d1ae09fb9b01dd298a7 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 24 Jun 2026 11:26:09 +0200 Subject: [PATCH 1/2] fix(encryption): explicit detection of child or root encrypted folders ensure when building encrypted folder metadata that information about its status (i.e. child folder or root folder) is set in arguments when testing sharing and leaving shares, it got broken Signed-off-by: Matthieu Gallien --- src/libsync/discovery.cpp | 10 ++++++++-- src/libsync/discoveryphase.cpp | 10 ++++++++-- src/libsync/encryptedfoldermetadatahandler.cpp | 9 ++++++++- src/libsync/encryptfolderjob.cpp | 9 +++++++-- src/libsync/foldermetadata.cpp | 7 ++++++- src/libsync/foldermetadata.h | 3 ++- test/testclientsideencryptionv2.cpp | 13 ++++++++----- test/testsecurefiledrop.cpp | 4 ++-- 8 files changed, 49 insertions(+), 16 deletions(-) diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 394a7b22fd2bb..faa590d3a954a 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -754,8 +754,13 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it Q_ASSERT(item->_e2eEncryptionStatus != SyncFileItem::EncryptionStatus::NotEncrypted); } item->_encryptedFileName = [=, this] { + auto result = QString{}; + + if (item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::NotEncrypted) { + return result; + } if (serverEntry.e2eMangledName.isEmpty()) { - return QString(); + return result; } Q_ASSERT(_discoveryData->_remoteFolder.startsWith('/')); @@ -763,7 +768,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it const auto rootPath = _discoveryData->_remoteFolder.mid(1); Q_ASSERT(serverEntry.e2eMangledName.startsWith(rootPath)); - return serverEntry.e2eMangledName.mid(rootPath.length()); + result = serverEntry.e2eMangledName.mid(rootPath.length()); + return result; }(); item->_locked = serverEntry.locked; item->_lockOwnerDisplayName = serverEntry.lockOwnerDisplayName; diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 602ded62931ba..ba5230aed31a2 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -671,11 +671,17 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in break; } + const auto rootEncryptedFolderInfo = RootEncryptedFolderInfo{Utility::fullRemotePathToRemoteSyncRootRelative(topLevelFolderPath, _remoteRootFolderPath)}; + + const auto folderType = _topLevelE2eeFolderPaths.contains(_subPath) ? FolderMetadata::FolderType::Root : FolderMetadata::FolderType::Nested; + Q_ASSERT((folderType == FolderMetadata::FolderType::Root) == (rootEncryptedFolderInfo.path == QStringLiteral("/"))); + const auto e2EeFolderMetadata = new FolderMetadata(_account, _remoteRootFolderPath, jsonMetadata, - RootEncryptedFolderInfo(Utility::fullRemotePathToRemoteSyncRootRelative(topLevelFolderPath, _remoteRootFolderPath)), - job->signature()); + rootEncryptedFolderInfo, + job->signature(), + folderType); connect(e2EeFolderMetadata, &FolderMetadata::setupComplete, this, [this, e2EeFolderMetadata] { e2EeFolderMetadata->deleteLater(); if (!e2EeFolderMetadata->isValid()) { diff --git a/src/libsync/encryptedfoldermetadatahandler.cpp b/src/libsync/encryptedfoldermetadatahandler.cpp index 6fa3fe52aef50..956d485b6691c 100644 --- a/src/libsync/encryptedfoldermetadatahandler.cpp +++ b/src/libsync/encryptedfoldermetadatahandler.cpp @@ -14,6 +14,8 @@ #include #include +using namespace Qt::StringLiterals; + namespace OCC { Q_LOGGING_CATEGORY(lcFetchAndUploadE2eeFolderMetadataJob, "nextcloud.sync.propagator.encryptedfoldermetadatahandler", QtInfoMsg) @@ -183,7 +185,12 @@ void EncryptedFolderMetadataHandler::slotMetadataReceived(const QJsonDocument &j const auto rawMetadata = statusCode == 404 ? QByteArray{} : json.toJson(QJsonDocument::Compact); - const auto metadata(QSharedPointer::create(_account, _remoteFolderRoot, rawMetadata, _rootEncryptedFolderInfo, job->signature())); + const auto metadata(QSharedPointer::create(_account, + _remoteFolderRoot, + rawMetadata, + _rootEncryptedFolderInfo, + job->signature(), + (_rootEncryptedFolderInfo.path == u"/"_s ? FolderMetadata::FolderType::Root : FolderMetadata::FolderType::Nested))); connect(metadata.data(), &FolderMetadata::setupComplete, this, [this, metadata] { if (!metadata->isValid()) { qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Error parsing or decrypting metadata for folder" << _folderFullRemotePath; diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index 5da898abffd09..92b33003596cd 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -10,6 +10,8 @@ #include "foldermetadata.h" #include +using namespace Qt::StringLiterals; + namespace OCC { Q_LOGGING_CATEGORY(lcEncryptFolderJob, "nextcloud.sync.propagator.encryptfolder", QtInfoMsg) @@ -103,12 +105,15 @@ void EncryptFolderJob::uploadMetadata() return; } + const auto rootEncryptedFolderInfo = RootEncryptedFolderInfo{RootEncryptedFolderInfo::createRootPath(currentPathRelative, rec.path())}; + const auto emptyMetadata(QSharedPointer::create( _account, _remoteSyncRootPath, QByteArray{}, - RootEncryptedFolderInfo(RootEncryptedFolderInfo::createRootPath(currentPathRelative, rec.path())), - QByteArray{})); + rootEncryptedFolderInfo, + QByteArray{}, + rootEncryptedFolderInfo.path == u"/"_s ? FolderMetadata::FolderType::Root : FolderMetadata::FolderType::Nested)); connect(emptyMetadata.data(), &FolderMetadata::setupComplete, this, [this, emptyMetadata] { const auto encryptedMetadata = !emptyMetadata->isValid() ? QByteArray{} : emptyMetadata->encryptedMetadata(); diff --git a/src/libsync/foldermetadata.cpp b/src/libsync/foldermetadata.cpp index d628eff04777d..3cd3e2c8705ff 100644 --- a/src/libsync/foldermetadata.cpp +++ b/src/libsync/foldermetadata.cpp @@ -68,17 +68,19 @@ FolderMetadata::FolderMetadata(AccountPtr account, const QByteArray &metadata, const RootEncryptedFolderInfo &rootEncryptedFolderInfo, const QByteArray &signature, + FolderType folderType, QObject *parent) : QObject(parent) , _account(account) , _remoteFolderRoot(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(remoteFolderRoot))) , _initialMetadata(metadata) - , _isRootEncryptedFolder(rootEncryptedFolderInfo.path == QStringLiteral("/")) + , _isRootEncryptedFolder(folderType == FolderType::Root) , _binaryMetadataKeyForEncryption(rootEncryptedFolderInfo.binaryKeyForEncryption) , _binaryMetadataKeyForDecryption(rootEncryptedFolderInfo.binaryKeyForDecryption) , _keyChecksums(rootEncryptedFolderInfo.keyChecksums) , _initialSignature(signature) { + Q_ASSERT(_isRootEncryptedFolder == (rootEncryptedFolderInfo.path == QStringLiteral("/"))); Q_ASSERT(!_remoteFolderRoot.isEmpty()); _existingMetadataVersion = setupVersionFromExistingMetadata(metadata); @@ -131,6 +133,9 @@ void FolderMetadata::setupExistingMetadata(const QByteArray &metadata) const auto folderUsers = metaDataDoc[usersKey].toArray(); const auto isUsersArrayValid = (!_isRootEncryptedFolder && folderUsers.isEmpty()) || (_isRootEncryptedFolder && !folderUsers.isEmpty()); + if (!isUsersArrayValid) { + qCCritical(lcCseMetadata()) << (_isRootEncryptedFolder ? "is root encrypted folder" : "is child encrypted folder") << folderUsers; + } Q_ASSERT(isUsersArrayValid); if (!isUsersArrayValid) { diff --git a/src/libsync/foldermetadata.h b/src/libsync/foldermetadata.h index c1b126ac21342..496f118edb19a 100644 --- a/src/libsync/foldermetadata.h +++ b/src/libsync/foldermetadata.h @@ -92,7 +92,7 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject }; Q_ENUM(MetadataVersion) - FolderMetadata(AccountPtr account, const QString &remoteFolderRoot, FolderType folderType = FolderType::Nested); + FolderMetadata(AccountPtr account, const QString &remoteFolderRoot, FolderType folderType); /* * construct metadata based on RootEncryptedFolderInfo * as per E2EE V2, the encryption key and users that have access are only stored in root(top-level) encrypted folder's metadata @@ -103,6 +103,7 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject const QByteArray &metadata, const RootEncryptedFolderInfo &rootEncryptedFolderInfo, const QByteArray &signature, + FolderType folderType, QObject *parent = nullptr); [[nodiscard]] QVector files() const; diff --git a/test/testclientsideencryptionv2.cpp b/test/testclientsideencryptionv2.cpp index 9e75af70e3ae7..faf394a8924b7 100644 --- a/test/testclientsideencryptionv2.cpp +++ b/test/testclientsideencryptionv2.cpp @@ -176,7 +176,7 @@ private slots: QScopedPointer metadataFromJson(new FolderMetadata(_account, "/", ocsDoc.toJson(), - RootEncryptedFolderInfo::makeDefault(), signature)); + RootEncryptedFolderInfo::makeDefault(), signature, FolderMetadata::FolderType::Root)); QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJson.data(), &FolderMetadata::setupComplete); metadataSetupExistingCompleteSpy.wait(); QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1); @@ -208,7 +208,8 @@ private slots: QScopedPointer metadataFromJson(new FolderMetadata(_account, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), - emptySignature)); + emptySignature, + FolderMetadata::FolderType::Root)); QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJson.data(), &FolderMetadata::setupComplete); metadataSetupExistingCompleteSpy.wait(); @@ -314,7 +315,7 @@ private slots: QJsonDocument ocsDoc = QJsonDocument::fromJson(QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadataCopy)).toUtf8()); - QScopedPointer metadataFromJsonForSecondUser(new FolderMetadata(_secondAccount, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature)); + QScopedPointer metadataFromJsonForSecondUser(new FolderMetadata(_secondAccount, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature, FolderMetadata::FolderType::Root)); QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJsonForSecondUser.data(), &FolderMetadata::setupComplete); metadataSetupExistingCompleteSpy.wait(); QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1); @@ -338,10 +339,12 @@ private slots: QJsonDocument ocsDocFromSecondUser = QJsonDocument::fromJson( QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadataFromSecondUser)).toUtf8()); - QScopedPointer metadataFromJsonForFirstUserToCheckCrossSharing(new FolderMetadata(_account, "/", + QScopedPointer metadataFromJsonForFirstUserToCheckCrossSharing(new FolderMetadata(_account, + "/", ocsDocFromSecondUser.toJson(), RootEncryptedFolderInfo::makeDefault(), - signatureAfterSecondUserModification)); + signatureAfterSecondUserModification, + FolderMetadata::FolderType::Root)); QSignalSpy metadataSetupForCrossSharingCompleteSpy(metadataFromJsonForFirstUserToCheckCrossSharing.data(), &FolderMetadata::setupComplete); metadataSetupForCrossSharingCompleteSpy.wait(); QCOMPARE(metadataSetupForCrossSharingCompleteSpy.count(), 1); diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp index 8e7162ae34d7d..bcae215bdb307 100644 --- a/test/testsecurefiledrop.cpp +++ b/test/testsecurefiledrop.cpp @@ -141,7 +141,7 @@ private slots: const auto signature = metadata->metadataSignature(); QJsonDocument ocsDoc = QJsonDocument::fromJson(QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadata)).toUtf8()); - _parsedMetadataWithFileDrop.reset(new FolderMetadata(_account, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature)); + _parsedMetadataWithFileDrop.reset(new FolderMetadata(_account, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature, FolderMetadata::FolderType::Root)); QSignalSpy metadataWithFileDropSetupCompleteSpy(_parsedMetadataWithFileDrop.data(), &FolderMetadata::setupComplete); metadataWithFileDropSetupCompleteSpy.wait(); @@ -164,7 +164,7 @@ private slots: QJsonDocument ocsDoc = QJsonDocument::fromJson(QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadata)).toUtf8()); - _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_account, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature)); + _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_account, "/", ocsDoc.toJson(), RootEncryptedFolderInfo::makeDefault(), signature, FolderMetadata::FolderType::Root)); QSignalSpy metadataAfterProcessingFileDropSetupCompleteSpy(_parsedMetadataAfterProcessingFileDrop.data(), &FolderMetadata::setupComplete); metadataAfterProcessingFileDropSetupCompleteSpy.wait(); From 8b9d44bc312a56da36d07f832e09aeb5c3d6d653 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 24 Jun 2026 13:24:18 +0200 Subject: [PATCH 2/2] fix(encryption): prevent user to leave encrypted shares: not possible it is not possible currently to implement end-to-end encryption RFC and allow an user to leave an encrypted share removing all code that used to allow that Signed-off-by: Matthieu Gallien --- src/gui/folderman.cpp | 29 +---------------------------- src/gui/folderman.h | 2 -- src/gui/socketapi/socketapi.cpp | 2 +- 3 files changed, 2 insertions(+), 31 deletions(-) diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 7fe6760ed2305..138c7c207aac5 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -1717,27 +1717,7 @@ void FolderMan::leaveShare(const QString &localFile) SyncJournalFileRecord rec; if (folder->journalDb()->getFileRecord(filePathRelative, &rec) && rec.isValid() && rec.isE2eEncrypted()) { - - if (_removeE2eeShareJob) { - _removeE2eeShareJob->deleteLater(); - } - - _removeE2eeShareJob = new UpdateE2eeFolderUsersMetadataJob(folder->accountState()->account(), - folder->journalDb(), - folder->remotePath(), - UpdateE2eeFolderUsersMetadataJob::Remove, - folder->remotePathTrailingSlash() + filePathRelative, - folder->accountState()->account()->davUser()); - _removeE2eeShareJob->setParent(this); - _removeE2eeShareJob->start(true); - connect(_removeE2eeShareJob, &UpdateE2eeFolderUsersMetadataJob::finished, this, [localFileNoTrailingSlash, this](int code, const QString &message) { - if (code != 200) { - qCWarning(lcFolderMan) << "Could not remove share from E2EE folder's metadata!" << code << message; - return; - } - slotLeaveShare(localFileNoTrailingSlash, _removeE2eeShareJob->folderToken()); - }); - + qCWarning(lcFolderMan) << "You cannot remove yourself from an encrypted share"; return; } slotLeaveShare(localFileNoTrailingSlash); @@ -1760,13 +1740,6 @@ void FolderMan::slotLeaveShare(const QString &localFile, const QByteArray &folde connect(leaveShareJob, &SimpleApiJob::resultReceived, this, [this, folder, localFile](int statusCode) { qCDebug(lcFolderMan) << "slotLeaveShare callback statusCode" << statusCode; Q_UNUSED(statusCode); - if (_removeE2eeShareJob) { - _removeE2eeShareJob->unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success); - connect(_removeE2eeShareJob.data(), &UpdateE2eeFolderUsersMetadataJob::folderUnlocked, this, [this, folder] { - scheduleFolder(folder); - }); - return; - } scheduleFolder(folder); }); leaveShareJob->start(); diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 9bb66f759571b..c74e1832861a6 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -397,8 +397,6 @@ private slots: NavigationPaneHelper _navigationPaneHelper; #endif - QPointer _removeE2eeShareJob; - bool _appRestartRequired = false; static FolderMan *_instance; diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 496a2a4d10131..324a5d5eb6c73 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -1169,7 +1169,7 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi if (!capabilities.shareAPI() || !(theme->userGroupSharing() || (theme->linkSharing() && capabilities.sharePublicLink()))) return; - if (record._isShared && !record._sharedByMe) { + if (record._isShared && !record._sharedByMe && itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem) { listener->sendMessage(QLatin1String("MENU_ITEM:LEAVESHARE") + flagString + tr("Leave this share")); }