From b6ba1fe0d61326d24b7c3700531f1a32db6736da Mon Sep 17 00:00:00 2001 From: alex-z Date: Thu, 12 Jan 2023 18:55:04 +0100 Subject: [PATCH] Implement Secure filedrop link share. Move data from 'filedrop' to 'files' when syncing E2EE folders. Signed-off-by: alex-z --- VERSION.cmake | 4 + src/gui/filedetails/ShareDelegate.qml | 5 +- src/gui/filedetails/ShareDetailsPage.qml | 2 + src/gui/filedetails/sharemodel.cpp | 139 +++++++++++---- src/gui/filedetails/sharemodel.h | 7 + src/gui/ocssharejob.cpp | 20 +++ src/gui/ocssharejob.h | 2 + src/gui/sharemanager.cpp | 8 + src/gui/sharemanager.h | 3 + src/gui/socketapi/socketapi.cpp | 82 +++++++-- src/gui/socketapi/socketapi.h | 13 +- src/libsync/CMakeLists.txt | 2 + src/libsync/account.cpp | 11 ++ src/libsync/account.h | 2 + src/libsync/bulkpropagatorjob.cpp | 2 +- src/libsync/bulkpropagatorjob.h | 2 +- src/libsync/clientsideencryption.cpp | 62 ++++++- src/libsync/clientsideencryption.h | 13 +- src/libsync/clientsideencryptionjobs.cpp | 6 +- src/libsync/discovery.cpp | 4 + src/libsync/discoveryphase.cpp | 6 + src/libsync/discoveryphase.h | 3 + src/libsync/encryptfolderjob.cpp | 4 +- src/libsync/owncloudpropagator.cpp | 14 +- src/libsync/owncloudpropagator.h | 12 +- src/libsync/propagateremotemove.h | 2 +- src/libsync/propagateuploadencrypted.cpp | 3 +- src/libsync/propagatorjobs.h | 2 +- src/libsync/syncfileitem.h | 2 + src/libsync/updatefiledropmetadata.cpp | 212 +++++++++++++++++++++++ src/libsync/updatefiledropmetadata.h | 69 ++++++++ test/CMakeLists.txt | 6 + test/fake2eelocksucceeded.json | 10 ++ test/fakefiledrope2eefoldermetadata.json | 10 ++ test/syncenginetestutils.cpp | 5 + test/testsecurefiledrop.cpp | 168 ++++++++++++++++++ version.h.in | 4 + 37 files changed, 830 insertions(+), 91 deletions(-) create mode 100644 src/libsync/updatefiledropmetadata.cpp create mode 100644 src/libsync/updatefiledropmetadata.h create mode 100644 test/fake2eelocksucceeded.json create mode 100644 test/fakefiledrope2eefoldermetadata.json create mode 100644 test/testsecurefiledrop.cpp diff --git a/VERSION.cmake b/VERSION.cmake index 1794a359a..2910a0668 100644 --- a/VERSION.cmake +++ b/VERSION.cmake @@ -9,6 +9,10 @@ set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR 16) set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR 0) set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH 0) +set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR 26) +set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR 0) +set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0) + if ( NOT DEFINED MIRALL_VERSION_SUFFIX ) set( MIRALL_VERSION_SUFFIX "git") #e.g. beta1, beta2, rc1 endif( NOT DEFINED MIRALL_VERSION_SUFFIX ) diff --git a/src/gui/filedetails/ShareDelegate.qml b/src/gui/filedetails/ShareDelegate.qml index 10ca1e679..913dec2e0 100644 --- a/src/gui/filedetails/ShareDelegate.qml +++ b/src/gui/filedetails/ShareDelegate.qml @@ -53,6 +53,7 @@ GridLayout { readonly property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink readonly property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink + readonly property bool isSecureFileDropPlaceholderLinkShare: model.shareType === ShareModel.ShareTypeSecureFileDropPlaceholderLink readonly property bool isInternalLinkShare: model.shareType === ShareModel.ShareTypeInternalLink readonly property string text: model.display ?? "" @@ -163,7 +164,7 @@ GridLayout { imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncTextColor - visible: root.isPlaceholderLinkShare && root.canCreateLinkShares + visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares enabled: visible onClicked: root.createNewLinkShare() @@ -212,7 +213,7 @@ GridLayout { imageSource: "image://svgimage-custom-color/more.svg/" + Style.ncTextColor - visible: !root.isPlaceholderLinkShare && !root.isInternalLinkShare + visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare enabled: visible onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition) diff --git a/src/gui/filedetails/ShareDetailsPage.qml b/src/gui/filedetails/ShareDetailsPage.qml index b4ce6d909..03dd7c52d 100644 --- a/src/gui/filedetails/ShareDetailsPage.qml +++ b/src/gui/filedetails/ShareDetailsPage.qml @@ -70,6 +70,7 @@ Page { readonly property bool expireDateEnforced: shareModelData.expireDateEnforced readonly property bool passwordProtectEnabled: shareModelData.passwordProtectEnabled readonly property bool passwordEnforced: shareModelData.passwordEnforced + readonly property bool isSecureFileDropLink: shareModelData.isSecureFileDropLink readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink @@ -328,6 +329,7 @@ Page { checked: root.editingAllowed text: qsTr("Allow editing") enabled: !root.waitingForEditingAllowedChange + visible: !root.isSecureFileDropLink onClicked: { root.toggleAllowEditing(checked); diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index 7b3743450..f210e4550 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -26,6 +26,7 @@ namespace { static const auto placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__"); static const auto internalLinkShareId = QStringLiteral("__internalLinkShareId__"); +static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secureFileDropPlaceholderLinkShareId__"); QString createRandomPassword() { @@ -39,8 +40,8 @@ QString createRandomPassword() } } -namespace OCC { - +namespace OCC +{ Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel") ShareModel::ShareModel(QObject *parent) @@ -52,7 +53,7 @@ ShareModel::ShareModel(QObject *parent) int ShareModel::rowCount(const QModelIndex &parent) const { - if(parent.isValid() || !_accountState || _localPath.isEmpty()) { + if (parent.isValid() || !_accountState || _localPath.isEmpty()) { return 0; } @@ -80,6 +81,7 @@ QHash ShareModel::roleNames() const roles[PasswordRole] = "password"; roles[PasswordEnforcedRole] = "passwordEnforced"; roles[EditingAllowedRole] = "editingAllowed"; + roles[IsSecureFileDropLinkRole] = "isSecureFileDropLink"; return roles; } @@ -95,8 +97,8 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const } // Some roles only provide values for the link and user/group share types - if(const auto linkShare = share.objectCast()) { - switch(role) { + if (const auto linkShare = share.objectCast()) { + switch (role) { case LinkRole: return linkShare->getLink(); case LinkShareNameRole: @@ -109,23 +111,21 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const return linkShare->getNote(); case ExpireDateEnabledRole: return linkShare->getExpireDate().isValid(); - case ExpireDateRole: - { + case ExpireDateRole: { const auto startOfExpireDayUTC = linkShare->getExpireDate().startOfDay(QTimeZone::utc()); return startOfExpireDayUTC.toMSecsSinceEpoch(); } } } else if (const auto userGroupShare = share.objectCast()) { - switch(role) { + switch (role) { case NoteEnabledRole: return !userGroupShare->getNote().isEmpty(); case NoteRole: return userGroupShare->getNote(); case ExpireDateEnabledRole: return userGroupShare->getExpireDate().isValid(); - case ExpireDateRole: - { + case ExpireDateRole: { const auto startOfExpireDayUTC = userGroupShare->getExpireDate().startOfDay(QTimeZone::utc()); return startOfExpireDayUTC.toMSecsSinceEpoch(); } @@ -134,7 +134,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const return _privateLinkUrl; } - switch(role) { + switch (role) { case Qt::DisplayRole: return displayStringForShare(share); case ShareRole: @@ -151,6 +151,8 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const return expireDateEnforcedForShare(share); case EnforcedMaximumExpireDateRole: return enforcedMaxExpireDateForShare(share); + case IsSecureFileDropLinkRole: + return _isSecureFileDropSupportedFolder && share->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate); case PasswordProtectEnabledRole: return share->isPasswordSet(); case PasswordRole: @@ -159,9 +161,9 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const } return _shareIdRecentlySetPasswords.value(share->getId()); case PasswordEnforcedRole: - return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() && - ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) || - (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword())); + return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() + && ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) + || (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword())); case EditingAllowedRole: return share->getPermissions().testFlag(SharePermissionUpdate); @@ -177,9 +179,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const return {}; } - qCWarning(lcShareModel) << "Got unknown role" << role - << "for share of type" << share->getShareType() - << "so returning null value."; + qCWarning(lcShareModel) << "Got unknown role" << role << "for share of type" << share->getShareType() << "so returning null value."; return {}; } @@ -214,8 +214,7 @@ void ShareModel::updateData() resetData(); if (_localPath.isEmpty() || !_accountState || _accountState->account().isNull()) { - qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath - << "Is account state null:" << !_accountState; + qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath << "Is account state null:" << !_accountState; return; } @@ -240,11 +239,8 @@ void ShareModel::updateData() SyncJournalFileRecord fileRecord; auto resharingAllowed = true; // lets assume the good - if(_folder->journalDb()->getFileRecord(relPath, &fileRecord) && - fileRecord.isValid() && - !fileRecord._remotePerm.isNull() && - !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) { - + if (_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid() && !fileRecord._remotePerm.isNull() + && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) { qCInfo(lcShareModel) << "File record says resharing not allowed"; resharingAllowed = false; } @@ -254,6 +250,10 @@ void ShareModel::updateData() _numericFileId = fileRecord.numericFileId(); + _isEncryptedItem = fileRecord._isE2eEncrypted; + _isSecureFileDropSupportedFolder = + fileRecord._isE2eEncrypted && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported(); + // Will get added when shares are fetched if no link shares are fetched _placeholderLinkShare.reset(new Share(_accountState->account(), placeholderLinkShareId, @@ -269,12 +269,17 @@ void ShareModel::updateData() _sharePath, Share::TypeInternalLink)); + _secureFileDropPlaceholderLinkShare.reset(new Share(_accountState->account(), + secureFileDropPlaceholderLinkShareId, + _accountState->account()->id(), + _accountState->account()->davDisplayName(), + _sharePath, + Share::TypeSecureFileDropPlaceholderLink)); + auto job = new PropfindJob(_accountState->account(), _sharePath); - job->setProperties( - QList() - << "http://open-collaboration-services.org/ns:share-permissions" - << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation - << "http://owncloud.org/ns:privatelink"); + job->setProperties(QList() << "http://open-collaboration-services.org/ns:share-permissions" + << "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation + << "http://owncloud.org/ns:privatelink"); job->setTimeout(10 * 1000); connect(job, &PropfindJob::result, this, &ShareModel::slotPropfindReceived); connect(job, &PropfindJob::finishedWithError, this, [&](const QNetworkReply *reply) { @@ -306,10 +311,12 @@ void ShareModel::initShareManager() if (_manager.isNull() && sharingPossible) { _manager.reset(new ShareManager(_accountState->account(), this)); connect(_manager.data(), &ShareManager::sharesFetched, this, &ShareModel::slotSharesFetched); - connect(_manager.data(), &ShareManager::shareCreated, this, [&]{ _manager->fetchShares(_sharePath); }); + connect(_manager.data(), &ShareManager::shareCreated, this, [&] { + _manager->fetchShares(_sharePath); + }); connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare); connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare); - connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message){ + connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message) { _hasInitialShareFetchCompleted = true; Q_EMIT hasInitialShareFetchCompletedChanged(); emit serverError(code, message); @@ -335,7 +342,7 @@ void ShareModel::handlePlaceholderLinkShare() placeholderLinkSharePresent = true; } - if(linkSharePresent && placeholderLinkSharePresent) { + if (linkSharePresent && placeholderLinkSharePresent) { break; } } @@ -349,6 +356,43 @@ void ShareModel::handlePlaceholderLinkShare() Q_EMIT sharesChanged(); } +void ShareModel::handleSecureFileDropLinkShare() +{ + // We want to add the placeholder if there are no link shares and + // if we are not already showing the placeholder link share + auto linkSharePresent = false; + auto secureFileDropLinkSharePresent = false; + + for (const auto &share : qAsConst(_shares)) { + const auto shareType = share->getShareType(); + + if (!linkSharePresent && shareType == Share::TypeLink) { + linkSharePresent = true; + } else if (!secureFileDropLinkSharePresent && shareType == Share::TypeSecureFileDropPlaceholderLink) { + secureFileDropLinkSharePresent = true; + } + + if (linkSharePresent && secureFileDropLinkSharePresent) { + break; + } + } + + if (linkSharePresent && secureFileDropLinkSharePresent) { + slotRemoveShareWithId(secureFileDropPlaceholderLinkShareId); + } else if (!linkSharePresent && !secureFileDropLinkSharePresent) { + slotAddShare(_secureFileDropPlaceholderLinkShare); + } +} + +void ShareModel::handleLinkShare() +{ + if (!_isEncryptedItem) { + handlePlaceholderLinkShare(); + } else if (_isSecureFileDropSupportedFolder) { + handleSecureFileDropLinkShare(); + } +} + void ShareModel::slotPropfindReceived(const QVariantMap &result) { _fetchOngoing = false; @@ -403,7 +447,7 @@ void ShareModel::slotSharesFetched(const QList &shares) slotAddShare(share); } - handlePlaceholderLinkShare(); + handleLinkShare(); } void ShareModel::setupInternalLinkShare() @@ -411,7 +455,8 @@ void ShareModel::setupInternalLinkShare() if (!_accountState || _accountState->account().isNull() || _localPath.isEmpty() || - _privateLinkUrl.isEmpty()) { + _privateLinkUrl.isEmpty() || + _isEncryptedItem) { return; } @@ -479,7 +524,8 @@ void ShareModel::slotAddShare(const SharePtr &share) connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError); } - handlePlaceholderLinkShare(); + handleLinkShare(); + Q_EMIT sharesChanged(); } void ShareModel::slotRemoveShareWithId(const QString &shareId) @@ -505,7 +551,9 @@ void ShareModel::slotRemoveShareWithId(const QString &shareId) _shares.removeAt(shareIndex.row()); endRemoveRows(); - handlePlaceholderLinkShare(); + handleLinkShare(); + + Q_EMIT sharesChanged(); } void ShareModel::slotServerError(const int code, const QString &message) @@ -533,7 +581,10 @@ void ShareModel::slotRemoveSharee(const ShareePtr &sharee) QString ShareModel::displayStringForShare(const SharePtr &share) const { if (const auto linkShare = share.objectCast()) { - const auto displayString = tr("Share link"); + + const auto isSecureFileDropShare = _isSecureFileDropSupportedFolder && linkShare->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate); + + const auto displayString = isSecureFileDropShare ? tr("Secure filedrop link") : tr("Share link"); if (!linkShare->getLabel().isEmpty()) { return QStringLiteral("%1 (%2)").arg(displayString, linkShare->getLabel()); @@ -544,6 +595,8 @@ QString ShareModel::displayStringForShare(const SharePtr &share) const return tr("Link share"); } else if (share->getShareType() == Share::TypeInternalLink) { return tr("Internal link"); + } else if (share->getShareType() == Share::TypeSecureFileDropPlaceholderLink) { + return tr("Secure file drop"); } else if (share->getShareWith()) { return share->getShareWith()->format(); } @@ -560,6 +613,7 @@ QString ShareModel::iconUrlForShare(const SharePtr &share) const case Share::TypeInternalLink: return QString(iconsPath + QStringLiteral("external.svg")); case Share::TypePlaceholderLink: + case Share::TypeSecureFileDropPlaceholderLink: case Share::TypeLink: return QString(iconsPath + QStringLiteral("public.svg")); case Share::TypeEmail: @@ -890,10 +944,19 @@ void ShareModel::setShareNoteFromQml(const QVariant &share, const QString ¬e) void ShareModel::createNewLinkShare() const { + if (_isEncryptedItem && !_isSecureFileDropSupportedFolder) { + qCWarning(lcShareModel) << "Attempt to create a link share for non-root encrypted folder or a file."; + return; + } + if (_manager) { const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword(); const auto password = askOptionalPassword ? createRandomPassword() : QString(); - _manager->createLinkShare(_sharePath, QString(), password); + if (_isSecureFileDropSupportedFolder) { + _manager->createSecureFileDropShare(_sharePath, {}, password); + return; + } + _manager->createLinkShare(_sharePath, {}, password); } } diff --git a/src/gui/filedetails/sharemodel.h b/src/gui/filedetails/sharemodel.h index b62c80031..9d1071298 100644 --- a/src/gui/filedetails/sharemodel.h +++ b/src/gui/filedetails/sharemodel.h @@ -57,6 +57,7 @@ public: PasswordRole, PasswordEnforcedRole, EditingAllowedRole, + IsSecureFileDropLinkRole, }; Q_ENUM(Roles) @@ -75,6 +76,7 @@ public: ShareTypeRoom = Share::TypeRoom, ShareTypePlaceholderLink = Share::TypePlaceholderLink, ShareTypeInternalLink = Share::TypeInternalLink, + ShareTypeSecureFileDropPlaceholderLink = Share::TypeSecureFileDropPlaceholderLink, }; Q_ENUM(ShareType); @@ -159,6 +161,8 @@ private slots: void updateData(); void initShareManager(); void handlePlaceholderLinkShare(); + void handleSecureFileDropLinkShare(); + void handleLinkShare(); void setupInternalLinkShare(); void slotPropfindReceived(const QVariantMap &result); @@ -188,6 +192,7 @@ private: bool _hasInitialShareFetchCompleted = false; SharePtr _placeholderLinkShare; SharePtr _internalLinkShare; + SharePtr _secureFileDropPlaceholderLinkShare; QPointer _accountState; QPointer _folder; @@ -196,6 +201,8 @@ private: QString _sharePath; SharePermissions _maxSharingPermissions; QByteArray _numericFileId; + bool _isEncryptedItem = false; + bool _isSecureFileDropSupportedFolder = false; SyncJournalFileLockInfo _filelockState; QString _privateLinkUrl; diff --git a/src/gui/ocssharejob.cpp b/src/gui/ocssharejob.cpp index 13c494c4e..dd370f338 100644 --- a/src/gui/ocssharejob.cpp +++ b/src/gui/ocssharejob.cpp @@ -155,6 +155,26 @@ void OcsShareJob::createLinkShare(const QString &path, start(); } +void OcsShareJob::createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password) +{ + setVerb("POST"); + + addParam(QString::fromLatin1("path"), path); + addParam(QString::fromLatin1("shareType"), QString::number(Share::TypeLink)); + addParam(QString::fromLatin1("permissions"), QString::number(4)); + + if (!name.isEmpty()) { + addParam(QString::fromLatin1("name"), name); + } + if (!password.isEmpty()) { + addParam(QString::fromLatin1("password"), password); + } + + addPassStatusCode(403); + + start(); +} + void OcsShareJob::createShare(const QString &path, const Share::ShareType shareType, const QString &shareWith, diff --git a/src/gui/ocssharejob.h b/src/gui/ocssharejob.h index 61d8c1d26..ba75f94c0 100644 --- a/src/gui/ocssharejob.h +++ b/src/gui/ocssharejob.h @@ -111,6 +111,8 @@ public: void createLinkShare(const QString &path, const QString &name, const QString &password); + void createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password); + /** * Create a new share * diff --git a/src/gui/sharemanager.cpp b/src/gui/sharemanager.cpp index 621ab6c47..c285331e2 100644 --- a/src/gui/sharemanager.cpp +++ b/src/gui/sharemanager.cpp @@ -396,6 +396,14 @@ void ShareManager::createLinkShare(const QString &path, job->createLinkShare(path, name, password); } +void ShareManager::createSecureFileDropShare(const QString &path, const QString &name, const QString &password) +{ + const auto createShareJob = new OcsShareJob(_account); + connect(createShareJob, &OcsShareJob::shareJobFinished, this, &ShareManager::slotLinkShareCreated); + connect(createShareJob, &OcsJob::ocsError, this, &ShareManager::slotOcsError); + createShareJob->createSecureFileDropLinkShare(path, name, password); +} + void ShareManager::slotLinkShareCreated(const QJsonDocument &reply) { QString message; diff --git a/src/gui/sharemanager.h b/src/gui/sharemanager.h index e03628936..37f5ee6f9 100644 --- a/src/gui/sharemanager.h +++ b/src/gui/sharemanager.h @@ -52,6 +52,7 @@ public: * Need to be in sync with Sharee::Type */ enum ShareType { + TypeSecureFileDropPlaceholderLink = -3, TypeInternalLink = -2, TypePlaceholderLink = -1, TypeUser = Sharee::User, @@ -377,6 +378,8 @@ public: const QString &name, const QString &password); + void createSecureFileDropShare(const QString &path, const QString &name, const QString &password); + /** * Tell the manager to create a new share * diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 959be7b01..4e0ef59ae 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -587,6 +587,13 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li return; } + if (!fileData.journalRecord().e2eMangledName().isEmpty()) { + // we can not share an encrypted file or a subfolder under encrypted root foolder + const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile); + listener->sendMessage(message); + return; + } + auto &remotePath = fileData.serverRelativePath; // Can't share root folder @@ -729,12 +736,13 @@ class GetOrCreatePublicLinkShare : public QObject { Q_OBJECT public: - GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, + GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, const bool isSecureFileDropOnlyFolder, QObject *parent) : QObject(parent) , _account(account) , _shareManager(account) , _localFile(localFile) + , _isSecureFileDropOnlyFolder(isSecureFileDropOnlyFolder) { connect(&_shareManager, &ShareManager::sharesFetched, this, &GetOrCreatePublicLinkShare::sharesFetched); @@ -771,7 +779,11 @@ private slots: // otherwise create a new one qCDebug(lcPublicLink) << "Creating new share"; - _shareManager.createLinkShare(_localFile, shareName, QString()); + if (_isSecureFileDropOnlyFolder) { + _shareManager.createSecureFileDropShare(_localFile, shareName, QString()); + } else { + _shareManager.createLinkShare(_localFile, shareName, QString()); + } } void linkShareCreated(const QSharedPointer &share) @@ -832,6 +844,7 @@ private: AccountPtr _account; ShareManager _shareManager; QString _localFile; + bool _isSecureFileDropOnlyFolder = false; }; #else @@ -852,19 +865,36 @@ public: #endif +void SocketApi::command_COPY_SECUREFILEDROP_LINK(const QString &localFile, SocketListener *) +{ + const auto fileData = FileData::get(localFile); + if (!fileData.folder) { + return; + } + + const auto account = fileData.folder->accountState()->account(); + const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, true, this); + connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) { copyUrlToClipboard(url); }); + connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() { emit shareCommandReceived(fileData.localPath); }); + getOrCreatePublicLinkShareJob->run(); +} + void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *) { - auto fileData = FileData::get(localFile); - if (!fileData.folder) + const auto fileData = FileData::get(localFile); + if (!fileData.folder) { return; + } - AccountPtr account = fileData.folder->accountState()->account(); - auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, this); - connect(job, &GetOrCreatePublicLinkShare::done, this, - [](const QString &url) { copyUrlToClipboard(url); }); - connect(job, &GetOrCreatePublicLinkShare::error, this, - [=]() { emit shareCommandReceived(fileData.localPath); }); - job->run(); + const auto account = fileData.folder->accountState()->account(); + const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, false, this); + connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) { + copyUrlToClipboard(url); + }); + connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() { + emit shareCommandReceived(fileData.localPath); + }); + getOrCreatePublicLinkShareJob->run(); } // Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599 @@ -1116,11 +1146,12 @@ void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *lis listener->sendMessage(QString("GET_STRINGS:END")); } -void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled) +void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag) { - auto record = fileData.journalRecord(); - bool isOnTheServer = record.isValid(); - auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:"); + const auto record = fileData.journalRecord(); + const auto isOnTheServer = record.isValid(); + const auto isSecureFileDropSupported = rootE2eeFolderFlag == SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder && fileData.folder->accountState()->account()->secureFileDropSupported(); + const auto flagString = isOnTheServer && (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem || isSecureFileDropSupported) ? QLatin1String("::") : QLatin1String(":d:"); auto capabilities = fileData.folder->accountState()->account()->capabilities(); auto theme = Theme::instance(); @@ -1148,13 +1179,23 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi && !capabilities.sharePublicLinkEnforcePassword(); if (canCreateDefaultPublicLink) { - listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link")); + if (isSecureFileDropSupported) { + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_SECUREFILEDROP_LINK") + QLatin1String("::") + tr("Copy secure filedrop link")); + } else { + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link")); + } } else if (publicLinksEnabled) { - listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link")); + if (isSecureFileDropSupported) { + listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + QLatin1String("::") + tr("Copy secure filedrop link")); + } else { + listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link")); + } } } - listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link")); + if (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem) { + listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link")); + } // Disabled: only providing email option for private links would look odd, // and the copy option is more general. @@ -1312,6 +1353,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe const auto record = fileData.journalRecord(); const bool isOnTheServer = record.isValid(); const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty(); + const auto isE2eEncryptedRootFolder = fileData.journalRecord()._isE2eEncrypted && fileData.journalRecord()._e2eMangledName.isEmpty(); auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:"); const QFileInfo fileInfo(fileData.localPath); @@ -1331,7 +1373,9 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe sendEncryptFolderCommandMenuEntries(fileInfo, fileData, isE2eEncryptedPath, listener); sendLockFileCommandMenuEntries(fileInfo, syncFolder, fileData, listener); - sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath); + const auto itemEncryptionFlag = isE2eEncryptedPath ? SharingContextItemEncryptedFlag::EncryptedItem : SharingContextItemEncryptedFlag::NotEncryptedItem; + const auto rootE2eeFolderFlag = isE2eEncryptedRootFolder ? SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder : SharingContextItemRootEncryptedFolderFlag::NonRootEncryptedFolder; + sendSharingContextMenuOptions(fileData, listener, itemEncryptionFlag, rootE2eeFolderFlag); // Conflict files get conflict resolution actions bool isConflict = Utility::isConflictFile(fileData.folderRelativePath); diff --git a/src/gui/socketapi/socketapi.h b/src/gui/socketapi/socketapi.h index 22ce587ca..afa5ef965 100644 --- a/src/gui/socketapi/socketapi.h +++ b/src/gui/socketapi/socketapi.h @@ -51,6 +51,16 @@ class SocketApi : public QObject { Q_OBJECT + enum SharingContextItemEncryptedFlag { + EncryptedItem, + NotEncryptedItem + }; + + enum SharingContextItemRootEncryptedFolderFlag { + RootEncryptedFolder, + NonRootEncryptedFolder + }; + public: explicit SocketApi(QObject *parent = nullptr); ~SocketApi() override; @@ -119,6 +129,7 @@ private: Q_INVOKABLE void command_SHARE(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_LEAVESHARE(const QString &localFile, SocketListener *listener); Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, OCC::SocketListener *listener); + Q_INVOKABLE void command_COPY_SECUREFILEDROP_LINK(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener); Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener); @@ -151,7 +162,7 @@ private: Q_INVOKABLE void command_GET_STRINGS(const QString &argument, OCC::SocketListener *listener); // Sends the context menu options relating to sharing to listener - void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled); + void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag); void sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo, const FileData &fileData, diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 8be29f9c1..a6ccb6d4f 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -99,6 +99,8 @@ set(libsync_SRCS syncoptions.cpp theme.h theme.cpp + updatefiledropmetadata.h + updatefiledropmetadata.cpp clientsideencryption.h clientsideencryption.cpp clientsideencryptionjobs.h diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index f9f343787..9cc5e1106 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -696,6 +696,17 @@ bool Account::serverVersionUnsupported() const NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH); } +bool Account::secureFileDropSupported() const +{ + if (serverVersionInt() == 0) { + // not detected yet, assume it is fine. + return true; + } + return serverVersionInt() >= makeServerVersion(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR, + NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR, + NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH); +} + bool Account::isUsernamePrefillSupported() const { return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersionMinSupportedMajor, 0, 0); diff --git a/src/libsync/account.h b/src/libsync/account.h index b752fff13..87f685dfe 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -259,6 +259,8 @@ public: */ [[nodiscard]] bool serverVersionUnsupported() const; + [[nodiscard]] bool secureFileDropSupported() const; + [[nodiscard]] bool isUsernamePrefillSupported() const; [[nodiscard]] bool isChecksumRecalculateRequestSupported() const; diff --git a/src/libsync/bulkpropagatorjob.cpp b/src/libsync/bulkpropagatorjob.cpp index 330b17fbb..f414c73f3 100644 --- a/src/libsync/bulkpropagatorjob.cpp +++ b/src/libsync/bulkpropagatorjob.cpp @@ -106,7 +106,7 @@ bool BulkPropagatorJob::scheduleSelfOrChild() return _items.empty() && _filesToUpload.empty(); } -PropagatorJob::JobParallelism BulkPropagatorJob::parallelism() +PropagatorJob::JobParallelism BulkPropagatorJob::parallelism() const { return PropagatorJob::JobParallelism::FullParallelism; } diff --git a/src/libsync/bulkpropagatorjob.h b/src/libsync/bulkpropagatorjob.h index 3daf9da26..451456e97 100644 --- a/src/libsync/bulkpropagatorjob.h +++ b/src/libsync/bulkpropagatorjob.h @@ -64,7 +64,7 @@ public: bool scheduleSelfOrChild() override; - JobParallelism parallelism() override; + [[nodiscard]] JobParallelism parallelism() const override; private slots: void startUploadFile(OCC::SyncFileItemPtr item, OCC::BulkPropagatorJob::UploadFileInfo fileToUpload); diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index c527c997e..1eb7a9bd9 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -24,7 +24,6 @@ #include #include #include -#include #include #include #include @@ -1534,6 +1533,8 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit(); QJsonObject files = metaDataDoc.object()["files"].toObject(); + _fileDrop = metaDataDoc.object().value("filedrop").toObject(); + QJsonDocument debugHelper; debugHelper.setObject(metadataKeys); qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact); @@ -1546,7 +1547,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) * We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC * Now we should be compatible with Android and IOS. Maybe we can fix it later. */ - QByteArray b64DecryptedKey = decryptMetadataKey(currB64Pass); + QByteArray b64DecryptedKey = decryptData(currB64Pass); if (b64DecryptedKey.isEmpty()) { qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key(); continue; @@ -1615,7 +1616,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. -QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const +QByteArray FolderMetadata::encryptData(const QByteArray& data) const { Bio publicKeyBio; QByteArray publicKeyPem = _account->e2e()->_publicKey.toPem(); @@ -1626,7 +1627,7 @@ QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64()); } -QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadata) const +QByteArray FolderMetadata::decryptData(const QByteArray &data) const { Bio privateKeyBio; QByteArray privateKeyPem = _account->e2e()->_privateKey; @@ -1634,8 +1635,7 @@ QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadat auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio); // Also base64 decode the result - QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric( - key, QByteArray::fromBase64(encryptedMetadata)); + QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data)); if (decryptResult.isEmpty()) { @@ -1672,7 +1672,7 @@ void FolderMetadata::setupEmptyMetadata() { _sharing.append({displayName, publicKey}); } -QByteArray FolderMetadata::encryptedMetadata() { +QByteArray FolderMetadata::encryptedMetadata() const { qCDebug(lcCse) << "Generating metadata"; if (_metadataKeys.isEmpty()) { @@ -1686,7 +1686,7 @@ QByteArray FolderMetadata::encryptedMetadata() { * We have to already base64 encode the metadatakey here. This was a misunderstanding in the RFC * Now we should be compatible with Android and IOS. Maybe we can fix it later. */ - const QByteArray encryptedKey = encryptMetadataKey(it.value().toBase64()); + const QByteArray encryptedKey = encryptData(it.value().toBase64()); metadataKeys.insert(QString::number(it.key()), QString(encryptedKey)); } @@ -1761,6 +1761,52 @@ QVector FolderMetadata::files() const { return _files; } +bool FolderMetadata::isFileDropPresent() const +{ + return _fileDrop.size() > 0; +} + +bool FolderMetadata::moveFromFileDropToFiles() +{ + if (_fileDrop.isEmpty()) { + return false; + } + + for (auto it = _fileDrop.constBegin(); it != _fileDrop.constEnd(); ++it) { + const auto fileObject = it.value().toObject(); + + const auto encryptedFile = fileObject["encrypted"].toString().toLocal8Bit(); + const auto decryptedFile = decryptData(encryptedFile); + const auto decryptedFileDocument = QJsonDocument::fromJson(decryptedFile); + const auto decryptedFileObject = decryptedFileDocument.object(); + + EncryptedFile file; + file.encryptedFilename = it.key(); + file.metadataKey = fileObject["metadataKey"].toInt(); + file.authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit()); + file.initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit()); + + file.originalFilename = decryptedFileObject["filename"].toString(); + file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit()); + file.mimetype = decryptedFileObject["mimetype"].toString().toLocal8Bit(); + file.fileVersion = decryptedFileObject["version"].toInt(); + + // In case we wrongly stored "inode/directory" we try to recover from it + if (file.mimetype == QByteArrayLiteral("inode/directory")) { + file.mimetype = QByteArrayLiteral("httpd/unix-directory"); + } + + _files.push_back(file); + } + + return true; +} + +QJsonObject FolderMetadata::fileDrop() const +{ + return _fileDrop; +} + bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag) { if (!input->open(QIODevice::ReadOnly)) { diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index d0109f968..4cbe776b1 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -188,13 +189,18 @@ struct EncryptedFile { class OWNCLOUDSYNC_EXPORT FolderMetadata { public: FolderMetadata(AccountPtr account, const QByteArray& metadata = QByteArray(), int statusCode = -1); - QByteArray encryptedMetadata(); + [[nodiscard]] QByteArray encryptedMetadata() const; void addEncryptedFile(const EncryptedFile& f); void removeEncryptedFile(const EncryptedFile& f); void removeAllEncryptedFiles(); [[nodiscard]] QVector files() const; [[nodiscard]] bool isMetadataSetup() const; + [[nodiscard]] bool isFileDropPresent() const; + + [[nodiscard]] bool moveFromFileDropToFiles(); + + [[nodiscard]] QJsonObject fileDrop() const; private: /* Use std::string and std::vector internally on this class @@ -203,8 +209,8 @@ private: void setupEmptyMetadata(); void setupExistingMetadata(const QByteArray& metadata); - [[nodiscard]] QByteArray encryptMetadataKey(const QByteArray& metadataKey) const; - [[nodiscard]] QByteArray decryptMetadataKey(const QByteArray& encryptedKey) const; + [[nodiscard]] QByteArray encryptData(const QByteArray &data) const; + [[nodiscard]] QByteArray decryptData(const QByteArray &data) const; [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; @@ -213,6 +219,7 @@ private: QMap _metadataKeys; AccountPtr _account; QVector> _sharing; + QJsonObject _fileDrop; }; } // namespace OCC diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index 843419fbc..6d4c56c09 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -293,8 +293,10 @@ bool LockEncryptFolderApiJob::finished() qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token; - const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token); - _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted); + if (!_publicKey.isNull()) { + const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token); + _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted); + } //TODO: Parse the token and submit. emit success(_fileId, token); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 963dc3867..e2c8a91d5 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -1848,6 +1848,10 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() _discoveryData->_currentlyActiveJobs++; _pendingAsyncJobs++; connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) { + if (_dirItem) { + _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); + qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true; + } _discoveryData->_currentlyActiveJobs--; _pendingAsyncJobs--; if (results) { diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 877703bfa..e2ec41d85 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -405,6 +405,11 @@ void DiscoverySingleDirectoryJob::abort() } } +bool DiscoverySingleDirectoryJob::isFileDropDetected() const +{ + return _isFileDropDetected; +} + static void propertyMapToRemoteInfo(const QMap &map, RemoteInfo &result) { for (auto it = map.constBegin(); it != map.constEnd(); ++it) { @@ -617,6 +622,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in Q_ASSERT(_subPath.startsWith('/')); const auto metadata = FolderMetadata(_account, json.toJson(QJsonDocument::Compact), statusCode); + _isFileDropDetected = metadata.isFileDropPresent(); const auto encryptedFiles = metadata.files(); const auto findEncryptedFile = [=](const QString &name) { diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index a34b3fbe4..61663925d 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -67,6 +67,7 @@ struct RemoteInfo int64_t sizeOfFolder = 0; bool isDirectory = false; bool isE2eEncrypted = false; + bool isFileDropDetected = false; QString e2eMangledName; bool sharedByMe = false; @@ -142,6 +143,7 @@ public: void setIsRootPath() { _isRootPath = true; } void start(); void abort(); + [[nodiscard]] bool isFileDropDetected() const; // This is not actually a network job, it is just a job signals: @@ -173,6 +175,7 @@ private: bool _isExternalStorage = false; // If this directory is e2ee bool _isE2eEncrypted = false; + bool _isFileDropDetected = false; // If set, the discovery will finish with an error int64_t _size = 0; QString _error; diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index 3dba706ce..033199ba3 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -83,8 +83,8 @@ void EncryptFolderJob::slotLockForEncryptionSuccess(const QByteArray &fileId, co { _folderToken = token; - FolderMetadata emptyMetadata(_account); - auto encryptedMetadata = emptyMetadata.encryptedMetadata(); + const FolderMetadata emptyMetadata(_account); + const auto encryptedMetadata = emptyMetadata.encryptedMetadata(); if (encryptedMetadata.isEmpty()) { //TODO: Mark the folder as unencrypted as the metadata generation failed. _errorString = tr("Could not generate the metadata for encryption, Unlocking the folder.\n" diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 70860b4ac..f73c82ccc 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -22,6 +22,7 @@ #include "propagateremotemove.h" #include "propagateremotemkdir.h" #include "bulkpropagatorjob.h" +#include "updatefiledropmetadata.h" #include "propagatorjobs.h" #include "filesystem.h" #include "common/utility.h" @@ -584,7 +585,7 @@ void OwncloudPropagator::start(SyncFileItemVector &&items) directoriesToRemove, removedDirectory, items); - } else { + } else if (!directories.top().second->_item->_isFileDropDetected) { startFilePropagation(item, directories, directoriesToRemove, @@ -645,6 +646,11 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, const auto currentDirJob = directories.top().second; currentDirJob->appendJob(directoryPropagationJob.get()); } + if (item->_isFileDropDetected) { + directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); + item->_instruction = CSYNC_INSTRUCTION_NONE; + _anotherSyncNeeded = true; + } directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release())); } @@ -1066,7 +1072,7 @@ OwncloudPropagator *PropagatorJob::propagator() const // ================================================================================ -PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism() +PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism() const { // If any of the running sub jobs is not parallel, we have to wait for (int i = 0; i < _runningJobs.count(); ++i) { @@ -1215,7 +1221,7 @@ PropagateDirectory::PropagateDirectory(OwncloudPropagator *propagator, const Syn connect(&_subJobs, &PropagatorJob::finished, this, &PropagateDirectory::slotSubJobsFinished); } -PropagatorJob::JobParallelism PropagateDirectory::parallelism() +PropagatorJob::JobParallelism PropagateDirectory::parallelism() const { // If any of the non-finished sub jobs is not parallel, we have to wait if (_firstJob && _firstJob->parallelism() != FullParallelism) { @@ -1330,7 +1336,7 @@ PropagateRootDirectory::PropagateRootDirectory(OwncloudPropagator *propagator) connect(&_dirDeletionJobs, &PropagatorJob::finished, this, &PropagateRootDirectory::slotDirDeletionJobsFinished); } -PropagatorJob::JobParallelism PropagateRootDirectory::parallelism() +PropagatorJob::JobParallelism PropagateRootDirectory::parallelism() const { // the root directory parallelism isn't important return WaitForFinished; diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index e1754a63a..2bdc09b63 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -62,7 +62,7 @@ class PropagatorCompositeJob; * * @ingroup libsync */ -class PropagatorJob : public QObject +class OWNCLOUDSYNC_EXPORT PropagatorJob : public QObject { Q_OBJECT @@ -98,7 +98,7 @@ public: Q_ENUM(JobParallelism) - virtual JobParallelism parallelism() { return FullParallelism; } + [[nodiscard]] virtual JobParallelism parallelism() const { return FullParallelism; } /** * For "small" jobs @@ -215,7 +215,7 @@ public: return true; } - JobParallelism parallelism() override { return _parallelism; } + [[nodiscard]] JobParallelism parallelism() const override { return _parallelism; } SyncFileItemPtr _item; @@ -254,7 +254,7 @@ public: } bool scheduleSelfOrChild() override; - JobParallelism parallelism() override; + [[nodiscard]] JobParallelism parallelism() const override; /* * Abort synchronously or asynchronously - some jobs @@ -320,7 +320,7 @@ public: } bool scheduleSelfOrChild() override; - JobParallelism parallelism() override; + [[nodiscard]] JobParallelism parallelism() const override; void abort(PropagatorJob::AbortType abortType) override { if (_firstJob) @@ -366,7 +366,7 @@ public: explicit PropagateRootDirectory(OwncloudPropagator *propagator); bool scheduleSelfOrChild() override; - JobParallelism parallelism() override; + [[nodiscard]] JobParallelism parallelism() const override; void abort(PropagatorJob::AbortType abortType) override; [[nodiscard]] qint64 committedDiskSpace() const override; diff --git a/src/libsync/propagateremotemove.h b/src/libsync/propagateremotemove.h index 64ee0842e..cc4311839 100644 --- a/src/libsync/propagateremotemove.h +++ b/src/libsync/propagateremotemove.h @@ -57,7 +57,7 @@ public: } void start() override; void abort(PropagatorJob::AbortType abortType) override; - JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; } + [[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; } /** * Rename the directory in the selective sync list diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 41bf8e8ae..24f11ed7c 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -111,8 +111,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataError(const QByteArray Q_UNUSED(fileId); Q_UNUSED(httpReturnCode); qCDebug(lcPropagateUploadEncrypted()) << "Error Getting the encrypted metadata. Pretend we got empty metadata."; - FolderMetadata emptyMetadata(_propagator->account()); - emptyMetadata.encryptedMetadata(); + const FolderMetadata emptyMetadata(_propagator->account()); auto json = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata()); slotFolderEncryptedMetadataReceived(json, httpReturnCode); } diff --git a/src/libsync/propagatorjobs.h b/src/libsync/propagatorjobs.h index 28cad7f17..d5e2148c1 100644 --- a/src/libsync/propagatorjobs.h +++ b/src/libsync/propagatorjobs.h @@ -87,7 +87,7 @@ class PropagateLocalRename : public PropagateItemJob public: PropagateLocalRename(OwncloudPropagator *propagator, const SyncFileItemPtr &item); void start() override; - JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; } + [[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; } private: bool deleteOldDbRecord(const QString &fileName); diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index e517ee362..3890e339f 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -317,6 +317,8 @@ public: time_t _lastShareStateFetchedTimestamp = 0; bool _sharedByMe = false; + + bool _isFileDropDetected = false; }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/src/libsync/updatefiledropmetadata.cpp b/src/libsync/updatefiledropmetadata.cpp new file mode 100644 index 000000000..0d23f2f70 --- /dev/null +++ b/src/libsync/updatefiledropmetadata.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#include "updatefiledropmetadata.h" + +#include "account.h" +#include "clientsideencryptionjobs.h" +#include "clientsideencryption.h" +#include "syncfileitem.h" + +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcUpdateFileDropMetadataJob, "nextcloud.sync.propagator.updatefiledropmetadatajob", QtInfoMsg) + +} + +namespace OCC { + +UpdateFileDropMetadataJob::UpdateFileDropMetadataJob(OwncloudPropagator *propagator, const QString &path) + : PropagatorJob(propagator) + , _path(path) +{ +} + +void UpdateFileDropMetadataJob::start() +{ + qCDebug(lcUpdateFileDropMetadataJob) << "Folder is encrypted, let's get the Id from it."; + const auto fetchFolderEncryptedIdJob = new LsColJob(propagator()->account(), _path, this); + fetchFolderEncryptedIdJob->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"}); + connect(fetchFolderEncryptedIdJob, &LsColJob::directoryListingSubfolders, this, &UpdateFileDropMetadataJob::slotFolderEncryptedIdReceived); + connect(fetchFolderEncryptedIdJob, &LsColJob::finishedWithError, this, &UpdateFileDropMetadataJob::slotFolderEncryptedIdError); + fetchFolderEncryptedIdJob->start(); +} + +bool UpdateFileDropMetadataJob::scheduleSelfOrChild() +{ + if (_state == Finished) { + return false; + } + + if (_state == NotYetStarted) { + _state = Running; + start(); + } + + return true; +} + +PropagatorJob::JobParallelism UpdateFileDropMetadataJob::parallelism() const +{ + return PropagatorJob::JobParallelism::WaitForFinished; +} + +void UpdateFileDropMetadataJob::slotFolderEncryptedIdReceived(const QStringList &list) +{ + qCDebug(lcUpdateFileDropMetadataJob) << "Received id of folder, trying to lock it so we can prepare the metadata"; + const auto fetchFolderEncryptedIdJob = qobject_cast(sender()); + Q_ASSERT(fetchFolderEncryptedIdJob); + if (!fetchFolderEncryptedIdJob) { + qCCritical(lcUpdateFileDropMetadataJob) << "slotFolderEncryptedIdReceived must be called by a signal"; + emit finished(SyncFileItem::Status::FatalError); + return; + } + Q_ASSERT(!list.isEmpty()); + if (list.isEmpty()) { + qCCritical(lcUpdateFileDropMetadataJob) << "slotFolderEncryptedIdReceived list.isEmpty()"; + emit finished(SyncFileItem::Status::FatalError); + return; + } + const auto &folderInfo = fetchFolderEncryptedIdJob->_folderInfos.value(list.first()); + slotTryLock(folderInfo.fileId); +} + +void UpdateFileDropMetadataJob::slotTryLock(const QByteArray &fileId) +{ + const auto lockJob = new LockEncryptFolderApiJob(propagator()->account(), fileId, propagator()->_journal, propagator()->account()->e2e()->_publicKey, this); + connect(lockJob, &LockEncryptFolderApiJob::success, this, &UpdateFileDropMetadataJob::slotFolderLockedSuccessfully); + connect(lockJob, &LockEncryptFolderApiJob::error, this, &UpdateFileDropMetadataJob::slotFolderLockedError); + lockJob->start(); +} + +void UpdateFileDropMetadataJob::slotFolderLockedSuccessfully(const QByteArray &fileId, const QByteArray &token) +{ + qCDebug(lcUpdateFileDropMetadataJob) << "Folder" << fileId << "Locked Successfully for Upload, Fetching Metadata"; + _folderToken = token; + _folderId = fileId; + _isFolderLocked = true; + + const auto fetchMetadataJob = new GetMetadataApiJob(propagator()->account(), _folderId); + connect(fetchMetadataJob, &GetMetadataApiJob::jsonReceived, this, &UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived); + connect(fetchMetadataJob, &GetMetadataApiJob::error, this, &UpdateFileDropMetadataJob::slotFolderEncryptedMetadataError); + + fetchMetadataJob->start(); +} + +void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode) +{ + Q_UNUSED(fileId); + Q_UNUSED(httpReturnCode); + qCDebug(lcUpdateFileDropMetadataJob()) << "Error Getting the encrypted metadata. Pretend we got empty metadata."; + const FolderMetadata emptyMetadata(propagator()->account()); + const auto encryptedMetadataJson = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata()); + slotFolderEncryptedMetadataReceived(encryptedMetadataJson, httpReturnCode); +} + +void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) +{ + qCDebug(lcUpdateFileDropMetadataJob) << "Metadata Received, Preparing it for the new file." << json.toVariant(); + + // Encrypt File! + _metadata.reset(new FolderMetadata(propagator()->account(), json.toJson(QJsonDocument::Compact), statusCode)); + if (!_metadata->moveFromFileDropToFiles()) { + unlockFolder(); + return; + } + + emit fileDropMetadataParsedAndAdjusted(_metadata.data()); + + const auto updateMetadataJob = new UpdateMetadataApiJob(propagator()->account(), _folderId, _metadata->encryptedMetadata(), _folderToken); + connect(updateMetadataJob, &UpdateMetadataApiJob::success, this, &UpdateFileDropMetadataJob::slotUpdateMetadataSuccess); + connect(updateMetadataJob, &UpdateMetadataApiJob::error, this, &UpdateFileDropMetadataJob::slotUpdateMetadataError); + updateMetadataJob->start(); +} + +void UpdateFileDropMetadataJob::slotUpdateMetadataSuccess(const QByteArray &fileId) +{ + Q_UNUSED(fileId); + qCDebug(lcUpdateFileDropMetadataJob) << "Uploading of the metadata success, Encrypting the file"; + + qCDebug(lcUpdateFileDropMetadataJob) << "Finalizing the upload part, now the actuall uploader will take over"; + unlockFolder(); +} + +void UpdateFileDropMetadataJob::slotUpdateMetadataError(const QByteArray &fileId, int httpErrorResponse) +{ + qCDebug(lcUpdateFileDropMetadataJob) << "Update metadata error for folder" << fileId << "with error" << httpErrorResponse; + qCDebug(lcUpdateFileDropMetadataJob()) << "Unlocking the folder."; + unlockFolder(); +} + +void UpdateFileDropMetadataJob::slotFolderLockedError(const QByteArray &fileId, int httpErrorCode) +{ + Q_UNUSED(httpErrorCode); + qCDebug(lcUpdateFileDropMetadataJob) << "Folder" << fileId << "with path" << _path << "Coundn't be locked. httpErrorCode" << httpErrorCode; + emit finished(SyncFileItem::Status::NormalError); +} + +void UpdateFileDropMetadataJob::slotFolderEncryptedIdError(QNetworkReply *reply) +{ + if (!reply) { + qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path; + } else { + qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path << "with httpErrorCode" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + } + emit finished(SyncFileItem::Status::NormalError); +} + +void UpdateFileDropMetadataJob::unlockFolder() +{ + Q_ASSERT(!_isUnlockRunning); + + if (!_isFolderLocked) { + emit finished(SyncFileItem::Status::Success); + return; + } + + if (_isUnlockRunning) { + qCWarning(lcUpdateFileDropMetadataJob) << "Double-call to unlockFolder."; + return; + } + + _isUnlockRunning = true; + + qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock"; + const auto unlockJob = new UnlockEncryptFolderApiJob(propagator()->account(), _folderId, _folderToken, propagator()->_journal, this); + + connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) { + qCDebug(lcUpdateFileDropMetadataJob) << "Successfully Unlocked"; + _folderToken = ""; + _folderId = ""; + _isFolderLocked = false; + + emit folderUnlocked(folderId, 200); + _isUnlockRunning = false; + emit finished(SyncFileItem::Status::Success); + }); + connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) { + qCDebug(lcUpdateFileDropMetadataJob) << "Unlock Error"; + + emit folderUnlocked(folderId, httpStatus); + _isUnlockRunning = false; + emit finished(SyncFileItem::Status::NormalError); + }); + unlockJob->start(); +} + + +} diff --git a/src/libsync/updatefiledropmetadata.h b/src/libsync/updatefiledropmetadata.h new file mode 100644 index 000000000..0f0fa1b3d --- /dev/null +++ b/src/libsync/updatefiledropmetadata.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) by Oleksandr Zolotov + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#pragma once + +#include "owncloudpropagator.h" + +#include + +class QNetworkReply; + +namespace OCC { + +class FolderMetadata; + +class OWNCLOUDSYNC_EXPORT UpdateFileDropMetadataJob : public PropagatorJob +{ + Q_OBJECT + +public: + explicit UpdateFileDropMetadataJob(OwncloudPropagator *propagator, const QString &path); + + bool scheduleSelfOrChild() override; + + [[nodiscard]] JobParallelism parallelism() const override; + +private slots: + void start(); + void slotFolderEncryptedIdReceived(const QStringList &list); + void slotFolderEncryptedIdError(QNetworkReply *reply); + void slotFolderLockedSuccessfully(const QByteArray &fileId, const QByteArray &token); + void slotFolderLockedError(const QByteArray &fileId, int httpErrorCode); + void slotTryLock(const QByteArray &fileId); + void slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode); + void slotFolderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode); + void slotUpdateMetadataSuccess(const QByteArray &fileId); + void slotUpdateMetadataError(const QByteArray &fileId, int httpReturnCode); + void unlockFolder(); + +signals: + void folderUnlocked(const QByteArray &folderId, int httpStatus); + + void fileDropMetadataParsedAndAdjusted(const FolderMetadata *const metadata); + +private: + QString _path; + bool _currentLockingInProgress = false; + + bool _isUnlockRunning = false; + bool _isFolderLocked = false; + + QByteArray _folderToken; + QByteArray _folderId; + + QScopedPointer _metadata; +}; + +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 972f40d6d..0718822dc 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -69,6 +69,12 @@ nextcloud_add_test(LockFile) nextcloud_add_test(ShareModel) nextcloud_add_test(ShareeModel) nextcloud_add_test(SortedShareModel) +nextcloud_add_test(SecureFileDrop) + +target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync) +configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY) +configure_file(fakefiledrope2eefoldermetadata.json "${PROJECT_BINARY_DIR}/bin/fakefiledrope2eefoldermetadata.json" COPYONLY) + if(ADD_E2E_TESTS) nextcloud_add_test(E2eServerSetup) diff --git a/test/fake2eelocksucceeded.json b/test/fake2eelocksucceeded.json new file mode 100644 index 000000000..eb1439ff4 --- /dev/null +++ b/test/fake2eelocksucceeded.json @@ -0,0 +1,10 @@ +{ + "ocs": { + "data": { "e2e-token": "U1SHqQwKzjEIlJUkFIcpYPJeZsM80T6OkegKFu2pSc6BFqORcGfB0Y8PZzRjc6Lm" }, + "meta": { + "message": "OK", + "status": "ok", + "statuscode": 200 + } + } +} diff --git a/test/fakefiledrope2eefoldermetadata.json b/test/fakefiledrope2eefoldermetadata.json new file mode 100644 index 000000000..d66444406 --- /dev/null +++ b/test/fakefiledrope2eefoldermetadata.json @@ -0,0 +1,10 @@ +{ + "ocs": { + "data": { "meta-data": "{\"files\":{\"083c2bffea5d4b0f824e2fd5df5369d2\":{\"authenticationTag\":\"LRWVv0vR01WUhrv26kGvOg==\",\"encrypted\":\"dUlVcf+8xaMgIxkWd7YYIsYLIotqD3ZjQtES1VsepKa7+aUYJGdNlPT25+CTQl65mY5ggu9d03hiysiBIUO7BH7klyUY9OQM80kGVE1xuWXQ1aCfgiFruN4h1VSS8S/9jrgBojxncBnsGZjU/NOGZUjA1svdE2hM+O4fywPKUyT09an9t2EbqUGgUl242ezJ|ja9flmYfZAl/MUjF11chaA==\",\"initializationVector\":\"vNfZNAVYVs0eGdB5vbo5KA==\",\"metadataKey\":0}},\"metadata\":{\"metadataKeys\":{\"0\":\"GHKkcNTxsyigJODA45neTO+8Y0NDfB+7mez90EwjW39mZvNnCUBeGO2R6vLzEf5apYjDNsWNS5sHvUZ188OLa9zCDmMm00m8dwfMPEUA0H5Rp9yewUbnM8YRl6vCZWvDa5HLTCdC8UCIKsbvuifAvveQXEO/vafzWrP8IAhG6WsNXZ4qqaUX/0pm84KXvHmStH60xpZpT8U/kKBNdxDeOTdp5T6FglRTnsF9wt9cplPtRHV+BzkC5NgfBqAvLVSP8gckj5iJNQrRM7IQmBPO0AMUv9yU609o7X4WUZ4LwNGqwL6ciCzS83BQ+FCEbf4HyViEWrEq2OLVFgDH7ML18Q==\"},\"version\":1},\"filedrop\":{\"1a1e95ae836b4005bf69e369661e81ba\":{\"encrypted\":\"r7o01Y3dBQbOlhR38ulPW77X0aoGS8riwJmnRp0k0fQgfySy5++GJkaTJqSdcQYvw8stn+hU7j6wwOJMA/aJ3UV5i8H7yPR+RQHiKjrU4L379HU+H1Xuu5KbkhbLoOc7GAxaCpyC0USYI4UCUcmKnSpqAhpHdnpScmyYztA6qnfumclIzgSfE87lCRRFnIp4mKm305hD+4gBuk3WfERewqbgK2Yo08sFhOjR6zULrJgqQBbHc61R7TZb18H26u0fa+mjRmehTSiqhy0dDePip7sr8+a0dCNCEcBG8qKK9xPDYCFmDrAq7iaEpcRoZ4CgUavXOSF+zdunBXXWmKTq6w==\",\"initializationVector\":\"hel7omho/1XsYqov8XCXgA==\",\"authenticationTag\":\"ni6/k9UpFEkS5FYoMPU+/g==\",\"metadataKey\":0}}}" }, + "meta": { + "message": "OK", + "status": "ok", + "statuscode": 200 + } + } +} diff --git a/test/syncenginetestutils.cpp b/test/syncenginetestutils.cpp index 2bf6430a5..b5d9beaed 100644 --- a/test/syncenginetestutils.cpp +++ b/test/syncenginetestutils.cpp @@ -1014,6 +1014,11 @@ QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData, QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { + if (op == QNetworkAccessManager::CustomOperation) { + qInfo() << "Operation" << request.attribute(QNetworkRequest::CustomVerbAttribute).toString() << request.url(); + } else { + qInfo() << "Operation" << op << request.url(); + } QNetworkReply *reply = nullptr; auto newRequest = request; newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId()); diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp new file mode 100644 index 000000000..33653b800 --- /dev/null +++ b/test/testsecurefiledrop.cpp @@ -0,0 +1,168 @@ +/* + * This software is in the public domain, furnished "as is", without technical + * support, and with no warranty, express or implied, as to its usefulness for + * any purpose. + * + */ + +#include "updatefiledropmetadata.h" +#include "syncengine.h" +#include "syncenginetestutils.h" +#include "testhelper.h" +#include "owncloudpropagator_p.h" +#include "propagatorjobs.h" +#include "clientsideencryption.h" + +#include + +namespace +{ + constexpr auto fakeE2eeFolderName = "fake_e2ee_folder"; + const QString fakeE2eeFolderPath = QStringLiteral("/") + fakeE2eeFolderName; + }; + +using namespace OCC; + +class TestSecureFileDrop : public QObject +{ + Q_OBJECT + + FakeFolder _fakeFolder{FileInfo()}; + QSharedPointer _propagator; + QScopedPointer _parsedMetadataWithFileDrop; + QScopedPointer _parsedMetadataAfterProcessingFileDrop; + + int _lockCallsCount = 0; + int _unlockCallsCount = 0; + int _propFindCallsCount = 0; + int _getMetadataCallsCount = 0; + int _putMetadataCallsCount = 0; + +private slots: + void initTestCase() + { + _fakeFolder.remoteModifier().mkdir(fakeE2eeFolderName); + _fakeFolder.remoteModifier().insert(fakeE2eeFolderName + QStringLiteral("/") + QStringLiteral("fake_e2ee_file"), 100); + _fakeFolder.setServerOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) { + Q_UNUSED(device); + QNetworkReply *reply = nullptr; + + const auto path = req.url().path(); + + if (path.contains(QStringLiteral("/end_to_end_encryption/api/v1/lock/"))) { + if (op == QNetworkAccessManager::DeleteOperation) { + reply = new FakePayloadReply(op, req, {}, nullptr); + ++_unlockCallsCount; + } else if (op == QNetworkAccessManager::PostOperation) { + QFile fakeJsonReplyFile(QStringLiteral("fake2eelocksucceeded.json")); + if (fakeJsonReplyFile.open(QFile::ReadOnly)) { + const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll()); + reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr); + ++_lockCallsCount; + } else { + qCritical() << "Could not open fake JSON file!"; + reply = new FakePayloadReply(op, req, {}, nullptr); + } + } + } else if (path.contains(QStringLiteral("/end_to_end_encryption/api/v1/meta-data/"))) { + if (op == QNetworkAccessManager::GetOperation) { + QFile fakeJsonReplyFile(QStringLiteral("fakefiledrope2eefoldermetadata.json")); + if (fakeJsonReplyFile.open(QFile::ReadOnly)) { + const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll()); + _parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson())); + _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson())); + [[maybe_unused]] const auto result = _parsedMetadataAfterProcessingFileDrop->moveFromFileDropToFiles(); + reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr); + ++_getMetadataCallsCount; + } else { + qCritical() << "Could not open fake JSON file!"; + reply = new FakePayloadReply(op, req, {}, nullptr); + } + } else if (op == QNetworkAccessManager::PutOperation) { + reply = new FakePayloadReply(op, req, {}, nullptr); + ++_putMetadataCallsCount; + } + } else if (req.attribute(QNetworkRequest::CustomVerbAttribute) == QStringLiteral("PROPFIND") && path.endsWith(fakeE2eeFolderPath)) { + auto fileState = _fakeFolder.currentRemoteState(); + reply = new FakePropfindReply(fileState, op, req, nullptr); + ++_propFindCallsCount; + } + + return reply; + }); + + auto transProgress = connect(&_fakeFolder.syncEngine(), &SyncEngine::transmissionProgress, [&](const ProgressInfo &pi) { + Q_UNUSED(pi); + _propagator = _fakeFolder.syncEngine().getPropagator(); + }); + + QVERIFY(_fakeFolder.syncOnce()); + + disconnect(transProgress); + }; + + void testUpdateFileDropMetadata() + { + const auto updateFileDropMetadataJob = new UpdateFileDropMetadataJob(_propagator.data(), fakeE2eeFolderPath); + connect(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::fileDropMetadataParsedAndAdjusted, this, [this](const FolderMetadata *const metadata) { + if (!metadata || metadata->files().isEmpty() || metadata->fileDrop().isEmpty()) { + return; + } + + if (_parsedMetadataAfterProcessingFileDrop->files().size() != metadata->files().size()) { + return; + } + + if (_parsedMetadataAfterProcessingFileDrop->fileDrop() != metadata->fileDrop()) { + return; + } + + bool isAnyFileDropFileMissing = false; + + for (const auto &key : metadata->fileDrop().keys()) { + if (std::find_if(metadata->files().constBegin(), metadata->files().constEnd(), [&key](const EncryptedFile &encryptedFile) { + return encryptedFile.encryptedFilename == key; + }) == metadata->files().constEnd()) { + isAnyFileDropFileMissing = true; + } + } + + if (!isAnyFileDropFileMissing) { + emit fileDropMetadataParsedAndAdjusted(); + } + }); + QSignalSpy updateFileDropMetadataJobSpy(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::finished); + QSignalSpy fileDropMetadataParsedAndAdjustedSpy(this, &TestSecureFileDrop::fileDropMetadataParsedAndAdjusted); + + QVERIFY(updateFileDropMetadataJob->scheduleSelfOrChild()); + + QVERIFY(updateFileDropMetadataJobSpy.wait(3000)); + + QVERIFY(_parsedMetadataWithFileDrop); + QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent()); + + QVERIFY(_parsedMetadataAfterProcessingFileDrop); + + QVERIFY(_parsedMetadataAfterProcessingFileDrop->files().size() != _parsedMetadataWithFileDrop->files().size()); + + QVERIFY(!updateFileDropMetadataJobSpy.isEmpty()); + QVERIFY(!updateFileDropMetadataJobSpy.at(0).isEmpty()); + QCOMPARE(updateFileDropMetadataJobSpy.at(0).first().toInt(), SyncFileItem::Status::Success); + + QVERIFY(!fileDropMetadataParsedAndAdjustedSpy.isEmpty()); + + QCOMPARE(_lockCallsCount, 1); + QCOMPARE(_unlockCallsCount, 1); + QCOMPARE(_propFindCallsCount, 2); + QCOMPARE(_getMetadataCallsCount, 1); + QCOMPARE(_putMetadataCallsCount, 1); + + updateFileDropMetadataJob->deleteLater(); + } + +signals: + void fileDropMetadataParsedAndAdjusted(); +}; + +QTEST_GUILESS_MAIN(TestSecureFileDrop) +#include "testsecurefiledrop.moc" diff --git a/version.h.in b/version.h.in index c3a7975e5..9e508d937 100644 --- a/version.h.in +++ b/version.h.in @@ -41,4 +41,8 @@ constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_V constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR@; constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH@; +constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR@; +constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR@; +constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH@; + #endif // VERSION_H