Always unlock E2EE folders, even when network failure or crash.

Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
alex-z 2023-01-25 18:09:37 +01:00
parent 2a64fc782b
commit bd9eb0c89f
11 changed files with 211 additions and 15 deletions

View File

@ -103,6 +103,10 @@ public:
CountDehydratedFilesQuery,
SetPinStateQuery,
WipePinStateQuery,
SetE2EeLockedFolderQuery,
GetE2EeLockedFolderQuery,
GetE2EeLockedFoldersQuery,
DeleteE2EeLockedFolderQuery,
PreparedQueryCount
};

View 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);

View 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.

View 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) {

View 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);

View 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);

View 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;

View 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;
};

View 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,

View 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";

View 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) {