Merge pull request #6350 from nextcloud/feature/e2ee-v2-foldersharing

Feature/e2ee v2 foldersharing
This commit is contained in:
Matthieu Gallien 2024-01-29 16:36:13 +01:00 committed by GitHub
commit 8cf4dee510
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
75 changed files with 5424 additions and 1998 deletions

View file

@ -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())

View file

@ -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

View file

@ -107,6 +107,7 @@ public:
GetE2EeLockedFolderQuery,
GetE2EeLockedFoldersQuery,
DeleteE2EeLockedFolderQuery,
ListAllTopLevelE2eeFoldersStatusLessThanQuery,
PreparedQueryCount
};

View file

@ -1030,6 +1030,108 @@ Result<void, QString> 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<void(const SyncJournalFileRecord &)> &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);

View file

@ -70,6 +70,9 @@ public:
[[nodiscard]] bool getFilesBelowPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
[[nodiscard]] bool listFilesInPath(const QByteArray &path, const std::function<void(const SyncJournalFileRecord&)> &rowCallback);
[[nodiscard]] Result<void, QString> setFileRecord(const SyncJournalFileRecord &record);
[[nodiscard]] bool getRootE2eFolderRecord(const QString &remoteFolderPath, SyncJournalFileRecord *rec);
[[nodiscard]] bool listAllE2eeFoldersWithEncryptionStatusLessThan(const int status, const std::function<void(const SyncJournalFileRecord &)> &rowCallback);
[[nodiscard]] bool findEncryptedAncestorForRecord(const QString &filename, SyncJournalFileRecord *rec);
void keyValueStoreSet(const QString &key, QVariant value);
[[nodiscard]] qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);

View file

@ -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);
}
}

View file

@ -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);

View file

@ -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

View file

@ -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()

View file

@ -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;

View file

@ -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 @@ public:
[[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 @@ signals:
void publicLinkSharesEnabledChanged();
void userGroupSharingEnabledChanged();
void sharePermissionsChanged();
void isShareDisabledEncryptedFolderChanged();
void lockExpireStringChanged();
void fetchOngoingChanged();
void hasInitialShareFetchCompletedChanged();
@ -141,7 +144,7 @@ signals:
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:
bool _hasInitialShareFetchCompleted = false;
bool _sharePermissionsChangeInProgress = false;
bool _hideDownloadEnabledChangeInProgress = false;
bool _isShareDisabledEncryptedFolder = false;
SharePtr _placeholderLinkShare;
SharePtr _internalLinkShare;
SharePtr _secureFileDropPlaceholderLinkShare;
QPointer<AccountState> _accountState;
QPointer<Folder> _folder;
QPointer<Folder> _synchronizationFolder;
QString _localPath;
QString _sharePath;
@ -240,6 +245,7 @@ private:
SharedItemType _sharedItemType = SharedItemType::SharedItemTypeUndefined;
SyncJournalFileLockInfo _filelockState;
QString _privateLinkUrl;
QByteArray _fileRemoteId;
QSharedPointer<ShareManager> _manager;

View file

@ -27,6 +27,7 @@
#include "gui/systray.h"
#include <pushnotifications.h>
#include <syncengine.h>
#include "updatee2eefolderusersmetadatajob.h"
#ifdef Q_OS_MAC
#include <CoreServices/CoreServices.h>
@ -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<Folder *> &folders,
SyncResult::Status *status, bool *unresolvedConflicts)
{

View file

@ -16,6 +16,7 @@
#ifndef FOLDERMAN_H
#define FOLDERMAN_H
#include <QByteArray>
#include <QObject>
#include <QQueue>
#include <QList>
@ -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:
QScopedPointer<SocketApi> _socketApi;
NavigationPaneHelper _navigationPaneHelper;
QPointer<UpdateE2eeFolderUsersMetadataJob> _removeE2eeShareJob;
bool _appRestartRequired = false;
static FolderMan *_instance;

View file

@ -17,6 +17,8 @@
#include "account.h"
#include "folderman.h"
#include "accountstate.h"
#include "clientsideencryption.h"
#include "updatee2eefolderusersmetadatajob.h"
#include <QUrl>
#include <QJsonDocument>
@ -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<UpdateE2eeFolderUsersMetadataJob *>(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);
}
}
}

View file

@ -67,6 +67,8 @@ public:
using Permissions = SharePermissions;
Q_ENUM(Permissions);
/*
* Constructor for shares
*/
@ -411,6 +413,23 @@ public:
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<LinkShare> parseLinkShare(const QJsonObject &data);
QSharedPointer<UserGroupShare> parseUserGroupShare(const QJsonObject &data);

