nextcloud-desktop/src/libsync/clientsideencryptionjobs.cpp
Kevin Ottens 10cb4170c7
Don't use depth infinity anymore to get the folders e2ee status
This way we avoid the expensive SQL query on the server at the price of
more round-trips since we're doing the recursive traversal by hand now.

Also it turns out this depth was used for all the other propfind calls
during sync when we want fresher information regarding a folder. This
was very inefficient in all cases and won't happen anymore.

Signed-off-by: Kevin Ottens <kevin.ottens@nextcloud.com>
2020-10-21 10:00:59 +02:00

462 lines
16 KiB
C++

#include "clientsideencryptionjobs.h"
#include <QDebug>
#include <QLoggingCategory>
#include <QFileInfo>
#include <QDir>
#include <QJsonObject>
#include <QXmlStreamReader>
#include <QXmlStreamNamespaceDeclaration>
#include <QStack>
#include <QInputDialog>
#include <QLineEdit>
#include "clientsideencryption.h"
#include "account.h"
#include "capabilities.h"
#include "networkjobs.h"
#include "clientsideencryptionjobs.h"
#include "theme.h"
#include "creds/abstractcredentials.h"
Q_LOGGING_CATEGORY(lcSignPublicKeyApiJob, "nextcloud.sync.networkjob.sendcsr", QtInfoMsg)
Q_LOGGING_CATEGORY(lcStorePrivateKeyApiJob, "nextcloud.sync.networkjob.storeprivatekey", QtInfoMsg)
Q_LOGGING_CATEGORY(lcCseJob, "nextcloud.sync.networkjob.clientsideencrypt", QtInfoMsg)
namespace OCC {
GetFolderEncryptStatusJob::GetFolderEncryptStatusJob(const AccountPtr& account, const QString& folder, QObject *parent)
: OCC::AbstractNetworkJob(account, QStringLiteral("remote.php/webdav"), parent), _folder(folder)
{
}
QString GetFolderEncryptStatusJob::folder() const
{
return _folder;
}
void GetFolderEncryptStatusJob::start()
{
QNetworkRequest req;
req.setPriority(QNetworkRequest::HighPriority);
req.setRawHeader("OCS-APIREQUEST", "true");
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/xml"));
req.setRawHeader("Depth", "1");
QByteArray xml = R"(<d:propfind xmlns:d="DAV:"> <d:prop xmlns:nc="http://nextcloud.org/ns"> <nc:is-encrypted/> </d:prop> </d:propfind>)";
auto *buf = new QBuffer(this);
buf->setData(xml);
buf->open(QIODevice::ReadOnly);
QString tmpPath = path() + (!_folder.isEmpty() ? "/" + _folder : QString());
sendRequest("PROPFIND", Utility::concatUrlPath(account()->url(), tmpPath), req, buf);
AbstractNetworkJob::start();
}
bool GetFolderEncryptStatusJob::finished()
{
qCInfo(lcCseJob()) << "GetFolderEncryptStatus of" << reply()->request().url() << "finished with status"
<< reply()->error()
<< (reply()->error() == QNetworkReply::NoError ? QLatin1String("") : errorString());
int http_result_code = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (http_result_code == 207) {
// Parse DAV response
QXmlStreamReader reader(reply());
reader.addExtraNamespaceDeclaration(QXmlStreamNamespaceDeclaration("d", "DAV:"));
/* Example Xml
<?xml version="1.0"?>
<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns" xmlns:nc="http://nextcloud.org/ns">
<d:response>
<d:href>/remote.php/webdav/</d:href>
<d:propstat>
<d:prop>
<nc:is-encrypted>0</nc:is-encrypted>
</d:prop>
<d:status>HTTP/1.1 200 OK</d:status>
</d:propstat>
</d:response>
</d:multistatus>
*/
QString base = account()->url().path();
if (base.endsWith(QLatin1Char('/')))
base.chop(1);
QString currFile;
int currEncryptedStatus = -1;
QHash<QString, bool> folderStatus;
while (!reader.atEnd()) {
auto type = reader.readNext();
if (type == QXmlStreamReader::StartElement) {
if (reader.name() == QLatin1String("href")) {
// If the current file is not a folder, ignore it.
currFile = QUrl::fromPercentEncoding(reader.readElementText(QXmlStreamReader::SkipChildElements).toUtf8());
currFile.remove(base + QLatin1String("/remote.php/webdav/"));
if (!currFile.endsWith('/'))
currFile.clear();
currEncryptedStatus = -1;
}
if (!currFile.isEmpty() && reader.name() == QLatin1String("is-encrypted")) {
currEncryptedStatus = (bool) reader.readElementText(QXmlStreamReader::SkipChildElements).toInt();
}
}
if (!currFile.isEmpty() && currEncryptedStatus != -1) {
folderStatus.insert(currFile, currEncryptedStatus);
currFile.clear();
currEncryptedStatus = -1;
}
}
emit encryptStatusReceived(folderStatus);
emit encryptStatusFolderReceived(_folder, folderStatus.value(_folder + QLatin1Char('/')));
} else {
qCWarning(lcCseJob()) << "*not* successful, http result code is" << http_result_code
<< (http_result_code == 302 ? reply()->header(QNetworkRequest::LocationHeader).toString() : QLatin1String(""));
emit encryptStatusError(http_result_code);
// emit finishedWithError(reply());
}
return true;
}
GetMetadataApiJob::GetMetadataApiJob(const AccountPtr& account,
const QByteArray& fileId,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
{
}
void GetMetadataApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
QUrlQuery query;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
qCInfo(lcCseJob()) << "Requesting the metadata for the fileId" << _fileId << "as encrypted";
sendRequest("GET", url, req);
AbstractNetworkJob::start();
}
bool GetMetadataApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error requesting the metadata" << path() << errorString() << retCode;
emit error(_fileId, retCode);
return true;
}
QJsonParseError error;
auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
return true;
}
StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account,
const QByteArray& fileId,
const QByteArray& b64Metadata,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _b64Metadata(b64Metadata)
{
}
void StoreMetaDataApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
QUrlQuery query;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
QByteArray data = QByteArray("metaData=") + QUrl::toPercentEncoding(_b64Metadata);
auto buffer = new QBuffer(this);
buffer->setData(data);
qCInfo(lcCseJob()) << "sending the metadata for the fileId" << _fileId << "as encrypted";
sendRequest("POST", url, req, buffer);
AbstractNetworkJob::start();
}
bool StoreMetaDataApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode;
emit error(_fileId, retCode);
}
qCInfo(lcCseJob()) << "Metadata submited to the server successfully";
emit success(_fileId);
return true;
}
UpdateMetadataApiJob::UpdateMetadataApiJob(const AccountPtr& account,
const QByteArray& fileId,
const QByteArray& b64Metadata,
const QByteArray& token,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent),
_fileId(fileId),
_b64Metadata(b64Metadata),
_token(token)
{
}
void UpdateMetadataApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
QUrlQuery urlQuery;
urlQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
urlQuery.addQueryItem(QStringLiteral("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);
buffer->setData(data);
qCInfo(lcCseJob()) << "updating the metadata for the fileId" << _fileId << "as encrypted";
sendRequest("PUT", url, req, buffer);
AbstractNetworkJob::start();
}
bool UpdateMetadataApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error updating the metadata" << path() << errorString() << retCode;
emit error(_fileId, retCode);
}
qCInfo(lcCseJob()) << "Metadata submited to the server successfully";
emit success(_fileId);
return true;
}
UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account,
const QByteArray& fileId,
const QByteArray& token,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token)
{
}
void UnlockEncryptFolderApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
req.setRawHeader("e2e-token", _token);
QUrl url = Utility::concatUrlPath(account()->url(), path());
sendRequest("DELETE", url, req);
AbstractNetworkJob::start();
qCInfo(lcCseJob()) << "Starting the request to unlock.";
}
bool UnlockEncryptFolderApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error unlocking file" << path() << errorString() << retCode;
qCInfo(lcCseJob()) << "Full Error Log" << reply()->readAll();
emit error(_fileId, retCode);
return true;
}
emit success(_fileId);
return true;
}
DeleteMetadataApiJob::DeleteMetadataApiJob(const AccountPtr& account,
const QByteArray& fileId,
QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId)
{
}
void DeleteMetadataApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
QUrl url = Utility::concatUrlPath(account()->url(), path());
sendRequest("DELETE", url, req);
AbstractNetworkJob::start();
qCInfo(lcCseJob()) << "Starting the request to remove the metadata.";
}
bool DeleteMetadataApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error removing metadata for" << path() << errorString() << retCode;
qCInfo(lcCseJob()) << "Full Error Log" << reply()->readAll();
emit error(_fileId, retCode);
return true;
}
emit success(_fileId);
return true;
}
LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId)
{
}
void LockEncryptFolderApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
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();
}
bool LockEncryptFolderApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200) {
qCInfo(lcCseJob()) << "error locking file" << path() << errorString() << retCode;
emit error(_fileId, retCode);
return true;
}
QJsonParseError error;
auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
auto obj = json.object().toVariantMap();
auto token = obj["ocs"].toMap()["data"].toMap()["e2e-token"].toByteArray();
qCInfo(lcCseJob()) << "got json:" << token;
//TODO: Parse the token and submit.
emit success(_fileId, token);
return true;
}
SetEncryptionFlagApiJob::SetEncryptionFlagApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent)
: AbstractNetworkJob(account, baseUrl() + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId)
{
}
void SetEncryptionFlagApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
QUrl url = Utility::concatUrlPath(account()->url(), path());
qCInfo(lcCseJob()) << "marking the file with id" << _fileId << "as encrypted";
sendRequest("PUT", url, req);
AbstractNetworkJob::start();
}
bool SetEncryptionFlagApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
qCInfo(lcCseJob()) << "Encryption Flag Return" << reply()->readAll();
if (retCode == 200) {
emit success(_fileId);
} else {
qCInfo(lcCseJob()) << "Setting the encrypted flag failed with" << path() << errorString() << retCode;
emit error(_fileId, retCode);
}
return true;
}
StorePrivateKeyApiJob::StorePrivateKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent)
: AbstractNetworkJob(account, path, parent)
{
}
void StorePrivateKeyApiJob::setPrivateKey(const QByteArray& privKey)
{
QByteArray data = "privateKey=";
data += QUrl::toPercentEncoding(privKey);
_privKey.setData(data);
}
void StorePrivateKeyApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
QUrlQuery query;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
qCInfo(lcStorePrivateKeyApiJob) << "Sending the private key" << _privKey.data();
sendRequest("POST", url, req, &_privKey);
AbstractNetworkJob::start();
}
bool StorePrivateKeyApiJob::finished()
{
int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (retCode != 200)
qCInfo(lcStorePrivateKeyApiJob()) << "Sending private key ended with" << path() << errorString() << retCode;
QJsonParseError error;
auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
return true;
}
SignPublicKeyApiJob::SignPublicKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent)
: AbstractNetworkJob(account, path, parent)
{
}
void SignPublicKeyApiJob::setCsr(const QByteArray& csr)
{
QByteArray data = "csr=";
data += QUrl::toPercentEncoding(csr);
_csr.setData(data);
}
void SignPublicKeyApiJob::start()
{
QNetworkRequest req;
req.setRawHeader("OCS-APIREQUEST", "true");
req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded"));
QUrlQuery query;
query.addQueryItem(QLatin1String("format"), QLatin1String("json"));
QUrl url = Utility::concatUrlPath(account()->url(), path());
url.setQuery(query);
qCInfo(lcSignPublicKeyApiJob) << "Sending the CSR" << _csr.data();
sendRequest("POST", url, req, &_csr);
AbstractNetworkJob::start();
}
bool SignPublicKeyApiJob::finished()
{
qCInfo(lcStorePrivateKeyApiJob()) << "Sending CSR ended with" << path() << errorString() << reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute);
QJsonParseError error;
auto json = QJsonDocument::fromJson(reply()->readAll(), &error);
emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt());
return true;
}
}