Implement Secure filedrop link share. Move data from 'filedrop' to 'files' when syncing E2EE folders.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-01-12 18:55:04 +01:00
parent 33e1a900ad
commit b6ba1fe0d6
37 changed files with 830 additions and 91 deletions

View file

@ -9,6 +9,10 @@ set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR 16)
set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR 0)
set(NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH 0)
set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR 26)
set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR 0)
set(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH 0)
if ( NOT DEFINED MIRALL_VERSION_SUFFIX )
set( MIRALL_VERSION_SUFFIX "git") #e.g. beta1, beta2, rc1
endif( NOT DEFINED MIRALL_VERSION_SUFFIX )

View file

@ -53,6 +53,7 @@ GridLayout {
readonly property bool isLinkShare: model.shareType === ShareModel.ShareTypeLink
readonly property bool isPlaceholderLinkShare: model.shareType === ShareModel.ShareTypePlaceholderLink
readonly property bool isSecureFileDropPlaceholderLinkShare: model.shareType === ShareModel.ShareTypeSecureFileDropPlaceholderLink
readonly property bool isInternalLinkShare: model.shareType === ShareModel.ShareTypeInternalLink
readonly property string text: model.display ?? ""
@ -163,7 +164,7 @@ GridLayout {
imageSource: "image://svgimage-custom-color/add.svg/" + Style.ncTextColor
visible: root.isPlaceholderLinkShare && root.canCreateLinkShares
visible: (root.isPlaceholderLinkShare || root.isSecureFileDropPlaceholderLinkShare) && root.canCreateLinkShares
enabled: visible
onClicked: root.createNewLinkShare()
@ -212,7 +213,7 @@ GridLayout {
imageSource: "image://svgimage-custom-color/more.svg/" + Style.ncTextColor
visible: !root.isPlaceholderLinkShare && !root.isInternalLinkShare
visible: !root.isPlaceholderLinkShare && !root.isSecureFileDropPlaceholderLinkShare && !root.isInternalLinkShare
enabled: visible
onClicked: root.rootStackView.push(shareDetailsPageComponent, {}, StackView.PushTransition)

View file

@ -70,6 +70,7 @@ Page {
readonly property bool expireDateEnforced: shareModelData.expireDateEnforced
readonly property bool passwordProtectEnabled: shareModelData.passwordProtectEnabled
readonly property bool passwordEnforced: shareModelData.passwordEnforced
readonly property bool isSecureFileDropLink: shareModelData.isSecureFileDropLink
readonly property bool isLinkShare: shareModelData.shareType === ShareModel.ShareTypeLink
@ -328,6 +329,7 @@ Page {
checked: root.editingAllowed
text: qsTr("Allow editing")
enabled: !root.waitingForEditingAllowedChange
visible: !root.isSecureFileDropLink
onClicked: {
root.toggleAllowEditing(checked);

View file

@ -26,6 +26,7 @@ namespace {
static const auto placeholderLinkShareId = QStringLiteral("__placeholderLinkShareId__");
static const auto internalLinkShareId = QStringLiteral("__internalLinkShareId__");
static const auto secureFileDropPlaceholderLinkShareId = QStringLiteral("__secureFileDropPlaceholderLinkShareId__");
QString createRandomPassword()
{
@ -39,8 +40,8 @@ QString createRandomPassword()
}
}
namespace OCC {
namespace OCC
{
Q_LOGGING_CATEGORY(lcShareModel, "com.nextcloud.sharemodel")
ShareModel::ShareModel(QObject *parent)
@ -80,6 +81,7 @@ QHash<int, QByteArray> ShareModel::roleNames() const
roles[PasswordRole] = "password";
roles[PasswordEnforcedRole] = "passwordEnforced";
roles[EditingAllowedRole] = "editingAllowed";
roles[IsSecureFileDropLinkRole] = "isSecureFileDropLink";
return roles;
}
@ -109,8 +111,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
return linkShare->getNote();
case ExpireDateEnabledRole:
return linkShare->getExpireDate().isValid();
case ExpireDateRole:
{
case ExpireDateRole: {
const auto startOfExpireDayUTC = linkShare->getExpireDate().startOfDay(QTimeZone::utc());
return startOfExpireDayUTC.toMSecsSinceEpoch();
}
@ -124,8 +125,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
return userGroupShare->getNote();
case ExpireDateEnabledRole:
return userGroupShare->getExpireDate().isValid();
case ExpireDateRole:
{
case ExpireDateRole: {
const auto startOfExpireDayUTC = userGroupShare->getExpireDate().startOfDay(QTimeZone::utc());
return startOfExpireDayUTC.toMSecsSinceEpoch();
}
@ -151,6 +151,8 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
return expireDateEnforcedForShare(share);
case EnforcedMaximumExpireDateRole:
return enforcedMaxExpireDateForShare(share);
case IsSecureFileDropLinkRole:
return _isSecureFileDropSupportedFolder && share->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate);
case PasswordProtectEnabledRole:
return share->isPasswordSet();
case PasswordRole:
@ -159,9 +161,9 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
}
return _shareIdRecentlySetPasswords.value(share->getId());
case PasswordEnforcedRole:
return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid() &&
((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced()) ||
(share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword()));
return _accountState && _accountState->account() && _accountState->account()->capabilities().isValid()
&& ((share->getShareType() == Share::TypeEmail && _accountState->account()->capabilities().shareEmailPasswordEnforced())
|| (share->getShareType() == Share::TypeLink && _accountState->account()->capabilities().sharePublicLinkEnforcePassword()));
case EditingAllowedRole:
return share->getPermissions().testFlag(SharePermissionUpdate);
@ -177,9 +179,7 @@ QVariant ShareModel::data(const QModelIndex &index, const int role) const
return {};
}
qCWarning(lcShareModel) << "Got unknown role" << role
<< "for share of type" << share->getShareType()
<< "so returning null value.";
qCWarning(lcShareModel) << "Got unknown role" << role << "for share of type" << share->getShareType() << "so returning null value.";
return {};
}
@ -214,8 +214,7 @@ void ShareModel::updateData()
resetData();
if (_localPath.isEmpty() || !_accountState || _accountState->account().isNull()) {
qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath
<< "Is account state null:" << !_accountState;
qCWarning(lcShareModel) << "Not updating share model data. Local path is:" << _localPath << "Is account state null:" << !_accountState;
return;
}
@ -240,11 +239,8 @@ void ShareModel::updateData()
SyncJournalFileRecord fileRecord;
auto resharingAllowed = true; // lets assume the good
if(_folder->journalDb()->getFileRecord(relPath, &fileRecord) &&
fileRecord.isValid() &&
!fileRecord._remotePerm.isNull() &&
!fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
if (_folder->journalDb()->getFileRecord(relPath, &fileRecord) && fileRecord.isValid() && !fileRecord._remotePerm.isNull()
&& !fileRecord._remotePerm.hasPermission(RemotePermissions::CanReshare)) {
qCInfo(lcShareModel) << "File record says resharing not allowed";
resharingAllowed = false;
}
@ -254,6 +250,10 @@ void ShareModel::updateData()
_numericFileId = fileRecord.numericFileId();
_isEncryptedItem = fileRecord._isE2eEncrypted;
_isSecureFileDropSupportedFolder =
fileRecord._isE2eEncrypted && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported();
// Will get added when shares are fetched if no link shares are fetched
_placeholderLinkShare.reset(new Share(_accountState->account(),
placeholderLinkShareId,
@ -269,10 +269,15 @@ void ShareModel::updateData()
_sharePath,
Share::TypeInternalLink));
_secureFileDropPlaceholderLinkShare.reset(new Share(_accountState->account(),
secureFileDropPlaceholderLinkShareId,
_accountState->account()->id(),
_accountState->account()->davDisplayName(),
_sharePath,
Share::TypeSecureFileDropPlaceholderLink));
auto job = new PropfindJob(_accountState->account(), _sharePath);
job->setProperties(
QList<QByteArray>()
<< "http://open-collaboration-services.org/ns:share-permissions"
job->setProperties(QList<QByteArray>() << "http://open-collaboration-services.org/ns:share-permissions"
<< "http://owncloud.org/ns:fileid" // numeric file id for fallback private link generation
<< "http://owncloud.org/ns:privatelink");
job->setTimeout(10 * 1000);
@ -306,7 +311,9 @@ void ShareModel::initShareManager()
if (_manager.isNull() && sharingPossible) {
_manager.reset(new ShareManager(_accountState->account(), this));
connect(_manager.data(), &ShareManager::sharesFetched, this, &ShareModel::slotSharesFetched);
connect(_manager.data(), &ShareManager::shareCreated, this, [&]{ _manager->fetchShares(_sharePath); });
connect(_manager.data(), &ShareManager::shareCreated, this, [&] {
_manager->fetchShares(_sharePath);
});
connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare);
connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare);
connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message) {
@ -349,6 +356,43 @@ void ShareModel::handlePlaceholderLinkShare()
Q_EMIT sharesChanged();
}
void ShareModel::handleSecureFileDropLinkShare()
{
// We want to add the placeholder if there are no link shares and
// if we are not already showing the placeholder link share
auto linkSharePresent = false;
auto secureFileDropLinkSharePresent = false;
for (const auto &share : qAsConst(_shares)) {
const auto shareType = share->getShareType();
if (!linkSharePresent && shareType == Share::TypeLink) {
linkSharePresent = true;
} else if (!secureFileDropLinkSharePresent && shareType == Share::TypeSecureFileDropPlaceholderLink) {
secureFileDropLinkSharePresent = true;
}
if (linkSharePresent && secureFileDropLinkSharePresent) {
break;
}
}
if (linkSharePresent && secureFileDropLinkSharePresent) {
slotRemoveShareWithId(secureFileDropPlaceholderLinkShareId);
} else if (!linkSharePresent && !secureFileDropLinkSharePresent) {
slotAddShare(_secureFileDropPlaceholderLinkShare);
}
}
void ShareModel::handleLinkShare()
{
if (!_isEncryptedItem) {
handlePlaceholderLinkShare();
} else if (_isSecureFileDropSupportedFolder) {
handleSecureFileDropLinkShare();
}
}
void ShareModel::slotPropfindReceived(const QVariantMap &result)
{
_fetchOngoing = false;
@ -403,7 +447,7 @@ void ShareModel::slotSharesFetched(const QList<SharePtr> &shares)
slotAddShare(share);
}
handlePlaceholderLinkShare();
handleLinkShare();
}
void ShareModel::setupInternalLinkShare()
@ -411,7 +455,8 @@ void ShareModel::setupInternalLinkShare()
if (!_accountState ||
_accountState->account().isNull() ||
_localPath.isEmpty() ||
_privateLinkUrl.isEmpty()) {
_privateLinkUrl.isEmpty() ||
_isEncryptedItem) {
return;
}
@ -479,7 +524,8 @@ void ShareModel::slotAddShare(const SharePtr &share)
connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError);
}
handlePlaceholderLinkShare();
handleLinkShare();
Q_EMIT sharesChanged();
}
void ShareModel::slotRemoveShareWithId(const QString &shareId)
@ -505,7 +551,9 @@ void ShareModel::slotRemoveShareWithId(const QString &shareId)
_shares.removeAt(shareIndex.row());
endRemoveRows();
handlePlaceholderLinkShare();
handleLinkShare();
Q_EMIT sharesChanged();
}
void ShareModel::slotServerError(const int code, const QString &message)
@ -533,7 +581,10 @@ void ShareModel::slotRemoveSharee(const ShareePtr &sharee)
QString ShareModel::displayStringForShare(const SharePtr &share) const
{
if (const auto linkShare = share.objectCast<LinkShare>()) {
const auto displayString = tr("Share link");
const auto isSecureFileDropShare = _isSecureFileDropSupportedFolder && linkShare->getPermissions().testFlag(OCC::SharePermission::SharePermissionCreate);
const auto displayString = isSecureFileDropShare ? tr("Secure filedrop link") : tr("Share link");
if (!linkShare->getLabel().isEmpty()) {
return QStringLiteral("%1 (%2)").arg(displayString, linkShare->getLabel());
@ -544,6 +595,8 @@ QString ShareModel::displayStringForShare(const SharePtr &share) const
return tr("Link share");
} else if (share->getShareType() == Share::TypeInternalLink) {
return tr("Internal link");
} else if (share->getShareType() == Share::TypeSecureFileDropPlaceholderLink) {
return tr("Secure file drop");
} else if (share->getShareWith()) {
return share->getShareWith()->format();
}
@ -560,6 +613,7 @@ QString ShareModel::iconUrlForShare(const SharePtr &share) const
case Share::TypeInternalLink:
return QString(iconsPath + QStringLiteral("external.svg"));
case Share::TypePlaceholderLink:
case Share::TypeSecureFileDropPlaceholderLink:
case Share::TypeLink:
return QString(iconsPath + QStringLiteral("public.svg"));
case Share::TypeEmail:
@ -890,10 +944,19 @@ void ShareModel::setShareNoteFromQml(const QVariant &share, const QString &note)
void ShareModel::createNewLinkShare() const
{
if (_isEncryptedItem && !_isSecureFileDropSupportedFolder) {
qCWarning(lcShareModel) << "Attempt to create a link share for non-root encrypted folder or a file.";
return;
}
if (_manager) {
const auto askOptionalPassword = _accountState->account()->capabilities().sharePublicLinkAskOptionalPassword();
const auto password = askOptionalPassword ? createRandomPassword() : QString();
_manager->createLinkShare(_sharePath, QString(), password);
if (_isSecureFileDropSupportedFolder) {
_manager->createSecureFileDropShare(_sharePath, {}, password);
return;
}
_manager->createLinkShare(_sharePath, {}, password);
}
}

View file

@ -57,6 +57,7 @@ public:
PasswordRole,
PasswordEnforcedRole,
EditingAllowedRole,
IsSecureFileDropLinkRole,
};
Q_ENUM(Roles)
@ -75,6 +76,7 @@ public:
ShareTypeRoom = Share::TypeRoom,
ShareTypePlaceholderLink = Share::TypePlaceholderLink,
ShareTypeInternalLink = Share::TypeInternalLink,
ShareTypeSecureFileDropPlaceholderLink = Share::TypeSecureFileDropPlaceholderLink,
};
Q_ENUM(ShareType);
@ -159,6 +161,8 @@ private slots:
void updateData();
void initShareManager();
void handlePlaceholderLinkShare();
void handleSecureFileDropLinkShare();
void handleLinkShare();
void setupInternalLinkShare();
void slotPropfindReceived(const QVariantMap &result);
@ -188,6 +192,7 @@ private:
bool _hasInitialShareFetchCompleted = false;
SharePtr _placeholderLinkShare;
SharePtr _internalLinkShare;
SharePtr _secureFileDropPlaceholderLinkShare;
QPointer<AccountState> _accountState;
QPointer<Folder> _folder;
@ -196,6 +201,8 @@ private:
QString _sharePath;
SharePermissions _maxSharingPermissions;
QByteArray _numericFileId;
bool _isEncryptedItem = false;
bool _isSecureFileDropSupportedFolder = false;
SyncJournalFileLockInfo _filelockState;
QString _privateLinkUrl;

