mirror of
https://github.com/nextcloud/desktop.git
synced 2024-11-26 15:06:08 +03:00
Merge pull request #6350 from nextcloud/feature/e2ee-v2-foldersharing
Feature/e2ee v2 foldersharing
This commit is contained in:
commit
8cf4dee510
75 changed files with 5424 additions and 1998 deletions
|
@ -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())
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -107,6 +107,7 @@ public:
|
|||
GetE2EeLockedFolderQuery,
|
||||
GetE2EeLockedFoldersQuery,
|
||||
DeleteE2EeLockedFolderQuery,
|
||||
ListAllTopLevelE2eeFoldersStatusLessThanQuery,
|
||||
|
||||
PreparedQueryCount
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -36,5 +36,6 @@ Q_DECLARE_OPERATORS_FOR_FLAGS(SharePermissions)
|
|||
} // namespace OCC
|
||||
|
||||
Q_DECLARE_METATYPE(OCC::SharePermission)
|
||||
Q_DECLARE_METATYPE(OCC::SharePermissions)
|
||||
|
||||
#endif
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
207
src/libsync/basepropagateremotedeleteencrypted.cpp
Normal file
207
src/libsync/basepropagateremotedeleteencrypted.cpp
Normal 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
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
107
src/libsync/clientsideencryptionprimitives.cpp
Normal file
107
src/libsync/clientsideencryptionprimitives.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
92
src/libsync/clientsideencryptionprimitives.h
Normal file
92
src/libsync/clientsideencryptionprimitives.h
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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--;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
376
src/libsync/encryptedfoldermetadatahandler.cpp
Normal file
376
src/libsync/encryptedfoldermetadatahandler.cpp
Normal 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;
|
||||
}
|
||||
|
||||
}
|
122
src/libsync/encryptedfoldermetadatahandler.h
Normal file
122
src/libsync/encryptedfoldermetadatahandler.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
1125
src/libsync/foldermetadata.cpp
Normal file
1125
src/libsync/foldermetadata.cpp
Normal file
File diff suppressed because it is too large
Load diff
245
src/libsync/foldermetadata.h
Normal file
245
src/libsync/foldermetadata.h
Normal 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
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
54
src/libsync/rootencryptedfolderinfo.cpp
Normal file
54
src/libsync/rootencryptedfolderinfo.cpp
Normal 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 ¤tPath, 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();
|
||||
}
|
||||
}
|
44
src/libsync/rootencryptedfolderinfo.h
Normal file
44
src/libsync/rootencryptedfolderinfo.h
Normal 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 ¤tPath, 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
|
|
@ -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;
|
||||
|
|
|
@ -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"));
|
||||
|
|
|
@ -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
|
||||
|
|
176
src/libsync/updatee2eefoldermetadatajob.cpp
Normal file
176
src/libsync/updatee2eefoldermetadatajob.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
58
src/libsync/updatee2eefoldermetadatajob.h
Normal file
58
src/libsync/updatee2eefoldermetadatajob.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
374
src/libsync/updatee2eefolderusersmetadatajob.cpp
Normal file
374
src/libsync/updatee2eefolderusersmetadatajob.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
111
src/libsync/updatee2eefolderusersmetadatajob.h
Normal file
111
src/libsync/updatee2eefolderusersmetadatajob.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
||||
}
|
96
src/libsync/updatemigratede2eemetadatajob.cpp
Normal file
96
src/libsync/updatemigratede2eemetadatajob.cpp
Normal 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);
|
||||
}
|
||||
|
||||
}
|
50
src/libsync/updatemigratede2eemetadatajob.h
Normal file
50
src/libsync/updatemigratede2eemetadatajob.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
382
test/testclientsideencryptionv2.cpp
Normal file
382
test/testclientsideencryptionv2.cpp
Normal 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"
|
|
@ -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));
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue