From 867249f339cc819a0f912acfa59fd02f74c8b407 Mon Sep 17 00:00:00 2001 From: alex-z Date: Wed, 7 Dec 2022 19:43:27 +0100 Subject: [PATCH] Do not sync encrypted folders if the E2EE mnemonic is not entered. Automatically blacklist them. Remove from blacklist once the E2EE mnemonic is provided. Signed-off-by: alex-z --- src/common/syncjournaldb.h | 4 +- src/csync/csync_exclude.h | 1 + src/gui/accountsettings.cpp | 70 ++++++++++++++++++++++++++++++++++- src/gui/accountsettings.h | 7 ++++ src/gui/folderstatusmodel.cpp | 49 +++++++++++++++++------- src/gui/folderstatusmodel.h | 3 ++ src/libsync/discovery.cpp | 35 +++++++++++++++++- src/libsync/discovery.h | 3 ++ 8 files changed, 156 insertions(+), 16 deletions(-) diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index 998a93392..18e1ebd38 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -168,7 +168,9 @@ public: SelectiveSyncWhiteList = 2, /** List of big sync folders that have not been confirmed by the user yet and that the UI * should notify about */ - SelectiveSyncUndecidedList = 3 + SelectiveSyncUndecidedList = 3, + /** List of encrypted folders that will need to be removed from the blacklist when E2EE gets set up*/ + SelectiveSyncE2eFoldersToRemoveFromBlacklist = 4, }; /* return the specified list from the database */ QStringList getSelectiveSyncList(SelectiveSyncListType type, bool *ok); diff --git a/src/csync/csync_exclude.h b/src/csync/csync_exclude.h index d701401af..0fd9acbf3 100644 --- a/src/csync/csync_exclude.h +++ b/src/csync/csync_exclude.h @@ -47,6 +47,7 @@ enum CSYNC_EXCLUDE_TYPE { CSYNC_FILE_EXCLUDE_SERVER_BLACKLISTED, CSYNC_FILE_EXCLUDE_LEADING_SPACE, CSYNC_FILE_EXCLUDE_LEADING_AND_TRAILING_SPACE, + CSYNC_FILE_E2E_COULD_NOT_DECRYPT_EXCLUDED, }; class ExcludedFilesTest; diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 2b417699a..3fa71150b 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -1370,6 +1370,70 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, } } +void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() +{ + if (_accountState->account()->e2e()->_mnemonic.isEmpty()) { + return; + } + + disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); + + for (const auto folder : FolderMan::instance()->map()) { + if (folder->accountState() != _accountState) { + continue; + } + bool ok = false; + const auto foldersToRemoveFromBlacklist = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); + if (foldersToRemoveFromBlacklist.isEmpty()) { + continue; + } + auto blackList = folder->journalDb()->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); + const auto blackListSize = blackList.size(); + if (blackListSize == 0) { + continue; + } + for (const auto &pathToRemoveFromBlackList : foldersToRemoveFromBlacklist) { + blackList.removeAll(pathToRemoveFromBlackList); + } + if (blackList.size() != blackListSize) { + if (folder->isSyncRunning()) { + folderTerminateSyncAndUpdateBlackList(blackList, folder, foldersToRemoveFromBlacklist); + return; + } + updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); + } + } +} + +void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const +{ + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); + folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, {}); + for (const auto &pathToRemoteDiscover : foldersToRemoveFromBlacklist) { + folder->journalDb()->schedulePathForRemoteDiscovery(pathToRemoteDiscover); + } + FolderMan::instance()->scheduleFolder(folder); +} + +void AccountSettings::folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) +{ + if (_folderConnections.contains(folder->alias())) { + qCWarning(lcAccountSettings) << "Folder " << folder->alias() << "is already terminating the sync."; + return; + } + // in case sync is already running - terminate it and start a new one + const QMetaObject::Connection syncTerminatedConnection = connect(folder, &Folder::syncFinished, this, [this, blackList, folder, foldersToRemoveFromBlacklist]() { + const auto foundConnectionIt = _folderConnections.find(folder->alias()); + if (foundConnectionIt != _folderConnections.end()) { + disconnect(*foundConnectionIt); + _folderConnections.erase(foundConnectionIt); + } + updateBlackListAndScheduleFolderSync(blackList, folder, foldersToRemoveFromBlacklist); + }); + _folderConnections.insert(folder->alias(), syncTerminatedConnection); + folder->slotTerminateSync(); +} + void AccountSettings::refreshSelectiveSyncStatus() { QString msg; @@ -1478,6 +1542,8 @@ void AccountSettings::customizeStyle() void AccountSettings::initializeE2eEncryption() { + connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); + if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) { slotE2eEncryptionMnemonicReady(); } else { @@ -1493,7 +1559,9 @@ void AccountSettings::initializeE2eEncryption() if (!_accountState->account()->e2e()->_publicKey.isNull()) { _ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled on this account with another device." "
" - "It can be enabled on this device by entering your mnemonic.")); + "It can be enabled on this device by entering your mnemonic." + "
" + "This will enable synchronisation of existing encrypted folders.")); } }); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(false); diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index cf32bc737..f8972d895 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -111,6 +111,11 @@ protected slots: void slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); + void slotPossiblyUnblacklistE2EeFoldersAndRestartSync(); + +private slots: + void updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const; + void folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist); private: void displayMnemonic(const QString &mnemonic); @@ -139,6 +144,8 @@ private: QAction *_addAccountAction; bool _menuShown; + + QHash _folderConnections; }; } // namespace OCC diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index fe981726e..a64189dd8 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -60,6 +60,8 @@ static bool sortByFolderHeader(const FolderStatusModel::SubFolderInfo &lhs, cons void FolderStatusModel::setAccountState(const AccountState *accountState) { + connect(accountState->account()->e2e(), &OCC::ClientSideEncryption::initializationFinished, this, &FolderStatusModel::e2eInitializationFinished); + beginResetModel(); _dirty = false; _folders.clear(); @@ -153,41 +155,49 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const return QVariant(); } case SubFolder: { - const auto &x = static_cast(index.internalPointer())->_subs.at(index.row()); - const auto supportsSelectiveSync = x._folder && x._folder->supportsSelectiveSync(); + const auto &subfolderInfo = static_cast(index.internalPointer())->_subs.at(index.row()); + const auto supportsSelectiveSync = subfolderInfo._folder && subfolderInfo._folder->supportsSelectiveSync(); switch (role) { - case Qt::DisplayRole: + case Qt::DisplayRole: { //: Example text: "File.txt (23KB)" - return x._size < 0 ? x._name : tr("%1 (%2)").arg(x._name, Utility::octetsToString(x._size)); + const auto &xParent = static_cast(index.internalPointer()); + const auto suffix = (subfolderInfo._isNonDecryptable && subfolderInfo._checked && (!xParent || !xParent->_isEncrypted)) + ? tr(" - %1").arg("Could not decrypt!") + : QString{}; + return subfolderInfo._size < 0 ? QString(subfolderInfo._name + suffix) : QString(tr("%1 (%2)").arg(subfolderInfo._name, Utility::octetsToString(subfolderInfo._size)) + suffix); + } case Qt::ToolTipRole: - return QString(QLatin1String("") + Utility::escape(x._size < 0 ? x._name : tr("%1 (%2)").arg(x._name, Utility::octetsToString(x._size))) + QLatin1String("")); + return QString(QLatin1String("") + Utility::escape(subfolderInfo._size < 0 ? subfolderInfo._name : tr("%1 (%2)").arg(subfolderInfo._name, Utility::octetsToString(subfolderInfo._size))) + QLatin1String("")); case Qt::CheckStateRole: if (supportsSelectiveSync) { - return x._checked; + return subfolderInfo._checked; } else { return QVariant(); } case Qt::DecorationRole: { - if (x._isEncrypted) { + if (subfolderInfo._isNonDecryptable && subfolderInfo._checked) { + return QIcon(QLatin1String(":/client/theme/lock-broken.svg")); + } + if (subfolderInfo._isEncrypted) { return QIcon(QLatin1String(":/client/theme/lock-https.svg")); - } else if (x._size > 0 && isAnyAncestorEncrypted(index)) { + } else if (subfolderInfo._size > 0 && isAnyAncestorEncrypted(index)) { return QIcon(QLatin1String(":/client/theme/lock-broken.svg")); } - return QFileIconProvider().icon(x._isExternal ? QFileIconProvider::Network : QFileIconProvider::Folder); + return QFileIconProvider().icon(subfolderInfo._isExternal ? QFileIconProvider::Network : QFileIconProvider::Folder); } case Qt::ForegroundRole: - if (x._isUndecided) { + if (subfolderInfo._isUndecided || (subfolderInfo._isNonDecryptable && subfolderInfo._checked)) { return QColor(Qt::red); } break; case FileIdRole: - return x._fileId; + return subfolderInfo._fileId; case FolderStatusDelegate::FolderPathRole: { - auto f = x._folder; + auto f = subfolderInfo._folder; if (!f) return QVariant(); - return QVariant(f->path() + x._path); + return QVariant(f->path() + subfolderInfo._path); } } } @@ -742,6 +752,10 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) newInfo._isEncrypted = encryptionMap.value(removeTrailingSlash(path)).toString() == QStringLiteral("1"); newInfo._path = relativePath; + newInfo._isNonDecryptable = newInfo._isEncrypted + && _accountState->account()->e2e() && !_accountState->account()->e2e()->_publicKey.isNull() + && _accountState->account()->e2e()->_privateKey.isNull(); + SyncJournalFileRecord rec; if (!parentInfo->_folder->journalDb()->getFileRecordByE2eMangledName(removeTrailingSlash(relativePath), &rec)) { qCWarning(lcFolderStatus) << "Could not get file record by E2E Mangled Name from local DB" << removeTrailingSlash(relativePath); @@ -1133,6 +1147,15 @@ void FolderStatusModel::slotSetProgress(const ProgressInfo &progress) emit dataChanged(index(folderIndex), index(folderIndex), roles); } +void FolderStatusModel::e2eInitializationFinished(bool isNewMnemonicGenerated) +{ + Q_UNUSED(isNewMnemonicGenerated); + + for (int i = 0; i < _folders.count(); ++i) { + resetAndFetch(index(i)); + } +} + void FolderStatusModel::slotFolderSyncStateChange(Folder *f) { if (!f) { diff --git a/src/gui/folderstatusmodel.h b/src/gui/folderstatusmodel.h index f5fe5018f..e2397295c 100644 --- a/src/gui/folderstatusmodel.h +++ b/src/gui/folderstatusmodel.h @@ -80,6 +80,8 @@ public: Qt::CheckState _checked = Qt::Checked; + bool _isNonDecryptable = false; + // Whether this has a FetchLabel subrow [[nodiscard]] bool hasLabel() const; @@ -125,6 +127,7 @@ public slots: void slotSyncAllPendingBigFolders(); void slotSyncNoPendingBigFolders(); void slotSetProgress(const OCC::ProgressInfo &progress); + void e2eInitializationFinished(bool isNewMnemonicGenerated); private slots: void slotUpdateDirectories(const QStringList &); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index aee314020..79758a777 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -12,6 +12,7 @@ * for more details. */ +#include "account.h" #include "discovery.h" #include "common/filesystembase.h" #include "common/syncjournaldb.h" @@ -250,6 +251,13 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent excluded = CSYNC_FILE_EXCLUDE_TRAILING_SPACE; } else if (startsWithSpace) { excluded = CSYNC_FILE_EXCLUDE_LEADING_SPACE; + } else if (entries.serverEntry.isValid() && entries.serverEntry.isE2eEncrypted) { + const auto wasE2eEnabledButNotSetup = _discoveryData->_account->e2e() + && !_discoveryData->_account->e2e()->_publicKey.isNull() + && _discoveryData->_account->e2e()->_privateKey.isNull(); + if (wasE2eEnabledButNotSetup) { + excluded = CSYNC_FILE_E2E_COULD_NOT_DECRYPT_EXCLUDED; + } } } @@ -296,7 +304,10 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent if (excluded == CSYNC_NOT_EXCLUDED && !entries.localEntry.isSymLink) { return false; - } else if (excluded == CSYNC_FILE_SILENTLY_EXCLUDED || excluded == CSYNC_FILE_EXCLUDE_AND_REMOVE) { + } else if (excluded == CSYNC_FILE_SILENTLY_EXCLUDED || excluded == CSYNC_FILE_EXCLUDE_AND_REMOVE || excluded == CSYNC_FILE_E2E_COULD_NOT_DECRYPT_EXCLUDED) { + if (excluded == CSYNC_FILE_E2E_COULD_NOT_DECRYPT_EXCLUDED && isDirectory && path != QStringLiteral("/")) { + checkAndUpdateSelectiveSyncListsForE2eeFolders(path); + } emit _discoveryData->silentlyExcluded(path); return true; } @@ -312,6 +323,7 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent } else { switch (excluded) { case CSYNC_NOT_EXCLUDED: + case CSYNC_FILE_E2E_COULD_NOT_DECRYPT_EXCLUDED: case CSYNC_FILE_SILENTLY_EXCLUDED: case CSYNC_FILE_EXCLUDE_AND_REMOVE: qFatal("These were handled earlier"); @@ -379,6 +391,27 @@ bool ProcessDirectoryJob::handleExcluded(const QString &path, const Entries &ent return true; } +void ProcessDirectoryJob::checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path) +{ + bool ok = false; + auto blackList = _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, &ok); + auto selectiveSyncE2eFoldersToRemoveFromBlacklist = + _discoveryData->_statedb->getSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, &ok); + const auto pathWithTrailingSpace = path.endsWith(QLatin1Char('/')) ? path : path + QLatin1Char('/'); + if (!blackList.contains(pathWithTrailingSpace)) { + blackList.push_back(pathWithTrailingSpace); + blackList.sort(); + _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); + } + // record it into a separate list to automatically remove from blacklist once the e2EE gets set up + if (!selectiveSyncE2eFoldersToRemoveFromBlacklist.contains(pathWithTrailingSpace)) { + selectiveSyncE2eFoldersToRemoveFromBlacklist.push_back(pathWithTrailingSpace); + selectiveSyncE2eFoldersToRemoveFromBlacklist.sort(); + _discoveryData->_statedb->setSelectiveSyncList(SyncJournalDb::SelectiveSyncE2eFoldersToRemoveFromBlacklist, + selectiveSyncE2eFoldersToRemoveFromBlacklist); + } +} + void ProcessDirectoryJob::processFile(PathTuple path, const LocalInfo &localEntry, const RemoteInfo &serverEntry, const SyncJournalFileRecord &dbEntry) diff --git a/src/libsync/discovery.h b/src/libsync/discovery.h index 1be9f5372..c77e31082 100644 --- a/src/libsync/discovery.h +++ b/src/libsync/discovery.h @@ -148,6 +148,9 @@ private: // path is the full relative path of the file. localName is the base name of the local entry. bool handleExcluded(const QString &path, const Entries &entries, bool isHidden); + // check if the path is an e2e encrypted and the e2ee is not set up, and insert it into a corresponding list in the sync journal + void checkAndUpdateSelectiveSyncListsForE2eeFolders(const QString &path); + /** Reconcile local/remote/db information for a single item. * * Can be a file or a directory. -- 2.30.2