diff --git a/src/common/remoteinfo.h b/src/common/remoteinfo.h index cbfcaaffb339d..7789717305c42 100644 --- a/src/common/remoteinfo.h +++ b/src/common/remoteinfo.h @@ -34,6 +34,9 @@ struct RemoteInfo bool _isE2eEncrypted = false; bool isFileDropDetected = false; QString e2eMangledName; + QByteArray _e2eFileEncryptionKey; + QByteArray _initializationVector; + QByteArray _authenticationTag; bool sharedByMe = false; [[nodiscard]] bool isValid() const { return !name.isNull(); } diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index acd97717c0ce6..816414d830ced 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -48,7 +48,7 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg) "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \ " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " \ " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, " \ - " sharedByMe, isLivePhoto, livePhotoFile, quotaBytesUsed, quotaBytesAvailable" \ + " sharedByMe, isLivePhoto, livePhotoFile, quotaBytesUsed, quotaBytesAvailable, e2eFileEncryptionKey, authenticationTag, initializationVector" \ " FROM metadata" \ " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" @@ -66,6 +66,9 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que rec._checksumHeader = query.baValue(9); rec._e2eMangledName = query.baValue(10); rec._e2eEncryptionStatus = static_cast(query.intValue(11)); + rec._e2eFileEncryptionKey = query.baValue(28); + rec._authenticationTag = query.baValue(29); + rec._initializationVector = query.baValue(30); rec._lockstate._locked = query.intValue(13) > 0; rec._lockstate._lockOwnerDisplayName = query.stringValue(14); rec._lockstate._lockOwnerId = query.stringValue(15); @@ -928,6 +931,10 @@ bool SyncJournalDb::updateMetadataTableStructure() addColumn(quotaBytesAvailable, bigInt, false, defaultCommand); } + addColumn(u"e2eFileEncryptionKey"_s, u"TEXT"_s); + addColumn(u"authenticationTag"_s, u"TEXT"_s); + addColumn(u"initializationVector"_s, u"TEXT"_s); + return re; } @@ -1089,9 +1096,13 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata " "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, " - "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, lock, lockType, lockOwnerDisplayName, lockOwnerId, " - "lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile, quotaBytesUsed, quotaBytesAvailable) " - "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34);"), + "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " + "lock, lockType, lockOwnerDisplayName, lockOwnerId, " + "lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, " + "sharedByMe, isLivePhoto, livePhotoFile, quotaBytesUsed, quotaBytesAvailable, " + "e2eFileEncryptionKey, authenticationTag, initializationVector) " + "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, " + "?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32, ?33, ?34, ?35, ?36, ?37);"), _db); if (!query) { qCWarning(lcDb) << "database error:" << query->error(); @@ -1117,6 +1128,9 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(17, record._e2eMangledName); query->bindValue(18, static_cast(record._e2eEncryptionStatus)); query->bindValue(19, {}); + query->bindValue(35, record._e2eFileEncryptionKey); + query->bindValue(36, record._authenticationTag); + query->bindValue(37, record._initializationVector); query->bindValue(20, record._lockstate._locked ? 1 : 0); query->bindValue(21, record._lockstate._lockOwnerType); query->bindValue(22, record._lockstate._lockOwnerDisplayName); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index 19d38be018a68..bab0cc4e35511 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -74,6 +74,9 @@ class OCSYNC_EXPORT SyncJournalFileRecord QByteArray _checksumHeader; QByteArray _e2eMangledName; EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted; + QByteArray _e2eFileEncryptionKey; + QByteArray _initializationVector; + QByteArray _authenticationTag; SyncJournalFileLockInfo _lockstate; bool _isShared = false; qint64 _lastShareStateFetchedTimestamp = 0; diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index cfd3cb59abfa4..2529bc9acd239 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -31,6 +31,7 @@ #include "tooltipupdater.h" #include "filesystem.h" #include "encryptfolderjob.h" +#include "repairfolderencryptionmetadatajob.h" #include "syncresult.h" #include "ignorelisttablewidget.h" #include "networksettings.h" @@ -455,6 +456,25 @@ void AccountSettings::slotEncryptFolderFinished(int status) job->deleteLater(); } +void AccountSettings::slotRepairEncryptedFolderFinished(int status) +{ + qCInfo(lcAccountSettings) << "Current folder encryption status code:" << status; + auto job = qobject_cast(sender()); + Q_ASSERT(job); + if (!job->errorString().isEmpty()) { + QMessageBox::warning(nullptr, tr("Repair of encryption failed"), job->errorString()); + } + + const auto folder = job->property(propertyFolder).value(); + Q_ASSERT(folder); + const auto path = job->property(propertyPath).toString(); + const auto index = _model->indexForPath(folder, path); + Q_ASSERT(index.isValid()); + _model->resetAndFetch(index.parent()); + + job->deleteLater(); +} + QString AccountSettings::selectedFolderAlias() const { const auto selected = _ui->_folderList->selectionModel()->currentIndex(); @@ -590,6 +610,33 @@ void AccountSettings::slotMarkSubfolderEncrypted(FolderStatusModel::SubFolderInf showEnableE2eeWarningDialog(encryptFolder); } +void AccountSettings::slotRepairEncryptedSubfolder(FolderStatusModel::SubFolderInfo *folderInfo) +{ + const auto folder = folderInfo->_folder; + Q_ASSERT(folder); + + const auto folderAlias = folder->alias(); + const auto path = folderInfo->_path; + const auto fileId = folderInfo->_fileId; + + if (!folder) { + qCWarning(lcAccountSettings) << "Could not repair encrypted folder because folder" << folderAlias << "does not exist anymore"; + QMessageBox::warning(nullptr, tr("Repair of encryption failed"), tr("Could not repair encryption because the folder does not exist anymore")); + return; + } + + // Folder info have directory paths in Foo/Bar/ convention... + Q_ASSERT(!path.startsWith('/') && path.endsWith('/')); + // But EncryptFolderJob expects directory path Foo/Bar convention + const auto choppedPath = path.chopped(1); + auto job = new RepairFolderEncryptionMetadataJob(accountsState()->account(), folder->journalDb(), choppedPath, choppedPath, folder->remotePath(), fileId); + job->setParent(this); + job->setProperty(propertyFolder, QVariant::fromValue(folder)); + job->setProperty(propertyPath, QVariant::fromValue(path)); + connect(job, &RepairFolderEncryptionMetadataJob::finished, this, &AccountSettings::slotRepairEncryptedFolderFinished); + job->start(); +} + void AccountSettings::slotEditCurrentIgnoredFiles() { const auto folder = FolderMan::instance()->folder(selectedFolderAlias()); @@ -713,6 +760,10 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index // Ignore decrypting for now since it only works with an empty folder // connect(ac, &QAction::triggered, [this, &info] { slotMarkSubfolderDecrypted(info); }); } + if (isEncrypted && _accountState->account()->e2e()->isInitialized()) { + ac = menu.addAction(tr("Repair encryption")); + connect(ac, &QAction::triggered, [this, info] { slotRepairEncryptedSubfolder(info); }); + } } ac = menu.addAction(tr("Edit Ignored Files")); diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 6c153d96bbe71..d30ce233b7e5e 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -111,6 +111,7 @@ protected Q_SLOTS: void slotToggleSignInState(); void refreshSelectiveSyncStatus(); void slotMarkSubfolderEncrypted(OCC::FolderStatusModel::SubFolderInfo *folderInfo); + void slotRepairEncryptedSubfolder(OCC::FolderStatusModel::SubFolderInfo *folderInfo); void slotSubfolderContextMenuRequested(const QModelIndex& idx, const QPoint& point); void slotCustomContextMenuRequested(const QPoint &); void slotFolderListClicked(const QModelIndex &indx); @@ -122,6 +123,7 @@ protected Q_SLOTS: void slotE2eEncryptionGenerateKeys(); void slotE2eEncryptionInitializationFinished(bool isNewMnemonicGenerated); void slotEncryptFolderFinished(int status); + void slotRepairEncryptedFolderFinished(int status); void slotSelectiveSyncChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector &roles); diff --git a/src/gui/folder.h b/src/gui/folder.h index 823b4fd577a8d..e7a834311633a 100644 --- a/src/gui/folder.h +++ b/src/gui/folder.h @@ -392,7 +392,7 @@ public Q_SLOTS: void slotTerminateSync(); // connected to the corresponding signals in the SyncEngine - void slotAboutToRemoveAllFiles(OCC::SyncFileItem::Direction, std::function callback); + void slotAboutToRemoveAllFiles(OCC::SyncFileItem::Direction, std::function callback);// clazy:exclude=fully-qualified-moc-types /** * Starts a sync operation diff --git a/src/gui/owncloudgui.h b/src/gui/owncloudgui.h index 609a4e7ebbda2..7d973b6295687 100644 --- a/src/gui/owncloudgui.h +++ b/src/gui/owncloudgui.h @@ -97,7 +97,7 @@ public Q_SLOTS: * to the folder). */ void slotShowShareDialog(const QString &localPath) const; - void slotShowGovernanceLabelsDialog(AccountPtr account, + void slotShowGovernanceLabelsDialog(OCC::AccountPtr account, const QString &localPath, const QString &fileId) const; void slotShowFileActivityDialog(const QString &localPath) const; diff --git a/src/gui/systray.h b/src/gui/systray.h index 876c3c0e1c8e0..7a25d19d9071b 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -136,7 +136,7 @@ public Q_SLOTS: void createEditFileLocallyLoadingDialog(const QString &fileName); void destroyEditFileLocallyLoadingDialog(); void createResolveConflictsDialog(const OCC::ActivityList &allConflicts); - void createGovernanceLabelsDialog(AccountPtr account, const QString &fileName, const QString &fileId); + void createGovernanceLabelsDialog(OCC::AccountPtr account, const QString &fileName, const QString &fileId); void createEncryptionTokenDiscoveryDialog(); void destroyEncryptionTokenDiscoveryDialog(); diff --git a/src/gui/tray/usermodel.h b/src/gui/tray/usermodel.h index e252fdcbdb32c..ca9dc7f54b2bb 100644 --- a/src/gui/tray/usermodel.h +++ b/src/gui/tray/usermodel.h @@ -203,8 +203,8 @@ public Q_SLOTS: private Q_SLOTS: void slotPushNotificationsReady(); void slotDisconnectPushNotifications(); - void slotReceivedPushFilesChanges(Account *account); - void slotReceivedPushFileIdsChanges(Account *account, const QList &fileIds); + void slotReceivedPushFilesChanges(OCC::Account *account); + void slotReceivedPushFileIdsChanges(OCC::Account *account, const QList &fileIds); void slotReceivedPushNotification(OCC::Account *account); void slotReceivedPushActivity(OCC::Account *account); void slotCheckExpiredActivities(); diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 72fe345dfa504..dd4d081954b92 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -138,6 +138,8 @@ set(libsync_SRCS clientsideencryptionprimitives.cpp clientsideencryptiontokenselector.h clientsideencryptiontokenselector.cpp + repairfolderencryptionmetadatajob.h + repairfolderencryptionmetadatajob.cpp datetimeprovider.h datetimeprovider.cpp rootencryptedfolderinfo.h diff --git a/src/libsync/abstractnetworkjob.cpp b/src/libsync/abstractnetworkjob.cpp index 85b38f71638a9..804bff629a5c4 100644 --- a/src/libsync/abstractnetworkjob.cpp +++ b/src/libsync/abstractnetworkjob.cpp @@ -624,3 +624,5 @@ std::optional AbstractNetworkJob::hstsErrorStringFromReply(QNetworkRepl } } // namespace OCC + +#include "moc_abstractnetworkjob.cpp" \ No newline at end of file diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 191c105527916..66b42721e2319 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -332,10 +332,10 @@ private Q_SLOTS: void privateKeyFetched(QKeychain::Job *incoming); void mnemonicKeyFetched(QKeychain::Job *incoming); - void handlePrivateKeyDeleted(const QKeychain::Job* const incoming); - void handleCertificateDeleted(const QKeychain::Job* const incoming); - void handleMnemonicDeleted(const QKeychain::Job* const incoming); - void handlePublicKeyDeleted(const QKeychain::Job* const incoming); + void handlePrivateKeyDeleted(const QKeychain::Job* incoming); + void handleCertificateDeleted(const QKeychain::Job* incoming); + void handleMnemonicDeleted(const QKeychain::Job* incoming); + void handlePublicKeyDeleted(const QKeychain::Job* incoming); void checkAllSensitiveDataDeleted(); void getPrivateKeyFromServer(); diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index 53dd69b452eaf..59064624d67ca 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -776,6 +776,9 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it result = serverEntry.e2eMangledName.mid(rootPath.length()); return result; }(); + item->_e2eFileEncryptionKey = serverEntry._e2eFileEncryptionKey; + item->_initializationVector = serverEntry._initializationVector; + item->_authenticationTag = serverEntry._authenticationTag; item->_locked = serverEntry.locked; item->_lockOwnerDisplayName = serverEntry.lockOwnerDisplayName; item->_lockOwnerId = serverEntry.lockOwnerId; diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 2ad6fb4bbb000..52b39135573dc 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -731,6 +731,9 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in result._isE2eEncrypted = true; result.e2eMangledName = _subPath.mid(1) + u'/' + result.name; result.name = encryptedFileInfo->originalFilename; + result._e2eFileEncryptionKey = encryptedFileInfo->encryptionKey; + result._initializationVector = encryptedFileInfo->initializationVector; + result._authenticationTag = encryptedFileInfo->authenticationTag; } return result; }); diff --git a/src/libsync/encryptedfoldermetadatahandler.cpp b/src/libsync/encryptedfoldermetadatahandler.cpp index 718092f0b8ed6..9beb1c41c2478 100644 --- a/src/libsync/encryptedfoldermetadatahandler.cpp +++ b/src/libsync/encryptedfoldermetadatahandler.cpp @@ -280,6 +280,12 @@ void EncryptedFolderMetadataHandler::unlockFolder(const UnlockFolderWithResult r unlockJob->start(); } +void EncryptedFolderMetadataHandler::repairMetadata(const QList &childItems, + OwncloudPropagator *propagator) +{ + _folderMetadata->repair(childItems, propagator); +} + void EncryptedFolderMetadataHandler::startUploadMetadata() { qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Metadata created, sending to the server."; diff --git a/src/libsync/encryptedfoldermetadatahandler.h b/src/libsync/encryptedfoldermetadatahandler.h index 0b039291b9b0d..b45e6445a3d95 100644 --- a/src/libsync/encryptedfoldermetadatahandler.h +++ b/src/libsync/encryptedfoldermetadatahandler.h @@ -10,6 +10,7 @@ #include "account.h" #include "rootencryptedfolderinfo.h" #include "common/syncjournaldb.h" +#include "foldermetadata.h" #include #include @@ -19,8 +20,10 @@ #include namespace OCC { -class FolderMetadata; + class SyncJournalDb; +class OwncloudPropagator; + // all metadata operations with server must be performed via this class class OWNCLOUDSYNC_EXPORT EncryptedFolderMetadataHandler : public QObject @@ -30,19 +33,20 @@ class OWNCLOUDSYNC_EXPORT EncryptedFolderMetadataHandler public: enum class FetchMode { NonEmptyMetadata = 0, - AllowEmptyMetadata + AllowEmptyMetadata, + AllowBrokenSignature, }; Q_ENUM(FetchMode); enum class UploadMode { DoNotKeepLock = 0, - KeepLock + KeepLock, }; Q_ENUM(UploadMode); enum class UnlockFolderWithResult { Success = 0, - Failure + Failure, }; Q_ENUM(UnlockFolderWithResult); @@ -66,6 +70,8 @@ class OWNCLOUDSYNC_EXPORT EncryptedFolderMetadataHandler void fetchMetadata(const FetchMode fetchMode = FetchMode::NonEmptyMetadata); void uploadMetadata(const UploadMode uploadMode = UploadMode::DoNotKeepLock); void unlockFolder(const UnlockFolderWithResult result = UnlockFolderWithResult::Success); + void repairMetadata(const QList &childItems, + OwncloudPropagator *propagator); private: void lockFolder(); diff --git a/src/libsync/foldermetadata.cpp b/src/libsync/foldermetadata.cpp index a4b7c6fdbb64d..069d8aa1fd8d0 100644 --- a/src/libsync/foldermetadata.cpp +++ b/src/libsync/foldermetadata.cpp @@ -8,7 +8,10 @@ #include "foldermetadata.h" #include "clientsideencryption.h" #include +#include "owncloudpropagator.h" + #include +#include #include #include #include @@ -77,6 +80,50 @@ bool FolderMetadata::EncryptedFile::isDirectory() const return mimetype.isEmpty() || mimetype == QByteArrayLiteral("inode/directory") || mimetype == QByteArrayLiteral("httpd/unix-directory"); } +bool FolderMetadata::EncryptedFile::isValid() const +{ + return !encryptionKey.isEmpty() && !initializationVector.isEmpty() && !originalFilename.isEmpty(); +} + +void FolderMetadata::EncryptedFile::initializeForNewItem(const QString &fileName, const QFileInfo &info) +{ + encryptionKey = EncryptionHelper::generateRandom(16); + encryptedFilename = EncryptionHelper::generateRandomFilename(); + originalFilename = fileName; + + QMimeDatabase mdb; + mimetype = mdb.mimeTypeForFile(info).name().toLocal8Bit(); + + // 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 (mimetype == QByteArrayLiteral("inode/directory")) { + mimetype = QByteArrayLiteral("httpd/unix-directory"); + } +} + +void FolderMetadata::EncryptedFile::initializeForRecovery(const QString &fileName, + const QString &encryptedFileName, + const QByteArray &existingEncryptionKey, + const QByteArray &existingInitializationVector, + const QByteArray &existingAuthenticationTag, + const QFileInfo &info) +{ + encryptionKey = existingEncryptionKey; + initializationVector = existingInitializationVector; + authenticationTag = existingAuthenticationTag; + encryptedFilename = encryptedFileName; + originalFilename = fileName; + + QMimeDatabase mdb; + mimetype = mdb.mimeTypeForFile(info).name().toLocal8Bit(); + + // 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 (mimetype == QByteArrayLiteral("inode/directory")) { + mimetype = QByteArrayLiteral("httpd/unix-directory"); + } +} + FolderMetadata::FolderMetadata(AccountPtr account, const QString &remoteFolderRoot, FolderType folderType) : _account(account), _remoteFolderRoot(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(remoteFolderRoot))), @@ -121,6 +168,8 @@ FolderMetadata::FolderMetadata(AccountPtr account, } } +FolderMetadata::~FolderMetadata() = default; + void FolderMetadata::initMetadata() { if (_initialMetadata.isEmpty()) { @@ -446,6 +495,37 @@ void FolderMetadata::setupExistingMetadataLegacy(const QByteArray &metadata) _isMetadataValid = true; } +void FolderMetadata::initMetadataFromClientState(const QList &childItems, + OwncloudPropagator *propagator) +{ + for (const auto &oneItem : childItems) { + auto newEncryptedItem = EncryptedFile{}; + auto fileInfo = QFileInfo{propagator->fullLocalPath(oneItem.originalFilename)}; + + if (fileInfo.isFile()) { + newEncryptedItem.initializeForRecovery(oneItem.originalFilename, oneItem.encryptedFilename, oneItem.encryptionKey, oneItem.initializationVector, oneItem.authenticationTag, fileInfo); + } else { + newEncryptedItem.originalFilename = oneItem.originalFilename; + newEncryptedItem.encryptedFilename = oneItem.encryptedFilename; + newEncryptedItem.mimetype = "httpd/unix-directory"_ba; + } + const auto result = addEncryptedFile(newEncryptedItem); + if (!result) { + qCWarning(lcCseMetadata()) << "Could not add encrypted file" << oneItem.originalFilename; + } + } + + const auto oldFolderUsers = _folderUsers; + for (const auto &oneUser : oldFolderUsers) { + if (!addUser(oneUser.userId, QSslCertificate{oneUser.certificatePem}, CertificateType::SoftwareNextcloudCertificate)) { + qCWarning(lcCseMetadata()) << "impossible to add former user into new metadata" << oneUser.userId; + _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + } + } + + _isMetadataValid = true; +} + FolderMetadata::MetadataVersion FolderMetadata::setupVersionFromExistingMetadata(const QByteArray &metadata) { auto resultVersion = FolderMetadata::MetadataVersion{}; @@ -603,22 +683,27 @@ FolderMetadata::EncryptedFile FolderMetadata::parseEncryptedFileFromJson(const Q return file; } -QJsonObject FolderMetadata::convertFileToJsonObject(const EncryptedFile *encryptedFile) const +QJsonObject FolderMetadata::convertFileToJsonObject(const EncryptedFile &encryptedFile) const { - if (!encryptedFile || !isOriginalFilenameValid(encryptedFile->originalFilename)) { + if (!isOriginalFilenameValid(encryptedFile.originalFilename)) { qCWarning(lcCseMetadata()) << "Metadata generation failed. Invalid original file name."; return {}; } + if (!encryptedFile.isValid()) { + qCWarning(lcCseMetadata()) << "Metadata generation failed. Invalid encryption metadata for file." << encryptedFile.originalFilename << encryptedFile.encryptedFilename; + return {}; + } + QJsonObject file; - file.insert("key", QString(encryptedFile->encryptionKey.toBase64())); - file.insert("filename", encryptedFile->originalFilename); - file.insert("mimetype", QString(encryptedFile->mimetype)); + 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())); + file.insert(nonceFinalKey, QString(encryptedFile.initializationVector.toBase64())); + file.insert(authenticationTagKey, QString(encryptedFile.authenticationTag.toBase64())); return file; } @@ -699,7 +784,7 @@ QByteArray FolderMetadata::encryptedMetadata() QJsonObject files, folders; for (auto it = _files.constBegin(), end = _files.constEnd(); it != end; ++it) { - const auto file = convertFileToJsonObject(&(*it)); + const auto &file = convertFileToJsonObject(*it); if (file.isEmpty()) { qCWarning(lcCseMetadata) << "Metadata generation failed for file" << it->encryptedFilename; return {}; @@ -946,6 +1031,12 @@ void FolderMetadata::updateSelfCertificate() } } +void FolderMetadata::repair(const QList &childItems, + OwncloudPropagator *propagator) +{ + initMetadataFromClientState(childItems, propagator); +} + quint64 FolderMetadata::newCounter() const { return _counter + 1; diff --git a/src/libsync/foldermetadata.h b/src/libsync/foldermetadata.h index b99dd5e559c77..476a2ab93c3a4 100644 --- a/src/libsync/foldermetadata.h +++ b/src/libsync/foldermetadata.h @@ -5,7 +5,6 @@ */ #include "accountfwd.h" -#include "encryptedfoldermetadatahandler.h" #include "csync.h" #include "rootencryptedfolderinfo.h" #include @@ -17,13 +16,20 @@ #include #include +class QFileInfo; 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 EncryptedFolderMetadataHandler; +class CertificateInformation; +class OwncloudPropagator; + +// 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; @@ -62,12 +68,32 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject public: struct EncryptedFile { QByteArray encryptionKey; - QByteArray mimetype; QByteArray initializationVector; QByteArray authenticationTag; + QByteArray mimetype; QString encryptedFilename; QString originalFilename; + [[nodiscard]] bool isDirectory() const; + + [[nodiscard]] bool isValid() const; + + void initializeForNewItem(const QString &fileName, const QFileInfo &info); + + void initializeForRecovery(const QString &fileName, + const QString &encryptedFileName, + const QByteArray &existingEncryptionKey, + const QByteArray &existingInitializationVector, + const QByteArray &existingAuthenticationTag, + const QFileInfo &info); + }; + + struct DatabaseEncryptedFile { + QString originalFilename; + QString encryptedFilename; + QByteArray encryptionKey; + QByteArray initializationVector; + QByteArray authenticationTag; }; enum class FolderType { @@ -106,6 +132,8 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject FolderType folderType, QObject *parent = nullptr); + ~FolderMetadata() override; + [[nodiscard]] QVector files() const; [[nodiscard]] bool isValid() const; @@ -146,6 +174,9 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject static MetadataVersion setupVersionFromExistingMetadata(const QByteArray &metadata); + void repair(const QList &childItems, + OwncloudPropagator *propagator); + public Q_SLOTS: [[nodiscard]] bool addEncryptedFile(const OCC::FolderMetadata::EncryptedFile &f); [[nodiscard]] bool removeEncryptedFile(const QString &originalFilename); @@ -170,7 +201,7 @@ public Q_SLOTS: [[nodiscard]] EncryptedFile parseEncryptedFileFromJson(const QString &encryptedFilename, const QJsonValue &fileJSON) const; - [[nodiscard]] QJsonObject convertFileToJsonObject(const EncryptedFile *encryptedFile) const; + [[nodiscard]] QJsonObject convertFileToJsonObject(const EncryptedFile &encryptedFile) const; [[nodiscard]] static bool isOriginalFilenameValid(const QString &originalFilename); @@ -193,6 +224,9 @@ private Q_SLOTS: void setupExistingMetadata(const QByteArray &metadata); void setupExistingMetadataLegacy(const QByteArray &metadata); + void initMetadataFromClientState(const QList &childItems, + OCC::OwncloudPropagator *propagator); + void startFetchRootE2eeFolderMetadata(const QString &path); void slotRootE2eeFolderMetadataReceived(int statusCode, const QString &message); diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 9a371bffa9f74..009cc3b42afde 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -119,18 +119,7 @@ void PropagateUploadEncrypted::slotFetchMetadataJobFinished(int statusCode, cons // New encrypted file so set it all up! if (!found) { - encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); - encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); - encryptedFile.originalFilename = fileName; - - QMimeDatabase mdb; - encryptedFile.mimetype = mdb.mimeTypeForFile(info).name().toLocal8Bit(); - - // 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.initializeForNewItem(fileName, info); } encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); diff --git a/src/libsync/repairfolderencryptionmetadatajob.cpp b/src/libsync/repairfolderencryptionmetadatajob.cpp new file mode 100644 index 0000000000000..52d2e5da09a81 --- /dev/null +++ b/src/libsync/repairfolderencryptionmetadatajob.cpp @@ -0,0 +1,76 @@ +#include "repairfolderencryptionmetadatajob.h" + +#include + +namespace OCC +{ + +Q_LOGGING_CATEGORY(lcRepairFolderEncryptionMetadataJob, "nextcloud.sync.clientsideencryption.repairjob", QtInfoMsg) + +RepairFolderEncryptionMetadataJob::RepairFolderEncryptionMetadataJob(const AccountPtr &account, + SyncJournalDb *journal, + const QString &path, + const QString &pathNonEncrypted, + const QString &remoteSyncRootPath, + const QByteArray &fileId, + OwncloudPropagator *propagator, + SyncFileItemPtr item, + QObject *parent) + : QObject{parent} + , _account{account} + , _journal{journal} + , _path{path} + , _pathNonEncrypted{pathNonEncrypted} + , _remoteSyncRootPath{remoteSyncRootPath} + , _fileId{fileId} + , _propagator{propagator} + , _item{item} +{ + SyncJournalFileRecord rec; + const auto currentPath = !_pathNonEncrypted.isEmpty() ? _pathNonEncrypted : _path; + const auto currentPathRelative = Utility::fullRemotePathToRemoteSyncRootRelative(currentPath, _remoteSyncRootPath); + const QString fullRemotePath = Utility::trailingSlashPath(Utility::noLeadingSlashPath(_remoteSyncRootPath)) + currentPathRelative; + [[maybe_unused]] const auto result = _journal->getRootE2eFolderRecord(Utility::fullRemotePathToRemoteSyncRootRelative(currentPath, _remoteSyncRootPath), &rec); + _encryptedFolderMetadataHandler.reset(new EncryptedFolderMetadataHandler(account, fullRemotePath, _remoteSyncRootPath, _journal, rec.path())); +} + +void RepairFolderEncryptionMetadataJob::start() +{ + auto childItems = QList{}; + const auto result = _journal->getFilesBelowPath(_path.toUtf8(), [this, &childItems] (const SyncJournalFileRecord &record) -> void { + const auto recordPath = QString::fromUtf8(record._path); + const auto recordRelativePath = recordPath.mid(_path.length()); + const auto isDirectChild = recordRelativePath.indexOf('/') == -1; + if (isDirectChild) { + childItems.emplace_back(record._path, record.e2eMangledName(), record._e2eFileEncryptionKey, record._initializationVector, record._authenticationTag); + } + }); + + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::uploadFinished, + this, &RepairFolderEncryptionMetadataJob::metadataUploadFinished); + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::fetchFinished, + this, [this, childItems] () { + _encryptedFolderMetadataHandler->repairMetadata(childItems, _propagator); + _encryptedFolderMetadataHandler->uploadMetadata(); + }); + + if (!result) { + qCWarning(lcRepairFolderEncryptionMetadataJob()) << "failed to fetch database records"; + } + + _encryptedFolderMetadataHandler->fetchMetadata(EncryptedFolderMetadataHandler::FetchMode::AllowBrokenSignature); +} + +QString RepairFolderEncryptionMetadataJob::errorString() const +{ + return _errorString; +} + +void RepairFolderEncryptionMetadataJob::metadataUploadFinished() +{ + Q_EMIT finished(static_cast(Status::Success)); +} + +} // namespace OCC + +#include "moc_repairfolderencryptionmetadatajob.cpp" \ No newline at end of file diff --git a/src/libsync/repairfolderencryptionmetadatajob.h b/src/libsync/repairfolderencryptionmetadatajob.h new file mode 100644 index 0000000000000..2f77318f7ac0f --- /dev/null +++ b/src/libsync/repairfolderencryptionmetadatajob.h @@ -0,0 +1,61 @@ +#ifndef REPAIRFOLDERENCRYPTIONMETADATAJOB_H +#define REPAIRFOLDERENCRYPTIONMETADATAJOB_H + +#include + +#include "accountfwd.h" +#include "syncfileitem.h" +#include "encryptedfoldermetadatahandler.h" + +namespace OCC +{ + +class SyncJournalDb; +class OwncloudPropagator; + +class OWNCLOUDSYNC_EXPORT RepairFolderEncryptionMetadataJob : public QObject +{ + Q_OBJECT +public: + enum class Status { + Success = 1, + Error = -1, + }; + Q_ENUM(Status) + + explicit RepairFolderEncryptionMetadataJob(const AccountPtr &account, + SyncJournalDb *journal, + const QString &path, + const QString &pathNonEncrypted, + const QString &remoteSyncRootPath, + const QByteArray &fileId, + OwncloudPropagator *propagator = nullptr, + SyncFileItemPtr item = {}, + QObject *parent = nullptr); + + void start(); + + [[nodiscard]] QString errorString() const; + +Q_SIGNALS: + void finished(int status); + +private Q_SLOTS: + void metadataUploadFinished(); + +private: + AccountPtr _account; + SyncJournalDb *_journal = nullptr; + QString _path; + QString _pathNonEncrypted; + QString _remoteSyncRootPath; + QByteArray _fileId; + QString _errorString; + OwncloudPropagator *_propagator = nullptr; + SyncFileItemPtr _item; + QScopedPointer _encryptedFolderMetadataHandler; +}; + +} // namespace OCC + +#endif // REPAIRFOLDERENCRYPTIONMETADATAJOB_H diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 0a7aa38dd1500..ad4affc5e64e6 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -504,7 +504,7 @@ void SyncEngine::startSync() const auto e2EeLockedFolders = _journal->e2EeLockedFolders(); - if (!e2EeLockedFolders.isEmpty()) { + if (_account->e2e()->isInitialized() && !e2EeLockedFolders.isEmpty()) { for (const auto &e2EeLockedFolder : e2EeLockedFolders) { const auto folderId = e2EeLockedFolder.first; qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId; diff --git a/src/libsync/syncengine.h b/src/libsync/syncengine.h index 7e7f08dd06f6c..8988225b4b43a 100644 --- a/src/libsync/syncengine.h +++ b/src/libsync/syncengine.h @@ -182,7 +182,7 @@ public Q_SLOTS: * This usually happen when the server was reset or something. * Set *cancel to true in a slot connected from this signal to abort the sync. */ - void aboutToRemoveAllFiles(OCC::SyncFileItem::Direction direction, std::function f); + void aboutToRemoveAllFiles(OCC::SyncFileItem::Direction direction, std::function f);// clazy:exclude=fully-qualified-moc-types // A new folder was discovered and was not synced because of the confirmation feature void newBigFolder(const QString &folder, bool isExternal); diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 45b24351efb90..4d0114b4f6514 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -113,6 +113,9 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); rec._e2eEncryptionStatus = EncryptionStatusEnums::toDbEncryptionStatus(_e2eEncryptionStatus); + rec._e2eFileEncryptionKey = _e2eFileEncryptionKey; + rec._initializationVector = _initializationVector; + rec._authenticationTag = _authenticationTag; rec._lockstate._locked = _locked == LockStatus::LockedItem; rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName; rec._lockstate._lockOwnerId = _lockOwnerId; @@ -155,6 +158,9 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_encryptedFileName = rec.e2eMangledName(); item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(rec._e2eEncryptionStatus); item->_e2eEncryptionServerCapability = item->_e2eEncryptionStatus; + item->_e2eFileEncryptionKey = rec._e2eFileEncryptionKey; + item->_initializationVector = rec._initializationVector; + item->_authenticationTag = rec._authenticationTag; item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem; item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName; item->_lockOwnerId = rec._lockstate._lockOwnerId; @@ -273,7 +279,7 @@ void SyncFileItem::updateLockStateFromDbRecord(const SyncJournalFileRecord &dbRe SyncJournalFileRecord SyncFileItem::fromSyncFileItem(const SyncFileItem &syncFile) { - SyncJournalFileRecord rec(syncFile.destination().toUtf8(), {}, {}, syncFile._type, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}); + SyncJournalFileRecord rec(syncFile.destination().toUtf8(), {}, {}, syncFile._type, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}); rec._modtime = syncFile._modtime; rec._type = syncFile._type; diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 50ffbd00faef5..d26c0054b1aeb 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -258,6 +258,10 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem /// the encrypted name on the server. QString _encryptedFileName; + QByteArray _e2eFileEncryptionKey; + QByteArray _initializationVector; + QByteArray _authenticationTag; + ItemType _type BITFIELD(3); Direction _direction BITFIELD(3); bool _serverHasIgnoredFiles BITFIELD(1); diff --git a/src/libsync/updatee2eefoldermetadatajob.h b/src/libsync/updatee2eefoldermetadatajob.h index 76a6e749fba8c..f5c9d6bfd06f9 100644 --- a/src/libsync/updatee2eefoldermetadatajob.h +++ b/src/libsync/updatee2eefoldermetadatajob.h @@ -37,7 +37,7 @@ private Q_SLOTS: void unlockFolder(const OCC::EncryptedFolderMetadataHandler::UnlockFolderWithResult result); Q_SIGNALS: - void fileDropMetadataParsedAndAdjusted(const OCC::FolderMetadata *const metadata); + void fileDropMetadataParsedAndAdjusted(const OCC::FolderMetadata *metadata); private: SyncFileItemPtr _item; diff --git a/test/testclientsideencryptionv2.cpp b/test/testclientsideencryptionv2.cpp index bbdb1f7cc3a4f..f65c0eb65b3d5 100644 --- a/test/testclientsideencryptionv2.cpp +++ b/test/testclientsideencryptionv2.cpp @@ -5,6 +5,8 @@ #include "syncenginetestutils.h" #include "clientsideencryption.h" #include "foldermetadata.h" +#include "encryptedfoldermetadatahandler.h" + #include using namespace OCC; diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp index 4011196f14bc6..a55b792801f06 100644 --- a/test/testsecurefiledrop.cpp +++ b/test/testsecurefiledrop.cpp @@ -5,6 +5,8 @@ #include "syncenginetestutils.h" #include "clientsideencryption.h" #include "foldermetadata.h" +#include "encryptedfoldermetadatahandler.h" + #include #include