nextcloud-desktop/src/libsync/encryptedfoldermetadatahandler.cpp
2024-03-20 00:54:27 +01:00

395 lines
16 KiB
C++

/*
* Copyright (C) 2023 by Oleksandr Zolotov <alex@nextcloud.com>
*
* 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 <QLoggingCategory>
#include <QNetworkReply>
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, _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, _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<LsColJob *>(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<GetMetadataApiJob *>(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<FolderMetadata>::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, [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<FolderMetadata> EncryptedFolderMetadataHandler::folderMetadata() const
{
return _folderMetadata;
}
void EncryptedFolderMetadataHandler::setPrefetchedMetadataAndId(const QSharedPointer<FolderMetadata> &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;
}
}