From 6a4028564e9ea25f0ad3a7fd81c6ca48f1822383 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 12:07:14 +0200 Subject: [PATCH 01/16] fix indentation style Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 772 +++++++++++++-------------- 1 file changed, 386 insertions(+), 386 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 0bc507d1a9..22b8082224 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -61,259 +61,259 @@ QString e2eeBaseUrl() } namespace { - constexpr char accountProperty[] = "account"; +constexpr char accountProperty[] = "account"; - const char e2e_cert[] = "_e2e-certificate"; - const char e2e_private[] = "_e2e-private"; - const char e2e_mnemonic[] = "_e2e-mnemonic"; +const char e2e_cert[] = "_e2e-certificate"; +const char e2e_private[] = "_e2e-private"; +const char e2e_mnemonic[] = "_e2e-mnemonic"; - constexpr qint64 blockSize = 1024; +constexpr qint64 blockSize = 1024; - QList oldCipherFormatSplit(const QByteArray &cipher) - { - const auto separator = QByteArrayLiteral("fA=="); // BASE64 encoded '|' - auto result = QList(); +QList oldCipherFormatSplit(const QByteArray &cipher) +{ + const auto separator = QByteArrayLiteral("fA=="); // BASE64 encoded '|' + auto result = QList(); - auto data = cipher; - auto index = data.indexOf(separator); - while (index >=0) { - result.append(data.left(index)); - data = data.mid(index + separator.size()); - index = data.indexOf(separator); - } - - result.append(data); - return result; + auto data = cipher; + auto index = data.indexOf(separator); + while (index >=0) { + result.append(data.left(index)); + data = data.mid(index + separator.size()); + index = data.indexOf(separator); } - QList splitCipherParts(const QByteArray &data) - { - const auto isOldFormat = !data.contains('|'); - const auto parts = isOldFormat ? oldCipherFormatSplit(data) : data.split('|'); - qCInfo(lcCse()) << "found parts:" << parts << "old format?" << isOldFormat; - return parts; - } + result.append(data); + return result; +} + +QList splitCipherParts(const QByteArray &data) +{ + const auto isOldFormat = !data.contains('|'); + const auto parts = isOldFormat ? oldCipherFormatSplit(data) : data.split('|'); + qCInfo(lcCse()) << "found parts:" << parts << "old format?" << isOldFormat; + return parts; +} } // ns namespace { - unsigned char* unsignedData(QByteArray& array) +unsigned char* unsignedData(QByteArray& array) +{ + return (unsigned char*)array.data(); +} + + // + // Simple classes for safe (RAII) handling of OpenSSL + // data structures + // + +class CipherCtx { +public: + CipherCtx() + : _ctx(EVP_CIPHER_CTX_new()) { - return (unsigned char*)array.data(); } - // - // Simple classes for safe (RAII) handling of OpenSSL - // data structures - // - - class CipherCtx { - public: - CipherCtx() - : _ctx(EVP_CIPHER_CTX_new()) - { - } - - ~CipherCtx() - { - EVP_CIPHER_CTX_free(_ctx); - } - - operator EVP_CIPHER_CTX*() - { - return _ctx; - } - - private: - Q_DISABLE_COPY(CipherCtx) - - EVP_CIPHER_CTX* _ctx; - }; - - class Bio { - public: - Bio() - : _bio(BIO_new(BIO_s_mem())) - { - } - - ~Bio() - { - BIO_free_all(_bio); - } - - operator BIO*() - { - return _bio; - } - - private: - Q_DISABLE_COPY(Bio) - - BIO* _bio; - }; - - class PKeyCtx { - public: - explicit PKeyCtx(int id, ENGINE *e = nullptr) - : _ctx(EVP_PKEY_CTX_new_id(id, e)) - { - } - - ~PKeyCtx() - { - EVP_PKEY_CTX_free(_ctx); - } - - // The move constructor is needed for pre-C++17 where - // return-value optimization (RVO) is not obligatory - // and we have a `forKey` static function that returns - // an instance of this class - PKeyCtx(PKeyCtx&& other) - { - std::swap(_ctx, other._ctx); - } - - PKeyCtx& operator=(PKeyCtx&& other) = delete; - - static PKeyCtx forKey(EVP_PKEY *pkey, ENGINE *e = nullptr) - { - PKeyCtx ctx; - ctx._ctx = EVP_PKEY_CTX_new(pkey, e); - return ctx; - } - - operator EVP_PKEY_CTX*() - { - return _ctx; - } - - private: - Q_DISABLE_COPY(PKeyCtx) - - PKeyCtx() = default; - - EVP_PKEY_CTX* _ctx = nullptr; - }; - - } - - class ClientSideEncryption::PKey { - public: - ~PKey() - { - EVP_PKEY_free(_pkey); - } - - // The move constructor is needed for pre-C++17 where - // return-value optimization (RVO) is not obligatory - // and we have a static functions that return - // an instance of this class - PKey(PKey&& other) - { - std::swap(_pkey, other._pkey); - } - - PKey& operator=(PKey&& other) = delete; - - static PKey readPublicKey(Bio &bio) - { - PKey result; - result._pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); - return result; - } - - static PKey readPrivateKey(Bio &bio) - { - PKey result; - result._pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); - return result; - } - - static PKey generate(PKeyCtx& ctx) - { - PKey result; - if (EVP_PKEY_keygen(ctx, &result._pkey) <= 0) { - result._pkey = nullptr; - } - return result; - } - - operator EVP_PKEY*() - { - return _pkey; - } - - operator EVP_PKEY*() const - { - return _pkey; - } - - private: - Q_DISABLE_COPY(PKey) - - PKey() = default; - - EVP_PKEY* _pkey = nullptr; - }; - - namespace + ~CipherCtx() { - class X509Certificate { - public: - ~X509Certificate() - { - X509_free(_certificate); - } - - // The move constructor is needed for pre-C++17 where - // return-value optimization (RVO) is not obligatory - // and we have a static functions that return - // an instance of this class - X509Certificate(X509Certificate&& other) - { - std::swap(_certificate, other._certificate); - } - - X509Certificate& operator=(X509Certificate&& other) = delete; - - static X509Certificate readCertificate(Bio &bio) - { - X509Certificate result; - result._certificate = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); - return result; - } - - operator X509*() - { - return _certificate; - } - - operator X509*() const - { - return _certificate; - } - - private: - Q_DISABLE_COPY(X509Certificate) - - X509Certificate() = default; - - X509* _certificate = nullptr; - }; - - QByteArray BIO2ByteArray(Bio &b) { - auto pending = static_cast(BIO_ctrl_pending(b)); - QByteArray res(pending, '\0'); - BIO_read(b, unsignedData(res), pending); - return res; + EVP_CIPHER_CTX_free(_ctx); } - QByteArray handleErrors() + operator EVP_CIPHER_CTX*() { - Bio bioErrors; - ERR_print_errors(bioErrors); // This line is not printing anything. - return BIO2ByteArray(bioErrors); + return _ctx; } + +private: + Q_DISABLE_COPY(CipherCtx) + + EVP_CIPHER_CTX* _ctx; +}; + +class Bio { +public: + Bio() + : _bio(BIO_new(BIO_s_mem())) + { + } + + ~Bio() + { + BIO_free_all(_bio); + } + + operator BIO*() + { + return _bio; + } + +private: + Q_DISABLE_COPY(Bio) + + BIO* _bio; +}; + +class PKeyCtx { +public: + explicit PKeyCtx(int id, ENGINE *e = nullptr) + : _ctx(EVP_PKEY_CTX_new_id(id, e)) + { + } + + ~PKeyCtx() + { + EVP_PKEY_CTX_free(_ctx); + } + + // The move constructor is needed for pre-C++17 where + // return-value optimization (RVO) is not obligatory + // and we have a `forKey` static function that returns + // an instance of this class + PKeyCtx(PKeyCtx&& other) + { + std::swap(_ctx, other._ctx); + } + + PKeyCtx& operator=(PKeyCtx&& other) = delete; + + static PKeyCtx forKey(EVP_PKEY *pkey, ENGINE *e = nullptr) + { + PKeyCtx ctx; + ctx._ctx = EVP_PKEY_CTX_new(pkey, e); + return ctx; + } + + operator EVP_PKEY_CTX*() + { + return _ctx; + } + +private: + Q_DISABLE_COPY(PKeyCtx) + + PKeyCtx() = default; + + EVP_PKEY_CTX* _ctx = nullptr; +}; + +} + +class ClientSideEncryption::PKey { +public: + ~PKey() + { + EVP_PKEY_free(_pkey); + } + + // The move constructor is needed for pre-C++17 where + // return-value optimization (RVO) is not obligatory + // and we have a static functions that return + // an instance of this class + PKey(PKey&& other) + { + std::swap(_pkey, other._pkey); + } + + PKey& operator=(PKey&& other) = delete; + + static PKey readPublicKey(Bio &bio) + { + PKey result; + result._pkey = PEM_read_bio_PUBKEY(bio, nullptr, nullptr, nullptr); + return result; + } + + static PKey readPrivateKey(Bio &bio) + { + PKey result; + result._pkey = PEM_read_bio_PrivateKey(bio, nullptr, nullptr, nullptr); + return result; + } + + static PKey generate(PKeyCtx& ctx) + { + PKey result; + if (EVP_PKEY_keygen(ctx, &result._pkey) <= 0) { + result._pkey = nullptr; + } + return result; + } + + operator EVP_PKEY*() + { + return _pkey; + } + + operator EVP_PKEY*() const + { + return _pkey; + } + +private: + Q_DISABLE_COPY(PKey) + + PKey() = default; + + EVP_PKEY* _pkey = nullptr; +}; + +namespace +{ +class X509Certificate { +public: + ~X509Certificate() + { + X509_free(_certificate); + } + + // The move constructor is needed for pre-C++17 where + // return-value optimization (RVO) is not obligatory + // and we have a static functions that return + // an instance of this class + X509Certificate(X509Certificate&& other) + { + std::swap(_certificate, other._certificate); + } + + X509Certificate& operator=(X509Certificate&& other) = delete; + + static X509Certificate readCertificate(Bio &bio) + { + X509Certificate result; + result._certificate = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + return result; + } + + operator X509*() + { + return _certificate; + } + + operator X509*() const + { + return _certificate; + } + +private: + Q_DISABLE_COPY(X509Certificate) + + X509Certificate() = default; + + X509* _certificate = nullptr; +}; + +QByteArray BIO2ByteArray(Bio &b) { + auto pending = static_cast(BIO_ctrl_pending(b)); + QByteArray res(pending, '\0'); + BIO_read(b, unsignedData(res), pending); + return res; +} + +QByteArray handleErrors() +{ + Bio bioErrors; + ERR_print_errors(bioErrors); // This line is not printing anything. + return BIO2ByteArray(bioErrors); +} } @@ -357,7 +357,7 @@ QByteArray generatePassword(const QString& wordlist, const QByteArray& salt) { iterationCount, // int iterations, keyLength, // int keylen, unsignedData(secretKey) // unsigned char *out - ); + ); if (ret != 1) { qCInfo(lcCse()) << "Failed to generate encryption key"; @@ -370,10 +370,10 @@ QByteArray generatePassword(const QString& wordlist, const QByteArray& salt) { } QByteArray encryptPrivateKey( - const QByteArray& key, - const QByteArray& privateKey, - const QByteArray& salt - ) { + const QByteArray& key, + const QByteArray& privateKey, + const QByteArray& salt + ) { QByteArray iv = generateRandom(12); @@ -883,10 +883,10 @@ void ClientSideEncryption::initialize(const AccountPtr &account) void ClientSideEncryption::fetchFromKeyChain(const AccountPtr &account) { const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_cert, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_cert, + account->id() + ); auto *job = new ReadPasswordJob(Theme::instance()->appName()); job->setProperty(accountProperty, QVariant::fromValue(account)); @@ -970,10 +970,10 @@ void ClientSideEncryption::publicKeyFetched(Job *incoming) qCInfo(lcCse()) << "Public key fetched from keychain"; const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_private, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_private, + account->id() + ); auto *job = new ReadPasswordJob(Theme::instance()->appName()); job->setProperty(accountProperty, QVariant::fromValue(account)); @@ -1008,10 +1008,10 @@ void ClientSideEncryption::privateKeyFetched(Job *incoming) qCInfo(lcCse()) << "Private key fetched from keychain"; const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_mnemonic, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_mnemonic, + account->id() + ); auto *job = new ReadPasswordJob(Theme::instance()->appName()); job->setProperty(accountProperty, QVariant::fromValue(account)); @@ -1046,10 +1046,10 @@ void ClientSideEncryption::mnemonicKeyFetched(QKeychain::Job *incoming) void ClientSideEncryption::writePrivateKey(const AccountPtr &account) { const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_private, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_private, + account->id() + ); auto *job = new WritePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); @@ -1065,10 +1065,10 @@ void ClientSideEncryption::writePrivateKey(const AccountPtr &account) void ClientSideEncryption::writeCertificate(const AccountPtr &account) { const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_cert, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_cert, + account->id() + ); auto *job = new WritePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); @@ -1084,10 +1084,10 @@ void ClientSideEncryption::writeCertificate(const AccountPtr &account) void ClientSideEncryption::writeMnemonic(const AccountPtr &account) { const QString kck = AbstractCredentials::keychainKey( - account->url().toString(), - account->credentials()->user() + e2e_mnemonic, - account->id() - ); + account->url().toString(), + account->credentials()->user() + e2e_mnemonic, + account->id() + ); auto *job = new WritePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); @@ -1240,11 +1240,11 @@ void ClientSideEncryption::generateCSR(const AccountPtr &account, PKey keyPair) qCInfo(lcCse()) << "Getting the following array for the account Id" << cnArray; auto certParams = std::map{ - {"C", "DE"}, - {"ST", "Baden-Wuerttemberg"}, - {"L", "Stuttgart"}, - {"O","Nextcloud"}, - {"CN", cnArray.constData()} + {"C", "DE"}, + {"ST", "Baden-Wuerttemberg"}, + {"L", "Stuttgart"}, + {"O","Nextcloud"}, + {"CN", cnArray.constData()} }; int ret = 0; @@ -1253,8 +1253,8 @@ void ClientSideEncryption::generateCSR(const AccountPtr &account, PKey keyPair) // 2. set version of x509 req X509_REQ *x509_req = X509_REQ_new(); auto release_on_exit_x509_req = qScopeGuard([&] { - X509_REQ_free(x509_req); - }); + X509_REQ_free(x509_req); + }); ret = X509_REQ_set_version(x509_req, nVersion); @@ -1341,15 +1341,15 @@ void ClientSideEncryption::encryptPrivateKey(const AccountPtr &account) connect(job, &StorePrivateKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { Q_UNUSED(doc); switch(retCode) { - case 200: - qCInfo(lcCse()) << "Private key stored encrypted on server."; - writePrivateKey(account); - writeCertificate(account); - writeMnemonic(account); - emit initializationFinished(true); - break; - default: - qCInfo(lcCse()) << "Store private key failed, return code:" << retCode; + case 200: + qCInfo(lcCse()) << "Private key stored encrypted on server."; + writePrivateKey(account); + writeCertificate(account); + writeMnemonic(account); + emit initializationFinished(true); + break; + default: + qCInfo(lcCse()) << "Store private key failed, return code:" << retCode; } }); job->start(); @@ -1423,16 +1423,16 @@ void ClientSideEncryption::getPrivateKeyFromServer(const AccountPtr &account) qCInfo(lcCse()) << "Retrieving private key from server"; auto job = new JsonApiJob(account, e2eeBaseUrl() + "private-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { - if (retCode == 200) { - QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString(); - qCInfo(lcCse()) << key; - qCInfo(lcCse()) << "Found private key, lets decrypt it!"; - decryptPrivateKey(account, key.toLocal8Bit()); - } else if (retCode == 404) { - qCInfo(lcCse()) << "No private key on the server: setup is incomplete."; - } else { - qCInfo(lcCse()) << "Error while requesting public key: " << retCode; - } + if (retCode == 200) { + QString key = doc.object()["ocs"].toObject()["data"].toObject()["private-key"].toString(); + qCInfo(lcCse()) << key; + qCInfo(lcCse()) << "Found private key, lets decrypt it!"; + decryptPrivateKey(account, key.toLocal8Bit()); + } else if (retCode == 404) { + qCInfo(lcCse()) << "No private key on the server: setup is incomplete."; + } else { + qCInfo(lcCse()) << "Error while requesting public key: " << retCode; + } }); job->start(); } @@ -1442,23 +1442,23 @@ void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) qCInfo(lcCse()) << "Retrieving public key from server"; auto job = new JsonApiJob(account, e2eeBaseUrl() + "public-key", this); connect(job, &JsonApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { - if (retCode == 200) { - QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString(); - _certificate = QSslCertificate(publicKey.toLocal8Bit(), QSsl::Pem); - _publicKey = _certificate.publicKey(); - qCInfo(lcCse()) << "Found Public key, requesting Server Public Key. Public key:" << publicKey; - fetchAndValidatePublicKeyFromServer(account); - } else if (retCode == 404) { - qCInfo(lcCse()) << "No public key on the server"; - if (!account->e2eEncryptionKeysGenerationAllowed()) { - qCInfo(lcCse()) << "User did not allow E2E keys generation."; - emit initializationFinished(); - return; - } - generateKeyPair(account); - } else { - qCInfo(lcCse()) << "Error while requesting public key: " << retCode; + if (retCode == 200) { + QString publicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-keys"].toObject()[account->davUser()].toString(); + _certificate = QSslCertificate(publicKey.toLocal8Bit(), QSsl::Pem); + _publicKey = _certificate.publicKey(); + qCInfo(lcCse()) << "Found Public key, requesting Server Public Key. Public key:" << publicKey; + fetchAndValidatePublicKeyFromServer(account); + } else if (retCode == 404) { + qCInfo(lcCse()) << "No public key on the server"; + if (!account->e2eEncryptionKeysGenerationAllowed()) { + qCInfo(lcCse()) << "User did not allow E2E keys generation."; + emit initializationFinished(); + return; } + generateKeyPair(account); + } else { + qCInfo(lcCse()) << "Error while requesting public key: " << retCode; + } }); job->start(); } @@ -1507,79 +1507,79 @@ FolderMetadata::FolderMetadata(AccountPtr account, const QByteArray& metadata, i void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) { - /* This is the json response from the server, it contains two extra objects that we are *not* interested. - * ocs and data. - */ - QJsonDocument doc = QJsonDocument::fromJson(metadata); - qCInfo(lcCseMetadata()) << doc.toJson(QJsonDocument::Compact); - - // The metadata is being retrieved as a string stored in a json. - // This *seems* to be broken but the RFC doesn't explicits how it wants. - // I'm currently unsure if this is error on my side or in the server implementation. - // And because inside of the meta-data there's an object called metadata, without '-' - // make it really different. - - QString metaDataStr = doc.object()["ocs"] - .toObject()["data"] - .toObject()["meta-data"] - .toString(); - - QJsonDocument metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); - QJsonObject metadataObj = metaDataDoc.object()["metadata"].toObject(); - QJsonObject metadataKeys = metadataObj["metadataKeys"].toObject(); - - if (metadataKeys.isEmpty()) { - qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; - return; - } - - QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit(); - QJsonObject files = metaDataDoc.object()["files"].toObject(); - - _fileDrop = metaDataDoc.object().value("filedrop").toObject(); - - QJsonDocument debugHelper; - debugHelper.setObject(metadataKeys); - qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact); - - // Iterate over the document to store the keys. I'm unsure that the keys are in order, - // perhaps it's better to store a map instead of a vector, perhaps this just doesn't matter. - for(auto it = metadataKeys.constBegin(), end = metadataKeys.constEnd(); it != end; it++) { - QByteArray currB64Pass = it.value().toString().toLocal8Bit(); - /* - * We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC - * Now we should be compatible with Android and IOS. Maybe we can fix it later. + /* This is the json response from the server, it contains two extra objects that we are *not* interested. + * ocs and data. */ - QByteArray b64DecryptedKey = decryptData(currB64Pass); - if (b64DecryptedKey.isEmpty()) { - qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key(); - continue; + QJsonDocument doc = QJsonDocument::fromJson(metadata); + qCInfo(lcCseMetadata()) << doc.toJson(QJsonDocument::Compact); + + // The metadata is being retrieved as a string stored in a json. + // This *seems* to be broken but the RFC doesn't explicits how it wants. + // I'm currently unsure if this is error on my side or in the server implementation. + // And because inside of the meta-data there's an object called metadata, without '-' + // make it really different. + + QString metaDataStr = doc.object()["ocs"] + .toObject()["data"] + .toObject()["meta-data"] + .toString(); + + QJsonDocument metaDataDoc = QJsonDocument::fromJson(metaDataStr.toLocal8Bit()); + QJsonObject metadataObj = metaDataDoc.object()["metadata"].toObject(); + QJsonObject metadataKeys = metadataObj["metadataKeys"].toObject(); + + if (metadataKeys.isEmpty()) { + qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; + return; } - QByteArray decryptedKey = QByteArray::fromBase64(b64DecryptedKey); - _metadataKeys.insert(it.key().toInt(), decryptedKey); - } + QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit(); + QJsonObject files = metaDataDoc.object()["files"].toObject(); - // Cool, We actually have the key, we can decrypt the rest of the metadata. - qCDebug(lcCse) << "Sharing: " << sharing; - if (sharing.size()) { - const auto metaDataKey = !_metadataKeys.isEmpty() ? _metadataKeys.last() : QByteArray{}; - if (metaDataKey.isEmpty()) { - qCDebug(lcCse) << "Failed to decrypt sharing! Empty metadata key!"; - } else { - auto sharingDecrypted = decryptJsonObject(sharing, metaDataKey); - qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted; + _fileDrop = metaDataDoc.object().value("filedrop").toObject(); - // Sharing is also a JSON object, so extract it and populate. - auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted); - auto sharingObj = sharingDoc.object(); - for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) { - _sharing.push_back({it.key(), it.value().toString()}); - } - } - } else { - qCDebug(lcCse) << "Skipping sharing section since it is empty"; - } + QJsonDocument debugHelper; + debugHelper.setObject(metadataKeys); + qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact); + + // Iterate over the document to store the keys. I'm unsure that the keys are in order, + // perhaps it's better to store a map instead of a vector, perhaps this just doesn't matter. + for(auto it = metadataKeys.constBegin(), end = metadataKeys.constEnd(); it != end; it++) { + QByteArray currB64Pass = it.value().toString().toLocal8Bit(); + /* + * We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC + * Now we should be compatible with Android and IOS. Maybe we can fix it later. + */ + QByteArray b64DecryptedKey = decryptData(currB64Pass); + if (b64DecryptedKey.isEmpty()) { + qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key(); + continue; + } + + QByteArray decryptedKey = QByteArray::fromBase64(b64DecryptedKey); + _metadataKeys.insert(it.key().toInt(), decryptedKey); + } + + // Cool, We actually have the key, we can decrypt the rest of the metadata. + qCDebug(lcCse) << "Sharing: " << sharing; + if (sharing.size()) { + const auto metaDataKey = !_metadataKeys.isEmpty() ? _metadataKeys.last() : QByteArray{}; + if (metaDataKey.isEmpty()) { + qCDebug(lcCse) << "Failed to decrypt sharing! Empty metadata key!"; + } else { + auto sharingDecrypted = decryptJsonObject(sharing, metaDataKey); + qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted; + + // Sharing is also a JSON object, so extract it and populate. + auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted); + auto sharingObj = sharingDoc.object(); + for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) { + _sharing.push_back({it.key(), it.value().toString()}); + } + } + } else { + qCDebug(lcCse) << "Skipping sharing section since it is empty"; + } for (auto it = files.constBegin(), end = files.constEnd(); it != end; it++) { EncryptedFile file; @@ -1642,8 +1642,8 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const if (decryptResult.isEmpty()) { - qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; - return {}; + qCDebug(lcCse()) << "ERROR. Could not decrypt the metadata key"; + return {}; } return QByteArray::fromBase64(decryptResult); } @@ -1694,9 +1694,9 @@ QByteArray FolderMetadata::encryptedMetadata() const { } QJsonObject metadata = { - {"metadataKeys", metadataKeys}, - // {"sharing", sharingEncrypted}, - {"version", 1} + {"metadataKeys", metadataKeys}, + // {"sharing", sharingEncrypted}, + {"version", 1} }; QJsonObject files; @@ -1711,7 +1711,7 @@ QByteArray FolderMetadata::encryptedMetadata() const { QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKeys.last()); if (encryptedEncrypted.isEmpty()) { - qCDebug(lcCse) << "Metadata generation failed!"; + qCDebug(lcCse) << "Metadata generation failed!"; } QJsonObject file; @@ -1724,8 +1724,8 @@ QByteArray FolderMetadata::encryptedMetadata() const { } QJsonObject metaObject = { - {"metadata", metadata}, - {"files", files} + {"metadata", metadata}, + {"files", files} }; QJsonDocument internalMetadata; @@ -1813,10 +1813,10 @@ QJsonObject FolderMetadata::fileDrop() const bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &iv, QFile *input, QFile *output, QByteArray& returnTag) { if (!input->open(QIODevice::ReadOnly)) { - qCDebug(lcCse) << "Could not open input file for reading" << input->errorString(); + qCDebug(lcCse) << "Could not open input file for reading" << input->errorString(); } if (!output->open(QIODevice::WriteOnly)) { - qCDebug(lcCse) << "Could not oppen output file for writing" << output->errorString(); + qCDebug(lcCse) << "Could not oppen output file for writing" << output->errorString(); } // Init @@ -1891,7 +1891,7 @@ bool EncryptionHelper::fileEncryption(const QByteArray &key, const QByteArray &i } bool EncryptionHelper::fileDecryption(const QByteArray &key, const QByteArray& iv, - QFile *input, QFile *output) + QFile *input, QFile *output) { input->open(QIODevice::ReadOnly); output->open(QIODevice::WriteOnly); From 1b0a93eabc8f1322ef299cba3c4db81944c7d2c6 Mon Sep 17 00:00:00 2001 From: alex-z Date: Mon, 20 Mar 2023 15:31:46 +0100 Subject: [PATCH 02/16] Migrate E2EE from v1 to v1.1 Signed-off-by: alex-z --- src/libsync/capabilities.cpp | 16 +++ src/libsync/capabilities.h | 2 + src/libsync/clientsideencryption.cpp | 125 ++++++++++------------- src/libsync/clientsideencryption.h | 6 +- src/libsync/propagateuploadencrypted.cpp | 2 - 5 files changed, 77 insertions(+), 74 deletions(-) diff --git a/src/libsync/capabilities.cpp b/src/libsync/capabilities.cpp index 1f4131e00a..12b049823e 100644 --- a/src/libsync/capabilities.cpp +++ b/src/libsync/capabilities.cpp @@ -166,6 +166,22 @@ bool Capabilities::clientSideEncryptionAvailable() const return capabilityAvailable; } +double Capabilities::clientSideEncryptionVersion() const +{ + const auto foundEndToEndEncryptionInCaps = _capabilities.constFind(QStringLiteral("end-to-end-encryption")); + if (foundEndToEndEncryptionInCaps == _capabilities.constEnd()) { + return 1.0; + } + + const auto properties = (*foundEndToEndEncryptionInCaps).toMap(); + const auto enabled = properties.value(QStringLiteral("enabled"), false).toBool(); + if (!enabled) { + return false; + } + + return properties.value(QStringLiteral("api-version"), 1.0).toDouble(); +} + bool Capabilities::notificationsAvailable() const { // We require the OCS style API in 9.x, can't deal with the REST one only found in 8.2 diff --git a/src/libsync/capabilities.h b/src/libsync/capabilities.h index 1029bcc5e4..a65329de00 100644 --- a/src/libsync/capabilities.h +++ b/src/libsync/capabilities.h @@ -89,6 +89,8 @@ public: /// returns true if the server supports client side encryption [[nodiscard]] bool clientSideEncryptionAvailable() const; + [[nodiscard]] double clientSideEncryptionVersion() const; + /// returns true if the capabilities are loaded already. [[nodiscard]] bool isValid() const; diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 22b8082224..ef6d0982c1 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -67,8 +67,12 @@ const char e2e_cert[] = "_e2e-certificate"; const char e2e_private[] = "_e2e-private"; const char e2e_mnemonic[] = "_e2e-mnemonic"; +constexpr auto metadataKeyJsonKey = "metadataKey"; + constexpr qint64 blockSize = 1024; +constexpr auto metadataKeySize = 16; + QList oldCipherFormatSplit(const QByteArray &cipher) { const auto separator = QByteArrayLiteral("fA=="); // BASE64 encoded '|' @@ -1528,7 +1532,30 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) QJsonObject metadataObj = metaDataDoc.object()["metadata"].toObject(); QJsonObject metadataKeys = metadataObj["metadataKeys"].toObject(); - if (metadataKeys.isEmpty()) { + const auto metadataKeyFromJson = metadataObj[metadataKeyJsonKey].toString().toLocal8Bit(); + if (!metadataKeyFromJson.isEmpty()) { + const auto decryptedMetadataKeyBase64 = decryptData(metadataKeyFromJson); + if (!decryptedMetadataKeyBase64.isEmpty()) { + _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); + } + } + + if (_metadataKey.isEmpty()) { + qCDebug(lcCse()) << "Migrating from v1.1 to v1.2"; + + if (metadataKeys.isEmpty()) { + qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; + return; + } + + const auto lastMetadataKey = metadataKeys.keys().last(); + const auto decryptedMetadataKeyBase64 = decryptData(metadataKeys.value(lastMetadataKey).toString().toLocal8Bit()); + if (!decryptedMetadataKeyBase64.isEmpty()) { + _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); + } + } + + if (_metadataKey.isEmpty()) { qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; return; } @@ -1538,65 +1565,39 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) _fileDrop = metaDataDoc.object().value("filedrop").toObject(); - QJsonDocument debugHelper; - debugHelper.setObject(metadataKeys); - qCDebug(lcCse) << "Keys: " << debugHelper.toJson(QJsonDocument::Compact); - // Iterate over the document to store the keys. I'm unsure that the keys are in order, // perhaps it's better to store a map instead of a vector, perhaps this just doesn't matter. - for(auto it = metadataKeys.constBegin(), end = metadataKeys.constEnd(); it != end; it++) { - QByteArray currB64Pass = it.value().toString().toLocal8Bit(); - /* - * We have to base64 decode the metadatakey here. This was a misunderstanding in the RFC - * Now we should be compatible with Android and IOS. Maybe we can fix it later. - */ - QByteArray b64DecryptedKey = decryptData(currB64Pass); - if (b64DecryptedKey.isEmpty()) { - qCDebug(lcCse()) << "Could not decrypt metadata for key" << it.key(); - continue; - } - - QByteArray decryptedKey = QByteArray::fromBase64(b64DecryptedKey); - _metadataKeys.insert(it.key().toInt(), decryptedKey); - } // Cool, We actually have the key, we can decrypt the rest of the metadata. qCDebug(lcCse) << "Sharing: " << sharing; if (sharing.size()) { - const auto metaDataKey = !_metadataKeys.isEmpty() ? _metadataKeys.last() : QByteArray{}; - if (metaDataKey.isEmpty()) { - qCDebug(lcCse) << "Failed to decrypt sharing! Empty metadata key!"; - } else { - auto sharingDecrypted = decryptJsonObject(sharing, metaDataKey); - qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted; + auto sharingDecrypted = decryptJsonObject(sharing, _metadataKey); + qCDebug(lcCse) << "Sharing Decrypted" << sharingDecrypted; - // Sharing is also a JSON object, so extract it and populate. - auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted); - auto sharingObj = sharingDoc.object(); - for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) { - _sharing.push_back({it.key(), it.value().toString()}); - } + // Sharing is also a JSON object, so extract it and populate. + auto sharingDoc = QJsonDocument::fromJson(sharingDecrypted); + auto sharingObj = sharingDoc.object(); + for (auto it = sharingObj.constBegin(), end = sharingObj.constEnd(); it != end; it++) { + _sharing.push_back({it.key(), it.value().toString()}); } } else { qCDebug(lcCse) << "Skipping sharing section since it is empty"; } - for (auto it = files.constBegin(), end = files.constEnd(); it != end; it++) { + for (auto it = files.constBegin(); it != files.constEnd(); ++it) { EncryptedFile file; file.encryptedFilename = it.key(); - auto fileObj = it.value().toObject(); - file.metadataKey = fileObj["metadataKey"].toInt(); + const auto fileObj = it.value().toObject(); file.authenticationTag = QByteArray::fromBase64(fileObj["authenticationTag"].toString().toLocal8Bit()); file.initializationVector = QByteArray::fromBase64(fileObj["initializationVector"].toString().toLocal8Bit()); - //Decrypt encrypted part - const auto key = _metadataKeys.value(file.metadataKey, {}); - auto encryptedFile = fileObj["encrypted"].toString().toLocal8Bit(); - auto decryptedFile = !key.isEmpty() ? decryptJsonObject(encryptedFile, key) : QByteArray{}; - auto decryptedFileDoc = QJsonDocument::fromJson(decryptedFile); + // Decrypt encrypted part + const auto encryptedFile = fileObj["encrypted"].toString().toLocal8Bit(); + const auto decryptedFile = decryptJsonObject(encryptedFile, _metadataKey); + const auto decryptedFileDoc = QJsonDocument::fromJson(decryptedFile); - auto decryptedFileObj = decryptedFileDoc.object(); + const auto decryptedFileObj = decryptedFileDoc.object(); if (decryptedFileObj["filename"].toString().isEmpty()) { qCDebug(lcCse) << "decrypted metadata" << decryptedFileDoc.toJson(QJsonDocument::Indented); @@ -1607,7 +1608,6 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) file.originalFilename = decryptedFileObj["filename"].toString(); file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); - file.fileVersion = decryptedFileObj["version"].toInt(); // In case we wrongly stored "inode/directory" we try to recover from it if (file.mimetype == QByteArrayLiteral("inode/directory")) { @@ -1616,6 +1616,10 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) _files.push_back(file); } + + // decryption finished, create new metadata key to be used for encryption + _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); + _isMetadataSetup = true; } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. @@ -1661,42 +1665,31 @@ QByteArray FolderMetadata::decryptJsonObject(const QByteArray& encryptedMetadata bool FolderMetadata::isMetadataSetup() const { - return !_metadataKeys.isEmpty(); + return _isMetadataSetup; } void FolderMetadata::setupEmptyMetadata() { qCDebug(lcCse) << "Settint up empty metadata"; - QByteArray newMetadataPass = EncryptionHelper::generateRandom(16); - _metadataKeys.insert(0, newMetadataPass); - + _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); QString publicKey = _account->e2e()->_publicKey.toPem().toBase64(); QString displayName = _account->displayName(); _sharing.append({displayName, publicKey}); + + _isMetadataSetup = true; } QByteArray FolderMetadata::encryptedMetadata() const { qCDebug(lcCse) << "Generating metadata"; - if (_metadataKeys.isEmpty()) { + if (_metadataKey.isEmpty()) { qCDebug(lcCse) << "Metadata generation failed! Empty metadata key!"; return {}; } - - QJsonObject metadataKeys; - for (auto it = _metadataKeys.constBegin(), end = _metadataKeys.constEnd(); it != end; it++) { - /* - * We have to already base64 encode the metadatakey here. This was a misunderstanding in the RFC - * Now we should be compatible with Android and IOS. Maybe we can fix it later. - */ - const QByteArray encryptedKey = encryptData(it.value().toBase64()); - metadataKeys.insert(QString::number(it.key()), QString(encryptedKey)); - } - - QJsonObject metadata = { - {"metadataKeys", metadataKeys}, - // {"sharing", sharingEncrypted}, - {"version", 1} + const auto version = _account->capabilities().clientSideEncryptionVersion(); + QJsonObject metadata{ + {"version", version}, + {metadataKeyJsonKey, QJsonValue::fromVariant(encryptData(_metadataKey.toBase64()))} }; QJsonObject files; @@ -1705,20 +1698,17 @@ QByteArray FolderMetadata::encryptedMetadata() const { encrypted.insert("key", QString(it->encryptionKey.toBase64())); encrypted.insert("filename", it->originalFilename); encrypted.insert("mimetype", QString(it->mimetype)); - encrypted.insert("version", it->fileVersion); QJsonDocument encryptedDoc; encryptedDoc.setObject(encrypted); - QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKeys.last()); + QString encryptedEncrypted = encryptJsonObject(encryptedDoc.toJson(QJsonDocument::Compact), _metadataKey); if (encryptedEncrypted.isEmpty()) { qCDebug(lcCse) << "Metadata generation failed!"; } - QJsonObject file; file.insert("encrypted", encryptedEncrypted); file.insert("initializationVector", QString(it->initializationVector.toBase64())); file.insert("authenticationTag", QString(it->authenticationTag.toBase64())); - file.insert("metadataKey", _metadataKeys.lastKey()); files.insert(it->encryptedFilename, file); } @@ -1741,7 +1731,6 @@ void FolderMetadata::addEncryptedFile(const EncryptedFile &f) { break; } } - _files.append(f); } @@ -1785,14 +1774,12 @@ bool FolderMetadata::moveFromFileDropToFiles() EncryptedFile file; file.encryptedFilename = it.key(); - file.metadataKey = fileObject["metadataKey"].toInt(); file.authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit()); file.initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit()); file.originalFilename = decryptedFileObject["filename"].toString(); file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit()); file.mimetype = decryptedFileObject["mimetype"].toString().toLocal8Bit(); - file.fileVersion = decryptedFileObject["version"].toInt(); // In case we wrongly stored "inode/directory" we try to recover from it if (file.mimetype == QByteArrayLiteral("inode/directory")) { diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 4cbe776b1e..a9a37e0cd8 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -182,8 +182,6 @@ struct EncryptedFile { QByteArray authenticationTag; QString encryptedFilename; QString originalFilename; - int fileVersion = 0; - int metadataKey = 0; }; class OWNCLOUDSYNC_EXPORT FolderMetadata { @@ -215,11 +213,13 @@ private: [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; + QByteArray _metadataKey; + QVector _files; - QMap _metadataKeys; AccountPtr _account; QVector> _sharing; QJsonObject _fileDrop; + bool _isMetadataSetup = false; }; } // namespace OCC diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 24f11ed7c4..f027c1c8ae 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -154,8 +154,6 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo if (!found) { encryptedFile.encryptionKey = EncryptionHelper::generateRandom(16); encryptedFile.encryptedFilename = EncryptionHelper::generateRandomFilename(); - encryptedFile.fileVersion = 1; - encryptedFile.metadataKey = 1; encryptedFile.originalFilename = fileName; QMimeDatabase mdb; From 1b14c127a4c969e86e1ffb8520d9772036651223 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 00:45:28 +0200 Subject: [PATCH 03/16] check checksum when getting e2ee metadata Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 45 ++++++++++++++++++++-------- src/libsync/clientsideencryption.h | 4 +++ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index ef6d0982c1..38c819076d 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include @@ -1555,10 +1556,10 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } } - if (_metadataKey.isEmpty()) { - qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; - return; - } + const auto sharing = metadataObj["sharing"].toString().toLocal8Bit(); + const auto files = metaDataDoc.object()["files"].toObject(); + const auto metadataKey = metaDataDoc.object()["metadata"].toObject()["metadataKey"].toString().toUtf8(); + const auto metadataKeyChecksum = metaDataDoc.object()["metadata"].toObject()["checksum"].toString().toUtf8(); QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit(); QJsonObject files = metaDataDoc.object()["files"].toObject(); @@ -1609,17 +1610,15 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); - // In case we wrongly stored "inode/directory" we try to recover from it - if (file.mimetype == QByteArrayLiteral("inode/directory")) { - file.mimetype = QByteArrayLiteral("httpd/unix-directory"); + if (!checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { + _metadataKey.clear(); + _files.clear(); + return; } - _files.push_back(file); - } - - // decryption finished, create new metadata key to be used for encryption - _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); - _isMetadataSetup = true; + // decryption finished, create new metadata key to be used for encryption + _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); + _isMetadataSetup = true; } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. @@ -1663,6 +1662,26 @@ QByteArray FolderMetadata::decryptJsonObject(const QByteArray& encryptedMetadata return EncryptionHelper::decryptStringSymmetric(pass, encryptedMetadata); } +bool FolderMetadata::checkMetadataKeyChecksum(const QByteArray &metadataKey, + const QByteArray &metadataKeyChecksum) const +{ + const auto referenceMetadataKeyValue = computeMetadataKeyChecksum(metadataKey); + return referenceMetadataKeyValue == metadataKeyChecksum; +} + +QByteArray FolderMetadata::computeMetadataKeyChecksum(const QByteArray &metadataKey) const +{ + auto checksumData = _account->e2e()->_mnemonic.remove(' '); + for (const auto &singleFile : _files) { + checksumData += singleFile.encryptedFilename; + } + checksumData += metadataKey; + + auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; + hashAlgorithm.addData(checksumData.toUtf8()); + return hashAlgorithm.result().toHex(); +} + bool FolderMetadata::isMetadataSetup() const { return _isMetadataSetup; diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index a9a37e0cd8..a180ef78f2 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -213,6 +213,10 @@ private: [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; + [[nodiscard]] bool checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const; + + [[nodiscard]] QByteArray computeMetadataKeyChecksum(const QByteArray &metadataKey) const; + QByteArray _metadataKey; QVector _files; From d1c18ecf2a254cba58c7549cf1eb42de79bf069b Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 10:57:55 +0200 Subject: [PATCH 04/16] when uploading new e2e metadata, adds a checksum Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 38c819076d..02128a2c08 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1708,7 +1708,8 @@ QByteArray FolderMetadata::encryptedMetadata() const { const auto version = _account->capabilities().clientSideEncryptionVersion(); QJsonObject metadata{ {"version", version}, - {metadataKeyJsonKey, QJsonValue::fromVariant(encryptData(_metadataKey.toBase64()))} + {metadataKeyJsonKey, QJsonValue::fromVariant(encryptData(_metadataKey.toBase64()))}, + {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(_metadataKey.toBase64()))}, }; QJsonObject files; From f181b91875539a3d82900a4d4b817e2de470f84c Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 11:59:24 +0200 Subject: [PATCH 05/16] when migrating older metadata, do not check missing checksum Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 32 +++++++++++++++++++--------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 02128a2c08..fce72815c3 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1541,8 +1541,10 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } } + auto migratedMetadata = false; if (_metadataKey.isEmpty()) { qCDebug(lcCse()) << "Migrating from v1.1 to v1.2"; + migratedMetadata = true; if (metadataKeys.isEmpty()) { qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; @@ -1556,14 +1558,16 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } } + if (_metadataKey.isEmpty()) { + qCDebug(lcCse()) << "Could not setup existing metadata with missing metadataKeys!"; + return; + } + const auto sharing = metadataObj["sharing"].toString().toLocal8Bit(); const auto files = metaDataDoc.object()["files"].toObject(); const auto metadataKey = metaDataDoc.object()["metadata"].toObject()["metadataKey"].toString().toUtf8(); const auto metadataKeyChecksum = metaDataDoc.object()["metadata"].toObject()["checksum"].toString().toUtf8(); - QByteArray sharing = metadataObj["sharing"].toString().toLocal8Bit(); - QJsonObject files = metaDataDoc.object()["files"].toObject(); - _fileDrop = metaDataDoc.object().value("filedrop").toObject(); // Iterate over the document to store the keys. I'm unsure that the keys are in order, @@ -1610,15 +1614,23 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) file.encryptionKey = QByteArray::fromBase64(decryptedFileObj["key"].toString().toLocal8Bit()); file.mimetype = decryptedFileObj["mimetype"].toString().toLocal8Bit(); - if (!checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { - _metadataKey.clear(); - _files.clear(); - return; + // In case we wrongly stored "inode/directory" we try to recover from it + if (file.mimetype == QByteArrayLiteral("inode/directory")) { + file.mimetype = QByteArrayLiteral("httpd/unix-directory"); } - // decryption finished, create new metadata key to be used for encryption - _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); - _isMetadataSetup = true; + _files.push_back(file); + } + + if (!migratedMetadata && !checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { + _metadataKey.clear(); + _files.clear(); + return; + } + + // decryption finished, create new metadata key to be used for encryption + _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); + _isMetadataSetup = true; } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. From ee3c18f9f2fbd6954b5bb664eb10c65d64247c23 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 16:52:35 +0200 Subject: [PATCH 06/16] put sane order over #include Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 33 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index fce72815c3..48d031da73 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1,24 +1,16 @@ -#include -#include -#include -#include -#include -#include - - #include "clientsideencryption.h" + #include "account.h" #include "capabilities.h" #include "networkjobs.h" #include "clientsideencryptionjobs.h" #include "theme.h" #include "creds/abstractcredentials.h" +#include "common/utility.h" +#include "common/constants.h" +#include "wordlist.h" -#include -#include -#include - -#include +#include #include #include @@ -35,11 +27,18 @@ #include #include -#include -#include -#include +#include +#include +#include -#include "wordlist.h" +#include +#include +#include +#include +#include +#include + +#include QDebug operator<<(QDebug out, const std::string& str) { From 1e018d1e562300d5a10384684bf463fafa1128c6 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Tue, 28 Mar 2023 16:53:02 +0200 Subject: [PATCH 07/16] display some logs when checksum verification is failed Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 48d031da73..30dafdded8 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1622,6 +1622,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } if (!migratedMetadata && !checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { + qCInfo(lcCseMetadata) << "checksum comparison failed" << "server value" << metadataKeyChecksum << "client value" << computeMetadataKeyChecksum(metadataKey); _metadataKey.clear(); _files.clear(); return; From 7c655cf679bc9c9e335d232d7f4b3e3a9739c651 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 09:22:22 +0200 Subject: [PATCH 08/16] use a getter to query encryption status Signed-off-by: Matthieu Gallien --- src/common/syncjournaldb.cpp | 4 ++-- src/common/syncjournalfilerecord.h | 1 + src/gui/accountsettings.cpp | 2 +- src/gui/filedetails/sharemodel.cpp | 4 ++-- src/gui/folder.cpp | 4 ++-- src/gui/folderstatusmodel.cpp | 12 ++++++------ src/gui/folderstatusmodel.h | 2 ++ src/gui/socketapi/socketapi.cpp | 6 +++--- src/gui/tray/activitylistmodel.cpp | 2 +- src/libsync/discovery.cpp | 14 +++++++------- src/libsync/discoveryphase.cpp | 6 +++--- src/libsync/discoveryphase.h | 6 +++++- src/libsync/owncloudpropagator.cpp | 6 +++--- src/libsync/owncloudpropagator.h | 2 +- src/libsync/propagatedownload.cpp | 10 +++++----- src/libsync/propagatedownload.h | 1 + src/libsync/propagateremotedelete.cpp | 2 +- .../propagateremotedeleteencryptedrootfolder.cpp | 2 +- src/libsync/propagateremotemkdir.cpp | 2 +- src/libsync/propagateupload.cpp | 2 +- src/libsync/syncfileitem.cpp | 4 ++-- src/libsync/syncfileitem.h | 2 ++ test/testfolderman.cpp | 2 +- 23 files changed, 54 insertions(+), 44 deletions(-) diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index fc3c4149fd..2a91a8f79b 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -910,7 +910,7 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & << "modtime:" << record._modtime << "type:" << record._type << "etag:" << record._etag << "fileId:" << record._fileId << "remotePerm:" << record._remotePerm.toString() << "fileSize:" << record._fileSize << "checksum:" << record._checksumHeader - << "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record._isE2eEncrypted + << "e2eMangledName:" << record.e2eMangledName() << "isE2eEncrypted:" << record.isE2eEncrypted() << "lock:" << (record._lockstate._locked ? "true" : "false") << "lock owner type:" << record._lockstate._lockOwnerType << "lock owner:" << record._lockstate._lockOwnerDisplayName @@ -968,7 +968,7 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(15, checksum); query->bindValue(16, contentChecksumTypeId); query->bindValue(17, record._e2eMangledName); - query->bindValue(18, record._isE2eEncrypted); + query->bindValue(18, record.isE2eEncrypted()); query->bindValue(19, record._lockstate._locked ? 1 : 0); query->bindValue(20, record._lockstate._lockOwnerType); query->bindValue(21, record._lockstate._lockOwnerDisplayName); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index a846aeeb17..f32e4c588b 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -67,6 +67,7 @@ public: [[nodiscard]] bool isVirtualFile() const { return _type == ItemTypeVirtualFile || _type == ItemTypeVirtualFileDownload; } [[nodiscard]] QString path() const { return QString::fromUtf8(_path); } [[nodiscard]] QString e2eMangledName() const { return QString::fromUtf8(_e2eMangledName); } + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; } QByteArray _path; quint64 _inode = 0; diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 61b6fcb46a..0212a28125 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -520,7 +520,7 @@ void AccountSettings::slotSubfolderContextMenuRequested(const QModelIndex& index if (acc->capabilities().clientSideEncryptionAvailable()) { // Verify if the folder is empty before attempting to encrypt. - const auto isEncrypted = info->_isEncrypted; + const auto isEncrypted = info->isEncrypted(); const auto isParentEncrypted = _model->isAnyAncestorEncrypted(index); if (!isEncrypted && !isParentEncrypted) { diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index f210e4550e..cee89c6d1b 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -250,9 +250,9 @@ void ShareModel::updateData() _numericFileId = fileRecord.numericFileId(); - _isEncryptedItem = fileRecord._isE2eEncrypted; + _isEncryptedItem = fileRecord.isE2eEncrypted(); _isSecureFileDropSupportedFolder = - fileRecord._isE2eEncrypted && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported(); + fileRecord.isE2eEncrypted() && fileRecord.e2eMangledName().isEmpty() && _accountState->account()->secureFileDropSupported(); // Will get added when shares are fetched if no link shares are fetched _placeholderLinkShare.reset(new Share(_accountState->account(), diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index 38db18d554..eb7d6c0104 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -1347,7 +1347,7 @@ void Folder::removeLocalE2eFiles() QStringList e2eFoldersToBlacklist; const auto couldGetFiles = _journal.getFilesBelowPath("", [this, &e2eFoldersToBlacklist, &folderRootDir](const SyncJournalFileRecord &rec) { // We only want to add the root-most encrypted folder to the blacklist - if (rec.isValid() && rec._isE2eEncrypted && rec.isDirectory()) { + if (rec.isValid() && rec.isE2eEncrypted() && rec.isDirectory()) { QDir pathDir(_canonicalLocalPath + rec.path()); bool parentPathEncrypted = false; @@ -1359,7 +1359,7 @@ void Folder::removeLocalE2eFiles() qCWarning(lcFolder) << "Failed to get file record for" << currentCanonicalPath; } - if (dirRec._isE2eEncrypted) { + if (dirRec.isE2eEncrypted()) { parentPathEncrypted = true; break; } diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index 2855ef51d9..8a92b291ab 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -162,7 +162,7 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const case Qt::DisplayRole: { //: Example text: "File.txt (23KB)" const auto &xParent = static_cast(index.internalPointer()); - const auto suffix = (subfolderInfo._isNonDecryptable && subfolderInfo._checked && (!xParent || !xParent->_isEncrypted)) + const auto suffix = (subfolderInfo._isNonDecryptable && subfolderInfo._checked && (!xParent || !xParent->isEncrypted())) ? QStringLiteral(" - ") + tr("Could not decrypt!") : QString{}; return subfolderInfo._size < 0 ? QString(subfolderInfo._name + suffix) : QString(tr("%1 (%2)").arg(subfolderInfo._name, Utility::octetsToString(subfolderInfo._size)) + suffix); @@ -179,7 +179,7 @@ QVariant FolderStatusModel::data(const QModelIndex &index, int role) const if (subfolderInfo._isNonDecryptable && subfolderInfo._checked) { return QIcon(QLatin1String(":/client/theme/lock-broken.svg")); } - if (subfolderInfo._isEncrypted) { + if (subfolderInfo.isEncrypted()) { return QIcon(QLatin1String(":/client/theme/lock-https.svg")); } else if (subfolderInfo._size > 0 && isAnyAncestorEncrypted(index)) { return QIcon(QLatin1String(":/client/theme/lock-broken.svg")); @@ -445,7 +445,7 @@ bool FolderStatusModel::isAnyAncestorEncrypted(const QModelIndex &index) const auto parentIndex = parent(index); while (parentIndex.isValid()) { const auto info = infoForIndex(parentIndex); - if (info->_isEncrypted) { + if (info->isEncrypted()) { return true; } parentIndex = parent(parentIndex); @@ -607,7 +607,7 @@ void FolderStatusModel::fetchMore(const QModelIndex &parent) QString path = info->_folder->remotePathTrailingSlash(); // info->_path always contains non-mangled name, so we need to use mangled when requesting nested folders for encrypted subfolders as required by LsColJob - const QString infoPath = (info->_isEncrypted && !info->_e2eMangledName.isEmpty()) ? info->_e2eMangledName : info->_path; + const QString infoPath = (info->isEncrypted() && !info->_e2eMangledName.isEmpty()) ? info->_e2eMangledName : info->_path; if (infoPath != QLatin1String("/")) { path += infoPath; @@ -752,7 +752,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) newInfo._isEncrypted = encryptionMap.value(removeTrailingSlash(path)).toString() == QStringLiteral("1"); newInfo._path = relativePath; - newInfo._isNonDecryptable = newInfo._isEncrypted + newInfo._isNonDecryptable = newInfo.isEncrypted() && _accountState->account()->e2e() && !_accountState->account()->e2e()->_publicKey.isNull() && _accountState->account()->e2e()->_privateKey.isNull(); @@ -762,7 +762,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) } if (rec.isValid()) { newInfo._name = removeTrailingSlash(rec._path).split('/').last(); - if (rec._isE2eEncrypted && !rec._e2eMangledName.isEmpty()) { + if (rec.isE2eEncrypted() && !rec._e2eMangledName.isEmpty()) { // we must use local path for Settings Dialog's filesystem tree, otherwise open and create new folder actions won't work // hence, we are storing _e2eMangledName separately so it can be use later for LsColJob newInfo._e2eMangledName = relativePath; diff --git a/src/gui/folderstatusmodel.h b/src/gui/folderstatusmodel.h index e2397295c7..96b7467d14 100644 --- a/src/gui/folderstatusmodel.h +++ b/src/gui/folderstatusmodel.h @@ -85,6 +85,8 @@ public: // Whether this has a FetchLabel subrow [[nodiscard]] bool hasLabel() const; + [[nodiscard]] bool isEncrypted() const { return _isEncrypted; } + // Reset all subfolders and fetch status void resetSubs(FolderStatusModel *model, QModelIndex index); diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index 4e0ef59ae1..7b8772a7e4 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -1220,7 +1220,7 @@ void SocketApi::sendEncryptFolderCommandMenuEntries(const QFileInfo &fileInfo, bool anyAncestorEncrypted = false; auto ancestor = fileData.parentFolder(); while (ancestor.journalRecord().isValid()) { - if (ancestor.journalRecord()._isE2eEncrypted) { + if (ancestor.journalRecord().isE2eEncrypted()) { anyAncestorEncrypted = true; break; } @@ -1352,8 +1352,8 @@ void SocketApi::command_GET_MENU_ITEMS(const QString &argument, OCC::SocketListe FileData fileData = FileData::get(argument); const auto record = fileData.journalRecord(); const bool isOnTheServer = record.isValid(); - const auto isE2eEncryptedPath = fileData.journalRecord()._isE2eEncrypted || !fileData.journalRecord()._e2eMangledName.isEmpty(); - const auto isE2eEncryptedRootFolder = fileData.journalRecord()._isE2eEncrypted && fileData.journalRecord()._e2eMangledName.isEmpty(); + const auto isE2eEncryptedPath = fileData.journalRecord().isE2eEncrypted() || !fileData.journalRecord()._e2eMangledName.isEmpty(); + const auto isE2eEncryptedRootFolder = fileData.journalRecord().isE2eEncrypted() && fileData.journalRecord()._e2eMangledName.isEmpty(); auto flagString = isOnTheServer && !isE2eEncryptedPath ? QLatin1String("::") : QLatin1String(":d:"); const QFileInfo fileInfo(fileData.localPath); diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 6b6b4569cc..b820ed941c 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -159,7 +159,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const if (!folder->journalDb()->getFileRecord(fileName.mid(1), &rec)) { qCWarning(lcActivity) << "could not get file from local DB" << fileName.mid(1); } - if (rec.isValid() && (rec._isE2eEncrypted || !rec._e2eMangledName.isEmpty())) { + if (rec.isValid() && (rec.isE2eEncrypted() || !rec._e2eMangledName.isEmpty())) { return QString(); } } diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index e2c8a91d52..c9aa2d02a1 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -218,7 +218,7 @@ void ProcessDirectoryJob::process() if (handleExcluded(path._target, e, isHidden)) continue; - const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted && + const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() && _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->_publicKey.isNull() && _discoveryData->_account->e2e()->_privateKey.isNull(); if (isEncryptedFolderButE2eIsNotSetup) { @@ -431,7 +431,7 @@ void ProcessDirectoryJob::processFile(PathTuple path, << " | fileid: " << dbEntry._fileId << "//" << serverEntry.fileId << " | inode: " << dbEntry._inode << "/" << localEntry.inode << "/" << " | type: " << dbEntry._type << "/" << localEntry.type << "/" << (serverEntry.isDirectory ? ItemTypeDirectory : ItemTypeFile) - << " | e2ee: " << dbEntry._isE2eEncrypted << "/" << serverEntry.isE2eEncrypted + << " | e2ee: " << dbEntry.isE2eEncrypted() << "/" << serverEntry.isE2eEncrypted() << " | e2eeMangledName: " << dbEntry.e2eMangledName() << "/" << serverEntry.e2eMangledName << " | file lock: " << localFileIsLocked << "//" << serverFileIsLocked; @@ -532,7 +532,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_etag = serverEntry.etag; item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadCookies = serverEntry.directDownloadCookies; - item->_isEncrypted = serverEntry.isE2eEncrypted; + item->_isEncrypted = serverEntry.isE2eEncrypted(); item->_encryptedFileName = [=] { if (serverEntry.e2eMangledName.isEmpty()) { return QString(); @@ -656,7 +656,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( // or, maybe, add a flag to the database - vfsE2eeSizeCorrected? if it is not set - subtract it from the placeholder's size and re-create/update a placeholder? const QueryMode serverQueryMode = [this, &dbEntry, &serverEntry]() { const bool isVfsModeOn = _discoveryData && _discoveryData->_syncOptions._vfs && _discoveryData->_syncOptions._vfs->mode() != Vfs::Off; - if (isVfsModeOn && dbEntry.isDirectory() && dbEntry._isE2eEncrypted) { + if (isVfsModeOn && dbEntry.isDirectory() && dbEntry.isE2eEncrypted()) { qint64 localFolderSize = 0; const auto listFilesCallback = [&localFolderSize](const OCC::SyncJournalFileRecord &record) { if (record.isFile()) { @@ -1240,7 +1240,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( return false; } - if (base._isE2eEncrypted || isInsideEncryptedTree()) { + if (base.isE2eEncrypted() || isInsideEncryptedTree()) { return false; } @@ -1289,7 +1289,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( // If it's not a move it's just a local-NEW if (!moveCheck()) { - if (base._isE2eEncrypted) { + if (base.isE2eEncrypted()) { // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information item->_isEncrypted = true; @@ -1551,7 +1551,7 @@ void ProcessDirectoryJob::processFileFinalize( if (recurse) { auto job = new ProcessDirectoryJob(path, item, recurseQueryLocal, recurseQueryServer, _lastSyncTimestamp, this); - job->setInsideEncryptedTree(isInsideEncryptedTree() || item->_isEncrypted); + job->setInsideEncryptedTree(isInsideEncryptedTree() || item->isEncrypted()); if (removed) { job->setParent(_discoveryData); _discoveryData->enqueueDirectoryToDelete(path._original, job); diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index e2ec41d855..dc327b2374 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -458,7 +458,7 @@ static void propertyMapToRemoteInfo(const QMap &map, RemoteInf result.sharedByMe = true; } } else if (property == "is-encrypted" && value == QStringLiteral("1")) { - result.isE2eEncrypted = true; + result._isE2eEncrypted = true; } else if (property == "lock") { result.locked = (value == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem); } @@ -576,7 +576,7 @@ void DiscoverySingleDirectoryJob::lsJobFinishedWithoutErrorSlot() emit finished(HttpError{ 0, _error }); deleteLater(); return; - } else if (_isE2eEncrypted) { + } else if (isE2eEncrypted()) { emit etag(_firstEtag, QDateTime::fromString(QString::fromUtf8(_lsColJob->responseTimestamp()), Qt::RFC2822Date)); fetchE2eMetadata(); return; @@ -640,7 +640,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in auto result = info; const auto encryptedFileInfo = findEncryptedFile(result.name); if (encryptedFileInfo) { - result.isE2eEncrypted = true; + result._isE2eEncrypted = true; result.e2eMangledName = _subPath.mid(1) + QLatin1Char('/') + result.name; result.name = encryptedFileInfo->originalFilename; } diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 61663925df..5f5a4223e4 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -66,12 +66,13 @@ struct RemoteInfo int64_t size = 0; int64_t sizeOfFolder = 0; bool isDirectory = false; - bool isE2eEncrypted = false; + bool _isE2eEncrypted = false; bool isFileDropDetected = false; QString e2eMangledName; bool sharedByMe = false; [[nodiscard]] bool isValid() const { return !name.isNull(); } + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; } QString directDownloadUrl; QString directDownloadCookies; @@ -160,6 +161,9 @@ private slots: void metadataError(const QByteArray& fileId, int httpReturnCode); private: + + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; } + QVector _results; QString _subPath; QByteArray _firstEtag; diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index f73c82cccb..51510ad888 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -326,7 +326,7 @@ bool PropagateItemJob::hasEncryptedAncestor() const qCWarning(lcPropagator) << "could not get file from local DB" << pathCompontentsJointed; } - if (rec.isValid() && rec._isE2eEncrypted) { + if (rec.isValid() && rec.isE2eEncrypted()) { return true; } pathComponents.removeLast(); @@ -1021,14 +1021,14 @@ bool OwncloudPropagator::isDelayedUploadItem(const SyncFileItemPtr &item) const if (!accountPtr->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || - !parentRec._isE2eEncrypted) { + !parentRec.isE2eEncrypted()) { return false; } return true; }; - return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->_isEncrypted && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file) && !checkFileShouldBeEncrypted(item); + return account()->capabilities().bulkUpload() && !_scheduleDelayedTasks && !item->isEncrypted() && _syncOptions._minChunkSize > item->_size && !isInBulkUploadBlackList(item->_file) && !checkFileShouldBeEncrypted(item); } void OwncloudPropagator::setScheduleDelayedTasks(bool active) diff --git a/src/libsync/owncloudpropagator.h b/src/libsync/owncloudpropagator.h index 2bdc09b637..dd0525d225 100644 --- a/src/libsync/owncloudpropagator.h +++ b/src/libsync/owncloudpropagator.h @@ -199,7 +199,7 @@ public: // TODO: In fact, we must make sure Lock/Unlock are not colliding and always wait for each other to complete. So, we could refactor this "_parallelism" later // so every "PropagateItemJob" that will potentially execute Lock job on E2EE folder will get executed sequentially. // As an alternative, we could optimize Lock/Unlock calls, so we do a batch-write on one folder and only lock and unlock a folder once per batch. - _parallelism = (_item->_isEncrypted || hasEncryptedAncestor()) ? WaitForFinished : FullParallelism; + _parallelism = (_item->isEncrypted() || hasEncryptedAncestor()) ? WaitForFinished : FullParallelism; } ~PropagateItemJob() override; diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index ead85459f9..970c8e246d 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -468,7 +468,7 @@ void PropagateDownloadFile::start() const auto account = propagator()->account(); if (!account->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || - !parentRec._isE2eEncrypted) { + !parentRec.isE2eEncrypted()) { startAfterIsEncryptedIsChecked(); } else { _downloadEncryptedHelper = new PropagateDownloadEncrypted(propagator(), parentPath, _item, this); @@ -718,7 +718,7 @@ void PropagateDownloadFile::startDownload() if (_item->_directDownloadUrl.isEmpty()) { // Normal job, download from oC instance _job = new GETFileJob(propagator()->account(), - propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file), + propagator()->fullRemotePath(isEncrypted() ? _item->_encryptedFileName : _item->_file), &_tmpFile, headers, expectedEtagForResume, _resumeStart, this); } else { // We were provided a direct URL, use that one @@ -940,7 +940,7 @@ void PropagateDownloadFile::slotChecksumFail(const QString &errMsg, { if (reason == ValidateChecksumHeader::FailureReason::ChecksumMismatch && propagator()->account()->isChecksumRecalculateRequestSupported()) { const QByteArray calculatedChecksumHeader(calculatedChecksumType + ':' + calculatedChecksum); - const QString fullRemotePathForFile(propagator()->fullRemotePath(_isEncrypted ? _item->_encryptedFileName : _item->_file)); + const QString fullRemotePathForFile(propagator()->fullRemotePath(isEncrypted() ? _item->_encryptedFileName : _item->_file)); auto *job = new SimpleFileJob(propagator()->account(), fullRemotePathForFile); QObject::connect(job, &SimpleFileJob::finishedSignal, this, [this, calculatedChecksumHeader, errMsg](const QNetworkReply *reply) { processChecksumRecalculate(reply, calculatedChecksumHeader, errMsg); @@ -1137,7 +1137,7 @@ void PropagateDownloadFile::localFileContentChecksumComputed(const QByteArray &c void PropagateDownloadFile::finalizeDownload() { - if (_isEncrypted) { + if (isEncrypted()) { if (_downloadEncryptedHelper->decryptFile(_tmpFile)) { downloadFinished(); } else { @@ -1329,7 +1329,7 @@ void PropagateDownloadFile::updateMetadata(bool isConflict) return; } - if (_isEncrypted) { + if (isEncrypted()) { propagator()->_journal->setDownloadInfo(_item->_file, SyncJournalDb::DownloadInfo()); } else { propagator()->_journal->setDownloadInfo(_item->_encryptedFileName, SyncJournalDb::DownloadInfo()); diff --git a/src/libsync/propagatedownload.h b/src/libsync/propagatedownload.h index e6a7b133bf..2a5fea962c 100644 --- a/src/libsync/propagatedownload.h +++ b/src/libsync/propagatedownload.h @@ -245,6 +245,7 @@ private slots: private: void startAfterIsEncryptedIsChecked(); void deleteExistingFolder(); + [[nodiscard]] bool isEncrypted() const { return _isEncrypted; } qint64 _resumeStart = 0; qint64 _downloadProgress = 0; diff --git a/src/libsync/propagateremotedelete.cpp b/src/libsync/propagateremotedelete.cpp index ddf3f11f79..84aab6fe40 100644 --- a/src/libsync/propagateremotedelete.cpp +++ b/src/libsync/propagateremotedelete.cpp @@ -33,7 +33,7 @@ void PropagateRemoteDelete::start() if (propagator()->_abortRequested) return; - if (!_item->_encryptedFileName.isEmpty() || _item->_isEncrypted) { + if (!_item->_encryptedFileName.isEmpty() || _item->isEncrypted()) { if (!_item->_encryptedFileName.isEmpty()) { _deleteEncryptedHelper = new PropagateRemoteDeleteEncrypted(propagator(), _item, this); } else { diff --git a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp index e8d86e3081..c9f82ee8e4 100644 --- a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp +++ b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp @@ -49,7 +49,7 @@ PropagateRemoteDeleteEncryptedRootFolder::PropagateRemoteDeleteEncryptedRootFold void PropagateRemoteDeleteEncryptedRootFolder::start() { - Q_ASSERT(_item->_isEncrypted); + Q_ASSERT(_item->isEncrypted()); const bool listFilesResult = _propagator->_journal->listFilesInPath(_item->_file.toUtf8(), [this](const OCC::SyncJournalFileRecord &record) { _nestedItems[record._e2eMangledName] = record; diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp index 88401dbd5f..b4dc65b971 100644 --- a/src/libsync/propagateremotemkdir.cpp +++ b/src/libsync/propagateremotemkdir.cpp @@ -146,7 +146,7 @@ void PropagateRemoteMkdir::finalizeMkColJob(QNetworkReply::NetworkError err, con _item->_isShared = _item->_remotePerm.hasPermission(RemotePermissions::IsShared) || _item->_sharedByMe; _item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); - if (!_uploadEncryptedHelper && !_item->_isEncrypted) { + if (!_uploadEncryptedHelper && !_item->isEncrypted()) { success(); } else { // We still need to mark that folder encrypted in case we were uploading it as encrypted one diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index b834c024b0..6f70ce5db6 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -219,7 +219,7 @@ void PropagateUploadFileCommon::start() if (!account->capabilities().clientSideEncryptionAvailable() || !parentRec.isValid() || - !parentRec._isE2eEncrypted) { + !parentRec.isE2eEncrypted()) { setupUnencryptedFile(); return; } diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 1727a89389..135f3c6371 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -49,7 +49,7 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._serverHasIgnoredFiles = _serverHasIgnoredFiles; rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); - rec._isE2eEncrypted = _isEncrypted; + rec._isE2eEncrypted = isEncrypted(); rec._lockstate._locked = _locked == LockStatus::LockedItem; rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName; rec._lockstate._lockOwnerId = _lockOwnerId; @@ -86,7 +86,7 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_serverHasIgnoredFiles = rec._serverHasIgnoredFiles; item->_checksumHeader = rec._checksumHeader; item->_encryptedFileName = rec.e2eMangledName(); - item->_isEncrypted = rec._isE2eEncrypted; + item->_isEncrypted = rec.isE2eEncrypted(); item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem; item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName; item->_lockOwnerId = rec._lockstate._lockOwnerId; diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 3890e339f1..4715901ab3 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -228,6 +228,8 @@ public: && !(_instruction == CSYNC_INSTRUCTION_CONFLICT && _status == SyncFileItem::Success); } + [[nodiscard]] bool isEncrypted() const { return _isEncrypted; } + // Variables useful for everybody /** The syncfolder-relative filesystem path that the operation is about diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index e81f2bfb65..2cd2afd49b 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -130,7 +130,7 @@ private slots: SyncJournalFileRecord updatedRec; QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &updatedRec)); - QVERIFY(updatedRec._isE2eEncrypted); + QVERIFY(updatedRec.isE2eEncrypted()); QVERIFY(updatedRec.isDirectory()); FolderMan::instance()->removeE2eFiles(account); From 8ec5518d8d682959bd66670bbbab7a6ae9d3baa0 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 11:26:20 +0200 Subject: [PATCH 09/16] checksum has to be computer from the encrypted metadataKey Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 30dafdded8..59e5134029 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1718,10 +1718,11 @@ QByteArray FolderMetadata::encryptedMetadata() const { return {}; } const auto version = _account->capabilities().clientSideEncryptionVersion(); + const auto encryptedMetadataKey = encryptData(_metadataKey.toBase64()); QJsonObject metadata{ {"version", version}, - {metadataKeyJsonKey, QJsonValue::fromVariant(encryptData(_metadataKey.toBase64()))}, - {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(_metadataKey.toBase64()))}, + {metadataKeyJsonKey, QJsonValue::fromVariant(encryptedMetadataKey)}, + {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(encryptedMetadataKey))}, }; QJsonObject files; From 8b10b3a9265b5146ce885549227d3245617c0a06 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 11:27:32 +0200 Subject: [PATCH 10/16] optimize the computation of checksum for metadata Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 59e5134029..715c00897d 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1683,14 +1683,14 @@ bool FolderMetadata::checkMetadataKeyChecksum(const QByteArray &metadataKey, QByteArray FolderMetadata::computeMetadataKeyChecksum(const QByteArray &metadataKey) const { - auto checksumData = _account->e2e()->_mnemonic.remove(' '); - for (const auto &singleFile : _files) { - checksumData += singleFile.encryptedFilename; - } - checksumData += metadataKey; - auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; - hashAlgorithm.addData(checksumData.toUtf8()); + + hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8()); + for (const auto &singleFile : _files) { + hashAlgorithm.addData(singleFile.encryptedFilename.toUtf8()); + } + hashAlgorithm.addData(metadataKey); + return hashAlgorithm.result().toHex(); } From 6fb16ce5f422060a891e5fb7e9bcc8b5143d76e2 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 11:34:46 +0200 Subject: [PATCH 11/16] when uplaoding e2ee metadata add an empty filedrop entry Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 715c00897d..04bd0f0b39 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1748,7 +1748,8 @@ QByteArray FolderMetadata::encryptedMetadata() const { QJsonObject metaObject = { {"metadata", metadata}, - {"files", files} + {"files", files}, + {"filedrop", QJsonObject{}}, }; QJsonDocument internalMetadata; From b121a127c177c58cef7974ec98042e16168e9280 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 11:35:17 +0200 Subject: [PATCH 12/16] improve logging when receiving metadata for encrypted folder Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryptionjobs.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index 6d4c56c09f..ba76d4576e 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -55,7 +55,9 @@ bool GetMetadataApiJob::finished() return true; } QJsonParseError error{}; - auto json = QJsonDocument::fromJson(reply()->readAll(), &error); + const auto replyData = reply()->readAll(); + auto json = QJsonDocument::fromJson(replyData, &error); + qCInfo(lcCseJob) << "metadata received for file id" << _fileId << json.toJson(QJsonDocument::Compact); emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); return true; } From 8659df22664eeedf33986fe91246d44fe2b3d8f8 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Wed, 29 Mar 2023 12:42:11 +0200 Subject: [PATCH 13/16] prevent downgrading e2ee metadata format after initial migration Signed-off-by: Matthieu Gallien --- src/common/syncjournaldb.cpp | 10 +++++-- src/common/syncjournalfilerecord.h | 12 +++++++-- src/libsync/clientsideencryption.cpp | 27 +++++++++++++++++-- src/libsync/clientsideencryption.h | 17 +++++++++++- src/libsync/discovery.cpp | 5 ++-- src/libsync/discoveryphase.cpp | 14 ++++++++-- src/libsync/discoveryphase.h | 6 +++-- src/libsync/encryptfolderjob.cpp | 2 +- src/libsync/owncloudpropagator.cpp | 2 +- src/libsync/propagatedownloadencrypted.cpp | 4 ++- .../propagateremotedeleteencrypted.cpp | 4 ++- ...opagateremotedeleteencryptedrootfolder.cpp | 4 ++- src/libsync/propagateremotemkdir.cpp | 2 +- src/libsync/propagateuploadencrypted.cpp | 6 +++-- src/libsync/syncfileitem.cpp | 6 ++--- src/libsync/syncfileitem.h | 14 +++++++--- src/libsync/updatefiledropmetadata.cpp | 6 +++-- test/testfolderman.cpp | 2 +- test/testsecurefiledrop.cpp | 4 +-- 19 files changed, 115 insertions(+), 32 deletions(-) diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index 2a91a8f79b..9aad5599b4 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -66,7 +66,7 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que rec._serverHasIgnoredFiles = (query.intValue(8) > 0); rec._checksumHeader = query.baValue(9); rec._e2eMangledName = query.baValue(10); - rec._isE2eEncrypted = query.intValue(11) > 0; + rec._isE2eEncrypted = static_cast(query.intValue(11)); rec._lockstate._locked = query.intValue(12) > 0; rec._lockstate._lockOwnerDisplayName = query.stringValue(13); rec._lockstate._lockOwnerId = query.stringValue(14); @@ -968,7 +968,7 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(15, checksum); query->bindValue(16, contentChecksumTypeId); query->bindValue(17, record._e2eMangledName); - query->bindValue(18, record.isE2eEncrypted()); + query->bindValue(18, static_cast(record._isE2eEncrypted)); query->bindValue(19, record._lockstate._locked ? 1 : 0); query->bindValue(20, record._lockstate._lockOwnerType); query->bindValue(21, record._lockstate._lockOwnerDisplayName); @@ -2694,4 +2694,10 @@ bool operator==(const SyncJournalDb::UploadInfo &lhs, && lhs._contentChecksum == rhs._contentChecksum; } +QDebug& operator<<(QDebug &stream, const SyncJournalFileRecord::EncryptionStatus status) +{ + stream << static_cast(status); + return stream; +} + } // namespace OCC diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index f32e4c588b..4daaf11209 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -53,6 +53,12 @@ public: return !_path.isEmpty(); } + enum class EncryptionStatus : int { + NotEncrypted = 0, + Encrypted = 1, + EncryptedMigratedV1_2 = 2, + }; + /** Returns the numeric part of the full id in _fileId. * * On the server this is sometimes known as the internal file id. @@ -67,7 +73,7 @@ public: [[nodiscard]] bool isVirtualFile() const { return _type == ItemTypeVirtualFile || _type == ItemTypeVirtualFileDownload; } [[nodiscard]] QString path() const { return QString::fromUtf8(_path); } [[nodiscard]] QString e2eMangledName() const { return QString::fromUtf8(_e2eMangledName); } - [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; } + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncJournalFileRecord::EncryptionStatus::NotEncrypted; } QByteArray _path; quint64 _inode = 0; @@ -80,13 +86,15 @@ public: bool _serverHasIgnoredFiles = false; QByteArray _checksumHeader; QByteArray _e2eMangledName; - bool _isE2eEncrypted = false; + EncryptionStatus _isE2eEncrypted = EncryptionStatus::NotEncrypted; SyncJournalFileLockInfo _lockstate; bool _isShared = false; qint64 _lastShareStateFetchedTimestamp = 0; bool _sharedByMe = false; }; +QDebug& operator<<(QDebug &stream, const SyncJournalFileRecord::EncryptionStatus status); + bool OCSYNC_EXPORT operator==(const SyncJournalFileRecord &lhs, const SyncJournalFileRecord &rhs); diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 04bd0f0b39..16f121e186 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1498,7 +1498,19 @@ void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr job->start(); } -FolderMetadata::FolderMetadata(AccountPtr account, const QByteArray& metadata, int statusCode) : _account(account) +FolderMetadata::FolderMetadata(AccountPtr account) + : _account(account) +{ + qCInfo(lcCseMetadata()) << "Setupping Empty Metadata"; + setupEmptyMetadata(); +} + +FolderMetadata::FolderMetadata(AccountPtr account, + RequiredMetadataVersion requiredMetadataVersion, + const QByteArray& metadata, + int statusCode) + : _account(account) + , _requiredMetadataVersion(requiredMetadataVersion) { if (metadata.isEmpty() || statusCode == 404) { qCInfo(lcCseMetadata()) << "Setupping Empty Metadata"; @@ -1541,7 +1553,7 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) } auto migratedMetadata = false; - if (_metadataKey.isEmpty()) { + if (_metadataKey.isEmpty() && _requiredMetadataVersion != RequiredMetadataVersion::Version1_2) { qCDebug(lcCse()) << "Migrating from v1.1 to v1.2"; migratedMetadata = true; @@ -1618,6 +1630,8 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) file.mimetype = QByteArrayLiteral("httpd/unix-directory"); } + qCDebug(lcCseMetadata) << "encrypted file" << decryptedFileObj["filename"].toString() << decryptedFileObj["key"].toString() << it.key(); + _files.push_back(file); } @@ -1631,6 +1645,10 @@ void FolderMetadata::setupExistingMetadata(const QByteArray& metadata) // decryption finished, create new metadata key to be used for encryption _metadataKey = EncryptionHelper::generateRandom(metadataKeySize); _isMetadataSetup = true; + + if (migratedMetadata) { + _encryptedMetadataNeedUpdate = true; + } } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. @@ -1792,6 +1810,11 @@ bool FolderMetadata::isFileDropPresent() const return _fileDrop.size() > 0; } +bool FolderMetadata::encryptedMetadataNeedUpdate() const +{ + return _encryptedMetadataNeedUpdate; +} + bool FolderMetadata::moveFromFileDropToFiles() { if (_fileDrop.isEmpty()) { diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index a180ef78f2..811dd8edbe 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -186,7 +186,18 @@ struct EncryptedFile { class OWNCLOUDSYNC_EXPORT FolderMetadata { public: - FolderMetadata(AccountPtr account, const QByteArray& metadata = QByteArray(), int statusCode = -1); + enum class RequiredMetadataVersion { + Version1, + Version1_2, + }; + + explicit FolderMetadata(AccountPtr account); + + explicit FolderMetadata(AccountPtr account, + RequiredMetadataVersion requiredMetadataVersion, + const QByteArray& metadata, + int statusCode = -1); + [[nodiscard]] QByteArray encryptedMetadata() const; void addEncryptedFile(const EncryptedFile& f); void removeEncryptedFile(const EncryptedFile& f); @@ -196,6 +207,8 @@ public: [[nodiscard]] bool isFileDropPresent() const; + [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + [[nodiscard]] bool moveFromFileDropToFiles(); [[nodiscard]] QJsonObject fileDrop() const; @@ -221,9 +234,11 @@ private: QVector _files; AccountPtr _account; + RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2; QVector> _sharing; QJsonObject _fileDrop; bool _isMetadataSetup = false; + bool _encryptedMetadataNeedUpdate = false; }; } // namespace OCC diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index c9aa2d02a1..fea3878651 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -532,7 +532,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_etag = serverEntry.etag; item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadCookies = serverEntry.directDownloadCookies; - item->_isEncrypted = serverEntry.isE2eEncrypted(); + item->_isEncrypted = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted; item->_encryptedFileName = [=] { if (serverEntry.e2eMangledName.isEmpty()) { return QString(); @@ -1292,7 +1292,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( if (base.isE2eEncrypted()) { // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information - item->_isEncrypted = true; + item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; } postProcessLocalNew(); finalize(); @@ -1850,6 +1850,7 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() connect(serverJob, &DiscoverySingleDirectoryJob::finished, this, [this, serverJob](const auto &results) { if (_dirItem) { _dirItem->_isFileDropDetected = serverJob->isFileDropDetected(); + _dirItem->_isEncryptedMetadataNeedUpdate = serverJob->encryptedMetadataNeedUpdate(); qCInfo(lcDisco) << "serverJob has finished for folder:" << _dirItem->_file << " and it has _isFileDropDetected:" << true; } _discoveryData->_currentlyActiveJobs--; diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index dc327b2374..54e8e4c38a 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -410,6 +410,11 @@ bool DiscoverySingleDirectoryJob::isFileDropDetected() const return _isFileDropDetected; } +bool DiscoverySingleDirectoryJob::encryptedMetadataNeedUpdate() const +{ + return _encryptedMetadataNeedUpdate; +} + static void propertyMapToRemoteInfo(const QMap &map, RemoteInfo &result) { for (auto it = map.constBegin(); it != map.constEnd(); ++it) { @@ -530,7 +535,7 @@ void DiscoverySingleDirectoryJob::directoryListingIteratedSlot(const QString &fi _fileId = map.value("id").toUtf8(); } if (map.contains("is-encrypted") && map.value("is-encrypted") == QStringLiteral("1")) { - _isE2eEncrypted = true; + _isE2eEncrypted = SyncFileItem::EncryptionStatus::Encrypted; Q_ASSERT(!_fileId.isEmpty()); } if (map.contains("size")) { @@ -621,8 +626,13 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in qCDebug(lcDiscovery) << "Metadata received, applying it to the result list"; Q_ASSERT(_subPath.startsWith('/')); - const auto metadata = FolderMetadata(_account, json.toJson(QJsonDocument::Compact), statusCode); + const auto metadata = FolderMetadata(_account, + _isE2eEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact), + statusCode); _isFileDropDetected = metadata.isFileDropPresent(); + _encryptedMetadataNeedUpdate = metadata.encryptedMetadataNeedUpdate(); + const auto encryptedFiles = metadata.files(); const auto findEncryptedFile = [=](const QString &name) { diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index 5f5a4223e4..c833fd719c 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -145,6 +145,7 @@ public: void start(); void abort(); [[nodiscard]] bool isFileDropDetected() const; + [[nodiscard]] bool encryptedMetadataNeedUpdate() const; // This is not actually a network job, it is just a job signals: @@ -162,7 +163,7 @@ private slots: private: - [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted; } + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; } QVector _results; QString _subPath; @@ -178,8 +179,9 @@ private: // If this directory is an external storage (The first item has 'M' in its permission) bool _isExternalStorage = false; // If this directory is e2ee - bool _isE2eEncrypted = false; + SyncFileItem::EncryptionStatus _isE2eEncrypted = SyncFileItem::EncryptionStatus::NotEncrypted; bool _isFileDropDetected = false; + bool _encryptedMetadataNeedUpdate = false; // If set, the discovery will finish with an error int64_t _size = 0; QString _error; diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index 033199ba33..99a5029850 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -56,7 +56,7 @@ void EncryptFolderJob::slotEncryptionFlagSuccess(const QByteArray &fileId) qCWarning(lcEncryptFolderJob) << "No valid record found in local DB for fileId" << fileId; } - rec._isE2eEncrypted = true; + rec._isE2eEncrypted = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2; const auto result = _journal->setFileRecord(rec); if (!result) { qCWarning(lcEncryptFolderJob) << "Error when setting the file record to the database" << rec._path << result.error(); diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 51510ad888..15567d6807 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -646,7 +646,7 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, const auto currentDirJob = directories.top().second; currentDirJob->appendJob(directoryPropagationJob.get()); } - if (item->_isFileDropDetected) { + if (item->_isFileDropDetected || item->_isEncryptedMetadataNeedUpdate) { directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); item->_instruction = CSYNC_INSTRUCTION_NONE; _anotherSyncNeeded = true; diff --git a/src/libsync/propagatedownloadencrypted.cpp b/src/libsync/propagatedownloadencrypted.cpp index 77aadbce95..9788d5cf1d 100644 --- a/src/libsync/propagatedownloadencrypted.cpp +++ b/src/libsync/propagatedownloadencrypted.cpp @@ -73,7 +73,9 @@ void PropagateDownloadEncrypted::checkFolderEncryptedMetadata(const QJsonDocumen qCDebug(lcPropagateDownloadEncrypted) << "Metadata Received reading" << _item->_instruction << _item->_file << _item->_encryptedFileName; const QString filename = _info.fileName(); - const FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact)); + const FolderMetadata metadata(_propagator->account(), + _item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact)); if (metadata.isMetadataSetup()) { const QVector files = metadata.files(); diff --git a/src/libsync/propagateremotedeleteencrypted.cpp b/src/libsync/propagateremotedeleteencrypted.cpp index 1d7ae8aa6c..ee1b57deef 100644 --- a/src/libsync/propagateremotedeleteencrypted.cpp +++ b/src/libsync/propagateremotedeleteencrypted.cpp @@ -51,7 +51,9 @@ void PropagateRemoteDeleteEncrypted::slotFolderEncryptedMetadataReceived(const Q return; } - FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode); + FolderMetadata metadata(_propagator->account(), + _item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact), statusCode); if (!metadata.isMetadataSetup()) { taskFailed(); diff --git a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp index c9f82ee8e4..5cae505ffd 100644 --- a/src/libsync/propagateremotedeleteencryptedrootfolder.cpp +++ b/src/libsync/propagateremotedeleteencryptedrootfolder.cpp @@ -81,7 +81,9 @@ void PropagateRemoteDeleteEncryptedRootFolder::slotFolderEncryptedMetadataReceiv return; } - FolderMetadata metadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode); + FolderMetadata metadata(_propagator->account(), + _item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact), statusCode); if (!metadata.isMetadataSetup()) { taskFailed(); diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp index b4dc65b971..82a09c0d70 100644 --- a/src/libsync/propagateremotemkdir.cpp +++ b/src/libsync/propagateremotemkdir.cpp @@ -243,7 +243,7 @@ void PropagateRemoteMkdir::slotEncryptFolderFinished() { qCDebug(lcPropagateRemoteMkdir) << "Success making the new folder encrypted"; propagator()->_activeJobList.removeOne(this); - _item->_isEncrypted = true; + _item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; success(); } diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index f027c1c8ae..ad2eff35cf 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -121,7 +121,9 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo qCDebug(lcPropagateUploadEncrypted) << "Metadata Received, Preparing it for the new file." << json.toVariant(); // Encrypt File! - _metadata.reset(new FolderMetadata(_propagator->account(), json.toJson(QJsonDocument::Compact), statusCode)); + _metadata.reset(new FolderMetadata(_propagator->account(), + _item->_isEncrypted == SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 ? FolderMetadata::RequiredMetadataVersion::Version1_2 : FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact), statusCode)); if (!_metadata->isMetadataSetup()) { if (_isFolderLocked) { @@ -169,7 +171,7 @@ void PropagateUploadEncrypted::slotFolderEncryptedMetadataReceived(const QJsonDo encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); _item->_encryptedFileName = _remoteParentPath + QLatin1Char('/') + encryptedFile.encryptedFilename; - _item->_isEncrypted = true; + _item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; qCDebug(lcPropagateUploadEncrypted) << "Creating the encrypted file."; diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 135f3c6371..ee2805e72d 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -49,7 +49,7 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._serverHasIgnoredFiles = _serverHasIgnoredFiles; rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); - rec._isE2eEncrypted = isEncrypted(); + rec._isE2eEncrypted = isEncrypted() ? SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2 : SyncJournalFileRecord::EncryptionStatus::NotEncrypted; rec._lockstate._locked = _locked == LockStatus::LockedItem; rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName; rec._lockstate._lockOwnerId = _lockOwnerId; @@ -86,7 +86,7 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_serverHasIgnoredFiles = rec._serverHasIgnoredFiles; item->_checksumHeader = rec._checksumHeader; item->_encryptedFileName = rec.e2eMangledName(); - item->_isEncrypted = rec.isE2eEncrypted(); + item->_isEncrypted = static_cast(rec._isE2eEncrypted); item->_locked = rec._lockstate._locked ? LockStatus::LockedItem : LockStatus::UnlockedItem; item->_lockOwnerDisplayName = rec._lockstate._lockOwnerDisplayName; item->_lockOwnerId = rec._lockstate._lockOwnerId; @@ -123,7 +123,7 @@ SyncFileItemPtr SyncFileItem::fromProperties(const QString &filePath, const QMap item->_isShared = item->_remotePerm.hasPermission(RemotePermissions::IsShared); item->_lastShareStateFetchedTimestamp = QDateTime::currentMSecsSinceEpoch(); - item->_isEncrypted = properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1"); + item->_isEncrypted = (properties.value(QStringLiteral("is-encrypted")) == QStringLiteral("1") ? SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted); item->_locked = properties.value(QStringLiteral("lock")) == QStringLiteral("1") ? SyncFileItem::LockStatus::LockedItem : SyncFileItem::LockStatus::UnlockedItem; item->_lockOwnerDisplayName = properties.value(QStringLiteral("lock-owner-displayname")); diff --git a/src/libsync/syncfileitem.h b/src/libsync/syncfileitem.h index 4715901ab3..f888ec6d48 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -46,6 +46,13 @@ public: }; Q_ENUM(Direction) + enum class EncryptionStatus : int { + NotEncrypted = 0, + Encrypted = 1, + EncryptedMigratedV1_2 = 2, + }; + Q_ENUM(EncryptionStatus) + // Note: the order of these statuses is used for ordering in the SortedActivityListModel enum Status { // stored in 4 bits NoStatus, @@ -138,7 +145,6 @@ public: , _status(NoStatus) , _isRestoration(false) , _isSelectiveSync(false) - , _isEncrypted(false) { } @@ -228,7 +234,7 @@ public: && !(_instruction == CSYNC_INSTRUCTION_CONFLICT && _status == SyncFileItem::Success); } - [[nodiscard]] bool isEncrypted() const { return _isEncrypted; } + [[nodiscard]] bool isEncrypted() const { return _isEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; } // Variables useful for everybody @@ -275,7 +281,7 @@ public: Status _status BITFIELD(4); bool _isRestoration BITFIELD(1); // The original operation was forbidden, and this is a restoration bool _isSelectiveSync BITFIELD(1); // The file is removed or ignored because it is in the selective sync list - bool _isEncrypted BITFIELD(1); // The file is E2EE or the content of the directory should be E2EE + EncryptionStatus _isEncrypted = EncryptionStatus::NotEncrypted; // The file is E2EE or the content of the directory should be E2EE quint16 _httpErrorCode = 0; RemotePermissions _remotePerm; QString _errorString; // Contains a string only in case of error @@ -321,6 +327,8 @@ public: bool _sharedByMe = false; bool _isFileDropDetected = false; + + bool _isEncryptedMetadataNeedUpdate = false; }; inline bool operator<(const SyncFileItemPtr &item1, const SyncFileItemPtr &item2) diff --git a/src/libsync/updatefiledropmetadata.cpp b/src/libsync/updatefiledropmetadata.cpp index 0d23f2f70f..02103a093b 100644 --- a/src/libsync/updatefiledropmetadata.cpp +++ b/src/libsync/updatefiledropmetadata.cpp @@ -122,8 +122,10 @@ void UpdateFileDropMetadataJob::slotFolderEncryptedMetadataReceived(const QJsonD qCDebug(lcUpdateFileDropMetadataJob) << "Metadata Received, Preparing it for the new file." << json.toVariant(); // Encrypt File! - _metadata.reset(new FolderMetadata(propagator()->account(), json.toJson(QJsonDocument::Compact), statusCode)); - if (!_metadata->moveFromFileDropToFiles()) { + _metadata.reset(new FolderMetadata(propagator()->account(), + FolderMetadata::RequiredMetadataVersion::Version1, + json.toJson(QJsonDocument::Compact), statusCode)); + if (!_metadata->moveFromFileDropToFiles() && !_metadata->encryptedMetadataNeedUpdate()) { unlockFolder(); return; } diff --git a/test/testfolderman.cpp b/test/testfolderman.cpp index 2cd2afd49b..db9b7d40ab 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -123,7 +123,7 @@ private slots: // the server, let's just manually set the encryption bool in the folder journal SyncJournalFileRecord rec; QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &rec)); - rec._isE2eEncrypted = true; + rec._isE2eEncrypted = SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2; rec._path = QStringLiteral("encrypted").toUtf8(); rec._type = CSyncEnums::ItemTypeDirectory; QVERIFY(folder->journalDb()->setFileRecord(rec)); diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp index 33653b800a..dd0a6ac00c 100644 --- a/test/testsecurefiledrop.cpp +++ b/test/testsecurefiledrop.cpp @@ -69,8 +69,8 @@ private slots: QFile fakeJsonReplyFile(QStringLiteral("fakefiledrope2eefoldermetadata.json")); if (fakeJsonReplyFile.open(QFile::ReadOnly)) { const auto jsonDoc = QJsonDocument::fromJson(fakeJsonReplyFile.readAll()); - _parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson())); - _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), jsonDoc.toJson())); + _parsedMetadataWithFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), FolderMetadata::RequiredMetadataVersion::Version1_2, jsonDoc.toJson())); + _parsedMetadataAfterProcessingFileDrop.reset(new FolderMetadata(_fakeFolder.syncEngine().account(), FolderMetadata::RequiredMetadataVersion::Version1_2, jsonDoc.toJson())); [[maybe_unused]] const auto result = _parsedMetadataAfterProcessingFileDrop->moveFromFileDropToFiles(); reply = new FakePayloadReply(op, req, jsonDoc.toJson(), nullptr); ++_getMetadataCallsCount; From 593ad6ee91395a4aa71679d54d63532cc04589fb Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 30 Mar 2023 09:14:26 +0200 Subject: [PATCH 14/16] better track encrypted item status Signed-off-by: Matthieu Gallien --- src/libsync/discovery.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index fea3878651..884d090335 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -532,7 +532,7 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo( item->_etag = serverEntry.etag; item->_directDownloadUrl = serverEntry.directDownloadUrl; item->_directDownloadCookies = serverEntry.directDownloadCookies; - item->_isEncrypted = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2 : SyncFileItem::EncryptionStatus::NotEncrypted; + item->_isEncrypted = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted; item->_encryptedFileName = [=] { if (serverEntry.e2eMangledName.isEmpty()) { return QString(); @@ -1292,7 +1292,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( if (base.isE2eEncrypted()) { // renaming the encrypted folder is done via remove + re-upload hence we need to mark the newly created folder as encrypted // base is a record in the SyncJournal database that contains the data about the being-renamed folder with it's old name and encryption information - item->_isEncrypted = SyncFileItem::EncryptionStatus::EncryptedMigratedV1_2; + item->_isEncrypted = static_cast(base._isE2eEncrypted); } postProcessLocalNew(); finalize(); From e22b130d984828d79eb5f6424c20a4f90b88bcc2 Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Thu, 30 Mar 2023 11:06:15 +0200 Subject: [PATCH 15/16] prevent syncing with downgraded metadata format should prevent a malicious server admin to make clients fallback to older vulnerable metadata format Signed-off-by: Matthieu Gallien --- src/libsync/owncloudpropagator.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index 15567d6807..7165431012 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -646,10 +646,23 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, const auto currentDirJob = directories.top().second; currentDirJob->appendJob(directoryPropagationJob.get()); } - if (item->_isFileDropDetected || item->_isEncryptedMetadataNeedUpdate) { + if (item->_isFileDropDetected) { directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); item->_instruction = CSYNC_INSTRUCTION_NONE; _anotherSyncNeeded = true; + } else if (item->_isEncryptedMetadataNeedUpdate) { + SyncJournalFileRecord record; + if (_journal->getFileRecord(item->_file, &record) && record._isE2eEncrypted == SyncJournalFileRecord::EncryptionStatus::EncryptedMigratedV1_2) { + qCDebug(lcPropagator) << "could have upgraded metadata"; + item->_instruction = CSyncEnums::CSYNC_INSTRUCTION_ERROR; + item->_errorString = tr("Error with the metadata. Getting unexpected metadata format."); + item->_status = SyncFileItem::NormalError; + emit itemCompleted(item); + } else { + directoryPropagationJob->appendJob(new UpdateFileDropMetadataJob(this, item->_file)); + item->_instruction = CSYNC_INSTRUCTION_NONE; + _anotherSyncNeeded = true; + } } directories.push(qMakePair(item->destination() + "/", directoryPropagationJob.release())); } From 6bf4570b99792678c3cc39e390c22aad215817bb Mon Sep 17 00:00:00 2001 From: Matthieu Gallien Date: Fri, 31 Mar 2023 17:17:26 +0200 Subject: [PATCH 16/16] compatibility with final file drop implementation Signed-off-by: Matthieu Gallien --- src/libsync/clientsideencryption.cpp | 59 +++++++++++++++++++++++----- src/libsync/clientsideencryption.h | 4 ++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index 16f121e186..93bcf9f384 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1681,6 +1681,24 @@ QByteArray FolderMetadata::decryptData(const QByteArray &data) const return QByteArray::fromBase64(decryptResult); } +QByteArray FolderMetadata::decryptDataUsingKey(const QByteArray &data, + const QByteArray &key, + const QByteArray &authenticationTag, + const QByteArray &initializationVector) const +{ + // Also base64 decode the result + QByteArray decryptResult = EncryptionHelper::decryptStringSymmetric(QByteArray::fromBase64(key), + data + '|' + initializationVector + '|' + authenticationTag); + + if (decryptResult.isEmpty()) + { + qCDebug(lcCse()) << "ERROR. Could not decrypt"; + return {}; + } + + return decryptResult; +} + // AES/GCM/NoPadding (128 bit key size) QByteArray FolderMetadata::encryptJsonObject(const QByteArray& obj, const QByteArray pass) const { @@ -1764,11 +1782,22 @@ QByteArray FolderMetadata::encryptedMetadata() const { files.insert(it->encryptedFilename, file); } - QJsonObject metaObject = { - {"metadata", metadata}, - {"files", files}, - {"filedrop", QJsonObject{}}, - }; + QJsonObject filedrop; + for (auto fileDropIt = _fileDrop.constBegin(), end = _fileDrop.constEnd(); fileDropIt != end; ++fileDropIt) { + filedrop.insert(fileDropIt.key(), fileDropIt.value()); + } + + auto metaObject = QJsonObject{ + {"metadata", metadata}, + }; + + if (files.count()) { + metaObject.insert("files", files); + } + + if (filedrop.count()) { + metaObject.insert("filedrop", filedrop); + } QJsonDocument internalMetadata; internalMetadata.setObject(metaObject); @@ -1821,18 +1850,29 @@ bool FolderMetadata::moveFromFileDropToFiles() return false; } - for (auto it = _fileDrop.constBegin(); it != _fileDrop.constEnd(); ++it) { + for (auto it = _fileDrop.begin(); it != _fileDrop.end(); ) { const auto fileObject = it.value().toObject(); + const auto decryptedKey = decryptData(fileObject["encryptedKey"].toString().toLocal8Bit()); + const auto decryptedAuthenticationTag = fileObject["encryptedTag"].toString().toLocal8Bit(); + const auto decryptedInitializationVector = fileObject["encryptedInitializationVector"].toString().toLocal8Bit(); + + if (decryptedKey.isEmpty() || decryptedAuthenticationTag.isEmpty() || decryptedInitializationVector.isEmpty()) { + qCDebug(lcCseMetadata) << "failed to decrypt filedrop entry" << it.key(); + continue; + } + const auto encryptedFile = fileObject["encrypted"].toString().toLocal8Bit(); - const auto decryptedFile = decryptData(encryptedFile); + const auto decryptedFile = decryptDataUsingKey(encryptedFile, decryptedKey, decryptedAuthenticationTag, decryptedInitializationVector); const auto decryptedFileDocument = QJsonDocument::fromJson(decryptedFile); const auto decryptedFileObject = decryptedFileDocument.object(); + const auto authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit()); + const auto initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit()); EncryptedFile file; file.encryptedFilename = it.key(); - file.authenticationTag = QByteArray::fromBase64(fileObject["authenticationTag"].toString().toLocal8Bit()); - file.initializationVector = QByteArray::fromBase64(fileObject["initializationVector"].toString().toLocal8Bit()); + file.authenticationTag = authenticationTag; + file.initializationVector = initializationVector; file.originalFilename = decryptedFileObject["filename"].toString(); file.encryptionKey = QByteArray::fromBase64(decryptedFileObject["key"].toString().toLocal8Bit()); @@ -1844,6 +1884,7 @@ bool FolderMetadata::moveFromFileDropToFiles() } _files.push_back(file); + it = _fileDrop.erase(it); } return true; diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 811dd8edbe..c4517379df 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -222,6 +222,10 @@ private: [[nodiscard]] QByteArray encryptData(const QByteArray &data) const; [[nodiscard]] QByteArray decryptData(const QByteArray &data) const; + [[nodiscard]] QByteArray decryptDataUsingKey(const QByteArray &data, + const QByteArray &key, + const QByteArray &authenticationTag, + const QByteArray &initializationVector) const; [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const;