From: alex-z Date: Wed, 25 Jan 2023 17:09:37 +0000 (+0100) Subject: Always unlock E2EE folders, even when network failure or crash. X-Git-Tag: archive/raspbian/3.16.7-1_deb13u1+rpi1~1^2~12^2~11^2~21^2 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=bd9eb0c89f4d22478d960930a591899bfad5f15f;p=nextcloud-desktop.git Always unlock E2EE folders, even when network failure or crash. Signed-off-by: alex-z --- diff --git a/src/common/preparedsqlquerymanager.h b/src/common/preparedsqlquerymanager.h index 97c57e736..beea7d74b 100644 --- a/src/common/preparedsqlquerymanager.h +++ b/src/common/preparedsqlquerymanager.h @@ -103,6 +103,10 @@ public: CountDehydratedFilesQuery, SetPinStateQuery, WipePinStateQuery, + SetE2EeLockedFolderQuery, + GetE2EeLockedFolderQuery, + GetE2EeLockedFoldersQuery, + DeleteE2EeLockedFolderQuery, PreparedQueryCount }; diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 7fdd76560..84668ed26 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -541,6 +541,16 @@ bool SyncJournalDb::checkConnect() return sqlFail(QStringLiteral("Create table version"), createQuery); } + // create the e2EeLockedFolders table. + createQuery.prepare( + "CREATE TABLE IF NOT EXISTS e2EeLockedFolders(" + "folderId VARCHAR(128) PRIMARY KEY," + "token VARCHAR(4096)" + ");"); + if (!createQuery.exec()) { + return sqlFail(QStringLiteral("Create table e2EeLockedFolders"), createQuery); + } + bool forceRemoteDiscovery = false; SqlQuery versionQuery("SELECT major, minor, patch FROM version;", _db); @@ -2395,6 +2405,79 @@ void SyncJournalDb::markVirtualFileForDownloadRecursively(const QByteArray &path } } +void SyncJournalDb::setE2EeLockedFolder(const QByteArray &folderId, const QByteArray &folderToken) +{ + QMutexLocker locker(&_mutex); + if (!checkConnect()) { + return; + } + + const auto query = _queryManager.get(PreparedSqlQueryManager::SetE2EeLockedFolderQuery, + QByteArrayLiteral("INSERT OR REPLACE INTO e2EeLockedFolders " + "(folderId, token) " + "VALUES (?1, ?2);"), + _db); + ASSERT(query) + query->bindValue(1, folderId); + query->bindValue(2, folderToken); + ASSERT(query->exec()) +} + +QByteArray SyncJournalDb::e2EeLockedFolder(const QByteArray &folderId) +{ + QMutexLocker locker(&_mutex); + if (!checkConnect()) { + return {}; + } + const auto query = _queryManager.get(PreparedSqlQueryManager::GetE2EeLockedFolderQuery, + QByteArrayLiteral("SELECT token FROM e2EeLockedFolders WHERE folderId=?1;"), + _db); + ASSERT(query) + query->bindValue(1, folderId); + ASSERT(query->exec()) + if (!query->next().hasData) { + return {}; + } + + return query->baValue(0); +} + +QList> SyncJournalDb::e2EeLockedFolders() +{ + QMutexLocker locker(&_mutex); + + QList> res; + + if (!checkConnect()) { + return res; + } + + const auto query = _queryManager.get(PreparedSqlQueryManager::GetE2EeLockedFoldersQuery, QByteArrayLiteral("SELECT * FROM e2EeLockedFolders"), _db); + ASSERT(query) + + if (!query->exec()) { + return res; + } + + while (query->next().hasData) { + res.append({query->baValue(0), query->baValue(1)}); + } + return res; +} + +void SyncJournalDb::deleteE2EeLockedFolder(const QByteArray &folderId) +{ + QMutexLocker locker(&_mutex); + if (!checkConnect()) { + return; + } + + const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteE2EeLockedFolderQuery, QByteArrayLiteral("DELETE FROM e2EeLockedFolders WHERE folderId=?1;"), _db); + ASSERT(query) + query->bindValue(1, folderId); + ASSERT(query->exec()) +} + Optional SyncJournalDb::PinStateInterface::rawForPath(const QByteArray &path) { QMutexLocker lock(&_db->_mutex); diff --git a/src/common/syncjournaldb.h b/src/common/syncjournaldb.h index 2da2d967b..4a0d3f54d 100644 --- a/src/common/syncjournaldb.h +++ b/src/common/syncjournaldb.h @@ -287,6 +287,11 @@ public: */ void markVirtualFileForDownloadRecursively(const QByteArray &path); + void setE2EeLockedFolder(const QByteArray &folderId, const QByteArray &folderToken); + QByteArray e2EeLockedFolder(const QByteArray &folderId); + QList> e2EeLockedFolders(); + void deleteE2EeLockedFolder(const QByteArray &folderId); + /** Grouping for all functions relating to pin states, * * Use internalPinStates() to get at them. diff --git a/src/libsync/abstractpropagateremotedeleteencrypted.cpp b/src/libsync/abstractpropagateremotedeleteencrypted.cpp index 5dd817244..2cbfd698b 100644 --- a/src/libsync/abstractpropagateremotedeleteencrypted.cpp +++ b/src/libsync/abstractpropagateremotedeleteencrypted.cpp @@ -75,7 +75,7 @@ void AbstractPropagateRemoteDeleteEncrypted::slotFolderEncryptedIdReceived(const void AbstractPropagateRemoteDeleteEncrypted::slotTryLock(const QByteArray &folderId) { - auto lockJob = new LockEncryptFolderApiJob(_propagator->account(), folderId, this); + auto lockJob = new LockEncryptFolderApiJob(_propagator->account(), folderId, _propagator->_journal, _propagator->account()->e2e()->_publicKey, this); connect(lockJob, &LockEncryptFolderApiJob::success, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderLockedSuccessfully); connect(lockJob, &LockEncryptFolderApiJob::error, this, &AbstractPropagateRemoteDeleteEncrypted::taskFailed); lockJob->start(); @@ -172,7 +172,7 @@ void AbstractPropagateRemoteDeleteEncrypted::unlockFolder() } qCDebug(ABSTRACT_PROPAGATE_REMOVE_ENCRYPTED) << "Unlocking folder" << _folderId; - auto unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), _folderId, _folderToken, this); + auto unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), _folderId, _folderToken, _propagator->_journal, this); connect(unlockJob, &UnlockEncryptFolderApiJob::success, this, &AbstractPropagateRemoteDeleteEncrypted::slotFolderUnLockedSuccessfully); connect(unlockJob, &UnlockEncryptFolderApiJob::error, this, [this] (const QByteArray& fileId, int httpReturnCode) { diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 4277af839..7cd3dd248 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -632,6 +632,42 @@ QByteArray privateKeyToPem(const QByteArray key) { return pem; } +QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data) +{ + Q_ASSERT(!key.isNull()); + if (key.isNull()) { + qCDebug(lcCse) << "Public key is null. Could not encrypt."; + return {}; + } + Bio publicKeyBio; + const auto publicKeyPem = key.toPem(); + BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); + const auto publicKey = ClientSideEncryption::PKey::readPublicKey(publicKeyBio); + return EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64()); +} + +QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data) +{ + Q_ASSERT(!privateKeyPem.isEmpty()); + if (privateKeyPem.isEmpty()) { + qCDebug(lcCse) << "Private key is empty. Could not encrypt."; + return {}; + } + + Bio privateKeyBio; + BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); + const auto key = ClientSideEncryption::PKey::readPrivateKey(privateKeyBio); + + // Also base64 decode the result + const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(key, QByteArray::fromBase64(data)); + + if (decryptResult.isEmpty()) { + qCDebug(lcCse()) << "ERROR. Could not decrypt data"; + return {}; + } + return QByteArray::fromBase64(decryptResult); +} + QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) { QByteArray iv = generateRandom(16); diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 335c76746..239bf877e 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -47,6 +47,8 @@ namespace EncryptionHelper { const QByteArray& key, const QByteArray& data ); + OWNCLOUDSYNC_EXPORT QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data); + OWNCLOUDSYNC_EXPORT QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data); QByteArray privateKeyToPem(const QByteArray key); diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index e99af0948..2e52a02a9 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -17,6 +17,7 @@ #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(lcStorePrivateKeyApiJob, "nextcloud.sync.networkjob.storeprivatekey", QtInfoMsg) @@ -153,8 +154,12 @@ bool UpdateMetadataApiJob::finished() UnlockEncryptFolderApiJob::UnlockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, const QByteArray& token, + SyncJournalDb *journalDb, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId), _token(token) + : AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent) + , _fileId(fileId) + , _token(token) + , _journalDb(journalDb) { } @@ -169,11 +174,22 @@ void UnlockEncryptFolderApiJob::start() AbstractNetworkJob::start(); qCInfo(lcCseJob()) << "Starting the request to unlock."; + + qCInfo(lcCseJob()) << "unlock folder started for:" << path() << " for fileId: " << _fileId; } bool UnlockEncryptFolderApiJob::finished() { int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + qCInfo(lcCseJob()) << "unlock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId; + + if (retCode != 0) { + _journalDb->deleteE2EeLockedFolder(_fileId); + } + + emit done(); + if (retCode != 200) { qCInfo(lcCseJob()) << "error unlocking file" << path() << errorString() << retCode; qCInfo(lcCseJob()) << "Full Error Log" << reply()->readAll(); @@ -217,13 +233,33 @@ bool DeleteMetadataApiJob::finished() return true; } -LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr& account, const QByteArray& fileId, QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent), _fileId(fileId) +LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr &account, + const QByteArray &fileId, + SyncJournalDb *journalDb, + const QSslKey publicKey, + QObject *parent) + : AbstractNetworkJob(account, e2eeBaseUrl() + QStringLiteral("lock/") + fileId, parent) + , _fileId(fileId) + , _journalDb(journalDb) + , _publicKey(publicKey) { } void LockEncryptFolderApiJob::start() { + const auto folderTokenEncrypted = _journalDb->e2EeLockedFolder(_fileId); + + if (!folderTokenEncrypted.isEmpty()) { + qCInfo(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId << " but we need to first lift the previous lock"; + const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, folderTokenEncrypted); + const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, folderToken, _journalDb, this); + connect(unlockJob, &UnlockEncryptFolderApiJob::done, this, [this]() { + this->start(); + }); + unlockJob->start(); + return; + } + QNetworkRequest req; req.setRawHeader("OCS-APIREQUEST", "true"); QUrlQuery query; @@ -234,23 +270,32 @@ void LockEncryptFolderApiJob::start() qCInfo(lcCseJob()) << "locking the folder with id" << _fileId << "as encrypted"; sendRequest("POST", url, req); AbstractNetworkJob::start(); + + qCInfo(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId; } 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, errorString()); + qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId; 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(); + 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(); qCInfo(lcCseJob()) << "got json:" << token; + qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token; + + const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token); + _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted); + //TODO: Parse the token and submit. emit success(_fileId, token); return true; diff --git a/src/libsync/clientsideencryptionjobs.h b/src/libsync/clientsideencryptionjobs.h index c32038292..f5cf6fbb6 100644 --- a/src/libsync/clientsideencryptionjobs.h +++ b/src/libsync/clientsideencryptionjobs.h @@ -5,6 +5,7 @@ #include "accountfwd.h" #include #include +#include namespace OCC { /* Here are all of the network jobs for the client side encryption. @@ -24,6 +25,8 @@ namespace OCC { * * @ingroup libsync */ + +class SyncJournalDb; class OWNCLOUDSYNC_EXPORT SignPublicKeyApiJob : public AbstractNetworkJob { Q_OBJECT @@ -142,7 +145,7 @@ class OWNCLOUDSYNC_EXPORT LockEncryptFolderApiJob : public AbstractNetworkJob { Q_OBJECT public: - explicit LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray& fileId, QObject *parent = nullptr); + explicit LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, SyncJournalDb *journalDb, const QSslKey publicKey, QObject *parent = nullptr); public slots: void start() override; @@ -158,6 +161,8 @@ signals: private: QByteArray _fileId; + QPointer _journalDb; + QSslKey _publicKey; }; @@ -169,6 +174,7 @@ public: const AccountPtr &account, const QByteArray& fileId, const QByteArray& token, + SyncJournalDb *journalDb, QObject *parent = nullptr); public slots: @@ -182,11 +188,13 @@ signals: void error(const QByteArray& fileId, const int httpReturnCode, const QString &errorMessage); + void done(); private: QByteArray _fileId; QByteArray _token; QBuffer *_tokenBuf; + QPointer _journalDb; }; diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index eadcb9357..3dba706ce 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -62,7 +62,7 @@ void EncryptFolderJob::slotEncryptionFlagSuccess(const QByteArray &fileId) qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error(); } - auto lockJob = new LockEncryptFolderApiJob(_account, fileId, this); + const auto lockJob = new LockEncryptFolderApiJob(_account, fileId, _journal, _account->e2e()->_publicKey, this); connect(lockJob, &LockEncryptFolderApiJob::success, this, &EncryptFolderJob::slotLockForEncryptionSuccess); connect(lockJob, &LockEncryptFolderApiJob::error, @@ -103,7 +103,7 @@ void EncryptFolderJob::slotLockForEncryptionSuccess(const QByteArray &fileId, co void EncryptFolderJob::slotUploadMetadataSuccess(const QByteArray &folderId) { - auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, this); + auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, _journal, this); connect(unlockJob, &UnlockEncryptFolderApiJob::success, this, &EncryptFolderJob::slotUnlockFolderSuccess); connect(unlockJob, &UnlockEncryptFolderApiJob::error, @@ -115,7 +115,7 @@ void EncryptFolderJob::slotUpdateMetadataError(const QByteArray &folderId, const { Q_UNUSED(httpReturnCode); - auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, this); + const auto unlockJob = new UnlockEncryptFolderApiJob(_account, folderId, _folderToken, _journal, this); connect(unlockJob, &UnlockEncryptFolderApiJob::success, this, &EncryptFolderJob::slotUnlockFolderSuccess); connect(unlockJob, &UnlockEncryptFolderApiJob::error, diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 8e7738e84..41bf8e8ae 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -82,7 +82,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedIdReceived(const QStringList & void PropagateUploadEncrypted::slotTryLock(const QByteArray& fileId) { - auto *lockJob = new LockEncryptFolderApiJob(_propagator->account(), fileId, this); + const auto lockJob = new LockEncryptFolderApiJob(_propagator->account(), fileId, _propagator->_journal, _propagator->account()->e2e()->_publicKey, this); connect(lockJob, &LockEncryptFolderApiJob::success, this, &PropagateUploadEncrypted::slotFolderLockedSuccessfully); connect(lockJob, &LockEncryptFolderApiJob::error, this, &PropagateUploadEncrypted::slotFolderLockedError); lockJob->start(); @@ -288,8 +288,7 @@ void PropagateUploadEncrypted::unlockFolder() _isUnlockRunning = true; qDebug() << "Calling Unlock"; - auto *unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), - _folderId, _folderToken, this); + auto *unlockJob = new UnlockEncryptFolderApiJob(_propagator->account(), _folderId, _folderToken, _propagator->_journal, this); connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) { qDebug() << "Successfully Unlocked"; diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 747a92a7c..fe89c34e1 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -30,6 +30,8 @@ #include "configfile.h" #include "discovery.h" #include "common/vfs.h" +#include "clientsideencryption.h" +#include "clientsideencryptionjobs.h" #ifdef Q_OS_WIN #include @@ -483,6 +485,18 @@ void SyncEngine::startSync() job->start(); return; } + + const auto e2EeLockedFolders = _journal->e2EeLockedFolders(); + + if (!e2EeLockedFolders.isEmpty()) { + for (const auto &e2EeLockedFolder : e2EeLockedFolders) { + const auto folderId = e2EeLockedFolder.first; + qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId; + const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, e2EeLockedFolder.second); + const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, folderToken, _journal, this); + unlockJob->start(); + } + } } if (s_anySyncRunning || _syncRunning) {