Always unlock E2EE folders, even when network failure or crash.
Signed-off-by: alex-z <blackslayer4@gmail.com>
This commit is contained in:
parent
2a64fc782b
commit
bd9eb0c89f
|
@ -103,6 +103,10 @@ public:
|
|||
CountDehydratedFilesQuery,
|
||||
SetPinStateQuery,
|
||||
WipePinStateQuery,
|
||||
SetE2EeLockedFolderQuery,
|
||||
GetE2EeLockedFolderQuery,
|
||||
GetE2EeLockedFoldersQuery,
|
||||
DeleteE2EeLockedFolderQuery,
|
||||
|
||||
PreparedQueryCount
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue