/* * 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 &folderFullRemotePath, const QString &remoteFolderRoot, SyncJournalDb *const journalDb, const QString &pathForTopLevelFolder, QObject *parent) : QObject(parent) , _account(account) , _journalDb(journalDb) , _folderFullRemotePath(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(folderFullRemotePath))) , _remoteFolderRoot(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(remoteFolderRoot))) { Q_ASSERT(!_remoteFolderRoot.isEmpty()); Q_ASSERT(_remoteFolderRoot == QStringLiteral("/") || _folderFullRemotePath.startsWith(_remoteFolderRoot)); const auto folderRelativePath = Utility::fullRemotePathToRemoteSyncRootRelative(_folderFullRemotePath, _remoteFolderRoot); _rootEncryptedFolderInfo = RootEncryptedFolderInfo(RootEncryptedFolderInfo::createRootPath(folderRelativePath, pathForTopLevelFolder)); } void EncryptedFolderMetadataHandler::fetchMetadata(const FetchMode fetchMode) { _fetchMode = fetchMode; fetchFolderEncryptedId(); } void EncryptedFolderMetadataHandler::fetchMetadata(const RootEncryptedFolderInfo &rootEncryptedFolderInfo, const FetchMode fetchMode) { Q_ASSERT(!rootEncryptedFolderInfo.path.isEmpty()); if (rootEncryptedFolderInfo.path.isEmpty()) { qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Error fetching metadata for" << _folderFullRemotePath << ". Invalid rootEncryptedFolderInfo!"; emit fetchFinished(-1, tr("Error fetching metadata.")); return; } _rootEncryptedFolderInfo = rootEncryptedFolderInfo; if (_rootEncryptedFolderInfo.path.isEmpty()) { qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Error fetching metadata for" << _folderFullRemotePath << ". Invalid _rootEncryptedFolderInfo!"; emit fetchFinished(-1, tr("Error fetching metadata.")); return; } if (_remoteFolderRoot != QStringLiteral("/") && !_folderFullRemotePath.startsWith(_remoteFolderRoot)) { qCWarning(lcFetchAndUploadE2eeFolderMetadataJob) << "Error fetching metadata for" << _folderFullRemotePath << " and remote root" << _remoteFolderRoot << ". Invalid _remoteFolderRoot or _folderFullRemotePath!"; 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, _account->e2e()->certificateSha256Fingerprint(), _journalDb, _account->e2e()->getPublicKey(), 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, _folderFullRemotePath); 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" << _folderFullRemotePath; emit fetchFinished(statusCode, tr("Error fetching metadata.")); return; } const auto rawMetadata = statusCode == 404 ? QByteArray{} : json.toJson(QJsonDocument::Compact); const auto metadata(QSharedPointer::create(_account, _remoteFolderRoot, rawMetadata, _rootEncryptedFolderInfo, job->signature())); connect(metadata.data(), &FolderMetadata::setupComplete, this, [this, metadata] { if (!metadata->isValid()) { qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Error parsing or decrypting metadata for folder" << _folderFullRemotePath; 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, unlockJob, [this](const QByteArray &folderId) { qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Successfully Unlocked"; _isFolderLocked = false; emit folderUnlocked(folderId, 200); _isUnlockRunning = false; }); connect(unlockJob, &UnlockEncryptFolderApiJob::error, unlockJob, [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; } }