View file

@ -36,5 +36,6 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(SharePermissions)
} // namespace OCC
Q_DECLARE_METATYPE(OCC::SharePermission)
Q_DECLARE_METATYPE(OCC::SharePermissions)
#endif

View file

@ -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,

View file

@ -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)

View file

@ -1,203 +0,0 @@
/*
* Copyright (C) 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 <QFileInfo>
#include <QLoggingCategory>
#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<LsColJob *>(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<DeleteJob *>(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

View file

@ -0,0 +1,207 @@
/*
* Copyright (C) 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 <QFileInfo>
#include <QLoggingCategory>
#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<DeleteJob *>(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<FolderMetadata> 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

View file

@ -17,22 +17,22 @@
#include <QObject>
#include <QString>
#include <QNetworkReply>
#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 @@ protected:
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> 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<OwncloudPropagator> _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> _encryptedFolderMetadataHandler;
};
}

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
#ifndef CLIENTSIDEENCRYPTION_H
#define CLIENTSIDEENCRYPTION_H
#include "clientsideencryptionprimitives.h"
#include <QString>
#include <QObject>
#include <QJsonDocument>
@ -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 @@ private:
class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject {
Q_OBJECT
public:
class PKey;
ClientSideEncryption();
QByteArray _privateKey;
@ -136,10 +142,20 @@ signals:
void certificateDeleted();
void mnemonicDeleted();
void publicKeyDeleted();
void certificateFetchedFromKeychain(QSslCertificate certificate);
void certificatesFetchedFromServer(const QHash<QString, QSslCertificate> &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<QByteArray> &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:
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<EncryptedFile> 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<EncryptedFile> _files;
AccountPtr _account;
RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2;
QVector<QPair<QString, QString>> _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

View file

@ -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)
{
}

View file

@ -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 @@ private:
QByteArray _fileId;
QPointer<SyncJournalDb> _journalDb;
QSslKey _publicKey;
quint64 _counter = 0;
};
@ -177,8 +180,11 @@ public:
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 @@ private:
QByteArray _token;
QBuffer *_tokenBuf = nullptr;
QPointer<SyncJournalDb> _journalDb;
bool _shouldRollbackMetadataChanges = false;
};
@ -205,7 +212,9 @@ public:
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 @@ signals:
private:
QByteArray _fileId;
QByteArray _token;
QByteArray _b64Metadata;
QByteArray _signature;
};
class OWNCLOUDSYNC_EXPORT UpdateMetadataApiJob : public AbstractNetworkJob
@ -232,6 +243,7 @@ public:
const QByteArray& fileId,
const QByteArray& b64Metadata,
const QByteArray& lockedToken,
const QByteArray &signature,
QObject *parent = nullptr);
public slots:
@ -248,6 +260,7 @@ private:
QByteArray _fileId;
QByteArray _b64Metadata;
QByteArray _token;
QByteArray _signature;
};
@ -260,6 +273,8 @@ public:
const QByteArray& fileId,
QObject *parent = nullptr);
[[nodiscard]] const QByteArray &signature() const;
public slots:
void start() override;
@ -272,6 +287,7 @@ signals:
private:
QByteArray _fileId;
QByteArray _signature;
};
class OWNCLOUDSYNC_EXPORT DeleteMetadataApiJob : public AbstractNetworkJob
@ -281,6 +297,7 @@ public:
explicit DeleteMetadataApiJob (
const AccountPtr &account,
const QByteArray& fileId,
const QByteArray& token,
QObject *parent = nullptr);
public slots:
@ -295,6 +312,7 @@ signals:
private:
QByteArray _fileId;
QByteArray _token;
};
}

View file

@ -0,0 +1,107 @@
/*
* 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 "clientsideencryptionprimitives.h"
#include <openssl/pem.h>
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;
}
}

View file

@ -0,0 +1,92 @@
/*
* Copyright (C) 2024 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.
*/
#pragma once
#include <openssl/evp.h>
#include <QtCore/qglobal.h>
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;
};
}

View file

@ -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--;

View file

