Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/common/syncjournaldb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1289,6 +1289,31 @@ qint64 SyncJournalDb::keyValueStoreGetInt(const QString &key, qint64 defaultValu
return query->int64Value(0);
}

QString SyncJournalDb::keyValueStoreGetString(const QString &key, const QString &defaultValue)
{
QMutexLocker locker(&_mutex);
if (!checkConnect()) {
return defaultValue;
}

const auto query = _queryManager.get(PreparedSqlQueryManager::GetKeyValueStoreQuery, QByteArrayLiteral("SELECT value FROM key_value_store WHERE key=?1"), _db);
if (!query) {
qCWarning(lcDb) << "database error:" << query->error();
return defaultValue;
}

query->bindValue(1, key);
query->exec();
const auto result = query->next();

if (!result.ok || !result.hasData) {
qCWarning(lcDb) << "database error:" << query->error();
return defaultValue;
}

return query->stringValue(0);
}

void SyncJournalDb::keyValueStoreDelete(const QString &key)
{
const auto query = _queryManager.get(PreparedSqlQueryManager::DeleteKeyValueStoreQuery, QByteArrayLiteral("DELETE FROM key_value_store WHERE key=?1;"), _db);
Expand Down
1 change: 1 addition & 0 deletions src/common/syncjournaldb.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject

void keyValueStoreSet(const QString &key, QVariant value);
[[nodiscard]] qint64 keyValueStoreGetInt(const QString &key, qint64 defaultValue);
[[nodiscard]] QString keyValueStoreGetString(const QString &key, const QString &defaultValue = {});
void keyValueStoreDelete(const QString &key);

[[nodiscard]] bool deleteFileRecord(const QString &filename, bool recursively = false);
Expand Down
99 changes: 99 additions & 0 deletions src/libsync/encryptedfoldermetadatahandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,83 @@
#include "clientsideencryptionjobs.h"
#include "clientsideencryption.h"

#include <QJsonArray>
#include <QJsonDocument>
#include <QLoggingCategory>
#include <QNetworkReply>
#include <algorithm>

namespace OCC {

Q_LOGGING_CATEGORY(lcFetchAndUploadE2eeFolderMetadataJob, "nextcloud.sync.propagator.encryptedfoldermetadatahandler", QtInfoMsg)

}

namespace {
constexpr auto counterStoreKeyPrefix = "e2ee_metadata_counter:";
constexpr auto keyChecksumsStoreKeyPrefix = "e2ee_metadata_key_checksums:";

QString normalizeStorePath(const QString &path)
{
const auto normalized = OCC::Utility::noLeadingSlashPath(OCC::Utility::noTrailingSlashPath(path));
return normalized.isEmpty() ? QStringLiteral("/") : normalized;
}

QString counterStoreKey(const QString &folderPath)
{
return QString::fromLatin1(counterStoreKeyPrefix) + normalizeStorePath(folderPath);
}

QString keyChecksumsStoreKey(const QString &rootPath)
{
return QString::fromLatin1(keyChecksumsStoreKeyPrefix) + normalizeStorePath(rootPath);
}

QString rootKeyPath(const OCC::RootEncryptedFolderInfo &rootEncryptedFolderInfo, const QString &folderFullRemotePath)
{
const auto normalizedRoot = normalizeStorePath(rootEncryptedFolderInfo.path);
if (normalizedRoot == QStringLiteral("/")) {
return normalizeStorePath(folderFullRemotePath);
}
return normalizedRoot;
}

QSet<QByteArray> parseKeyChecksums(const QString &value)
{
if (value.isEmpty()) {
return {};
}
const auto doc = QJsonDocument::fromJson(value.toUtf8());
if (!doc.isArray()) {
return {};
}
QSet<QByteArray> checksums;
for (const auto &entry : doc.array()) {
const auto checksum = entry.toString().toUtf8();
if (!checksum.isEmpty()) {
checksums.insert(checksum);
}
}
return checksums;
}

QString serializeKeyChecksums(const QSet<QByteArray> &checksums)
{
if (checksums.isEmpty()) {
return {};
}
auto sortedChecksums = checksums.values();
std::sort(sortedChecksums.begin(), sortedChecksums.end());
QJsonArray array;
for (const auto &checksum : sortedChecksums) {
if (!checksum.isEmpty()) {
array.push_back(QString::fromUtf8(checksum));
}
}
return QString::fromUtf8(QJsonDocument(array).toJson(QJsonDocument::Compact));
}
}

namespace OCC {

EncryptedFolderMetadataHandler::EncryptedFolderMetadataHandler(const AccountPtr &account,
Expand All @@ -44,6 +112,22 @@ EncryptedFolderMetadataHandler::EncryptedFolderMetadataHandler(const AccountPtr
void EncryptedFolderMetadataHandler::fetchMetadata(const FetchMode fetchMode)
{
_fetchMode = fetchMode;
if (_journalDb) {
const auto storedCounter = _journalDb->keyValueStoreGetInt(counterStoreKey(_folderFullRemotePath), -1);
if (storedCounter >= 0) {
const auto storedCounterValue = static_cast<quint64>(storedCounter);
if (_rootEncryptedFolderInfo.counter < storedCounterValue) {
_rootEncryptedFolderInfo.counter = storedCounterValue;
}
}

const auto storedChecksumsRaw =
_journalDb->keyValueStoreGetString(keyChecksumsStoreKey(rootKeyPath(_rootEncryptedFolderInfo, _folderFullRemotePath)));
const auto storedChecksums = parseKeyChecksums(storedChecksumsRaw);
if (!storedChecksums.isEmpty()) {
_rootEncryptedFolderInfo.keyChecksums.unite(storedChecksums);
}
}
fetchFolderEncryptedId();
}

Expand Down Expand Up @@ -190,6 +274,21 @@ void EncryptedFolderMetadataHandler::slotMetadataReceived(const QJsonDocument &j
emit fetchFinished(-1, tr("Error parsing or decrypting metadata."));
return;
}
if (_journalDb) {
const auto counterValue = metadata->counter();
if (counterValue > 0) {
_journalDb->keyValueStoreSet(counterStoreKey(_folderFullRemotePath), static_cast<qulonglong>(counterValue));
}
const auto keyChecksums = metadata->keyChecksums();
if (!keyChecksums.isEmpty()) {
const auto rootPath = rootKeyPath(_rootEncryptedFolderInfo, _folderFullRemotePath);
_journalDb->keyValueStoreSet(keyChecksumsStoreKey(rootPath), serializeKeyChecksums(keyChecksums));
}
}
_rootEncryptedFolderInfo.counter = metadata->counter();
if (!metadata->keyChecksums().isEmpty()) {
_rootEncryptedFolderInfo.keyChecksums = metadata->keyChecksums();
}
_folderMetadata = metadata;
emit fetchFinished(200);
});
Expand Down
50 changes: 37 additions & 13 deletions src/libsync/foldermetadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ FolderMetadata::FolderMetadata(AccountPtr account,
, _metadataKeyForEncryption(rootEncryptedFolderInfo.keyForEncryption)
, _metadataKeyForDecryption(rootEncryptedFolderInfo.keyForDecryption)
, _keyChecksums(rootEncryptedFolderInfo.keyChecksums)
, _counter(rootEncryptedFolderInfo.counter)
, _initialSignature(signature)
{
Q_ASSERT(!_remoteFolderRoot.isEmpty());
Expand Down Expand Up @@ -213,17 +214,32 @@ void FolderMetadata::setupExistingMetadata(const QByteArray &metadata)

const auto cipherTextDocument = QJsonDocument::fromJson(cipherTextDecrypted);

const auto previousKeyChecksums = _keyChecksums;
const auto keyCheckSums = cipherTextDocument[keyChecksumsKey].toArray();
if (!keyCheckSums.isEmpty()) {
_keyChecksums.clear();
}
for (auto it = keyCheckSums.constBegin(); it != keyCheckSums.constEnd(); ++it) {
const auto keyChecksum = it->toVariant().toString().toUtf8();
if (!keyChecksum.isEmpty()) {
//TODO: check that no hash has been removed from the keyChecksums
// How do we check that?
_keyChecksums.insert(keyChecksum);
if (keyCheckSums.isEmpty()) {
if (_isRootEncryptedFolder && !previousKeyChecksums.isEmpty()) {
qCWarning(lcCseMetadata()) << "Missing keyChecksums in metadata.";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
} else {
QSet<QByteArray> parsedKeyChecksums;
for (auto it = keyCheckSums.constBegin(); it != keyCheckSums.constEnd(); ++it) {
const auto keyChecksum = it->toVariant().toString().toUtf8();
if (!keyChecksum.isEmpty()) {
parsedKeyChecksums.insert(keyChecksum);
}
}
if (_isRootEncryptedFolder && !previousKeyChecksums.isEmpty()) {
for (auto it = previousKeyChecksums.constBegin(); it != previousKeyChecksums.constEnd(); ++it) {
if (!parsedKeyChecksums.contains(*it)) {
qCWarning(lcCseMetadata()) << "Detected removal of metadata key checksums.";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
}
}
_keyChecksums = parsedKeyChecksums;
}

if (!verifyMetadataKey(metadataKeyForDecryption())) {
Expand All @@ -238,10 +254,13 @@ void FolderMetadata::setupExistingMetadata(const QByteArray &metadata)

const auto counterVariantFromJson = cipherTextObj.value(counterKey).toVariant();
if (counterVariantFromJson.isValid() && counterVariantFromJson.canConvert<quint64>()) {
// TODO: We need to check counter: new counter must be greater than locally stored counter
// What does that mean? We store the counter in metadata, should we now store it in local database as we do for all file records in SyncJournal?
// What if metadata was not updated for a while? The counter will then not be greater than locally stored (in SyncJournal DB?)
_counter = counterVariantFromJson.value<quint64>();
const auto counterFromJson = counterVariantFromJson.value<quint64>();
if (_counter > 0 && counterFromJson < _counter) {
qCWarning(lcCseMetadata()) << "Detected metadata counter rollback.";
_account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError);
return;
}
_counter = counterFromJson;
}

for (auto it = files.constBegin(), end = files.constEnd(); it != end; ++it) {
Expand Down Expand Up @@ -835,6 +854,11 @@ quint64 FolderMetadata::newCounter() const
return _counter + 1;
}

quint64 FolderMetadata::counter() const
{
return _counter;
}

EncryptionStatusEnums::ItemEncryptionStatus FolderMetadata::fromMedataVersionToItemEncryptionStatus(const MetadataVersion metadataVersion)
{
switch (metadataVersion) {
Expand Down
1 change: 1 addition & 0 deletions src/libsync/foldermetadata.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject
[[nodiscard]] bool isVersion2AndUp() const;

[[nodiscard]] quint64 newCounter() const;
[[nodiscard]] quint64 counter() const;

[[nodiscard]] QByteArray metadataSignature() const;

Expand Down
94 changes: 94 additions & 0 deletions test/testclientsideencryptionv2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "syncenginetestutils.h"
#include "clientsideencryption.h"
#include "foldermetadata.h"
#include "common/checksums.h"
#include <QtTest>

using namespace OCC;
Expand Down Expand Up @@ -412,6 +413,99 @@ private slots:
}
QVERIFY(isFirstUserPresentAndCanDecrypt);
}

void testRejectsKeyChecksumRemoval()
{
QScopedPointer<FolderMetadata> metadata(new FolderMetadata(_account, "/", FolderMetadata::FolderType::Root));
QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete);
metadataSetupCompleteSpy.wait();
QCOMPARE(metadataSetupCompleteSpy.count(), 1);
QVERIFY(metadata->isValid());

FolderMetadata::EncryptedFile encryptedFile;
encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16);
encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename();
encryptedFile.originalFilename = "fakefile.txt";
encryptedFile.mimetype = "application/octet-stream";
encryptedFile.initializationVector = EncryptionHelper::generateRandom(16);
metadata->addEncryptedFile(encryptedFile);

QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->getCertificate(), FolderMetadata::CertificateType::SoftwareNextcloudCertificate));

const auto previousChecksums = metadata->keyChecksums();
QVERIFY(previousChecksums.size() > 1);

const auto currentChecksum = calcSha256(metadata->metadataKeyForEncryption());
QVERIFY(previousChecksums.contains(currentChecksum));

auto reducedChecksums = previousChecksums;
for (const auto &checksum : previousChecksums) {
if (checksum != currentChecksum) {
reducedChecksums.remove(checksum);
break;
}
}
QCOMPARE(reducedChecksums.size(), previousChecksums.size() - 1);

metadata->_keyChecksums = reducedChecksums;

const auto tamperedMetadata = metadata->encryptedMetadata();
const auto tamperedSignature = metadata->metadataSignature();
QVERIFY(!tamperedMetadata.isEmpty());
QVERIFY(!tamperedSignature.isEmpty());

auto tamperedMetadataCopy = tamperedMetadata;
tamperedMetadataCopy.replace("\"", "\\\"");
const QJsonDocument ocsDoc = QJsonDocument::fromJson(
QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(tamperedMetadataCopy)).toUtf8());

const auto rootInfo = RootEncryptedFolderInfo(QStringLiteral("/"),
metadata->metadataKeyForEncryption(),
metadata->metadataKeyForEncryption(),
previousChecksums);

QScopedPointer<FolderMetadata> metadataFromJson(new FolderMetadata(_account, "/", ocsDoc.toJson(), rootInfo, tamperedSignature));
QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJson.data(), &FolderMetadata::setupComplete);
metadataSetupExistingCompleteSpy.wait();
QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1);
QVERIFY(!metadataFromJson->isValid());
}

void testRejectsCounterRollback()
{
QScopedPointer<FolderMetadata> metadata(new FolderMetadata(_account, "/", FolderMetadata::FolderType::Root));
QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete);
metadataSetupCompleteSpy.wait();
QCOMPARE(metadataSetupCompleteSpy.count(), 1);
QVERIFY(metadata->isValid());

FolderMetadata::EncryptedFile encryptedFile;
encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16);
encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename();
encryptedFile.originalFilename = "fakefile.txt";
encryptedFile.mimetype = "application/octet-stream";
encryptedFile.initializationVector = EncryptionHelper::generateRandom(16);
metadata->addEncryptedFile(encryptedFile);

