/* * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: GPL-2.0-or-later */ #include "clientsideencryptionjobs.h" #include #include #include #include #include #include #include #include #include #include #include "clientsideencryption.h" #include "account.h" #include "capabilities.h" #include "networkjobs.h" #include "clientsideencryptionjobs.h" #include "theme.h" #include "creds/abstractcredentials.h" #include "common/syncjournaldb.h" Q_LOGGING_CATEGORY(lcSignPublicKeyApiJob, "nextcloud.sync.networkjob.sendcsr", QtInfoMsg) Q_LOGGING_CATEGORY(lcStorePublicKeyApiJob, "nextcloud.sync.networkjob.storepublickey", QtInfoMsg) 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(account) + QStringLiteral("meta-data/") + fileId, parent) , _fileId(fileId) { } const QByteArray &GetMetadataApiJob::signature() const { return _signature; } 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); qCDebug(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) { qCWarning(lcCseJob()) << "error requesting the metadata" << path() << errorString() << retCode; 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); emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); return true; } StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray &token, const QByteArray& b64Metadata, const QByteArray &signature, QObject* parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent), _fileId(fileId), _token(token), _b64Metadata(b64Metadata), _signature(signature) { } 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); } else { qCWarning(lcCseJob()) << "empty signature for" << _fileId; } } 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); QByteArray data = QByteArray("metaData=") + QUrl::toPercentEncoding(_b64Metadata); auto buffer = new QBuffer(this); buffer->setData(data); qCDebug(lcCseJob()) << "sending the metadata for the fileId" << _fileId << "as encrypted"; sendRequest("POST", url, req, buffer); AbstractNetworkJob::start(); } bool StoreMetaDataApiJob::finished() { const auto retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (retCode != 200) { qCWarning(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode; emit error(_fileId, retCode); return false; } qCDebug(lcCseJob()) << "Metadata submitted to the server successfully"; emit success(_fileId); return true; } UpdateMetadataApiJob::UpdateMetadataApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& b64Metadata, const QByteArray& token, const QByteArray& signature, QObject* parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent) , _fileId(fileId), _b64Metadata(b64Metadata), _token(token), _signature(signature) { } void UpdateMetadataApiJob::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 urlQuery; urlQuery.addQueryItem(QStringLiteral("format"), QStringLiteral("json")); 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)); QByteArray data = params.query().toLocal8Bit(); auto buffer = new QBuffer(this); buffer->setData(data); qCDebug(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) { qCWarning(lcCseJob()) << "error updating the metadata" << path() << errorString() << retCode; emit error(_fileId, retCode); return false; } qCDebug(lcCseJob()) << "Metadata submitted to the server successfully"; emit success(_fileId); return true; } UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& token, SyncJournalDb *journalDb, QObject* parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent) , _fileId(fileId) , _token(token) , _journalDb(journalDb) { } void UnlockEncryptFolderApiJob::start() { QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); 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(); qCDebug(lcCseJob()) << "unlock folder started for:" << path() << " for fileId: " << _fileId << "token:" << _token; } void UnlockEncryptFolderApiJob::setShouldRollbackMetadataChanges(bool shouldRollbackMetadataChanges) { _shouldRollbackMetadataChanges = shouldRollbackMetadataChanges; } [[nodiscard]] bool UnlockEncryptFolderApiJob::shouldRollbackMetadataChanges() const { return _shouldRollbackMetadataChanges; } bool UnlockEncryptFolderApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCDebug(lcCseJob()) << "unlock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId; if (retCode != 0) { _journalDb->deleteE2EeLockedFolder(_fileId); } emit done(); if (retCode != 200) { qCWarning(lcCseJob()) << "error unlocking file" << path() << errorString() << retCode << "Full Error Log" << reply()->readAll(); emit error(_fileId, retCode, errorString()); return true; } emit success(_fileId); return true; } 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) { } 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); AbstractNetworkJob::start(); qCDebug(lcCseJob()) << "Starting the request to remove the metadata."; } bool DeleteMetadataApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (retCode != 200) { qCWarning(lcCseJob()) << "error removing metadata for" << path() << errorString() << retCode << "Full Error Log" << reply()->readAll(); emit error(_fileId, retCode); return true; } emit success(_fileId); return true; } LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, const QByteArray &certificateSha256Fingerprint, SyncJournalDb *journalDb, const QSslKey &sslkey, QObject *parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent) , _fileId(fileId) , _certificateSha256Fingerprint(certificateSha256Fingerprint) , _journalDb(journalDb) { Q_UNUSED(sslkey) } void LockEncryptFolderApiJob::start() { const auto folderTokenEncrypted = _journalDb->e2EeLockedFolder(_fileId); if (!folderTokenEncrypted.isEmpty()) { qCDebug(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()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), folderTokenEncrypted); if (!folderToken) { qCWarning(lcCseJob()) << "decrypt failed"; return; } const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, *folderToken, _journalDb, this); unlockJob->setShouldRollbackMetadataChanges(true); connect(unlockJob, &UnlockEncryptFolderApiJob::done, this, [this]() { this->start(); }); unlockJob->start(); return; } 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); sendRequest("POST", url, req); AbstractNetworkJob::start(); qCDebug(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId; } bool LockEncryptFolderApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (retCode != 200) { qCWarning(lcCseJob()) << "lock folder finished with code" << retCode << errorString() << " for:" << path() << " for fileId: " << _fileId; emit error(_fileId, retCode, errorString()); return true; } QJsonParseError error{}; const auto json = QJsonDocument::fromJson(reply()->readAll(), &error); const auto obj = json.object().toVariantMap(); const auto token = obj["ocs"].toMap()["data"].toMap()["e2e-token"].toByteArray(); qCDebug(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token; if (!_account->e2e()->getPublicKey().isNull()) { const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), token); if (!folderTokenEncrypted) { qCWarning(lcCseJob()) << "decrypt failed"; return false; } _journalDb->setE2EeLockedFolder(_fileId, *folderTokenEncrypted); } //TODO: Parse the token and submit. emit success(_fileId, token); return true; } void LockEncryptFolderApiJob::setCounter(quint64 counter) { _counter = counter; } SetEncryptionFlagApiJob::SetEncryptionFlagApiJob(const AccountPtr& account, const QByteArray& fileId, FlagAction flagAction, QObject* parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("encrypted/") + fileId, parent), _fileId(fileId), _flagAction(flagAction) { } void SetEncryptionFlagApiJob::start() { QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); QUrl url = Utility::concatUrlPath(account()->url(), path()); qCDebug(lcCseJob()) << "marking the file with id" << _fileId << "as" << (_flagAction == Set ? "encrypted" : "non-encrypted") << "."; sendRequest(_flagAction == Set ? "PUT" : "DELETE", url, req); AbstractNetworkJob::start(); } bool SetEncryptionFlagApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qCDebug(lcCseJob()) << "Encryption Flag Return" << reply()->readAll(); if (retCode == 200) { emit success(_fileId); } else { qCWarning(lcCseJob()) << "Setting the encrypted flag failed with" << path() << errorString() << retCode; emit error(_fileId, retCode, errorString()); } return true; } StorePublicKeyApiJob::StorePublicKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent) : AbstractNetworkJob(account, path, parent) { } void StorePublicKeyApiJob::setPublicKey(const QByteArray& publicKey) { QByteArray data = "publicKey="; data += QUrl::toPercentEncoding(publicKey); _publicKey.setData(data); } void StorePublicKeyApiJob::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); qCDebug(lcStorePublicKeyApiJob) << "Sending the public key"; sendRequest("PUT", url, req, &_publicKey); AbstractNetworkJob::start(); } bool StorePublicKeyApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (retCode != 200) { qCWarning(lcStorePublicKeyApiJob()) << "Sending public 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; } 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); qCDebug(lcStorePrivateKeyApiJob) << "Sending the private key"; sendRequest("POST", url, req, &_privKey); AbstractNetworkJob::start(); } bool StorePrivateKeyApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (retCode != 200) { qCWarning(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); qCDebug(lcSignPublicKeyApiJob) << "Sending the CSR"; sendRequest("POST", url, req, &_csr); AbstractNetworkJob::start(); } bool SignPublicKeyApiJob::finished() { qCDebug(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; } }