@ -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<QString> &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<QString, QString> &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 job = qobject_cast<GetMetadataApiJob *>(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 encryptedFiles = metadata.files();
// 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;
}
}
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;
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;
}
_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<FolderMetadata::EncryptedFile>();
} else {
return Optional<FolderMetadata::EncryptedFile>(*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;
});
if (it == std::cend(encryptedFiles)) {
return Optional<EncryptedFile>();
} else {
return Optional<EncryptedFile>(*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)

View file

@ -132,6 +132,7 @@ private:
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<QString> &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<RemoteInfo> _results;
QString _subPath;
@ -182,14 +191,18 @@ private:
// 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> _lsColJob;
// store top level E2EE folder paths as they are used later when discovering nested folders
QSet<QString> _topLevelE2eeFolderPaths;
public:
QByteArray _dataFingerprint;
};
@ -323,6 +336,8 @@ public:
bool _noCaseConflictRecordsInDb = false;
QSet<QString> _topLevelE2eeFolderPaths;
signals:
void fatalError(const QString &errorString, const OCC::ErrorCategory errorCategory);
void itemDiscovered(const OCC::SyncFileItemPtr &item);

View file

@ -0,0 +1,376 @@
/*
* 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 &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<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" << _folderPath;
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, 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<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;
}
}

View file

@ -0,0 +1,122 @@
/*
* 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.
*/
#pragma once
#include "account.h"
#include "rootencryptedfolderinfo.h"
#include <QHash>
#include <QMutex>
#include <QObject>
#include <QSslCertificate>
#include <QString>
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> folderMetadata() const;
// use this when metadata is already fetched so no fetching will happen in this class
void setPrefetchedMetadataAndId(const QSharedPointer<FolderMetadata> &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<SyncJournalDb> _journalDb;
QByteArray _folderId;
QByteArray _folderToken;
QSharedPointer<FolderMetadata> _folderMetadata;
RootEncryptedFolderInfo _rootEncryptedFolderInfo;
int _uploadErrorCode = 200;
FetchMode _fetchMode = FetchMode::NonEmptyMetadata;
bool _isFolderLocked = false;
bool _isUnlockRunning = false;
bool _isNewMetadataCreated = false;
UploadMode _uploadMode = UploadMode::DoNotKeepLock;
};
}

View file

@ -16,23 +16,30 @@
#include "common/syncjournaldb.h"
#include "clientsideencryptionjobs.h"
#include "foldermetadata.h"
#include <QLoggingCategory>
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();
const auto emptyMetadata(QSharedPointer<FolderMetadata>::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::slotUploadMetadataSuccess(const QByteArray &folderId)
void EncryptFolderJob::slotUploadMetadataFinished(int statusCode, const QString &message)
{
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();
}
void EncryptFolderJob::slotLockForEncryptionError(const QByteArray &fileId,
const int httpErrorCode,
const QString &errorMessage)
{
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());
}
}

View file