const auto encryptedMetadata = metadata->encryptedMetadata();
const auto signature = metadata->metadataSignature();
QVERIFY(!encryptedMetadata.isEmpty());
QVERIFY(!signature.isEmpty());

auto encryptedMetadataCopy = encryptedMetadata;
encryptedMetadataCopy.replace("\"", "\\\"");
const QJsonDocument ocsDoc = QJsonDocument::fromJson(
QStringLiteral("{\"ocs\": {\"data\": {\"meta-data\": \"%1\"}}}").arg(QString::fromUtf8(encryptedMetadataCopy)).toUtf8());

const auto previousCounter = metadata->newCounter() + 1;
const auto rootInfo = RootEncryptedFolderInfo(QStringLiteral("/"), {}, {}, metadata->keyChecksums(), previousCounter);

QScopedPointer<FolderMetadata> metadataFromJson(new FolderMetadata(_account, "/", ocsDoc.toJson(), rootInfo, signature));
QSignalSpy metadataSetupExistingCompleteSpy(metadataFromJson.data(), &FolderMetadata::setupComplete);
metadataSetupExistingCompleteSpy.wait();
QCOMPARE(metadataSetupExistingCompleteSpy.count(), 1);
QVERIFY(!metadataFromJson->isValid());
}
};

QTEST_GUILESS_MAIN(TestClientSideEncryptionV2)
Expand Down