diff --git a/src/common/checksums.cpp b/src/common/checksums.cpp index 370730c3c3461..5787f2d26dd09 100644 --- a/src/common/checksums.cpp +++ b/src/common/checksums.cpp @@ -91,6 +91,21 @@ Q_LOGGING_CATEGORY(lcChecksums, "nextcloud.sync.checksums", QtInfoMsg) #define BUFSIZE qint64(500 * 1024) // 500 KiB +static QByteArray calcCryptoHash(const QByteArray &data, QCryptographicHash::Algorithm algo) +{ + if (data.isEmpty()) { + return {}; + } + QCryptographicHash crypto(algo); + crypto.addData(data); + return crypto.result().toHex(); +} + +QByteArray calcSha256(const QByteArray &data) +{ + return calcCryptoHash(data, QCryptographicHash::Sha256); +} + QByteArray makeChecksumHeader(const QByteArray &checksumType, const QByteArray &checksum) { if (checksumType.isEmpty() || checksum.isEmpty()) diff --git a/src/common/checksums.h b/src/common/checksums.h index bd13dc07fb82e..519825961d3ea 100644 --- a/src/common/checksums.h +++ b/src/common/checksums.h @@ -56,6 +56,8 @@ OCSYNC_EXPORT QByteArray parseChecksumHeaderType(const QByteArray &header); /// Checks OWNCLOUD_DISABLE_CHECKSUM_UPLOAD OCSYNC_EXPORT bool uploadChecksumEnabled(); +OCSYNC_EXPORT QByteArray calcSha256(const QByteArray &data); + /** * Computes the checksum of a file. * \ingroup libsync diff --git a/src/common/preparedsqlquerymanager.h b/src/common/preparedsqlquerymanager.h index beea7d74b1041..a5afa0fe66d93 100644 --- a/src/common/preparedsqlquerymanager.h +++ b/src/common/preparedsqlquerymanager.h @@ -107,6 +107,7 @@ class OCSYNC_EXPORT PreparedSqlQueryManager GetE2EeLockedFolderQuery, GetE2EeLockedFoldersQuery, DeleteE2EeLockedFolderQuery, + ListAllTopLevelE2eeFoldersStatusLessThanQuery, PreparedQueryCount }; diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index e8c1f55619194..895ad115c4d81 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -1030,6 +1030,108 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & return {}; } +bool SyncJournalDb::getRootE2eFolderRecord(const QString &remoteFolderPath, SyncJournalFileRecord *rec) +{ + Q_ASSERT(rec); + rec->_path.clear(); + Q_ASSERT(!rec->isValid()); + + Q_ASSERT(!remoteFolderPath.isEmpty()); + + Q_ASSERT(!remoteFolderPath.isEmpty() && remoteFolderPath != QStringLiteral("/")); + if (remoteFolderPath.isEmpty() || remoteFolderPath == QStringLiteral("/")) { + qCWarning(lcDb) << "Invalid folder path!"; + return false; + } + + auto remoteFolderPathSplit = remoteFolderPath.split(QLatin1Char('/'), Qt::SkipEmptyParts); + + if (remoteFolderPathSplit.isEmpty()) { + qCWarning(lcDb) << "Invalid folder path!"; + return false; + } + + while (!remoteFolderPathSplit.isEmpty()) { + const auto result = getFileRecord(remoteFolderPathSplit.join(QLatin1Char('/')), rec); + if (!result) { + return false; + } + if (rec->isE2eEncrypted() && rec->_e2eMangledName.isEmpty()) { + // it's a toplevel folder record + return true; + } + remoteFolderPathSplit.removeLast(); + } + + return true; +} + +bool SyncJournalDb::listAllE2eeFoldersWithEncryptionStatusLessThan(const int status, const std::function &rowCallback) +{ + QMutexLocker locker(&_mutex); + + if (_metadataTableIsEmpty) + return true; + + if (!checkConnect()) + return false; + const auto query = _queryManager.get(PreparedSqlQueryManager::ListAllTopLevelE2eeFoldersStatusLessThanQuery, + QByteArrayLiteral(GET_FILE_RECORD_QUERY " WHERE type == 2 AND isE2eEncrypted >= ?1 AND isE2eEncrypted < ?2 ORDER BY path||'/' ASC"), + _db); + if (!query) { + return false; + } + query->bindValue(1, SyncJournalFileRecord::EncryptionStatus::Encrypted); + query->bindValue(2, status); + + if (!query->exec()) + return false; + + forever { + auto next = query->next(); + if (!next.ok) + return false; + if (!next.hasData) + break; + + SyncJournalFileRecord rec; + fillFileRecordFromGetQuery(rec, *query); + + if (rec._type == ItemTypeSkip) { + continue; + } + + rowCallback(rec); + } + + return true; +} + +bool SyncJournalDb::findEncryptedAncestorForRecord(const QString &filename, SyncJournalFileRecord *rec) +{ + Q_ASSERT(rec); + rec->_path.clear(); + Q_ASSERT(!rec->isValid()); + + const auto slashPosition = filename.lastIndexOf(QLatin1Char('/')); + const auto parentPath = slashPosition >= 0 ? filename.left(slashPosition) : QString(); + + auto pathComponents = parentPath.split(QLatin1Char('/')); + while (!pathComponents.isEmpty()) { + const auto pathCompontentsJointed = pathComponents.join(QLatin1Char('/')); + if (!getFileRecord(pathCompontentsJointed, rec)) { + qCDebug(lcDb) << "could not get file from local DB" << pathCompontentsJointed; + return false; + } + + if (rec->isValid() && rec->isE2eEncrypted()) { + break; + } + pathComponents.removeLast(); + } + return true; +} + void SyncJournalDb::keyValueStoreSet(const QString &key, QVariant value) { QMutexLocker locker(&_mutex); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index 01cc1e39012be..ae4824a41766c 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -70,6 +70,9 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject [[nodiscard]] bool getFilesBelowPath(const QByteArray &path, const std::function &rowCallback); [[nodiscard]] bool listFilesInPath(const QByteArray &path, const std::function &rowCallback); [[nodiscard]] Result setFileRecord(const SyncJournalFileRecord &record); + [[nodiscard]] bool getRootE2eFolderRecord(const QString &remoteFolderPath, SyncJournalFileRecord *rec); + [[nodiscard]] bool listAllE2eeFoldersWithEncryptionStatusLessThan(const int status, const std::function &rowCallback); + [[nodiscard]] bool findEncryptedAncestorForRecord(const QString &filename, SyncJournalFileRecord *rec); void keyValueStoreSet(const QString &key, QVariant value); [[nodiscard]] qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue); diff --git a/src/csync/csync.h b/src/csync/csync.h index ca40aa6b4f406..235f0cd729afb 100644 --- a/src/csync/csync.h +++ b/src/csync/csync.h @@ -50,12 +50,13 @@ class SyncJournalFileRecord; namespace EncryptionStatusEnums { -Q_NAMESPACE +OCSYNC_EXPORT Q_NAMESPACE enum class ItemEncryptionStatus : int { NotEncrypted = 0, Encrypted = 1, EncryptedMigratedV1_2 = 2, + EncryptedMigratedV2_0 = 3, }; Q_ENUM_NS(ItemEncryptionStatus) @@ -65,6 +66,7 @@ enum class JournalDbEncryptionStatus : int { Encrypted = 1, EncryptedMigratedV1_2Invalid = 2, EncryptedMigratedV1_2 = 3, + EncryptedMigratedV2_0 = 4, }; Q_ENUM_NS(JournalDbEncryptionStatus) @@ -73,6 +75,8 @@ ItemEncryptionStatus fromDbEncryptionStatus(JournalDbEncryptionStatus encryption JournalDbEncryptionStatus toDbEncryptionStatus(ItemEncryptionStatus encryptionStatus); +ItemEncryptionStatus fromEndToEndEncryptionApiVersion(const double version); + } } diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 312e1fdf02e20..c7d85648c945b 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -425,7 +425,8 @@ void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInf Q_ASSERT(!path.startsWith('/') && path.endsWith('/')); // But EncryptFolderJob expects directory path Foo/Bar convention const auto choppedPath = path.chopped(1); - auto job = new OCC::EncryptFolderJob(accountsState()->account(), folder->journalDb(), choppedPath, fileId, this); + auto job = new OCC::EncryptFolderJob(accountsState()->account(), folder->journalDb(), choppedPath, fileId); + job->setParent(this); job->setProperty(propertyFolder, QVariant::fromValue(folder)); job->setProperty(propertyPath, QVariant::fromValue(path)); connect(job, &OCC::EncryptFolderJob::finished, this, &AccountSettings::slotEncryptFolderFinished); diff --git a/src/gui/filedetails/ShareView.qml b/src/gui/filedetails/ShareView.qml index 8622463abec4d..c4c58fde14910 100644 --- a/src/gui/filedetails/ShareView.qml +++ b/src/gui/filedetails/ShareView.qml @@ -146,7 +146,7 @@ ColumnLayout { Layout.rightMargin: root.horizontalPadding visible: root.userGroupSharingPossible - enabled: visible && !root.loading + enabled: visible && !root.loading && !root.shareModel.isShareDisabledEncryptedFolder && !shareeSearchField.isShareeFetchOngoing accountState: root.accountState shareItemIsFolder: root.fileDetails && root.fileDetails.isFolder diff --git a/src/gui/filedetails/ShareeSearchField.qml b/src/gui/filedetails/ShareeSearchField.qml index 9fbb144820ba0..300b9c36004b5 100644 --- a/src/gui/filedetails/ShareeSearchField.qml +++ b/src/gui/filedetails/ShareeSearchField.qml @@ -29,6 +29,7 @@ TextField { property var accountState: ({}) property bool shareItemIsFolder: false property var shareeBlocklist: ({}) + property bool isShareeFetchOngoing: shareeModel.fetchOngoing property ShareeModel shareeModel: ShareeModel { accountState: root.accountState shareItemIsFolder: root.shareItemIsFolder @@ -44,9 +45,8 @@ TextField { shareeListView.count > 0 ? suggestionsPopup.open() : suggestionsPopup.close(); } - placeholderText: qsTr("Search for users or groups…") + placeholderText: enabled ? qsTr("Search for users or groups…") : qsTr("Sharing is not available for this folder") placeholderTextColor: placeholderColor - enabled: !shareeModel.fetchOngoing onActiveFocusChanged: triggerSuggestionsVisibility() onTextChanged: triggerSuggestionsVisibility() diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index 418f5353d47e9..0f65454ddbcf0 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -25,6 +25,8 @@ #include "folderman.h" #include "sharepermissions.h" #include "theme.h" +#include "updatee2eefolderusersmetadatajob.h" +#include "wordlist.h" namespace { @@ -180,6 +182,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const || (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword())); case EditingAllowedRole: return share->getPermissions().testFlag(SharePermissionUpdate); + case ResharingAllowedRole: return share->getPermissions().testFlag(SharePermissionShare); @@ -204,7 +207,7 @@ void ShareModel::resetData() { beginResetModel(); - _folder = nullptr; + _synchronizationFolder = nullptr; _sharePath.clear(); _maxSharingPermissions = {}; _numericFileId.clear(); @@ -238,9 +241,9 @@ void ShareModel::updateData() return; } - _folder = FolderMan::instance()->folderForPath(_localPath); + _synchronizationFolder = FolderMan::instance()->folderForPath(_localPath); - if (!_folder) { + if (!_synchronizationFolder) { qCWarning(lcShareModel) << "Could not update share model data for" << _localPath << "no responsible folder found"; resetData(); return; @@ -248,13 +251,13 @@ void ShareModel::updateData() qCDebug(lcShareModel) << "Updating share model data now."; - const auto relPath = _localPath.mid(_folder->cleanPath().length() + 1); - _sharePath = _folder->remotePathTrailingSlash() + relPath; + const auto relPath = _localPath.mid(_synchronizationFolder->cleanPath().length() + 1); + _sharePath = _synchronizationFolder->remotePathTrailingSlash() + relPath; SyncJournalFileRecord fileRecord; auto resharingAllowed = true; // lets assume the good - if (_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid() && !fileRecord._remotePerm.isNull() + if (_synchronizationFolder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid() && !fileRecord._remotePerm.isNull() && !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) { qCInfo(lcShareModel) << "File record says resharing not allowed"; resharingAllowed = false; @@ -275,6 +278,14 @@ void ShareModel::updateData() _sharedItemType = fileRecord.isE2eEncrypted() ? SharedItemType::SharedItemTypeEncryptedFile : SharedItemType::SharedItemTypeFile; } + const auto prevIsShareDisabledEncryptedFolder = _isShareDisabledEncryptedFolder; + _isShareDisabledEncryptedFolder = fileRecord.isE2eEncrypted() + && (_sharedItemType != SharedItemType::SharedItemTypeEncryptedTopLevelFolder + || fileRecord._e2eEncryptionStatus < SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV2_0); + if (prevIsShareDisabledEncryptedFolder != _isShareDisabledEncryptedFolder) { + emit isShareDisabledEncryptedFolderChanged(); + } + // Will get added when shares are fetched if no link shares are fetched _placeholderLinkShare.reset(new Share(_accountState->account(), placeholderLinkShareId, @@ -435,14 +446,14 @@ void ShareModel::slotPropfindReceived(const QVariantMap &result) } const auto privateLinkUrl = result["privatelink"].toString(); - const auto numericFileId = result["fileid"].toByteArray(); + _fileRemoteId = result["fileid"].toByteArray(); if (!privateLinkUrl.isEmpty()) { qCInfo(lcShareModel) << "Received private link url for" << _sharePath << privateLinkUrl; _privateLinkUrl = privateLinkUrl; - } else if (!numericFileId.isEmpty()) { - qCInfo(lcShareModel) << "Received numeric file id for" << _sharePath << numericFileId; - _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(numericFileId).toString(QUrl::FullyEncoded); + } else if (!_fileRemoteId.isEmpty()) { + qCInfo(lcShareModel) << "Received numeric file id for" << _sharePath << _fileRemoteId; + _privateLinkUrl = _accountState->account()->deprecatedPrivateLinkUrl(_fileRemoteId).toString(QUrl::FullyEncoded); } setupInternalLinkShare(); @@ -825,6 +836,44 @@ void ShareModel::slotShareExpireDateSet(const QString &shareId) Q_EMIT dataChanged(shareModelIndex, shareModelIndex, { ExpireDateEnabledRole, ExpireDateRole }); } +void ShareModel::slotDeleteE2EeShare(const SharePtr &share) const +{ + const auto account = accountState()->account(); + QString folderAlias; + for (const auto &f : FolderMan::instance()->map()) { + if (f->accountState()->account() != account) { + continue; + } + const auto folderPath = f->remotePath(); + if (share->path().startsWith(folderPath) && (share->path() == folderPath || folderPath.endsWith('/') || share->path()[folderPath.size()] == '/')) { + folderAlias = f->alias(); + } + } + + auto folder = FolderMan::instance()->folder(folderAlias); + if (!folder || !folder->journalDb()) { + emit serverError(404, tr("Could not find local folder for %1").arg(share->path())); + return; + } + + const auto removeE2eeShareJob = new UpdateE2eeFolderUsersMetadataJob(account, + folder->journalDb(), + folder->remotePath(), + UpdateE2eeFolderUsersMetadataJob::Remove, + share->path(), + share->getShareWith()->shareWith()); + removeE2eeShareJob->setParent(_manager.data()); + removeE2eeShareJob->start(); + connect(removeE2eeShareJob, &UpdateE2eeFolderUsersMetadataJob::finished, this, [share, this](int code, const QString &message) { + if (code != 200) { + qCWarning(lcShareModel) << "Could not remove share from E2EE folder's metadata!"; + emit serverError(code, message); + return; + } + share->deleteShare(); + }); +} + // ----------------------- Shares modification slots ----------------------- // void ShareModel::toggleShareAllowEditing(const SharePtr &share, const bool enable) @@ -1099,11 +1148,15 @@ void ShareModel::createNewUserGroupShare(const ShareePtr &sharee) return; } - _manager->createShare(_sharePath, - Share::ShareType(sharee->type()), - sharee->shareWith(), - _maxSharingPermissions, - {}); + if (isSecureFileDropSupportedFolder()) { + if (!_synchronizationFolder) { + qCWarning(lcShareModel) << "Could not share an E2EE folder" << _localPath << "no responsible folder found"; + return; + } + _manager->createE2EeShareJob(_sharePath, sharee, _maxSharingPermissions, {}); + } else { + _manager->createShare(_sharePath, Share::ShareType(sharee->type()), sharee->shareWith(), _maxSharingPermissions, {}); + } } void ShareModel::createNewUserGroupShareWithPassword(const ShareePtr &sharee, const QString &password) const @@ -1137,7 +1190,11 @@ void ShareModel::deleteShare(const SharePtr &share) const return; } - share->deleteShare(); + if (isEncryptedItem() && Share::isShareTypeUserGroupEmailRoomOrRemote(share->getShareType())) { + slotDeleteE2EeShare(share); + } else { + share->deleteShare(); + } } void ShareModel::deleteShareFromQml(const QVariant &share) const @@ -1254,6 +1311,11 @@ bool ShareModel::serverAllowsResharing() const && _accountState->account()->capabilities().shareResharing(); } +bool ShareModel::isShareDisabledEncryptedFolder() const +{ + return _isShareDisabledEncryptedFolder; +} + QVariantList ShareModel::sharees() const { QVariantList returnSharees; diff --git a/src/gui/filedetails/sharemodel.h b/src/gui/filedetails/sharemodel.h index 735d207c9f082..694f0d37eeb33 100644 --- a/src/gui/filedetails/sharemodel.h +++ b/src/gui/filedetails/sharemodel.h @@ -33,6 +33,7 @@ class ShareModel : public QAbstractListModel Q_PROPERTY(bool publicLinkSharesEnabled READ publicLinkSharesEnabled NOTIFY publicLinkSharesEnabledChanged) Q_PROPERTY(bool userGroupSharingEnabled READ userGroupSharingEnabled NOTIFY userGroupSharingEnabledChanged) Q_PROPERTY(bool canShare READ canShare NOTIFY sharePermissionsChanged) + Q_PROPERTY(bool isShareDisabledEncryptedFolder READ isShareDisabledEncryptedFolder NOTIFY isShareDisabledEncryptedFolderChanged) Q_PROPERTY(bool fetchOngoing READ fetchOngoing NOTIFY fetchOngoingChanged) Q_PROPERTY(bool hasInitialShareFetchCompleted READ hasInitialShareFetchCompleted NOTIFY hasInitialShareFetchCompletedChanged) Q_PROPERTY(bool serverAllowsResharing READ serverAllowsResharing NOTIFY serverAllowsResharingChanged) @@ -118,6 +119,7 @@ class ShareModel : public QAbstractListModel [[nodiscard]] bool userGroupSharingEnabled() const; [[nodiscard]] bool canShare() const; [[nodiscard]] bool serverAllowsResharing() const; + [[nodiscard]] bool isShareDisabledEncryptedFolder() const; [[nodiscard]] bool fetchOngoing() const; [[nodiscard]] bool hasInitialShareFetchCompleted() const; @@ -134,6 +136,7 @@ class ShareModel : public QAbstractListModel void publicLinkSharesEnabledChanged(); void userGroupSharingEnabledChanged(); void sharePermissionsChanged(); + void isShareDisabledEncryptedFolderChanged(); void lockExpireStringChanged(); void fetchOngoingChanged(); void hasInitialShareFetchCompletedChanged(); @@ -141,7 +144,7 @@ class ShareModel : public QAbstractListModel void internalLinkReady(); void serverAllowsResharingChanged(); - void serverError(const int code, const QString &message); + void serverError(const int code, const QString &message) const; void passwordSetError(const QString &shareId, const int code, const QString &message); void requestPasswordForLinkShare(); void requestPasswordForEmailSharee(const OCC::ShareePtr &sharee); @@ -211,6 +214,7 @@ private slots: void slotShareNameSet(const QString &shareId); void slotShareLabelSet(const QString &shareId); void slotShareExpireDateSet(const QString &shareId); + void slotDeleteE2EeShare(const SharePtr &share) const; private: [[nodiscard]] QString displayStringForShare(const SharePtr &share) const; @@ -226,12 +230,13 @@ private slots: bool _hasInitialShareFetchCompleted = false; bool _sharePermissionsChangeInProgress = false; bool _hideDownloadEnabledChangeInProgress = false; + bool _isShareDisabledEncryptedFolder = false; SharePtr _placeholderLinkShare; SharePtr _internalLinkShare; SharePtr _secureFileDropPlaceholderLinkShare; QPointer _accountState; - QPointer _folder; + QPointer _synchronizationFolder; QString _localPath; QString _sharePath; @@ -240,6 +245,7 @@ private slots: SharedItemType _sharedItemType = SharedItemType::SharedItemTypeUndefined; SyncJournalFileLockInfo _filelockState; QString _privateLinkUrl; + QByteArray _fileRemoteId; QSharedPointer _manager; diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 551abde93a061..2361862fddc4c 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -27,6 +27,7 @@ #include "gui/systray.h" #include #include +#include "updatee2eefolderusersmetadatajob.h" #ifdef Q_OS_MAC #include @@ -1534,19 +1535,69 @@ void FolderMan::setDirtyNetworkLimits() void FolderMan::leaveShare(const QString &localFile) { - if (const auto folder = FolderMan::instance()->folderForPath(localFile)) { - const auto filePathRelative = QString(localFile).remove(folder->path()); + const auto localFileNoTrailingSlash = localFile.endsWith('/') ? localFile.chopped(1) : localFile; + if (const auto folder = FolderMan::instance()->folderForPath(localFileNoTrailingSlash)) { + const auto filePathRelative = QString(localFileNoTrailingSlash).remove(folder->path()); - const auto leaveShareJob = new SimpleApiJob(folder->accountState()->account(), folder->accountState()->account()->davPath() + filePathRelative); - leaveShareJob->setVerb(SimpleApiJob::Verb::Delete); - connect(leaveShareJob, &SimpleApiJob::resultReceived, this, [this, folder](int statusCode) { - Q_UNUSED(statusCode) - scheduleFolder(folder); - }); - leaveShareJob->start(); + 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, + //TODO: Might need to add a slash to "filePathRelative" once the server is working + 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) { + qCDebug(lcFolderMan) << "Could not remove share from E2EE folder's metadata!" << code << message; + return; + } + slotLeaveShare(localFileNoTrailingSlash, _removeE2eeShareJob->folderToken()); + }); + + return; + } + slotLeaveShare(localFileNoTrailingSlash); } } +void FolderMan::slotLeaveShare(const QString &localFile, const QByteArray &folderToken) +{ + const auto folder = FolderMan::instance()->folderForPath(localFile); + + if (!folder) { + qCWarning(lcFolderMan) << "Could not find a folder for localFile:" << localFile; + return; + } + + const auto filePathRelative = QString(localFile).remove(folder->path()); + const auto leaveShareJob = new SimpleApiJob(folder->accountState()->account(), folder->accountState()->account()->davPath() + filePathRelative); + leaveShareJob->setVerb(SimpleApiJob::Verb::Delete); + leaveShareJob->addRawHeader("e2e-token", folderToken); + 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(); +} + void FolderMan::trayOverallStatus(const QList &folders, SyncResult::Status *status, bool *unresolvedConflicts) { diff --git a/src/gui/folderman.h b/src/gui/folderman.h index 65f23ec5e39be..af2d5e092e696 100644 --- a/src/gui/folderman.h +++ b/src/gui/folderman.h @@ -16,6 +16,7 @@ #ifndef FOLDERMAN_H #define FOLDERMAN_H +#include #include #include #include @@ -38,6 +39,7 @@ class Application; class SyncResult; class SocketApi; class LockWatcher; +class UpdateE2eeFolderUsersMetadataJob; /** * @brief The FolderMan class @@ -326,6 +328,8 @@ private slots: void slotProcessFilesPushNotification(OCC::Account *account); void slotConnectToPushNotifications(OCC::Account *account); + void slotLeaveShare(const QString &localFile, const QByteArray &folderToken = {}); + private: /** Adds a new folder, does not add it to the account settings and * does not set an account on the new folder. @@ -392,6 +396,8 @@ private slots: QScopedPointer _socketApi; NavigationPaneHelper _navigationPaneHelper; + QPointer _removeE2eeShareJob; + bool _appRestartRequired = false; static FolderMan *_instance; diff --git a/src/gui/sharemanager.cpp b/src/gui/sharemanager.cpp index 892454305ce1d..76bceee89d12e 100644 --- a/src/gui/sharemanager.cpp +++ b/src/gui/sharemanager.cpp @@ -17,6 +17,8 @@ #include "account.h" #include "folderman.h" #include "accountstate.h" +#include "clientsideencryption.h" +#include "updatee2eefolderusersmetadatajob.h" #include #include @@ -486,6 +488,37 @@ void ShareManager::createShare(const QString &path, job->getSharedWithMe(); } +void ShareManager::createE2EeShareJob(const QString &path, + const ShareePtr sharee, + const Share::Permissions permissions, + const QString &password) +{ + Folder *folder = nullptr; + for (const auto &f : FolderMan::instance()->map()) { + if (f->accountState()->account() != _account) { + continue; + } + folder = f; + } + + if (!folder) { + emit serverError(0, "Failed creating share"); + return; + } + + const auto createE2eeShareJob = new UpdateE2eeFolderUsersMetadataJob(_account, + folder->journalDb(), + folder->remotePath(), + UpdateE2eeFolderUsersMetadataJob::Add, + path, + sharee->shareWith(), + QSslCertificate{}, + this); + + createE2eeShareJob->setUserData({sharee, permissions, password}); + connect(createE2eeShareJob, &UpdateE2eeFolderUsersMetadataJob::finished, this, &ShareManager::slotCreateE2eeShareJobFinised); + createE2eeShareJob->start(); +} void ShareManager::slotShareCreated(const QJsonDocument &reply) { @@ -630,4 +663,28 @@ void ShareManager::slotOcsError(int statusCode, const QString &message) { emit serverError(statusCode, message); } + + +void ShareManager::slotCreateE2eeShareJobFinised(int statusCode, const QString &message) +{ + const auto job = qobject_cast(sender()); + Q_ASSERT(job); + if (!job) { + qCWarning(lcUserGroupShare) << "slotCreateE2eeShareJobFinised must be called by UpdateE2eeShareMetadataJob::finished signal!"; + return; + } + disconnect(job, &UpdateE2eeFolderUsersMetadataJob::finished, this, &ShareManager::slotCreateE2eeShareJobFinised); + const auto userData = job->userData(); + Q_ASSERT(userData.sharee); + if (!userData.sharee) { + qCWarning(lcUserGroupShare) << "missing userData Map in UpdateE2eeShareMetadataJob instance!"; + emit serverError(-1, tr("Error")); + return; + } + if (statusCode != 200) { + emit serverError(statusCode, message); + } else { + createShare(job->path(), Share::ShareType(userData.sharee->type()), userData.sharee->shareWith(), userData.desiredPermissions, userData.password); + } +} } diff --git a/src/gui/sharemanager.h b/src/gui/sharemanager.h index f7e33d16f0593..66844dec21e63 100644 --- a/src/gui/sharemanager.h +++ b/src/gui/sharemanager.h @@ -67,6 +67,8 @@ class Share : public QObject using Permissions = SharePermissions; + Q_ENUM(Permissions); + /* * Constructor for shares */ @@ -411,6 +413,23 @@ class ShareManager : public QObject const Share::Permissions permissions, const QString &password = ""); + /** + * Tell the manager to create and start new UpdateE2eeShareMetadataJob job + * + * @param path The path of the share relative to the user folder on the server + * @param shareType The type of share (TypeUser, TypeGroup, TypeRemote) + * @param Permissions The share permissions + * @param folderId The id for an E2EE folder + * @param password An optional password for a share + * + * On success the signal shareCreated is emitted + * In case of a server error the serverError signal is emitted + */ + void createE2EeShareJob(const QString &path, + const ShareePtr sharee, + const Share::Permissions permissions, + const QString &password = ""); + /** * Fetch all the shares for path * @@ -440,6 +459,8 @@ private slots: void slotLinkShareCreated(const QJsonDocument &reply); void slotShareCreated(const QJsonDocument &reply); void slotOcsError(int statusCode, const QString &message); + void slotCreateE2eeShareJobFinised(int statusCode, const QString &message); + private: QSharedPointer parseLinkShare(const QJsonObject &data); QSharedPointer parseUserGroupShare(const QJsonObject &data); diff --git a/src/gui/sharepermissions.h b/src/gui/sharepermissions.h index 5a9763f0dead2..528a3818ac82a 100644 --- a/src/gui/sharepermissions.h +++ b/src/gui/sharepermissions.h @@ -36,5 +36,6 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(SharePermissions) } // namespace OCC Q_DECLARE_METATYPE(OCC::SharePermission) +Q_DECLARE_METATYPE(OCC::SharePermissions) #endif diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 1df11cf555080..aa909934ea761 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -542,7 +542,8 @@ void SocketApi::processEncryptRequest(const QString &localFile) choppedPath = choppedPath.mid(1); } - auto job = new OCC::EncryptFolderJob(account, folder->journalDb(), choppedPath, rec.numericFileId(), this); + auto job = new OCC::EncryptFolderJob(account, folder->journalDb(), choppedPath, rec.numericFileId()); + job->setParent(this); connect(job, &OCC::EncryptFolderJob::finished, this, [fileData, job](const int status) { if (status == OCC::EncryptFolderJob::Error) { const int ret = QMessageBox::critical(nullptr, diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index cd124bd3b653b..aafbc91382c6d 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -1,4 +1,5 @@ project(libsync) +find_package(KF5Archive REQUIRED) include(DefinePlatformDefaults) set(CMAKE_AUTOMOC TRUE) @@ -41,6 +42,8 @@ set(libsync_SRCS discoveryphase.cpp encryptfolderjob.h encryptfolderjob.cpp + encryptedfoldermetadatahandler.h + encryptedfoldermetadatahandler.cpp filesystem.h filesystem.cpp helpers.cpp @@ -62,8 +65,8 @@ set(libsync_SRCS owncloudpropagator.cpp nextcloudtheme.h nextcloudtheme.cpp - abstractpropagateremotedeleteencrypted.h - abstractpropagateremotedeleteencrypted.cpp + basepropagateremotedeleteencrypted.h + basepropagateremotedeleteencrypted.cpp deletejob.h deletejob.cpp progressdispatcher.h @@ -108,16 +111,28 @@ set(libsync_SRCS syncoptions.cpp theme.h theme.cpp - updatefiledropmetadata.h - updatefiledropmetadata.cpp + updatee2eefoldermetadatajob.h + updatee2eefoldermetadatajob.cpp + updatemigratede2eemetadatajob.h + updatemigratede2eemetadatajob.cpp + updatee2eefolderusersmetadatajob.h + updatee2eefolderusersmetadatajob.cpp clientsideencryption.h clientsideencryption.cpp clientsideencryptionjobs.h clientsideencryptionjobs.cpp + clientsideencryptionprimitives.h + clientsideencryptionprimitives.cpp datetimeprovider.h datetimeprovider.cpp + rootencryptedfolderinfo.h + rootencryptedfolderinfo.cpp + foldermetadata.h + foldermetadata.cpp ocsuserstatusconnector.h ocsuserstatusconnector.cpp + rootencryptedfolderinfo.cpp + rootencryptedfolderinfo.h userstatusconnector.h userstatusconnector.cpp ocsprofileconnector.h @@ -196,6 +211,7 @@ target_link_libraries(nextcloudsync Qt5::WebSockets Qt5::Xml Qt5::Sql + KF5::Archive ) if (NOT TOKEN_AUTH_ONLY) diff --git a/src/libsync/abstractpropagateremotedeleteencrypted.cpp b/src/libsync/abstractpropagateremotedeleteencrypted.cpp deleted file mode 100644 index 2cbfd698b8a82..0000000000000 --- a/src/libsync/abstractpropagateremotedeleteencrypted.cpp +++ /dev/null @@ -1,203 +0,0 @@ -/* - * 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 -#include - -#include "abstractpropagateremotedeleteencrypted.h" -#include "account.h" -#include "clientsideencryptionjobs.h" -#include "deletejob.h" -#include "owncloudpropagator.h" - -Q_LOGGING_CATEGORY(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED, "nextcloud.sync.propagator.remove.encrypted") - -namespace OCC { - -AbstractPropagateRemoteDeleteEncrypted::AbstractPropagateRemoteDeleteEncrypted(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent) - : QObject(parent) - , _propagator(propagator) - , _item(item) -{} - -QNetworkReply::NetworkError AbstractPropagateRemoteDeleteEncrypted::networkError() const -{ - return _networkError; -} - -QString AbstractPropagateRemoteDeleteEncrypted::errorString() const -{ - return _errorString; -} - -void AbstractPropagateRemoteDeleteEncrypted::storeFirstError(QNetworkReply::NetworkError err) -{ - if (_networkError == QNetworkReply::NetworkError::NoError) { - _networkError = err; - } -} - -void AbstractPropagateRemoteDeleteEncrypted::storeFirstErrorString(const QString &errString) -{ - if (_errorString.isEmpty()) { - _errorString = errString; - } -} - -void AbstractPropagateRemoteDeleteEncrypted::startLsColJob(const QString &path) -{ - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Folder is encrypted, let's get the Id from it."; - auto job = new LsColJob(_propagator->account(), _propagator->fullRemotePath(path), this); - job->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"}); - connect(job, &LsColJob::directoryListingSubfolders, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderEncryptedIdReceived); - connect(job, &LsColJob::finishedWithError, this, &AbstractPropagateRemoteDeleteEncrypted::taskFailed); - job->start(); -} - -void AbstractPropagateRemoteDeleteEncrypted::slotFolderEncryptedIdReceived(const QStringList &list) -{ - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Received id of folder, trying to lock it so we can prepare the metadata"; - auto job = qobject_cast(sender()); - const ExtraFolderInfo folderInfo = job->_folderInfos.value(list.first()); - slotTryLock(folderInfo.fileId); -} - -void AbstractPropagateRemoteDeleteEncrypted::slotTryLock(const QByteArray &folderId) -{ - auto lockJob = new LockEncryptFolderApiJob(_propagator->account(), folderId, _propagator->_journal, _propagator->account()->e2e()->_publicKey, this); - connect(lockJob, &LockEncryptFolderApiJob::success, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderLockedSuccessfully); - connect(lockJob, &LockEncryptFolderApiJob::error, this, &AbstractPropagateRemoteDeleteEncrypted::taskFailed); - lockJob->start(); -} - -void AbstractPropagateRemoteDeleteEncrypted::slotFolderLockedSuccessfully(const QByteArray &folderId, const QByteArray &token) -{ - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Folder id" << folderId << "Locked Successfully for Upload, Fetching Metadata"; - _folderLocked = true; - _folderToken = token; - _folderId = folderId; - - auto job = new GetMetadataApiJob(_propagator->account(), _folderId); - connect(job, &GetMetadataApiJob::jsonReceived, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived); - connect(job, &GetMetadataApiJob::error, this, &AbstractPropagateRemoteDeleteEncrypted::taskFailed); - job->start(); -} - -void AbstractPropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully(const QByteArray &folderId) -{ - Q_UNUSED(folderId); - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Folder id" << folderId << "successfully unlocked"; - _folderLocked = false; - _folderToken = ""; -} - -void AbstractPropagateRemoteDeleteEncrypted::slotDeleteRemoteItemFinished() -{ - auto *deleteJob = qobject_cast(QObject::sender()); - - Q_ASSERT(deleteJob); - - if (!deleteJob) { - qCCritical(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Sender is not a DeleteJob instance."; - taskFailed(); - return; - } - - const auto err = deleteJob->reply()->error(); - - _item->_httpErrorCode = deleteJob->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - _item->_responseTimeStamp = deleteJob->responseTimestamp(); - _item->_requestId = deleteJob->requestId(); - - if (err != QNetworkReply::NoError && err != QNetworkReply::ContentNotFoundError) { - storeFirstErrorString(deleteJob->errorString()); - storeFirstError(err); - - taskFailed(); - return; - } - - // A 404 reply is also considered a success here: We want to make sure - // a file is gone from the server. It not being there in the first place - // is ok. This will happen for files that are in the DB but not on - // the server or the local file system. - if (_item->_httpErrorCode != 204 && _item->_httpErrorCode != 404) { - // Normally we expect "204 No Content" - // If it is not the case, it might be because of a proxy or gateway intercepting the request, so we must - // throw an error. - storeFirstErrorString(tr("Wrong HTTP code returned by server. Expected 204, but received \"%1 %2\".") - .arg(_item->_httpErrorCode) - .arg(deleteJob->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); - - taskFailed(); - return; - } - - if (!_propagator->_journal->deleteFileRecord(_item->_originalFile, _item->isDirectory())) { - qCWarning(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Failed to delete file record from local DB" << _item->_originalFile; - } - _propagator->_journal->commit("Remote Remove"); - - unlockFolder(); -} - -void AbstractPropagateRemoteDeleteEncrypted::deleteRemoteItem(const QString &filename) -{ - qCInfo(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Deleting nested encrypted item" << filename; - - auto deleteJob = new DeleteJob(_propagator->account(), _propagator->fullRemotePath(filename), this); - deleteJob->setFolderToken(_folderToken); - - connect(deleteJob, &DeleteJob::finishedSignal, this, &AbstractPropagateRemoteDeleteEncrypted::slotDeleteRemoteItemFinished); - - deleteJob->start(); -} - -void AbstractPropagateRemoteDeleteEncrypted::unlockFolder() -{ - if (!_folderLocked) { - emit finished(true); - return; - } - - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Unlocking folder" << _folderId; - auto unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), _folderId, _folderToken, _propagator->_journal, this); - - connect(unlockJob, &UnlockEncryptFolderApiJob::success, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully); - connect(unlockJob, &UnlockEncryptFolderApiJob::error, this, [this] (const QByteArray& fileId, int httpReturnCode) { - Q_UNUSED(fileId); - _folderLocked = false; - _folderToken = ""; - _item->_httpErrorCode = httpReturnCode; - _errorString = tr("\"%1 Failed to unlock encrypted folder %2\".") - .arg(httpReturnCode) - .arg(QString::fromUtf8(fileId)); - _item->_errorString =_errorString; - taskFailed(); - }); - unlockJob->start(); -} - -void AbstractPropagateRemoteDeleteEncrypted::taskFailed() -{ - qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Task failed for job" << sender(); - _isTaskFailed = true; - if (_folderLocked) { - unlockFolder(); - } else { - emit finished(false); - } -} - -} // namespace OCC diff --git a/src/libsync/basepropagateremotedeleteencrypted.cpp b/src/libsync/basepropagateremotedeleteencrypted.cpp new file mode 100644 index 0000000000000..9503a081091ef --- /dev/null +++ b/src/libsync/basepropagateremotedeleteencrypted.cpp @@ -0,0 +1,207 @@ +/* + * 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 +#include +#include "foldermetadata.h" +#include "basepropagateremotedeleteencrypted.h" +#include "account.h" +#include "clientsideencryptionjobs.h" +#include "deletejob.h" +#include "owncloudpropagator.h" + +Q_LOGGING_CATEGORY(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED, "nextcloud.sync.propagator.remove.encrypted") + +namespace OCC { + +BasePropagateRemoteDeleteEncrypted::BasePropagateRemoteDeleteEncrypted(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent) + : QObject(parent) + , _propagator(propagator) + , _item(item) +{} + +QNetworkReply::NetworkError BasePropagateRemoteDeleteEncrypted::networkError() const +{ + return _networkError; +} + +QString BasePropagateRemoteDeleteEncrypted::errorString() const +{ + return _errorString; +} + +void BasePropagateRemoteDeleteEncrypted::storeFirstError(QNetworkReply::NetworkError err) +{ + if (_networkError == QNetworkReply::NetworkError::NoError) { + _networkError = err; + } +} + +void BasePropagateRemoteDeleteEncrypted::storeFirstErrorString(const QString &errString) +{ + if (_errorString.isEmpty()) { + _errorString = errString; + } +} + +void BasePropagateRemoteDeleteEncrypted::fetchMetadataForPath(const QString &path) +{ + qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Folder is encrypted, let's its metadata."; + _fullFolderRemotePath = _propagator->fullRemotePath(path); + + SyncJournalFileRecord rec; + if (!_propagator->_journal->getRootE2eFolderRecord(_fullFolderRemotePath, &rec) || !rec.isValid()) { + taskFailed(); + return; + } + + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(_propagator->account(), + _fullFolderRemotePath, + _propagator->_journal, + rec.path())); + + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::fetchFinished, + this, + &BasePropagateRemoteDeleteEncrypted::slotFetchMetadataJobFinished); + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::uploadFinished, + this, + &BasePropagateRemoteDeleteEncrypted::slotUpdateMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(); +} + +void BasePropagateRemoteDeleteEncrypted::uploadMetadata(const EncryptedFolderMetadataHandler::UploadMode uploadMode) +{ + _encryptedFolderMetadataHandler->uploadMetadata(uploadMode); +} + +void BasePropagateRemoteDeleteEncrypted::slotFolderUnLockFinished(const QByteArray &folderId, int statusCode) +{ + if (statusCode != 200) { + _item->_httpErrorCode = statusCode; + _errorString = tr("\"%1 Failed to unlock encrypted folder %2\".").arg(statusCode).arg(QString::fromUtf8(folderId)); + _item->_errorString = _errorString; + taskFailed(); + return; + } + qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Folder id" << folderId << "successfully unlocked"; +} + +void BasePropagateRemoteDeleteEncrypted::slotDeleteRemoteItemFinished() +{ + auto *deleteJob = qobject_cast(QObject::sender()); + + Q_ASSERT(deleteJob); + + if (!deleteJob) { + qCCritical(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Sender is not a DeleteJob instance."; + taskFailed(); + return; + } + + const auto err = deleteJob->reply()->error(); + + _item->_httpErrorCode = deleteJob->reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + _item->_responseTimeStamp = deleteJob->responseTimestamp(); + _item->_requestId = deleteJob->requestId(); + + if (err != QNetworkReply::NoError && err != QNetworkReply::ContentNotFoundError) { + storeFirstErrorString(deleteJob->errorString()); + storeFirstError(err); + + taskFailed(); + return; + } + + // A 404 reply is also considered a success here: We want to make sure + // a file is gone from the server. It not being there in the first place + // is ok. This will happen for files that are in the DB but not on + // the server or the local file system. + if (_item->_httpErrorCode != 204 && _item->_httpErrorCode != 404) { + // Normally we expect "204 No Content" + // If it is not the case, it might be because of a proxy or gateway intercepting the request, so we must + // throw an error. + storeFirstErrorString(tr("Wrong HTTP code returned by server. Expected 204, but received \"%1 %2\".") + .arg(_item->_httpErrorCode) + .arg(deleteJob->reply()->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString())); + + taskFailed(); + return; + } + + if (!_propagator->_journal->deleteFileRecord(_item->_originalFile, _item->isDirectory())) { + qCWarning(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Failed to delete file record from local DB" << _item->_originalFile; + } + _propagator->_journal->commit("Remote Remove"); + + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success); +} + +void BasePropagateRemoteDeleteEncrypted::deleteRemoteItem(const QString &filename) +{ + qCInfo(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Deleting nested encrypted item" << filename; + + const auto deleteJob = new DeleteJob(_propagator->account(), _propagator->fullRemotePath(filename), this); + if (_encryptedFolderMetadataHandler && _encryptedFolderMetadataHandler->folderMetadata() + && _encryptedFolderMetadataHandler->folderMetadata()->isValid()) { + deleteJob->setFolderToken(_encryptedFolderMetadataHandler->folderToken()); + } + + connect(deleteJob, &DeleteJob::finishedSignal, this, &BasePropagateRemoteDeleteEncrypted::slotDeleteRemoteItemFinished); + deleteJob->start(); +} + +void BasePropagateRemoteDeleteEncrypted::unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result) +{ + if (!_encryptedFolderMetadataHandler) { + qCWarning(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Null _encryptedFolderMetadataHandler"; + } + if (!_encryptedFolderMetadataHandler || !_encryptedFolderMetadataHandler->isFolderLocked()) { + emit finished(true); + return; + } + + qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Unlocking folder" << _encryptedFolderMetadataHandler->folderId(); + + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &BasePropagateRemoteDeleteEncrypted::slotFolderUnLockFinished); + _encryptedFolderMetadataHandler->unlockFolder(result); +} + +void BasePropagateRemoteDeleteEncrypted::taskFailed() +{ + qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Task failed for job" << sender(); + _isTaskFailed = true; + if (_encryptedFolderMetadataHandler && _encryptedFolderMetadataHandler->isFolderLocked()) { + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + } else { + emit finished(false); + } +} + +QSharedPointer BasePropagateRemoteDeleteEncrypted::folderMetadata() const +{ + Q_ASSERT(_encryptedFolderMetadataHandler->folderMetadata()); + if (!_encryptedFolderMetadataHandler->folderMetadata()) { + qCWarning(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Metadata is null!"; + } + return _encryptedFolderMetadataHandler->folderMetadata(); +} + +const QByteArray BasePropagateRemoteDeleteEncrypted::folderToken() const +{ + return _encryptedFolderMetadataHandler->folderToken(); +} + +} // namespace OCC diff --git a/src/libsync/abstractpropagateremotedeleteencrypted.h b/src/libsync/basepropagateremotedeleteencrypted.h similarity index 51% rename from src/libsync/abstractpropagateremotedeleteencrypted.h rename to src/libsync/basepropagateremotedeleteencrypted.h index 3d3dd83425d7a..653f1da982869 100644 --- a/src/libsync/abstractpropagateremotedeleteencrypted.h +++ b/src/libsync/basepropagateremotedeleteencrypted.h @@ -17,22 +17,22 @@ #include #include #include - +#include "encryptedfoldermetadatahandler.h" #include "syncfileitem.h" namespace OCC { class OwncloudPropagator; /** - * @brief The AbstractPropagateRemoteDeleteEncrypted class is the base class for Propagate Remote Delete Encrypted jobs + * @brief The BasePropagateRemoteDeleteEncrypted class is the base class for Propagate Remote Delete Encrypted jobs * @ingroup libsync */ -class AbstractPropagateRemoteDeleteEncrypted : public QObject +class BasePropagateRemoteDeleteEncrypted : public QObject { Q_OBJECT public: - AbstractPropagateRemoteDeleteEncrypted(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent); - ~AbstractPropagateRemoteDeleteEncrypted() override = default; + BasePropagateRemoteDeleteEncrypted(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent); + ~BasePropagateRemoteDeleteEncrypted() override = default; [[nodiscard]] QNetworkReply::NetworkError networkError() const; [[nodiscard]] QString errorString() const; @@ -46,27 +46,33 @@ class AbstractPropagateRemoteDeleteEncrypted : public QObject void storeFirstError(QNetworkReply::NetworkError err); void storeFirstErrorString(const QString &errString); - void startLsColJob(const QString &path); - void slotFolderEncryptedIdReceived(const QStringList &list); - void slotTryLock(const QByteArray &folderId); - void slotFolderLockedSuccessfully(const QByteArray &folderId, const QByteArray &token); - virtual void slotFolderUnLockedSuccessfully(const QByteArray &folderId); - virtual void slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) = 0; - void slotDeleteRemoteItemFinished(); + void fetchMetadataForPath(const QString &path); + void uploadMetadata(const EncryptedFolderMetadataHandler::UploadMode uploadMode = EncryptedFolderMetadataHandler::UploadMode::DoNotKeepLock); + + [[nodiscard]] QSharedPointer folderMetadata() const; + [[nodiscard]] const QByteArray folderToken() const; void deleteRemoteItem(const QString &filename); - void unlockFolder(); + + void unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result); void taskFailed(); +protected slots: + virtual void slotFolderUnLockFinished(const QByteArray &folderId, int statusCode); + virtual void slotFetchMetadataJobFinished(int statusCode, const QString &message) = 0; + virtual void slotUpdateMetadataJobFinished(int statusCode, const QString &message) = 0; + void slotDeleteRemoteItemFinished(); + protected: - OwncloudPropagator *_propagator = nullptr; + QPointer _propagator = nullptr; SyncFileItemPtr _item; - QByteArray _folderToken; - QByteArray _folderId; - bool _folderLocked = false; bool _isTaskFailed = false; QNetworkReply::NetworkError _networkError = QNetworkReply::NoError; QString _errorString; + QString _fullFolderRemotePath; + +private: + QScopedPointer _encryptedFolderMetadataHandler; }; } diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 0b73b6f7a4652..a189da2ac3fba 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -159,7 +159,7 @@ bool Capabilities::clientSideEncryptionAvailable() const return false; } - const auto capabilityAvailable = (major == 1 && minor >= 1); + const auto capabilityAvailable = (major >= 1 && minor >= 0); if (!capabilityAvailable) { qCInfo(lcServerCapabilities) << "Incompatible E2EE API version:" << version; } @@ -176,10 +176,10 @@ double Capabilities::clientSideEncryptionVersion() const const auto properties = (*foundEndToEndEncryptionInCaps).toMap(); const auto enabled = properties.value(QStringLiteral("enabled"), false).toBool(); if (!enabled) { - return false; + return 0.0; } - return properties.value(QStringLiteral("api-version"), 1.0).toDouble(); + return properties.value(QStringLiteral("api-version"), "1.0").toDouble(); } bool Capabilities::notificationsAvailable() const diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index dd9263b024d62..ba34300420a16 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1,9 +1,8 @@ -#define OPENSSL_SUPPRESS_DEPRECATED - #include "clientsideencryption.h" #include #include +#include #include #include #include @@ -17,6 +16,7 @@ #include "creds/abstractcredentials.h" #include "common/utility.h" #include "common/constants.h" +#include #include "wordlist.h" #include @@ -34,8 +34,11 @@ #include #include #include +#include #include +#include + #include #include #include @@ -55,27 +58,30 @@ namespace OCC Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg) Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg) -Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg) -QString e2eeBaseUrl() +QString e2eeBaseUrl(const OCC::AccountPtr &account) { - return QStringLiteral("ocs/v2.php/apps/end_to_end_encryption/api/v1/"); + Q_ASSERT(account); + if (!account) { + qCWarning(lcCse()) << "Account must be not null!"; + } + const QString apiVersion = account && account->capabilities().clientSideEncryptionVersion() >= 2.0 + ? QStringLiteral("v2") + : QStringLiteral("v1"); + return QStringLiteral("ocs/v2.php/apps/end_to_end_encryption/api/%1/").arg(apiVersion); } namespace { constexpr char accountProperty[] = "account"; constexpr char e2e_cert[] = "_e2e-certificate"; +constexpr auto e2e_cert_sharing = "_sharing"; constexpr char e2e_private[] = "_e2e-private"; constexpr char e2e_public[] = "_e2e-public"; constexpr char e2e_mnemonic[] = "_e2e-mnemonic"; -constexpr auto metadataKeyJsonKey = "metadataKey"; - constexpr qint64 blockSize = 1024; -constexpr auto metadataKeySize = 16; - QList oldCipherFormatSplit(const QByteArray &cipher) { const auto separator = QByteArrayLiteral("fA=="); // BASE64 encoded '|' @@ -135,139 +141,8 @@ class CipherCtx { EVP_CIPHER_CTX* _ctx; }; - -class Bio { -public: - Bio() - : _bio(BIO_new(BIO_s_mem())) - { - } - - ~Bio() - { - BIO_free_all(_bio); - } - - operator const BIO*() const - { - return _bio; - } - - operator BIO*() - { - return _bio; - } - -private: - Q_DISABLE_COPY(Bio) - - BIO* _bio; -}; - -class PKeyCtx { -public: - explicit PKeyCtx(int id, ENGINE *e = nullptr) - : _ctx(EVP_PKEY_CTX_new_id(id, e)) - { - } - - ~PKeyCtx() - { - EVP_PKEY_CTX_free(_ctx); - } - - // The move constructor is needed for pre-C++17 where - // return-value optimization (RVO) is not obligatory - // and we have a `forKey` static function that returns - // an instance of this class - PKeyCtx(PKeyCtx&& other) - { - std::swap(_ctx, other._ctx); - } - - PKeyCtx& operator=(PKeyCtx&& other) = delete; - - static PKeyCtx forKey(EVP_PKEY *pkey, ENGINE *e = nullptr) - { - PKeyCtx ctx; - ctx._ctx = EVP_PKEY_CTX_new(pkey, e); - return ctx; - } - - operator EVP_PKEY_CTX*() - { - return _ctx; - } - -private: - Q_DISABLE_COPY(PKeyCtx) - - PKeyCtx() = default; - - EVP_PKEY_CTX* _ctx = nullptr; -}; - } -class ClientSideEncryption::PKey { -public: - ~PKey() - { - EVP_PKEY_free(_pkey); - } - - // The move constructor is needed for pre-C++17 where - // return-value optimization (RVO) is not obligatory - // and we have a static functions that return - // an instance of this class - PKey(PKey&& other) - { - std::swap(_pkey, other._pkey); - } - - PKey& operator=(PKey&& other) = delete; - - static PKey readPublicKey(Bio &bio) - { - PKey result; - result._pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); - return result; - } - - static PKey readPrivateKey(Bio &bio) - { - PKey result; - result._pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); - return result; - } - - static PKey generate(PKeyCtx& ctx) - { - PKey result; - if (EVP_PKEY_keygen(ctx, &result._pkey) <= 0) { - result._pkey = nullptr; - } - return result; - } - - operator EVP_PKEY*() - { - return _pkey; - } - - operator EVP_PKEY*() const - { - return _pkey; - } - -private: - Q_DISABLE_COPY(PKey) - - PKey() = default; - - EVP_PKEY* _pkey = nullptr; -}; - namespace { class X509Certificate { @@ -313,7 +188,8 @@ class X509Certificate { X509* _certificate = nullptr; }; -QByteArray BIO2ByteArray(Bio &b) { +QByteArray BIO2ByteArray(Bio &b) +{ auto pending = static_cast(BIO_ctrl_pending(b)); QByteArray res(pending, '\0'); BIO_read(b, unsignedData(res), pending); @@ -330,10 +206,6 @@ QByteArray handleErrors() namespace EncryptionHelper { - - - - QByteArray generateRandomFilename() { return QUuid::createUuid().toRfc4122().toHex(); @@ -634,7 +506,7 @@ QByteArray decryptStringSymmetric(const QByteArray& key, const QByteArray& data) QByteArray privateKeyToPem(const QByteArray key) { Bio privateKeyBio; BIO_write(privateKeyBio, key.constData(), key.size()); - auto pkey = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio); + auto pkey = PKey::readPrivateKey(privateKeyBio); Bio pemBio; PEM_write_bio_PKCS8PrivateKey(pemBio, pkey, nullptr, nullptr, 0, nullptr, nullptr); @@ -653,8 +525,8 @@ QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data) Bio publicKeyBio; const auto publicKeyPem = key.toPem(); BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); - const auto publicKey = ClientSideEncryption::PKey::readPublicKey(publicKeyBio); - return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64()); + const auto publicKey = PKey::readPublicKey(publicKeyBio); + return EncryptionHelper::encryptStringAsymmetric(publicKey, data); } QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data) @@ -667,16 +539,16 @@ QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteA Bio privateKeyBio; BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); - const auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio); + const auto key = PKey::readPrivateKey(privateKeyBio); // Also base64 decode the result - const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data)); + const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(key, data); if (decryptResult.isEmpty()) { qCDebug(lcCse()) << "ERROR. Could not decrypt data"; return {}; } - return QByteArray::fromBase64(decryptResult); + return decryptResult; } QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) { @@ -868,14 +740,12 @@ QByteArray encryptStringAsymmetric(EVP_PKEY *publicKey, const QByteArray& data) exit(1); } - // Transform the encrypted data into base64. qCInfo(lcCse()) << out.toBase64(); - return out.toBase64(); + return out; } } - ClientSideEncryption::ClientSideEncryption() = default; void ClientSideEncryption::initialize(const AccountPtr &account) @@ -908,6 +778,18 @@ void ClientSideEncryption::fetchCertificateFromKeyChain(const AccountPtr &accoun job->start(); } +void ClientSideEncryption::fetchCertificateFromKeyChain(const OCC::AccountPtr &account, const QString &userId) +{ + const auto keyChainKey = AbstractCredentials::keychainKey(account->url().toString(), userId + e2e_cert + e2e_cert_sharing, userId); + + const auto job = new ReadPasswordJob(Theme::instance()->appName()); + job->setProperty(accountProperty, QVariant::fromValue(account)); + job->setInsecureFallback(false); + job->setKey(keyChainKey); + connect(job, &ReadPasswordJob::finished, this, &ClientSideEncryption::publicKeyFetchedForUserId); + job->start(); +} + void ClientSideEncryption::fetchPublicKeyFromKeyChain(const AccountPtr &account) { const QString kck = AbstractCredentials::keychainKey( @@ -940,7 +822,7 @@ bool ClientSideEncryption::checkPublicKeyValidity(const AccountPtr &account) con BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); auto key = PKey::readPrivateKey(privateKeyBio); - QByteArray decryptResult = QByteArray::fromBase64(EncryptionHelper::decryptStringAsymmetric( key, QByteArray::fromBase64(encryptedData))); + QByteArray decryptResult = QByteArray::fromBase64(EncryptionHelper::decryptStringAsymmetric(key, encryptedData)); if (data != decryptResult) { qCInfo(lcCse()) << "invalid private key"; @@ -1011,6 +893,86 @@ void ClientSideEncryption::publicCertificateFetched(Job *incoming) job->start(); } +QByteArray ClientSideEncryption::generateSignatureCryptographicMessageSyntax(const QByteArray &data) const +{ + Bio certificateBio; + const auto certificatePem = _certificate.toPem(); + BIO_write(certificateBio, certificatePem.constData(), certificatePem.size()); + const auto x509Certificate = X509Certificate::readCertificate(certificateBio); + if (!x509Certificate) { + qCInfo(lcCse()) << "Client certificate is invalid. Could not check it against the server public key"; + return {}; + } + + Bio privateKeyBio; + BIO_write(privateKeyBio, _privateKey.constData(), _privateKey.size()); + const auto privateKey = PKey::readPrivateKey(privateKeyBio); + + Bio dataBio; + BIO_write(dataBio, data.constData(), data.size()); + + const auto contentInfo = CMS_sign(x509Certificate, privateKey, nullptr, dataBio, CMS_DETACHED); + + if (!contentInfo) { + return {}; + } + + Bio i2dCmsBioOut; + [[maybe_unused]] auto resultI2dCms = i2d_CMS_bio(i2dCmsBioOut, contentInfo); + const auto i2dCmsBio = BIO2ByteArray(i2dCmsBioOut); + + CMS_ContentInfo_free(contentInfo); + + return i2dCmsBio; +} + +bool ClientSideEncryption::verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector &certificatePems) const +{ + Bio cmsContentBio; + BIO_write(cmsContentBio, cmsContent.constData(), cmsContent.size()); + const auto cmsDataFromBio = d2i_CMS_bio(cmsContentBio, nullptr); + if (!cmsDataFromBio) { + return false; + } + + Bio detachedData; + BIO_write(detachedData, data.constData(), data.size()); + + if (CMS_verify(cmsDataFromBio, nullptr, nullptr, detachedData, nullptr, CMS_DETACHED | CMS_NO_SIGNER_CERT_VERIFY) != 1) { + CMS_ContentInfo_free(cmsDataFromBio); + return false; + } + + const auto signerInfos = CMS_get0_SignerInfos(cmsDataFromBio); + + if (!signerInfos) { + CMS_ContentInfo_free(cmsDataFromBio); + return false; + } + + const auto numSignerInfos = sk_CMS_SignerInfo_num(signerInfos); + + for (const auto &certificatePem : certificatePems) { + Bio certificateBio; + BIO_write(certificateBio, certificatePem.constData(), certificatePem.size()); + const auto x509Certificate = X509Certificate::readCertificate(certificateBio); + + if (!x509Certificate) { + continue; + } + + for (auto i = 0; i < numSignerInfos; ++i) { + const auto signerInfo = sk_CMS_SignerInfo_value(signerInfos, i); + if (CMS_SignerInfo_cert_cmp(signerInfo, x509Certificate) == 0) { + CMS_ContentInfo_free(cmsDataFromBio); + return true; + } + } + } + CMS_ContentInfo_free(cmsDataFromBio); + return false; +} + void ClientSideEncryption::publicKeyFetched(QKeychain::Job *incoming) { const auto readJob = dynamic_cast(incoming); @@ -1046,6 +1008,18 @@ void ClientSideEncryption::publicKeyFetched(QKeychain::Job *incoming) job->start(); } +void ClientSideEncryption::publicKeyFetchedForUserId(QKeychain::Job *incoming) +{ + const auto readJob = dynamic_cast(incoming); + Q_ASSERT(readJob); + + if (readJob->error() != NoError || readJob->binaryData().isEmpty()) { + emit certificateFetchedFromKeychain(QSslCertificate{}); + return; + } + emit certificateFetchedFromKeychain(QSslCertificate(readJob->binaryData(), QSsl::Pem)); +} + void ClientSideEncryption::privateKeyFetched(Job *incoming) { auto *readJob = dynamic_cast(incoming); @@ -1141,6 +1115,22 @@ void ClientSideEncryption::writeCertificate(const AccountPtr &account) job->start(); } +void ClientSideEncryption::writeCertificate(const AccountPtr &account, const QString &userId, const QSslCertificate &certificate) +{ + const auto keyChainKey = AbstractCredentials::keychainKey(account->url().toString(), userId + e2e_cert + e2e_cert_sharing, userId); + + const auto job = new WritePasswordJob(Theme::instance()->appName()); + job->setInsecureFallback(false); + job->setKey(keyChainKey); + job->setBinaryData(certificate.toPem()); + connect(job, &WritePasswordJob::finished, [this, certificate](Job *incoming) { + Q_UNUSED(incoming); + qCInfo(lcCse()) << "Certificate stored in keychain"; + emit certificateWriteComplete(certificate); + }); + job->start(); +} + void ClientSideEncryption::generateMnemonic() { const auto list = WordList::getRandomWords(12); @@ -1199,6 +1189,35 @@ void ClientSideEncryption::forgetSensitiveData(const AccountPtr &account) deleteMnemonicJob->start(); } +void ClientSideEncryption::getUsersPublicKeyFromServer(const AccountPtr &account, const QStringList &userIds) +{ + qCInfo(lcCse()) << "Retrieving public keys from server, for users:" << userIds; + const auto job = new JsonApiJob(account, e2eeBaseUrl(account) + QStringLiteral("public-key"), this); + connect(job, &JsonApiJob::jsonReceived, [this, account, userIds](const QJsonDocument &doc, int retCode) { + if (retCode == 200) { + QHash results; + const auto publicKeys = doc.object()[QStringLiteral("ocs")].toObject()[QStringLiteral("data")].toObject()[QStringLiteral("public-keys")].toObject(); + for (const auto &userId : publicKeys.keys()) { + if (userIds.contains(userId)) { + results.insert(userId, QSslCertificate(publicKeys.value(userId).toString().toLocal8Bit(), QSsl::Pem)); + } + } + emit certificatesFetchedFromServer(results); + } else if (retCode == 404) { + qCInfo(lcCse()) << "No public key on the server"; + emit certificatesFetchedFromServer({}); + } else { + qCInfo(lcCse()) << "Error while requesting public keys for users: " << retCode; + emit certificatesFetchedFromServer({}); + } + }); + QUrlQuery urlQuery; + const auto userIdsJSON = QJsonDocument::fromVariant(userIds); + urlQuery.addQueryItem(QStringLiteral("users"), userIdsJSON.toJson(QJsonDocument::Compact).toPercentEncoding()); + job->addQueryParams(urlQuery); + job->start(); +} + void ClientSideEncryption::handlePrivateKeyDeleted(const QKeychain::Job* const incoming) { const auto error = incoming->error(); @@ -1262,7 +1281,6 @@ bool ClientSideEncryption::sensitiveDataRemaining() const void ClientSideEncryption::failedToInitialize(const AccountPtr &account) { forgetSensitiveData(account); - account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); Q_EMIT initializationFinished(); } @@ -1335,7 +1353,7 @@ void ClientSideEncryption::generateKeyPair(const AccountPtr &account) }); } -std::pair ClientSideEncryption::generateCSR(const AccountPtr &account, +std::pair ClientSideEncryption::generateCSR(const AccountPtr &account, PKey keyPair, PKey privateKey) { @@ -1408,7 +1426,7 @@ void ClientSideEncryption::sendSignRequestCSR(const AccountPtr &account, PKey keyPair, const QByteArray &csrContent) { - auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl() + "public-key", this); + auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl(account) + "public-key", this); job->setCsr(csrContent); connect(job, &SignPublicKeyApiJob::jsonReceived, [this, account, keyPair = std::move(keyPair)](const QJsonDocument& json, const int retCode) { @@ -1524,7 +1542,7 @@ void ClientSideEncryption::checkUserKeyOnServer(const QString &keyType, SUCCESS_CALLBACK nextCheck, ERROR_CALLBACK onError) { - auto job = new JsonApiJob(account, e2eeBaseUrl() + keyType, this); + auto job = new JsonApiJob(account, e2eeBaseUrl(account) + keyType, this); connect(job, &JsonApiJob::jsonReceived, [nextCheck, onError](const QJsonDocument& doc, int retCode) { Q_UNUSED(doc) @@ -1566,7 +1584,7 @@ void ClientSideEncryption::encryptPrivateKey(const AccountPtr &account) auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt); // Send private key to the server - auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl() + "private-key", this); + auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl(account) + "private-key", this); job->setPrivateKey(cryptedText); connect(job, &StorePrivateKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { Q_UNUSED(doc); @@ -1645,7 +1663,7 @@ void ClientSideEncryption::decryptPrivateKey(const AccountPtr &account, const QB void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account) { - auto job = new JsonApiJob(account, e2eeBaseUrl() + "private-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl(account) + "private-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString(); @@ -1665,7 +1683,7 @@ void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account) void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) { - auto job = new JsonApiJob(account, e2eeBaseUrl() + "public-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl(account) + "public-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString(); @@ -1690,7 +1708,7 @@ void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr &account) { - auto job = new JsonApiJob(account, e2eeBaseUrl() + "server-key", this); + auto job = new JsonApiJob(account, e2eeBaseUrl(account) + "server-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { if (retCode == 200) { const auto serverPublicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-key"].toString().toLatin1(); @@ -1715,443 +1733,196 @@ void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr job->start(); } -FolderMetadata::FolderMetadata(AccountPtr account) - : _account(account) -{ - qCInfo(lcCseMetadata()) << "Setupping Empty Metadata"; - setupEmptyMetadata(); -} - -FolderMetadata::FolderMetadata(AccountPtr account, - RequiredMetadataVersion requiredMetadataVersion, - const QByteArray& metadata, - int statusCode) - : _account(std::move(account)) - , _requiredMetadataVersion(requiredMetadataVersion) +bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag) { - if (metadata.isEmpty() || statusCode == 404) { - qCInfo(lcCseMetadata()) << "Setupping Empty Metadata"; - setupEmptyMetadata(); - } else { - qCInfo(lcCseMetadata()) << "Setting up existing metadata"; - setupExistingMetadata(metadata); + if (!input->open(QIODevice::ReadOnly)) { + qCDebug(lcCse) << "Could not open input file for reading" << input->errorString(); } -} - -void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) -{ - /* This is the json response from the server, it contains two extra objects that we are *not* interested. - * ocs and data. - */ - QJsonDocument doc = QJsonDocument::fromJson(metadata); - qCInfo(lcCseMetadata()) << doc.toJson(QJsonDocument::Compact); - - // The metadata is being retrieved as a string stored in a json. - // This *seems* to be broken but the RFC doesn't explicit how it wants. - // I'm currently unsure if this is error on my side or in the server implementation. - // And because inside of the meta-data there's an object called metadata, without '-' - // make it really different. - - QString metaDataStr = doc.object()["ocs"] - .toObject()["data"] - .toObject()["meta-data"] - .toString(); - - QJsonDocument metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); - QJsonObject metadataObj = metaDataDoc.object()["metadata"].toObject(); - QJsonObject metadataKeys = metadataObj["metadataKeys"].toObject(); - - const auto metadataKeyFromJson = metadataObj[metadataKeyJsonKey].toString().toLocal8Bit(); - if (!metadataKeyFromJson.isEmpty()) { - const auto decryptedMetadataKeyBase64 = decryptData(metadataKeyFromJson); - if (!decryptedMetadataKeyBase64.isEmpty()) { - _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); - } + if (!output->open(QIODevice::WriteOnly)) { + qCDebug(lcCse) << "Could not oppen output file for writing" << output->errorString(); } - auto migratedMetadata = false; - if (_metadataKey.isEmpty() && _requiredMetadataVersion != RequiredMetadataVersion::Version1_2) { - qCDebug(lcCse()) << "Migrating from v1.1 to v1.2"; - migratedMetadata = true; - - if (metadataKeys.isEmpty()) { - qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return; - } + // Init + CipherCtx ctx; - const auto lastMetadataKey = metadataKeys.keys().last(); - const auto decryptedMetadataKeyBase64 = decryptData(metadataKeys.value(lastMetadataKey).toString().toLocal8Bit()); - if (!decryptedMetadataKeyBase64.isEmpty()) { - _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); - } + /* Create and initialise the context */ + if(!ctx) { + qCInfo(lcCse()) << "Could not create context"; + return false; } - if (_metadataKey.isEmpty()) { - qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return; + /* Initialise the decryption operation. */ + if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { + qCInfo(lcCse()) << "Could not init cipher"; + return false; } - const auto sharing = metadataObj["sharing"].toString().toLocal8Bit(); - const auto files = metaDataDoc.object()["files"].toObject(); - const auto metadataKey = metaDataDoc.object()["metadata"].toObject()["metadataKey"].toString().toUtf8(); - const auto metadataKeyChecksum = metaDataDoc.object()["metadata"].toObject()["checksum"].toString().toUtf8(); - - _fileDrop = metaDataDoc.object().value("filedrop").toObject(); - // for unit tests - _fileDropFromServer = metaDataDoc.object().value("filedrop").toObject(); - - // Iterate over the document to store the keys. I'm unsure that the keys are in order, - // perhaps it's better to store a map instead of a vector, perhaps this just doesn't matter. - - // Cool, We actually have the key, we can decrypt the rest of the metadata. - qCDebug(lcCse) << "Sharing: " << sharing; - if (sharing.size()) { - auto sharingDecrypted = decryptJsonObject(sharing, _metadataKey); - qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted; + EVP_CIPHER_CTX_set_padding(ctx, 0); - // Sharing is also a JSON object, so extract it and populate. - auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted); - auto sharingObj = sharingDoc.object(); - for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) { - _sharing.push_back({it.key(), it.value().toString()}); - } - } else { - qCDebug(lcCse) << "Skipping sharing section since it is empty"; + /* Set IV length. */ + if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { + qCInfo(lcCse()) << "Could not set iv length"; + return false; } - for (auto it = files.constBegin(); it != files.constEnd(); ++it) { - EncryptedFile file; - file.encryptedFilename = it.key(); - - const auto fileObj = it.value().toObject(); - file.authenticationTag = QByteArray::fromBase64(fileObj["authenticationTag"].toString().toLocal8Bit()); - file.initializationVector = QByteArray::fromBase64(fileObj["initializationVector"].toString().toLocal8Bit()); - - // Decrypt encrypted part - const auto encryptedFile = fileObj["encrypted"].toString().toLocal8Bit(); - const auto decryptedFile = decryptJsonObject(encryptedFile, _metadataKey); - const auto decryptedFileDoc = QJsonDocument::fromJson(decryptedFile); - - const auto decryptedFileObj = decryptedFileDoc.object(); + /* Initialise key and IV */ + if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *)key.constData(), (const unsigned char *)iv.constData())) { + qCInfo(lcCse()) << "Could not set key and iv"; + return false; + } - if (decryptedFileObj["filename"].toString().isEmpty()) { - qCDebug(lcCse) << "decrypted metadata" << decryptedFileDoc.toJson(QJsonDocument::Indented); - qCWarning(lcCse) << "skipping encrypted file" << file.encryptedFilename << "metadata has an empty file name"; - continue; - } + QByteArray out(blockSize + OCC::Constants::e2EeTagSize - 1, '\0'); + int len = 0; - file.originalFilename = decryptedFileObj["filename"].toString(); - file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); - file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); + qCDebug(lcCse) << "Starting to encrypt the file" << input->fileName() << input->atEnd(); + while(!input->atEnd()) { + const auto data = input->read(blockSize); - // 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"); + if (data.size() == 0) { + qCInfo(lcCse()) << "Could not read data from file"; + return false; } - qCDebug(lcCseMetadata) << "encrypted file" << decryptedFileObj["filename"].toString() << decryptedFileObj["key"].toString() << it.key(); - - _files.push_back(file); - } - - if (!migratedMetadata && !checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { - qCInfo(lcCseMetadata) << "checksum comparison failed" << "server value" << metadataKeyChecksum << "client value" << computeMetadataKeyChecksum(metadataKey); - if (_account->shouldSkipE2eeMetadataChecksumValidation()) { - qCDebug(lcCseMetadata) << "shouldSkipE2eeMetadataChecksumValidation is set. Allowing invalid checksum until next sync."; - _encryptedMetadataNeedUpdate = true; - } else { - _metadataKey.clear(); - _files.clear(); - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return; + if(!EVP_EncryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { + qCInfo(lcCse()) << "Could not encrypt"; + return false; } - } - - // decryption finished, create new metadata key to be used for encryption - _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); - _isMetadataSetup = true; - if (migratedMetadata) { - _encryptedMetadataNeedUpdate = true; + output->write(out, len); } -} -// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. -QByteArray FolderMetadata::encryptData(const QByteArray& data) const -{ - Bio publicKeyBio; - QByteArray publicKeyPem = _account->e2e()->_publicKey.toPem(); - BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); - auto publicKey = ClientSideEncryption::PKey::readPublicKey(publicKeyBio); - - // The metadata key is binary so base64 encode it first - return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64()); -} - -QByteArray FolderMetadata::decryptData(const QByteArray &data) const -{ - Bio privateKeyBio; - QByteArray privateKeyPem = _account->e2e()->_privateKey; - BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); - auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio); - - // Also base64 decode the result - QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data)); - - if (decryptResult.isEmpty()) - { - qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return {}; + if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(out), &len)) { + qCInfo(lcCse()) << "Could finalize encryption"; + return false; } - return QByteArray::fromBase64(decryptResult); -} - -QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data, - const QByteArray &key, - const QByteArray &authenticationTag, - const QByteArray &initializationVector) const -{ - // Also base64 decode the result - QByteArray decryptResult = EncryptionHelper::decryptStringSymmetric(QByteArray::fromBase64(key), - data + '|' + initializationVector + '|' + authenticationTag); + output->write(out, len); - if (decryptResult.isEmpty()) - { - qCDebug(lcCse()) << "ERROR. Could not decrypt"; - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return {}; + /* Get the e2EeTag */ + QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0'); + if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) { + qCInfo(lcCse()) << "Could not get e2EeTag"; + return false; } - return decryptResult; -} - -// AES/GCM/NoPadding (128 bit key size) -QByteArray FolderMetadata::encryptJsonObject(const QByteArray& obj, const QByteArray pass) const -{ - return EncryptionHelper::encryptStringSymmetric(pass, obj); -} + returnTag = e2EeTag; + output->write(e2EeTag, OCC::Constants::e2EeTagSize); -QByteArray FolderMetadata::decryptJsonObject(const QByteArray& encryptedMetadata, const QByteArray& pass) const -{ - return EncryptionHelper::decryptStringSymmetric(pass, encryptedMetadata); + input->close(); + output->close(); + qCDebug(lcCse) << "File Encrypted Successfully"; + return true; } -bool FolderMetadata::checkMetadataKeyChecksum(const QByteArray &metadataKey, - const QByteArray &metadataKeyChecksum) const +bool EncryptionHelper::fileDecryption(const QByteArray &key, const QByteArray& iv, + QFile *input, QFile *output) { - const auto referenceMetadataKeyValue = computeMetadataKeyChecksum(metadataKey); - - return referenceMetadataKeyValue == metadataKeyChecksum; -} + input->open(QIODevice::ReadOnly); + output->open(QIODevice::WriteOnly); -QByteArray FolderMetadata::computeMetadataKeyChecksum(const QByteArray &metadataKey) const -{ - auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; + // Init + CipherCtx ctx; - hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8()); - auto sortedFiles = _files; - std::sort(sortedFiles.begin(), sortedFiles.end(), [] (const auto &first, const auto &second) { - return first.encryptedFilename < second.encryptedFilename; - }); - for (const auto &singleFile : sortedFiles) { - hashAlgorithm.addData(singleFile.encryptedFilename.toUtf8()); + /* Create and initialise the context */ + if(!ctx) { + qCInfo(lcCse()) << "Could not create context"; + return false; } - hashAlgorithm.addData(metadataKey); - - return hashAlgorithm.result().toHex(); -} - -bool FolderMetadata::isMetadataSetup() const -{ - return _isMetadataSetup; -} - -void FolderMetadata::setupEmptyMetadata() { - qCDebug(lcCse) << "Settint up empty metadata"; - _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); - QString publicKey = _account->e2e()->_publicKey.toPem().toBase64(); - QString displayName = _account->displayName(); - _sharing.append({displayName, publicKey}); - - _isMetadataSetup = true; -} - -QByteArray FolderMetadata::encryptedMetadata() const { - qCDebug(lcCse) << "Generating metadata"; - - if (_metadataKey.isEmpty()) { - qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!"; - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - return {}; + /* Initialise the decryption operation. */ + if(!EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { + qCInfo(lcCse()) << "Could not init cipher"; + return false; } - const auto version = _account->capabilities().clientSideEncryptionVersion(); - const auto encryptedMetadataKey = encryptData(_metadataKey.toBase64()); - QJsonObject metadata{ - {"version", version}, - {metadataKeyJsonKey, QJsonValue::fromVariant(encryptedMetadataKey)}, - {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(encryptedMetadataKey))}, - }; - QJsonObject files; - for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; it++) { - QJsonObject encrypted; - encrypted.insert("key", QString(it->encryptionKey.toBase64())); - encrypted.insert("filename", it->originalFilename); - encrypted.insert("mimetype", QString(it->mimetype)); - QJsonDocument encryptedDoc; - encryptedDoc.setObject(encrypted); - - QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey); - if (encryptedEncrypted.isEmpty()) { - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - qCDebug(lcCse) << "Metadata generation failed!"; - } - QJsonObject file; - file.insert("encrypted", encryptedEncrypted); - file.insert("initializationVector", QString(it->initializationVector.toBase64())); - file.insert("authenticationTag", QString(it->authenticationTag.toBase64())); + EVP_CIPHER_CTX_set_padding(ctx, 0); - files.insert(it->encryptedFilename, file); + /* Set IV length. */ + if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { + qCInfo(lcCse()) << "Could not set iv length"; + return false; } - QJsonObject filedrop; - for (auto fileDropIt = _fileDrop.constBegin(), end = _fileDrop.constEnd(); fileDropIt != end; ++fileDropIt) { - filedrop.insert(fileDropIt.key(), fileDropIt.value()); + /* Initialise key and IV */ + if(!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *) key.constData(), (const unsigned char *) iv.constData())) { + qCInfo(lcCse()) << "Could not set key and iv"; + return false; } - auto metaObject = QJsonObject{ - {"metadata", metadata}, - }; + qint64 size = input->size() - OCC::Constants::e2EeTagSize; - if (files.count()) { - metaObject.insert("files", files); - } + QByteArray out(blockSize + OCC::Constants::e2EeTagSize - 1, '\0'); + int len = 0; - if (filedrop.count()) { - metaObject.insert("filedrop", filedrop); - } + while(input->pos() < size) { - QJsonDocument internalMetadata; - internalMetadata.setObject(metaObject); - return internalMetadata.toJson(); -} + auto toRead = size - input->pos(); + if (toRead > blockSize) { + toRead = blockSize; + } -void FolderMetadata::addEncryptedFile(const EncryptedFile &f) { + QByteArray data = input->read(toRead); - for (int i = 0; i < _files.size(); i++) { - if (_files.at(i).originalFilename == f.originalFilename) { - _files.removeAt(i); - break; + if (data.size() == 0) { + qCInfo(lcCse()) << "Could not read data from file"; + return false; } - } - _files.append(f); -} -void FolderMetadata::removeEncryptedFile(const EncryptedFile &f) -{ - for (int i = 0; i < _files.size(); i++) { - if (_files.at(i).originalFilename == f.originalFilename) { - _files.removeAt(i); - break; + if(!EVP_DecryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { + qCInfo(lcCse()) << "Could not decrypt"; + return false; } - } -} - -void FolderMetadata::removeAllEncryptedFiles() -{ - _files.clear(); -} - -QVector FolderMetadata::files() const { - return _files; -} -bool FolderMetadata::isFileDropPresent() const -{ - return _fileDrop.size() > 0; -} + output->write(out, len); + } -bool FolderMetadata::encryptedMetadataNeedUpdate() const -{ - return _encryptedMetadataNeedUpdate; -} + const QByteArray e2EeTag = input->read(OCC::Constants::e2EeTagSize); -bool FolderMetadata::moveFromFileDropToFiles() -{ - if (_fileDrop.isEmpty()) { + /* Set expected e2EeTag value. Works in OpenSSL 1.0.1d and later */ + if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, e2EeTag.size(), (unsigned char *)e2EeTag.constData())) { + qCInfo(lcCse()) << "Could not set expected e2EeTag"; return false; } - for (auto it = _fileDrop.begin(); it != _fileDrop.end(); ) { - const auto fileObject = it.value().toObject(); - - const auto decryptedKey = decryptData(fileObject["encryptedKey"].toString().toLocal8Bit()); - const auto decryptedAuthenticationTag = fileObject["encryptedTag"].toString().toLocal8Bit(); - const auto decryptedInitializationVector = fileObject["encryptedInitializationVector"].toString().toLocal8Bit(); - - if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) { - qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key(); - _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); - continue; - } - - const auto encryptedFile = fileObject["encrypted"].toString().toLocal8Bit(); - const auto decryptedFile = decryptDataUsingKey(encryptedFile, decryptedKey, decryptedAuthenticationTag, decryptedInitializationVector); - const auto decryptedFileDocument = QJsonDocument::fromJson(decryptedFile); - const auto decryptedFileObject = decryptedFileDocument.object(); - const auto authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit()); - const auto initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit()); - - EncryptedFile file; - file.encryptedFilename = it.key(); - file.authenticationTag = authenticationTag; - file.initializationVector = initializationVector; - - file.originalFilename = decryptedFileObject["filename"].toString(); - file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit()); - file.mimetype = decryptedFileObject["mimetype"].toString().toLocal8Bit(); - - // 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); - it = _fileDrop.erase(it); + if(1 != EVP_DecryptFinal_ex(ctx, unsignedData(out), &len)) { + qCInfo(lcCse()) << "Could finalize decryption"; + return false; } + output->write(out, len); + input->close(); + output->close(); return true; } -QJsonObject FolderMetadata::fileDrop() const +bool EncryptionHelper::dataEncryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output, QByteArray &returnTag) { - return _fileDropFromServer; -} + if (input.isEmpty()) { + qCDebug(lcCse) << "Could not use empty input data"; + } -bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag) -{ - if (!input->open(QIODevice::ReadOnly)) { - qCDebug(lcCse) << "Could not open input file for reading" << input->errorString(); + QByteArray inputCopy = input; + + QBuffer inputBuffer(&inputCopy); + if (!inputBuffer.open(QIODevice::ReadOnly)) { + qCDebug(lcCse) << "Could not open input buffer for reading" << inputBuffer.errorString(); } - if (!output->open(QIODevice::WriteOnly)) { - qCDebug(lcCse) << "Could not oppen output file for writing" << output->errorString(); + + QBuffer outputBuffer(&output); + if (!outputBuffer.open(QIODevice::WriteOnly)) { + qCDebug(lcCse) << "Could not oppen output buffer for writing" << outputBuffer.errorString(); } // Init CipherCtx ctx; /* Create and initialise the context */ - if(!ctx) { + if (!ctx) { qCInfo(lcCse()) << "Could not create context"; return false; } /* Initialise the decryption operation. */ - if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { + if (!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { qCInfo(lcCse()) << "Could not init cipher"; return false; } @@ -2159,13 +1930,13 @@ bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &i EVP_CIPHER_CTX_set_padding(ctx, 0); /* Set IV length. */ - if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { qCInfo(lcCse()) << "Could not set iv length"; return false; } /* Initialise key and IV */ - if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *)key.constData(), (const unsigned char *)iv.constData())) { + if (!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *)key.constData(), (const unsigned char *)iv.constData())) { qCInfo(lcCse()) << "Could not set key and iv"; return false; } @@ -2173,62 +1944,75 @@ bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &i QByteArray out(blockSize + OCC::Constants::e2EeTagSize - 1, '\0'); int len = 0; - qCDebug(lcCse) << "Starting to encrypt the file" << input->fileName() << input->atEnd(); - while(!input->atEnd()) { - const auto data = input->read(blockSize); + qCDebug(lcCse) << "Starting to encrypt a buffer"; + + while (!inputBuffer.atEnd()) { + const auto data = inputBuffer.read(blockSize); if (data.size() == 0) { qCInfo(lcCse()) << "Could not read data from file"; return false; } - if(!EVP_EncryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { + if (!EVP_EncryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { qCInfo(lcCse()) << "Could not encrypt"; return false; } - output->write(out, len); + outputBuffer.write(out, len); } - if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(out), &len)) { + if (1 != EVP_EncryptFinal_ex(ctx, unsignedData(out), &len)) { qCInfo(lcCse()) << "Could finalize encryption"; return false; } - output->write(out, len); + outputBuffer.write(out, len); /* Get the e2EeTag */ QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0'); - if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) { + if (1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) { qCInfo(lcCse()) << "Could not get e2EeTag"; return false; } returnTag = e2EeTag; - output->write(e2EeTag, OCC::Constants::e2EeTagSize); + outputBuffer.write(e2EeTag, OCC::Constants::e2EeTagSize); - input->close(); - output->close(); - qCDebug(lcCse) << "File Encrypted Successfully"; + inputBuffer.close(); + outputBuffer.close(); + qCDebug(lcCse) << "Buffer Encrypted Successfully"; return true; } -bool EncryptionHelper::fileDecryption(const QByteArray &key, const QByteArray& iv, - QFile *input, QFile *output) +bool EncryptionHelper::dataDecryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output) { - input->open(QIODevice::ReadOnly); - output->open(QIODevice::WriteOnly); + if (input.isEmpty()) { + qCDebug(lcCse) << "Could not use empty input data"; + } + + QByteArray inputCopy = input; + + QBuffer inputBuffer(&inputCopy); + if (!inputBuffer.open(QIODevice::ReadOnly)) { + qCDebug(lcCse) << "Could not open input buffer for reading" << inputBuffer.errorString(); + } + + QBuffer outputBuffer(&output); + if (!outputBuffer.open(QIODevice::WriteOnly)) { + qCDebug(lcCse) << "Could not oppen output buffer for writing" << outputBuffer.errorString(); + } // Init CipherCtx ctx; /* Create and initialise the context */ - if(!ctx) { + if (!ctx) { qCInfo(lcCse()) << "Could not create context"; return false; } /* Initialise the decryption operation. */ - if(!EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { + if (!EVP_DecryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { qCInfo(lcCse()) << "Could not init cipher"; return false; } @@ -2236,63 +2020,116 @@ bool EncryptionHelper::fileDecryption(const QByteArray &key, const QByteArray& i EVP_CIPHER_CTX_set_padding(ctx, 0); /* Set IV length. */ - if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { qCInfo(lcCse()) << "Could not set iv length"; return false; } /* Initialise key and IV */ - if(!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *) key.constData(), (const unsigned char *) iv.constData())) { + if (!EVP_DecryptInit_ex(ctx, nullptr, nullptr, (const unsigned char *)key.constData(), (const unsigned char *)iv.constData())) { qCInfo(lcCse()) << "Could not set key and iv"; return false; } - qint64 size = input->size() - OCC::Constants::e2EeTagSize; + qint64 size = inputBuffer.size() - OCC::Constants::e2EeTagSize; QByteArray out(blockSize + OCC::Constants::e2EeTagSize - 1, '\0'); int len = 0; - while(input->pos() < size) { - - auto toRead = size - input->pos(); + while (inputBuffer.pos() < size) { + auto toRead = size - inputBuffer.pos(); if (toRead > blockSize) { toRead = blockSize; } - QByteArray data = input->read(toRead); + QByteArray data = inputBuffer.read(toRead); if (data.size() == 0) { qCInfo(lcCse()) << "Could not read data from file"; return false; } - if(!EVP_DecryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { + if (!EVP_DecryptUpdate(ctx, unsignedData(out), &len, (unsigned char *)data.constData(), data.size())) { qCInfo(lcCse()) << "Could not decrypt"; return false; } - output->write(out, len); + outputBuffer.write(out, len); } - const QByteArray e2EeTag = input->read(OCC::Constants::e2EeTagSize); + const QByteArray e2EeTag = inputBuffer.read(OCC::Constants::e2EeTagSize); /* Set expected e2EeTag value. Works in OpenSSL 1.0.1d and later */ - if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, e2EeTag.size(), (unsigned char *)e2EeTag.constData())) { + if (!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, e2EeTag.size(), (unsigned char *)e2EeTag.constData())) { qCInfo(lcCse()) << "Could not set expected e2EeTag"; return false; } - if(1 != EVP_DecryptFinal_ex(ctx, unsignedData(out), &len)) { + if (1 != EVP_DecryptFinal_ex(ctx, unsignedData(out), &len)) { qCInfo(lcCse()) << "Could finalize decryption"; return false; } - output->write(out, len); + outputBuffer.write(out, len); - input->close(); - output->close(); + inputBuffer.close(); + outputBuffer.close(); return true; } +QByteArray EncryptionHelper::gzipThenEncryptData(const QByteArray &key, const QByteArray &inputData, const QByteArray &iv, QByteArray &returnTag) +{ + QBuffer gZipBuffer; + auto gZipCompressionDevice = KCompressionDevice(&gZipBuffer, false, KCompressionDevice::GZip); + if (!gZipCompressionDevice.open(QIODevice::WriteOnly)) { + return {}; + } + const auto bytesWritten = gZipCompressionDevice.write(inputData); + gZipCompressionDevice.close(); + if (bytesWritten < 0) { + return {}; + } + + if (!gZipBuffer.open(QIODevice::ReadOnly)) { + return {}; + } + + QByteArray outputData; + returnTag.clear(); + const auto gZippedAndNotEncrypted = gZipBuffer.readAll(); + EncryptionHelper::dataEncryption(key, iv, gZippedAndNotEncrypted, outputData, returnTag); + gZipBuffer.close(); + return outputData; +} + +QByteArray EncryptionHelper::decryptThenUnGzipData(const QByteArray &key, const QByteArray &inputData, const QByteArray &iv) +{ + QByteArray decryptedAndUnGzipped; + if (!EncryptionHelper::dataDecryption(key, iv, inputData, decryptedAndUnGzipped)) { + qCDebug(lcCse()) << "Could not decrypt"; + return {}; + } + + QBuffer gZipBuffer; + if (!gZipBuffer.open(QIODevice::WriteOnly)) { + return {}; + } + const auto bytesWritten = gZipBuffer.write(decryptedAndUnGzipped); + gZipBuffer.close(); + if (bytesWritten < 0) { + return {}; + } + + auto gZipUnCompressionDevice = KCompressionDevice(&gZipBuffer, false, KCompressionDevice::GZip); + if (!gZipUnCompressionDevice.open(QIODevice::ReadOnly)) { + return {}; + } + + decryptedAndUnGzipped = gZipUnCompressionDevice.readAll(); + gZipUnCompressionDevice.close(); + + return decryptedAndUnGzipped; +} + EncryptionHelper::StreamingDecryptor::StreamingDecryptor(const QByteArray &key, const QByteArray &iv, quint64 totalSize) : _totalSize(totalSize) { if (_ctx && !key.isEmpty() && !iv.isEmpty() && totalSize > 0) { diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index a4a4c8be4f5ca..be9ba70248498 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -1,6 +1,8 @@ #ifndef CLIENTSIDEENCRYPTION_H #define CLIENTSIDEENCRYPTION_H +#include "clientsideencryptionprimitives.h" + #include #include #include @@ -24,10 +26,10 @@ class ReadPasswordJob; namespace OCC { -QString e2eeBaseUrl(); +QString e2eeBaseUrl(const OCC::AccountPtr &account); namespace EncryptionHelper { - QByteArray generateRandomFilename(); + OWNCLOUDSYNC_EXPORT QByteArray generateRandomFilename(); OWNCLOUDSYNC_EXPORT QByteArray generateRandom(int size); QByteArray generatePassword(const QString &wordlist, const QByteArray& salt); OWNCLOUDSYNC_EXPORT QByteArray encryptPrivateKey( @@ -69,6 +71,12 @@ namespace EncryptionHelper { OWNCLOUDSYNC_EXPORT bool fileDecryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output); + OWNCLOUDSYNC_EXPORT bool dataEncryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output, QByteArray &returnTag); + OWNCLOUDSYNC_EXPORT bool dataDecryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output); + + OWNCLOUDSYNC_EXPORT QByteArray gzipThenEncryptData(const QByteArray &key, const QByteArray &inputData, const QByteArray &iv, QByteArray &returnTag); + OWNCLOUDSYNC_EXPORT QByteArray decryptThenUnGzipData(const QByteArray &key, const QByteArray &inputData, const QByteArray &iv); + // // Simple classes for safe (RAII) handling of OpenSSL // data structures @@ -119,8 +127,6 @@ class OWNCLOUDSYNC_EXPORT StreamingDecryptor class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { Q_OBJECT public: - class PKey; - ClientSideEncryption(); QByteArray _privateKey; @@ -136,10 +142,20 @@ class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { void certificateDeleted(); void mnemonicDeleted(); void publicKeyDeleted(); + void certificateFetchedFromKeychain(QSslCertificate certificate); + void certificatesFetchedFromServer(const QHash &results); + void certificateWriteComplete(const QSslCertificate &certificate); + +public: + [[nodiscard]] QByteArray generateSignatureCryptographicMessageSyntax(const QByteArray &data) const; + [[nodiscard]] bool verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector &certificatePems) const; public slots: void initialize(const OCC::AccountPtr &account); void forgetSensitiveData(const OCC::AccountPtr &account); + void getUsersPublicKeyFromServer(const AccountPtr &account, const QStringList &userIds); + void fetchCertificateFromKeyChain(const OCC::AccountPtr &account, const QString &userId); + void writeCertificate(const AccountPtr &account, const QString &userId, const QSslCertificate &certificate); private slots: void generateKeyPair(const OCC::AccountPtr &account); @@ -147,6 +163,7 @@ private slots: void publicCertificateFetched(QKeychain::Job *incoming); void publicKeyFetched(QKeychain::Job *incoming); + void publicKeyFetchedForUserId(QKeychain::Job *incoming); void privateKeyFetched(QKeychain::Job *incoming); void mnemonicKeyFetched(QKeychain::Job *incoming); @@ -211,79 +228,5 @@ private slots: bool isInitialized = false; }; - -/* Generates the Metadata for the folder */ -struct EncryptedFile { - QByteArray encryptionKey; - QByteArray mimetype; - QByteArray initializationVector; - QByteArray authenticationTag; - QString encryptedFilename; - QString originalFilename; -}; - -class OWNCLOUDSYNC_EXPORT FolderMetadata { -public: - enum class RequiredMetadataVersion { - Version1, - Version1_2, - }; - - explicit FolderMetadata(AccountPtr account); - - explicit FolderMetadata(AccountPtr account, - RequiredMetadataVersion requiredMetadataVersion, - const QByteArray& metadata, - int statusCode = -1); - - [[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 encryptedMetadataNeedUpdate() const; - - [[nodiscard]] bool moveFromFileDropToFiles(); - - [[nodiscard]] QJsonObject fileDrop() const; - -private: - /* Use std::string and std::vector internally on this class - * to ease the port to Nlohmann Json API - */ - void setupEmptyMetadata(); - void setupExistingMetadata(const QByteArray& metadata); - - [[nodiscard]] QByteArray encryptData(const QByteArray &data) const; - [[nodiscard]] QByteArray decryptData(const QByteArray &data) const; - [[nodiscard]] QByteArray decryptDataUsingKey(const QByteArray &data, - const QByteArray &key, - const QByteArray &authenticationTag, - const QByteArray &initializationVector) const; - - [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; - [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; - - [[nodiscard]] bool checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const; - - [[nodiscard]] QByteArray computeMetadataKeyChecksum(const QByteArray &metadataKey) const; - - QByteArray _metadataKey; - - QVector _files; - AccountPtr _account; - RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2; - QVector> _sharing; - QJsonObject _fileDrop; - // used by unit tests, must get assigned simultaneously with _fileDrop and not erased - QJsonObject _fileDropFromServer; - bool _isMetadataSetup = false; - bool _encryptedMetadataNeedUpdate = false; -}; - } // namespace OCC #endif diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index 31d3b7377e111..d39c393d1777b 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -23,15 +23,26 @@ Q_LOGGING_CATEGORY(lcSignPublicKeyApiJob, "nextcloud.sync.networkjob.sendcsr", Q Q_LOGGING_CATEGORY(lcStorePrivateKeyApiJob, "nextcloud.sync.networkjob.storeprivatekey", QtInfoMsg) Q_LOGGING_CATEGORY(lcCseJob, "nextcloud.sync.networkjob.clientsideencrypt", QtInfoMsg) +namespace +{ +constexpr auto e2eeSignatureHeaderName = "X-NC-E2EE-SIGNATURE"; +} + namespace OCC { GetMetadataApiJob::GetMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) + : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent) + , _fileId(fileId) { } +const QByteArray &GetMetadataApiJob::signature() const +{ + return _signature; +} + void GetMetadataApiJob::start() { QNetworkRequest req; @@ -54,6 +65,9 @@ bool GetMetadataApiJob::finished() emit error(_fileId, retCode); return true; } + if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + _signature = reply()->rawHeader(e2eeSignatureHeaderName); + } QJsonParseError error{}; const auto replyData = reply()->readAll(); auto json = QJsonDocument::fromJson(replyData, &error); @@ -64,9 +78,15 @@ bool GetMetadataApiJob::finished() StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account, const QByteArray& fileId, + const QByteArray &token, const QByteArray& b64Metadata, + const QByteArray &signature, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata) +: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent), +_fileId(fileId), +_token(token), +_b64Metadata(b64Metadata), +_signature(signature) { } @@ -75,8 +95,18 @@ void StoreMetaDataApiJob::start() QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); + if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + if (!_signature.isEmpty()) { + req.setRawHeader(e2eeSignatureHeaderName, _signature); + } + } QUrlQuery query; query.addQueryItem(QLatin1String("format"), QLatin1String("json")); + if (_account->capabilities().clientSideEncryptionVersion() < 2.0) { + query.addQueryItem(QStringLiteral("e2e-token"), _token); + } else { + req.setRawHeader(QByteArrayLiteral("e2e-token"), _token); + } QUrl url = Utility::concatUrlPath(account()->url(), path()); url.setQuery(query); @@ -95,8 +125,8 @@ bool StoreMetaDataApiJob::finished() if (retCode != 200) { qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode; emit error(_fileId, retCode); + return false; } - qCInfo(lcCseJob()) << "Metadata submitted to the server successfully"; emit success(_fileId); return true; @@ -106,11 +136,13 @@ UpdateMetadataApiJob::UpdateMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& b64Metadata, const QByteArray& token, + const QByteArray& signature, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent) +: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent) , _fileId(fileId), _b64Metadata(b64Metadata), -_token(token) +_token(token), +_signature(signature) { } @@ -120,16 +152,26 @@ void UpdateMetadataApiJob::start() req.setRawHeader("OCS-APIREQUEST", "true"); req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); + if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + if (!_signature.isEmpty()) { + req.setRawHeader(e2eeSignatureHeaderName, _signature); + } + } + QUrlQuery urlQuery; urlQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("json")); - urlQuery.addQueryItem(QStringLiteral("e2e-token"), _token); + + if (_account->capabilities().clientSideEncryptionVersion() < 2.0) { + urlQuery.addQueryItem(QStringLiteral("e2e-token"), _token); + } else { + req.setRawHeader(QByteArrayLiteral("e2e-token"), _token); + } QUrl url = Utility::concatUrlPath(account()->url(), path()); url.setQuery(urlQuery); QUrlQuery params; params.addQueryItem("metaData",QUrl::toPercentEncoding(_b64Metadata)); - params.addQueryItem("e2e-token", _token); QByteArray data = params.query().toLocal8Bit(); auto buffer = new QBuffer(this); @@ -146,6 +188,7 @@ bool UpdateMetadataApiJob::finished() if (retCode != 200) { qCInfo(lcCseJob()) << "error updating the metadata" << path() << errorString() << retCode; emit error(_fileId, retCode); + return false; } qCInfo(lcCseJob()) << "Metadata submitted to the server successfully"; @@ -158,7 +201,7 @@ UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& token, SyncJournalDb *journalDb, QObject* parent) - : AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent) + : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent) , _fileId(fileId) , _token(token) , _journalDb(journalDb) @@ -172,6 +215,13 @@ void UnlockEncryptFolderApiJob::start() req.setRawHeader("e2e-token", _token); QUrl url = Utility::concatUrlPath(account()->url(), path()); + + if (shouldRollbackMetadataChanges()) { + QUrlQuery query(url); + query.addQueryItem(QLatin1String("abort"), QLatin1String("true")); + url.setQuery(query); + } + sendRequest("DELETE", url, req); AbstractNetworkJob::start(); @@ -180,6 +230,16 @@ void UnlockEncryptFolderApiJob::start() qCInfo(lcCseJob()) << "unlock folder started for:" << path() << " for fileId: " << _fileId; } +void UnlockEncryptFolderApiJob::setShouldRollbackMetadataChanges(bool shouldRollbackMetadataChanges) +{ + _shouldRollbackMetadataChanges = shouldRollbackMetadataChanges; +} + +[[nodiscard]] bool UnlockEncryptFolderApiJob::shouldRollbackMetadataChanges() const +{ + return _shouldRollbackMetadataChanges; +} + bool UnlockEncryptFolderApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); @@ -198,15 +258,16 @@ bool UnlockEncryptFolderApiJob::finished() emit error(_fileId, retCode, errorString()); return true; } + emit success(_fileId); return true; } -DeleteMetadataApiJob::DeleteMetadataApiJob(const AccountPtr& account, - const QByteArray& fileId, - QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId) +DeleteMetadataApiJob::DeleteMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray &token, QObject* parent) +: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent), +_fileId(fileId), +_token(token) { } @@ -214,6 +275,7 @@ void DeleteMetadataApiJob::start() { QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); + req.setRawHeader(QByteArrayLiteral("e2e-token"), _token); QUrl url = Utility::concatUrlPath(account()->url(), path()); sendRequest("DELETE", url, req); @@ -240,7 +302,7 @@ LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr &account, SyncJournalDb *journalDb, const QSslKey publicKey, QObject *parent) - : AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent) + : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent) , _fileId(fileId) , _journalDb(journalDb) , _publicKey(publicKey) @@ -255,6 +317,7 @@ void LockEncryptFolderApiJob::start() qCInfo(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId << " but we need to first lift the previous lock"; const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, folderTokenEncrypted); const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, folderToken, _journalDb, this); + unlockJob->setShouldRollbackMetadataChanges(true); connect(unlockJob, &UnlockEncryptFolderApiJob::done, this, [this]() { this->start(); }); @@ -264,12 +327,18 @@ void LockEncryptFolderApiJob::start() QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); + if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + if (_counter > 0) { + req.setRawHeader("X-NC-E2EE-COUNTER", QByteArray::number(_counter)); + } + } QUrlQuery query; query.addQueryItem(QLatin1String("format"), QLatin1String("json")); QUrl url = Utility::concatUrlPath(account()->url(), path()); url.setQuery(query); qCInfo(lcCseJob()) << "locking the folder with id" << _fileId << "as encrypted"; + sendRequest("POST", url, req); AbstractNetworkJob::start(); @@ -305,8 +374,13 @@ bool LockEncryptFolderApiJob::finished() return true; } +void LockEncryptFolderApiJob::setCounter(quint64 counter) +{ + _counter = counter; +} + SetEncryptionFlagApiJob::SetEncryptionFlagApiJob(const AccountPtr& account, const QByteArray& fileId, FlagAction flagAction, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction) +: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction) { } diff --git a/src/libsync/clientsideencryptionjobs.h b/src/libsync/clientsideencryptionjobs.h index 7fcbac10d1d76..9052e9bbcb420 100644 --- a/src/libsync/clientsideencryptionjobs.h +++ b/src/libsync/clientsideencryptionjobs.h @@ -147,6 +147,8 @@ class OWNCLOUDSYNC_EXPORT LockEncryptFolderApiJob : public AbstractNetworkJob public: explicit LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, SyncJournalDb *journalDb, const QSslKey publicKey, QObject *parent = nullptr); + void setCounter(const quint64 counter); + public slots: void start() override; @@ -163,6 +165,7 @@ public slots: QByteArray _fileId; QPointer _journalDb; QSslKey _publicKey; + quint64 _counter = 0; }; @@ -177,8 +180,11 @@ class OWNCLOUDSYNC_EXPORT UnlockEncryptFolderApiJob : public AbstractNetworkJob SyncJournalDb *journalDb, QObject *parent = nullptr); + [[nodiscard]] bool shouldRollbackMetadataChanges() const; + public slots: void start() override; + void setShouldRollbackMetadataChanges(bool shouldRollbackMetadataChanges); protected: bool finished() override; @@ -195,6 +201,7 @@ public slots: QByteArray _token; QBuffer *_tokenBuf = nullptr; QPointer _journalDb; + bool _shouldRollbackMetadataChanges = false; }; @@ -205,7 +212,9 @@ class OWNCLOUDSYNC_EXPORT StoreMetaDataApiJob : public AbstractNetworkJob explicit StoreMetaDataApiJob ( const AccountPtr &account, const QByteArray& fileId, + const QByteArray &token, const QByteArray& b64Metadata, + const QByteArray &signature, QObject *parent = nullptr); public slots: @@ -220,7 +229,9 @@ public slots: private: QByteArray _fileId; + QByteArray _token; QByteArray _b64Metadata; + QByteArray _signature; }; class OWNCLOUDSYNC_EXPORT UpdateMetadataApiJob : public AbstractNetworkJob @@ -232,6 +243,7 @@ class OWNCLOUDSYNC_EXPORT UpdateMetadataApiJob : public AbstractNetworkJob const QByteArray& fileId, const QByteArray& b64Metadata, const QByteArray& lockedToken, + const QByteArray &signature, QObject *parent = nullptr); public slots: @@ -248,6 +260,7 @@ public slots: QByteArray _fileId; QByteArray _b64Metadata; QByteArray _token; + QByteArray _signature; }; @@ -260,6 +273,8 @@ class OWNCLOUDSYNC_EXPORT GetMetadataApiJob : public AbstractNetworkJob const QByteArray& fileId, QObject *parent = nullptr); + [[nodiscard]] const QByteArray &signature() const; + public slots: void start() override; @@ -272,6 +287,7 @@ public slots: private: QByteArray _fileId; + QByteArray _signature; }; class OWNCLOUDSYNC_EXPORT DeleteMetadataApiJob : public AbstractNetworkJob @@ -281,6 +297,7 @@ class OWNCLOUDSYNC_EXPORT DeleteMetadataApiJob : public AbstractNetworkJob explicit DeleteMetadataApiJob ( const AccountPtr &account, const QByteArray& fileId, + const QByteArray& token, QObject *parent = nullptr); public slots: @@ -295,6 +312,7 @@ public slots: private: QByteArray _fileId; + QByteArray _token; }; } diff --git a/src/libsync/clientsideencryptionprimitives.cpp b/src/libsync/clientsideencryptionprimitives.cpp new file mode 100644 index 0000000000000..92210df5fc1ef --- /dev/null +++ b/src/libsync/clientsideencryptionprimitives.cpp @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2023 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 "clientsideencryptionprimitives.h" +#include + +namespace OCC +{ +Bio::Bio() + : _bio(BIO_new(BIO_s_mem())) +{ +} +Bio::~Bio() +{ + BIO_free_all(_bio); +} +Bio::operator const BIO *() const +{ + return _bio; +} + +Bio::operator BIO *() +{ + return _bio; +} + +PKeyCtx::PKeyCtx(int id, ENGINE *e) + : _ctx(EVP_PKEY_CTX_new_id(id, e)) +{ +} + +PKeyCtx::PKeyCtx(PKeyCtx &&other) +{ + std::swap(_ctx, other._ctx); +} + +PKeyCtx::~PKeyCtx() +{ + EVP_PKEY_CTX_free(_ctx); +} + +PKeyCtx PKeyCtx::forKey(EVP_PKEY *pkey, ENGINE *e) +{ + PKeyCtx ctx; + ctx._ctx = EVP_PKEY_CTX_new(pkey, e); + return ctx; +} + +PKeyCtx::operator EVP_PKEY_CTX *() +{ + return _ctx; +} + +PKey::~PKey() +{ + EVP_PKEY_free(_pkey); +} + +PKey::PKey(PKey &&other) +{ + std::swap(_pkey, other._pkey); +} + +PKey PKey::readPublicKey(Bio &bio) +{ + PKey result; + result._pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + return result; +} + +PKey PKey::readPrivateKey(Bio &bio) +{ + PKey result; + result._pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + return result; +} + +PKey PKey::generate(PKeyCtx &ctx) +{ + PKey result; + if (EVP_PKEY_keygen(ctx, &result._pkey) <= 0) { + result._pkey = nullptr; + } + return result; +} + +PKey::operator EVP_PKEY *() +{ + return _pkey; +} + +PKey::operator EVP_PKEY *() const +{ + return _pkey; +} + +} \ No newline at end of file diff --git a/src/libsync/clientsideencryptionprimitives.h b/src/libsync/clientsideencryptionprimitives.h new file mode 100644 index 0000000000000..28efc9b29c3df --- /dev/null +++ b/src/libsync/clientsideencryptionprimitives.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 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 +#include + +namespace OCC +{ +class Bio +{ +public: + Bio(); + + ~Bio(); + + operator const BIO *() const; + operator BIO *(); + +private: + Q_DISABLE_COPY(Bio) + + BIO *_bio; +}; + +class PKeyCtx +{ +public: + explicit PKeyCtx(int id, ENGINE *e = nullptr); + + ~PKeyCtx(); + + // The move constructor is needed for pre-C++17 where + // return-value optimization (RVO) is not obligatory + // and we have a `forKey` static function that returns + // an instance of this class + PKeyCtx(PKeyCtx &&other); + PKeyCtx &operator=(PKeyCtx &&other) = delete; + + static PKeyCtx forKey(EVP_PKEY *pkey, ENGINE *e = nullptr); + + operator EVP_PKEY_CTX *(); + +private: + Q_DISABLE_COPY(PKeyCtx) + + PKeyCtx() = default; + + EVP_PKEY_CTX *_ctx = nullptr; +}; + +class PKey +{ +public: + ~PKey(); + + // The move constructor is needed for pre-C++17 where + // return-value optimization (RVO) is not obligatory + // and we have a static functions that return + // an instance of this class + PKey(PKey &&other); + + PKey &operator=(PKey &&other) = delete; + + static PKey readPublicKey(Bio &bio); + + static PKey readPrivateKey(Bio &bio); + + static PKey generate(PKeyCtx &ctx); + + operator EVP_PKEY *(); + + operator EVP_PKEY *() const; + +private: + Q_DISABLE_COPY(PKey) + + PKey() = default; + + EVP_PKEY *_pkey = nullptr; +}; +} \ No newline at end of file diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index eb1686e5f608e..35bbaed4acbbe 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -628,6 +628,9 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadCookies = serverEntry.directDownloadCookies; item->_e2eEncryptionStatus = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted; + if (serverEntry.isE2eEncrypted()) { + item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); + } item->_encryptedFileName = [=] { if (serverEntry.e2eMangledName.isEmpty()) { return QString(); @@ -1019,6 +1022,13 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( item->_status = SyncFileItem::Status::NormalError; } + if (dbEntry.isValid() && item->isDirectory()) { + item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(dbEntry._e2eEncryptionStatus); + if (item->isEncrypted()) { + item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); + } + } + auto recurseQueryLocal = _queryLocal == ParentNotChanged ? ParentNotChanged : localEntry.isDirectory || item->_instruction == CSYNC_INSTRUCTION_RENAME ? NormalQuery : ParentDontExist; processFileFinalize(item, path, recurse, recurseQueryLocal, recurseQueryServer); }; @@ -1375,8 +1385,22 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus); + item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); } postProcessLocalNew(); + /*if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_NEW && item->_direction == SyncFileItem::Up + && _discoveryData->_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + OCC::SyncJournalFileRecord rec; + _discoveryData->_statedb->findEncryptedAncestorForRecord(item->_file, &rec); + if (rec.isValid() && rec._e2eEncryptionStatus >= OCC::SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV2_0) { + qCDebug(lcDisco) << "Attempting to create a subfolder in top-level E2EE V2 folder. Ignoring."; + item->_instruction = CSYNC_INSTRUCTION_IGNORE; + item->_direction = SyncFileItem::None; + item->_status = SyncFileItem::NormalError; + item->_errorString = tr("Creating nested encrypted folders is not supported yet."); + } + }*/ + finalize(); return; } @@ -1939,17 +1963,34 @@ void ProcessDirectoryJob::chopVirtualFileSuffix(QString &str) const DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() { + if (_dirItem && _dirItem->isEncrypted() && _dirItem->_encryptedFileName.isEmpty()) { + _discoveryData->_topLevelE2eeFolderPaths.insert(QLatin1Char('/') + _dirItem->_file); + } auto serverJob = new DiscoverySingleDirectoryJob(_discoveryData->_account, - _discoveryData->_remoteFolder + _currentFolder._server, this); - if (!_dirItem) + _discoveryData->_remoteFolder + _currentFolder._server, + _discoveryData->_topLevelE2eeFolderPaths, + this); + if (!_dirItem) { serverJob->setIsRootPath(); // query the fingerprint on the root + } + connect(serverJob, &DiscoverySingleDirectoryJob::etag, this, &ProcessDirectoryJob::etag); _discoveryData->_currentlyActiveJobs++; _pendingAsyncJobs++; connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) { if (_dirItem) { - _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); - _dirItem->_isEncryptedMetadataNeedUpdate = serverJob->encryptedMetadataNeedUpdate(); + if (_dirItem->isEncrypted()) { + _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); + + SyncJournalFileRecord record; + const auto alreadyDownloaded = _discoveryData->_statedb->getFileRecord(_dirItem->_file, &record) && record.isValid(); + // we need to make sure we first download all e2ee files/folders before migrating + _dirItem->_isEncryptedMetadataNeedUpdate = alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); + _dirItem->_e2eEncryptionStatus = serverJob->currentEncryptionStatus(); + _dirItem->_e2eEncryptionStatusRemote = serverJob->currentEncryptionStatus(); + _dirItem->_e2eEncryptionServerCapability = serverJob->requiredEncryptionStatus(); + _discoveryData->_anotherSyncNeeded = !alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); + } qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true; } _discoveryData->_currentlyActiveJobs--; diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 77a10b4fe244e..68e91bdd26f74 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -21,6 +21,7 @@ #include "account.h" #include "clientsideencryptionjobs.h" +#include "foldermetadata.h" #include "common/asserts.h" #include "common/checksums.h" @@ -363,10 +364,14 @@ void DiscoverySingleLocalDirectoryJob::run() { emit finished(results); } -DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, QObject *parent) +DiscoverySingleDirectoryJob::DiscoverySingleDirectoryJob(const AccountPtr &account, + const QString &path, + const QSet &topLevelE2eeFolderPaths, + QObject *parent) : QObject(parent) , _subPath(path) , _account(account) + , _topLevelE2eeFolderPaths(topLevelE2eeFolderPaths) { } @@ -435,6 +440,16 @@ bool DiscoverySingleDirectoryJob::encryptedMetadataNeedUpdate() const return _encryptedMetadataNeedUpdate; } +SyncFileItem::EncryptionStatus DiscoverySingleDirectoryJob::currentEncryptionStatus() const +{ + return _encryptionStatusCurrent; +} + +SyncFileItem::EncryptionStatus DiscoverySingleDirectoryJob::requiredEncryptionStatus() const +{ + return _encryptionStatusRequired; +} + static void propertyMapToRemoteInfo(const QMap &map, RemoteInfo &result) { for (auto it = map.constBegin(); it != map.constEnd(); ++it) { @@ -555,7 +570,7 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi _fileId = map.value("id").toUtf8(); } if (map.contains("is-encrypted") && map.value("is-encrypted") == QStringLiteral("1")) { - _isE2eEncrypted = SyncFileItem::EncryptionStatus::Encrypted; + _encryptionStatusCurrent = SyncFileItem::EncryptionStatus::Encrypted; Q_ASSERT(!_fileId.isEmpty()); } if (map.contains("size")) { @@ -646,39 +661,74 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in qCDebug(lcDiscovery) << "Metadata received, applying it to the result list"; Q_ASSERT(_subPath.startsWith('/')); - const auto metadata = FolderMetadata(_account, - _isE2eEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact), - statusCode); - _isFileDropDetected = metadata.isFileDropPresent(); - _encryptedMetadataNeedUpdate = metadata.encryptedMetadataNeedUpdate(); - - const auto encryptedFiles = metadata.files(); + const auto job = qobject_cast(sender()); + Q_ASSERT(job); + if (!job) { + qCDebug(lcDiscovery) << "metadataReceived must be called from GetMetadataApiJob's signal"; + emit finished(HttpError{0, tr("Encrypted metadata setup error!")}); + deleteLater(); + return; + } - const auto findEncryptedFile = [=](const QString &name) { - const auto it = std::find_if(std::cbegin(encryptedFiles), std::cend(encryptedFiles), [=](const EncryptedFile &file) { - return file.encryptedFilename == name; - }); - if (it == std::cend(encryptedFiles)) { - return Optional(); - } else { - return Optional(*it); + // as per E2EE V2, top level folder is the only source of encryption keys and users that have access to it + // hence, we need to find its path and pass to any subfolder's metadata, so it will fetch the top level metadata when needed + // see https://github.com/nextcloud/end_to_end_encryption_rfc/blob/v2.1/RFC.md + auto topLevelFolderPath = QStringLiteral("/"); + for (const QString &topLevelPath : _topLevelE2eeFolderPaths) { + if (_subPath == topLevelPath) { + topLevelFolderPath = QStringLiteral("/"); + break; } - }; + if (_subPath.startsWith(topLevelPath + QLatin1Char('/'))) { + const auto topLevelPathSplit = topLevelPath.split(QLatin1Char('/')); + topLevelFolderPath = topLevelPathSplit.join(QLatin1Char('/')); + break; + } + } - std::transform(std::cbegin(_results), std::cend(_results), std::begin(_results), [=](const RemoteInfo &info) { - auto result = info; - const auto encryptedFileInfo = findEncryptedFile(result.name); - if (encryptedFileInfo) { - result._isE2eEncrypted = true; - result.e2eMangledName = _subPath.mid(1) + QLatin1Char('/') + result.name; - result.name = encryptedFileInfo->originalFilename; + const auto e2EeFolderMetadata = new FolderMetadata(_account, + statusCode == 404 ? QByteArray{} : json.toJson(QJsonDocument::Compact), + RootEncryptedFolderInfo(topLevelFolderPath), + job->signature()); + connect(e2EeFolderMetadata, &FolderMetadata::setupComplete, this, [this, e2EeFolderMetadata] { + e2EeFolderMetadata->deleteLater(); + if (!e2EeFolderMetadata->isValid()) { + emit finished(HttpError{0, tr("Encrypted metadata setup error!")}); + deleteLater(); + return; } - return result; - }); + _isFileDropDetected = e2EeFolderMetadata->isFileDropPresent(); + _encryptedMetadataNeedUpdate = e2EeFolderMetadata->encryptedMetadataNeedUpdate(); + _encryptionStatusRequired = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion()); + _encryptionStatusCurrent = e2EeFolderMetadata->existingMetadataEncryptionStatus(); + + const auto encryptedFiles = e2EeFolderMetadata->files(); + + const auto findEncryptedFile = [=](const QString &name) { + const auto it = std::find_if(std::cbegin(encryptedFiles), std::cend(encryptedFiles), [=](const FolderMetadata::EncryptedFile &file) { + return file.encryptedFilename == name; + }); + if (it == std::cend(encryptedFiles)) { + return Optional(); + } else { + return Optional(*it); + } + }; + + std::transform(std::cbegin(_results), std::cend(_results), std::begin(_results), [=](const RemoteInfo &info) { + auto result = info; + const auto encryptedFileInfo = findEncryptedFile(result.name); + if (encryptedFileInfo) { + result._isE2eEncrypted = true; + result.e2eMangledName = _subPath.mid(1) + QLatin1Char('/') + result.name; + result.name = encryptedFileInfo->originalFilename; + } + return result; + }); - emit finished(_results); - deleteLater(); + emit finished(_results); + deleteLater(); + }); } void DiscoverySingleDirectoryJob::metadataError(const QByteArray &fileId, int httpReturnCode) diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 561e9ee9f08d6..ccad6bb92a361 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -132,6 +132,7 @@ private slots: public: }; +class FolderMetadata; /** * @brief Run a PROPFIND on a directory and process the results for Discovery @@ -142,13 +143,21 @@ class DiscoverySingleDirectoryJob : public QObject { Q_OBJECT public: - explicit DiscoverySingleDirectoryJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr); + explicit DiscoverySingleDirectoryJob(const AccountPtr &account, + const QString &path, + /* TODO for topLevelE2eeFolderPaths, from review: I still do not get why giving the whole QSet instead of just the parent of the folder we are in + sounds to me like it would be much more efficient to just have the e2ee parent folder that we are + inside*/ + const QSet &topLevelE2eeFolderPaths, + QObject *parent = nullptr); // Specify that this is the root and we need to check the data-fingerprint void setIsRootPath() { _isRootPath = true; } void start(); void abort(); [[nodiscard]] bool isFileDropDetected() const; [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + [[nodiscard]] SyncFileItem::EncryptionStatus currentEncryptionStatus() const; + [[nodiscard]] SyncFileItem::EncryptionStatus requiredEncryptionStatus() const; // This is not actually a network job, it is just a job signals: @@ -166,7 +175,7 @@ private slots: private: - [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; } + [[nodiscard]] bool isE2eEncrypted() const { return _encryptionStatusCurrent != SyncFileItem::EncryptionStatus::NotEncrypted; } QVector _results; QString _subPath; @@ -182,14 +191,18 @@ private slots: // If this directory is an external storage (The first item has 'M' in its permission) bool _isExternalStorage = false; // If this directory is e2ee - SyncFileItem::EncryptionStatus _isE2eEncrypted = SyncFileItem::EncryptionStatus::NotEncrypted; + SyncFileItem::EncryptionStatus _encryptionStatusCurrent = SyncFileItem::EncryptionStatus::NotEncrypted; bool _isFileDropDetected = false; bool _encryptedMetadataNeedUpdate = false; + SyncFileItem::EncryptionStatus _encryptionStatusRequired = SyncFileItem::EncryptionStatus::NotEncrypted; // If set, the discovery will finish with an error int64_t _size = 0; QString _error; QPointer _lsColJob; + // store top level E2EE folder paths as they are used later when discovering nested folders + QSet _topLevelE2eeFolderPaths; + public: QByteArray _dataFingerprint; }; @@ -323,6 +336,8 @@ class DiscoveryPhase : public QObject bool _noCaseConflictRecordsInDb = false; + QSet _topLevelE2eeFolderPaths; + signals: void fatalError(const QString &errorString, const OCC::ErrorCategory errorCategory); void itemDiscovered(const OCC::SyncFileItemPtr &item); diff --git a/src/libsync/encryptedfoldermetadatahandler.cpp b/src/libsync/encryptedfoldermetadatahandler.cpp new file mode 100644 index 0000000000000..63de59f900eab --- /dev/null +++ b/src/libsync/encryptedfoldermetadatahandler.cpp @@ -0,0 +1,376 @@ +/* + * Copyright (C) 2023 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 "rootencryptedfolderinfo.h" +#include "encryptedfoldermetadatahandler.h" +#include "foldermetadata.h" +#include "account.h" +#include "common/syncjournaldb.h" +#include "clientsideencryptionjobs.h" +#include "clientsideencryption.h" + +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcFetchAndUploadE2eeFolderMetadataJob, "nextcloud.sync.propagator.encryptedfoldermetadatahandler", QtInfoMsg) + +} + +namespace OCC { + +EncryptedFolderMetadataHandler::EncryptedFolderMetadataHandler(const AccountPtr &account, + const QString &folderPath, + SyncJournalDb *const journalDb, + const QString &pathForTopLevelFolder, + QObject *parent) + : QObject(parent) + , _account(account) + , _folderPath(folderPath) + , _journalDb(journalDb) +{ + _rootEncryptedFolderInfo = RootEncryptedFolderInfo( + RootEncryptedFolderInfo::createRootPath(folderPath, pathForTopLevelFolder)); +} + +void EncryptedFolderMetadataHandler::fetchMetadata(const FetchMode fetchMode) +{ + _fetchMode = fetchMode; + fetchFolderEncryptedId(); +} + +void EncryptedFolderMetadataHandler::fetchMetadata(const RootEncryptedFolderInfo &rootEncryptedFolderInfo, const FetchMode fetchMode) +{ + _rootEncryptedFolderInfo = rootEncryptedFolderInfo; + if (_rootEncryptedFolderInfo.path.isEmpty()) { + qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Error fetching metadata for" << _folderPath << ". Invalid _rootEncryptedFolderInfo!"; + emit fetchFinished(-1, tr("Error fetching metadata.")); + return; + } + fetchMetadata(fetchMode); +} + +void EncryptedFolderMetadataHandler::uploadMetadata(const UploadMode uploadMode) +{ + _uploadMode = uploadMode; + if (!_folderToken.isEmpty()) { + // use existing token + startUploadMetadata(); + return; + } + lockFolder(); +} + +void EncryptedFolderMetadataHandler::lockFolder() +{ + if (!validateBeforeLock()) { + return; + } + + const auto lockJob = new LockEncryptFolderApiJob(_account, _folderId, _journalDb, _account->e2e()->_publicKey, this); + connect(lockJob, &LockEncryptFolderApiJob::success, this, &EncryptedFolderMetadataHandler::slotFolderLockedSuccessfully); + connect(lockJob, &LockEncryptFolderApiJob::error, this, &EncryptedFolderMetadataHandler::slotFolderLockedError); + if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { + lockJob->setCounter(folderMetadata()->newCounter()); + } + lockJob->start(); +} + +void EncryptedFolderMetadataHandler::startFetchMetadata() +{ + const auto job = new GetMetadataApiJob(_account, _folderId); + connect(job, &GetMetadataApiJob::jsonReceived, this, &EncryptedFolderMetadataHandler::slotMetadataReceived); + connect(job, &GetMetadataApiJob::error, this, &EncryptedFolderMetadataHandler::slotMetadataReceivedError); + job->start(); +} + +void EncryptedFolderMetadataHandler::fetchFolderEncryptedId() +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Folder is encrypted, let's get the Id from it."; + const auto job = new LsColJob(_account, _folderPath, this); + job->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"}); + connect(job, &LsColJob::directoryListingSubfolders, this, &EncryptedFolderMetadataHandler::slotFolderEncryptedIdReceived); + connect(job, &LsColJob::finishedWithError, this, &EncryptedFolderMetadataHandler::slotFolderEncryptedIdError); + job->start(); +} + +bool EncryptedFolderMetadataHandler::validateBeforeLock() +{ + //Q_ASSERT(!_isFolderLocked && folderMetadata() && folderMetadata()->isValid() && folderMetadata()->isRootEncryptedFolder()); + if (_isFolderLocked) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error locking folder" << _folderId << "already locked"; + emit uploadFinished(-1, tr("Error locking folder.")); + return false; + } + + if (!folderMetadata() || !folderMetadata()->isValid()) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error locking folder" << _folderId << "invalid or null metadata"; + emit uploadFinished(-1, tr("Error locking folder.")); + return false; + } + + // normally, we should allow locking any nested folder to update its metadata, yet, with the new V2 architecture, this is something we might want to disallow + /*if (!folderMetadata()->isRootEncryptedFolder()) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error locking folder" << _folderId << "as it is not a top level folder"; + emit uploadFinished(-1, tr("Error locking folder.")); + return false; + }*/ + return true; +} + +void EncryptedFolderMetadataHandler::slotFolderEncryptedIdReceived(const QStringList &list) +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Received id of folder. Fetching metadata..."; + const auto job = qobject_cast(sender()); + const auto &folderInfo = job->_folderInfos.value(list.first()); + _folderId = folderInfo.fileId; + startFetchMetadata(); +} + +void EncryptedFolderMetadataHandler::slotFolderEncryptedIdError(QNetworkReply *reply) +{ + Q_ASSERT(reply); + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error retrieving the Id of the encrypted folder."; + if (!reply) { + emit fetchFinished(-1, tr("Error fetching encrypted folder id.")); + return; + } + const auto errorCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + emit fetchFinished(errorCode, reply->errorString()); +} + +void EncryptedFolderMetadataHandler::slotMetadataReceived(const QJsonDocument &json, int statusCode) +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Metadata Received, parsing it and decrypting" << json.toVariant(); + + const auto job = qobject_cast(sender()); + Q_ASSERT(job); + if (!job) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "slotMetadataReceived must be called from GetMetadataApiJob's signal"; + emit fetchFinished(statusCode, tr("Error fetching metadata.")); + return; + } + + _fetchMode = FetchMode::NonEmptyMetadata; + + if (statusCode != 200 && statusCode != 404) { + // neither successfully fetched, nor a folder without a metadata, fail further logic + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error fetching metadata for folder" << _folderPath; + emit fetchFinished(statusCode, tr("Error fetching metadata.")); + return; + } + + const auto rawMetadata = statusCode == 404 + ? QByteArray{} : json.toJson(QJsonDocument::Compact); + const auto metadata(QSharedPointer::create(_account, rawMetadata, _rootEncryptedFolderInfo, job->signature())); + connect(metadata.data(), &FolderMetadata::setupComplete, this, [this, metadata] { + if (!metadata->isValid()) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error parsing or decrypting metadata for folder" << _folderPath; + emit fetchFinished(-1, tr("Error parsing or decrypting metadata.")); + return; + } + _folderMetadata = metadata; + emit fetchFinished(200); + }); +} + +void EncryptedFolderMetadataHandler::slotMetadataReceivedError(const QByteArray &folderId, int httpReturnCode) +{ + Q_UNUSED(folderId); + if (_fetchMode == FetchMode::AllowEmptyMetadata) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error Getting the encrypted metadata. Pretend we got empty metadata. In case when posting it for the first time."; + _isNewMetadataCreated = true; + slotMetadataReceived({}, httpReturnCode); + return; + } + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error Getting the encrypted metadata."; + emit fetchFinished(httpReturnCode, tr("Error fetching metadata.")); +} + +void EncryptedFolderMetadataHandler::slotFolderLockedSuccessfully(const QByteArray &folderId, const QByteArray &token) +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Folder" << folderId << "Locked Successfully for Upload, Fetching Metadata"; + _folderToken = token; + _isFolderLocked = true; + startUploadMetadata(); +} + +void EncryptedFolderMetadataHandler::slotFolderLockedError(const QByteArray &folderId, int httpErrorCode) +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error locking folder" << folderId; + emit fetchFinished(httpErrorCode, tr("Error locking folder.")); +} + +void EncryptedFolderMetadataHandler::unlockFolder(const UnlockFolderWithResult result) +{ + Q_ASSERT(!_isUnlockRunning); + Q_ASSERT(_isFolderLocked); + + if (_isUnlockRunning) { + qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Double-call to unlockFolder."; + return; + } + + if (!_isFolderLocked) { + qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Folder is not locked."; + emit folderUnlocked(_folderId, 204); + return; + } + + if (_uploadMode == UploadMode::DoNotKeepLock) { + if (result == UnlockFolderWithResult::Success) { + connect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadSuccess); + } else { + connect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadError); + } + } + + if (_folderToken.isEmpty()) { + emit folderUnlocked(_folderId, 200); + return; + } + + _isUnlockRunning = true; + + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Calling Unlock"; + + const auto unlockJob = new UnlockEncryptFolderApiJob(_account, _folderId, _folderToken, _journalDb, this); + connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) { + qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Successfully Unlocked"; + _isFolderLocked = false; + emit folderUnlocked(folderId, 200); + _isUnlockRunning = false; + }); + connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) { + qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Unlock Error"; + emit folderUnlocked(folderId, httpStatus); + _isUnlockRunning = false; + }); + unlockJob->start(); +} + +void EncryptedFolderMetadataHandler::startUploadMetadata() +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Metadata created, sending to the server."; + + _uploadErrorCode = 200; + + if (!folderMetadata() || !folderMetadata()->isValid()) { + slotUploadMetadataError(_folderId, -1); + return; + } + + const auto encryptedMetadata = folderMetadata()->encryptedMetadata(); + if (_isNewMetadataCreated) { + const auto job = new StoreMetaDataApiJob(_account, _folderId, _folderToken, encryptedMetadata, folderMetadata()->metadataSignature()); + connect(job, &StoreMetaDataApiJob::success, this, &EncryptedFolderMetadataHandler::slotUploadMetadataSuccess); + connect(job, &StoreMetaDataApiJob::error, this, &EncryptedFolderMetadataHandler::slotUploadMetadataError); + job->start(); + } else { + const auto job = new UpdateMetadataApiJob(_account, _folderId, encryptedMetadata, _folderToken, folderMetadata()->metadataSignature()); + connect(job, &UpdateMetadataApiJob::success, this, &EncryptedFolderMetadataHandler::slotUploadMetadataSuccess); + connect(job, &UpdateMetadataApiJob::error, this, &EncryptedFolderMetadataHandler::slotUploadMetadataError); + job->start(); + } +} + +void EncryptedFolderMetadataHandler::slotUploadMetadataSuccess(const QByteArray &folderId) +{ + Q_UNUSED(folderId); + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Uploading of the metadata success."; + if (_uploadMode == UploadMode::KeepLock || !_isFolderLocked) { + slotEmitUploadSuccess(); + return; + } + connect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadSuccess); + unlockFolder(UnlockFolderWithResult::Success); +} + +void EncryptedFolderMetadataHandler::slotUploadMetadataError(const QByteArray &folderId, int httpReturnCode) +{ + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Update metadata error for folder" << folderId << "with error" << httpReturnCode; + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Unlocking the folder."; + _uploadErrorCode = httpReturnCode; + if (_isFolderLocked && _uploadMode == UploadMode::DoNotKeepLock) { + connect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadError); + unlockFolder(UnlockFolderWithResult::Failure); + return; + } + emit uploadFinished(_uploadErrorCode); +} + +void EncryptedFolderMetadataHandler::slotEmitUploadSuccess() +{ + disconnect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadSuccess); + emit uploadFinished(_uploadErrorCode); +} + +void EncryptedFolderMetadataHandler::slotEmitUploadError() +{ + disconnect(this, &EncryptedFolderMetadataHandler::folderUnlocked, this, &EncryptedFolderMetadataHandler::slotEmitUploadError); + emit uploadFinished(_uploadErrorCode, tr("Failed to upload metadata")); +} + +QSharedPointer EncryptedFolderMetadataHandler::folderMetadata() const +{ + return _folderMetadata; +} + +void EncryptedFolderMetadataHandler::setPrefetchedMetadataAndId(const QSharedPointer &metadata, const QByteArray &id) +{ + Q_ASSERT(metadata && metadata->isValid()); + Q_ASSERT(!id.isEmpty()); + + if (!metadata || !metadata->isValid()) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "invalid metadata argument"; + return; + } + + if (id.isEmpty()) { + qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "invalid id argument"; + return; + } + + _folderId = id; + _folderMetadata = metadata; + _isNewMetadataCreated = metadata->initialMetadata().isEmpty(); +} + +const QByteArray& EncryptedFolderMetadataHandler::folderId() const +{ + return _folderId; +} + +void EncryptedFolderMetadataHandler::setFolderToken(const QByteArray &token) +{ + _folderToken = token; +} + +const QByteArray& EncryptedFolderMetadataHandler::folderToken() const +{ + return _folderToken; +} + +bool EncryptedFolderMetadataHandler::isUnlockRunning() const +{ + return _isUnlockRunning; +} + +bool EncryptedFolderMetadataHandler::isFolderLocked() const +{ + return _isFolderLocked; +} + +} diff --git a/src/libsync/encryptedfoldermetadatahandler.h b/src/libsync/encryptedfoldermetadatahandler.h new file mode 100644 index 0000000000000..dcecce5c70eba --- /dev/null +++ b/src/libsync/encryptedfoldermetadatahandler.h @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2023 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 "account.h" +#include "rootencryptedfolderinfo.h" +#include +#include +#include +#include +#include + +namespace OCC { +class FolderMetadata; +class SyncJournalDb; +// all metadata operations with server must be performed via this class +class OWNCLOUDSYNC_EXPORT EncryptedFolderMetadataHandler + : public QObject +{ + Q_OBJECT + +public: + enum class FetchMode { + NonEmptyMetadata = 0, + AllowEmptyMetadata + }; + Q_ENUM(FetchMode); + + enum class UploadMode { + DoNotKeepLock = 0, + KeepLock + }; + Q_ENUM(UploadMode); + + enum class UnlockFolderWithResult { + Success = 0, + Failure + }; + Q_ENUM(UnlockFolderWithResult); + + explicit EncryptedFolderMetadataHandler(const AccountPtr &account, const QString &folderPath, SyncJournalDb *const journalDb, const QString &pathForTopLevelFolder, QObject *parent = nullptr); + + [[nodiscard]] QSharedPointer folderMetadata() const; + + // use this when metadata is already fetched so no fetching will happen in this class + void setPrefetchedMetadataAndId(const QSharedPointer &metadata, const QByteArray &id); + + // use this when modifying metadata for multiple folders inside top-level one which is locked + void setFolderToken(const QByteArray &token); + [[nodiscard]] const QByteArray& folderToken() const; + + [[nodiscard]] const QByteArray& folderId() const; + + [[nodiscard]] bool isUnlockRunning() const; + [[nodiscard]] bool isFolderLocked() const; + + void fetchMetadata(const RootEncryptedFolderInfo &rootEncryptedFolderInfo, const FetchMode fetchMode = FetchMode::NonEmptyMetadata); + void fetchMetadata(const FetchMode fetchMode = FetchMode::NonEmptyMetadata); + void uploadMetadata(const UploadMode uploadMode = UploadMode::DoNotKeepLock); + void unlockFolder(const UnlockFolderWithResult result = UnlockFolderWithResult::Success); + +private: + void lockFolder(); + void startUploadMetadata(); + void startFetchMetadata(); + void fetchFolderEncryptedId(); + bool validateBeforeLock(); + +private slots: + void slotFolderEncryptedIdReceived(const QStringList &list); + void slotFolderEncryptedIdError(QNetworkReply *reply); + + void slotMetadataReceived(const QJsonDocument &json, int statusCode); + void slotMetadataReceivedError(const QByteArray &folderId, int httpReturnCode); + + void slotFolderLockedSuccessfully(const QByteArray &folderId, const QByteArray &token); + void slotFolderLockedError(const QByteArray &folderId, int httpErrorCode); + + void slotUploadMetadataSuccess(const QByteArray &folderId); + void slotUploadMetadataError(const QByteArray &folderId, int httpReturnCode); + + void slotEmitUploadSuccess(); + void slotEmitUploadError(); + +public: signals: + void fetchFinished(int code, const QString &message = {}); + void uploadFinished(int code, const QString &message = {}); + void folderUnlocked(const QByteArray &folderId, int httpStatus); + +private: + AccountPtr _account; + QString _folderPath; + QPointer _journalDb; + QByteArray _folderId; + QByteArray _folderToken; + + QSharedPointer _folderMetadata; + + RootEncryptedFolderInfo _rootEncryptedFolderInfo; + + int _uploadErrorCode = 200; + + FetchMode _fetchMode = FetchMode::NonEmptyMetadata; + bool _isFolderLocked = false; + bool _isUnlockRunning = false; + bool _isNewMetadataCreated = false; + UploadMode _uploadMode = UploadMode::DoNotKeepLock; +}; + +} diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index ff8d29bba316f..11e383f4136bc 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -16,23 +16,30 @@ #include "common/syncjournaldb.h" #include "clientsideencryptionjobs.h" - +#include "foldermetadata.h" #include namespace OCC { Q_LOGGING_CATEGORY(lcEncryptFolderJob, "nextcloud.sync.propagator.encryptfolder", QtInfoMsg) -EncryptFolderJob::EncryptFolderJob(const AccountPtr &account, SyncJournalDb *journal, const QString &path, const QByteArray &fileId, QObject *parent) +EncryptFolderJob::EncryptFolderJob(const AccountPtr &account, SyncJournalDb *journal, const QString &path, const QByteArray &fileId, OwncloudPropagator *propagator, SyncFileItemPtr item, + QObject * parent) : QObject(parent) , _account(account) , _journal(journal) , _path(path) , _fileId(fileId) + , _propagator(propagator) + , _item(item) { + SyncJournalFileRecord rec; + const auto currentPath = !_pathNonEncrypted.isEmpty() ? _pathNonEncrypted : _path; + [[maybe_unused]] const auto result = _journal->getRootE2eFolderRecord(currentPath, &rec); + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(account, _path, _journal, rec.path())); } -void EncryptFolderJob::start() +void EncryptFolderJob::slotSetEncryptionFlag() { auto job = new OCC::SetEncryptionFlagApiJob(_account, _fileId, OCC::SetEncryptionFlagApiJob::Set, this); connect(job, &OCC::SetEncryptionFlagApiJob::success, this, &EncryptFolderJob::slotEncryptionFlagSuccess); @@ -40,34 +47,50 @@ void EncryptFolderJob::start() job->start(); } +void EncryptFolderJob::start() +{ + slotSetEncryptionFlag(); +} + QString EncryptFolderJob::errorString() const { return _errorString; } +void EncryptFolderJob::setPathNonEncrypted(const QString &pathNonEncrypted) +{ + _pathNonEncrypted = pathNonEncrypted; +} + void EncryptFolderJob::slotEncryptionFlagSuccess(const QByteArray &fileId) { SyncJournalFileRecord rec; - if (!_journal->getFileRecord(_path, &rec)) { - qCWarning(lcEncryptFolderJob) << "could not get file from local DB" << _path; + const auto currentPath = !_pathNonEncrypted.isEmpty() ? _pathNonEncrypted : _path; + if (!_journal->getFileRecord(currentPath, &rec)) { + qCWarning(lcEncryptFolderJob) << "could not get file from local DB" << currentPath; } if (!rec.isValid()) { - qCWarning(lcEncryptFolderJob) << "No valid record found in local DB for fileId" << fileId; + if (_propagator && _item) { + qCWarning(lcEncryptFolderJob) << "No valid record found in local DB for fileId" << fileId << "going to create it now..."; + const auto updateResult = _propagator->updateMetadata(*_item.data()); + if (updateResult) { + [[maybe_unused]] const auto result = _journal->getFileRecord(currentPath, &rec); + } + } else { + qCWarning(lcEncryptFolderJob) << "No valid record found in local DB for fileId" << fileId; + } } - rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2; - const auto result = _journal->setFileRecord(rec); - if (!result) { - qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error(); + if (!rec.isE2eEncrypted()) { + rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::Encrypted; + const auto result = _journal->setFileRecord(rec); + if (!result) { + qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error(); + } } - const auto lockJob = new LockEncryptFolderApiJob(_account, fileId, _journal, _account->e2e()->_publicKey, this); - connect(lockJob, &LockEncryptFolderApiJob::success, - this, &EncryptFolderJob::slotLockForEncryptionSuccess); - connect(lockJob, &LockEncryptFolderApiJob::error, - this, &EncryptFolderJob::slotLockForEncryptionError); - lockJob->start(); + uploadMetadata(); } void EncryptFolderJob::slotEncryptionFlagError(const QByteArray &fileId, @@ -76,74 +99,54 @@ void EncryptFolderJob::slotEncryptionFlagError(const QByteArray &fileId, { qDebug() << "Error on the encryption flag of" << fileId << "HTTP code:" << httpErrorCode; _errorString = errorMessage; - emit finished(Error); + emit finished(Error, EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted); } -void EncryptFolderJob::slotLockForEncryptionSuccess(const QByteArray &fileId, const QByteArray &token) +void EncryptFolderJob::uploadMetadata() { - _folderToken = token; - - 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" - "This can be an issue with your OpenSSL libraries."); - emit finished(Error); + const auto currentPath = !_pathNonEncrypted.isEmpty() ? _pathNonEncrypted : _path; + SyncJournalFileRecord rec; + if (!_journal->getRootE2eFolderRecord(currentPath, &rec)) { + emit finished(Error, EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted); return; } - auto storeMetadataJob = new StoreMetaDataApiJob(_account, fileId, emptyMetadata.encryptedMetadata(), this); - connect(storeMetadataJob, &StoreMetaDataApiJob::success, - this, &EncryptFolderJob::slotUploadMetadataSuccess); - connect(storeMetadataJob, &StoreMetaDataApiJob::error, - this, &EncryptFolderJob::slotUpdateMetadataError); - storeMetadataJob->start(); -} - -void EncryptFolderJob::slotUploadMetadataSuccess(const QByteArray &folderId) -{ - auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, _journal, this); - connect(unlockJob, &UnlockEncryptFolderApiJob::success, - this, &EncryptFolderJob::slotUnlockFolderSuccess); - connect(unlockJob, &UnlockEncryptFolderApiJob::error, - this, &EncryptFolderJob::slotUnlockFolderError); - unlockJob->start(); -} - -void EncryptFolderJob::slotUpdateMetadataError(const QByteArray &folderId, const int httpReturnCode) -{ - Q_UNUSED(httpReturnCode); - - const auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, _journal, this); - connect(unlockJob, &UnlockEncryptFolderApiJob::success, - this, &EncryptFolderJob::slotUnlockFolderSuccess); - connect(unlockJob, &UnlockEncryptFolderApiJob::error, - this, &EncryptFolderJob::slotUnlockFolderError); - unlockJob->start(); + const auto emptyMetadata(QSharedPointer::create( + _account, + QByteArray{}, + RootEncryptedFolderInfo(RootEncryptedFolderInfo::createRootPath(currentPath, rec.path())), + QByteArray{})); + + connect(emptyMetadata.data(), &FolderMetadata::setupComplete, this, [this, emptyMetadata] { + const auto encryptedMetadata = !emptyMetadata->isValid() ? QByteArray{} : 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" + "This can be an issue with your OpenSSL libraries."); + emit finished(Error, EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted); + return; + } + _encryptedFolderMetadataHandler->setPrefetchedMetadataAndId(emptyMetadata, _fileId); + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::uploadFinished, + this, + &EncryptFolderJob::slotUploadMetadataFinished); + _encryptedFolderMetadataHandler->uploadMetadata(); + }); } -void EncryptFolderJob::slotLockForEncryptionError(const QByteArray &fileId, - const int httpErrorCode, - const QString &errorMessage) +void EncryptFolderJob::slotUploadMetadataFinished(int statusCode, const QString &message) { - qCInfo(lcEncryptFolderJob()) << "Locking error for" << fileId << "HTTP code:" << httpErrorCode; - _errorString = errorMessage; - emit finished(Error); -} - -void EncryptFolderJob::slotUnlockFolderError(const QByteArray &fileId, - const int httpErrorCode, - const QString &errorMessage) -{ - qCInfo(lcEncryptFolderJob()) << "Unlocking error for" << fileId << "HTTP code:" << httpErrorCode; - _errorString = errorMessage; - emit finished(Error); -} -void EncryptFolderJob::slotUnlockFolderSuccess(const QByteArray &fileId) -{ - qCInfo(lcEncryptFolderJob()) << "Unlocking success for" << fileId; - emit finished(Success); + if (statusCode != 200) { + qCDebug(lcEncryptFolderJob) << "Update metadata error for folder" << _encryptedFolderMetadataHandler->folderId() << "with error" + << message; + qCDebug(lcEncryptFolderJob()) << "Unlocking the folder."; + _errorString = message; + emit finished(Error, EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted); + return; + } + emit finished(Success, _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus()); } } diff --git a/src/libsync/encryptfolderjob.h b/src/libsync/encryptfolderjob.h index bad5a8976987b..7d5600d840d39 100644 --- a/src/libsync/encryptfolderjob.h +++ b/src/libsync/encryptfolderjob.h @@ -13,9 +13,12 @@ */ #pragma once -#include - #include "account.h" +#include "encryptedfoldermetadatahandler.h" +#include "syncfileitem.h" +#include "owncloudpropagator.h" + +#include namespace OCC { class SyncJournalDb; @@ -30,30 +33,41 @@ class OWNCLOUDSYNC_EXPORT EncryptFolderJob : public QObject }; Q_ENUM(Status) - explicit EncryptFolderJob(const AccountPtr &account, SyncJournalDb *journal, const QString &path, const QByteArray &fileId, QObject *parent = nullptr); + explicit EncryptFolderJob(const AccountPtr &account, + SyncJournalDb *journal, + const QString &path, + const QByteArray &fileId, + OwncloudPropagator *propagator = nullptr, + SyncFileItemPtr item = {}, + QObject *parent = nullptr); void start(); [[nodiscard]] QString errorString() const; signals: - void finished(int status); + void finished(int status, EncryptionStatusEnums::ItemEncryptionStatus encryptionStatus); + +public slots: + void setPathNonEncrypted(const QString &pathNonEncrypted); + +private: + void uploadMetadata(); private slots: void slotEncryptionFlagSuccess(const QByteArray &folderId); void slotEncryptionFlagError(const QByteArray &folderId, const int httpReturnCode, const QString &errorMessage); - void slotLockForEncryptionSuccess(const QByteArray &folderId, const QByteArray &token); - void slotLockForEncryptionError(const QByteArray &folderId, const int httpReturnCode, const QString &errorMessage); - void slotUnlockFolderSuccess(const QByteArray &folderId); - void slotUnlockFolderError(const QByteArray &folderId, const int httpReturnCode, const QString &errorMessage); - void slotUploadMetadataSuccess(const QByteArray &folderId); - void slotUpdateMetadataError(const QByteArray &folderId, const int httpReturnCode); + void slotUploadMetadataFinished(int statusCode, const QString &message); + void slotSetEncryptionFlag(); private: AccountPtr _account; SyncJournalDb *_journal; QString _path; + QString _pathNonEncrypted; QByteArray _fileId; - QByteArray _folderToken; QString _errorString; + OwncloudPropagator *_propagator = nullptr; + SyncFileItemPtr _item; + QScopedPointer _encryptedFolderMetadataHandler; }; } diff --git a/src/libsync/foldermetadata.cpp b/src/libsync/foldermetadata.cpp new file mode 100644 index 0000000000000..aaa2bdd60b39f --- /dev/null +++ b/src/libsync/foldermetadata.cpp @@ -0,0 +1,1125 @@ +/* + * Copyright (C) 2023 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 "account.h" +#include "encryptedfoldermetadatahandler.h" +#include "foldermetadata.h" +#include "clientsideencryption.h" +#include "clientsideencryptionjobs.h" +#include +#include +#include +#include + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg) + +namespace +{ +constexpr auto authenticationTagKey = "authenticationTag"; +constexpr auto cipherTextKey = "ciphertext"; +constexpr auto counterKey = "counter"; +constexpr auto filesKey = "files"; +constexpr auto filedropKey = "filedrop"; +constexpr auto foldersKey = "folders"; +constexpr auto initializationVectorKey = "initializationVector"; +constexpr auto keyChecksumsKey = "keyChecksums"; +constexpr auto metadataJsonKey = "metadata"; +constexpr auto metadataKeyKey = "metadataKey"; +constexpr auto nonceKey = "nonce"; +constexpr auto usersKey = "users"; +constexpr auto usersUserIdKey = "userId"; +constexpr auto usersCertificateKey = "certificate"; +constexpr auto usersEncryptedMetadataKey = "encryptedMetadataKey"; +constexpr auto usersEncryptedFiledropKey = "encryptedFiledropKey"; +constexpr auto versionKey = "version"; +constexpr auto encryptedKey = "encrypted"; + +const auto metadataKeySize = 16; + +QString metadataStringFromOCsDocument(const QJsonDocument &ocsDoc) +{ + return ocsDoc.object()["ocs"].toObject()["data"].toObject()["meta-data"].toString(); +} +} + +bool FolderMetadata::EncryptedFile::isDirectory() const +{ + return mimetype.isEmpty() || mimetype == QByteArrayLiteral("inode/directory") || mimetype == QByteArrayLiteral("httpd/unix-directory"); +} + +FolderMetadata::FolderMetadata(AccountPtr account, FolderType folderType) + : _account(account), + _isRootEncryptedFolder(folderType == FolderType::Root) +{ + qCInfo(lcCseMetadata()) << "Setting up an Empty Metadata"; + initEmptyMetadata(); +} + +FolderMetadata::FolderMetadata(AccountPtr account, + const QByteArray &metadata, + const RootEncryptedFolderInfo &rootEncryptedFolderInfo, + const QByteArray &signature, + QObject *parent) + : QObject(parent) + , _account(account) + , _initialMetadata(metadata) + , _isRootEncryptedFolder(rootEncryptedFolderInfo.path == QStringLiteral("/")) + , _metadataKeyForEncryption(rootEncryptedFolderInfo.keyForEncryption) + , _metadataKeyForDecryption(rootEncryptedFolderInfo.keyForDecryption) + , _keyChecksums(rootEncryptedFolderInfo.keyChecksums) + , _initialSignature(signature) +{ + setupVersionFromExistingMetadata(metadata); + + const auto doc = QJsonDocument::fromJson(metadata); + qCInfo(lcCseMetadata()) << doc.toJson(QJsonDocument::Compact); + if (!_isRootEncryptedFolder + && !rootEncryptedFolderInfo.keysSet() + && !rootEncryptedFolderInfo.path.isEmpty()) { + startFetchRootE2eeFolderMetadata(rootEncryptedFolderInfo.path); + } else { + initMetadata(); + } +} + +void FolderMetadata::initMetadata() +{ + if (_initialMetadata.isEmpty()) { + qCInfo(lcCseMetadata()) << "Setting up empty metadata"; + initEmptyMetadata(); + return; + } + + qCInfo(lcCseMetadata()) << "Setting up existing metadata"; + setupExistingMetadata(_initialMetadata); + + if (metadataKeyForDecryption().isEmpty() || metadataKeyForEncryption().isEmpty()) { + qCWarning(lcCseMetadata()) << "Failed to setup FolderMetadata. Could not parse/create metadataKey!"; + } + emitSetupComplete(); +} + +void FolderMetadata::setupExistingMetadata(const QByteArray &metadata) +{ + const auto doc = QJsonDocument::fromJson(metadata); + qCDebug(lcCseMetadata()) << "Got existing metadata:" << doc.toJson(QJsonDocument::Compact); + + if (_existingMetadataVersion < MetadataVersion::Version1) { + qCDebug(lcCseMetadata()) << "Could not setup metadata. Incorrect version" << _existingMetadataVersion; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + if (_existingMetadataVersion < MetadataVersion::Version2_0) { + setupExistingMetadataLegacy(metadata); + return; + } + + qCDebug(lcCseMetadata()) << "Setting up latest metadata version" << _existingMetadataVersion; + const auto metaDataStr = metadataStringFromOCsDocument(doc); + const auto metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); + + const auto folderUsers = metaDataDoc[usersKey].toArray(); + + const auto isUsersArrayValid = (!_isRootEncryptedFolder && folderUsers.isEmpty()) || (_isRootEncryptedFolder && !folderUsers.isEmpty()); + Q_ASSERT(isUsersArrayValid); + + if (!isUsersArrayValid) { + qCDebug(lcCseMetadata()) << "Could not decrypt metadata key. Users array is invalid!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + if (_isRootEncryptedFolder) { + QJsonDocument debugHelper; + debugHelper.setArray(folderUsers); + qCDebug(lcCseMetadata()) << "users: " << debugHelper.toJson(QJsonDocument::Compact); + } + + for (auto it = folderUsers.constBegin(); it != folderUsers.constEnd(); ++it) { + const auto folderUserObject = it->toObject(); + const auto userId = folderUserObject.value(usersUserIdKey).toString(); + UserWithFolderAccess folderUser; + folderUser.userId = userId; + /* TODO: does it make sense to store each certificatePem that has been successfuly verified? Is this secure? + / Can the attacker use outdated certificate as an attack vector?*/ + folderUser.certificatePem = folderUserObject.value(usersCertificateKey).toString().toUtf8(); + folderUser.encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value(usersEncryptedMetadataKey).toString().toUtf8()); + _folderUsers[userId] = folderUser; + } + + if (_isRootEncryptedFolder && !_initialSignature.isEmpty()) { + const auto metadataForSignature = prepareMetadataForSignature(metaDataDoc); + + QVector certificatePems; + certificatePems.reserve(_folderUsers.size()); + for (const auto &folderUser : std::as_const(_folderUsers)) { + certificatePems.push_back(folderUser.certificatePem); + } + + if (!_account->e2e()->verifySignatureCryptographicMessageSyntax(QByteArray::fromBase64(_initialSignature), metadataForSignature.toBase64(), certificatePems)) { + qCDebug(lcCseMetadata()) << "Could not parse encrypred folder metadata. Failed to verify signature!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + } + + if (!parseFileDropPart(metaDataDoc)) { + qCDebug(lcCseMetadata()) << "Could not parse filedrop part"; + return; + } + + if (_folderUsers.contains(_account->davUser())) { + const auto currentFolderUser = _folderUsers.value(_account->davUser()); + _metadataKeyForEncryption = decryptDataWithPrivateKey(currentFolderUser.encryptedMetadataKey); + _metadataKeyForDecryption = _metadataKeyForEncryption; + } + + if (metadataKeyForDecryption().isEmpty() || metadataKeyForEncryption().isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not setup metadata key!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + const auto metadataObj = metaDataDoc.object()[metadataJsonKey].toObject(); + _metadataNonce = QByteArray::fromBase64(metadataObj[nonceKey].toString().toLocal8Bit()); + const auto cipherTextEncrypted = metadataObj[cipherTextKey].toString().toLocal8Bit(); + + // for compatibility, the format is "cipheredpart|initializationVector", so we need to extract the "cipheredpart" + const auto cipherTextPartExtracted = cipherTextEncrypted.split('|').at(0); + + const auto cipherTextDecrypted = EncryptionHelper::decryptThenUnGzipData(metadataKeyForDecryption(), QByteArray::fromBase64(cipherTextPartExtracted), _metadataNonce); + if (cipherTextDecrypted.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not decrypt cipher text!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted); + + const auto keyCheckSums = cipherTextDocument[keyChecksumsKey].toArray(); + if (!keyCheckSums.isEmpty()) { + _keyChecksums.clear(); + } + for (auto it = keyCheckSums.constBegin(); it != keyCheckSums.constEnd(); ++it) { + const auto keyChecksum = it->toVariant().toString().toUtf8(); + if (!keyChecksum.isEmpty()) { + //TODO: check that no hash has been removed from the keyChecksums + // How do we check that? + _keyChecksums.insert(keyChecksum); + } + } + + if (!verifyMetadataKey(metadataKeyForDecryption())) { + qCDebug(lcCseMetadata()) << "Could not verify metadataKey!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + const auto files = cipherTextDocument.object()[filesKey].toObject(); + const auto folders = cipherTextDocument.object()[foldersKey].toObject(); + + const auto counterVariantFromJson = cipherTextDocument.object().value(counterKey).toVariant(); + if (counterVariantFromJson.isValid() && counterVariantFromJson.canConvert()) { + // TODO: We need to check counter: new counter must be greater than locally stored counter + // What does that mean? We store the counter in metadata, should we now store it in local database as we do for all file records in SyncJournal? + // What if metadata was not updated for a while? The counter will then not be greater than locally stored (in SyncJournal DB?) + _counter = counterVariantFromJson.value(); + } + + for (auto it = files.constBegin(), end = files.constEnd(); it != end; ++it) { + const auto parsedEncryptedFile = parseEncryptedFileFromJson(it.key(), it.value()); + if (!parsedEncryptedFile.originalFilename.isEmpty()) { + _files.push_back(parsedEncryptedFile); + } + } + + for (auto it = folders.constBegin(); it != folders.constEnd(); ++it) { + const auto folderName = it.value().toString(); + if (!folderName.isEmpty()) { + EncryptedFile file; + file.encryptedFilename = it.key(); + file.originalFilename = folderName; + _files.push_back(file); + } + } + _isMetadataValid = true; +} + +void FolderMetadata::setupExistingMetadataLegacy(const QByteArray &metadata) +{ + const auto doc = QJsonDocument::fromJson(metadata); + qCDebug(lcCseMetadata()) << "Setting up legacy existing metadata version" << _existingMetadataVersion << doc.toJson(QJsonDocument::Compact); + + const auto metaDataStr = metadataStringFromOCsDocument(doc); + const auto metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); + const auto metadataObj = metaDataDoc.object()[metadataJsonKey].toObject(); + + // we will use metadata key from metadata to decrypt legacy metadata, so let's clear the decryption key if any provided by top-level folder + _metadataKeyForDecryption.clear(); + + const auto metadataKeyFromJson = metadataObj[metadataKeyKey].toString().toLocal8Bit(); + if (!metadataKeyFromJson.isEmpty()) { + // parse version 1.2 + const auto decryptedMetadataKeyBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(metadataKeyFromJson)); + if (!decryptedMetadataKeyBase64.isEmpty()) { + // fromBase64() multiple times just to stick with the old wrong way + _metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(decryptedMetadataKeyBase64)); + } + } + + if (metadataKeyForDecryption().isEmpty() + && _existingMetadataVersion < latestSupportedMetadataVersion()) { + // parse version 1.0 + qCDebug(lcCseMetadata()) << "Migrating from" << _existingMetadataVersion << "to" + << latestSupportedMetadataVersion(); + const auto metadataKeys = metadataObj["metadataKeys"].toObject(); + if (metadataKeys.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not migrate. No metadata keys found!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + const auto lastMetadataKeyFromJson = metadataKeys.keys().last().toLocal8Bit(); + if (!lastMetadataKeyFromJson.isEmpty()) { + const auto lastMetadataKeyValueFromJson = metadataKeys.value(lastMetadataKeyFromJson).toString().toLocal8Bit(); + if (!lastMetadataKeyValueFromJson.isEmpty()) { + const auto lastMetadataKeyValueFromJsonBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(lastMetadataKeyValueFromJson)); + if (!lastMetadataKeyValueFromJsonBase64.isEmpty()) { + _metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(lastMetadataKeyValueFromJsonBase64)); + } + } + } + } + + if (metadataKeyForDecryption().isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not setup existing metadata with missing metadataKeys!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + + if (metadataKeyForEncryption().isEmpty()) { + _metadataKeyForEncryption = metadataKeyForDecryption(); + } + + const auto sharing = metadataObj["sharing"].toString().toLocal8Bit(); + const auto files = metaDataDoc.object()[filesKey].toObject(); + const auto metadataKey = metaDataDoc.object()[metadataJsonKey].toObject()[metadataKeyKey].toString().toUtf8(); + const auto metadataKeyChecksum = metaDataDoc.object()[metadataJsonKey].toObject()["checksum"].toString().toUtf8(); + + setFileDrop(metaDataDoc.object().value("filedrop").toObject()); + // for unit tests + _fileDropFromServer = _fileDrop; + + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { + EncryptedFile file; + file.encryptedFilename = it.key(); + + const auto fileObj = it.value().toObject(); + file.authenticationTag = QByteArray::fromBase64(fileObj[authenticationTagKey].toString().toLocal8Bit()); + file.initializationVector = QByteArray::fromBase64(fileObj[initializationVectorKey].toString().toLocal8Bit()); + + // Decrypt encrypted part + const auto encryptedFile = fileObj[encryptedKey].toString().toLocal8Bit(); + const auto decryptedFile = decryptJsonObject(encryptedFile, metadataKeyForDecryption()); + const auto decryptedFileDoc = QJsonDocument::fromJson(decryptedFile); + + const auto decryptedFileObj = decryptedFileDoc.object(); + + if (decryptedFileObj["filename"].toString().isEmpty()) { + qCDebug(lcCseMetadata) << "decrypted metadata" << decryptedFileDoc.toJson(QJsonDocument::Indented); + qCWarning(lcCseMetadata) << "skipping encrypted file" << file.encryptedFilename << "metadata has an empty file name"; + continue; + } + + file.originalFilename = decryptedFileObj["filename"].toString(); + file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); + file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); + + // 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"); + } + + qCDebug(lcCseMetadata) << "encrypted file" << decryptedFileObj["filename"].toString() << decryptedFileObj["key"].toString() << it.key(); + + _files.push_back(file); + } + + if (!checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum) && _existingMetadataVersion >= MetadataVersion::Version1_2) { + qCInfo(lcCseMetadata) << "checksum comparison failed" + << "server value" << metadataKeyChecksum << "client value" << computeMetadataKeyChecksum(metadataKey); + if (!_account->shouldSkipE2eeMetadataChecksumValidation()) { + qCDebug(lcCseMetadata) << "Failed to validate checksum for legacy metadata!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + qCDebug(lcCseMetadata) << "shouldSkipE2eeMetadataChecksumValidation is set. Allowing invalid checksum until next sync."; + } + _isMetadataValid = true; +} + +void FolderMetadata::setupVersionFromExistingMetadata(const QByteArray &metadata) +{ + const auto doc = QJsonDocument::fromJson(metadata); + const auto metaDataStr = metadataStringFromOCsDocument(doc); + const auto metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); + const auto metadataObj = metaDataDoc.object()[metadataJsonKey].toObject(); + + QString versionStringFromMetadata; + + if (metadataObj.contains(versionKey)) { + const auto metadataVersionValue = metadataObj.value(versionKey); + if (metadataVersionValue.type() == QJsonValue::Type::String) { + versionStringFromMetadata = metadataObj[versionKey].toString(); + } else if (metadataVersionValue.type() == QJsonValue::Type::Double) { + versionStringFromMetadata = QString::number(metadataVersionValue.toDouble(), 'f', 1); + } + } + else if (metaDataDoc.object().contains(versionKey)) { + const auto metadataVersionValue = metaDataDoc.object()[versionKey].toVariant(); + if (metadataVersionValue.type() == QVariant::Type::String) { + versionStringFromMetadata = metadataVersionValue.toString(); + } else if (metadataVersionValue.type() == QVariant::Type::Double) { + versionStringFromMetadata = QString::number(metadataVersionValue.toDouble(), 'f', 1); + } else if (metadataVersionValue.type() == QVariant::Type::Int) { + versionStringFromMetadata = QString::number(metadataVersionValue.toInt()) + QStringLiteral(".0"); + } + } + + if (versionStringFromMetadata == QStringLiteral("1.2")) { + _existingMetadataVersion = MetadataVersion::Version1_2; + } else if (versionStringFromMetadata == QStringLiteral("2.0") || versionStringFromMetadata == QStringLiteral("2")) { + _existingMetadataVersion = MetadataVersion::Version2_0; + } else if (versionStringFromMetadata == QStringLiteral("1.0") || versionStringFromMetadata == QStringLiteral("1")) { + _existingMetadataVersion = MetadataVersion::Version1; + } +} + +void FolderMetadata::emitSetupComplete() +{ + QTimer::singleShot(0, this, [this]() { + emit setupComplete(); + }); +} + +// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. +QByteArray FolderMetadata::encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const +{ + Bio publicKeyBio; + const auto publicKeyPem = key.toPem(); + BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); + const auto publicKey = PKey::readPublicKey(publicKeyBio); + + // The metadata key is binary so base64 encode it first + return EncryptionHelper::encryptStringAsymmetric(publicKey, data); +} + +QByteArray FolderMetadata::decryptDataWithPrivateKey(const QByteArray &data) const +{ + Bio privateKeyBio; + BIO_write(privateKeyBio, _account->e2e()->_privateKey.constData(), _account->e2e()->_privateKey.size()); + const auto privateKey = PKey::readPrivateKey(privateKeyBio); + + const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(privateKey, data); + if (decryptResult.isEmpty()) { + qCDebug(lcCseMetadata()) << "ERROR. Could not decrypt the metadata key"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + } + return decryptResult; +} + +// AES/GCM/NoPadding (128 bit key size) +QByteArray FolderMetadata::encryptJsonObject(const QByteArray& obj, const QByteArray pass) const +{ + return EncryptionHelper::encryptStringSymmetric(pass, obj); +} + +QByteArray FolderMetadata::decryptJsonObject(const QByteArray& encryptedMetadata, const QByteArray& pass) const +{ + return EncryptionHelper::decryptStringSymmetric(pass, encryptedMetadata); +} + +bool FolderMetadata::checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const +{ + const auto referenceMetadataKeyValue = computeMetadataKeyChecksum(metadataKey); + + return referenceMetadataKeyValue == metadataKeyChecksum; +} + +QByteArray FolderMetadata::computeMetadataKeyChecksum(const QByteArray &metadataKey) const +{ + auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; + + hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8()); + auto sortedFiles = _files; + std::sort(sortedFiles.begin(), sortedFiles.end(), [](const auto &first, const auto &second) { + return first.encryptedFilename < second.encryptedFilename; + }); + for (const auto &singleFile : sortedFiles) { + hashAlgorithm.addData(singleFile.encryptedFilename.toUtf8()); + } + hashAlgorithm.addData(metadataKey); + + return hashAlgorithm.result().toHex(); +} + +bool FolderMetadata::isValid() const +{ + return _isMetadataValid; +} + +FolderMetadata::EncryptedFile FolderMetadata::parseEncryptedFileFromJson(const QString &encryptedFilename, const QJsonValue &fileJSON) const +{ + const auto fileObj = fileJSON.toObject(); + if (fileObj["filename"].toString().isEmpty()) { + qCWarning(lcCseMetadata()) << "skipping encrypted file" << encryptedFilename << "metadata has an empty file name"; + return {}; + } + + EncryptedFile file; + file.encryptedFilename = encryptedFilename; + file.authenticationTag = QByteArray::fromBase64(fileObj[authenticationTagKey].toString().toLocal8Bit()); + auto nonce = QByteArray::fromBase64(fileObj[initializationVectorKey].toString().toLocal8Bit()); + if (nonce.isEmpty()) { + nonce = QByteArray::fromBase64(fileObj[nonceKey].toString().toLocal8Bit()); + } + file.initializationVector = nonce; + file.originalFilename = fileObj["filename"].toString(); + file.encryptionKey = QByteArray::fromBase64(fileObj["key"].toString().toLocal8Bit()); + file.mimetype = fileObj["mimetype"].toString().toLocal8Bit(); + + // 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"); + } + + return file; +} + +QJsonObject FolderMetadata::convertFileToJsonObject(const EncryptedFile *encryptedFile) const +{ + QJsonObject file; + file.insert("key", QString(encryptedFile->encryptionKey.toBase64())); + file.insert("filename", encryptedFile->originalFilename); + file.insert("mimetype", QString(encryptedFile->mimetype)); + const auto nonceFinalKey = latestSupportedMetadataVersion() < MetadataVersion::Version2_0 + ? initializationVectorKey + : nonceKey; + file.insert(nonceFinalKey, QString(encryptedFile->initializationVector.toBase64())); + file.insert(authenticationTagKey, QString(encryptedFile->authenticationTag.toBase64())); + + return file; +} + +const QByteArray FolderMetadata::metadataKeyForEncryption() const +{ + return _metadataKeyForEncryption; +} + +const QSet& FolderMetadata::keyChecksums() const +{ + return _keyChecksums; +} + +void FolderMetadata::initEmptyMetadata() +{ + if (_account->capabilities().clientSideEncryptionVersion() < 2.0) { + return initEmptyMetadataLegacy(); + } + qCDebug(lcCseMetadata()) << "Setting up empty metadata v2"; + if (_isRootEncryptedFolder) { + if (!addUser(_account->davUser(), _account->e2e()->_certificate)) { + qCDebug(lcCseMetadata) << "Empty metadata setup failed. Could not add first user."; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return; + } + _metadataKeyForDecryption = _metadataKeyForEncryption; + } + _isMetadataValid = true; + + emitSetupComplete(); +} + +void FolderMetadata::initEmptyMetadataLegacy() +{ + qCDebug(lcCseMetadata) << "Settint up legacy empty metadata"; + _metadataKeyForEncryption = EncryptionHelper::generateRandom(metadataKeySize); + _metadataKeyForDecryption = _metadataKeyForEncryption; + QString publicKey = _account->e2e()->_publicKey.toPem().toBase64(); + QString displayName = _account->displayName(); + + _isMetadataValid = true; + + emitSetupComplete(); +} + +QByteArray FolderMetadata::encryptedMetadata() +{ + Q_ASSERT(_isMetadataValid); + if (!_isMetadataValid) { + qCCritical(lcCseMetadata()) << "Could not encrypt non-initialized metadata!"; + return {}; + } + + if (latestSupportedMetadataVersion() < MetadataVersion::Version2_0) { + return encryptedMetadataLegacy(); + } + + qCDebug(lcCseMetadata()) << "Encrypting metadata for latest version" + << latestSupportedMetadataVersion(); + if (_isRootEncryptedFolder && _folderUsers.isEmpty() && _existingMetadataVersion < MetadataVersion::Version2_0) { + // migrated from legacy version, create metadata key and setup folderUsrs array + createNewMetadataKeyForEncryption(); + } + + if (metadataKeyForEncryption().isEmpty()) { + qCDebug(lcCseMetadata()) << "Encrypting metadata failed! Empty metadata key!"; + return {}; + } + + QJsonObject files, folders; + for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; ++it) { + const auto file = convertFileToJsonObject(it); + if (file.isEmpty()) { + qCDebug(lcCseMetadata) << "Metadata generation failed for file" << it->encryptedFilename; + return {}; + } + const auto isDirectory = + it->mimetype.isEmpty() || it->mimetype == QByteArrayLiteral("inode/directory") || it->mimetype == QByteArrayLiteral("httpd/unix-directory"); + if (isDirectory) { + folders.insert(it->encryptedFilename, it->originalFilename); + } else { + files.insert(it->encryptedFilename, file); + } + } + + QJsonArray keyChecksums; + if (_isRootEncryptedFolder) { + for (auto it = _keyChecksums.constBegin(), end = _keyChecksums.constEnd(); it != end; ++it) { + keyChecksums.push_back(QJsonValue::fromVariant(*it)); + } + } + + QJsonObject cipherText = {{counterKey, QJsonValue::fromVariant(newCounter())}, {filesKey, files}, {foldersKey, folders}}; + + const auto isChecksumsArrayValid = (!_isRootEncryptedFolder && keyChecksums.isEmpty()) || (_isRootEncryptedFolder && !keyChecksums.isEmpty()); + Q_ASSERT(isChecksumsArrayValid); + if (!isChecksumsArrayValid) { + qCDebug(lcCseMetadata) << "Empty keyChecksums while shouldn't be empty!"; + return {}; + } + if (!keyChecksums.isEmpty()) { + cipherText.insert(keyChecksumsKey, keyChecksums); + } + + const QJsonDocument cipherTextDoc(cipherText); + + QByteArray authenticationTag; + const auto initializationVector = EncryptionHelper::generateRandom(metadataKeySize); + const auto initializationVectorBase64 = initializationVector.toBase64(); + const auto gzippedThenEncryptData = EncryptionHelper::gzipThenEncryptData(metadataKeyForEncryption(), cipherTextDoc.toJson(QJsonDocument::Compact), initializationVector, authenticationTag).toBase64(); + // backwards compatible with old versions ("ciphertext|initializationVector") + const auto encryptedCipherText = QByteArray(gzippedThenEncryptData + QByteArrayLiteral("|") + initializationVectorBase64); + const QJsonObject metadata{{cipherTextKey, QJsonValue::fromVariant(encryptedCipherText)}, + {nonceKey, QJsonValue::fromVariant(initializationVectorBase64)}, + {authenticationTagKey, QJsonValue::fromVariant(authenticationTag.toBase64())}}; + + QJsonObject metaObject = {{metadataJsonKey, metadata}, {versionKey, QString::number(_account->capabilities().clientSideEncryptionVersion(), 'f', 1)}}; + + QJsonArray folderUsers; + if (_isRootEncryptedFolder) { + for (auto it = _folderUsers.constBegin(), end = _folderUsers.constEnd(); it != end; ++it) { + const auto folderUser = it.value(); + + const QJsonObject folderUserJson{{usersUserIdKey, folderUser.userId}, + {usersCertificateKey, QJsonValue::fromVariant(folderUser.certificatePem)}, + {usersEncryptedMetadataKey, QJsonValue::fromVariant(folderUser.encryptedMetadataKey.toBase64())}}; + folderUsers.push_back(folderUserJson); + } + } + const auto isFolderUsersArrayValid = (!_isRootEncryptedFolder && folderUsers.isEmpty()) || (_isRootEncryptedFolder && !folderUsers.isEmpty()); + Q_ASSERT(isFolderUsersArrayValid); + if (!isFolderUsersArrayValid) { + qCCritical(lcCseMetadata) << "Empty folderUsers while shouldn't be empty!"; + return {}; + } + + if (!folderUsers.isEmpty()) { + metaObject.insert(usersKey, folderUsers); + } + + if (!_fileDrop.isEmpty()) { + // if we did not consume _fileDrop, we must keep it where it was, on the server + metaObject.insert(filedropKey, _fileDrop); + } + + QJsonDocument internalMetadata; + internalMetadata.setObject(metaObject); + + const auto jsonString = internalMetadata.toJson(); + + const auto metadataForSignature = prepareMetadataForSignature(internalMetadata); + _metadataSignature = _account->e2e()->generateSignatureCryptographicMessageSyntax(metadataForSignature.toBase64()).toBase64(); + + _encryptedMetadataVersion = latestSupportedMetadataVersion(); + + return jsonString; +} + +QByteArray FolderMetadata::encryptedMetadataLegacy() +{ + qCDebug(lcCseMetadata) << "Generating metadata"; + + if (_metadataKeyForEncryption.isEmpty()) { + qCDebug(lcCseMetadata) << "Metadata generation failed! Empty metadata key!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return {}; + } + const auto version = _account->capabilities().clientSideEncryptionVersion(); + // multiple toBase64() just to keep with the old (wrong way) + const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption().toBase64().toBase64(), _account->e2e()->_publicKey).toBase64(); + const QJsonObject metadata{ + {versionKey, QString::number(version, 'f', 1)}, + {metadataKeyKey, QJsonValue::fromVariant(encryptedMetadataKey)}, + {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(encryptedMetadataKey))}, + }; + + QJsonObject files; + for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; ++it) { + QJsonObject encrypted; + encrypted.insert("key", QString(it->encryptionKey.toBase64())); + encrypted.insert("filename", it->originalFilename); + encrypted.insert("mimetype", QString(it->mimetype)); + QJsonDocument encryptedDoc; + encryptedDoc.setObject(encrypted); + + QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), metadataKeyForEncryption()); + if (encryptedEncrypted.isEmpty()) { + qCDebug(lcCseMetadata) << "Metadata generation failed!"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + } + QJsonObject file; + file.insert(encryptedKey, encryptedEncrypted); + file.insert(initializationVectorKey, QString(it->initializationVector.toBase64())); + file.insert(authenticationTagKey, QString(it->authenticationTag.toBase64())); + + files.insert(it->encryptedFilename, file); + } + + QJsonObject filedrop; + for (auto fileDropIt = _fileDrop.constBegin(), end = _fileDrop.constEnd(); fileDropIt != end; ++fileDropIt) { + filedrop.insert(fileDropIt.key(), fileDropIt.value()); + } + + auto metaObject = QJsonObject{ + {metadataJsonKey, metadata}, + }; + + if (files.count()) { + metaObject.insert(filesKey, files); + } + + if (filedrop.count()) { + metaObject.insert(filedropKey, filedrop); + } + + _encryptedMetadataVersion = fromItemEncryptionStatusToMedataVersion(EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(version)); + + QJsonDocument internalMetadata; + internalMetadata.setObject(metaObject); + return internalMetadata.toJson(); +} + +EncryptionStatusEnums::ItemEncryptionStatus FolderMetadata::existingMetadataEncryptionStatus() const +{ + return FolderMetadata::fromMedataVersionToItemEncryptionStatus(_existingMetadataVersion); +} + +EncryptionStatusEnums::ItemEncryptionStatus FolderMetadata::encryptedMetadataEncryptionStatus() const +{ + return FolderMetadata::fromMedataVersionToItemEncryptionStatus(_encryptedMetadataVersion); +} + +bool FolderMetadata::isVersion2AndUp() const +{ + return _existingMetadataVersion >= MetadataVersion::Version2_0; +} + +FolderMetadata::MetadataVersion FolderMetadata::latestSupportedMetadataVersion() const +{ + const auto itemEncryptionStatusFromApiVersion = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion()); + return fromItemEncryptionStatusToMedataVersion(itemEncryptionStatusFromApiVersion); +} + +bool FolderMetadata::parseFileDropPart(const QJsonDocument &doc) +{ + const auto fileDropObject = doc.object().value(filedropKey).toObject(); + const auto fileDropMap = fileDropObject.toVariantMap(); + + for (auto it = std::cbegin(fileDropMap); it != std::cend(fileDropMap); ++it) { + const auto fileDropEntryParsed = it.value().toMap(); + FileDropEntry fileDropEntry{it.key(), + fileDropEntryParsed.value(cipherTextKey).toByteArray(), + QByteArray::fromBase64(fileDropEntryParsed.value(nonceKey).toByteArray()), + QByteArray::fromBase64(fileDropEntryParsed.value(authenticationTagKey).toByteArray()), + {}}; + const auto usersRaw = fileDropEntryParsed.value(usersKey).toList(); + for (const auto &userRaw : usersRaw) { + const auto userParsed = userRaw.toMap(); + const auto userParsedId = userParsed.value(usersUserIdKey).toByteArray(); + if (userParsedId == _account->davUser()) { + const auto fileDropEntryUser = UserWithFileDropEntryAccess{ + userParsedId, + decryptDataWithPrivateKey(QByteArray::fromBase64(userParsed.value(usersEncryptedFiledropKey).toByteArray()))}; + if (!fileDropEntryUser.isValid()) { + qCDebug(lcCseMetadata()) << "Could not parse filedrop data. encryptedFiledropKey decryption failed"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return false; + } + fileDropEntry.currentUser = fileDropEntryUser; + break; + } + } + if (!fileDropEntry.isValid()) { + qCDebug(lcCseMetadata()) << "Could not parse filedrop data. fileDropEntry is invalid for userId" << fileDropEntry.currentUser.userId; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return false; + } + if (fileDropEntry.currentUser.isValid()) { + _fileDropEntries.push_back(fileDropEntry); + } + } + return true; +} + +void FolderMetadata::setFileDrop(const QJsonObject &fileDrop) +{ + _fileDrop = fileDrop; +} + +QByteArray FolderMetadata::metadataSignature() const +{ + return _metadataSignature; +} + +QByteArray FolderMetadata::initialMetadata() const +{ + return _initialMetadata; +} + +quint64 FolderMetadata::newCounter() const +{ + return _counter + 1; +} + +EncryptionStatusEnums::ItemEncryptionStatus FolderMetadata::fromMedataVersionToItemEncryptionStatus(const MetadataVersion metadataVersion) +{ + switch (metadataVersion) { + case FolderMetadata::MetadataVersion::Version2_0: + return SyncFileItem::EncryptionStatus::EncryptedMigratedV2_0; + case FolderMetadata::MetadataVersion::Version1_2: + return SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; + case FolderMetadata::MetadataVersion::Version1: + return SyncFileItem::EncryptionStatus::Encrypted; + case FolderMetadata::MetadataVersion::VersionUndefined: + return SyncFileItem::EncryptionStatus::NotEncrypted; + } + return SyncFileItem::EncryptionStatus::NotEncrypted; +} + +FolderMetadata::MetadataVersion FolderMetadata::fromItemEncryptionStatusToMedataVersion(const EncryptionStatusEnums::ItemEncryptionStatus encryptionStatus) +{ + switch (encryptionStatus) { + case EncryptionStatusEnums::ItemEncryptionStatus::Encrypted: + return MetadataVersion::Version1; + case EncryptionStatusEnums::ItemEncryptionStatus::EncryptedMigratedV1_2: + return MetadataVersion::Version1_2; + case EncryptionStatusEnums::ItemEncryptionStatus::EncryptedMigratedV2_0: + return MetadataVersion::Version2_0; + case EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted: + return MetadataVersion::VersionUndefined; + } + return MetadataVersion::VersionUndefined; +} + +QByteArray FolderMetadata::prepareMetadataForSignature(const QJsonDocument &fullMetadata) +{ + auto metdataModified = fullMetadata; + + auto modifiedObject = metdataModified.object(); + modifiedObject.remove(filedropKey); + + if (modifiedObject.contains(usersKey)) { + const auto folderUsers = modifiedObject[usersKey].toArray(); + + QJsonArray modofiedFolderUsers; + + for (auto it = folderUsers.constBegin(); it != folderUsers.constEnd(); ++it) { + auto folderUserObject = it->toObject(); + folderUserObject.remove(usersEncryptedFiledropKey); + modofiedFolderUsers.push_back(folderUserObject); + } + modifiedObject.insert(usersKey, modofiedFolderUsers); + } + + metdataModified.setObject(modifiedObject); + return metdataModified.toJson(QJsonDocument::Compact); +} + +void FolderMetadata::addEncryptedFile(const EncryptedFile &f) { + Q_ASSERT(_isMetadataValid); + if (!_isMetadataValid) { + qCCritical(lcCseMetadata()) << "Could not add encrypted file to non-initialized metadata!"; + return; + } + + for (int i = 0; i < _files.size(); ++i) { + if (_files.at(i).originalFilename == f.originalFilename) { + _files.removeAt(i); + break; + } + } + + _files.append(f); +} + +const QByteArray FolderMetadata::metadataKeyForDecryption() const +{ + return _metadataKeyForDecryption; +} + +void FolderMetadata::removeEncryptedFile(const EncryptedFile &f) +{ + for (int i = 0; i < _files.size(); ++i) { + if (_files.at(i).originalFilename == f.originalFilename) { + _files.removeAt(i); + break; + } + } +} + +void FolderMetadata::removeAllEncryptedFiles() +{ + _files.clear(); +} + +QVector FolderMetadata::files() const +{ + return _files; +} + +bool FolderMetadata::isFileDropPresent() const +{ + return !_fileDropEntries.isEmpty(); +} + +bool FolderMetadata::isRootEncryptedFolder() const +{ + return _isRootEncryptedFolder; +} + +bool FolderMetadata::encryptedMetadataNeedUpdate() const +{ + // TODO: For now we do not migrated to V2 if a folder has subfolders, remove the following code and only leave "return latestSupportedMetadataVersion() > _existingMetadataVersion;" + if (latestSupportedMetadataVersion() <= _existingMetadataVersion) { + return false; + } + + const auto foundNestedFoldersOrIsNestedFolder = !_isRootEncryptedFolder + || std::find_if(std::cbegin(_files), std::cend(_files), [](const auto &file) { return file.isDirectory(); }) != std::cend(_files); + + return !foundNestedFoldersOrIsNestedFolder; +} + +bool FolderMetadata::moveFromFileDropToFiles() +{ + if (_fileDropEntries.isEmpty()) { + return false; + } + + for (const auto &fileDropEntry : std::as_const(_fileDropEntries)) { + const auto cipherTextDecrypted = EncryptionHelper::decryptThenUnGzipData( + fileDropEntry.currentUser.decryptedFiledropKey, + QByteArray::fromBase64(fileDropEntry.cipherText), + fileDropEntry.nonce); + + if (cipherTextDecrypted.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not decrypt filedrop metadata."; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return false; + } + + const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted); + const auto parsedEncryptedFile = parseEncryptedFileFromJson(fileDropEntry.encryptedFilename, cipherTextDocument.object()); + if (parsedEncryptedFile.originalFilename.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could parse filedrop metadata. Encrypted file" << parsedEncryptedFile.encryptedFilename << "metadata is invalid"; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return false; + } + if (parsedEncryptedFile.mimetype.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could parse filedrop metadata. mimetype is empty for file" << parsedEncryptedFile.originalFilename; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return false; + } + addEncryptedFile(parsedEncryptedFile); + } + + _fileDropEntries.clear(); + setFileDrop({}); + + return true; +} + +void FolderMetadata::startFetchRootE2eeFolderMetadata(const QString &path) +{ + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(_account, path, nullptr, "/")); + + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::fetchFinished, + this, + &FolderMetadata::slotRootE2eeFolderMetadataReceived); + _encryptedFolderMetadataHandler->fetchMetadata(RootEncryptedFolderInfo::makeDefault(), EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata); +} + +void FolderMetadata::slotRootE2eeFolderMetadataReceived(int statusCode, const QString &message) +{ + Q_UNUSED(message); + if (statusCode != 200) { + qCDebug(lcCseMetadata()) << "Could not fetch root folder metadata" << statusCode << message; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + } + const auto rootE2eeFolderMetadata = _encryptedFolderMetadataHandler->folderMetadata(); + if (statusCode != 200 || !rootE2eeFolderMetadata->isValid() || !rootE2eeFolderMetadata->isVersion2AndUp()) { + initMetadata(); + return; + } + + _metadataKeyForEncryption = rootE2eeFolderMetadata->metadataKeyForEncryption(); + + if (!isVersion2AndUp()) { + initMetadata(); + return; + } + + _metadataKeyForDecryption = rootE2eeFolderMetadata->metadataKeyForDecryption(); + _metadataKeyForEncryption = rootE2eeFolderMetadata->metadataKeyForEncryption(); + _keyChecksums = rootE2eeFolderMetadata->keyChecksums(); + initMetadata(); +} + +bool FolderMetadata::addUser(const QString &userId, const QSslCertificate &certificate) +{ + Q_ASSERT(_isRootEncryptedFolder); + if (!_isRootEncryptedFolder) { + qCWarning(lcCseMetadata()) << "Could not add a folder user to a non top level folder."; + return false; + } + + const auto certificatePublicKey = certificate.publicKey(); + if (userId.isEmpty() || certificate.isNull() || certificatePublicKey.isNull()) { + qCWarning(lcCseMetadata()) << "Could not add a folder user. Invalid userId or certificate."; + return false; + } + + createNewMetadataKeyForEncryption(); + UserWithFolderAccess newFolderUser; + newFolderUser.userId = userId; + newFolderUser.certificatePem = certificate.toPem(); + newFolderUser.encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey); + _folderUsers[userId] = newFolderUser; + updateUsersEncryptedMetadataKey(); + + return true; +} + +bool FolderMetadata::removeUser(const QString &userId) +{ + Q_ASSERT(_isRootEncryptedFolder); + if (!_isRootEncryptedFolder) { + qCWarning(lcCseMetadata()) << "Could not add remove folder user from a non top level folder."; + return false; + } + Q_ASSERT(!userId.isEmpty()); + if (userId.isEmpty()) { + qCDebug(lcCseMetadata()) << "Could not remove a folder user. Invalid userId."; + return false; + } + + createNewMetadataKeyForEncryption(); + _folderUsers.remove(userId); + updateUsersEncryptedMetadataKey(); + + return true; +} + +void FolderMetadata::updateUsersEncryptedMetadataKey() +{ + Q_ASSERT(_isRootEncryptedFolder); + if (!_isRootEncryptedFolder) { + qCWarning(lcCseMetadata()) << "Could not update folder users in a non top level folder."; + return; + } + Q_ASSERT(!metadataKeyForEncryption().isEmpty()); + if (metadataKeyForEncryption().isEmpty()) { + qCWarning(lcCseMetadata()) << "Could not update folder users with empty metadataKey!"; + return; + } + for (auto it = _folderUsers.constBegin(); it != _folderUsers.constEnd(); ++it) { + auto folderUser = it.value(); + + const QSslCertificate certificate(folderUser.certificatePem); + const auto certificatePublicKey = certificate.publicKey(); + if (certificate.isNull() || certificatePublicKey.isNull()) { + qCWarning(lcCseMetadata()) << "Could not update folder users with null certificatePublicKey!"; + continue; + } + + const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey); + if (encryptedMetadataKey.isEmpty()) { + qCWarning(lcCseMetadata()) << "Could not update folder users with empty encryptedMetadataKey!"; + continue; + } + + folderUser.encryptedMetadataKey = encryptedMetadataKey; + + _folderUsers[it.key()] = folderUser; + } +} + +void FolderMetadata::createNewMetadataKeyForEncryption() +{ + if (!_isRootEncryptedFolder) { + return; + } + _metadataKeyForEncryption = EncryptionHelper::generateRandom(metadataKeySize); + if (!metadataKeyForEncryption().isEmpty()) { + _keyChecksums.insert(calcSha256(metadataKeyForEncryption())); + } +} + +bool FolderMetadata::verifyMetadataKey(const QByteArray &metadataKey) const +{ + if (_existingMetadataVersion < MetadataVersion::Version2_0) { + return true; + } + if (metadataKey.isEmpty() || metadataKey.size() < metadataKeySize ) { + return false; + } + const QByteArray metadataKeyLimitedLength(metadataKey.data(), metadataKeySize ); + // _keyChecksums should not be empty, fix this by taking a proper _keyChecksums from the topLevelFolder + return _keyChecksums.contains(calcSha256(metadataKeyLimitedLength)) || _keyChecksums.isEmpty(); +} +} diff --git a/src/libsync/foldermetadata.h b/src/libsync/foldermetadata.h new file mode 100644 index 0000000000000..2a7bbc919e925 --- /dev/null +++ b/src/libsync/foldermetadata.h @@ -0,0 +1,245 @@ +#pragma once +/* + * Copyright (C) 2024 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 "accountfwd.h" +#include "encryptedfoldermetadatahandler.h" +#include "csync.h" +#include "rootencryptedfolderinfo.h" +#include +#include +#include +#include +#include +#include +#include +#include + +class QSslCertificate; +class QJsonDocument; +class TestClientSideEncryptionV2; +class TestSecureFileDrop; +namespace OCC +{ + // Handles parsing and altering the metadata, encryption and decryption. Setup of the instance is always asynchronouse and emits void setupComplete() +class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject +{ + friend class ::TestClientSideEncryptionV2; + friend class ::TestSecureFileDrop; + Q_OBJECT + + struct UserWithFolderAccess { + QString userId; + QByteArray certificatePem; + QByteArray encryptedMetadataKey; + }; + + // based on api-version and "version" key in metadata JSON + enum MetadataVersion { + VersionUndefined = -1, + Version1, + Version1_2, + Version2_0, + }; + + struct UserWithFileDropEntryAccess { + QString userId; + QByteArray decryptedFiledropKey; + + inline bool isValid() const + { + return !userId.isEmpty() && !decryptedFiledropKey.isEmpty(); + } + }; + + struct FileDropEntry { + QString encryptedFilename; + QByteArray cipherText; + QByteArray nonce; + QByteArray authenticationTag; + UserWithFileDropEntryAccess currentUser; + + inline bool isValid() const + { + return !cipherText.isEmpty() && !nonce.isEmpty() && !authenticationTag.isEmpty(); + } + }; + +public: + struct EncryptedFile { + QByteArray encryptionKey; + QByteArray mimetype; + QByteArray initializationVector; + QByteArray authenticationTag; + QString encryptedFilename; + QString originalFilename; + bool isDirectory() const; + }; + + enum class FolderType { + Nested = 0, + Root = 1, + }; + Q_ENUM(FolderType) + + FolderMetadata(AccountPtr account, FolderType folderType = FolderType::Nested); + /* + * 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 + * see: https://github.com/nextcloud/end_to_end_encryption_rfc/blob/v2.1/RFC.md + */ + FolderMetadata(AccountPtr account, + const QByteArray &metadata, + const RootEncryptedFolderInfo &rootEncryptedFolderInfo, + const QByteArray &signature, + QObject *parent = nullptr); + + [[nodiscard]] QVector files() const; + + [[nodiscard]] bool isValid() const; + + [[nodiscard]] bool isFileDropPresent() const; + + [[nodiscard]] bool isRootEncryptedFolder() const; + + [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + + [[nodiscard]] bool moveFromFileDropToFiles(); + + // adds a user to have access to this folder (always generates new metadata key) + [[nodiscard]] bool addUser(const QString &userId, const QSslCertificate &certificate); + // removes a user from this folder and removes and generates a new metadata key + [[nodiscard]] bool removeUser(const QString &userId); + + [[nodiscard]] const QByteArray metadataKeyForEncryption() const; + [[nodiscard]] const QByteArray metadataKeyForDecryption() const; + [[nodiscard]] const QSet &keyChecksums() const; + + [[nodiscard]] QByteArray encryptedMetadata(); + + [[nodiscard]] EncryptionStatusEnums::ItemEncryptionStatus existingMetadataEncryptionStatus() const; + [[nodiscard]] EncryptionStatusEnums::ItemEncryptionStatus encryptedMetadataEncryptionStatus() const; + + [[nodiscard]] bool isVersion2AndUp() const; + + [[nodiscard]] quint64 newCounter() const; + + [[nodiscard]] QByteArray metadataSignature() const; + + [[nodiscard]] QByteArray initialMetadata() const; + +public slots: + void addEncryptedFile(const EncryptedFile &f); + void removeEncryptedFile(const EncryptedFile &f); + void removeAllEncryptedFiles(); + +private: + [[nodiscard]] QByteArray encryptedMetadataLegacy(); + + [[nodiscard]] bool verifyMetadataKey(const QByteArray &metadataKey) const; + + [[nodiscard]] QByteArray encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const; + [[nodiscard]] QByteArray decryptDataWithPrivateKey(const QByteArray &data) const; + + [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; + [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; + + [[nodiscard]] bool checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const; + + [[nodiscard]] QByteArray computeMetadataKeyChecksum(const QByteArray &metadataKey) const; + + [[nodiscard]] EncryptedFile parseEncryptedFileFromJson(const QString &encryptedFilename, const QJsonValue &fileJSON) const; + + [[nodiscard]] QJsonObject convertFileToJsonObject(const EncryptedFile *encryptedFile) const; + + [[nodiscard]] MetadataVersion latestSupportedMetadataVersion() const; + + [[nodiscard]] bool parseFileDropPart(const QJsonDocument &doc); + + void setFileDrop(const QJsonObject &fileDrop); + + static EncryptionStatusEnums::ItemEncryptionStatus fromMedataVersionToItemEncryptionStatus(const MetadataVersion metadataVersion); + static MetadataVersion fromItemEncryptionStatusToMedataVersion(const EncryptionStatusEnums::ItemEncryptionStatus encryptionStatus); + + static QByteArray prepareMetadataForSignature(const QJsonDocument &fullMetadata); + +private slots: + void initMetadata(); + void initEmptyMetadata(); + void initEmptyMetadataLegacy(); + + void setupExistingMetadata(const QByteArray &metadata); + void setupExistingMetadataLegacy(const QByteArray &metadata); + + void setupVersionFromExistingMetadata(const QByteArray &metadata); + + void startFetchRootE2eeFolderMetadata(const QString &path); + void slotRootE2eeFolderMetadataReceived(int statusCode, const QString &message); + + void updateUsersEncryptedMetadataKey(); + void createNewMetadataKeyForEncryption(); + + void emitSetupComplete(); + +signals: + void setupComplete(); + +private: + AccountPtr _account; + QByteArray _initialMetadata; + + bool _isRootEncryptedFolder = false; + // always contains the last generated metadata key (non-encrypted and non-base64) + QByteArray _metadataKeyForEncryption; + // used for storing initial metadataKey to use for decryption, especially in nested folders when changing the metadataKey and re-encrypting nested dirs + QByteArray _metadataKeyForDecryption; + QByteArray _metadataNonce; + // metadatakey checksums for validation during setting up from existing metadata + QSet _keyChecksums; + + // filedrop part non-parsed, for upload in case parsing can not be done (due to not having access for the current user, etc.) + QJsonObject _fileDrop; + // used by unit tests, must get assigned simultaneously with _fileDrop and never erased + QJsonObject _fileDropFromServer; + + // legacy, remove after migration is done + QMap _metadataKeys; + + // users that have access to current folder's "ciphertext", except "filedrop" part + QHash _folderUsers; + + // must increment on each metadata upload + quint64 _counter = 0; + + MetadataVersion _existingMetadataVersion = MetadataVersion::VersionUndefined; + MetadataVersion _encryptedMetadataVersion = MetadataVersion::VersionUndefined; + + // generated each time QByteArray encryptedMetadata() is called, and will later be used for validation if uploaded + QByteArray _metadataSignature; + // signature from server-side metadata + QByteArray _initialSignature; + + // both files and folders info + QVector _files; + + // parsed filedrop entries ready for move + QVector _fileDropEntries; + + // sets to "true" on successful parse + bool _isMetadataValid = false; + + QScopedPointer _encryptedFolderMetadataHandler; +}; + +} // namespace OCC diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 118bf86d55630..6664be246616b 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -22,7 +22,8 @@ #include "propagateremotemove.h" #include "propagateremotemkdir.h" #include "bulkpropagatorjob.h" -#include "updatefiledropmetadata.h" +#include "updatee2eefoldermetadatajob.h" +#include "updatemigratede2eemetadatajob.h" #include "propagatorjobs.h" #include "filesystem.h" #include "common/utility.h" @@ -30,6 +31,7 @@ #include "common/asserts.h" #include "discoveryphase.h" #include "syncfileitem.h" +#include "foldermetadata.h" #ifdef Q_OS_WIN #include @@ -317,25 +319,9 @@ bool PropagateItemJob::hasEncryptedAncestor() const return false; } - const auto path = _item->_file; - const auto slashPosition = path.lastIndexOf('/'); - const auto parentPath = slashPosition >= 0 ? path.left(slashPosition) : QString(); - - auto pathComponents = parentPath.split('/'); - while (!pathComponents.isEmpty()) { - SyncJournalFileRecord rec; - const auto pathCompontentsJointed = pathComponents.join('/'); - if (!propagator()->_journal->getFileRecord(pathCompontentsJointed, &rec)) { - qCWarning(lcPropagator) << "could not get file from local DB" << pathCompontentsJointed; - } - - if (rec.isValid() && rec.isE2eEncrypted()) { - return true; - } - pathComponents.removeLast(); - } - - return false; + SyncJournalFileRecord rec; + return propagator()->_journal->findEncryptedAncestorForRecord(_item->_file, &rec) + && rec.isValid() && rec.isE2eEncrypted(); } void PropagateItemJob::reportClientStatuses() @@ -687,29 +673,15 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, const auto currentDirJob = directories.top().second; currentDirJob->appendJob(directoryPropagationJob.get()); } + directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release())); if (item->_isFileDropDetected) { - directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); - item->_instruction = CSYNC_INSTRUCTION_NONE; + const auto currentDirJob = directories.top().second; + currentDirJob->appendJob(new UpdateE2eeFolderMetadataJob(this, item, item->_file)); + item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA; _anotherSyncNeeded = true; } else if (item->_isEncryptedMetadataNeedUpdate) { - SyncJournalFileRecord record; - const auto isUnexpectedMetadataFormat = _journal->getFileRecord(item->_file, &record) - && record._e2eEncryptionStatus == SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2; - if (isUnexpectedMetadataFormat && _account->shouldSkipE2eeMetadataChecksumValidation()) { - qCDebug(lcPropagator) << "Getting unexpected metadata format, but allowing to continue as shouldSkipE2eeMetadataChecksumValidation is set."; - } else if (isUnexpectedMetadataFormat && !_account->shouldSkipE2eeMetadataChecksumValidation()) { - qCDebug(lcPropagator) << "could have upgraded metadata"; - item->_instruction = CSyncEnums::CSYNC_INSTRUCTION_ERROR; - item->_errorString = tr("Error with the metadata. Getting unexpected metadata format."); - item->_status = SyncFileItem::NormalError; - emit itemCompleted(item, OCC::ErrorCategory::GenericError); - } else { - directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); - item->_instruction = CSYNC_INSTRUCTION_NONE; - _anotherSyncNeeded = true; - } + processE2eeMetadataMigration(item, directories); } - directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release())); } void OwncloudPropagator::startFilePropagation(const SyncFileItemPtr &item, @@ -736,6 +708,61 @@ void OwncloudPropagator::startFilePropagation(const SyncFileItemPtr &item, } } +void OwncloudPropagator::processE2eeMetadataMigration(const SyncFileItemPtr &item, QStack> &directories) +{ + if (item->_e2eEncryptionServerCapability >= EncryptionStatusEnums::ItemEncryptionStatus::EncryptedMigratedV2_0) { + // migrating to v2.0+ + const auto rootE2eeFolderPath = item->_file.split('/').first(); + const auto rootE2eeFolderPathWithSlash = QString(rootE2eeFolderPath + "/"); + + QPair foundDirectory = {QString{}, nullptr}; + for (auto it = std::rbegin(directories); it != std::rend(directories); ++it) { + if (it->first == rootE2eeFolderPathWithSlash) { + foundDirectory = *it; + break; + } + } + + UpdateMigratedE2eeMetadataJob *existingUpdateJob = nullptr; + + SyncFileItemPtr topLevelitem = item; + if (foundDirectory.second) { + topLevelitem = foundDirectory.second->_item; + if (!foundDirectory.second->_subJobs._jobsToDo.isEmpty()) { + for (const auto jobToDo : foundDirectory.second->_subJobs._jobsToDo) { + if (const auto foundExistingUpdateMigratedE2eeMetadataJob = qobject_cast(jobToDo)) { + existingUpdateJob = foundExistingUpdateMigratedE2eeMetadataJob; + break; + } + } + } + } + + if (!existingUpdateJob) { + // we will need to update topLevelitem encryption status so it gets written to database + const auto currentDirJob = directories.top().second; + const auto rootE2eeFolderPathFullRemotePath = fullRemotePath(rootE2eeFolderPath); + const auto updateMetadataJob = new UpdateMigratedE2eeMetadataJob(this, topLevelitem, rootE2eeFolderPathFullRemotePath, remotePath()); + if (item != topLevelitem) { + updateMetadataJob->addSubJobItem(item->_encryptedFileName, item); + } + currentDirJob->appendJob(updateMetadataJob); + } else { + if (item != topLevelitem) { + // simply append subJob item so we can set its encryption status when corresponging subjob finishes + existingUpdateJob->addSubJobItem(item->_encryptedFileName, item); + } + } + } else { + // migrating to v1.2 + const auto remoteFilename = item->_encryptedFileName.isEmpty() ? item->_file : item->_encryptedFileName; + const auto currentDirJob = directories.top().second; + currentDirJob->appendJob(new UpdateE2eeFolderMetadataJob(this, item, remoteFilename)); + } + + item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA; +} + const SyncOptions &OwncloudPropagator::syncOptions() const { return _syncOptions; @@ -1122,8 +1149,6 @@ bool OwncloudPropagator::isInBulkUploadBlackList(const QString &file) const return _bulkUploadBlackList.contains(file); } -// ================================================================================ - PropagatorJob::PropagatorJob(OwncloudPropagator *propagator) : QObject(propagator) { diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index 06e840fb106fb..9f5c8d0581825 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -58,6 +58,7 @@ void blacklistUpdate(SyncJournalDb *journal, SyncFileItem &item); class SyncJournalDb; class OwncloudPropagator; class PropagatorCompositeJob; +class FolderMetadata; /** * @brief the base class of propagator jobs @@ -449,6 +450,8 @@ class OWNCLOUDSYNC_EXPORT OwncloudPropagator : public QObject QString &removedDirectory, QString &maybeConflictDirectory); + void processE2eeMetadataMigration(const SyncFileItemPtr &item, QStack> &directories); + [[nodiscard]] const SyncOptions &syncOptions() const; void setSyncOptions(const SyncOptions &syncOptions); diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index afdf33d491b42..caa6922dbdb24 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -379,7 +379,7 @@ QString GETFileJob::errorString() const GETEncryptedFileJob::GETEncryptedFileJob(AccountPtr account, const QString &path, QIODevice *device, const QMap &headers, const QByteArray &expectedEtagForResume, - qint64 resumeStart, EncryptedFile encryptedInfo, QObject *parent) + qint64 resumeStart, FolderMetadata::EncryptedFile encryptedInfo, QObject *parent) : GETFileJob(account, path, device, headers, expectedEtagForResume, resumeStart, parent) , _encryptedFileInfo(encryptedInfo) { @@ -387,7 +387,7 @@ GETEncryptedFileJob::GETEncryptedFileJob(AccountPtr account, const QString &path GETEncryptedFileJob::GETEncryptedFileJob(AccountPtr account, const QUrl &url, QIODevice *device, const QMap &headers, const QByteArray &expectedEtagForResume, - qint64 resumeStart, EncryptedFile encryptedInfo, QObject *parent) + qint64 resumeStart, FolderMetadata::EncryptedFile encryptedInfo, QObject *parent) : GETFileJob(account, url, device, headers, expectedEtagForResume, resumeStart, parent) , _encryptedFileInfo(encryptedInfo) { diff --git a/src/libsync/propagatedownload.h b/src/libsync/propagatedownload.h index 2a5fea962ce50..a56cdd4a1b0d4 100644 --- a/src/libsync/propagatedownload.h +++ b/src/libsync/propagatedownload.h @@ -18,6 +18,7 @@ #include "networkjobs.h" #include "clientsideencryption.h" #include +#include "foldermetadata.h" #include #include @@ -136,10 +137,10 @@ class OWNCLOUDSYNC_EXPORT GETEncryptedFileJob : public GETFileJob // DOES NOT take ownership of the device. explicit GETEncryptedFileJob(AccountPtr account, const QString &path, QIODevice *device, const QMap &headers, const QByteArray &expectedEtagForResume, - qint64 resumeStart, EncryptedFile encryptedInfo, QObject *parent = nullptr); + qint64 resumeStart, FolderMetadata::EncryptedFile encryptedInfo, QObject *parent = nullptr); explicit GETEncryptedFileJob(AccountPtr account, const QUrl &url, QIODevice *device, const QMap &headers, const QByteArray &expectedEtagForResume, - qint64 resumeStart, EncryptedFile encryptedInfo, QObject *parent = nullptr); + qint64 resumeStart, FolderMetadata::EncryptedFile encryptedInfo, QObject *parent = nullptr); ~GETEncryptedFileJob() override = default; protected: @@ -147,7 +148,7 @@ class OWNCLOUDSYNC_EXPORT GETEncryptedFileJob : public GETFileJob private: QSharedPointer _decryptor; - EncryptedFile _encryptedFileInfo = {}; + FolderMetadata::EncryptedFile _encryptedFileInfo = {}; QByteArray _pendingBytes; qint64 _processedSoFar = 0; }; @@ -253,7 +254,7 @@ private slots: QFile _tmpFile; bool _deleteExisting = false; bool _isEncrypted = false; - EncryptedFile _encryptedInfo; + FolderMetadata::EncryptedFile _encryptedInfo; ConflictRecord _conflictRecord; QElapsedTimer _stopwatch; diff --git a/src/libsync/propagatedownloadencrypted.cpp b/src/libsync/propagatedownloadencrypted.cpp index ccea7d8fb7c7d..837c4f98ffd2b 100644 --- a/src/libsync/propagatedownloadencrypted.cpp +++ b/src/libsync/propagatedownloadencrypted.cpp @@ -1,5 +1,7 @@ #include "propagatedownloadencrypted.h" #include "clientsideencryptionjobs.h" +#include "encryptedfoldermetadatahandler.h" +#include "foldermetadata.h" Q_LOGGING_CATEGORY(lcPropagateDownloadEncrypted, "nextcloud.sync.propagator.download.encrypted", QtInfoMsg) @@ -12,10 +14,6 @@ PropagateDownloadEncrypted::PropagateDownloadEncrypted(OwncloudPropagator *propa , _localParentPath(localParentPath) , _item(item) , _info(_item->_file) -{ -} - -void PropagateDownloadEncrypted::start() { const auto rootPath = [=]() { const auto result = _propagator->remotePath(); @@ -28,71 +26,62 @@ void PropagateDownloadEncrypted::start() const auto remoteFilename = _item->_encryptedFileName.isEmpty() ? _item->_file : _item->_encryptedFileName; const auto remotePath = QString(rootPath + remoteFilename); const auto remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); + _remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); - // Is encrypted Now we need the folder-id - auto job = new LsColJob(_propagator->account(), remoteParentPath, this); - job->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"}); - connect(job, &LsColJob::directoryListingSubfolders, - this, &PropagateDownloadEncrypted::checkFolderId); - connect(job, &LsColJob::finishedWithError, - this, &PropagateDownloadEncrypted::folderIdError); - job->start(); + const auto filenameInDb = _item->_file; + const auto pathInDb = QString(rootPath + filenameInDb); + const auto parentPathInDb = pathInDb.left(pathInDb.lastIndexOf('/')); + _parentPathInDb = pathInDb.left(pathInDb.lastIndexOf('/')); } -void PropagateDownloadEncrypted::folderIdError() +void PropagateDownloadEncrypted::start() { - qCDebug(lcPropagateDownloadEncrypted) << "Failed to get encrypted metadata of folder"; + SyncJournalFileRecord rec; + if (!_propagator->_journal->getRootE2eFolderRecord(_remoteParentPath, &rec) || !rec.isValid()) { + emit failed(); + return; + } + _encryptedFolderMetadataHandler.reset( + new EncryptedFolderMetadataHandler(_propagator->account(), _remoteParentPath, _propagator->_journal, rec.path())); + + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::fetchFinished, + this, + &PropagateDownloadEncrypted::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata); } -void PropagateDownloadEncrypted::checkFolderId(const QStringList &list) +void PropagateDownloadEncrypted::slotFetchMetadataJobFinished(int statusCode, const QString &message) { - auto job = qobject_cast(sender()); - const QString folderId = list.first(); - qCDebug(lcPropagateDownloadEncrypted) << "Received id of folder" << folderId; + if (statusCode != 200) { + qCCritical(lcPropagateDownloadEncrypted) << "Failed to find encrypted metadata information of remote file" << _info.fileName() << message; + emit failed(); + return; + } - const ExtraFolderInfo &folderInfo = job->_folderInfos.value(folderId); + qCDebug(lcPropagateDownloadEncrypted) << "Metadata Received reading" << _item->_instruction << _item->_file << _item->_encryptedFileName; - // Now that we have the folder-id we need it's JSON metadata - auto metadataJob = new GetMetadataApiJob(_propagator->account(), folderInfo.fileId); - connect(metadataJob, &GetMetadataApiJob::jsonReceived, - this, &PropagateDownloadEncrypted::checkFolderEncryptedMetadata); - connect(metadataJob, &GetMetadataApiJob::error, - this, &PropagateDownloadEncrypted::folderEncryptedMetadataError); + const auto metadata = _encryptedFolderMetadataHandler->folderMetadata(); - metadataJob->start(); -} + if (!metadata || !metadata->isValid()) { + emit failed(); + qCCritical(lcPropagateDownloadEncrypted) << "Failed to find encrypted metadata information of remote file" << _info.fileName(); + } -void PropagateDownloadEncrypted::folderEncryptedMetadataError(const QByteArray & /*fileId*/, int /*httpReturnCode*/) -{ - qCCritical(lcPropagateDownloadEncrypted) << "Failed to find encrypted metadata information of remote file" << _info.fileName(); - emit failed(); -} + const auto files = metadata->files(); -void PropagateDownloadEncrypted::checkFolderEncryptedMetadata(const QJsonDocument &json) -{ - qCDebug(lcPropagateDownloadEncrypted) << "Metadata Received reading" - << _item->_instruction << _item->_file << _item->_encryptedFileName; - const QString filename = _info.fileName(); - const FolderMetadata metadata(_propagator->account(), - _item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact)); - if (metadata.isMetadataSetup()) { - const QVector files = metadata.files(); - - const QString encryptedFilename = _item->_encryptedFileName.section(QLatin1Char('/'), -1); - for (const EncryptedFile &file : files) { - if (encryptedFilename == file.encryptedFilename) { - _encryptedInfo = file; - - qCDebug(lcPropagateDownloadEncrypted) << "Found matching encrypted metadata for file, starting download"; - emit fileMetadataFound(); - return; - } - } - } - - emit failed(); - qCCritical(lcPropagateDownloadEncrypted) << "Failed to find encrypted metadata information of remote file" << filename; + const auto encryptedFilename = _item->_encryptedFileName.section(QLatin1Char('/'), -1); + for (const FolderMetadata::EncryptedFile &file : files) { + if (encryptedFilename == file.encryptedFilename) { + _encryptedInfo = file; + + qCDebug(lcPropagateDownloadEncrypted) << "Found matching encrypted metadata for file, starting download"; + emit fileMetadataFound(); + return; + } + } + qCCritical(lcPropagateDownloadEncrypted) << "Failed to find matching encrypted metadata for file, starting download of remote file" << _info.fileName(); + emit failed(); } // TODO: Fix this. Exported in the wrong place. @@ -133,4 +122,4 @@ QString PropagateDownloadEncrypted::errorString() const return _errorString; } -} +} \ No newline at end of file diff --git a/src/libsync/propagatedownloadencrypted.h b/src/libsync/propagatedownloadencrypted.h index b379f96cb6867..bc5c4a83ca6c4 100644 --- a/src/libsync/propagatedownloadencrypted.h +++ b/src/libsync/propagatedownloadencrypted.h @@ -7,11 +7,12 @@ #include "syncfileitem.h" #include "owncloudpropagator.h" #include "clientsideencryption.h" +#include "foldermetadata.h" class QJsonDocument; namespace OCC { - +class EncryptedFolderMetadataHandler; class PropagateDownloadEncrypted : public QObject { Q_OBJECT public: @@ -20,11 +21,8 @@ class PropagateDownloadEncrypted : public QObject { bool decryptFile(QFile& tmpFile); [[nodiscard]] QString errorString() const; -public slots: - void checkFolderId(const QStringList &list); - void checkFolderEncryptedMetadata(const QJsonDocument &json); - void folderIdError(); - void folderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode); +private slots: + void slotFetchMetadataJobFinished(int statusCode, const QString &message); signals: void fileMetadataFound(); @@ -37,8 +35,12 @@ public slots: QString _localParentPath; SyncFileItemPtr _item; QFileInfo _info; - EncryptedFile _encryptedInfo; + FolderMetadata::EncryptedFile _encryptedInfo; QString _errorString; + QString _remoteParentPath; + QString _parentPathInDb; + + QScopedPointer _encryptedFolderMetadataHandler; }; } diff --git a/src/libsync/propagateremotedelete.cpp b/src/libsync/propagateremotedelete.cpp index ede4f07b40943..306ed4b1d73bf 100644 --- a/src/libsync/propagateremotedelete.cpp +++ b/src/libsync/propagateremotedelete.cpp @@ -39,7 +39,7 @@ void PropagateRemoteDelete::start() } else { _deleteEncryptedHelper = new PropagateRemoteDeleteEncryptedRootFolder(propagator(), _item, this); } - connect(_deleteEncryptedHelper, &AbstractPropagateRemoteDeleteEncrypted::finished, this, [this] (bool success) { + connect(_deleteEncryptedHelper, &BasePropagateRemoteDeleteEncrypted::finished, this, [this] (bool success) { if (!success) { SyncFileItem::Status status = SyncFileItem::NormalError; if (_deleteEncryptedHelper->networkError() != QNetworkReply::NoError && _deleteEncryptedHelper->networkError() != QNetworkReply::ContentNotFoundError) { diff --git a/src/libsync/propagateremotedelete.h b/src/libsync/propagateremotedelete.h index 4385e45451357..244217e5229a7 100644 --- a/src/libsync/propagateremotedelete.h +++ b/src/libsync/propagateremotedelete.h @@ -20,7 +20,7 @@ namespace OCC { class DeleteJob; -class AbstractPropagateRemoteDeleteEncrypted; +class BasePropagateRemoteDeleteEncrypted; /** * @brief The PropagateRemoteDelete class @@ -30,7 +30,7 @@ class PropagateRemoteDelete : public PropagateItemJob { Q_OBJECT QPointer _job; - AbstractPropagateRemoteDeleteEncrypted *_deleteEncryptedHelper = nullptr; + BasePropagateRemoteDeleteEncrypted *_deleteEncryptedHelper = nullptr; public: PropagateRemoteDelete(OwncloudPropagator *propagator, const SyncFileItemPtr &item) diff --git a/src/libsync/propagateremotedeleteencrypted.cpp b/src/libsync/propagateremotedeleteencrypted.cpp index 3a81a9968bb97..0bb8c92e6a4b4 100644 --- a/src/libsync/propagateremotedeleteencrypted.cpp +++ b/src/libsync/propagateremotedeleteencrypted.cpp @@ -14,6 +14,7 @@ #include "propagateremotedeleteencrypted.h" #include "clientsideencryptionjobs.h" +#include "foldermetadata.h" #include "owncloudpropagator.h" #include "encryptfolderjob.h" #include @@ -24,7 +25,7 @@ using namespace OCC; Q_LOGGING_CATEGORY(PROPAGATE_REMOVE_ENCRYPTED, "nextcloud.sync.propagator.remove.encrypted") PropagateRemoteDeleteEncrypted::PropagateRemoteDeleteEncrypted(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent) - : AbstractPropagateRemoteDeleteEncrypted(propagator, item, parent) + : BasePropagateRemoteDeleteEncrypted(propagator, item, parent) { } @@ -34,28 +35,26 @@ void PropagateRemoteDeleteEncrypted::start() Q_ASSERT(!_item->_encryptedFileName.isEmpty()); const QFileInfo info(_item->_encryptedFileName); - startLsColJob(info.path()); + fetchMetadataForPath(info.path()); } -void PropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully(const QByteArray &folderId) +void PropagateRemoteDeleteEncrypted::slotFolderUnLockFinished(const QByteArray &folderId, int statusCode) { - AbstractPropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully(folderId); + BasePropagateRemoteDeleteEncrypted::slotFolderUnLockFinished(folderId, statusCode); emit finished(!_isTaskFailed); } -void PropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) +void PropagateRemoteDeleteEncrypted::slotFetchMetadataJobFinished(int statusCode, const QString &message) { + Q_UNUSED(message); if (statusCode == 404) { qCDebug(PROPAGATE_REMOVE_ENCRYPTED) << "Metadata not found, but let's proceed with removing the file anyway."; deleteRemoteItem(_item->_encryptedFileName); return; } - FolderMetadata metadata(_propagator->account(), - _item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact), statusCode); - - if (!metadata.isMetadataSetup()) { + const auto metadata = folderMetadata(); + if (!metadata || !metadata->isValid()) { taskFailed(); return; } @@ -67,10 +66,10 @@ void PropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived(const Q // Find existing metadata for this file bool found = false; - const QVector files = metadata.files(); - for (const EncryptedFile &file : files) { + const QVector files = metadata->files(); + for (const FolderMetadata::EncryptedFile &file : files) { if (file.originalFilename == fileName) { - metadata.removeEncryptedFile(file); + metadata->removeEncryptedFile(file); found = true; break; } @@ -83,12 +82,12 @@ void PropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived(const Q } qCDebug(PROPAGATE_REMOVE_ENCRYPTED) << "Metadata updated, sending to the server."; + uploadMetadata(EncryptedFolderMetadataHandler::UploadMode::KeepLock); +} - auto job = new UpdateMetadataApiJob(_propagator->account(), _folderId, metadata.encryptedMetadata(), _folderToken); - connect(job, &UpdateMetadataApiJob::success, this, [this](const QByteArray& fileId) { - Q_UNUSED(fileId); - deleteRemoteItem(_item->_encryptedFileName); - }); - connect(job, &UpdateMetadataApiJob::error, this, &PropagateRemoteDeleteEncrypted::taskFailed); - job->start(); +void PropagateRemoteDeleteEncrypted::slotUpdateMetadataJobFinished(int statusCode, const QString &message) +{ + Q_UNUSED(statusCode); + Q_UNUSED(message); + deleteRemoteItem(_item->_encryptedFileName); } diff --git a/src/libsync/propagateremotedeleteencrypted.h b/src/libsync/propagateremotedeleteencrypted.h index 85bad20942473..526073a957ec2 100644 --- a/src/libsync/propagateremotedeleteencrypted.h +++ b/src/libsync/propagateremotedeleteencrypted.h @@ -14,11 +14,11 @@ #pragma once -#include "abstractpropagateremotedeleteencrypted.h" +#include "basepropagateremotedeleteencrypted.h" namespace OCC { -class PropagateRemoteDeleteEncrypted : public AbstractPropagateRemoteDeleteEncrypted +class PropagateRemoteDeleteEncrypted : public BasePropagateRemoteDeleteEncrypted { Q_OBJECT public: @@ -27,8 +27,9 @@ class PropagateRemoteDeleteEncrypted : public AbstractPropagateRemoteDeleteEncry void start() override; private: - void slotFolderUnLockedSuccessfully(const QByteArray &folderId) override; - void slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) override; + void slotFolderUnLockFinished(const QByteArray &folderId, int statusCode) override; + void slotFetchMetadataJobFinished(int statusCode, const QString &message) override; + void slotUpdateMetadataJobFinished(int statusCode, const QString &message) override; }; } diff --git a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp index dfde76eca3d9c..849e5f2b27e21 100644 --- a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp +++ b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp @@ -29,6 +29,7 @@ #include "deletejob.h" #include "clientsideencryptionjobs.h" #include "clientsideencryption.h" +#include "foldermetadata.h" #include "encryptfolderjob.h" #include "owncloudpropagator.h" #include "propagateremotedeleteencryptedrootfolder.h" @@ -42,7 +43,7 @@ using namespace OCC; Q_LOGGING_CATEGORY(PROPAGATE_REMOVE_ENCRYPTED_ROOTFOLDER, "nextcloud.sync.propagator.remove.encrypted.rootfolder") PropagateRemoteDeleteEncryptedRootFolder::PropagateRemoteDeleteEncryptedRootFolder(OwncloudPropagator *propagator, SyncFileItemPtr item, QObject *parent) - : AbstractPropagateRemoteDeleteEncrypted(propagator, item, parent) + : BasePropagateRemoteDeleteEncrypted(propagator, item, parent) { } @@ -61,19 +62,23 @@ void PropagateRemoteDeleteEncryptedRootFolder::start() return; } - startLsColJob(_item->_file); + fetchMetadataForPath(_item->_file); } -void PropagateRemoteDeleteEncryptedRootFolder::slotFolderUnLockedSuccessfully(const QByteArray &folderId) +void PropagateRemoteDeleteEncryptedRootFolder::slotFolderUnLockFinished(const QByteArray &folderId, int statusCode) { - AbstractPropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully(folderId); - decryptAndRemoteDelete(); + BasePropagateRemoteDeleteEncrypted::slotFolderUnLockFinished(folderId, statusCode); + if (statusCode == 200) { + decryptAndRemoteDelete(); + } } -void PropagateRemoteDeleteEncryptedRootFolder::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) +void PropagateRemoteDeleteEncryptedRootFolder::slotFetchMetadataJobFinished(int statusCode, const QString &message) { + Q_UNUSED(message); if (statusCode == 404) { - // we've eneded up having no metadata, but, _nestedItems is not empty since we went this far, let's proceed with removing the nested items without modifying the metadata + // we've eneded up having no metadata, but, _nestedItems is not empty since we went this far, let's proceed with removing the nested items without + // modifying the metadata qCDebug(PROPAGATE_REMOVE_ENCRYPTED_ROOTFOLDER) << "There is no metadata for this folder. Just remove it's nested items."; for (auto it = _nestedItems.constBegin(); it != _nestedItems.constEnd(); ++it) { deleteNestedRemoteItem(it.key()); @@ -81,30 +86,31 @@ void PropagateRemoteDeleteEncryptedRootFolder::slotFolderEncryptedMetadataReceiv return; } - FolderMetadata metadata(_propagator->account(), - _item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact), statusCode); + const auto metadata = folderMetadata(); - if (!metadata.isMetadataSetup()) { + if (!metadata || !metadata->isValid()) { taskFailed(); return; } qCDebug(PROPAGATE_REMOVE_ENCRYPTED_ROOTFOLDER) << "It's a root encrypted folder. Let's remove nested items first."; - metadata.removeAllEncryptedFiles(); + metadata->removeAllEncryptedFiles(); qCDebug(PROPAGATE_REMOVE_ENCRYPTED_ROOTFOLDER) << "Metadata updated, sending to the server."; + uploadMetadata(EncryptedFolderMetadataHandler::UploadMode::KeepLock); +} - auto job = new UpdateMetadataApiJob(_propagator->account(), _folderId, metadata.encryptedMetadata(), _folderToken); - connect(job, &UpdateMetadataApiJob::success, this, [this](const QByteArray& fileId) { - Q_UNUSED(fileId); - for (auto it = _nestedItems.constBegin(); it != _nestedItems.constEnd(); ++it) { - deleteNestedRemoteItem(it.key()); - } - }); - connect(job, &UpdateMetadataApiJob::error, this, &PropagateRemoteDeleteEncryptedRootFolder::taskFailed); - job->start(); +void PropagateRemoteDeleteEncryptedRootFolder::slotUpdateMetadataJobFinished(int statusCode, const QString &message) +{ + Q_UNUSED(message); + if (statusCode != 200) { + taskFailed(); + return; + } + for (auto it = _nestedItems.constBegin(); it != _nestedItems.constEnd(); ++it) { + deleteNestedRemoteItem(it.key()); + } } void PropagateRemoteDeleteEncryptedRootFolder::slotDeleteNestedRemoteItemFinished() @@ -167,7 +173,7 @@ void PropagateRemoteDeleteEncryptedRootFolder::slotDeleteNestedRemoteItemFinishe taskFailed(); return; } - unlockFolder(); + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success); } } @@ -176,7 +182,7 @@ void PropagateRemoteDeleteEncryptedRootFolder::deleteNestedRemoteItem(const QStr qCInfo(PROPAGATE_REMOVE_ENCRYPTED_ROOTFOLDER) << "Deleting nested encrypted remote item" << filename; auto deleteJob = new DeleteJob(_propagator->account(), _propagator->fullRemotePath(filename), this); - deleteJob->setFolderToken(_folderToken); + deleteJob->setFolderToken(folderToken()); deleteJob->setProperty(encryptedFileNamePropertyKey, filename); connect(deleteJob, &DeleteJob::finishedSignal, this, &PropagateRemoteDeleteEncryptedRootFolder::slotDeleteNestedRemoteItemFinished); diff --git a/src/libsync/propagateremotedeleteencryptedrootfolder.h b/src/libsync/propagateremotedeleteencryptedrootfolder.h index d0210f7f350f8..73c22475230aa 100644 --- a/src/libsync/propagateremotedeleteencryptedrootfolder.h +++ b/src/libsync/propagateremotedeleteencryptedrootfolder.h @@ -16,12 +16,12 @@ #include -#include "abstractpropagateremotedeleteencrypted.h" +#include "basepropagateremotedeleteencrypted.h" #include "syncfileitem.h" namespace OCC { -class PropagateRemoteDeleteEncryptedRootFolder : public AbstractPropagateRemoteDeleteEncrypted +class PropagateRemoteDeleteEncryptedRootFolder : public BasePropagateRemoteDeleteEncrypted { Q_OBJECT public: @@ -30,8 +30,9 @@ class PropagateRemoteDeleteEncryptedRootFolder : public AbstractPropagateRemoteD void start() override; private: - void slotFolderUnLockedSuccessfully(const QByteArray &folderId) override; - void slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) override; + void slotFolderUnLockFinished(const QByteArray &folderId, int statusCode) override; + void slotFetchMetadataJobFinished(int statusCode, const QString &message) override; + void slotUpdateMetadataJobFinished(int statusCode, const QString &message) override; void slotDeleteNestedRemoteItemFinished(); void deleteNestedRemoteItem(const QString &filename); diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp index 805b7dc8e8de6..8f8da5fd30624 100644 --- a/src/libsync/propagateremotemkdir.cpp +++ b/src/libsync/propagateremotemkdir.cpp @@ -21,6 +21,7 @@ #include "common/asserts.h" #include "encryptfolderjob.h" #include "filesystem.h" +#include "csync/csync.h" #include #include @@ -156,7 +157,9 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con // We're expecting directory path in /Foo/Bar convention... Q_ASSERT(jobPath.startsWith('/') && !jobPath.endsWith('/')); // But encryption job expect it in Foo/Bar/ convention - auto job = new OCC::EncryptFolderJob(propagator()->account(), propagator()->_journal, jobPath.mid(1), _item->_fileId, this); + auto job = new OCC::EncryptFolderJob(propagator()->account(), propagator()->_journal, jobPath.mid(1), _item->_fileId, propagator(), _item); + job->setParent(this); + job->setPathNonEncrypted(_item->_file); connect(job, &OCC::EncryptFolderJob::finished, this, &PropagateRemoteMkdir::slotEncryptFolderFinished); job->start(); } @@ -239,11 +242,19 @@ void PropagateRemoteMkdir::slotMkcolJobFinished() } } -void PropagateRemoteMkdir::slotEncryptFolderFinished() +void PropagateRemoteMkdir::slotEncryptFolderFinished(int status, EncryptionStatusEnums::ItemEncryptionStatus encryptionStatus) { + if (status != EncryptFolderJob::Success) { + done(SyncFileItem::FatalError, tr("Failed to encrypt a folder %1").arg(_item->_file), ErrorCategory::GenericError); + return; + } qCDebug(lcPropagateRemoteMkdir) << "Success making the new folder encrypted"; propagator()->_activeJobList.removeOne(this); - _item->_e2eEncryptionStatus = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; + _item->_e2eEncryptionStatus = encryptionStatus; + _item->_e2eEncryptionStatusRemote = encryptionStatus; + if (_item->isEncrypted()) { + _item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion()); + } success(); } diff --git a/src/libsync/propagateremotemkdir.h b/src/libsync/propagateremotemkdir.h index 2ba9fbc39aaf0..3b93824f48aab 100644 --- a/src/libsync/propagateremotemkdir.h +++ b/src/libsync/propagateremotemkdir.h @@ -53,7 +53,7 @@ private slots: void slotStartMkcolJob(); void slotStartEncryptedMkcolJob(const QString &path, const QString &filename, quint64 size); void slotMkcolJobFinished(); - void slotEncryptFolderFinished(); + void slotEncryptFolderFinished(int status, EncryptionStatusEnums::ItemEncryptionStatus encryptionStatus); void success(); private: diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 78b04205cc361..b723e95858617 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -438,8 +438,8 @@ void PropagateUploadFileCommon::slotStartUpload(const QByteArray &transmissionCh void PropagateUploadFileCommon::slotFolderUnlocked(const QByteArray &folderId, int httpReturnCode) { - qDebug() << "Failed to unlock encrypted folder" << folderId; if (_uploadStatus.status == SyncFileItem::NoStatus && httpReturnCode != 200) { + qDebug() << "Failed to unlock encrypted folder" << folderId; done(SyncFileItem::FatalError, tr("Failed to unlock encrypted folder.")); } else { done(_uploadStatus.status, _uploadStatus.message); diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index fbbbee3e7b1c2..2e24e77254694 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -2,8 +2,9 @@ #include "clientsideencryptionjobs.h" #include "networkjobs.h" #include "clientsideencryption.h" +#include "foldermetadata.h" +#include "encryptedfoldermetadatahandler.h" #include "account.h" - #include #include #include @@ -21,11 +22,6 @@ PropagateUploadEncrypted::PropagateUploadEncrypted(OwncloudPropagator *propagato , _propagator(propagator) , _remoteParentPath(remoteParentPath) , _item(item) - , _metadata(nullptr) -{ -} - -void PropagateUploadEncrypted::start() { const auto rootPath = [=]() { const auto result = _propagator->remotePath(); @@ -35,15 +31,18 @@ void PropagateUploadEncrypted::start() return result; } }(); - const auto absoluteRemoteParentPath = [=]{ + _remoteParentAbsolutePath = [=] { auto path = QString(rootPath + _remoteParentPath); if (path.endsWith('/')) { path.chop(1); } return path; }(); +} +void PropagateUploadEncrypted::start() +{ /* If the file is in a encrypted folder, which we know, we wouldn't be here otherwise, * we need to do the long road: * find the ID of the folder. @@ -54,257 +53,147 @@ void PropagateUploadEncrypted::start() * upload the metadata * unlock the folder. */ - qCDebug(lcPropagateUploadEncrypted) << "Folder is encrypted, let's get the Id from it."; - auto job = new LsColJob(_propagator->account(), absoluteRemoteParentPath, this); - job->setProperties({"resourcetype", "http://owncloud.org/ns:fileid"}); - connect(job, &LsColJob::directoryListingSubfolders, this, &PropagateUploadEncrypted::slotFolderEncryptedIdReceived); - connect(job, &LsColJob::finishedWithError, this, &PropagateUploadEncrypted::slotFolderEncryptedIdError); - job->start(); + // Encrypt File! + SyncJournalFileRecord rec; + if (!_propagator->_journal->getRootE2eFolderRecord(_remoteParentAbsolutePath, &rec) || !rec.isValid()) { + emit error(); + return; + } + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(_propagator->account(), + _remoteParentAbsolutePath, + _propagator->_journal, + rec.path())); + + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::fetchFinished, + this, &PropagateUploadEncrypted::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata); } -/* We try to lock a folder, if it's locked we try again in one second. - * if it's still locked we try again in one second. looping until one minute. - * -> fail. - * the 'loop': / - * slotFolderEncryptedIdReceived -> slotTryLock -> lockError -> stillTime? -> slotTryLock - * \ - * -> success. - */ - -void PropagateUploadEncrypted::slotFolderEncryptedIdReceived(const QStringList &list) +void PropagateUploadEncrypted::unlockFolder() { - qCDebug(lcPropagateUploadEncrypted) << "Received id of folder, trying to lock it so we can prepare the metadata"; - auto job = qobject_cast(sender()); - const auto& folderInfo = job->_folderInfos.value(list.first()); - _folderLockFirstTry.start(); - slotTryLock(folderInfo.fileId); + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &PropagateUploadEncrypted::folderUnlocked); + _encryptedFolderMetadataHandler->unlockFolder(); } -void PropagateUploadEncrypted::slotTryLock(const QByteArray& fileId) +bool PropagateUploadEncrypted::isUnlockRunning() const { - const auto lockJob = new LockEncryptFolderApiJob(_propagator->account(), fileId, _propagator->_journal, _propagator->account()->e2e()->_publicKey, this); - connect(lockJob, &LockEncryptFolderApiJob::success, this, &PropagateUploadEncrypted::slotFolderLockedSuccessfully); - connect(lockJob, &LockEncryptFolderApiJob::error, this, &PropagateUploadEncrypted::slotFolderLockedError); - lockJob->start(); + return _encryptedFolderMetadataHandler->isUnlockRunning(); } -void PropagateUploadEncrypted::slotFolderLockedSuccessfully(const QByteArray& fileId, const QByteArray& token) +bool PropagateUploadEncrypted::isFolderLocked() const { - qCDebug(lcPropagateUploadEncrypted) << "Folder" << fileId << "Locked Successfully for Upload, Fetching Metadata"; - // Should I use a mutex here? - _currentLockingInProgress = true; - _folderToken = token; - _folderId = fileId; - _isFolderLocked = true; - - auto job = new GetMetadataApiJob(_propagator->account(), _folderId); - connect(job, &GetMetadataApiJob::jsonReceived, - this, &PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived); - connect(job, &GetMetadataApiJob::error, - this, &PropagateUploadEncrypted::slotFolderEncryptedMetadataError); - - job->start(); + return _encryptedFolderMetadataHandler->isFolderLocked(); } -void PropagateUploadEncrypted::slotFolderEncryptedMetadataError(const QByteArray& fileId, int httpReturnCode) +const QByteArray PropagateUploadEncrypted::folderToken() const { - Q_UNUSED(fileId); - Q_UNUSED(httpReturnCode); - qCDebug(lcPropagateUploadEncrypted()) << "Error Getting the encrypted metadata. Pretend we got empty metadata."; - const FolderMetadata emptyMetadata(_propagator->account()); - auto json = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata()); - slotFolderEncryptedMetadataReceived(json, httpReturnCode); + return _encryptedFolderMetadataHandler ? _encryptedFolderMetadataHandler->folderToken() : QByteArray{}; } -void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode) +void PropagateUploadEncrypted::slotFetchMetadataJobFinished(int statusCode, const QString &message) { - qCDebug(lcPropagateUploadEncrypted) << "Metadata Received, Preparing it for the new file." << json.toVariant(); - - // Encrypt File! - _metadata.reset(new FolderMetadata(_propagator->account(), - _item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact), statusCode)); - - if (!_metadata->isMetadataSetup()) { - if (_isFolderLocked) { - connect(this, &PropagateUploadEncrypted::folderUnlocked, this, &PropagateUploadEncrypted::error); - unlockFolder(); - } else { - emit error(); - } - return; - } - - QFileInfo info(_propagator->fullLocalPath(_item->_file)); - const QString fileName = info.fileName(); - - // Find existing metadata for this file - bool found = false; - EncryptedFile encryptedFile; - const QVector files = _metadata->files(); - - for(const EncryptedFile &file : files) { - if (file.originalFilename == fileName) { - encryptedFile = file; - found = true; + qCDebug(lcPropagateUploadEncrypted) << "Metadata Received, Preparing it for the new file." << message; + + if (statusCode != 200) { + emit error(); + return; } - } + if (!_encryptedFolderMetadataHandler->folderMetadata() || !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) { + qCDebug(lcPropagateUploadEncrypted()) << "There was an error encrypting the file, aborting upload. Invalid metadata."; + emit error(); + return; + } + + const auto metadata = _encryptedFolderMetadataHandler->folderMetadata(); - // New encrypted file so set it all up! - if (!found) { - encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); - encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); - encryptedFile.originalFilename = fileName; + QFileInfo info(_propagator->fullLocalPath(_item->_file)); + const QString fileName = info.fileName(); - QMimeDatabase mdb; - encryptedFile.mimetype = mdb.mimeTypeForFile(info).name().toLocal8Bit(); + // Find existing metadata for this file + bool found = false; + FolderMetadata::EncryptedFile encryptedFile; + const QVector files = metadata->files(); - // Other clients expect "httpd/unix-directory" instead of "inode/directory" - // Doesn't matter much for us since we don't do much about that mimetype anyway - if (encryptedFile.mimetype == QByteArrayLiteral("inode/directory")) { - encryptedFile.mimetype = QByteArrayLiteral("httpd/unix-directory"); - } - } - - encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + for (const FolderMetadata::EncryptedFile &file : files) { + if (file.originalFilename == fileName) { + encryptedFile = file; + found = true; + } + } - _item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename; - _item->_e2eEncryptionStatus = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; + // New encrypted file so set it all up! + if (!found) { + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fileName; - qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file."; + QMimeDatabase mdb; + encryptedFile.mimetype = mdb.mimeTypeForFile(info).name().toLocal8Bit(); - if (info.isDir()) { - _completeFileName = encryptedFile.encryptedFilename; - } else { - QFile input(info.absoluteFilePath()); - QFile output(QDir::tempPath() + QDir::separator() + encryptedFile.encryptedFilename); + // Other clients expect "httpd/unix-directory" instead of "inode/directory" + // Doesn't matter much for us since we don't do much about that mimetype anyway + if (encryptedFile.mimetype == QByteArrayLiteral("inode/directory")) { + encryptedFile.mimetype = QByteArrayLiteral("httpd/unix-directory"); + } + } - QByteArray tag; - bool encryptionResult = EncryptionHelper::fileEncryption( - encryptedFile.encryptionKey, - encryptedFile.initializationVector, - &input, &output, tag); + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); - if (!encryptionResult) { - qCDebug(lcPropagateUploadEncrypted()) << "There was an error encrypting the file, aborting upload."; - connect(this, &PropagateUploadEncrypted::folderUnlocked, this, &PropagateUploadEncrypted::error); - unlockFolder(); - return; - } - - encryptedFile.authenticationTag = tag; - _completeFileName = output.fileName(); - } - - qCDebug(lcPropagateUploadEncrypted) << "Creating the metadata for the encrypted file."; - - _metadata->addEncryptedFile(encryptedFile); - _encryptedFile = encryptedFile; - - qCDebug(lcPropagateUploadEncrypted) << "Metadata created, sending to the server."; - - if (statusCode == 404) { - auto job = new StoreMetaDataApiJob(_propagator->account(), - _folderId, - _metadata->encryptedMetadata()); - connect(job, &StoreMetaDataApiJob::success, this, &PropagateUploadEncrypted::slotUpdateMetadataSuccess); - connect(job, &StoreMetaDataApiJob::error, this, &PropagateUploadEncrypted::slotUpdateMetadataError); - job->start(); - } else { - auto job = new UpdateMetadataApiJob(_propagator->account(), - _folderId, - _metadata->encryptedMetadata(), - _folderToken); - - connect(job, &UpdateMetadataApiJob::success, this, &PropagateUploadEncrypted::slotUpdateMetadataSuccess); - connect(job, &UpdateMetadataApiJob::error, this, &PropagateUploadEncrypted::slotUpdateMetadataError); - job->start(); - } -} + _item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename; + _item->_e2eEncryptionStatusRemote = metadata->existingMetadataEncryptionStatus(); + _item->_e2eEncryptionServerCapability = + EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_propagator->account()->capabilities().clientSideEncryptionVersion()); -void PropagateUploadEncrypted::slotUpdateMetadataSuccess(const QByteArray& fileId) -{ - Q_UNUSED(fileId); - qCDebug(lcPropagateUploadEncrypted) << "Uploading of the metadata success, Encrypting the file"; - QFileInfo outputInfo(_completeFileName); + qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file."; - qCDebug(lcPropagateUploadEncrypted) << "Encrypted Info:" << outputInfo.path() << outputInfo.fileName() << outputInfo.size(); - qCDebug(lcPropagateUploadEncrypted) << "Finalizing the upload part, now the actual uploader will take over"; - emit finalized(outputInfo.path() + QLatin1Char('/') + outputInfo.fileName(), - _remoteParentPath + QLatin1Char('/') + outputInfo.fileName(), - outputInfo.size()); -} + if (info.isDir()) { + _completeFileName = encryptedFile.encryptedFilename; + } else { + QFile input(info.absoluteFilePath()); + QFile output(QDir::tempPath() + QDir::separator() + encryptedFile.encryptedFilename); -void PropagateUploadEncrypted::slotUpdateMetadataError(const QByteArray& fileId, int httpErrorResponse) -{ - qCDebug(lcPropagateUploadEncrypted) << "Update metadata error for folder" << fileId << "with error" << httpErrorResponse; - qCDebug(lcPropagateUploadEncrypted()) << "Unlocking the folder."; - connect(this, &PropagateUploadEncrypted::folderUnlocked, this, &PropagateUploadEncrypted::error); - unlockFolder(); -} + QByteArray tag; + bool encryptionResult = EncryptionHelper::fileEncryption(encryptedFile.encryptionKey, encryptedFile.initializationVector, &input, &output, tag); -void PropagateUploadEncrypted::slotFolderLockedError(const QByteArray& fileId, int httpErrorCode) -{ - Q_UNUSED(httpErrorCode); - /* try to call the lock from 5 to 5 seconds - * and fail if it's more than 5 minutes. */ - QTimer::singleShot(5000, this, [this, fileId]{ - if (!_currentLockingInProgress) { - qCDebug(lcPropagateUploadEncrypted) << "Error locking the folder while no other update is locking it up."; - qCDebug(lcPropagateUploadEncrypted) << "Perhaps another client locked it."; - qCDebug(lcPropagateUploadEncrypted) << "Abort"; - return; + if (!encryptionResult) { + qCDebug(lcPropagateUploadEncrypted()) << "There was an error encrypting the file, aborting upload."; + emit error(); + return; } - // Perhaps I should remove the elapsed timer if the lock is from this client? - if (_folderLockFirstTry.elapsed() > /* five minutes */ 1000 * 60 * 5 ) { - qCDebug(lcPropagateUploadEncrypted) << "One minute passed, ignoring more attempts to lock the folder."; - return; - } - slotTryLock(fileId); - }); + encryptedFile.authenticationTag = tag; + _completeFileName = output.fileName(); + } - qCDebug(lcPropagateUploadEncrypted) << "Folder" << fileId << "Coundn't be locked."; -} + qCDebug(lcPropagateUploadEncrypted) << "Creating the metadata for the encrypted file."; -void PropagateUploadEncrypted::slotFolderEncryptedIdError(QNetworkReply *r) -{ - Q_UNUSED(r); - qCDebug(lcPropagateUploadEncrypted) << "Error retrieving the Id of the encrypted folder."; + metadata->addEncryptedFile(encryptedFile); + + qCDebug(lcPropagateUploadEncrypted) << "Metadata created, sending to the server."; + + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::uploadFinished, this, &PropagateUploadEncrypted::slotUploadMetadataFinished); + _encryptedFolderMetadataHandler->uploadMetadata(EncryptedFolderMetadataHandler::UploadMode::KeepLock); } -void PropagateUploadEncrypted::unlockFolder() +void PropagateUploadEncrypted::slotUploadMetadataFinished(int statusCode, const QString &message) { - ASSERT(!_isUnlockRunning); - - if (_isUnlockRunning) { - qWarning() << "Double-call to unlockFolder."; + if (statusCode != 200) { + qCDebug(lcPropagateUploadEncrypted) << "Update metadata error for folder" << _encryptedFolderMetadataHandler->folderId() << "with error" << message; + qCDebug(lcPropagateUploadEncrypted()) << "Unlocking the folder."; + emit error(); return; } - _isUnlockRunning = true; - - qDebug() << "Calling Unlock"; - auto *unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), _folderId, _folderToken, _propagator->_journal, this); - - connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) { - qDebug() << "Successfully Unlocked"; - _folderToken = ""; - _folderId = ""; - _isFolderLocked = false; - - emit folderUnlocked(folderId, 200); - _isUnlockRunning = false; - }); - connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) { - qDebug() << "Unlock Error"; + qCDebug(lcPropagateUploadEncrypted) << "Uploading of the metadata success, Encrypting the file"; + QFileInfo outputInfo(_completeFileName); - emit folderUnlocked(folderId, httpStatus); - _isUnlockRunning = false; - }); - unlockJob->start(); + qCDebug(lcPropagateUploadEncrypted) << "Encrypted Info:" << outputInfo.path() << outputInfo.fileName() << outputInfo.size(); + qCDebug(lcPropagateUploadEncrypted) << "Finalizing the upload part, now the actuall uploader will take over"; + emit finalized(outputInfo.path() + QLatin1Char('/') + outputInfo.fileName(), + _remoteParentPath + QLatin1Char('/') + outputInfo.fileName(), + outputInfo.size()); } -} // namespace OCC +} // namespace OCC \ No newline at end of file diff --git a/src/libsync/propagateuploadencrypted.h b/src/libsync/propagateuploadencrypted.h index ddcce6e605384..effb8f6455667 100644 --- a/src/libsync/propagateuploadencrypted.h +++ b/src/libsync/propagateuploadencrypted.h @@ -15,7 +15,6 @@ #include "clientsideencryption.h" namespace OCC { -class FolderMetadata; /* This class is used if the server supports end to end encryption. * It will fire for *any* folder, encrypted or not, because when the @@ -29,6 +28,8 @@ class FolderMetadata; * */ +class EncryptedFolderMetadataHandler; + class PropagateUploadEncrypted : public QObject { Q_OBJECT @@ -40,20 +41,13 @@ class PropagateUploadEncrypted : public QObject void unlockFolder(); - [[nodiscard]] bool isUnlockRunning() const { return _isUnlockRunning; } - [[nodiscard]] bool isFolderLocked() const { return _isFolderLocked; } - [[nodiscard]] const QByteArray folderToken() const { return _folderToken; } + [[nodiscard]] bool isUnlockRunning() const; + [[nodiscard]] bool isFolderLocked() const; + [[nodiscard]] const QByteArray folderToken() const; private slots: - void slotFolderEncryptedIdReceived(const QStringList &list); - void slotFolderEncryptedIdError(QNetworkReply *r); - 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 slotFetchMetadataJobFinished(int statusCode, const QString &message); + void slotUploadMetadataFinished(int statusCode, const QString &message); signals: // Emitted after the file is encrypted and everything is setup. @@ -66,9 +60,6 @@ private slots: QString _remoteParentPath; SyncFileItemPtr _item; - QByteArray _folderToken; - QByteArray _folderId; - QElapsedTimer _folderLockFirstTry; bool _currentLockingInProgress = false; @@ -77,9 +68,10 @@ private slots: QByteArray _generatedKey; QByteArray _generatedIv; - QScopedPointer _metadata; - EncryptedFile _encryptedFile; QString _completeFileName; + QString _remoteParentAbsolutePath; + + QScopedPointer _encryptedFolderMetadataHandler; }; diff --git a/src/libsync/rootencryptedfolderinfo.cpp b/src/libsync/rootencryptedfolderinfo.cpp new file mode 100644 index 0000000000000..f447067a50624 --- /dev/null +++ b/src/libsync/rootencryptedfolderinfo.cpp @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 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 "rootencryptedfolderinfo.h" + +namespace OCC +{ +RootEncryptedFolderInfo::RootEncryptedFolderInfo() +{ + *this = RootEncryptedFolderInfo::makeDefault(); +} + +RootEncryptedFolderInfo::RootEncryptedFolderInfo(const QString &remotePath, + const QByteArray &encryptionKey, + const QByteArray &decryptionKey, + const QSet &checksums, + const quint64 counter) + : path(remotePath) + , keyForEncryption(encryptionKey) + , keyForDecryption(decryptionKey) + , keyChecksums(checksums) + , counter(counter) +{ +} + +RootEncryptedFolderInfo RootEncryptedFolderInfo::makeDefault() +{ + return RootEncryptedFolderInfo{QStringLiteral("/")}; +} + +QString RootEncryptedFolderInfo::createRootPath(const QString ¤tPath, const QString &topLevelPath) +{ + const auto currentPathNoLeadingSlash = currentPath.startsWith(QLatin1Char('/')) ? currentPath.mid(1) : currentPath; + const auto topLevelPathNoLeadingSlash = topLevelPath.startsWith(QLatin1Char('/')) ? topLevelPath.mid(1) : topLevelPath; + + return currentPathNoLeadingSlash == topLevelPathNoLeadingSlash ? QStringLiteral("/") : topLevelPath; +} + +bool RootEncryptedFolderInfo::keysSet() const +{ + return !keyForEncryption.isEmpty() && !keyForDecryption.isEmpty() && !keyChecksums.isEmpty(); +} +} diff --git a/src/libsync/rootencryptedfolderinfo.h b/src/libsync/rootencryptedfolderinfo.h new file mode 100644 index 0000000000000..15191f45b0fdc --- /dev/null +++ b/src/libsync/rootencryptedfolderinfo.h @@ -0,0 +1,44 @@ +#pragma once +/* + * Copyright (C) 2023 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 +#include +#include +#include +#include + +namespace OCC +{ +// required parts from root E2EE folder's metadata for version 2.0+ +struct OWNCLOUDSYNC_EXPORT RootEncryptedFolderInfo { + RootEncryptedFolderInfo(); + explicit RootEncryptedFolderInfo(const QString &remotePath, + const QByteArray &encryptionKey = {}, + const QByteArray &decryptionKey = {}, + const QSet &checksums = {}, + const quint64 counter = 0); + + static RootEncryptedFolderInfo makeDefault(); + + static QString createRootPath(const QString ¤tPath, const QString &topLevelPath); + + QString path; + QByteArray keyForEncryption; // it can be different from keyForDecryption when new metadatKey is generated in root E2EE foler + QByteArray keyForDecryption; // always storing previous metadataKey to be able to decrypt nested E2EE folders' previous metadata + QSet keyChecksums; + quint64 counter = 0; + [[nodiscard]] bool keysSet() const; +}; + +} // namespace OCC diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 2c85fe5136f07..72c043fbbafc3 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -510,7 +510,9 @@ void SyncEngine::startSync() const auto folderId = e2EeLockedFolder.first; qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId; const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, e2EeLockedFolder.second); + // TODO: We need to rollback changes done to metadata in case we have an active lock, this needs to be implemented on the server first const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, folderToken, _journal, this); + unlockJob->setShouldRollbackMetadataChanges(true); unlockJob->start(); } } @@ -519,6 +521,10 @@ void SyncEngine::startSync() if (s_anySyncRunning || _syncRunning) { return; } + const auto currentEncryptionStatus = EncryptionStatusEnums::toDbEncryptionStatus(EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion())); + [[maybe_unused]] const auto result = _journal->listAllE2eeFoldersWithEncryptionStatusLessThan(static_cast(currentEncryptionStatus), [this](const SyncJournalFileRecord &record) { + _journal->schedulePathForRemoteDiscovery(record.path()); + }); s_anySyncRunning = true; _syncRunning = true; diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 0076bcad0c39b..4a169d9cbefbb 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -43,6 +43,9 @@ ItemEncryptionStatus fromDbEncryptionStatus(JournalDbEncryptionStatus encryption case JournalDbEncryptionStatus::EncryptedMigratedV1_2Invalid: result = ItemEncryptionStatus::Encrypted; break; + case JournalDbEncryptionStatus::EncryptedMigratedV2_0: + result = ItemEncryptionStatus::EncryptedMigratedV2_0; + break; case JournalDbEncryptionStatus::NotEncrypted: result = ItemEncryptionStatus::NotEncrypted; break; @@ -63,6 +66,9 @@ JournalDbEncryptionStatus toDbEncryptionStatus(ItemEncryptionStatus encryptionSt case ItemEncryptionStatus::EncryptedMigratedV1_2: result = JournalDbEncryptionStatus::EncryptedMigratedV1_2; break; + case ItemEncryptionStatus::EncryptedMigratedV2_0: + result = JournalDbEncryptionStatus::EncryptedMigratedV2_0; + break; case ItemEncryptionStatus::NotEncrypted: result = JournalDbEncryptionStatus::NotEncrypted; break; @@ -71,6 +77,19 @@ JournalDbEncryptionStatus toDbEncryptionStatus(ItemEncryptionStatus encryptionSt return result; } +ItemEncryptionStatus fromEndToEndEncryptionApiVersion(const double version) +{ + if (version >= 2.0) { + return ItemEncryptionStatus::EncryptedMigratedV2_0; + } else if (version >= 1.2) { + return ItemEncryptionStatus::EncryptedMigratedV1_2; + } else if (version >= 1.0) { + return ItemEncryptionStatus::Encrypted; + } else { + return ItemEncryptionStatus::NotEncrypted; + } +} + } SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QString &localFileName) const @@ -136,6 +155,7 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_checksumHeader = rec._checksumHeader; item->_encryptedFileName = rec.e2eMangledName(); item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(rec._e2eEncryptionStatus); + item->_e2eEncryptionServerCapability = item->_e2eEncryptionStatus; item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem; item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName; item->_lockOwnerId = rec._lockstate._lockOwnerId; @@ -172,7 +192,10 @@ SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap item->_isShared = item->_remotePerm.hasPermission(RemotePermissions::IsShared); item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); - item->_e2eEncryptionStatus = (properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1") ? SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted); + item->_e2eEncryptionStatus = (properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1") ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted); + if (item->isEncrypted()) { + item->_e2eEncryptionServerCapability = item->_e2eEncryptionStatus; + } item->_locked = properties.value(QStringLiteral("lock")) == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem; item->_lockOwnerDisplayName = properties.value(QStringLiteral("lock-owner-displayname")); diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index c0d880be6097f..89e68ca99b209 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -284,6 +284,8 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem bool _isRestoration BITFIELD(1); // The original operation was forbidden, and this is a restoration bool _isSelectiveSync BITFIELD(1); // The file is removed or ignored because it is in the selective sync list EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted; // The file is E2EE or the content of the directory should be E2EE + EncryptionStatus _e2eEncryptionServerCapability = EncryptionStatus::NotEncrypted; + EncryptionStatus _e2eEncryptionStatusRemote = EncryptionStatus::NotEncrypted; quint16 _httpErrorCode = 0; RemotePermissions _remotePerm; QString _errorString; // Contains a string only in case of error diff --git a/src/libsync/updatee2eefoldermetadatajob.cpp b/src/libsync/updatee2eefoldermetadatajob.cpp new file mode 100644 index 0000000000000..7bca9c4381b70 --- /dev/null +++ b/src/libsync/updatee2eefoldermetadatajob.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2023 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 "updatee2eefoldermetadatajob.h" + +#include "account.h" +#include "clientsideencryption.h" +#include "foldermetadata.h" + +#include +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcUpdateFileDropMetadataJob, "nextcloud.sync.propagator.updatee2eefoldermetadatajob", QtInfoMsg) + +} + +namespace OCC { + +UpdateE2eeFolderMetadataJob::UpdateE2eeFolderMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &item, const QString &encryptedRemotePath) + : PropagatorJob(propagator), + _item(item), + _encryptedRemotePath(encryptedRemotePath) +{ +} + +void UpdateE2eeFolderMetadataJob::start() +{ + Q_ASSERT(_item); + qCDebug(lcUpdateFileDropMetadataJob) << "Folder is encrypted, let's fetch metadata."; + + SyncJournalFileRecord rec; + if (!propagator()->_journal->getRootE2eFolderRecord(_encryptedRemotePath, &rec) || !rec.isValid()) { + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + return; + } + _encryptedFolderMetadataHandler.reset( + new EncryptedFolderMetadataHandler(propagator()->account(), _encryptedRemotePath, propagator()->_journal, rec.path())); + + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::fetchFinished, + this, &UpdateE2eeFolderMetadataJob::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata); +} + +bool UpdateE2eeFolderMetadataJob::scheduleSelfOrChild() +{ + if (_state == Finished) { + return false; + } + + if (_state == NotYetStarted) { + _state = Running; + start(); + } + + return true; +} + +PropagatorJob::JobParallelism UpdateE2eeFolderMetadataJob::parallelism() const +{ + return PropagatorJob::JobParallelism::WaitForFinished; +} + +void UpdateE2eeFolderMetadataJob::slotFetchMetadataJobFinished(int httpReturnCode, const QString &message) +{ + if (httpReturnCode != 200) { + qCDebug(lcUpdateFileDropMetadataJob()) << "Error Getting the encrypted metadata."; + _item->_status = SyncFileItem::FatalError; + _item->_errorString = message; + finished(SyncFileItem::FatalError); + return; + } + + SyncJournalFileRecord rec; + if (!propagator()->_journal->getRootE2eFolderRecord(_encryptedRemotePath, &rec) || !rec.isValid()) { + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + return; + } + + const auto folderMetadata = _encryptedFolderMetadataHandler->folderMetadata(); + if (!folderMetadata || !folderMetadata->isValid() || (!folderMetadata->moveFromFileDropToFiles() && !folderMetadata->encryptedMetadataNeedUpdate())) { + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + return; + } + + emit fileDropMetadataParsedAndAdjusted(folderMetadata.data()); + _encryptedFolderMetadataHandler->uploadMetadata(); + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::uploadFinished, + this, &UpdateE2eeFolderMetadataJob::slotUpdateMetadataFinished); +} + +void UpdateE2eeFolderMetadataJob::slotUpdateMetadataFinished(int httpReturnCode, const QString &message) +{ + const auto itemStatus = httpReturnCode != 200 ? SyncFileItem::FatalError : SyncFileItem::Success; + if (httpReturnCode != 200) { + _item->_errorString = message; + qCDebug(lcUpdateFileDropMetadataJob) << "Update metadata error for folder" << _encryptedFolderMetadataHandler->folderId() << "with error" << httpReturnCode << message; + } else { + qCDebug(lcUpdateFileDropMetadataJob) << "Uploading of the metadata success, Encrypting the file"; + } + propagator()->_journal->schedulePathForRemoteDiscovery(_item->_file); + propagator()->_anotherSyncNeeded = true; + _item->_status = itemStatus; + finished(itemStatus); +} + +void UpdateE2eeFolderMetadataJob::unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result) +{ + Q_ASSERT(!_encryptedFolderMetadataHandler->isUnlockRunning()); + Q_ASSERT(_item); + + if (_encryptedFolderMetadataHandler->isUnlockRunning()) { + qCWarning(lcUpdateFileDropMetadataJob) << "Double-call to unlockFolder."; + return; + } + + if (result != EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success) { + _item->_errorString = tr("Failed to update folder metadata."); + } + + const auto isSuccess = result == EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success; + + const auto itemStatus = isSuccess ? SyncFileItem::Success : SyncFileItem::FatalError; + + if (!_encryptedFolderMetadataHandler->isFolderLocked()) { + if (isSuccess && _encryptedFolderMetadataHandler->folderMetadata()) { + _item->_e2eEncryptionStatus = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus(); + if (_item->isEncrypted()) { + _item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion()); + } + } + finished(itemStatus); + return; + } + + qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock"; + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, [this](const QByteArray &folderId, int httpStatus) { + if (httpStatus != 200) { + qCWarning(lcUpdateFileDropMetadataJob) << "Unlock Error" << folderId << httpStatus; + propagator()->account()->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + _item->_errorString = tr("Failed to unlock encrypted folder."); + finished(SyncFileItem::FatalError); + return; + } + + qCDebug(lcUpdateFileDropMetadataJob) << "Successfully Unlocked"; + + if (!_encryptedFolderMetadataHandler->folderMetadata() + || !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) { + qCWarning(lcUpdateFileDropMetadataJob) << "Failed to finalize item. Invalid metadata."; + _item->_errorString = tr("Failed to finalize item."); + finished(SyncFileItem::FatalError); + return; + } + + _item->_e2eEncryptionStatus = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus(); + _item->_e2eEncryptionStatusRemote = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus(); + + finished(SyncFileItem::Success); + }); + _encryptedFolderMetadataHandler->unlockFolder(result); +} + +} diff --git a/src/libsync/updatee2eefoldermetadatajob.h b/src/libsync/updatee2eefoldermetadatajob.h new file mode 100644 index 0000000000000..1190a0eed6e5e --- /dev/null +++ b/src/libsync/updatee2eefoldermetadatajob.h @@ -0,0 +1,58 @@ +/* + * 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 "encryptedfoldermetadatahandler.h" //NOTE: Forward declarion is not gonna work because of OWNCLOUDSYNC_EXPORT for UpdateE2eeFolderMetadataJob +#include "owncloudpropagator.h" +#include "syncfileitem.h" + +#include + +class QNetworkReply; + +namespace OCC { + +class FolderMetadata; + +class EncryptedFolderMetadataHandler; + +class OWNCLOUDSYNC_EXPORT UpdateE2eeFolderMetadataJob : public PropagatorJob +{ + Q_OBJECT + +public: + explicit UpdateE2eeFolderMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &item, const QString &encryptedRemotePath); + + bool scheduleSelfOrChild() override; + + [[nodiscard]] JobParallelism parallelism() const override; + +private slots: + void start(); + void slotFetchMetadataJobFinished(int httpReturnCode, const QString &message); + void slotUpdateMetadataFinished(int httpReturnCode, const QString &message); + void unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result); + +signals: + void fileDropMetadataParsedAndAdjusted(const OCC::FolderMetadata *const metadata); + +private: + SyncFileItemPtr _item; + QString _encryptedRemotePath; + + QScopedPointer _encryptedFolderMetadataHandler; +}; + +} diff --git a/src/libsync/updatee2eefolderusersmetadatajob.cpp b/src/libsync/updatee2eefolderusersmetadatajob.cpp new file mode 100644 index 0000000000000..e6d951dd968bc --- /dev/null +++ b/src/libsync/updatee2eefolderusersmetadatajob.cpp @@ -0,0 +1,374 @@ +/* + * Copyright (C) 2023 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 "account.h" +#include "updatee2eefolderusersmetadatajob.h" +#include "foldermetadata.h" +#include "common/syncjournalfilerecord.h" +#include "common/syncjournaldb.h" + +#include + +namespace OCC +{ +Q_LOGGING_CATEGORY(lcUpdateE2eeFolderUsersMetadataJob, "nextcloud.gui.updatee2eefolderusersmetadatajob", QtInfoMsg) + +UpdateE2eeFolderUsersMetadataJob::UpdateE2eeFolderUsersMetadataJob(const AccountPtr &account, + SyncJournalDb *journalDb, + const QString &syncFolderRemotePath, + const Operation operation, + const QString &path, + const QString &folderUserId, + const QSslCertificate &certificate, + QObject *parent) + : QObject(parent) + , _account(account) + , _journalDb(journalDb) + , _syncFolderRemotePath(syncFolderRemotePath) + , _operation(operation) + , _path(path) + , _folderUserId(folderUserId) + , _folderUserCertificate(certificate) +{ + const auto pathSanitized = _path.startsWith(QLatin1Char('/')) ? _path.mid(1) : _path; + const auto folderPath = _syncFolderRemotePath + pathSanitized; + + SyncJournalFileRecord rec; + if (!_journalDb->getRootE2eFolderRecord(_path, &rec) || !rec.isValid()) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Could not get root E2ee folder recort for path" << _path; + return; + } + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(_account, folderPath, _journalDb, rec.path())); +} + +void UpdateE2eeFolderUsersMetadataJob::start(const bool keepLock) +{ + qCWarning(lcUpdateE2eeFolderUsersMetadataJob) << "[DEBUG_LEAVE_SHARE]: UpdateE2eeFolderUsersMetadataJob::start"; + + if (!_encryptedFolderMetadataHandler) { + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + if (keepLock) { + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::deleteLater); + } else { + connect(this, &UpdateE2eeFolderUsersMetadataJob::slotFolderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::deleteLater); + } + _keepLock = keepLock; + if (_operation != Operation::Add && _operation != Operation::Remove && _operation != Operation::ReEncrypt) { + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + if (_operation == Operation::Add) { + connect(this, &UpdateE2eeFolderUsersMetadataJob::certificateReady, this, &UpdateE2eeFolderUsersMetadataJob::slotStartE2eeMetadataJobs); + if (!_folderUserCertificate.isNull()) { + emit certificateReady(); + return; + } + connect(_account->e2e(), &ClientSideEncryption::certificateFetchedFromKeychain, + this, &UpdateE2eeFolderUsersMetadataJob::slotCertificateFetchedFromKeychain); + _account->e2e()->fetchCertificateFromKeyChain(_account, _folderUserId); + return; + } + slotStartE2eeMetadataJobs(); +} + +void UpdateE2eeFolderUsersMetadataJob::slotStartE2eeMetadataJobs() +{ + if (_operation == Operation::Add && _folderUserCertificate.isNull()) { + emit finished(404, tr("Could not fetch publicKey for user %1").arg(_folderUserId)); + return; + } + + const auto pathSanitized = _path.startsWith(QLatin1Char('/')) ? _path.mid(1) : _path; + const auto folderPath = _syncFolderRemotePath + pathSanitized; + SyncJournalFileRecord rec; + if (!_journalDb->getRootE2eFolderRecord(_path, &rec) || !rec.isValid()) { + emit finished(404, tr("Could not find root encrypted folder for folder %1").arg(_path)); + return; + } + + const auto rootEncFolderInfo = RootEncryptedFolderInfo(RootEncryptedFolderInfo::createRootPath(folderPath, rec.path()), _metadataKeyForEncryption, _metadataKeyForDecryption, _keyChecksums); + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::fetchFinished, + this, &UpdateE2eeFolderUsersMetadataJob::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(rootEncFolderInfo, EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata); +} + +void UpdateE2eeFolderUsersMetadataJob::slotFetchMetadataJobFinished(int statusCode, const QString &message) +{ + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Metadata Received, Preparing it for the new file." << message; + + if (statusCode != 200) { + qCritical(lcUpdateE2eeFolderUsersMetadataJob) << "fetch metadata finished with error" << statusCode << message; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + if (!_encryptedFolderMetadataHandler->folderMetadata() || !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) { + emit finished(403, tr("Could not add or remove a folder user %1, for folder %2").arg(_folderUserId).arg(_path)); + return; + } + startUpdate(); +} + +void UpdateE2eeFolderUsersMetadataJob::startUpdate() +{ + if (_operation == Operation::Invalid) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Invalid operation"; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + if (_operation == Operation::Add || _operation == Operation::Remove) { + if (!_encryptedFolderMetadataHandler->folderMetadata()) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Metadata is null"; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + const auto result = _operation == Operation::Add + ? _encryptedFolderMetadataHandler->folderMetadata()->addUser(_folderUserId, _folderUserCertificate) + : _encryptedFolderMetadataHandler->folderMetadata()->removeUser(_folderUserId); + + if (!result) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Could not perform operation" << _operation << "on metadata"; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + return; + } + + } + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::uploadFinished, + this, &UpdateE2eeFolderUsersMetadataJob::slotUpdateMetadataFinished); + _encryptedFolderMetadataHandler->setFolderToken(_folderToken); + _encryptedFolderMetadataHandler->uploadMetadata(EncryptedFolderMetadataHandler::UploadMode::KeepLock); +} + +void UpdateE2eeFolderUsersMetadataJob::slotUpdateMetadataFinished(int code, const QString &message) +{ + if (code != 200) { + qCWarning(lcUpdateE2eeFolderUsersMetadataJob) << "Update metadata error for folder" << _encryptedFolderMetadataHandler->folderId() << "with error" + << code << message; + + if (_operation == Operation::Add || _operation == Operation::Remove) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob()) << "Unlocking the folder."; + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + } else { + emit finished(code, tr("Error updating metadata for a folder %1").arg(_path) + QStringLiteral(":%1").arg(message)); + } + return; + } + + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Uploading of the metadata success."; + if (_operation == Operation::Add || _operation == Operation::Remove) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Trying to schedule more jobs."; + scheduleSubJobs(); + if (_subJobs.isEmpty()) { + if (_keepLock) { + emit finished(200); + } else { + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success); + } + } else { + _subJobs.values().last()->start(); + } + } else { + emit finished(200); + } +} + +void UpdateE2eeFolderUsersMetadataJob::scheduleSubJobs() +{ + const auto isMetadataValid = _encryptedFolderMetadataHandler->folderMetadata() && _encryptedFolderMetadataHandler->folderMetadata()->isValid(); + if (!isMetadataValid) { + if (_operation == Operation::Add || _operation == Operation::Remove) { + qCWarning(lcUpdateE2eeFolderUsersMetadataJob()) << "Metadata is invalid. Unlocking the folder."; + unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); + } else { + qCWarning(lcUpdateE2eeFolderUsersMetadataJob()) << "Metadata is invalid."; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path)); + } + return; + } + + const auto pathInDb = _path.mid(_syncFolderRemotePath.size()); + [[maybe_unused]] const auto result = _journalDb->getFilesBelowPath(pathInDb.toUtf8(), [this](const SyncJournalFileRecord &record) { + if (record.isDirectory()) { + const auto folderMetadata = _encryptedFolderMetadataHandler->folderMetadata(); + const auto subJob = new UpdateE2eeFolderUsersMetadataJob(_account, _journalDb, _syncFolderRemotePath, UpdateE2eeFolderUsersMetadataJob::ReEncrypt, QString::fromUtf8(record._e2eMangledName)); + subJob->setMetadataKeyForEncryption(folderMetadata->metadataKeyForEncryption()); + subJob->setMetadataKeyForDecryption(folderMetadata->metadataKeyForDecryption()); + subJob->setKeyChecksums(folderMetadata->keyChecksums()); + subJob->setParent(this); + subJob->setFolderToken(_encryptedFolderMetadataHandler->folderToken()); + _subJobs.insert(subJob); + connect(subJob, &UpdateE2eeFolderUsersMetadataJob::finished, this, &UpdateE2eeFolderUsersMetadataJob::slotSubJobFinished); + } + }); +} + +void UpdateE2eeFolderUsersMetadataJob::unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result) +{ + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Calling Unlock"; + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::slotFolderUnlocked); + _encryptedFolderMetadataHandler->unlockFolder(result); +} + +void UpdateE2eeFolderUsersMetadataJob::slotFolderUnlocked(const QByteArray &folderId, int httpStatus) +{ + emit folderUnlocked(); + if (_keepLock) { + return; + } + if (httpStatus != 200) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "Failed to unlock a folder" << folderId << httpStatus; + } + const auto message = httpStatus != 200 ? tr("Failed to unlock a folder.") : QString{}; + emit finished(httpStatus, message); +} + +void UpdateE2eeFolderUsersMetadataJob::subJobsFinished(bool success) +{ + unlockFolder(success + ? EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success + : EncryptedFolderMetadataHandler::UnlockFolderWithResult::Failure); +} + +void UpdateE2eeFolderUsersMetadataJob::slotSubJobFinished(int code, const QString &message) +{ + if (code != 200) { + qCDebug(lcUpdateE2eeFolderUsersMetadataJob) << "sub job finished with error" << message; + subJobsFinished(false); + return; + } + const auto job = qobject_cast(sender()); + Q_ASSERT(job); + if (!job) { + qCWarning(lcUpdateE2eeFolderUsersMetadataJob) << "slotSubJobFinished must be invoked by signal"; + emit finished(-1, tr("Error updating metadata for a folder %1").arg(_path) + QStringLiteral(":%1").arg(message)); + subJobsFinished(false); + return; + } + + { + QMutexLocker locker(&_subJobSyncItemsMutex); + const auto foundInHash = _subJobSyncItems.constFind(job->path()); + if (foundInHash != _subJobSyncItems.constEnd() && foundInHash.value()) { + foundInHash.value()->_e2eEncryptionStatus = job->encryptionStatus(); + foundInHash.value()->_e2eEncryptionStatusRemote = job->encryptionStatus(); + foundInHash.value()->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion()); + _subJobSyncItems.erase(foundInHash); + } + } + + _subJobs.remove(job); + job->deleteLater(); + + if (_subJobs.isEmpty()) { + subJobsFinished(true); + } else { + _subJobs.values().last()->start(); + } +} + +void UpdateE2eeFolderUsersMetadataJob::slotCertificateFetchedFromKeychain(const QSslCertificate &certificate) +{ + disconnect(_account->e2e(), + &ClientSideEncryption::certificateFetchedFromKeychain, + this, + &UpdateE2eeFolderUsersMetadataJob::slotCertificateFetchedFromKeychain); + if (certificate.isNull()) { + // get folder user's public key + _account->e2e()->getUsersPublicKeyFromServer(_account, {_folderUserId}); + connect(_account->e2e(), + &ClientSideEncryption::certificatesFetchedFromServer, + this, + &UpdateE2eeFolderUsersMetadataJob::slotCertificatesFetchedFromServer); + return; + } + _folderUserCertificate = certificate; + emit certificateReady(); +} + +void UpdateE2eeFolderUsersMetadataJob::slotCertificatesFetchedFromServer(const QHash &results) +{ + const auto certificate = results.isEmpty() ? QSslCertificate{} : results.value(_folderUserId); + _folderUserCertificate = certificate; + if (certificate.isNull()) { + emit certificateReady(); + return; + } + _account->e2e()->writeCertificate(_account, _folderUserId, certificate); + connect(_account->e2e(), &ClientSideEncryption::certificateWriteComplete, this, &UpdateE2eeFolderUsersMetadataJob::certificateReady); +} + +void UpdateE2eeFolderUsersMetadataJob::setUserData(const UserData &userData) +{ + _userData = userData; +} + +void UpdateE2eeFolderUsersMetadataJob::setFolderToken(const QByteArray &folderToken) +{ + _folderToken = folderToken; +} + +void UpdateE2eeFolderUsersMetadataJob::setMetadataKeyForEncryption(const QByteArray &metadataKey) +{ + _metadataKeyForEncryption = metadataKey; +} + +void UpdateE2eeFolderUsersMetadataJob::setMetadataKeyForDecryption(const QByteArray &metadataKey) +{ + _metadataKeyForDecryption = metadataKey; +} + +void UpdateE2eeFolderUsersMetadataJob::setKeyChecksums(const QSet &keyChecksums) +{ + _keyChecksums = keyChecksums; +} + +void UpdateE2eeFolderUsersMetadataJob::setSubJobSyncItems(const QHash &subJobSyncItems) +{ + _subJobSyncItems = subJobSyncItems; +} + +const QString &UpdateE2eeFolderUsersMetadataJob::path() const +{ + return _path; +} + +const UpdateE2eeFolderUsersMetadataJob::UserData &UpdateE2eeFolderUsersMetadataJob::userData() const +{ + return _userData; +} + +SyncFileItem::EncryptionStatus UpdateE2eeFolderUsersMetadataJob::encryptionStatus() const +{ + const auto folderMetadata = _encryptedFolderMetadataHandler->folderMetadata(); + const auto isMetadataValid = folderMetadata && folderMetadata->isValid(); + if (!isMetadataValid) { + qCWarning(lcUpdateE2eeFolderUsersMetadataJob) << "_encryptedFolderMetadataHandler->folderMetadata() is invalid"; + } + return !isMetadataValid + ? EncryptionStatusEnums::ItemEncryptionStatus::NotEncrypted + : folderMetadata->encryptedMetadataEncryptionStatus(); +} + +const QByteArray UpdateE2eeFolderUsersMetadataJob::folderToken() const +{ + return _encryptedFolderMetadataHandler->folderToken(); +} + +} diff --git a/src/libsync/updatee2eefolderusersmetadatajob.h b/src/libsync/updatee2eefolderusersmetadatajob.h new file mode 100644 index 0000000000000..830fa43f06f92 --- /dev/null +++ b/src/libsync/updatee2eefolderusersmetadatajob.h @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2023 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 "accountfwd.h" +#include "encryptedfoldermetadatahandler.h" //NOTE: Forward declarion is not gonna work because of OWNCLOUDSYNC_EXPORT for UpdateE2eeFolderUsersMetadataJob +#include "gui/sharemanager.h" +#include "syncfileitem.h" +#include "gui/sharee.h" + +#include +#include +#include +#include +#include + +class QSslCertificate; +namespace OCC +{ +class SyncJournalDb; +class OWNCLOUDSYNC_EXPORT UpdateE2eeFolderUsersMetadataJob : public QObject +{ + Q_OBJECT +public: + enum Operation { Invalid = -1, Add = 0, Remove, ReEncrypt }; + + struct UserData { + ShareePtr sharee; + Share::Permissions desiredPermissions; + QString password; + }; + + explicit UpdateE2eeFolderUsersMetadataJob(const AccountPtr &account, SyncJournalDb *journalDb,const QString &syncFolderRemotePath, const Operation operation, const QString &path = {}, const QString &folderUserId = {}, const QSslCertificate &certificate = QSslCertificate{}, QObject *parent = nullptr); + ~UpdateE2eeFolderUsersMetadataJob() override = default; + +public: + [[nodiscard]] const QString &path() const; + [[nodiscard]] const UserData &userData() const; + [[nodiscard]] SyncFileItem::EncryptionStatus encryptionStatus() const; + [[nodiscard]] const QByteArray folderToken() const; + + void unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result); + +public slots: + void start(const bool keepLock = false); + void setUserData(const UserData &userData); + + void setFolderToken(const QByteArray &folderToken); + void setMetadataKeyForEncryption(const QByteArray &metadataKey); + void setMetadataKeyForDecryption(const QByteArray &metadataKey); + void setKeyChecksums(const QSet &keyChecksums); + + void setSubJobSyncItems(const QHash &subJobSyncItems); + +private: + void scheduleSubJobs(); + void startUpdate(); + void subJobsFinished(bool success); + +private slots: + void slotStartE2eeMetadataJobs(); + void slotFetchMetadataJobFinished(int statusCode, const QString &message); + + void slotSubJobFinished(int code, const QString &message = {}); + + void slotFolderUnlocked(const QByteArray &folderId, int httpStatus); + + void slotUpdateMetadataFinished(int code, const QString &message = {}); + void slotCertificatesFetchedFromServer(const QHash &results); + void slotCertificateFetchedFromKeychain(const QSslCertificate &certificate); + +private: signals: + void certificateReady(); + void finished(int code, const QString &message = {}); + void folderUnlocked(); + +private: + AccountPtr _account; + QPointer _journalDb; + QString _syncFolderRemotePath; + Operation _operation = Invalid; + QString _path; + QString _folderUserId; + QSslCertificate _folderUserCertificate; + QByteArray _folderToken; + // needed when re-encrypting nested folders' metadata after top-level folder's metadata has changed + QByteArray _metadataKeyForEncryption; + QByteArray _metadataKeyForDecryption; + QSet _keyChecksums; + //------------------------------------------------------------------------------------------------- + QSet _subJobs; + UserData _userData; // share info, etc. + QHash _subJobSyncItems; //used when migrating to update corresponding SyncFileItem(s) for nested folders, such that records in db will get updated when propagate item job is finalized + QMutex _subJobSyncItemsMutex; + QScopedPointer _encryptedFolderMetadataHandler; + bool _keepLock = false; +}; + +} diff --git a/src/libsync/updatefiledropmetadata.cpp b/src/libsync/updatefiledropmetadata.cpp deleted file mode 100644 index 22fa0499d536f..0000000000000 --- a/src/libsync/updatefiledropmetadata.cpp +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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(), - FolderMetadata::RequiredMetadataVersion::Version1, - json.toJson(QJsonDocument::Compact), statusCode)); - if (!_metadata->moveFromFileDropToFiles() && !_metadata->encryptedMetadataNeedUpdate()) { - 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 actual 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 deleted file mode 100644 index 8608cf08388c3..0000000000000 --- a/src/libsync/updatefiledropmetadata.h +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 OCC::FolderMetadata *const metadata); - -private: - QString _path; - bool _currentLockingInProgress = false; - - bool _isUnlockRunning = false; - bool _isFolderLocked = false; - - QByteArray _folderToken; - QByteArray _folderId; - - QScopedPointer _metadata; -}; - -} diff --git a/src/libsync/updatemigratede2eemetadatajob.cpp b/src/libsync/updatemigratede2eemetadatajob.cpp new file mode 100644 index 0000000000000..bfb61081f4ccc --- /dev/null +++ b/src/libsync/updatemigratede2eemetadatajob.cpp @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2023 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 "updatemigratede2eemetadatajob.h" +#include "updatee2eefolderusersmetadatajob.h" + +#include "account.h" +#include "syncfileitem.h" + +#include + +namespace OCC { + +Q_LOGGING_CATEGORY(lcUpdateMigratedE2eeMetadataJob, "nextcloud.sync.propagator.updatemigratede2eemetadatajob", QtInfoMsg) + +} + +namespace OCC { + +UpdateMigratedE2eeMetadataJob::UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, + const SyncFileItemPtr &syncFileItem, + const QString &path, + const QString &folderRemotePath) + : PropagatorJob(propagator) + , _item(syncFileItem) + , _path(path) + , _folderRemotePath(folderRemotePath) +{ +} + +void UpdateMigratedE2eeMetadataJob::start() +{ + const auto updateMedatadaAndSubfoldersJob = new UpdateE2eeFolderUsersMetadataJob(propagator()->account(), + propagator()->_journal, + _folderRemotePath, + UpdateE2eeFolderUsersMetadataJob::Add, + _path, + propagator()->account()->davUser(), + propagator()->account()->e2e()->_certificate); + updateMedatadaAndSubfoldersJob->setParent(this); + updateMedatadaAndSubfoldersJob->setSubJobSyncItems(_subJobItems); + _subJobItems.clear(); + updateMedatadaAndSubfoldersJob->start(); + connect(updateMedatadaAndSubfoldersJob, &UpdateE2eeFolderUsersMetadataJob::finished, this, [this, updateMedatadaAndSubfoldersJob](const int code, const QString& message) { + if (code == 200) { + _item->_e2eEncryptionStatus = updateMedatadaAndSubfoldersJob->encryptionStatus(); + _item->_e2eEncryptionStatusRemote = updateMedatadaAndSubfoldersJob->encryptionStatus(); + emit finished(SyncFileItem::Status::Success); + } else { + _item->_errorString = message; + emit finished(SyncFileItem::Status::NormalError); + } + }); +} + +bool UpdateMigratedE2eeMetadataJob::scheduleSelfOrChild() +{ + if (_state == Finished) { + return false; + } + + if (_state == NotYetStarted) { + _state = Running; + start(); + } + + return true; +} + +PropagatorJob::JobParallelism UpdateMigratedE2eeMetadataJob::parallelism() const +{ + return PropagatorJob::JobParallelism::WaitForFinished; +} + +QString UpdateMigratedE2eeMetadataJob::path() const +{ + return _path; +} + +void UpdateMigratedE2eeMetadataJob::addSubJobItem(const QString &key, const SyncFileItemPtr &syncFileItem) +{ + _subJobItems.insert(key, syncFileItem); +} + +} diff --git a/src/libsync/updatemigratede2eemetadatajob.h b/src/libsync/updatemigratede2eemetadatajob.h new file mode 100644 index 0000000000000..dbc18aa77c013 --- /dev/null +++ b/src/libsync/updatemigratede2eemetadatajob.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2023 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" + +class QNetworkReply; + +namespace OCC { + +class FolderMetadata; + +class OWNCLOUDSYNC_EXPORT UpdateMigratedE2eeMetadataJob : public PropagatorJob +{ + Q_OBJECT + +public: + explicit UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &syncFileItem, const QString &path, const QString &folderRemotePath); + + [[nodiscard]] bool scheduleSelfOrChild() override; + + [[nodiscard]] JobParallelism parallelism() const override; + + [[nodiscard]] QString path() const; + + void addSubJobItem(const QString &key, const SyncFileItemPtr &syncFileItem); + +private slots: + void start(); + +private: + SyncFileItemPtr _item; + QHash _subJobItems; + QString _path; + QString _folderRemotePath; +}; + +} diff --git a/src/libsync/vfs/cfapi/hydrationjob.cpp b/src/libsync/vfs/cfapi/hydrationjob.cpp index a90b2a07975a3..bed4155e74990 100644 --- a/src/libsync/vfs/cfapi/hydrationjob.cpp +++ b/src/libsync/vfs/cfapi/hydrationjob.cpp @@ -18,6 +18,8 @@ #include "propagatedownload.h" #include "vfs/cfapi/vfs_cfapi.h" #include +#include "encryptedfoldermetadatahandler.h" +#include "foldermetadata.h" #include "filesystem.h" @@ -175,73 +177,6 @@ void OCC::HydrationJob::start() connect(_transferDataServer, &QLocalServer::newConnection, this, &HydrationJob::onNewConnection); } -void OCC::HydrationJob::slotFolderIdError() -{ - // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) - qCCritical(lcHydration) << "Failed to get encrypted metadata of folder" << _requestId << _localPath << _folderPath; - emitFinished(Error); -} - -void OCC::HydrationJob::slotCheckFolderId(const QStringList &list) -{ - // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) - auto job = qobject_cast(sender()); - const QString folderId = list.first(); - qCDebug(lcHydration) << "Received id of folder" << folderId; - - const ExtraFolderInfo &folderInfo = job->_folderInfos.value(folderId); - - // Now that we have the folder-id we need it's JSON metadata - auto metadataJob = new GetMetadataApiJob(_account, folderInfo.fileId); - connect(metadataJob, &GetMetadataApiJob::jsonReceived, - this, &HydrationJob::slotCheckFolderEncryptedMetadata); - connect(metadataJob, &GetMetadataApiJob::error, - this, &HydrationJob::slotFolderEncryptedMetadataError); - - metadataJob->start(); -} - -void OCC::HydrationJob::slotFolderEncryptedMetadataError(const QByteArray & /*fileId*/, int /*httpReturnCode*/) -{ - // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) - qCCritical(lcHydration) << "Failed to find encrypted metadata information of remote file" << e2eMangledName(); - emitFinished(Error); - return; -} - -void OCC::HydrationJob::slotCheckFolderEncryptedMetadata(const QJsonDocument &json) -{ - // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) - qCDebug(lcHydration) << "Metadata Received reading" << e2eMangledName(); - const QString filename = e2eMangledName(); - const FolderMetadata metadata(_account, FolderMetadata::RequiredMetadataVersion::Version1, json.toJson(QJsonDocument::Compact)); - - if (metadata.isMetadataSetup()) { - const QVector files = metadata.files(); - - EncryptedFile encryptedInfo = {}; - - const QString encryptedFileExactName = e2eMangledName().section(QLatin1Char('/'), -1); - for (const EncryptedFile &file : files) { - if (encryptedFileExactName == file.encryptedFilename) { - EncryptedFile encryptedInfo = file; - encryptedInfo = file; - - qCDebug(lcHydration) << "Found matching encrypted metadata for file, starting download" << _requestId << _folderPath; - _transferDataSocket = _transferDataServer->nextPendingConnection(); - _job = new GETEncryptedFileJob(_account, _remotePath + e2eMangledName(), _transferDataSocket, {}, {}, 0, encryptedInfo, this); - - connect(qobject_cast(_job), &GETEncryptedFileJob::finishedSignal, this, &HydrationJob::onGetFinished); - _job->start(); - return; - } - } - } - - qCCritical(lcHydration) << "Failed to find encrypted metadata information of a remote file" << filename; - emitFinished(Error); -} - void OCC::HydrationJob::cancel() { _isCancelled = true; @@ -347,6 +282,38 @@ void OCC::HydrationJob::finalize(OCC::VfsCfApi *vfs) } } +void OCC::HydrationJob::slotFetchMetadataJobFinished(int statusCode, const QString &message) +{ + if (statusCode != 200) { + qCCritical(lcHydration) << "Failed to find encrypted metadata information of remote file" << e2eMangledName() << message; + emitFinished(Error); + return; + } + + // TODO: the following code is borrowed from PropagateDownloadEncrypted (see HydrationJob::onNewConnection() for explanation of next steps) + qCDebug(lcHydration) << "Metadata Received reading" << e2eMangledName(); + const auto metadata = _encryptedFolderMetadataHandler->folderMetadata(); + if (!metadata->isValid()) { + qCCritical(lcHydration) << "Failed to find encrypted metadata information of a remote file" << e2eMangledName(); + emitFinished(Error); + return; + } + + const auto files = metadata->files(); + const QString encryptedFileExactName = e2eMangledName().section(QLatin1Char('/'), -1); + for (const FolderMetadata::EncryptedFile &file : files) { + if (encryptedFileExactName == file.encryptedFilename) { + qCDebug(lcHydration) << "Found matching encrypted metadata for file, starting download" << _requestId << _folderPath; + _transferDataSocket = _transferDataServer->nextPendingConnection(); + _job = new GETEncryptedFileJob(_account, _remotePath + e2eMangledName(), _transferDataSocket, {}, {}, 0, file, this); + + connect(qobject_cast(_job), &GETEncryptedFileJob::finishedSignal, this, &HydrationJob::onGetFinished); + _job->start(); + return; + } + } +} + void OCC::HydrationJob::onGetFinished() { _errorCode = _job->reply()->error(); @@ -400,13 +367,17 @@ void OCC::HydrationJob::handleNewConnectionForEncryptedFile() const auto remoteFilename = e2eMangledName(); const auto remotePath = QString(rootPath + remoteFilename); - const auto remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); - - auto job = new LsColJob(_account, remoteParentPath, this); - job->setProperties({ "resourcetype", "http://owncloud.org/ns:fileid" }); - connect(job, &LsColJob::directoryListingSubfolders, - this, &HydrationJob::slotCheckFolderId); - connect(job, &LsColJob::finishedWithError, - this, &HydrationJob::slotFolderIdError); - job->start(); + const auto _remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); + + SyncJournalFileRecord rec; + if (!_journal->getRootE2eFolderRecord(_remoteParentPath, &rec) || !rec.isValid()) { + emitFinished(Error); + return; + } + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(_account, _remoteParentPath, _journal, rec.path())); + connect(_encryptedFolderMetadataHandler.data(), + &EncryptedFolderMetadataHandler::fetchFinished, + this, + &HydrationJob::slotFetchMetadataJobFinished); + _encryptedFolderMetadataHandler->fetchMetadata(); } diff --git a/src/libsync/vfs/cfapi/hydrationjob.h b/src/libsync/vfs/cfapi/hydrationjob.h index 153da71f913f5..27990c11a2ac5 100644 --- a/src/libsync/vfs/cfapi/hydrationjob.h +++ b/src/libsync/vfs/cfapi/hydrationjob.h @@ -21,6 +21,7 @@ class QLocalServer; class QLocalSocket; namespace OCC { +class EncryptedFolderMetadataHandler; class GETFileJob; class SyncJournalDb; class VfsCfApi; @@ -79,15 +80,12 @@ class HydrationJob : public QObject void cancel(); void finalize(OCC::VfsCfApi *vfs); -public slots: - void slotCheckFolderId(const QStringList &list); - void slotFolderIdError(); - void slotCheckFolderEncryptedMetadata(const QJsonDocument &json); - void slotFolderEncryptedMetadataError(const QByteArray &fileId, int httpReturnCode); - signals: void finished(HydrationJob *job); +private slots: + void slotFetchMetadataJobFinished(int statusCode, const QString &message); + private: void emitFinished(Status status); @@ -121,6 +119,9 @@ public slots: int _errorCode = 0; int _statusCode = 0; QString _errorString; + QString _remoteParentPath; + + QScopedPointer _encryptedFolderMetadataHandler; }; } // namespace OCC diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 44396f29a6caa..3ba407e75d935 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -35,6 +35,7 @@ nextcloud_add_test(XmlParse) nextcloud_add_test(ChecksumValidator) nextcloud_add_test(ClientSideEncryption) +nextcloud_add_test(ClientSideEncryptionV2) nextcloud_add_test(ExcludedFiles) nextcloud_add_test(Utility) diff --git a/test/syncenginetestutils.h b/test/syncenginetestutils.h index 3a299f3ac927e..c688eddce0441 100644 --- a/test/syncenginetestutils.h +++ b/test/syncenginetestutils.h @@ -497,10 +497,11 @@ class FakeQNAM : public QNetworkAccessManager class FakeCredentials : public OCC::AbstractCredentials { QNetworkAccessManager *_qnam; + QString _userName = "admin"; public: FakeCredentials(QNetworkAccessManager *qnam) : _qnam{qnam} { } [[nodiscard]] QString authType() const override { return "test"; } - [[nodiscard]] QString user() const override { return "admin"; } + [[nodiscard]] QString user() const override { return _userName; } [[nodiscard]] QString password() const override { return "password"; } [[nodiscard]] QNetworkAccessManager *createQNAM() const override { return _qnam; } [[nodiscard]] bool ready() const override { return true; } @@ -510,6 +511,10 @@ class FakeCredentials : public OCC::AbstractCredentials void persist() override { } void invalidateToken() override { } void forgetSensitiveData() override { } + void setUserName(const QString &userName) + { + _userName = userName; + } }; class FakeFolder diff --git a/test/testclientsideencryption.cpp b/test/testclientsideencryption.cpp index a2115561d9092..311dd9d9e4ed4 100644 --- a/test/testclientsideencryption.cpp +++ b/test/testclientsideencryption.cpp @@ -247,6 +247,26 @@ private slots: QCOMPARE(generateHash(chunkedOutputDecrypted.readAll()), originalFileHash); chunkedOutputDecrypted.close(); } + + void testGzipThenEncryptDataAndBack() + { + const auto metadataKeySize = 16; + + const auto keyForEncryption = EncryptionHelper::generateRandom(metadataKeySize); + const auto inputData = QByteArrayLiteral("sample text for encryption test"); + const auto initializationVector = EncryptionHelper::generateRandom(metadataKeySize); + + QByteArray authenticationTag; + const auto gzippedThenEncryptData = EncryptionHelper::gzipThenEncryptData(keyForEncryption, inputData, initializationVector, authenticationTag); + + QVERIFY(!gzippedThenEncryptData.isEmpty()); + + const auto decryptedThebGzipUnzippedData = EncryptionHelper::decryptThenUnGzipData(keyForEncryption, gzippedThenEncryptData, initializationVector); + + QVERIFY(!decryptedThebGzipUnzippedData.isEmpty()); + + QCOMPARE(inputData, decryptedThebGzipUnzippedData); + } }; QTEST_APPLESS_MAIN(TestClientSideEncryption) diff --git a/test/testclientsideencryptionv2.cpp b/test/testclientsideencryptionv2.cpp new file mode 100644 index 0000000000000..63554e2d23a40 --- /dev/null +++ b/test/testclientsideencryptionv2.cpp @@ -0,0 +1,382 @@ +/* + * Copyright (C) 2024 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 "syncenginetestutils.h" +#include "clientsideencryption.h" +#include "foldermetadata.h" +#include + +using namespace OCC; + +class TestClientSideEncryptionV2 : public QObject +{ + Q_OBJECT + + QScopedPointer _fakeQnam; + QScopedPointer _parsedMetadataWithFileDrop; + QScopedPointer _parsedMetadataAfterProcessingFileDrop; + + AccountPtr _account; + AccountPtr _secondAccount; + +private slots: + void initTestCase() + { + QVariantMap fakeCapabilities; + fakeCapabilities[QStringLiteral("end-to-end-encryption")] = QVariantMap{ + {QStringLiteral("enabled"), true}, + {QStringLiteral("api-version"), "2.0"} + }; + const QUrl fakeUrl("http://example.de"); + + { + _account = Account::create(); + _fakeQnam.reset(new FakeQNAM({})); + const auto cred = new FakeCredentials{_fakeQnam.data()}; + cred->setUserName("test"); + _account->setCredentials(cred); + _account->setUrl(fakeUrl); + _account->setCapabilities(fakeCapabilities); + } + { + // make a second fake account so we can share metadata to it later + _secondAccount = Account::create(); + _fakeQnam.reset(new FakeQNAM({})); + const auto credSecond = new FakeCredentials{_fakeQnam.data()}; + credSecond->setUserName("sharee"); + _secondAccount->setCredentials(credSecond); + _secondAccount->setUrl(fakeUrl); + _secondAccount->setCapabilities(fakeCapabilities); + } + + QSslCertificate cert; + QSslKey publicKey; + QByteArray privateKey; + + { + QFile e2eTestFakeCert(QStringLiteral("e2etestsfakecert.pem")); + QVERIFY(e2eTestFakeCert.open(QFile::ReadOnly)); + cert = QSslCertificate(e2eTestFakeCert.readAll()); + } + { + QFile e2etestsfakecertpublickey(QStringLiteral("e2etestsfakecertpublickey.pem")); + QVERIFY(e2etestsfakecertpublickey.open(QFile::ReadOnly)); + publicKey = QSslKey(e2etestsfakecertpublickey.readAll(), QSsl::KeyAlgorithm::Rsa, QSsl::EncodingFormat::Pem, QSsl::KeyType::PublicKey); + e2etestsfakecertpublickey.close(); + } + { + QFile e2etestsfakecertprivatekey(QStringLiteral("e2etestsfakecertprivatekey.pem")); + QVERIFY(e2etestsfakecertprivatekey.open(QFile::ReadOnly)); + privateKey = e2etestsfakecertprivatekey.readAll(); + } + + QVERIFY(!cert.isNull()); + QVERIFY(!publicKey.isNull()); + QVERIFY(!privateKey.isEmpty()); + + _account->e2e()->_certificate = cert; + _account->e2e()->_publicKey = publicKey; + _account->e2e()->_privateKey = privateKey; + + _secondAccount->e2e()->_certificate = cert; + _secondAccount->e2e()->_publicKey = publicKey; + _secondAccount->e2e()->_privateKey = privateKey; + + } + + void testInitializeNewRootFolderMetadataThenEncryptAndDecrypt() + { + QScopedPointer metadata(new FolderMetadata(_account, FolderMetadata::FolderType::Root)); + QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete); + metadataSetupCompleteSpy.wait(); + QCOMPARE(metadataSetupCompleteSpy.count(), 1); + QVERIFY(metadata->isValid()); + + const auto fakeFileName = "fakefile.txt"; + + FolderMetadata::EncryptedFile encryptedFile; + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fakeFileName; + encryptedFile.mimetype = "application/octet-stream"; + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + metadata->addEncryptedFile(encryptedFile); + + const auto encryptedMetadata = metadata->encryptedMetadata(); + QVERIFY(!encryptedMetadata.isEmpty()); + + const auto signature = metadata->metadataSignature(); + QVERIFY(!signature.isEmpty()); + + const auto metaDataDoc = QJsonDocument::fromJson(encryptedMetadata); + const auto folderUsers = metaDataDoc["users"].toArray(); + QVERIFY(!folderUsers.isEmpty()); + + auto isCurrentUserPresentAndCanDecrypt = false; + for (auto it = folderUsers.constBegin(); it != folderUsers.constEnd(); ++it) { + const auto folderUserObject = it->toObject(); + const auto userId = folderUserObject.value("userId").toString(); + + if (userId != _account->davUser()) { + continue; + } + + const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); + + if (!encryptedMetadataKey.isEmpty()) { + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + if (decryptedMetadataKey.isEmpty()) { + break; + } + + const auto metadataObj = metaDataDoc.object()["metadata"].toObject(); + + const auto cipherTextEncrypted = metadataObj["ciphertext"].toString().toLocal8Bit(); + + // for compatibility, the format is "cipheredpart|initializationVector", so we need to extract the "cipheredpart" + const auto cipherTextPartExtracted = cipherTextEncrypted.split('|').at(0); + + const auto nonce = QByteArray::fromBase64(metadataObj["nonce"].toString().toLocal8Bit()); + + const auto cipherTextDecrypted = + EncryptionHelper::decryptThenUnGzipData(decryptedMetadataKey, QByteArray::fromBase64(cipherTextPartExtracted), nonce); + if (cipherTextDecrypted.isEmpty()) { + break; + } + + const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted); + const auto files = cipherTextDocument.object()["files"].toObject(); + + if (files.isEmpty()) { + break; + } + + const auto parsedEncryptedFile = metadata->parseEncryptedFileFromJson(files.keys().first(), files.value(files.keys().first())); + + QCOMPARE(parsedEncryptedFile.originalFilename, fakeFileName); + + isCurrentUserPresentAndCanDecrypt = true; + break; + } + } + QVERIFY(isCurrentUserPresentAndCanDecrypt); + + auto encryptedMetadataCopy = encryptedMetadata; + encryptedMetadataCopy.replace("\"", "\\\""); + + QJsonDocument ocsDoc = QJsonDocument::fromJson(QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadataCopy)).toUtf8()); + + + QScopedPointer metadataFromJson(new FolderMetadata(_account, + ocsDoc.toJson(), + RootEncryptedFolderInfo::makeDefault(), signature)); + QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJson.data(), &FolderMetadata::setupComplete); + metadataSetupExistingCompleteSpy.wait(); + QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1); + QVERIFY(metadataFromJson->isValid()); + } + + void testE2EeFolderMetadataSharing() + { + // instantiate empty metadata, add a file, and share with a second user "sharee" + QScopedPointer metadata(new FolderMetadata(_account, FolderMetadata::FolderType::Root)); + QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete); + metadataSetupCompleteSpy.wait(); + QCOMPARE(metadataSetupCompleteSpy.count(), 1); + QVERIFY(metadata->isValid()); + + const auto fakeFileName = "fakefile.txt"; + + FolderMetadata::EncryptedFile encryptedFile; + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fakeFileName; + encryptedFile.mimetype = "application/octet-stream"; + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + metadata->addEncryptedFile(encryptedFile); + + QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate)); + + QVERIFY(metadata->removeUser(_secondAccount->davUser())); + + QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate)); + + const auto encryptedMetadata = metadata->encryptedMetadata(); + QVERIFY(!encryptedMetadata.isEmpty()); + + const auto signature = metadata->metadataSignature(); + QVERIFY(!signature.isEmpty()); + + const auto metaDataDoc = QJsonDocument::fromJson(encryptedMetadata); + const auto folderUsers = metaDataDoc["users"].toArray(); + QVERIFY(!folderUsers.isEmpty()); + + // make sure metadata setup was a success and we can parse and decrypt it with a second account "sharee" + auto isShareeUserPresentAndCanDecrypt = false; + for (auto it = folderUsers.constBegin(); it != folderUsers.constEnd(); ++it) { + const auto folderUserObject = it->toObject(); + const auto userId = folderUserObject.value("userId").toString(); + + if (userId != _secondAccount->davUser()) { + continue; + } + + const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); + + if (!encryptedMetadataKey.isEmpty()) { + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + if (decryptedMetadataKey.isEmpty()) { + break; + } + + const auto metadataObj = metaDataDoc.object()["metadata"].toObject(); + + const auto cipherTextEncrypted = metadataObj["ciphertext"].toString().toLocal8Bit(); + + // for compatibility, the format is "cipheredpart|initializationVector", so we need to extract the "cipheredpart" + const auto cipherTextPartExtracted = cipherTextEncrypted.split('|').at(0); + + const auto nonce = QByteArray::fromBase64(metadataObj["nonce"].toString().toLocal8Bit()); + + const auto cipherTextDecrypted = + EncryptionHelper::decryptThenUnGzipData(decryptedMetadataKey, QByteArray::fromBase64(cipherTextPartExtracted), nonce); + if (cipherTextDecrypted.isEmpty()) { + break; + } + + const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted); + const auto files = cipherTextDocument.object()["files"].toObject(); + + if (files.isEmpty()) { + break; + } + + const auto parsedEncryptedFile = metadata->parseEncryptedFileFromJson(files.keys().first(), files.value(files.keys().first())); + + QCOMPARE(parsedEncryptedFile.originalFilename, fakeFileName); + + isShareeUserPresentAndCanDecrypt = true; + break; + } + } + QVERIFY(isShareeUserPresentAndCanDecrypt); + + // now, setup existing metadata for the second user "sharee", add a file, and get encrypted JSON again + auto encryptedMetadataCopy = encryptedMetadata; + encryptedMetadataCopy.replace("\"", "\\\""); + + 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)); + QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJsonForSecondUser.data(), &FolderMetadata::setupComplete); + metadataSetupExistingCompleteSpy.wait(); + QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1); + QVERIFY(metadataFromJsonForSecondUser->isValid()); + + const auto fakeFileNameFromSecondUser = "fakefileFromSecondUser.txt"; + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fakeFileNameFromSecondUser; + encryptedFile.mimetype = "application/octet-stream"; + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + metadataFromJsonForSecondUser->addEncryptedFile(encryptedFile); + + auto encryptedMetadataFromSecondUser = metadataFromJsonForSecondUser->encryptedMetadata(); + encryptedMetadataFromSecondUser.replace("\"", "\\\""); + + const auto signatureAfterSecondUserModification = metadataFromJsonForSecondUser->metadataSignature(); + QVERIFY(!signatureAfterSecondUserModification.isEmpty()); + + QJsonDocument ocsDocFromSecondUser = QJsonDocument::fromJson( + QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadataFromSecondUser)).toUtf8()); + + QScopedPointer metadataFromJsonForFirstUserToCheckCrossSharing(new FolderMetadata(_account, + ocsDocFromSecondUser.toJson(), + RootEncryptedFolderInfo::makeDefault(), + signatureAfterSecondUserModification)); + QSignalSpy metadataSetupForCrossSharingCompleteSpy(metadataFromJsonForFirstUserToCheckCrossSharing.data(), &FolderMetadata::setupComplete); + metadataSetupForCrossSharingCompleteSpy.wait(); + QCOMPARE(metadataSetupForCrossSharingCompleteSpy.count(), 1); + QVERIFY(metadataFromJsonForFirstUserToCheckCrossSharing->isValid()); + + // now, check if the first user can decrypt metadata and get the file info added by the second user "sharee" + const auto encryptedMetadataForFirstUserCrossSharing = metadataFromJsonForFirstUserToCheckCrossSharing->encryptedMetadata(); + QVERIFY(!encryptedMetadataForFirstUserCrossSharing.isEmpty()); + + const auto metaDataDocForFirstUserCrossSharing = QJsonDocument::fromJson(encryptedMetadataForFirstUserCrossSharing); + const auto folderUsersForFirstUserCrossSharing = metaDataDocForFirstUserCrossSharing["users"].toArray(); + QVERIFY(!folderUsers.isEmpty()); + + // make sure metadata setup was a success and we can parse and decrypt it with a second account "sharee" + auto isFirstUserPresentAndCanDecrypt = false; + for (auto it = folderUsersForFirstUserCrossSharing.constBegin(); it != folderUsersForFirstUserCrossSharing.constEnd(); ++it) { + const auto folderUserObject = it->toObject(); + const auto userId = folderUserObject.value("userId").toString(); + + if (userId != _secondAccount->davUser()) { + continue; + } + + const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); + + if (!encryptedMetadataKey.isEmpty()) { + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + if (decryptedMetadataKey.isEmpty()) { + break; + } + + const auto metadataObj = metaDataDocForFirstUserCrossSharing.object()["metadata"].toObject(); + + const auto cipherTextEncrypted = metadataObj["ciphertext"].toString().toLocal8Bit(); + + // for compatibility, the format is "cipheredpart|initializationVector", so we need to extract the "cipheredpart" + const auto cipherTextPartExtracted = cipherTextEncrypted.split('|').at(0); + + const auto nonce = QByteArray::fromBase64(metadataObj["nonce"].toString().toLocal8Bit()); + + const auto cipherTextDecrypted = + EncryptionHelper::decryptThenUnGzipData(decryptedMetadataKey, QByteArray::fromBase64(cipherTextPartExtracted), nonce); + if (cipherTextDecrypted.isEmpty()) { + break; + } + + const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted); + const auto files = cipherTextDocument.object()["files"].toObject(); + + if (files.isEmpty()) { + break; + } + + FolderMetadata::EncryptedFile foundFile; + for (auto it = files.constBegin(), end = files.constEnd(); it != end; ++it) { + const auto parsedEncryptedFile = metadata->parseEncryptedFileFromJson(it.key(), it.value()); + if (!parsedEncryptedFile.originalFilename.isEmpty() && parsedEncryptedFile.originalFilename == fakeFileNameFromSecondUser) { + foundFile = parsedEncryptedFile; + } + } + QCOMPARE(foundFile.originalFilename, fakeFileNameFromSecondUser); + + isFirstUserPresentAndCanDecrypt = true; + break; + } + } + QVERIFY(isFirstUserPresentAndCanDecrypt); + } +}; + +QTEST_GUILESS_MAIN(TestClientSideEncryptionV2) +#include "testclientsideencryptionv2.moc" diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index dfa6873261175..d132c7d9758e3 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -123,7 +123,7 @@ private slots: // the server, let's just manually set the encryption bool in the folder journal SyncJournalFileRecord rec; QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &rec)); - rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2; + rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV2_0; rec._path = QStringLiteral("encrypted").toUtf8(); rec._type = CSyncEnums::ItemTypeDirectory; QVERIFY(folder->journalDb()->setFileRecord(rec)); diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp index 96d71da2d9ec3..c116e301fcb53 100644 --- a/test/testsecurefiledrop.cpp +++ b/test/testsecurefiledrop.cpp @@ -1,25 +1,27 @@ /* - * 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. + * Copyright (C) 2024 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 "syncengine.h" #include "syncenginetestutils.h" -#include "testhelper.h" -#include "owncloudpropagator_p.h" -#include "propagatorjobs.h" #include "clientsideencryption.h" - +#include "foldermetadata.h" +#include #include namespace { - constexpr auto fakeE2eeFolderName = "fake_e2ee_folder"; - const QString fakeE2eeFolderPath = QStringLiteral("/") + fakeE2eeFolderName; - }; + const std::array fakeFiles{"fakefile.txt", "fakefile1.txt"}; + const std::array fakeFilesFileDrop{"fakefiledropped.txt", "fakefiledropped1.txt"}; +}; using namespace OCC; @@ -27,165 +29,158 @@ class TestSecureFileDrop : public QObject { Q_OBJECT - FakeFolder _fakeFolder{FileInfo()}; - QSharedPointer _propagator; + QScopedPointer _fakeQnam; + AccountPtr _account; + 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); + QVariantMap capabilities; + capabilities[QStringLiteral("end-to-end-encryption")] = QVariantMap{{QStringLiteral("enabled"), true}, {QStringLiteral("api-version"), "2.0"}}; + + _account = Account::create(); + const QUrl url("http://example.de"); + _fakeQnam.reset(new FakeQNAM({})); + const auto cred = new FakeCredentials{_fakeQnam.data()}; + cred->setUserName("test"); + _account->setCredentials(cred); + _account->setUrl(url); + _account->setCapabilities(capabilities); + + QSslCertificate cert; + QSslKey publicKey; + QByteArray privateKey; { QFile e2eTestFakeCert(QStringLiteral("e2etestsfakecert.pem")); - if (e2eTestFakeCert.open(QFile::ReadOnly)) { - _fakeFolder.syncEngine().account()->e2e()->_certificate = QSslCertificate(e2eTestFakeCert.readAll()); - e2eTestFakeCert.close(); - } + QVERIFY(e2eTestFakeCert.open(QFile::ReadOnly)); + cert = QSslCertificate(e2eTestFakeCert.readAll()); } { QFile e2etestsfakecertpublickey(QStringLiteral("e2etestsfakecertpublickey.pem")); - if (e2etestsfakecertpublickey.open(QFile::ReadOnly)) { - _fakeFolder.syncEngine().account()->e2e()->_publicKey = QSslKey(e2etestsfakecertpublickey.readAll(), QSsl::KeyAlgorithm::Rsa, QSsl::EncodingFormat::Pem, QSsl::KeyType::PublicKey); - e2etestsfakecertpublickey.close(); - } + QVERIFY(e2etestsfakecertpublickey.open(QFile::ReadOnly)); + publicKey = QSslKey(e2etestsfakecertpublickey.readAll(), QSsl::KeyAlgorithm::Rsa, QSsl::EncodingFormat::Pem, QSsl::KeyType::PublicKey); + e2etestsfakecertpublickey.close(); } { QFile e2etestsfakecertprivatekey(QStringLiteral("e2etestsfakecertprivatekey.pem")); - if (e2etestsfakecertprivatekey.open(QFile::ReadOnly)) { - _fakeFolder.syncEngine().account()->e2e()->_privateKey = e2etestsfakecertprivatekey.readAll(); - e2etestsfakecertprivatekey.close(); - } + QVERIFY(e2etestsfakecertprivatekey.open(QFile::ReadOnly)); + privateKey = e2etestsfakecertprivatekey.readAll(); } - _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(), FolderMetadata::RequiredMetadataVersion::Version1_2, jsonDoc.toJson())); - _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), FolderMetadata::RequiredMetadataVersion::Version1_2, 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; - - const auto allKeys = metadata->fileDrop().keys(); - for (const auto &key : allKeys) { - 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(!cert.isNull()); + QVERIFY(!publicKey.isNull()); + QVERIFY(!privateKey.isEmpty()); + + _account->e2e()->_certificate = cert; + _account->e2e()->_publicKey = publicKey; + _account->e2e()->_privateKey = privateKey; + + QScopedPointer metadata(new FolderMetadata(_account, FolderMetadata::FolderType::Root)); + QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete); + metadataSetupCompleteSpy.wait(); + QCOMPARE(metadataSetupCompleteSpy.count(), 1); + QVERIFY(metadata->isValid()); + + for (const auto &fakeFileName : fakeFiles) { + FolderMetadata::EncryptedFile encryptedFile; + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fakeFileName; + encryptedFile.mimetype = "application/octet-stream"; + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + metadata->addEncryptedFile(encryptedFile); + } - QVERIFY(updateFileDropMetadataJobSpy.wait(3000)); + QJsonObject fakeFileDropPart; - QVERIFY(_parsedMetadataWithFileDrop); - QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent()); + QJsonArray fileDropUsers; + for (const auto &folderUser : metadata->_folderUsers) { + QJsonObject fileDropUser; + fileDropUser.insert("userId", folderUser.userId); + fileDropUser.insert("encryptedFiledropKey", QString::fromUtf8(folderUser.encryptedMetadataKey.toBase64())); + fileDropUsers.push_back(fileDropUser); + } - QVERIFY(_parsedMetadataAfterProcessingFileDrop); + for (const auto &fakeFileName : fakeFilesFileDrop) { + FolderMetadata::EncryptedFile encryptedFile; + encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); + encryptedFile.originalFilename = fakeFileName; + encryptedFile.mimetype = "application/octet-stream"; + encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); + + QJsonObject fakeFileDropEntry; + fakeFileDropEntry.insert("ciphertext", ""); + + QJsonObject fakeFileDropMetadataObject; + fakeFileDropMetadataObject.insert("filename", encryptedFile.originalFilename); + fakeFileDropMetadataObject.insert("mimetype", QString::fromUtf8(encryptedFile.mimetype)); + fakeFileDropMetadataObject.insert("nonce", QString::fromUtf8(encryptedFile.initializationVector.toBase64())); + fakeFileDropMetadataObject.insert("key", QString::fromUtf8(encryptedFile.encryptionKey.toBase64())); + fakeFileDropMetadataObject.insert("authenticationTag", QString::fromUtf8(QByteArrayLiteral("123").toBase64())); + QJsonDocument fakeFileDropMetadata; + fakeFileDropMetadata.setObject(fakeFileDropMetadataObject); + + + QByteArray authenticationTag; + const auto initializationVector = EncryptionHelper::generateRandom(16); + const auto cipherTextEncrypted = EncryptionHelper::gzipThenEncryptData(metadata->_metadataKeyForEncryption, + fakeFileDropMetadata.toJson(QJsonDocument::JsonFormat::Compact), + initializationVector, + authenticationTag); + fakeFileDropEntry.insert("ciphertext", QString::fromUtf8(cipherTextEncrypted.toBase64())); + fakeFileDropEntry.insert("nonce", QString::fromUtf8(initializationVector.toBase64())); + fakeFileDropEntry.insert("authenticationTag", QString::fromUtf8(authenticationTag.toBase64())); + fakeFileDropEntry.insert("users", fileDropUsers); + + fakeFileDropPart.insert(encryptedFile.encryptedFilename, fakeFileDropEntry); + } + metadata->setFileDrop(fakeFileDropPart); - QVERIFY(_parsedMetadataAfterProcessingFileDrop->files().size() != _parsedMetadataWithFileDrop->files().size()); + auto encryptedMetadata = metadata->encryptedMetadata(); + encryptedMetadata.replace("\"", "\\\""); + 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)); - QVERIFY(!updateFileDropMetadataJobSpy.isEmpty()); - QVERIFY(!updateFileDropMetadataJobSpy.at(0).isEmpty()); - QCOMPARE(updateFileDropMetadataJobSpy.at(0).first().toInt(), SyncFileItem::Status::Success); + QSignalSpy metadataWithFileDropSetupCompleteSpy(_parsedMetadataWithFileDrop.data(), &FolderMetadata::setupComplete); + metadataWithFileDropSetupCompleteSpy.wait(); + QCOMPARE(metadataWithFileDropSetupCompleteSpy.count(), 1); + QVERIFY(_parsedMetadataWithFileDrop->isValid()); - QVERIFY(!fileDropMetadataParsedAndAdjustedSpy.isEmpty()); + QCOMPARE(_parsedMetadataWithFileDrop->_fileDropEntries.count(), fakeFilesFileDrop.size()); + } - QCOMPARE(_lockCallsCount, 1); - QCOMPARE(_unlockCallsCount, 1); - QCOMPARE(_propFindCallsCount, 2); - QCOMPARE(_getMetadataCallsCount, 1); - QCOMPARE(_putMetadataCallsCount, 1); + void testMoveFileDropMetadata() + { + QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent()); + QVERIFY(_parsedMetadataWithFileDrop->moveFromFileDropToFiles()); - updateFileDropMetadataJob->deleteLater(); + auto encryptedMetadata = _parsedMetadataWithFileDrop->encryptedMetadata(); + encryptedMetadata.replace("\"", "\\\""); + const auto signature = _parsedMetadataWithFileDrop->metadataSignature(); + 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)); + + QSignalSpy metadataAfterProcessingFileDropSetupCompleteSpy(_parsedMetadataAfterProcessingFileDrop.data(), &FolderMetadata::setupComplete); + metadataAfterProcessingFileDropSetupCompleteSpy.wait(); + QCOMPARE(metadataAfterProcessingFileDropSetupCompleteSpy.count(), 1); + QVERIFY(_parsedMetadataAfterProcessingFileDrop->isValid()); + + for (const auto &fakeFileName : fakeFilesFileDrop) { + const auto foundInEncryptedFiles = std::find_if(std::cbegin(_parsedMetadataAfterProcessingFileDrop->_files), std::cend(_parsedMetadataAfterProcessingFileDrop->_files), [fakeFileName](const auto &encryptedFile) { + return encryptedFile.originalFilename == fakeFileName; + }); + QVERIFY(foundInEncryptedFiles != std::cend(_parsedMetadataAfterProcessingFileDrop->_files)); + } } - -signals: - void fileDropMetadataParsedAndAdjusted(); }; QTEST_GUILESS_MAIN(TestSecureFileDrop)