@ -13,9 +13,12 @@
*/
#pragma once
#include <QObject>
#include "account.h"
#include "encryptedfoldermetadatahandler.h"
#include "syncfileitem.h"
#include "owncloudpropagator.h"
#include <QObject>
namespace OCC {
class SyncJournalDb;
@ -30,30 +33,41 @@ public:
};
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> _encryptedFolderMetadataHandler;
};
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,245 @@
#pragma once
/*
* Copyright (C) 2024 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 "accountfwd.h"
#include "encryptedfoldermetadatahandler.h"
#include "csync.h"
#include "rootencryptedfolderinfo.h"
#include <QByteArray>
#include <QHash>
#include <QJsonObject>
#include <QObject>
#include <QSet>
#include <QSslKey>
#include <QString>
#include <QVector>
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<EncryptedFile> 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<QByteArray> &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<QByteArray> _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<int, QByteArray> _metadataKeys;
// users that have access to current folder's "ciphertext", except "filedrop" part
QHash<QString, UserWithFolderAccess> _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<EncryptedFile> _files;
// parsed filedrop entries ready for move
QVector<FileDropEntry> _fileDropEntries;
// sets to "true" on successful parse
bool _isMetadataValid = false;
QScopedPointer<EncryptedFolderMetadataHandler> _encryptedFolderMetadataHandler;
};
} // namespace OCC

View file

@ -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 <windef.h>
@ -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<QPair<QString, PropagateDirectory *>> &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<QString, PropagateDirectory *> 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<UpdateMigratedE2eeMetadataJob *>(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)
{

View file

@ -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 @@ public:
QString &removedDirectory,
QString &maybeConflictDirectory);
void processE2eeMetadataMigration(const SyncFileItemPtr &item, QStack<QPair<QString, PropagateDirectory *>> &directories);
[[nodiscard]] const SyncOptions &syncOptions() const;
void setSyncOptions(const SyncOptions &syncOptions);

View file

@ -379,7 +379,7 @@ QString GETFileJob::errorString() const
GETEncryptedFileJob::GETEncryptedFileJob(AccountPtr account, const QString &path, QIODevice *device,
const QMap<QByteArray, QByteArray> &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<QByteArray, QByteArray> &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)
{

View file

@ -18,6 +18,7 @@
#include "networkjobs.h"
#include "clientsideencryption.h"
#include <common/checksums.h>
#include "foldermetadata.h"
#include <QBuffer>
#include <QFile>
@ -136,10 +137,10 @@ public:
// DOES NOT take ownership of the device.
explicit GETEncryptedFileJob(AccountPtr account, const QString &path, QIODevice *device,
const QMap<QByteArray, QByteArray> &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<QByteArray, QByteArray> &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 @@ protected:
private:
QSharedPointer<EncryptionHelper::StreamingDecryptor> _decryptor;
EncryptedFile _encryptedFileInfo = {};
FolderMetadata::EncryptedFile _encryptedFileInfo = {};
QByteArray _pendingBytes;
qint64 _processedSoFar = 0;
};
@ -253,7 +254,7 @@ private:
QFile _tmpFile;
bool _deleteExisting = false;
bool _isEncrypted = false;
EncryptedFile _encryptedInfo;
FolderMetadata::EncryptedFile _encryptedInfo;
ConflictRecord _conflictRecord;
QElapsedTimer _stopwatch;

View file

@ -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,73 +26,64 @@ 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<LsColJob*>(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();
const auto files = metadata->files();
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();
}
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<EncryptedFile> 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;
}
// TODO: Fix this. Exported in the wrong place.
QString createDownloadTmpFileName(const QString &previous);
@ -133,4 +122,4 @@ QString PropagateDownloadEncrypted::errorString() const
return _errorString;
}
}
}

View file

@ -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 @@ public:
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 @@ private:
QString _localParentPath;
SyncFileItemPtr _item;
QFileInfo _info;
EncryptedFile _encryptedInfo;
FolderMetadata::EncryptedFile _encryptedInfo;
QString _errorString;
QString _remoteParentPath;
QString _parentPathInDb;
QScopedPointer<EncryptedFolderMetadataHandler> _encryptedFolderMetadataHandler;
};
}

View file

@ -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) {

View file

@ -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<DeleteJob> _job;
AbstractPropagateRemoteDeleteEncrypted *_deleteEncryptedHelper = nullptr;
BasePropagateRemoteDeleteEncrypted *_deleteEncryptedHelper = nullptr;
public:
PropagateRemoteDelete(OwncloudPropagator *propagator, const SyncFileItemPtr &item)

View file

@ -14,6 +14,7 @@
#include "propagateremotedeleteencrypted.h"
#include "clientsideencryptionjobs.h"
#include "foldermetadata.h"
#include "owncloudpropagator.h"
#include "encryptfolderjob.h"
#include <QLoggingCategory>
@ -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<EncryptedFile> files = metadata.files();
for (const EncryptedFile &file : files) {
const QVector<FolderMetadata::EncryptedFile> 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.";
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();
uploadMetadata(EncryptedFolderMetadataHandler::UploadMode::KeepLock);
}
void PropagateRemoteDeleteEncrypted::slotUpdateMetadataJobFinished(int statusCode, const QString &message)
{
Q_UNUSED(statusCode);
Q_UNUSED(message);
deleteRemoteItem(_item->_encryptedFileName);
}

View file

@ -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 @@ public:
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;
};
}

View file

@ -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);

View file

@ -16,12 +16,12 @@
#include <QMap>
#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 @@ public:
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);

View file

@ -21,6 +21,7 @@
#include "common/asserts.h"
#include "encryptfolderjob.h"
#include "filesystem.h"
#include "csync/csync.h"
#include <QFile>
#include <QLoggingCategory>
@ -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();
}

View file

@ -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:

View file

@ -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);

View file

@ -2,8 +2,9 @@
#include "clientsideencryptionjobs.h"
#include "networkjobs.h"
#include "clientsideencryption.h"
#include "foldermetadata.h"
#include "encryptedfoldermetadatahandler.h"
#include "account.h"
#include <QFileInfo>
#include <QDir>
#include <QUrl>
@ -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();
}
/* 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)
{
qCDebug(lcPropagateUploadEncrypted) << "Received id of folder, trying to lock it so we can prepare the metadata";
auto job = qobject_cast<LsColJob *>(sender());
const auto& folderInfo = job->_folderInfos.value(list.first());
_folderLockFirstTry.start();
slotTryLock(folderInfo.fileId);
}
void PropagateUploadEncrypted::slotTryLock(const QByteArray& fileId)
{
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();
}
void PropagateUploadEncrypted::slotFolderLockedSuccessfully(const QByteArray& fileId, const QByteArray& token)
{
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();
}
void PropagateUploadEncrypted::slotFolderEncryptedMetadataError(const QByteArray& fileId, int httpReturnCode)
{
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);
}
void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDocument &json, int statusCode)
{
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<EncryptedFile> files = _metadata->files();
for(const EncryptedFile &file : files) {
if (file.originalFilename == fileName) {
encryptedFile = file;
found = true;
}
}
// 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.initializationVector = EncryptionHelper::generateRandom(16);
_item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename;
_item->_e2eEncryptionStatus = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2;
qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file.";
if (info.isDir()) {
_completeFileName = encryptedFile.encryptedFilename;
} else {
QFile input(info.absoluteFilePath());
QFile output(QDir::tempPath() + QDir::separator() + encryptedFile.encryptedFilename);
QByteArray tag;
bool encryptionResult = EncryptionHelper::fileEncryption(
encryptedFile.encryptionKey,
encryptedFile.initializationVector,
&input, &output, tag);
if (!encryptionResult) {
qCDebug(lcPropagateUploadEncrypted()) << "There was an error encrypting the file, aborting upload.";
connect(this, &PropagateUploadEncrypted::folderUnlocked, this, &PropagateUploadEncrypted::error);
unlockFolder();
// 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()));
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();
}
connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::fetchFinished,
this, &PropagateUploadEncrypted::slotFetchMetadataJobFinished);
_encryptedFolderMetadataHandler->fetchMetadata(EncryptedFolderMetadataHandler::FetchMode::AllowEmptyMetadata);
}
void PropagateUploadEncrypted::slotUpdateMetadataSuccess(const QByteArray& fileId)
void PropagateUploadEncrypted::unlockFolder()
{
Q_UNUSED(fileId);
connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &PropagateUploadEncrypted::folderUnlocked);
_encryptedFolderMetadataHandler->unlockFolder();
}
bool PropagateUploadEncrypted::isUnlockRunning() const
{
return _encryptedFolderMetadataHandler->isUnlockRunning();
}
bool PropagateUploadEncrypted::isFolderLocked() const
{
return _encryptedFolderMetadataHandler->isFolderLocked();
}
const QByteArray PropagateUploadEncrypted::folderToken() const
{
return _encryptedFolderMetadataHandler ? _encryptedFolderMetadataHandler->folderToken() : QByteArray{};
}
void PropagateUploadEncrypted::slotFetchMetadataJobFinished(int statusCode, const QString &message)
{
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();
QFileInfo info(_propagator->fullLocalPath(_item->_file));
const QString fileName = info.fileName();
// Find existing metadata for this file
bool found = false;
FolderMetadata::EncryptedFile encryptedFile;
const QVector<FolderMetadata::EncryptedFile> files = metadata->files();
for (const FolderMetadata::EncryptedFile &file : files) {
if (file.originalFilename == fileName) {
encryptedFile = file;
found = true;
}
}
// 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.initializationVector = EncryptionHelper::generateRandom(16);
_item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename;
_item->_e2eEncryptionStatusRemote = metadata->existingMetadataEncryptionStatus();
_item->_e2eEncryptionServerCapability =
EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_propagator->account()->capabilities().clientSideEncryptionVersion());
qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file.";
if (info.isDir()) {
_completeFileName = encryptedFile.encryptedFilename;
} else {
QFile input(info.absoluteFilePath());
QFile output(QDir::tempPath() + QDir::separator() + encryptedFile.encryptedFilename);
QByteArray tag;
bool encryptionResult = EncryptionHelper::fileEncryption(encryptedFile.encryptionKey, encryptedFile.initializationVector, &input, &output, tag);
if (!encryptionResult) {
qCDebug(lcPropagateUploadEncrypted()) << "There was an error encrypting the file, aborting upload.";
emit error();
return;
}
encryptedFile.authenticationTag = tag;
_completeFileName = output.fileName();
}
qCDebug(lcPropagateUploadEncrypted) << "Creating the metadata for the encrypted file.";
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::slotUploadMetadataFinished(int statusCode, const QString &message)
{
if (statusCode != 200) {
qCDebug(lcPropagateUploadEncrypted) << "Update metadata error for folder" << _encryptedFolderMetadataHandler->folderId() << "with error" << message;
qCDebug(lcPropagateUploadEncrypted()) << "Unlocking the folder.";
emit error();
return;
}
qCDebug(lcPropagateUploadEncrypted) << "Uploading of the metadata success, Encrypting the file";
QFileInfo outputInfo(_completeFileName);
qCDebug(lcPropagateUploadEncrypted) << "Encrypted Info:" << outputInfo.path() << outputInfo.fileName() << outputInfo.size();
qCDebug(lcPropagateUploadEncrypted) << "Finalizing the upload part, now the actual uploader will take over";
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());
}
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();
}
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;
}
// 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);
});
qCDebug(lcPropagateUploadEncrypted) << "Folder" << fileId << "Coundn't be locked.";
}
void PropagateUploadEncrypted::slotFolderEncryptedIdError(QNetworkReply *r)
{
Q_UNUSED(r);
qCDebug(lcPropagateUploadEncrypted) << "Error retrieving the Id of the encrypted folder.";
}
void PropagateUploadEncrypted::unlockFolder()
{
ASSERT(!_isUnlockRunning);
if (_isUnlockRunning) {
qWarning() << "Double-call to unlockFolder.";
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";
emit folderUnlocked(folderId, httpStatus);
_isUnlockRunning = false;
});
unlockJob->start();
}
} // namespace OCC
} // namespace OCC

View file

@ -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 @@ public:
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:
QString _remoteParentPath;
SyncFileItemPtr _item;
QByteArray _folderToken;
QByteArray _folderId;
QElapsedTimer _folderLockFirstTry;
bool _currentLockingInProgress = false;
@ -77,9 +68,10 @@ private:
QByteArray _generatedKey;
QByteArray _generatedIv;
QScopedPointer<FolderMetadata> _metadata;
EncryptedFile _encryptedFile;
QString _completeFileName;
QString _remoteParentAbsolutePath;
QScopedPointer<EncryptedFolderMetadataHandler> _encryptedFolderMetadataHandler;
};

View file

@ -0,0 +1,54 @@
/*
* 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"
namespace OCC
{
RootEncryptedFolderInfo::RootEncryptedFolderInfo()
{
*this = RootEncryptedFolderInfo::makeDefault();
}
RootEncryptedFolderInfo::RootEncryptedFolderInfo(const QString &remotePath,
const QByteArray &encryptionKey,
const QByteArray &decryptionKey,
const QSet<QByteArray> &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 &currentPath, 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();
}
}

View file

@ -0,0 +1,44 @@
#pragma once
/*
* 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 <QByteArray>
#include <QSet>
#include <QString>
#include <csync.h>
#include <owncloudlib.h>
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<QByteArray> &checksums = {},
const quint64 counter = 0);
static RootEncryptedFolderInfo makeDefault();
static QString createRootPath(const QString &currentPath, 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<QByteArray> keyChecksums;
quint64 counter = 0;
[[nodiscard]] bool keysSet() const;
};
} // namespace OCC

View file

@ -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<int>(currentEncryptionStatus), [this](const SyncJournalFileRecord &record) {
_journal->schedulePathForRemoteDiscovery(record.path());
});
s_anySyncRunning = true;
_syncRunning = true;

View file

@ -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"));

View file

@ -284,6 +284,8 @@ public:
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

View file

@ -0,0 +1,176 @@
/*
* 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 "updatee2eefoldermetadatajob.h"
#include "account.h"
#include "clientsideencryption.h"
#include "foldermetadata.h"
#include <QLoggingCategory>
#include <QNetworkReply>
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);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright (C) 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.
*/
#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 <QScopedPointer>
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> _encryptedFolderMetadataHandler;
};
}