View file

@ -155,6 +155,26 @@ void OcsShareJob::createLinkShare(const QString &path,
start();
}
void OcsShareJob::createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password)
{
setVerb("POST");
addParam(QString::fromLatin1("path"), path);
addParam(QString::fromLatin1("shareType"), QString::number(Share::TypeLink));
addParam(QString::fromLatin1("permissions"), QString::number(4));
if (!name.isEmpty()) {
addParam(QString::fromLatin1("name"), name);
}
if (!password.isEmpty()) {
addParam(QString::fromLatin1("password"), password);
}
addPassStatusCode(403);
start();
}
void OcsShareJob::createShare(const QString &path,
const Share::ShareType shareType,
const QString &shareWith,

View file

@ -111,6 +111,8 @@ public:
void createLinkShare(const QString &path, const QString &name,
const QString &password);
void createSecureFileDropLinkShare(const QString &path, const QString &name, const QString &password);
/**
* Create a new share
*

View file

@ -396,6 +396,14 @@ void ShareManager::createLinkShare(const QString &path,
job->createLinkShare(path, name, password);
}
void ShareManager::createSecureFileDropShare(const QString &path, const QString &name, const QString &password)
{
const auto createShareJob = new OcsShareJob(_account);
connect(createShareJob, &OcsShareJob::shareJobFinished, this, &ShareManager::slotLinkShareCreated);
connect(createShareJob, &OcsJob::ocsError, this, &ShareManager::slotOcsError);
createShareJob->createSecureFileDropLinkShare(path, name, password);
}
void ShareManager::slotLinkShareCreated(const QJsonDocument &reply)
{
QString message;

View file

@ -52,6 +52,7 @@ public:
* Need to be in sync with Sharee::Type
*/
enum ShareType {
TypeSecureFileDropPlaceholderLink = -3,
TypeInternalLink = -2,
TypePlaceholderLink = -1,
TypeUser = Sharee::User,
@ -377,6 +378,8 @@ public:
const QString &name,
const QString &password);
void createSecureFileDropShare(const QString &path, const QString &name, const QString &password);
/**
* Tell the manager to create a new share
*

View file

@ -587,6 +587,13 @@ void SocketApi::processShareRequest(const QString &localFile, SocketListener *li
return;
}
if (!fileData.journalRecord().e2eMangledName().isEmpty()) {
// we can not share an encrypted file or a subfolder under encrypted root foolder
const QString message = QLatin1String("SHARE:NOP:") + QDir::toNativeSeparators(localFile);
listener->sendMessage(message);
return;
}
auto &remotePath = fileData.serverRelativePath;
// Can't share root folder
@ -729,12 +736,13 @@ class GetOrCreatePublicLinkShare : public QObject
{
Q_OBJECT
public:
GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile,
GetOrCreatePublicLinkShare(const AccountPtr &account, const QString &localFile, const bool isSecureFileDropOnlyFolder,
QObject *parent)
: QObject(parent)
, _account(account)
, _shareManager(account)
, _localFile(localFile)
, _isSecureFileDropOnlyFolder(isSecureFileDropOnlyFolder)
{
connect(&_shareManager, &ShareManager::sharesFetched,
this, &GetOrCreatePublicLinkShare::sharesFetched);
@ -771,8 +779,12 @@ private slots:
// otherwise create a new one
qCDebug(lcPublicLink) << "Creating new share";
if (_isSecureFileDropOnlyFolder) {
_shareManager.createSecureFileDropShare(_localFile, shareName, QString());
} else {
_shareManager.createLinkShare(_localFile, shareName, QString());
}
}
void linkShareCreated(const QSharedPointer<OCC::LinkShare> &share)
{
@ -832,6 +844,7 @@ private:
AccountPtr _account;
ShareManager _shareManager;
QString _localFile;
bool _isSecureFileDropOnlyFolder = false;
};
#else
@ -852,19 +865,36 @@ public:
#endif
void SocketApi::command_COPY_SECUREFILEDROP_LINK(const QString &localFile, SocketListener *)
{
const auto fileData = FileData::get(localFile);
if (!fileData.folder) {
return;
}
const auto account = fileData.folder->accountState()->account();
const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, true, this);
connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) { copyUrlToClipboard(url); });
connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() { emit shareCommandReceived(fileData.localPath); });
getOrCreatePublicLinkShareJob->run();
}
void SocketApi::command_COPY_PUBLIC_LINK(const QString &localFile, SocketListener *)
{
auto fileData = FileData::get(localFile);
if (!fileData.folder)
const auto fileData = FileData::get(localFile);
if (!fileData.folder) {
return;
}
AccountPtr account = fileData.folder->accountState()->account();
auto job = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, this);
connect(job, &GetOrCreatePublicLinkShare::done, this,
[](const QString &url) { copyUrlToClipboard(url); });
connect(job, &GetOrCreatePublicLinkShare::error, this,
[=]() { emit shareCommandReceived(fileData.localPath); });
job->run();
const auto account = fileData.folder->accountState()->account();
const auto getOrCreatePublicLinkShareJob = new GetOrCreatePublicLinkShare(account, fileData.serverRelativePath, false, this);
connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::done, this, [](const QString &url) {
copyUrlToClipboard(url);
});
connect(getOrCreatePublicLinkShareJob, &GetOrCreatePublicLinkShare::error, this, [=]() {
emit shareCommandReceived(fileData.localPath);
});
getOrCreatePublicLinkShareJob->run();
}
// Windows Shell / Explorer pinning fallbacks, see issue: https://github.com/nextcloud/desktop/issues/1599
@ -1116,11 +1146,12 @@ void SocketApi::command_GET_STRINGS(const QString &argument, SocketListener *lis
listener->sendMessage(QString("GET_STRINGS:END"));
}
void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled)
void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag)
{
auto record = fileData.journalRecord();
bool isOnTheServer = record.isValid();
auto flagString = isOnTheServer && enabled ? QLatin1String("::") : QLatin1String(":d:");
const auto record = fileData.journalRecord();
const auto isOnTheServer = record.isValid();
const auto isSecureFileDropSupported = rootE2eeFolderFlag == SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder && fileData.folder->accountState()->account()->secureFileDropSupported();
const auto flagString = isOnTheServer && (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem || isSecureFileDropSupported) ? QLatin1String("::") : QLatin1String(":d:");
auto capabilities = fileData.folder->accountState()->account()->capabilities();
auto theme = Theme::instance();
@ -1148,13 +1179,23 @@ void SocketApi::sendSharingContextMenuOptions(const FileData &fileData, SocketLi
&& !capabilities.sharePublicLinkEnforcePassword();
if (canCreateDefaultPublicLink) {
if (isSecureFileDropSupported) {
listener->sendMessage(QLatin1String("MENU_ITEM:COPY_SECUREFILEDROP_LINK") + QLatin1String("::") + tr("Copy secure filedrop link"));
} else {
listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PUBLIC_LINK") + flagString + tr("Copy public link"));
}
} else if (publicLinksEnabled) {
if (isSecureFileDropSupported) {
listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + QLatin1String("::") + tr("Copy secure filedrop link"));
} else {
listener->sendMessage(QLatin1String("MENU_ITEM:MANAGE_PUBLIC_LINKS") + flagString + tr("Copy public link"));
}
}
}
if (itemEncryptionFlag == SharingContextItemEncryptedFlag::NotEncryptedItem) {
listener->sendMessage(QLatin1String("MENU_ITEM:COPY_PRIVATE_LINK") + flagString + tr("Copy internal link"));
}
// Disabled: only providing email option for private links would look odd,
// and the copy option is more general.
@ -1312,6 +1353,7 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
const auto record = fileData.journalRecord();
const bool isOnTheServer = record.isValid();
const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty();
const auto isE2eEncryptedRootFolder = fileData.journalRecord()._isE2eEncrypted && fileData.journalRecord()._e2eMangledName.isEmpty();
auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:");
const QFileInfo fileInfo(fileData.localPath);
@ -1331,7 +1373,9 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe
sendEncryptFolderCommandMenuEntries(fileInfo, fileData, isE2eEncryptedPath, listener);
sendLockFileCommandMenuEntries(fileInfo, syncFolder, fileData, listener);
sendSharingContextMenuOptions(fileData, listener, !isE2eEncryptedPath);
const auto itemEncryptionFlag = isE2eEncryptedPath ? SharingContextItemEncryptedFlag::EncryptedItem : SharingContextItemEncryptedFlag::NotEncryptedItem;
const auto rootE2eeFolderFlag = isE2eEncryptedRootFolder ? SharingContextItemRootEncryptedFolderFlag::RootEncryptedFolder : SharingContextItemRootEncryptedFolderFlag::NonRootEncryptedFolder;
sendSharingContextMenuOptions(fileData, listener, itemEncryptionFlag, rootE2eeFolderFlag);
// Conflict files get conflict resolution actions
bool isConflict = Utility::isConflictFile(fileData.folderRelativePath);

View file

@ -51,6 +51,16 @@ class SocketApi : public QObject
{
Q_OBJECT
enum SharingContextItemEncryptedFlag {
EncryptedItem,
NotEncryptedItem
};
enum SharingContextItemRootEncryptedFolderFlag {
RootEncryptedFolder,
NonRootEncryptedFolder
};
public:
explicit SocketApi(QObject *parent = nullptr);
~SocketApi() override;
@ -119,6 +129,7 @@ private:
Q_INVOKABLE void command_SHARE(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_LEAVESHARE(const QString &localFile, SocketListener *listener);
Q_INVOKABLE void command_MANAGE_PUBLIC_LINKS(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_COPY_SECUREFILEDROP_LINK(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_COPY_PUBLIC_LINK(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_COPY_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener);
Q_INVOKABLE void command_EMAIL_PRIVATE_LINK(const QString &localFile, OCC::SocketListener *listener);
@ -151,7 +162,7 @@ private:
Q_INVOKABLE void command_GET_STRINGS(const QString &argument, OCC::SocketListener *listener);
// Sends the context menu options relating to sharing to listener
void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, bool enabled);
void sendSharingContextMenuOptions(const FileData &fileData, SocketListener *listener, SharingContextItemEncryptedFlag itemEncryptionFlag, SharingContextItemRootEncryptedFolderFlag rootE2eeFolderFlag);
void sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo,
const FileData &fileData,

View file

@ -99,6 +99,8 @@ set(libsync_SRCS
syncoptions.cpp
theme.h
theme.cpp
updatefiledropmetadata.h
updatefiledropmetadata.cpp
clientsideencryption.h
clientsideencryption.cpp
clientsideencryptionjobs.h

View file

@ -696,6 +696,17 @@ bool Account::serverVersionUnsupported() const
NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR, NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH);
}
bool Account::secureFileDropSupported() const
{
if (serverVersionInt() == 0) {
// not detected yet, assume it is fine.
return true;
}
return serverVersionInt() >= makeServerVersion(NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR,
NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR,
NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH);
}
bool Account::isUsernamePrefillSupported() const
{
return serverVersionInt() >= makeServerVersion(usernamePrefillServerVersionMinSupportedMajor, 0, 0);

View file

@ -259,6 +259,8 @@ public:
*/
[[nodiscard]] bool serverVersionUnsupported() const;
[[nodiscard]] bool secureFileDropSupported() const;
[[nodiscard]] bool isUsernamePrefillSupported() const;
[[nodiscard]] bool isChecksumRecalculateRequestSupported() const;

View file

@ -106,7 +106,7 @@ bool BulkPropagatorJob::scheduleSelfOrChild()
return _items.empty() && _filesToUpload.empty();
}
PropagatorJob::JobParallelism BulkPropagatorJob::parallelism()
PropagatorJob::JobParallelism BulkPropagatorJob::parallelism() const
{
return PropagatorJob::JobParallelism::FullParallelism;
}

