diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index fc3c4149fd..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); @@ -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, 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 a846aeeb17..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,6 +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 != SyncJournalFileRecord::EncryptionStatus::NotEncrypted; } QByteArray _path; quint64 _inode = 0; @@ -79,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/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/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 0bc507d1a9..93bcf9f384 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 @@ -33,12 +25,20 @@ #include #include #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) { @@ -61,259 +61,263 @@ 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 auto metadataKeyJsonKey = "metadataKey"; - QList oldCipherFormatSplit(const QByteArray &cipher) - { - const auto separator = QByteArrayLiteral("fA=="); // BASE64 encoded '|' - auto result = QList(); +constexpr qint64 blockSize = 1024; - 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); - } +constexpr auto metadataKeySize = 16; - result.append(data); - return result; +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); } - 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 +361,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 +374,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 +887,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 +974,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 +1012,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 +1050,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 +1069,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 +1088,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 +1244,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 +1257,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 +1345,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 +1427,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 +1446,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(); } @@ -1494,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"; @@ -1507,96 +1523,97 @@ 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(); + + const auto metadataKeyFromJson = metadataObj[metadataKeyJsonKey].toString().toLocal8Bit(); + if (!metadataKeyFromJson.isEmpty()) { + const auto decryptedMetadataKeyBase64 = decryptData(metadataKeyFromJson); + if (!decryptedMetadataKeyBase64.isEmpty()) { + _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); + } } - QByteArray decryptedKey = QByteArray::fromBase64(b64DecryptedKey); - _metadataKeys.insert(it.key().toInt(), decryptedKey); - } + auto migratedMetadata = false; + if (_metadataKey.isEmpty() && _requiredMetadataVersion != RequiredMetadataVersion::Version1_2) { + qCDebug(lcCse()) << "Migrating from v1.1 to v1.2"; + migratedMetadata = true; - // 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; + if (metadataKeys.isEmpty()) { + qCDebug(lcCse()) << "Could not migrate. No metadata keys found!"; + return; + } - // 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"; - } + const auto lastMetadataKey = metadataKeys.keys().last(); + const auto decryptedMetadataKeyBase64 = decryptData(metadataKeys.value(lastMetadataKey).toString().toLocal8Bit()); + if (!decryptedMetadataKeyBase64.isEmpty()) { + _metadataKey = QByteArray::fromBase64(decryptedMetadataKeyBase64); + } + } - for (auto it = files.constBegin(), end = files.constEnd(); it != end; it++) { + 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(); + + _fileDrop = metaDataDoc.object().value("filedrop").toObject(); + + // 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. + + // Cool, We actually have the key, we can decrypt the rest of the metadata. + qCDebug(lcCse) << "Sharing: " << sharing; + if (sharing.size()) { + 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(); 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,15 +1624,31 @@ 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")) { file.mimetype = QByteArrayLiteral("httpd/unix-directory"); } + qCDebug(lcCseMetadata) << "encrypted file" << decryptedFileObj["filename"].toString() << decryptedFileObj["key"].toString() << it.key(); + _files.push_back(file); } + + if (!migratedMetadata && !checkMetadataKeyChecksum(metadataKey, metadataKeyChecksum)) { + qCInfo(lcCseMetadata) << "checksum comparison failed" << "server value" << metadataKeyChecksum << "client value" << computeMetadataKeyChecksum(metadataKey); + _metadataKey.clear(); + _files.clear(); + return; + } + + // 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. @@ -1642,12 +1675,30 @@ 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); } +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 { @@ -1659,44 +1710,55 @@ 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 hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; + + hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8()); + for (const auto &singleFile : _files) { + hashAlgorithm.addData(singleFile.encryptedFilename.toUtf8()); + } + hashAlgorithm.addData(metadataKey); + + return hashAlgorithm.result().toHex(); +} + 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(); + const auto encryptedMetadataKey = encryptData(_metadataKey.toBase64()); + QJsonObject metadata{ + {"version", version}, + {metadataKeyJsonKey, QJsonValue::fromVariant(encryptedMetadataKey)}, + {"checksum", QJsonValue::fromVariant(computeMetadataKeyChecksum(encryptedMetadataKey))}, }; QJsonObject files; @@ -1705,28 +1767,37 @@ 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!"; + 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); } - QJsonObject metaObject = { - {"metadata", metadata}, - {"files", files} - }; + 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); @@ -1741,7 +1812,6 @@ void FolderMetadata::addEncryptedFile(const EncryptedFile &f) { break; } } - _files.append(f); } @@ -1769,30 +1839,44 @@ bool FolderMetadata::isFileDropPresent() const return _fileDrop.size() > 0; } +bool FolderMetadata::encryptedMetadataNeedUpdate() const +{ + return _encryptedMetadataNeedUpdate; +} + bool FolderMetadata::moveFromFileDropToFiles() { if (_fileDrop.isEmpty()) { 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.metadataKey = fileObject["metadataKey"].toInt(); - 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()); 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")) { @@ -1800,6 +1884,7 @@ bool FolderMetadata::moveFromFileDropToFiles() } _files.push_back(file); + it = _fileDrop.erase(it); } return true; @@ -1813,10 +1898,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 +1976,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); diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 4cbe776b1e..c4517379df 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -182,13 +182,22 @@ struct EncryptedFile { QByteArray authenticationTag; QString encryptedFilename; QString originalFilename; - int fileVersion = 0; - int metadataKey = 0; }; 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); @@ -198,6 +207,8 @@ public: [[nodiscard]] bool isFileDropPresent() const; + [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + [[nodiscard]] bool moveFromFileDropToFiles(); [[nodiscard]] QJsonObject fileDrop() const; @@ -211,15 +222,27 @@ 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; + [[nodiscard]] bool checkMetadataKeyChecksum(const QByteArray &metadataKey, const QByteArray &metadataKeyChecksum) const; + + [[nodiscard]] QByteArray computeMetadataKeyChecksum(const QByteArray &metadataKey) const; + + QByteArray _metadataKey; + QVector _files; - QMap _metadataKeys; AccountPtr _account; + RequiredMetadataVersion _requiredMetadataVersion = RequiredMetadataVersion::Version1_2; QVector> _sharing; QJsonObject _fileDrop; + bool _isMetadataSetup = false; + bool _encryptedMetadataNeedUpdate = false; }; } // namespace OCC 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; } diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index e2c8a91d52..884d090335 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() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted; 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,10 +1289,10 @@ 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; + item->_isEncrypted = static_cast(base._isE2eEncrypted); } postProcessLocalNew(); finalize(); @@ -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); @@ -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 e2ec41d855..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) { @@ -458,7 +463,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); } @@ -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")) { @@ -576,7 +581,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; @@ -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) { @@ -640,7 +650,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..c833fd719c 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; @@ -144,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: @@ -160,6 +162,9 @@ private slots: void metadataError(const QByteArray& fileId, int httpReturnCode); private: + + [[nodiscard]] bool isE2eEncrypted() const { return _isE2eEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; } + QVector _results; QString _subPath; QByteArray _firstEtag; @@ -174,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 f73c82cccb..7165431012 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(); @@ -650,6 +650,19 @@ void OwncloudPropagator::startDirectoryPropagation(const SyncFileItemPtr &item, 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())); } @@ -1021,14 +1034,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/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/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/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 e8d86e3081..5cae505ffd 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; @@ -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 88401dbd5f..82a09c0d70 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 @@ -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/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/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 24f11ed7c4..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) { @@ -154,8 +156,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; @@ -171,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 1727a89389..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 3890e339f1..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,6 +234,8 @@ public: && !(_instruction == CSYNC_INSTRUCTION_CONFLICT && _status == SyncFileItem::Success); } + [[nodiscard]] bool isEncrypted() const { return _isEncrypted != SyncFileItem::EncryptionStatus::NotEncrypted; } + // Variables useful for everybody /** The syncfolder-relative filesystem path that the operation is about @@ -273,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 @@ -319,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 e81f2bfb65..db9b7d40ab 100644 --- a/test/testfolderman.cpp +++ b/test/testfolderman.cpp @@ -123,14 +123,14 @@ 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)); SyncJournalFileRecord updatedRec; QVERIFY(folder->journalDb()->getFileRecord(QStringLiteral("encrypted"), &updatedRec)); - QVERIFY(updatedRec._isE2eEncrypted); + QVERIFY(updatedRec.isE2eEncrypted()); QVERIFY(updatedRec.isDirectory()); FolderMan::instance()->removeE2eFiles(account); 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;