View file

@ -0,0 +1,374 @@
/*
* 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 "account.h"
#include "updatee2eefolderusersmetadatajob.h"
#include "foldermetadata.h"
#include "common/syncjournalfilerecord.h"
#include "common/syncjournaldb.h"
#include <QSslCertificate>
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<UpdateE2eeFolderUsersMetadataJob *>(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<QString, QSslCertificate> &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<QByteArray> &keyChecksums)
{
_keyChecksums = keyChecksums;
}
void UpdateE2eeFolderUsersMetadataJob::setSubJobSyncItems(const QHash<QString, SyncFileItemPtr> &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();
}
}

View file

@ -0,0 +1,111 @@
/*
* 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.
*/
#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 <QHash>
#include <QMutex>
#include <QObject>
#include <QScopedPointer>
#include <QString>
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<QByteArray> &keyChecksums);
void setSubJobSyncItems(const QHash<QString, SyncFileItemPtr> &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<QString, QSslCertificate> &results);
void slotCertificateFetchedFromKeychain(const QSslCertificate &certificate);
private: signals:
void certificateReady();
void finished(int code, const QString &message = {});
void folderUnlocked();
private:
AccountPtr _account;
QPointer<SyncJournalDb> _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<QByteArray> _keyChecksums;
//-------------------------------------------------------------------------------------------------
QSet<UpdateE2eeFolderUsersMetadataJob *> _subJobs;
UserData _userData; // share info, etc.
QHash<QString, SyncFileItemPtr> _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> _encryptedFolderMetadataHandler;
bool _keepLock = false;
};
}

View file

@ -1,214 +0,0 @@
/*
* Copyright (C) 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 "updatefiledropmetadata.h"
#include "account.h"
#include "clientsideencryptionjobs.h"
#include "clientsideencryption.h"
#include "syncfileitem.h"
#include <QLoggingCategory>
#include <QNetworkReply>
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<LsColJob *>(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();
}
}

View file

@ -1,69 +0,0 @@
/*
* Copyright (C) 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.
*/
#pragma once
#include "owncloudpropagator.h"
#include <QScopedPointer>
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<FolderMetadata> _metadata;
};
}

View file

@ -0,0 +1,96 @@
/*
* 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 "updatemigratede2eemetadatajob.h"
#include "updatee2eefolderusersmetadatajob.h"
#include "account.h"
#include "syncfileitem.h"
#include <QLoggingCategory>
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);
}
}

View file

@ -0,0 +1,50 @@
/*
* 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.
*/
#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<QString, SyncFileItemPtr> _subJobItems;
QString _path;
QString _folderRemotePath;
};
}

View file

@ -18,6 +18,8 @@
#include "propagatedownload.h"
#include "vfs/cfapi/vfs_cfapi.h"
#include <clientsideencryptionjobs.h>
#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<LsColJob *>(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<EncryptedFile> 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<GETEncryptedFileJob *>(_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<GETEncryptedFileJob *>(_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('/'));
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();
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();
}

View file

@ -21,6 +21,7 @@ class QLocalServer;
class QLocalSocket;
namespace OCC {
class EncryptedFolderMetadataHandler;
class GETFileJob;
class SyncJournalDb;
class VfsCfApi;
@ -79,15 +80,12 @@ public:
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 @@ private:
int _errorCode = 0;
int _statusCode = 0;
QString _errorString;
QString _remoteParentPath;
QScopedPointer<EncryptedFolderMetadataHandler> _encryptedFolderMetadataHandler;
};
} // namespace OCC

View file

@ -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)

View file

@ -497,10 +497,11 @@ protected:
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 @@ public:
void persist() override { }
void invalidateToken() override { }
void forgetSensitiveData() override { }
void setUserName(const QString &userName)
{
_userName = userName;
}
};
class FakeFolder

View file

@ -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)

View file

@ -0,0 +1,382 @@
/*
* Copyright (C) 2024 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 "syncenginetestutils.h"
#include "clientsideencryption.h"
#include "foldermetadata.h"
#include <QtTest>
using namespace OCC;
class TestClientSideEncryptionV2 : public QObject
{
Q_OBJECT
QScopedPointer<FakeQNAM> _fakeQnam;
QScopedPointer<FolderMetadata> _parsedMetadataWithFileDrop;
QScopedPointer<FolderMetadata> _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<FolderMetadata> 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<FolderMetadata> 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<FolderMetadata> 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<FolderMetadata> 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<FolderMetadata> 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"

View file

@ -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));

View file

@ -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 <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 "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 <array>
#include <QtTest>
namespace
{
constexpr auto fakeE2eeFolderName = "fake_e2ee_folder";
const QString fakeE2eeFolderPath = QStringLiteral("/") + fakeE2eeFolderName;
};
const std::array<QString, 2> fakeFiles{"fakefile.txt", "fakefile1.txt"};
const std::array<QString, 2> fakeFilesFileDrop{"fakefiledropped.txt", "fakefiledropped1.txt"};
};
using namespace OCC;
@ -27,165 +29,158 @@ class TestSecureFileDrop : public QObject
{
Q_OBJECT
FakeFolder _fakeFolder{FileInfo()};
QSharedPointer<OwncloudPropagator> _propagator;
QScopedPointer<FakeQNAM> _fakeQnam;
AccountPtr _account;
QScopedPointer<FolderMetadata> _parsedMetadataWithFileDrop;
QScopedPointer<FolderMetadata> _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;
QVERIFY(!cert.isNull());
QVERIFY(!publicKey.isNull());
QVERIFY(!privateKey.isEmpty());
const auto path = req.url().path();
_account->e2e()->_certificate = cert;
_account->e2e()->_publicKey = publicKey;
_account->e2e()->_privateKey = privateKey;
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;
}
QScopedPointer<FolderMetadata> metadata(new FolderMetadata(_account, FolderMetadata::FolderType::Root));
QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete);
metadataSetupCompleteSpy.wait();
QCOMPARE(metadataSetupCompleteSpy.count(), 1);
QVERIFY(metadata->isValid());
return reply;
});
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);
}
auto transProgress = connect(&_fakeFolder.syncEngine(), &SyncEngine::transmissionProgress, [&](const ProgressInfo &pi) {
Q_UNUSED(pi);
_propagator = _fakeFolder.syncEngine().getPropagator();
});
QJsonObject fakeFileDropPart;
QVERIFY(_fakeFolder.syncOnce());
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);
}
disconnect(transProgress);
};
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);
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;
}
QJsonObject fakeFileDropEntry;
fakeFileDropEntry.insert("ciphertext", "");
if (_parsedMetadataAfterProcessingFileDrop->files().size() != metadata->files().size()) {
return;
}
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);
if (_parsedMetadataAfterProcessingFileDrop->fileDrop() != metadata->fileDrop()) {
return;
}
bool isAnyFileDropFileMissing = false;
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);
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;
}
}
fakeFileDropPart.insert(encryptedFile.encryptedFilename, fakeFileDropEntry);
}
metadata->setFileDrop(fakeFileDropPart);
if (!isAnyFileDropFileMissing) {
emit fileDropMetadataParsedAndAdjusted();
}
});
QSignalSpy updateFileDropMetadataJobSpy(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::finished);
QSignalSpy fileDropMetadataParsedAndAdjustedSpy(this, &TestSecureFileDrop::fileDropMetadataParsedAndAdjusted);
QVERIFY(updateFileDropMetadataJob->scheduleSelfOrChild());
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.wait(3000));
QSignalSpy metadataWithFileDropSetupCompleteSpy(_parsedMetadataWithFileDrop.data(), &FolderMetadata::setupComplete);
metadataWithFileDropSetupCompleteSpy.wait();
QCOMPARE(metadataWithFileDropSetupCompleteSpy.count(), 1);
QVERIFY(_parsedMetadataWithFileDrop->isValid());
QVERIFY(_parsedMetadataWithFileDrop);
QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent());
QVERIFY(_parsedMetadataAfterProcessingFileDrop);
QVERIFY(_parsedMetadataAfterProcessingFileDrop->files().size() != _parsedMetadataWithFileDrop->files().size());
QVERIFY(!updateFileDropMetadataJobSpy.isEmpty());
QVERIFY(!updateFileDropMetadataJobSpy.at(0).isEmpty());
QCOMPARE(updateFileDropMetadataJobSpy.at(0).first().toInt(), SyncFileItem::Status::Success);
QVERIFY(!fileDropMetadataParsedAndAdjustedSpy.isEmpty());
QCOMPARE(_lockCallsCount, 1);
QCOMPARE(_unlockCallsCount, 1);
QCOMPARE(_propFindCallsCount, 2);
QCOMPARE(_getMetadataCallsCount, 1);
QCOMPARE(_putMetadataCallsCount, 1);
updateFileDropMetadataJob->deleteLater();
QCOMPARE(_parsedMetadataWithFileDrop->_fileDropEntries.count(), fakeFilesFileDrop.size());
}
signals:
void fileDropMetadataParsedAndAdjusted();
void testMoveFileDropMetadata()
{
QVERIFY(_parsedMetadataWithFileDrop->isFileDropPresent());
QVERIFY(_parsedMetadataWithFileDrop->moveFromFileDropToFiles());
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));
}
}
};
QTEST_GUILESS_MAIN(TestSecureFileDrop)