View file

@ -64,7 +64,7 @@ public:
bool scheduleSelfOrChild() override;
JobParallelism parallelism() override;
[[nodiscard]] JobParallelism parallelism() const override;
private slots:
void startUploadFile(OCC::SyncFileItemPtr item, OCC::BulkPropagatorJob::UploadFileInfo fileToUpload);

View file

@ -24,7 +24,6 @@
#include <QLoggingCategory>
#include <QFileInfo>
#include <QDir>
#include <QJsonObject>
#include <QXmlStreamReader>
#include <QXmlStreamNamespaceDeclaration>
#include <QStack>
@ -1534,6 +1533,8 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit();
QJsonObject files = metaDataDoc.object()["files"].toObject();
_fileDrop = metaDataDoc.object().value("filedrop").toObject();
QJsonDocument debugHelper;
debugHelper.setObject(metadataKeys);
qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact);
@ -1546,7 +1547,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
* We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC
* Now we should be compatible with Android and IOS. Maybe we can fix it later.
*/
QByteArray b64DecryptedKey = decryptMetadataKey(currB64Pass);
QByteArray b64DecryptedKey = decryptData(currB64Pass);
if (b64DecryptedKey.isEmpty()) {
qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key();
continue;
@ -1615,7 +1616,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata)
}
// RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key.
QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const
QByteArray FolderMetadata::encryptData(const QByteArray& data) const
{
Bio publicKeyBio;
QByteArray publicKeyPem = _account->e2e()->_publicKey.toPem();
@ -1626,7 +1627,7 @@ QByteArray FolderMetadata::encryptMetadataKey(const QByteArray& data) const
return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64());
}
QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadata) const
QByteArray FolderMetadata::decryptData(const QByteArray &data) const
{
Bio privateKeyBio;
QByteArray privateKeyPem = _account->e2e()->_privateKey;
@ -1634,8 +1635,7 @@ QByteArray FolderMetadata::decryptMetadataKey(const QByteArray& encryptedMetadat
auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio);
// Also base64 decode the result
QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(
key, QByteArray::fromBase64(encryptedMetadata));
QByteArray decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data));
if (decryptResult.isEmpty())
{
@ -1672,7 +1672,7 @@ void FolderMetadata::setupEmptyMetadata() {
_sharing.append({displayName, publicKey});
}
QByteArray FolderMetadata::encryptedMetadata() {
QByteArray FolderMetadata::encryptedMetadata() const {
qCDebug(lcCse) << "Generating metadata";
if (_metadataKeys.isEmpty()) {
@ -1686,7 +1686,7 @@ QByteArray FolderMetadata::encryptedMetadata() {
* We have to already base64 encode the metadatakey here. This was a misunderstanding in the RFC
* Now we should be compatible with Android and IOS. Maybe we can fix it later.
*/
const QByteArray encryptedKey = encryptMetadataKey(it.value().toBase64());
const QByteArray encryptedKey = encryptData(it.value().toBase64());
metadataKeys.insert(QString::number(it.key()), QString(encryptedKey));
}
@ -1761,6 +1761,52 @@ QVector<EncryptedFile> FolderMetadata::files() const {
return _files;
}
bool FolderMetadata::isFileDropPresent() const
{
return _fileDrop.size() > 0;
}
bool FolderMetadata::moveFromFileDropToFiles()
{
if (_fileDrop.isEmpty()) {
return false;
}
for (auto it = _fileDrop.constBegin(); it != _fileDrop.constEnd(); ++it) {
const auto fileObject = it.value().toObject();
const auto encryptedFile = fileObject["encrypted"].toString().toLocal8Bit();
const auto decryptedFile = decryptData(encryptedFile);
const auto decryptedFileDocument = QJsonDocument::fromJson(decryptedFile);
const auto decryptedFileObject = decryptedFileDocument.object();
EncryptedFile file;
file.encryptedFilename = it.key();
file.metadataKey = fileObject["metadataKey"].toInt();
file.authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit());
file.initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit());
file.originalFilename = decryptedFileObject["filename"].toString();
file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit());
file.mimetype = decryptedFileObject["mimetype"].toString().toLocal8Bit();
file.fileVersion = decryptedFileObject["version"].toInt();
// In case we wrongly stored "inode/directory" we try to recover from it
if (file.mimetype == QByteArrayLiteral("inode/directory")) {
file.mimetype = QByteArrayLiteral("httpd/unix-directory");
}
_files.push_back(file);
}
return true;
}
QJsonObject FolderMetadata::fileDrop() const
{
return _fileDrop;
}
bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag)
{
if (!input->open(QIODevice::ReadOnly)) {

View file

@ -4,6 +4,7 @@
#include <QString>
#include <QObject>
#include <QJsonDocument>
#include <QJsonObject>
#include <QSslCertificate>
#include <QSslKey>
#include <QFile>
@ -188,13 +189,18 @@ struct EncryptedFile {
class OWNCLOUDSYNC_EXPORT FolderMetadata {
public:
FolderMetadata(AccountPtr account, const QByteArray& metadata = QByteArray(), int statusCode = -1);
QByteArray encryptedMetadata();
[[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 moveFromFileDropToFiles();
[[nodiscard]] QJsonObject fileDrop() const;
private:
/* Use std::string and std::vector internally on this class
@ -203,8 +209,8 @@ private:
void setupEmptyMetadata();
void setupExistingMetadata(const QByteArray& metadata);
[[nodiscard]] QByteArray encryptMetadataKey(const QByteArray& metadataKey) const;
[[nodiscard]] QByteArray decryptMetadataKey(const QByteArray& encryptedKey) const;
[[nodiscard]] QByteArray encryptData(const QByteArray &data) const;
[[nodiscard]] QByteArray decryptData(const QByteArray &data) const;
[[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const;
[[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const;
@ -213,6 +219,7 @@ private:
QMap<int, QByteArray> _metadataKeys;
AccountPtr _account;
QVector<QPair<QString, QString>> _sharing;
QJsonObject _fileDrop;
};
} // namespace OCC

View file

@ -293,8 +293,10 @@ bool LockEncryptFolderApiJob::finished()
qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token;
if (!_publicKey.isNull()) {
const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token);
_journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted);
}
//TODO: Parse the token and submit.
emit success(_fileId, token);

View file

@ -1848,6 +1848,10 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery()
_discoveryData->_currentlyActiveJobs++;
_pendingAsyncJobs++;
connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) {
if (_dirItem) {
_dirItem->_isFileDropDetected = serverJob->isFileDropDetected();
qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true;
}
_discoveryData->_currentlyActiveJobs--;
_pendingAsyncJobs--;
if (results) {

View file

@ -405,6 +405,11 @@ void DiscoverySingleDirectoryJob::abort()
}
}
bool DiscoverySingleDirectoryJob::isFileDropDetected() const
{
return _isFileDropDetected;
}
static void propertyMapToRemoteInfo(const QMap<QString, QString> &map, RemoteInfo &result)
{
for (auto it = map.constBegin(); it != map.constEnd(); ++it) {
@ -617,6 +622,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in
Q_ASSERT(_subPath.startsWith('/'));
const auto metadata = FolderMetadata(_account, json.toJson(QJsonDocument::Compact), statusCode);
_isFileDropDetected = metadata.isFileDropPresent();
const auto encryptedFiles = metadata.files();
const auto findEncryptedFile = [=](const QString &name) {

View file

@ -67,6 +67,7 @@ struct RemoteInfo
int64_t sizeOfFolder = 0;
bool isDirectory = false;
bool isE2eEncrypted = false;
bool isFileDropDetected = false;
QString e2eMangledName;
bool sharedByMe = false;
@ -142,6 +143,7 @@ public:
void setIsRootPath() { _isRootPath = true; }
void start();
void abort();
[[nodiscard]] bool isFileDropDetected() const;
// This is not actually a network job, it is just a job
signals:
@ -173,6 +175,7 @@ private:
bool _isExternalStorage = false;
// If this directory is e2ee
bool _isE2eEncrypted = false;
bool _isFileDropDetected = false;
// If set, the discovery will finish with an error
int64_t _size = 0;
QString _error;

View file

@ -83,8 +83,8 @@ void EncryptFolderJob::slotLockForEncryptionSuccess(const QByteArray &fileId, co
{
_folderToken = token;
FolderMetadata emptyMetadata(_account);
auto encryptedMetadata = emptyMetadata.encryptedMetadata();
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"

View file

@ -22,6 +22,7 @@
#include "propagateremotemove.h"
#include "propagateremotemkdir.h"
#include "bulkpropagatorjob.h"
#include "updatefiledropmetadata.h"
#include "propagatorjobs.h"
#include "filesystem.h"
#include "common/utility.h"
@ -584,7 +585,7 @@ void OwncloudPropagator::start(SyncFileItemVector &&items)
directoriesToRemove,
removedDirectory,
items);
} else {
} else if (!directories.top().second->_item->_isFileDropDetected) {
startFilePropagation(item,
directories,
directoriesToRemove,
@ -645,6 +646,11 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item,
const auto currentDirJob = directories.top().second;
currentDirJob->appendJob(directoryPropagationJob.get());
}
if (item->_isFileDropDetected) {
directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file));
item->_instruction = CSYNC_INSTRUCTION_NONE;
_anotherSyncNeeded = true;
}
directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release()));
}
@ -1066,7 +1072,7 @@ OwncloudPropagator *PropagatorJob::propagator() const
// ================================================================================
PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism()
PropagatorJob::JobParallelism PropagatorCompositeJob::parallelism() const
{
// If any of the running sub jobs is not parallel, we have to wait
for (int i = 0; i < _runningJobs.count(); ++i) {
@ -1215,7 +1221,7 @@ PropagateDirectory::PropagateDirectory(OwncloudPropagator *propagator, const Syn
connect(&_subJobs, &PropagatorJob::finished, this, &PropagateDirectory::slotSubJobsFinished);
}
PropagatorJob::JobParallelism PropagateDirectory::parallelism()
PropagatorJob::JobParallelism PropagateDirectory::parallelism() const
{
// If any of the non-finished sub jobs is not parallel, we have to wait
if (_firstJob && _firstJob->parallelism() != FullParallelism) {
@ -1330,7 +1336,7 @@ PropagateRootDirectory::PropagateRootDirectory(OwncloudPropagator *propagator)
connect(&_dirDeletionJobs, &PropagatorJob::finished, this, &PropagateRootDirectory::slotDirDeletionJobsFinished);
}
PropagatorJob::JobParallelism PropagateRootDirectory::parallelism()
PropagatorJob::JobParallelism PropagateRootDirectory::parallelism() const
{
// the root directory parallelism isn't important
return WaitForFinished;

View file

@ -62,7 +62,7 @@ class PropagatorCompositeJob;
*
* @ingroup libsync
*/
class PropagatorJob : public QObject
class OWNCLOUDSYNC_EXPORT PropagatorJob : public QObject
{
Q_OBJECT
@ -98,7 +98,7 @@ public:
Q_ENUM(JobParallelism)
virtual JobParallelism parallelism() { return FullParallelism; }
[[nodiscard]] virtual JobParallelism parallelism() const { return FullParallelism; }
/**
* For "small" jobs
@ -215,7 +215,7 @@ public:
return true;
}
JobParallelism parallelism() override { return _parallelism; }
[[nodiscard]] JobParallelism parallelism() const override { return _parallelism; }
SyncFileItemPtr _item;
@ -254,7 +254,7 @@ public:
}
bool scheduleSelfOrChild() override;
JobParallelism parallelism() override;
[[nodiscard]] JobParallelism parallelism() const override;
/*
* Abort synchronously or asynchronously - some jobs
@ -320,7 +320,7 @@ public:
}
bool scheduleSelfOrChild() override;
JobParallelism parallelism() override;
[[nodiscard]] JobParallelism parallelism() const override;
void abort(PropagatorJob::AbortType abortType) override
{
if (_firstJob)
@ -366,7 +366,7 @@ public:
explicit PropagateRootDirectory(OwncloudPropagator *propagator);
bool scheduleSelfOrChild() override;
JobParallelism parallelism() override;
[[nodiscard]] JobParallelism parallelism() const override;
void abort(PropagatorJob::AbortType abortType) override;
[[nodiscard]] qint64 committedDiskSpace() const override;

View file

@ -57,7 +57,7 @@ public:
}
void start() override;
void abort(PropagatorJob::AbortType abortType) override;
JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
[[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
/**
* Rename the directory in the selective sync list

View file

@ -111,8 +111,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataError(const QByteArray
Q_UNUSED(fileId);
Q_UNUSED(httpReturnCode);
qCDebug(lcPropagateUploadEncrypted()) << "Error Getting the encrypted metadata. Pretend we got empty metadata.";
FolderMetadata emptyMetadata(_propagator->account());
emptyMetadata.encryptedMetadata();
const FolderMetadata emptyMetadata(_propagator->account());
auto json = QJsonDocument::fromJson(emptyMetadata.encryptedMetadata());
slotFolderEncryptedMetadataReceived(json, httpReturnCode);
}

View file

@ -87,7 +87,7 @@ class PropagateLocalRename : public PropagateItemJob
public:
PropagateLocalRename(OwncloudPropagator *propagator, const SyncFileItemPtr &item);
void start() override;
JobParallelism parallelism() override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
[[nodiscard]] JobParallelism parallelism() const override { return _item->isDirectory() ? WaitForFinished : FullParallelism; }
private:
bool deleteOldDbRecord(const QString &fileName);

View file

@ -317,6 +317,8 @@ public:
time_t _lastShareStateFetchedTimestamp = 0;
bool _sharedByMe = false;
bool _isFileDropDetected = false;
};
inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2)

View file

@ -0,0 +1,212 @@
/*
* 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(), json.toJson(QJsonDocument::Compact), statusCode));
if (!_metadata->moveFromFileDropToFiles()) {
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 actuall uploader will take over";
unlockFolder();
}
void UpdateFileDropMetadataJob::slotUpdateMetadataError(const QByteArray &fileId, int httpErrorResponse)
{
qCDebug(lcUpdateFileDropMetadataJob) << "Update metadata error for folder" << fileId << "with error" << httpErrorResponse;
qCDebug(lcUpdateFileDropMetadataJob()) << "Unlocking the folder.";
unlockFolder();
}
void UpdateFileDropMetadataJob::slotFolderLockedError(const QByteArray &fileId, int httpErrorCode)
{
Q_UNUSED(httpErrorCode);
qCDebug(lcUpdateFileDropMetadataJob) << "Folder" << fileId << "with path" << _path << "Coundn't be locked. httpErrorCode" << httpErrorCode;
emit finished(SyncFileItem::Status::NormalError);
}
void UpdateFileDropMetadataJob::slotFolderEncryptedIdError(QNetworkReply *reply)
{
if (!reply) {
qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path;
} else {
qCDebug(lcUpdateFileDropMetadataJob) << "Error retrieving the Id of the encrypted folder" << _path << "with httpErrorCode" << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
}
emit finished(SyncFileItem::Status::NormalError);
}
void UpdateFileDropMetadataJob::unlockFolder()
{
Q_ASSERT(!_isUnlockRunning);
if (!_isFolderLocked) {
emit finished(SyncFileItem::Status::Success);
return;
}
if (_isUnlockRunning) {
qCWarning(lcUpdateFileDropMetadataJob) << "Double-call to unlockFolder.";
return;
}
_isUnlockRunning = true;
qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock";
const auto unlockJob = new UnlockEncryptFolderApiJob(propagator()->account(), _folderId, _folderToken, propagator()->_journal, this);
connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) {
qCDebug(lcUpdateFileDropMetadataJob) << "Successfully Unlocked";
_folderToken = "";
_folderId = "";
_isFolderLocked = false;
emit folderUnlocked(folderId, 200);
_isUnlockRunning = false;
emit finished(SyncFileItem::Status::Success);
});
connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) {
qCDebug(lcUpdateFileDropMetadataJob) << "Unlock Error";
emit folderUnlocked(folderId, httpStatus);
_isUnlockRunning = false;
emit finished(SyncFileItem::Status::NormalError);
});
unlockJob->start();
}
}

View file

@ -0,0 +1,69 @@
/*
* 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 FolderMetadata *const metadata);
private:
QString _path;
bool _currentLockingInProgress = false;
bool _isUnlockRunning = false;
bool _isFolderLocked = false;
QByteArray _folderToken;
QByteArray _folderId;
QScopedPointer<FolderMetadata> _metadata;
};
}

View file

@ -69,6 +69,12 @@ nextcloud_add_test(LockFile)
nextcloud_add_test(ShareModel)
nextcloud_add_test(ShareeModel)
nextcloud_add_test(SortedShareModel)
nextcloud_add_test(SecureFileDrop)
target_link_libraries(SecureFileDropTest PRIVATE Nextcloud::sync)
configure_file(fake2eelocksucceeded.json "${PROJECT_BINARY_DIR}/bin/fake2eelocksucceeded.json" COPYONLY)
configure_file(fakefiledrope2eefoldermetadata.json "${PROJECT_BINARY_DIR}/bin/fakefiledrope2eefoldermetadata.json" COPYONLY)
if(ADD_E2E_TESTS)
nextcloud_add_test(E2eServerSetup)

View file

@ -0,0 +1,10 @@
{
"ocs": {
"data": { "e2e-token": "U1SHqQwKzjEIlJUkFIcpYPJeZsM80T6OkegKFu2pSc6BFqORcGfB0Y8PZzRjc6Lm" },
"meta": {
"message": "OK",
"status": "ok",
"statuscode": 200
}
}
}

View file

@ -0,0 +1,10 @@
{
"ocs": {
"data": { "meta-data": "{\"files\":{\"083c2bffea5d4b0f824e2fd5df5369d2\":{\"authenticationTag\":\"LRWVv0vR01WUhrv26kGvOg==\",\"encrypted\":\"dUlVcf+8xaMgIxkWd7YYIsYLIotqD3ZjQtES1VsepKa7+aUYJGdNlPT25+CTQl65mY5ggu9d03hiysiBIUO7BH7klyUY9OQM80kGVE1xuWXQ1aCfgiFruN4h1VSS8S/9jrgBojxncBnsGZjU/NOGZUjA1svdE2hM+O4fywPKUyT09an9t2EbqUGgUl242ezJ|ja9flmYfZAl/MUjF11chaA==\",\"initializationVector\":\"vNfZNAVYVs0eGdB5vbo5KA==\",\"metadataKey\":0}},\"metadata\":{\"metadataKeys\":{\"0\":\"GHKkcNTxsyigJODA45neTO+8Y0NDfB+7mez90EwjW39mZvNnCUBeGO2R6vLzEf5apYjDNsWNS5sHvUZ188OLa9zCDmMm00m8dwfMPEUA0H5Rp9yewUbnM8YRl6vCZWvDa5HLTCdC8UCIKsbvuifAvveQXEO/vafzWrP8IAhG6WsNXZ4qqaUX/0pm84KXvHmStH60xpZpT8U/kKBNdxDeOTdp5T6FglRTnsF9wt9cplPtRHV+BzkC5NgfBqAvLVSP8gckj5iJNQrRM7IQmBPO0AMUv9yU609o7X4WUZ4LwNGqwL6ciCzS83BQ+FCEbf4HyViEWrEq2OLVFgDH7ML18Q==\"},\"version\":1},\"filedrop\":{\"1a1e95ae836b4005bf69e369661e81ba\":{\"encrypted\":\"r7o01Y3dBQbOlhR38ulPW77X0aoGS8riwJmnRp0k0fQgfySy5++GJkaTJqSdcQYvw8stn+hU7j6wwOJMA/aJ3UV5i8H7yPR+RQHiKjrU4L379HU+H1Xuu5KbkhbLoOc7GAxaCpyC0USYI4UCUcmKnSpqAhpHdnpScmyYztA6qnfumclIzgSfE87lCRRFnIp4mKm305hD+4gBuk3WfERewqbgK2Yo08sFhOjR6zULrJgqQBbHc61R7TZb18H26u0fa+mjRmehTSiqhy0dDePip7sr8+a0dCNCEcBG8qKK9xPDYCFmDrAq7iaEpcRoZ4CgUavXOSF+zdunBXXWmKTq6w==\",\"initializationVector\":\"hel7omho/1XsYqov8XCXgA==\",\"authenticationTag\":\"ni6/k9UpFEkS5FYoMPU+/g==\",\"metadataKey\":0}}}" },
"meta": {
"message": "OK",
"status": "ok",
"statuscode": 200
}
}
}

View file

@ -1014,6 +1014,11 @@ QJsonObject FakeQNAM::forEachReplyPart(QIODevice *outgoingData,
QNetworkReply *FakeQNAM::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData)
{
if (op == QNetworkAccessManager::CustomOperation) {
qInfo() << "Operation" << request.attribute(QNetworkRequest::CustomVerbAttribute).toString() << request.url();
} else {
qInfo() << "Operation" << op << request.url();
}
QNetworkReply *reply = nullptr;
auto newRequest = request;
newRequest.setRawHeader("X-Request-ID", OCC::AccessManager::generateRequestId());

168
test/testsecurefiledrop.cpp Normal file
View file

@ -0,0 +1,168 @@
/*
* 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.
*
*/
#include "updatefiledropmetadata.h"
#include "syncengine.h"
#include "syncenginetestutils.h"
#include "testhelper.h"
#include "owncloudpropagator_p.h"
#include "propagatorjobs.h"
#include "clientsideencryption.h"
#include <QtTest>
namespace
{
constexpr auto fakeE2eeFolderName = "fake_e2ee_folder";
const QString fakeE2eeFolderPath = QStringLiteral("/") + fakeE2eeFolderName;
};
using namespace OCC;
class TestSecureFileDrop : public QObject
{
Q_OBJECT
FakeFolder _fakeFolder{FileInfo()};
QSharedPointer<OwncloudPropagator> _propagator;
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);
_fakeFolder.setServerOverride([this](QNetworkAccessManager::Operation op, const QNetworkRequest &req, QIODevice *device) {
Q_UNUSED(device);
QNetworkReply *reply = nullptr;
const auto path = req.url().path();
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(), jsonDoc.toJson()));
_parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), 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;
}
return reply;
});
auto transProgress = connect(&_fakeFolder.syncEngine(), &SyncEngine::transmissionProgress, [&](const ProgressInfo &pi) {
Q_UNUSED(pi);
_propagator = _fakeFolder.syncEngine().getPropagator();
});
QVERIFY(_fakeFolder.syncOnce());
disconnect(transProgress);
};
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;
}
if (_parsedMetadataAfterProcessingFileDrop->files().size() != metadata->files().size()) {
return;
}
if (_parsedMetadataAfterProcessingFileDrop->fileDrop() != metadata->fileDrop()) {
return;
}
bool isAnyFileDropFileMissing = false;
for (const auto &key : metadata->fileDrop().keys()) {
if (std::find_if(metadata->files().constBegin(), metadata->files().constEnd(), [&key](const EncryptedFile &encryptedFile) {
return encryptedFile.encryptedFilename == key;
}) == metadata->files().constEnd()) {
isAnyFileDropFileMissing = true;
}
}
if (!isAnyFileDropFileMissing) {
emit fileDropMetadataParsedAndAdjusted();
}
});
QSignalSpy updateFileDropMetadataJobSpy(updateFileDropMetadataJob, &UpdateFileDropMetadataJob::finished);
QSignalSpy fileDropMetadataParsedAndAdjustedSpy(this, &TestSecureFileDrop::fileDropMetadataParsedAndAdjusted);
QVERIFY(updateFileDropMetadataJob->scheduleSelfOrChild());
QVERIFY(updateFileDropMetadataJobSpy.wait(3000));
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();
}
signals:
void fileDropMetadataParsedAndAdjusted();
};
QTEST_GUILESS_MAIN(TestSecureFileDrop)
#include "testsecurefiledrop.moc"

View file

@ -41,4 +41,8 @@ constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_V
constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_MINOR@;
constexpr int NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_MIN_SUPPORTED_PATCH@;
constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MAJOR@;
constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_MINOR@;
constexpr int NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH = @NEXTCLOUD_SERVER_VERSION_SECURE_FILEDROP_MIN_SUPPORTED_PATCH@;
#endif // VERSION_H