Always unlock E2EE folders, even when network failure or crash.
authoralex-z <blackslayer4@gmail.com>
Wed, 25 Jan 2023 17:09:37 +0000 (18:09 +0100)
committeralex-z <blackslayer4@gmail.com>
Mon, 30 Jan 2023 15:01:19 +0000 (16:01 +0100)
Signed-off-by: alex-z <blackslayer4@gmail.com>
src/common/preparedsqlquerymanager.h
src/common/syncjournaldb.cpp
src/common/syncjournaldb.h
src/libsync/abstractpropagateremotedeleteencrypted.cpp
src/libsync/clientsideencryption.cpp
src/libsync/clientsideencryption.h
src/libsync/clientsideencryptionjobs.cpp
src/libsync/clientsideencryptionjobs.h
src/libsync/encryptfolderjob.cpp
src/libsync/propagateuploadencrypted.cpp
src/libsync/syncengine.cpp

index 97c57e73672f1595ac5a084f7fe1c549060dbb91..beea7d74b10415dbb0882db8985d7839c849da3a 100644 (file)
@@ -103,6 +103,10 @@ public:
         CountDehydratedFilesQuery,
         SetPinStateQuery,
         WipePinStateQuery,
+        SetE2EeLockedFolderQuery,
+        GetE2EeLockedFolderQuery,
+        GetE2EeLockedFoldersQuery,
+        DeleteE2EeLockedFolderQuery,
 
         PreparedQueryCount
     };
index 7fdd76560f04a9ee3f58c5191d8f2b7ebbf29090..84668ed26f924691e6432c18bb0f4298ea5c176f 100644 (file)
@@ -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<QPair<QByteArray, QByteArray>> SyncJournalDb::e2EeLockedFolders()
+{
+    QMutexLocker locker(&_mutex);
+
+    QList<QPair<QByteArray, QByteArray>> 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<PinState> SyncJournalDb::PinStateInterface::rawForPath(const QByteArray &path)
 {
     QMutexLocker lock(&_db->_mutex);
index 2da2d967bcd2dbf750d0e6dc96056400a18287d6..4a0d3f54dafcf26cc08f808fbeaa298a4ba63eeb 100644 (file)
@@ -287,6 +287,11 @@ public:
      */
     void markVirtualFileForDownloadRecursively(const QByteArray &path);
 
+    void setE2EeLockedFolder(const QByteArray &folderId, const QByteArray &folderToken);
+    QByteArray e2EeLockedFolder(const QByteArray &folderId);
+    QList<QPair<QByteArray, QByteArray>> e2EeLockedFolders();
+    void deleteE2EeLockedFolder(const QByteArray &folderId);
+
     /** Grouping for all functions relating to pin states,
      *
      * Use internalPinStates() to get at them.
index 5dd817244ec96b3a5d216a7ca656ce1c8b82e11a..2cbfd698b8a82395f1bae919e03e6dfea3baf8b6 100644 (file)
@@ -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) {
index 4277af83925d53de2c20cd04840708bfbca1b69b..7cd3dd2484a469f9fc3051cff89d30e28b4b190f 100644 (file)
@@ -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);
 
index 335c767465604199d875cfda7de168a7cbb457ba..239bf877e9a0eb1ce98f24eb5367a79e76a9c938 100644 (file)
@@ -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);
 
index e99af0948a35a587279c6692133e29d0c5f6bb70..2e52a02a99176ac277900ec553270bb40dd09458 100644 (file)
@@ -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;
index c32038292e0dc2f584006137236fc537ef8e900b..f5cf6fbb6a2138c7c8b941fd7ed8fded5f670ff6 100644 (file)
@@ -5,6 +5,7 @@
 #include "accountfwd.h"
 #include <QString>
 #include <QJsonDocument>
+#include <QSslKey>
 
 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<SyncJournalDb> _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<SyncJournalDb> _journalDb;
 };
 
 
index eadcb935739ad05a28451afc9845b2cdec9d6a06..3dba706ce498a9c503297615e64d6d027d70ce4e 100644 (file)
@@ -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,
index 8e7738e84721062855ca3fde47f9a007c7410a5d..41bf8e8aea8ad6da63497e7795cee3b255838902 100644 (file)
@@ -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";
index 747a92a7c0f80454bd8c6ba6b8679e0b8b48756c..fe89c34e1febce16fa5ca7ebb11233217f8ba87f 100644 (file)
@@ -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 <windows.h>
@@ -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) {