diff --git a/.drone.yml b/.drone.yml index 8e8b3c55a5e0a..181a3167ebe43 100644 --- a/.drone.yml +++ b/.drone.yml @@ -208,4 +208,3 @@ trigger: kind: signature hmac: 1fbd0241ba0d4ea2702804324f4932b3f29d3d937ef75906a529cd00c4252a57 -... diff --git a/CMakeLists.txt b/CMakeLists.txt index 351b3eaa00c41..f0926e9582411 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -231,20 +231,25 @@ if(BUILD_CLIENT) find_package(Sphinx) find_package(PdfLatex) find_package(OpenSSL 1.1 REQUIRED ) + find_package(PkgConfig REQUIRED) + pkg_check_modules(OPENSC-LIBP11 libp11 REQUIRED IMPORTED_TARGET) - find_package(ZLIB REQUIRED) - find_package(SQLite3 3.9.0 REQUIRED) + set(ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "c:/Windows/System32/eTPKCS11.dll" CACHE PATH "Path to the driver for end-to-end encryption token") + option(CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN "Enforce use of an hardware token for end-to-end encryption" true) - if(NOT WIN32 AND NOT APPLE) - find_package(PkgConfig REQUIRED) - pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET) + find_package(ZLIB REQUIRED) + find_package(SQLite3 3.9.0 REQUIRED) - if(CLOUDPROVIDERS_FOUND) - pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET) - pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET) - pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET) - endif() - endif() + if(NOT WIN32 AND NOT APPLE) + find_package(PkgConfig REQUIRED) + pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET) + + if(CLOUDPROVIDERS_FOUND) + pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET) + pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET) + pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET) + endif() + endif() endif() option(BUILD_WITH_WEBENGINE "BUILD_WITH_WEBENGINE" ON) diff --git a/NEXTCLOUD.cmake b/NEXTCLOUD.cmake index e2632dbcfb6d6..982b8cb515758 100644 --- a/NEXTCLOUD.cmake +++ b/NEXTCLOUD.cmake @@ -61,7 +61,6 @@ set( APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR ${NEXTCLOUD_BACKGROUND_COLOR} CA set( APPLICATION_WIZARD_HEADER_TITLE_COLOR "#ffffff" CACHE STRING "Hex color of the text in the wizard header") option( APPLICATION_WIZARD_USE_CUSTOM_LOGO "Use the logo from ':/client/theme/colored/wizard_logo.(png|svg)' else the default application icon is used" ON ) - # ## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen" # diff --git a/config.h.in b/config.h.in index be3005502032f..3156eec4ffd66 100644 --- a/config.h.in +++ b/config.h.in @@ -65,4 +65,8 @@ #cmakedefine WITH_WEBENGINE +#cmakedefine01 CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN + +#cmakedefine ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "@ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH@" + #endif diff --git a/resources.qrc b/resources.qrc index 9d1da12609314..62b27a6ba2483 100644 --- a/resources.qrc +++ b/resources.qrc @@ -7,6 +7,7 @@ src/gui/PredefinedStatusButton.qml src/gui/BasicComboBox.qml src/gui/ErrorBox.qml + src/gui/EncryptionTokenSelectionWindow.qml src/gui/filedetails/FileActivityView.qml src/gui/filedetails/FileDetailsPage.qml src/gui/filedetails/FileDetailsView.qml @@ -46,6 +47,7 @@ src/gui/tray/TalkReplyTextField.qml src/gui/tray/CallNotificationDialog.qml src/gui/tray/EditFileLocallyLoadingDialog.qml + src/gui/tray/EncryptionTokenDiscoveryDialog.qml src/gui/tray/NCBusyIndicator.qml src/gui/tray/NCIconWithBackgroundImage.qml src/gui/tray/NCToolTip.qml diff --git a/src/common/syncjournaldb.cpp b/src/common/syncjournaldb.cpp index fffd846b547a1..be99a1f802df1 100644 --- a/src/common/syncjournaldb.cpp +++ b/src/common/syncjournaldb.cpp @@ -48,8 +48,9 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg) #define GET_FILE_RECORD_QUERY \ "SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \ - " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \ - " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile" \ + " ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " \ + " lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, " \ + " sharedByMe, isLivePhoto, livePhotoFile" \ " FROM metadata" \ " LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id" @@ -67,19 +68,21 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que rec._checksumHeader = query.baValue(9); rec._e2eMangledName = query.baValue(10); rec._e2eEncryptionStatus = static_cast(query.intValue(11)); - rec._lockstate._locked = query.intValue(12) > 0; - rec._lockstate._lockOwnerDisplayName = query.stringValue(13); - rec._lockstate._lockOwnerId = query.stringValue(14); - rec._lockstate._lockOwnerType = query.int64Value(15); - rec._lockstate._lockEditorApp = query.stringValue(16); - rec._lockstate._lockTime = query.int64Value(17); - rec._lockstate._lockTimeout = query.int64Value(18); - rec._lockstate._lockToken = query.stringValue(19); - rec._isShared = query.intValue(20) > 0; - rec._lastShareStateFetchedTimestamp = query.int64Value(21); - rec._sharedByMe = query.intValue(22) > 0; - rec._isLivePhoto = query.intValue(23) > 0; - rec._livePhotoFile = query.stringValue(24); + rec._e2eCertificateFingerprint = query.baValue(12); + //Q_ASSERT(rec._e2eEncryptionStatus == SyncJournalFileRecord::EncryptionStatus::NotEncrypted || !rec._e2eCertificateFingerprint.isEmpty()); + rec._lockstate._locked = query.intValue(13) > 0; + rec._lockstate._lockOwnerDisplayName = query.stringValue(14); + rec._lockstate._lockOwnerId = query.stringValue(15); + rec._lockstate._lockOwnerType = query.int64Value(16); + rec._lockstate._lockEditorApp = query.stringValue(17); + rec._lockstate._lockTime = query.int64Value(18); + rec._lockstate._lockTimeout = query.int64Value(19); + rec._lockstate._lockToken = query.stringValue(20); + rec._isShared = query.intValue(21) > 0; + rec._lastShareStateFetchedTimestamp = query.int64Value(22); + rec._sharedByMe = query.intValue(23) > 0; + rec._isLivePhoto = query.intValue(24) > 0; + rec._livePhotoFile = query.stringValue(25); } static QByteArray defaultJournalMode(const QString &dbPath) @@ -783,6 +786,7 @@ bool SyncJournalDb::updateMetadataTableStructure() addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT")); addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER")); + addColumn(QStringLiteral("e2eCertificateFingerprint"), QStringLiteral("TEXT")); addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER")); addColumn(QStringLiteral("sharedByMe"), QStringLiteral("INTEGER")); @@ -995,9 +999,9 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata " "(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, " - "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, " + "contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, lock, lockType, lockOwnerDisplayName, lockOwnerId, " "lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile) " - "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31);"), + "VALUES (?1 , ?2, ?3 , ?4 , ?5 , ?6 , ?7, ?8 , ?9 , ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, ?20, ?21, ?22, ?23, ?24, ?25, ?26, ?27, ?28, ?29, ?30, ?31, ?32);"), _db); if (!query) { qCDebug(lcDb) << "database error:" << query->error(); @@ -1022,19 +1026,20 @@ Result SyncJournalDb::setFileRecord(const SyncJournalFileRecord & query->bindValue(16, contentChecksumTypeId); query->bindValue(17, record._e2eMangledName); query->bindValue(18, static_cast(record._e2eEncryptionStatus)); - query->bindValue(19, record._lockstate._locked ? 1 : 0); - query->bindValue(20, record._lockstate._lockOwnerType); - query->bindValue(21, record._lockstate._lockOwnerDisplayName); - query->bindValue(22, record._lockstate._lockOwnerId); - query->bindValue(23, record._lockstate._lockEditorApp); - query->bindValue(24, record._lockstate._lockTime); - query->bindValue(25, record._lockstate._lockTimeout); - query->bindValue(26, record._lockstate._lockToken); - query->bindValue(27, record._isShared); - query->bindValue(28, record._lastShareStateFetchedTimestamp); - query->bindValue(29, record._sharedByMe); - query->bindValue(30, record._isLivePhoto); - query->bindValue(31, record._livePhotoFile); + query->bindValue(19, record._e2eCertificateFingerprint); + query->bindValue(20, record._lockstate._locked ? 1 : 0); + query->bindValue(21, record._lockstate._lockOwnerType); + query->bindValue(22, record._lockstate._lockOwnerDisplayName); + query->bindValue(23, record._lockstate._lockOwnerId); + query->bindValue(24, record._lockstate._lockEditorApp); + query->bindValue(25, record._lockstate._lockTime); + query->bindValue(26, record._lockstate._lockTimeout); + query->bindValue(27, record._lockstate._lockToken); + query->bindValue(28, record._isShared); + query->bindValue(29, record._lastShareStateFetchedTimestamp); + query->bindValue(30, record._sharedByMe); + query->bindValue(31, record._isLivePhoto); + query->bindValue(32, record._livePhotoFile); if (!query->exec()) { qCDebug(lcDb) << "database error:" << query->error(); diff --git a/src/common/syncjournalfilerecord.h b/src/common/syncjournalfilerecord.h index 4d299e3a9ff8d..f36b53095b300 100644 --- a/src/common/syncjournalfilerecord.h +++ b/src/common/syncjournalfilerecord.h @@ -84,6 +84,7 @@ class OCSYNC_EXPORT SyncJournalFileRecord QByteArray _checksumHeader; QByteArray _e2eMangledName; EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted; + QByteArray _e2eCertificateFingerprint; SyncJournalFileLockInfo _lockstate; bool _isShared = false; qint64 _lastShareStateFetchedTimestamp = 0; diff --git a/src/csync/csync.h b/src/csync/csync.h index 9da7497f75c7f..8329020f58463 100644 --- a/src/csync/csync.h +++ b/src/csync/csync.h @@ -140,24 +140,25 @@ Q_ENUM_NS(csync_status_codes_e) * the csync state of a file. */ enum SyncInstructions { - CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */ - CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */ - CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */ - CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */ - CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */ - CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */ - CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */ - CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */ - CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */ - CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7, - CSYNC_INSTRUCTION_ERROR = 1 << 8, - CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE) - Used when the type of something changes from directory to file - or back. */ - CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db, - but without any propagation (UPDATE|RECONCILE) */ - CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */ - CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */ + CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */ + CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */ + CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */ + CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */ + CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */ + CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */ + CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */ + CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */ + CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */ + CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7, + CSYNC_INSTRUCTION_ERROR = 1 << 8, + CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE) + Used when the type of something changes from directory to file + or back. */ + CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db, + but without any propagation (UPDATE|RECONCILE) */ + CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */ + CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */ + CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA = 1 << 14, /* encryption metadata needs update after certificate was migrated */ }; Q_ENUM_NS(SyncInstructions) diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt index 9660bfb7ee7f2..0370858e2b056 100644 --- a/src/gui/CMakeLists.txt +++ b/src/gui/CMakeLists.txt @@ -146,6 +146,7 @@ set(client_SRCS syncrunfilelog.cpp systray.h systray.cpp + EncryptionTokenSelectionWindow.qml thumbnailjob.h thumbnailjob.cpp userinfo.h diff --git a/src/gui/EncryptionTokenSelectionWindow.qml b/src/gui/EncryptionTokenSelectionWindow.qml new file mode 100644 index 0000000000000..8872d978335a5 --- /dev/null +++ b/src/gui/EncryptionTokenSelectionWindow.qml @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2023 by Matthieu Gallien + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 +import QtQml.Models 2.15 + +import com.nextcloud.desktopclient 1.0 +import Style 1.0 + +import "./tray" + +ApplicationWindow { + id: encryptionKeyChooserDialog + + required property var certificatesInfo + required property ClientSideEncryptionTokenSelector certificateSelector + property string selectedSerialNumber: '' + + flags: Qt.Window | Qt.Dialog + visible: true + modality: Qt.ApplicationModal + + width: 400 + height: 600 + minimumWidth: 400 + minimumHeight: 600 + + title: qsTr('Token Encryption Key Chooser') + + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + brightText: Style.ncTextBrightColor + highlight: Style.lightHover + highlightedText: Style.ncTextColor + light: Style.lightHover + midlight: Style.ncSecondaryTextColor + mid: Style.darkerHover + dark: Style.menuBorder + button: Style.buttonBackgroundColor + window: Style.backgroundColor + base: Style.backgroundColor + toolTipBase: Style.backgroundColor + toolTipText: Style.ncTextColor + } + + onClosing: function(close) { + Systray.destroyDialog(self); + close.accepted = true + } + + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: 20 + anchors.rightMargin: 20 + anchors.bottomMargin: 20 + anchors.topMargin: 20 + spacing: 15 + z: 2 + + EnforcedPlainTextLabel { + text: qsTr("Available Keys for end-to-end Encryption:") + font.bold: true + font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog + Layout.fillWidth: true + } + + ScrollView { + Layout.fillWidth: true + Layout.fillHeight: true + + clip: true + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: tokensListView + + currentIndex: -1 + + model: DelegateModel { + model: certificatesInfo + + delegate: ItemDelegate { + width: tokensListView.contentItem.width + + text: modelData.subject + + highlighted: tokensListView.currentIndex === index + + onClicked: function() + { + tokensListView.currentIndex = index + selectedSerialNumber = modelData.serialNumber + } + } + } + } + } + + DialogButtonBox { + Layout.fillWidth: true + + Button { + text: qsTr("Choose") + DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole + } + Button { + text: qsTr("Cancel") + DialogButtonBox.buttonRole: DialogButtonBox.RejectRole + } + + onAccepted: function() { + Systray.destroyDialog(encryptionKeyChooserDialog) + certificateSelector.serialNumber = selectedSerialNumber + } + + onRejected: function() { + Systray.destroyDialog(encryptionKeyChooserDialog) + certificateSelector.serialNumber = '' + } + } + } + + Rectangle { + color: Style.backgroundColor + anchors.fill: parent + z: 1 + } +} diff --git a/src/gui/accountmanager.cpp b/src/gui/accountmanager.cpp index f27d2742703a7..3e98720a4dc3b 100644 --- a/src/gui/accountmanager.cpp +++ b/src/gui/accountmanager.cpp @@ -59,6 +59,7 @@ constexpr auto networkUploadLimitSettingC = "networkUploadLimitSetting"; constexpr auto networkDownloadLimitSettingC = "networkDownloadLimitSetting"; constexpr auto networkUploadLimitC = "networkUploadLimit"; constexpr auto networkDownloadLimitC = "networkDownloadLimit"; +constexpr auto encryptionCertificateSha256FingerprintC = "encryptionCertificateSha256Fingerprint"; constexpr auto generalC = "General"; constexpr auto dummyAuthTypeC = "dummy"; @@ -334,12 +335,12 @@ void AccountManager::saveAccountHelper(Account *account, QSettings &settings, bo settings.setValue(QLatin1String(versionC), maxAccountVersion); settings.setValue(QLatin1String(urlC), account->_url.toString()); settings.setValue(QLatin1String(davUserC), account->_davUser); - settings.setValue(QLatin1String(displayNameC), account->davDisplayName()); + settings.setValue(QLatin1String(displayNameC), account->_displayName); settings.setValue(QLatin1String(serverVersionC), account->_serverVersion); settings.setValue(QLatin1String(serverColorC), account->_serverColor); settings.setValue(QLatin1String(serverTextColorC), account->_serverTextColor); settings.setValue(QLatin1String(serverHasValidSubscriptionC), account->serverHasValidSubscription()); - + settings.setValue(QLatin1String(encryptionCertificateSha256FingerprintC), account->encryptionCertificateFingerprint()); if (!account->_skipE2eeMetadataChecksumValidation) { settings.remove(QLatin1String(skipE2eeMetadataChecksumValidationC)); } else { @@ -545,6 +546,8 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings) }); job->start(); + acc->setEncryptionCertificateFingerprint(settings.value(QLatin1String(encryptionCertificateSha256FingerprintC)).toByteArray()); + // now the server cert, it is in the general group settings.beginGroup(QLatin1String(generalC)); const auto certs = QSslCertificate::fromData(settings.value(caCertsKeyC).toByteArray()); diff --git a/src/gui/accountsettings.cpp b/src/gui/accountsettings.cpp index 6a7051a092a27..29084e8f296a8 100644 --- a/src/gui/accountsettings.cpp +++ b/src/gui/accountsettings.cpp @@ -74,6 +74,7 @@ constexpr auto e2eUiActionIdKey = "id"; constexpr auto e2EeUiActionEnableEncryptionId = "enable_encryption"; constexpr auto e2EeUiActionDisableEncryptionId = "disable_encryption"; constexpr auto e2EeUiActionDisplayMnemonicId = "display_mnemonic"; +constexpr auto e2EeUiActionMigrateCertificateId = "migrate_certificate"; } namespace OCC { @@ -290,6 +291,11 @@ AccountSettings::AccountSettings(AccountState *accountState, QWidget *parent) this, &AccountSettings::slotUpdateQuota); customizeStyle(); + + connect(_accountState->account()->e2e(), &ClientSideEncryption::startingDiscoveryEncryptionUsbToken, + Systray::instance(), &Systray::createEncryptionTokenDiscoveryDialog); + connect(_accountState->account()->e2e(), &ClientSideEncryption::finishedDiscoveryEncryptionUsbToken, + Systray::instance(), &Systray::destroyEncryptionTokenDiscoveryDialog); } void AccountSettings::slotE2eEncryptionMnemonicReady() @@ -299,10 +305,16 @@ void AccountSettings::slotE2eEncryptionMnemonicReady() disableEncryptionForAccount(_accountState->account()); }); - const auto actionDisplayMnemonic = addActionToEncryptionMessage(tr("Display mnemonic"), e2EeUiActionDisplayMnemonicId); - connect(actionDisplayMnemonic, &QAction::triggered, this, [this]() { - displayMnemonic(_accountState->account()->e2e()->_mnemonic); - }); + if (_accountState->account()->e2e()->userCertificateNeedsMigration()) { + slotE2eEncryptionCertificateNeedMigration(); + } + + if (!_accountState->account()->e2e()->getMnemonic().isEmpty()) { + const auto actionDisplayMnemonic = addActionToEncryptionMessage(tr("Display mnemonic"), e2EeUiActionDisplayMnemonicId); + connect(actionDisplayMnemonic, &QAction::triggered, this, [this]() { + displayMnemonic(_accountState->account()->e2e()->getMnemonic()); + }); + } _ui->encryptionMessage->setMessageType(KMessageWidget::Positive); _ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled for this account")); @@ -315,18 +327,19 @@ void AccountSettings::slotE2eEncryptionGenerateKeys() connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(true); _accountState->account()->setAskUserForMnemonic(true); - _accountState->account()->e2e()->initialize(_accountState->account()); + _accountState->account()->e2e()->initialize(this, _accountState->account()); } void AccountSettings::slotE2eEncryptionInitializationFinished(bool isNewMnemonicGenerated) { disconnect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotE2eEncryptionInitializationFinished); - if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) { + if (_accountState->account()->e2e()->isInitialized()) { removeActionFromEncryptionMessage(e2EeUiActionEnableEncryptionId); slotE2eEncryptionMnemonicReady(); if (isNewMnemonicGenerated) { - displayMnemonic(_accountState->account()->e2e()->_mnemonic); + displayMnemonic(_accountState->account()->e2e()->getMnemonic()); } + Q_EMIT _accountState->account()->wantsFoldersSynced(); } _accountState->account()->setAskUserForMnemonic(false); } @@ -396,7 +409,7 @@ bool AccountSettings::canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo return false; } - if (!_accountState->account()->e2e() || _accountState->account()->e2e()->_mnemonic.isEmpty()) { + if (!_accountState->account()->e2e() || !_accountState->account()->e2e()->isInitialized()) { QMessageBox msgBox; msgBox.setText(tr("End-to-end encryption is not configured on this device. " "Once it is configured, you will be able to encrypt this folder.\n" @@ -1122,6 +1135,16 @@ void AccountSettings::disableEncryptionForAccount(const AccountPtr &account) con } } +void AccountSettings::migrateCertificateForAccount(const AccountPtr &account) +{ + for (const auto action : _ui->encryptionMessage->actions()) { + _ui->encryptionMessage->removeAction(action); + } + + account->e2e()->migrateCertificate(); + slotE2eEncryptionGenerateKeys(); +} + void AccountSettings::showConnectionLabel(const QString &message, QStringList errors) { const auto errStyle = QLatin1String("color:#ffffff; background-color:#bb4d4d;padding:5px;" @@ -1469,7 +1492,7 @@ void AccountSettings::slotSelectiveSyncChanged(const QModelIndex &topLeft, void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() { - if (_accountState->account()->e2e()->_mnemonic.isEmpty()) { + if (!_accountState->account()->e2e()->isInitialized()) { return; } @@ -1502,6 +1525,14 @@ void AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync() } } +void AccountSettings::slotE2eEncryptionCertificateNeedMigration() +{ + const auto actionMigrateCertificate = addActionToEncryptionMessage(tr("Migrate certificate to a new one"), e2EeUiActionMigrateCertificateId); + connect(actionMigrateCertificate, &QAction::triggered, this, [this] { + migrateCertificateForAccount(_accountState->account()); + }); +} + void AccountSettings::updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const { folder->journalDb()->setSelectiveSyncList(SyncJournalDb::SelectiveSyncBlackList, blackList); @@ -1647,13 +1678,13 @@ void AccountSettings::initializeE2eEncryption() { connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, &AccountSettings::slotPossiblyUnblacklistE2EeFoldersAndRestartSync); - if (!_accountState->account()->e2e()->_mnemonic.isEmpty()) { + if (_accountState->account()->e2e()->isInitialized()) { slotE2eEncryptionMnemonicReady(); } else { initializeE2eEncryptionSettingsMessage(); connect(_accountState->account()->e2e(), &ClientSideEncryption::initializationFinished, this, [this] { - if (!_accountState->account()->e2e()->_publicKey.isNull()) { + if (!_accountState->account()->e2e()->getPublicKey().isNull()) { _ui->encryptionMessage->setText(tr("End-to-end encryption has been enabled on this account with another device." "
" "It can be enabled on this device by entering your mnemonic." @@ -1662,7 +1693,7 @@ void AccountSettings::initializeE2eEncryption() } }); _accountState->account()->setE2eEncryptionKeysGenerationAllowed(false); - _accountState->account()->e2e()->initialize(_accountState->account()); + _accountState->account()->e2e()->initialize(this, _accountState->account()); } } @@ -1677,7 +1708,7 @@ void AccountSettings::resetE2eEncryption() checkClientSideEncryptionState(); const auto account = _accountState->account(); - if (account->e2e()->_mnemonic.isEmpty()) { + if (!account->e2e()->isInitialized()) { FolderMan::instance()->removeE2eFiles(account); } } diff --git a/src/gui/accountsettings.h b/src/gui/accountsettings.h index 8981f1dffe6f8..592e7175e1d5c 100644 --- a/src/gui/accountsettings.h +++ b/src/gui/accountsettings.h @@ -62,6 +62,7 @@ class AccountSettings : public QWidget ~AccountSettings() override; [[nodiscard]] QSize sizeHint() const override { return ownCloudGui::settingsDialogSize(); } bool canEncryptOrDecrypt(const FolderStatusModel::SubFolderInfo* folderInfo); + [[nodiscard]] OCC::AccountState *accountsState() const { return _accountState; } signals: void folderChanged(); @@ -76,7 +77,6 @@ public slots: void slotUpdateQuota(qint64 total, qint64 used); void slotAccountStateChanged(); void slotStyleChanged(); - OCC::AccountState *accountsState() { return _accountState; } void slotHideSelectiveSyncWidget(); protected slots: @@ -116,6 +116,8 @@ protected slots: const QVector &roles); void slotPossiblyUnblacklistE2EeFoldersAndRestartSync(); + void slotE2eEncryptionCertificateNeedMigration(); + private slots: void updateBlackListAndScheduleFolderSync(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist) const; void folderTerminateSyncAndUpdateBlackList(const QStringList &blackList, OCC::Folder *folder, const QStringList &foldersToRemoveFromBlacklist); @@ -123,6 +125,7 @@ private slots: private slots: void displayMnemonic(const QString &mnemonic); void disableEncryptionForAccount(const OCC::AccountPtr &account) const; + void migrateCertificateForAccount(const OCC::AccountPtr &account); void showConnectionLabel(const QString &message, QStringList errors = QStringList()); void openIgnoredFilesDialog(const QString & absFolderPath); void customizeStyle(); diff --git a/src/gui/connectionvalidator.cpp b/src/gui/connectionvalidator.cpp index 75cb21762fcb0..ebe1c1af525f6 100644 --- a/src/gui/connectionvalidator.cpp +++ b/src/gui/connectionvalidator.cpp @@ -321,7 +321,7 @@ void ConnectionValidator::slotUserFetched(UserInfo *userInfo) #ifndef TOKEN_AUTH_ONLY connect(_account->e2e(), &ClientSideEncryption::initializationFinished, this, &ConnectionValidator::reportConnected); - _account->e2e()->initialize(_account); + _account->e2e()->initialize(nullptr, _account); #else reportResult(Connected); #endif diff --git a/src/gui/filedetails/sharemodel.cpp b/src/gui/filedetails/sharemodel.cpp index a199231998ad7..4dfa8346d5a4d 100644 --- a/src/gui/filedetails/sharemodel.cpp +++ b/src/gui/filedetails/sharemodel.cpp @@ -369,8 +369,12 @@ void ShareModel::initShareManager() connect(_manager.data(), &ShareManager::linkShareCreated, this, &ShareModel::slotAddShare); connect(_manager.data(), &ShareManager::linkShareRequiresPassword, this, &ShareModel::requestPasswordForLinkShare); connect(_manager.data(), &ShareManager::serverError, this, [this](const int code, const QString &message) { - _hasInitialShareFetchCompleted = true; - Q_EMIT hasInitialShareFetchCompletedChanged(); + if (!_hasInitialShareFetchCompleted) { + _hasInitialShareFetchCompleted = true; + Q_EMIT hasInitialShareFetchCompletedChanged(); + } + + qCWarning(lcShareModel) << "Error from server from ShareManager class and initShareManager" << code << message; emit serverError(code, message); }); @@ -633,7 +637,10 @@ void ShareModel::slotAddShare(const SharePtr &share) const QPersistentModelIndex sharePersistentIndex(shareModelIndex); _shareIdIndexHash.insert(shareId, sharePersistentIndex); - connect(share.data(), &Share::serverError, this, &ShareModel::slotServerError); + connect(share.data(), &Share::serverError, this, [this] (int code, const QString &message) { + qCWarning(lcShareModel) << "Error from server from Share class" << code << message; + Q_EMIT serverError(code, message); + }); connect(share.data(), &Share::passwordSetError, this, [this, shareId](const int code, const QString &message) { _shareIdRecentlySetPasswords.remove(shareId); slotSharePasswordSet(shareId); @@ -656,10 +663,6 @@ void ShareModel::slotAddShare(const SharePtr &share) connect(userGroupShare.data(), &UserGroupShare::expireDateSet, this, [this, shareId]{ slotShareExpireDateSet(shareId); }); } - if (_manager) { - connect(_manager.data(), &ShareManager::serverError, this, &ShareModel::slotServerError); - } - handleLinkShare(); Q_EMIT sharesChanged(); } @@ -708,12 +711,6 @@ void ShareModel::slotRemoveShareWithId(const QString &shareId) Q_EMIT sharesChanged(); } -void ShareModel::slotServerError(const int code, const QString &message) -{ - qCWarning(lcShareModel) << "Error from server" << code << message; - Q_EMIT serverError(code, message); -} - void ShareModel::slotAddSharee(const ShareePtr &sharee) { if(!sharee) { diff --git a/src/gui/filedetails/sharemodel.h b/src/gui/filedetails/sharemodel.h index 5357a9366d6f3..e34dece4b1f9d 100644 --- a/src/gui/filedetails/sharemodel.h +++ b/src/gui/filedetails/sharemodel.h @@ -216,7 +216,6 @@ private slots: void setHideDownloadEnabledChangeInProgress(const QString &shareId, const bool isInProgress); void slotPropfindReceived(const QVariantMap &result); - void slotServerError(const int code, const QString &message); void slotAddShare(const OCC::SharePtr &share); void slotRemoveShareWithId(const QString &shareId); void slotSharesFetched(const QList &shares); diff --git a/src/gui/folder.cpp b/src/gui/folder.cpp index a2547dafafa69..b055392d72c9e 100644 --- a/src/gui/folder.cpp +++ b/src/gui/folder.cpp @@ -138,6 +138,11 @@ Folder::Folder(const FolderDefinition &definition, connect(_accountState->account().data(), &Account::capabilitiesChanged, this, &Folder::slotCapabilitiesChanged); + connect(_accountState->account().data(), &Account::wantsFoldersSynced, this, [this] () { + _engine->setLocalDiscoveryOptions(OCC::LocalDiscoveryStyle::FilesystemOnly); + QMetaObject::invokeMethod(_engine.data(), "startSync", Qt::QueuedConnection); + }); + // Potentially upgrade suffix vfs to windows vfs ENFORCE(_vfs); if (_definition.virtualFilesMode == Vfs::WithSuffix diff --git a/src/gui/folderman.cpp b/src/gui/folderman.cpp index 2f08c40a2f29c..c8f803b9c300a 100644 --- a/src/gui/folderman.cpp +++ b/src/gui/folderman.cpp @@ -668,7 +668,7 @@ void FolderMan::forceSyncForFolder(Folder *folder) void FolderMan::removeE2eFiles(const AccountPtr &account) const { - Q_ASSERT(account->e2e()->_mnemonic.isEmpty()); + Q_ASSERT(!account->e2e()->isInitialized()); for (const auto folder : map()) { if(folder->accountState()->account()->id() == account->id()) { folder->removeLocalE2eFiles(); diff --git a/src/gui/folderstatusmodel.cpp b/src/gui/folderstatusmodel.cpp index f712cf92cffdc..4639f2f413bec 100644 --- a/src/gui/folderstatusmodel.cpp +++ b/src/gui/folderstatusmodel.cpp @@ -758,8 +758,7 @@ void FolderStatusModel::slotUpdateDirectories(const QStringList &list) newInfo._isNonDecryptable = newInfo.isEncrypted() && _accountState->account()->e2e() - && !_accountState->account()->e2e()->_publicKey.isNull() - && _accountState->account()->e2e()->_privateKey.isNull(); + && !_accountState->account()->e2e()->isInitialized(); SyncJournalFileRecord rec; if (!parentInfo->_folder->journalDb()->getFileRecordByE2eMangledName(removeTrailingSlash(relativePath), &rec)) { diff --git a/src/gui/owncloudgui.cpp b/src/gui/owncloudgui.cpp index dccdcc3a8c7f9..6ce1c0a3fb4de 100644 --- a/src/gui/owncloudgui.cpp +++ b/src/gui/owncloudgui.cpp @@ -149,6 +149,7 @@ ownCloudGui::ownCloudGui(Application *parent) qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UnifiedSearchResultsListModel", "UnifiedSearchResultsListModel"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "UserStatus", "Access to Status enum"); qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "Sharee", "Access to Type enum"); + qmlRegisterUncreatableType("com.nextcloud.desktopclient", 1, 0, "ClientSideEncryptionTokenSelector", "Access to the certificate selector"); qRegisterMetaType("ActivityListModel*"); qRegisterMetaType("UnifiedSearchResultsListModel*"); @@ -605,10 +606,15 @@ void ownCloudGui::slotShowSettings() if (_settingsDialog.isNull()) { _settingsDialog = new SettingsDialog(this); _settingsDialog->setAttribute(Qt::WA_DeleteOnClose, true); + #ifdef Q_OS_MAC auto *fgbg = new ForegroundBackground(); _settingsDialog->installEventFilter(fgbg); #endif + + connect(_tray.data(), &Systray::hideSettingsDialog, + _settingsDialog.data(), &SettingsDialog::close); + _settingsDialog->show(); } raiseDialog(_settingsDialog.data()); diff --git a/src/gui/settingsdialog.cpp b/src/gui/settingsdialog.cpp index 7b4d778946f22..ae76ec4aeafb9 100644 --- a/src/gui/settingsdialog.cpp +++ b/src/gui/settingsdialog.cpp @@ -222,6 +222,10 @@ void SettingsDialog::slotSwitchPage(QAction *action) void SettingsDialog::showFirstPage() { + foreach(auto ai, AccountManager::instance()->accounts()) { + accountAdded(ai.data()); + } + QList actions = _toolBar->actions(); if (!actions.empty()) { actions.first()->trigger(); diff --git a/src/gui/socketapi/socketapi.cpp b/src/gui/socketapi/socketapi.cpp index ae32d0c6be3aa..75dd094fb68f8 100644 --- a/src/gui/socketapi/socketapi.cpp +++ b/src/gui/socketapi/socketapi.cpp @@ -545,7 +545,7 @@ void SocketApi::processEncryptRequest(const QString &localFile) const auto rec = fileData.journalRecord(); Q_ASSERT(rec.isValid()); - if (!account->e2e() || account->e2e()->_mnemonic.isEmpty()) { + if (!account->e2e() || !account->e2e()->isInitialized()) { const int ret = QMessageBox::critical(nullptr, tr("Failed to encrypt folder at \"%1\"").arg(fileData.folderRelativePath), tr("The account %1 does not have end-to-end encryption configured. " diff --git a/src/gui/systray.cpp b/src/gui/systray.cpp index 85fee594c9f7b..a2fd1eab773e6 100644 --- a/src/gui/systray.cpp +++ b/src/gui/systray.cpp @@ -24,6 +24,7 @@ #include "configfile.h" #include "accessmanager.h" #include "callstatechecker.h" +#include "clientsideencryptiontokenselector.h" #include #include @@ -34,6 +35,8 @@ #include #include #include +#include +#include #ifdef USE_FDO_NOTIFICATIONS #include @@ -317,6 +320,34 @@ void Systray::createResolveConflictsDialog(const OCC::ActivityList &allConflicts dialog.take(); } +void Systray::createEncryptionTokenDiscoveryDialog() +{ + if (_encryptionTokenDiscoveryDialog) { + return; + } + + qCDebug(lcSystray) << "Opening an encryption token discovery dialog..."; + + const auto encryptionTokenDiscoveryDialog = new QQmlComponent(_trayEngine.get(), QStringLiteral("qrc:/qml/src/gui/tray/EncryptionTokenDiscoveryDialog.qml")); + + if (encryptionTokenDiscoveryDialog->isError()) { + qCWarning(lcSystray) << encryptionTokenDiscoveryDialog->errorString(); + return; + } + + _encryptionTokenDiscoveryDialog = encryptionTokenDiscoveryDialog->createWithInitialProperties(QVariantMap{}); +} + +void Systray::destroyEncryptionTokenDiscoveryDialog() +{ + if (!_encryptionTokenDiscoveryDialog) { + return; + } + qCDebug(lcSystray) << "Closing an encryption token discovery dialog..."; + _encryptionTokenDiscoveryDialog->deleteLater(); + _encryptionTokenDiscoveryDialog = nullptr; +} + bool Systray::raiseDialogs() { return raiseFileDetailDialogs(); diff --git a/src/gui/systray.h b/src/gui/systray.h index 7b17bab423aae..f1b0557e06590 100644 --- a/src/gui/systray.h +++ b/src/gui/systray.h @@ -19,6 +19,7 @@ #include "tray/usermodel.h" #include +#include #include #include @@ -31,6 +32,8 @@ class QGuiApplication; namespace OCC { +class ClientSideEncryptionTokenSelector; + class AccessManagerFactory : public QQmlNetworkAccessManagerFactory { public: @@ -115,6 +118,8 @@ class Systray : public QSystemTrayIcon void syncIsPausedChanged(); void isOpenChanged(); + void hideSettingsDialog(); + public slots: void setTrayEngine(QQmlApplicationEngine *trayEngine); void create(); @@ -127,6 +132,8 @@ public slots: void createEditFileLocallyLoadingDialog(const QString &fileName); void destroyEditFileLocallyLoadingDialog(); void createResolveConflictsDialog(const OCC::ActivityList &allConflicts); + void createEncryptionTokenDiscoveryDialog(); + void destroyEncryptionTokenDiscoveryDialog(); void slotCurrentUserChanged(); @@ -187,7 +194,9 @@ private slots: QSet _callsAlreadyNotified; QPointer _editFileLocallyLoadingDialog; + QPointer _encryptionTokenDiscoveryDialog; QVector _fileDetailDialogs; + QQuickWindow* _tokenInitDialog = nullptr; QStringListModel _fakeActivityModel; }; diff --git a/src/gui/tray/EncryptionTokenDiscoveryDialog.qml b/src/gui/tray/EncryptionTokenDiscoveryDialog.qml new file mode 100644 index 0000000000000..123cc750f7aa3 --- /dev/null +++ b/src/gui/tray/EncryptionTokenDiscoveryDialog.qml @@ -0,0 +1,89 @@ +import QtQuick 2.15 +import QtQuick.Window 2.15 +import Style 1.0 +import com.nextcloud.desktopclient 1.0 +import QtQuick.Layouts 1.15 +import QtQuick.Controls 2.15 + +ApplicationWindow { + id: root + flags: Qt.Dialog | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint + + color: "transparent" + + width: 320 + height: contentLayout.implicitHeight + modality: Qt.ApplicationModal + + readonly property real fontPixelSize: Style.topLinePixelSize * 1.5 + readonly property real iconWidth: fontPixelSize * 2 + + // TODO: Rather than setting all these palette colours manually, + // create a custom style and do it for all components globally + palette { + text: Style.ncTextColor + windowText: Style.ncTextColor + buttonText: Style.ncTextColor + brightText: Style.ncTextBrightColor + highlight: Style.lightHover + highlightedText: Style.ncTextColor + light: Style.lightHover + midlight: Style.ncSecondaryTextColor + mid: Style.darkerHover + dark: Style.menuBorder + button: Style.buttonBackgroundColor + window: Style.backgroundColor + base: Style.backgroundColor + toolTipBase: Style.backgroundColor + toolTipText: Style.ncTextColor + } + + Component.onCompleted: { + Systray.forceWindowInit(root); + x = Screen.width / 2 - width / 2 + y = Screen.height / 2 - height / 2 + root.show(); + root.raise(); + root.requestActivate(); + } + + Rectangle { + id: windowBackground + color: Style.backgroundColor + radius: Style.trayWindowRadius + border.color: palette.dark + anchors.fill: parent + } + + ColumnLayout { + id: contentLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: Style.standardSpacing + anchors.rightMargin: Style.standardSpacing + spacing: Style.standardSpacing + + NCBusyIndicator { + id: busyIndicator + Layout.topMargin: Style.standardSpacing + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: root.iconWidth + Layout.preferredHeight: root.iconWidth + imageSourceSizeHeight: root.iconWidth + imageSourceSizeWidth: root.iconWidth + padding: 0 + color: palette.windowText + running: true + } + EnforcedPlainTextLabel { + id: labelMessage + Layout.alignment: Qt.AlignHCenter + Layout.fillWidth: true + Layout.bottomMargin: Style.standardSpacing + text: qsTr("Discovering the certificates stored on your USB token") + elide: Text.ElideRight + font.pixelSize: root.fontPixelSize + horizontalAlignment: Text.AlignHCenter + } + } +} diff --git a/src/gui/tray/activitydata.h b/src/gui/tray/activitydata.h index e613ad5bf5dd3..3bcd4281eeb50 100644 --- a/src/gui/tray/activitydata.h +++ b/src/gui/tray/activitydata.h @@ -149,6 +149,7 @@ class Activity // Note that these are in the order we want to present them in the model! enum Type { DummyFetchingActivityType, + OpenSettingsNotificationType, NotificationType, SyncResultType, SyncFileItemType, diff --git a/src/gui/tray/activitylistmodel.cpp b/src/gui/tray/activitylistmodel.cpp index 47cc441f6c14f..c91b541b2a611 100644 --- a/src/gui/tray/activitylistmodel.cpp +++ b/src/gui/tray/activitylistmodel.cpp @@ -313,14 +313,14 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const case Activity::DummyMoreActivitiesAvailableType: return "Activity"; case Activity::NotificationType: + case Activity::OpenSettingsNotificationType: return "Notification"; case Activity::SyncFileItemType: return "File"; case Activity::SyncResultType: return "Sync"; - default: - return QVariant(); } + break; } case ActionTextRole: if(a._subjectDisplay.isEmpty()) { @@ -364,7 +364,8 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const case IsCurrentUserFileActivityRole: return a._isCurrentUserFileActivity; case ThumbnailRole: { - if (a._type == Activity::NotificationType && !a._talkNotificationData.userAvatar.isEmpty()) { + if ((a._type == Activity::NotificationType || a._type == Activity::OpenSettingsNotificationType) && + !a._talkNotificationData.userAvatar.isEmpty()) { return generateAvatarThumbnailMap(a._talkNotificationData.userAvatar); } @@ -389,7 +390,7 @@ QVariant ActivityListModel::data(const QModelIndex &index, int role) const return QVariant::fromValue(a); } - return QVariant(); + return {}; } int ActivityListModel::rowCount(const QModelIndex &parent) const @@ -664,9 +665,10 @@ void ActivityListModel::removeActivityFromActivityList(const Activity &activity) } if (activity._type != Activity::ActivityType && - activity._type != Activity::DummyFetchingActivityType && - activity._type != Activity::DummyMoreActivitiesAvailableType && - activity._type != Activity::NotificationType) { + activity._type != Activity::DummyFetchingActivityType && + activity._type != Activity::DummyMoreActivitiesAvailableType && + activity._type != Activity::NotificationType && + activity._type != Activity::OpenSettingsNotificationType) { const auto notificationErrorsListIndex = _notificationErrorsLists.indexOf(activity); if (notificationErrorsListIndex != -1) @@ -735,6 +737,8 @@ void ActivityListModel::slotTriggerDefaultAction(const int activityIndex) _currentInvalidFilenameDialog->open(); ownCloudGui::raiseDialog(_currentInvalidFilenameDialog); return; + } else if (activity._type == Activity::OpenSettingsNotificationType) { + Q_EMIT showSettingsDialog(); } if (!path.isEmpty()) { diff --git a/src/gui/tray/activitylistmodel.h b/src/gui/tray/activitylistmodel.h index 6251a12e9a1ff..c0dcb58ef3ebf 100644 --- a/src/gui/tray/activitylistmodel.h +++ b/src/gui/tray/activitylistmodel.h @@ -153,6 +153,8 @@ public slots: void interactiveActivityReceived(); + void showSettingsDialog(); + protected: [[nodiscard]] bool currentlyFetching() const; diff --git a/src/gui/tray/usermodel.cpp b/src/gui/tray/usermodel.cpp index 30b3d699f81ee..31a79a93ceb5d 100644 --- a/src/gui/tray/usermodel.cpp +++ b/src/gui/tray/usermodel.cpp @@ -93,8 +93,27 @@ User::User(AccountStatePtr &account, const bool &isCurrent, QObject *parent) connect(_account->account().data(), &Account::capabilitiesChanged, this, &User::slotAccountCapabilitiesChangedRefreshGroupFolders); connect(_activityModel, &ActivityListModel::sendNotificationRequest, this, &User::slotSendNotificationRequest); - + connect(_activityModel, &ActivityListModel::showSettingsDialog, + Systray::instance(), &Systray::openSettings); + connect(this, &User::sendReplyMessage, this, &User::slotSendReplyMessage); + + connect(_account->account().data(), &Account::userCertificateNeedsMigrationChanged, this, [this] () { + auto certificateNeedMigration = Activity{}; + certificateNeedMigration._type = Activity::OpenSettingsNotificationType; + certificateNeedMigration._subject = tr("End-to-end certificate needs to be migrated to a new one"); + certificateNeedMigration._dateTime = QDateTime::fromString(QDateTime::currentDateTime().toString(), Qt::ISODate); + certificateNeedMigration._message = tr("Trigger the migration"); + certificateNeedMigration._accName = _account->account()->displayName(); + certificateNeedMigration._id = qHash("migrate-certificate"); + + _activityModel->removeActivityFromActivityList(certificateNeedMigration); + + if (_account->account()->e2e()->userCertificateNeedsMigration()) { + _activityModel->addNotificationToActivityList(certificateNeedMigration); + showDesktopNotification(certificateNeedMigration); + } + }); } void User::checkNotifiedNotifications() diff --git a/src/libsync/CMakeLists.txt b/src/libsync/CMakeLists.txt index 58d820a3412b1..888fd3164f6c9 100644 --- a/src/libsync/CMakeLists.txt +++ b/src/libsync/CMakeLists.txt @@ -12,6 +12,12 @@ if ( APPLE ) ) endif() +if (WIN32) + list(APPEND OS_SPECIFIC_LINK_LIBRARIES + Crypt32 + ) +endif() + set(libsync_SRCS account.h account.cpp @@ -123,6 +129,8 @@ set(libsync_SRCS clientsideencryptionjobs.cpp clientsideencryptionprimitives.h clientsideencryptionprimitives.cpp + clientsideencryptiontokenselector.h + clientsideencryptiontokenselector.cpp datetimeprovider.h datetimeprovider.cpp rootencryptedfolderinfo.h @@ -205,6 +213,7 @@ target_link_libraries(nextcloudsync Nextcloud::csync OpenSSL::Crypto OpenSSL::SSL + PkgConfig::OPENSC-LIBP11 ${OS_SPECIFIC_LINK_LIBRARIES} Qt::Core Qt::Network diff --git a/src/libsync/account.cpp b/src/libsync/account.cpp index 0b20a3551bdc9..5b2e0de250541 100644 --- a/src/libsync/account.cpp +++ b/src/libsync/account.cpp @@ -33,6 +33,8 @@ #include "clientsideencryption.h" #include "ocsuserstatusconnector.h" +#include "config.h" + #include #include #include @@ -80,6 +82,9 @@ Account::Account(QObject *parent) _pushNotificationsReconnectTimer.setInterval(pushNotificationsReconnectInterval); connect(&_pushNotificationsReconnectTimer, &QTimer::timeout, this, &Account::trySetupPushNotifications); + + connect(&_e2e, &ClientSideEncryption::userCertificateNeedsMigrationChanged, + this, &Account::userCertificateNeedsMigrationChanged); } AccountPtr Account::create() @@ -1079,6 +1084,41 @@ bool Account::askUserForMnemonic() const return _e2eAskUserForMnemonic; } +bool Account::enforceUseHardwareTokenEncryption() const +{ +#if defined CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN + return CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN; +#else + return false; +#endif +} + +QString Account::encryptionHardwareTokenDriverPath() const +{ +#if defined ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH + return ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH; +#else + return {}; +#endif +} + +QByteArray Account::encryptionCertificateFingerprint() const +{ + return _encryptionCertificateFingerprint; +} + +void Account::setEncryptionCertificateFingerprint(const QByteArray &fingerprint) +{ + if (_encryptionCertificateFingerprint == fingerprint) { + return; + } + + _encryptionCertificateFingerprint = fingerprint; + _e2e.usbTokenInformation()->setSha256Fingerprint(fingerprint); + Q_EMIT encryptionCertificateFingerprintChanged(); + Q_EMIT wantsAccountSaved(this); +} + void Account::setAskUserForMnemonic(const bool ask) { _e2eAskUserForMnemonic = ask; diff --git a/src/libsync/account.h b/src/libsync/account.h index 28165f05320fd..eb63073c15f43 100644 --- a/src/libsync/account.h +++ b/src/libsync/account.h @@ -102,6 +102,9 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject Q_PROPERTY(AccountNetworkTransferLimitSetting downloadLimitSetting READ downloadLimitSetting WRITE setDownloadLimitSetting NOTIFY downloadLimitSettingChanged) Q_PROPERTY(unsigned int uploadLimit READ uploadLimit WRITE setUploadLimit NOTIFY uploadLimitChanged) Q_PROPERTY(unsigned int downloadLimit READ downloadLimit WRITE setDownloadLimit NOTIFY downloadLimitChanged) + Q_PROPERTY(bool enforceUseHardwareTokenEncryption READ enforceUseHardwareTokenEncryption NOTIFY enforceUseHardwareTokenEncryptionChanged) + Q_PROPERTY(QString encryptionHardwareTokenDriverPath READ encryptionHardwareTokenDriverPath NOTIFY encryptionHardwareTokenDriverPathChanged) + Q_PROPERTY(QByteArray encryptionCertificateFingerprint READ encryptionCertificateFingerprint WRITE setEncryptionCertificateFingerprint NOTIFY encryptionCertificateFingerprintChanged) public: // We need to decide whether to use the client's global proxy settings or whether to use @@ -412,6 +415,13 @@ class OWNCLOUDSYNC_EXPORT Account : public QObject [[nodiscard]] bool serverHasValidSubscription() const; void setServerHasValidSubscription(bool valid); + [[nodiscard]] bool enforceUseHardwareTokenEncryption() const; + + [[nodiscard]] QString encryptionHardwareTokenDriverPath() const; + + [[nodiscard]] QByteArray encryptionCertificateFingerprint() const; + void setEncryptionCertificateFingerprint(const QByteArray &fingerprint); + public slots: /// Used when forgetting credentials void clearQNAMCache(); @@ -434,12 +444,16 @@ public slots: // e.g. when the approved SSL certificates changed void wantsAccountSaved(OCC::Account *acc); + void wantsFoldersSynced(); + void serverVersionChanged(OCC::Account *account, const QString &newVersion, const QString &oldVersion); void accountChangedAvatar(); void accountChangedDisplayName(); void prettyNameChanged(); void askUserForMnemonicChanged(); + void enforceUseHardwareTokenEncryptionChanged(); + void encryptionHardwareTokenDriverPathChanged(); /// Used in RemoteWipe void appPasswordRetrieved(QString); @@ -469,6 +483,9 @@ public slots: void downloadLimitChanged(); void termsOfServiceNeedToBeChecked(); + void encryptionCertificateFingerprintChanged(); + void userCertificateNeedsMigrationChanged(); + protected Q_SLOTS: void slotCredentialsFetched(); void slotCredentialsAsked(); @@ -556,8 +573,8 @@ private slots: AccountNetworkTransferLimitSetting _downloadLimitSetting = AccountNetworkTransferLimitSetting::GlobalLimit; unsigned int _uploadLimit = 0; unsigned int _downloadLimit = 0; - bool _serverHasValidSubscription = false; + QByteArray _encryptionCertificateFingerprint; /* IMPORTANT - remove later - FIXME MS@2019-12-07 --> * TODO: For "Log out" & "Remove account": Remove client CA certs and KEY! diff --git a/src/libsync/clientsideencryption.cpp b/src/libsync/clientsideencryption.cpp index a5c0364c2b7ab..eec73e645fe7c 100644 --- a/src/libsync/clientsideencryption.cpp +++ b/src/libsync/clientsideencryption.cpp @@ -1,12 +1,19 @@ -#include "clientsideencryption.h" +/* + * Copyright © 2017, Tomaz Canabrava + * Copyright © 2020, Andreas Jellinghaus + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ -#include -#include -#include -#include -#include -#include -#include +#include "clientsideencryption.h" #include "account.h" #include "capabilities.h" @@ -31,6 +38,8 @@ #include #include #include +#include +#include #include #include #include @@ -38,11 +47,22 @@ #include #include #include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include #include #include #include - +#include #include QDebug operator<<(QDebug out, const std::string& str) @@ -57,7 +77,8 @@ namespace OCC { Q_LOGGING_CATEGORY(lcCse, "nextcloud.sync.clientsideencryption", QtInfoMsg) -Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.e2e", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCseDecryption, "nextcloud.sync.clientsideencryption.decryption", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCseEncryption, "nextcloud.sync.clientsideencryption.encryption", QtInfoMsg) QString e2eeBaseUrl(const OCC::AccountPtr &account) { @@ -80,6 +101,9 @@ constexpr char e2e_private[] = "_e2e-private"; constexpr char e2e_public[] = "_e2e-public"; constexpr char e2e_mnemonic[] = "_e2e-mnemonic"; +constexpr auto metadataKeyJsonKey = "metadataKey"; +constexpr auto certificateSha256FingerprintKey = "certificateSha256Fingerprint"; + constexpr qint64 blockSize = 1024; QList oldCipherFormatSplit(const QByteArray &cipher) @@ -265,14 +289,12 @@ QByteArray encryptPrivateKey( /* Create and initialise the context */ if(!ctx) { - qCInfo(lcCse()) << "Error creating cipher"; - handleErrors(); + qCInfo(lcCse()) << "Error creating cipher" << handleErrors(); } /* Initialise the decryption operation. */ if(!EVP_EncryptInit_ex(ctx, EVP_aes_256_gcm(), nullptr, nullptr, nullptr)) { - qCInfo(lcCse()) << "Error initializing context with aes_256"; - handleErrors(); + qCInfo(lcCse()) << "Error initializing context with aes_256" << handleErrors(); } // No padding @@ -280,14 +302,12 @@ QByteArray encryptPrivateKey( /* Set IV length. */ if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { - qCInfo(lcCse()) << "Error setting iv length"; - handleErrors(); + qCInfo(lcCse()) << "Error setting iv length" << handleErrors(); } /* Initialise key and IV */ if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) { - qCInfo(lcCse()) << "Error initialising key and iv"; - handleErrors(); + qCInfo(lcCse()) << "Error initialising key and iv" << handleErrors(); } // We write the base64 encoded private key @@ -299,8 +319,7 @@ QByteArray encryptPrivateKey( // Do the actual encryption int len = 0; if(!EVP_EncryptUpdate(ctx, unsignedData(ctext), &len, (unsigned char *)privateKeyB64.constData(), privateKeyB64.size())) { - qCInfo(lcCse()) << "Error encrypting"; - handleErrors(); + qCInfo(lcCse()) << "Error encrypting" << handleErrors(); } int clen = len; @@ -309,16 +328,14 @@ QByteArray encryptPrivateKey( * this stage, but this does not occur in GCM mode */ if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(ctext) + len, &len)) { - qCInfo(lcCse()) << "Error finalizing encryption"; - handleErrors(); + qCInfo(lcCse()) << "Error finalizing encryption" << handleErrors(); } clen += len; /* Get the e2EeTag */ QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0'); if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) { - qCInfo(lcCse()) << "Error getting the e2EeTag"; - handleErrors(); + qCInfo(lcCse()) << "Error getting the e2EeTag" << handleErrors(); } QByteArray cipherTXT; @@ -515,40 +532,86 @@ QByteArray privateKeyToPem(const QByteArray key) { return pem; } -QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data) +namespace internals { + +[[nodiscard]] std::optional encryptStringAsymmetric(ENGINE *sslEngine, + EVP_PKEY *publicKey, + int pad_mode, + const QByteArray& binaryData); + +[[nodiscard]] std::optional decryptStringAsymmetric(ENGINE *sslEngine, + EVP_PKEY *privateKey, + int pad_mode, + const QByteArray& binaryData); + +} + +std::optional encryptStringAsymmetric(const CertificateInformation &selectedCertificate, + const int paddingMode, + const ClientSideEncryption &encryptionEngine, + const QByteArray &binaryData) { - Q_ASSERT(!key.isNull()); - if (key.isNull()) { - qCDebug(lcCse) << "Public key is null. Could not encrypt."; + if (!encryptionEngine.isInitialized()) { + qCWarning(lcCseDecryption()) << "end-to-end encryption is disabled"; return {}; } - Bio publicKeyBio; - const auto publicKeyPem = key.toPem(); - BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); - const auto publicKey = PKey::readPublicKey(publicKeyBio); - return EncryptionHelper::encryptStringAsymmetric(publicKey, data); + + if (encryptionEngine.useTokenBasedEncryption()) { + qCDebug(lcCseEncryption()) << "use certificate on hardware token"; + } else { + qCDebug(lcCseEncryption()) << "use certificate on software storage"; + } + + const auto publicKey = selectedCertificate.getEvpPublicKey(); + Q_ASSERT(publicKey); + + auto encryptedBase64Result = internals::encryptStringAsymmetric(encryptionEngine.sslEngine(), publicKey, paddingMode, binaryData); + + if (!encryptedBase64Result) { + qCWarning(lcCseEncryption()) << "encrypt failed"; + return {}; + } + + if (encryptedBase64Result->isEmpty()) { + qCDebug(lcCseEncryption()) << "ERROR. Could not encrypt data"; + return {}; + } + + return encryptedBase64Result; } -QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data) +std::optional decryptStringAsymmetric(const CertificateInformation &selectedCertificate, + const int paddingMode, + const ClientSideEncryption &encryptionEngine, + const QByteArray &base64Data) { - Q_ASSERT(!privateKeyPem.isEmpty()); - if (privateKeyPem.isEmpty()) { - qCDebug(lcCse) << "Private key is empty. Could not encrypt."; + if (!encryptionEngine.isInitialized()) { + qCWarning(lcCseDecryption()) << "end-to-end encryption is disabled"; return {}; } - Bio privateKeyBio; - BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); - const auto key = PKey::readPrivateKey(privateKeyBio); + if (encryptionEngine.useTokenBasedEncryption()) { + qCDebug(lcCseDecryption()) << "use certificate on hardware token"; + } else { + qCDebug(lcCseDecryption()) << "use certificate on software storage"; + } + const auto key = selectedCertificate.getEvpPrivateKey(); + if (!key) { + qCWarning(lcCseDecryption()) << "invalid private key handle"; + return {}; + } - // Also base64 decode the result - const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(key, data); + const auto decryptBase64Result = internals::decryptStringAsymmetric(encryptionEngine.sslEngine(), key, paddingMode, QByteArray::fromBase64(base64Data)); + if (!decryptBase64Result) { + qCWarning(lcCseDecryption()) << "decrypt failed"; + return {}; + } - if (decryptResult.isEmpty()) { - qCDebug(lcCse()) << "ERROR. Could not decrypt data"; + if (decryptBase64Result->isEmpty()) { + qCDebug(lcCseDecryption()) << "ERROR. Could not decrypt data"; return {}; } - return decryptResult; + return decryptBase64Result; } QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) { @@ -558,15 +621,13 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) /* Create and initialise the context */ if(!ctx) { - qCInfo(lcCse()) << "Error creating cipher"; - handleErrors(); + qCInfo(lcCse()) << "Error creating cipher" << handleErrors(); return {}; } /* Initialise the decryption operation. */ if(!EVP_EncryptInit_ex(ctx, EVP_aes_128_gcm(), nullptr, nullptr, nullptr)) { - qCInfo(lcCse()) << "Error initializing context with aes_128"; - handleErrors(); + qCInfo(lcCse()) << "Error initializing context with aes_128" << handleErrors(); return {}; } @@ -575,15 +636,13 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) /* Set IV length. */ if(!EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_IVLEN, iv.size(), nullptr)) { - qCInfo(lcCse()) << "Error setting iv length"; - handleErrors(); + qCInfo(lcCse()) << "Error setting iv length" << handleErrors(); return {}; } /* Initialise key and IV */ if(!EVP_EncryptInit_ex(ctx, nullptr, nullptr, (unsigned char *)key.constData(), (unsigned char *)iv.constData())) { - qCInfo(lcCse()) << "Error initialising key and iv"; - handleErrors(); + qCInfo(lcCse()) << "Error initialising key and iv" << handleErrors(); return {}; } @@ -596,8 +655,7 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) // Do the actual encryption int len = 0; if(!EVP_EncryptUpdate(ctx, unsignedData(ctext), &len, (unsigned char *)dataB64.constData(), dataB64.size())) { - qCInfo(lcCse()) << "Error encrypting"; - handleErrors(); + qCInfo(lcCse()) << "Error encrypting" << handleErrors(); return {}; } @@ -607,8 +665,7 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) * this stage, but this does not occur in GCM mode */ if(1 != EVP_EncryptFinal_ex(ctx, unsignedData(ctext) + len, &len)) { - qCInfo(lcCse()) << "Error finalizing encryption"; - handleErrors(); + qCInfo(lcCse()) << "Error finalizing encryption" << handleErrors(); return {}; } clen += len; @@ -616,8 +673,7 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) /* Get the e2EeTag */ QByteArray e2EeTag(OCC::Constants::e2EeTagSize, '\0'); if(1 != EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, OCC::Constants::e2EeTagSize, unsignedData(e2EeTag))) { - qCInfo(lcCse()) << "Error getting the e2EeTag"; - handleErrors(); + qCInfo(lcCse()) << "Error getting the e2EeTag" << handleErrors(); return {}; } @@ -633,122 +689,269 @@ QByteArray encryptStringSymmetric(const QByteArray& key, const QByteArray& data) return result; } -QByteArray decryptStringAsymmetric(EVP_PKEY *privateKey, const QByteArray& data) { +namespace internals { + +std::optional decryptStringAsymmetric(ENGINE *sslEngine, + EVP_PKEY *privateKey, + int pad_mode, + const QByteArray& binaryData) { int err = -1; - qCInfo(lcCseDecryption()) << "Start to work the decryption."; - auto ctx = PKeyCtx::forKey(privateKey, ENGINE_get_default_RSA()); + auto ctx = PKeyCtx::forKey(privateKey, sslEngine); if (!ctx) { - qCInfo(lcCseDecryption()) << "Could not create the PKEY context."; - handleErrors(); + qCInfo(lcCseDecryption()) << "Could not create the PKEY context." << handleErrors(); return {}; } err = EVP_PKEY_decrypt_init(ctx); if (err <= 0) { - qCInfo(lcCseDecryption()) << "Could not init the decryption of the metadata"; - handleErrors(); + qCInfo(lcCseDecryption()) << "Could not init the decryption of the metadata" << handleErrors(); return {}; } - if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { - qCInfo(lcCseDecryption()) << "Error setting the encryption padding."; - handleErrors(); + if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad_mode) <= 0) { + qCInfo(lcCseDecryption()) << "Error setting the encryption padding." << handleErrors(); return {}; } - if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { - qCInfo(lcCseDecryption()) << "Error setting OAEP SHA 256"; - handleErrors(); + if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha1()) <= 0) { + qCInfo(lcCseDecryption()) << "Error setting OAEP SHA 256" << handleErrors(); return {}; } - if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) { - qCInfo(lcCseDecryption()) << "Error setting MGF1 padding"; - handleErrors(); + if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha1()) <= 0) { + qCInfo(lcCseDecryption()) << "Error setting MGF1 padding" << handleErrors(); return {}; } size_t outlen = 0; - err = EVP_PKEY_decrypt(ctx, nullptr, &outlen, (unsigned char *)data.constData(), data.size()); + err = EVP_PKEY_decrypt(ctx, nullptr, &outlen, (unsigned char *)binaryData.constData(), binaryData.size()); if (err <= 0) { - qCInfo(lcCseDecryption()) << "Could not determine the buffer length"; - handleErrors(); + qCInfo(lcCseDecryption()) << "Could not determine the buffer length" << handleErrors(); return {}; - } else { - qCInfo(lcCseDecryption()) << "Size of output is: " << outlen; - qCInfo(lcCseDecryption()) << "Size of data is: " << data.size(); } QByteArray out(static_cast(outlen), '\0'); - if (EVP_PKEY_decrypt(ctx, unsignedData(out), &outlen, (unsigned char *)data.constData(), data.size()) <= 0) { + if (EVP_PKEY_decrypt(ctx, unsignedData(out), &outlen, (unsigned char *)binaryData.constData(), binaryData.size()) <= 0) { const auto error = handleErrors(); qCCritical(lcCseDecryption()) << "Could not decrypt the data." << error; return {}; - } else { - qCInfo(lcCseDecryption()) << "data decrypted successfully"; } // we don't need extra zeroes in out, so let's only return meaningful data out = QByteArray(out.constData(), outlen); - - qCInfo(lcCse()) << out; - return out; + return out.toBase64(); } -QByteArray encryptStringAsymmetric(EVP_PKEY *publicKey, const QByteArray& data) { - int err = -1; - - auto ctx = PKeyCtx::forKey(publicKey, ENGINE_get_default_RSA()); +std::optional encryptStringAsymmetric(ENGINE *sslEngine, + EVP_PKEY *publicKey, + int pad_mode, + const QByteArray& binaryData) { + auto ctx = PKeyCtx::forKey(publicKey, sslEngine); if (!ctx) { - qCInfo(lcCse()) << "Could not initialize the pkey context."; - exit(1); + qCInfo(lcCseEncryption()) << "Could not initialize the pkey context." << publicKey << sslEngine; + return {}; } if (EVP_PKEY_encrypt_init(ctx) != 1) { - qCInfo(lcCse()) << "Error initilaizing the encryption."; - exit(1); + qCInfo(lcCseEncryption()) << "Error initilaizing the encryption." << handleErrors(); + return {}; } - if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) { - qCInfo(lcCse()) << "Error setting the encryption padding."; - exit(1); + if (EVP_PKEY_CTX_set_rsa_padding(ctx, pad_mode) <= 0) { + qCInfo(lcCseEncryption()) << "Error setting the encryption padding." << handleErrors(); + return {}; } - if (EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha256()) <= 0) { - qCInfo(lcCse()) << "Error setting OAEP SHA 256"; - exit(1); + if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_oaep_md(ctx, EVP_sha1()) <= 0) { + qCInfo(lcCseEncryption()) << "Error setting OAEP SHA 256" << handleErrors(); + return {}; } - if (EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha256()) <= 0) { - qCInfo(lcCse()) << "Error setting MGF1 padding"; - exit(1); + if (pad_mode != RSA_PKCS1_PADDING && EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, EVP_sha1()) <= 0) { + qCInfo(lcCseEncryption()) << "Error setting MGF1 padding" << handleErrors(); + return {}; } size_t outLen = 0; - if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, (unsigned char *)data.constData(), data.size()) != 1) { - qCInfo(lcCse()) << "Error retrieving the size of the encrypted data"; - exit(1); - } else { - qCInfo(lcCse()) << "Encryption Length:" << outLen; + if (EVP_PKEY_encrypt(ctx, nullptr, &outLen, (unsigned char *)binaryData.constData(), binaryData.size()) != 1) { + qCInfo(lcCseEncryption()) << "Error retrieving the size of the encrypted data" << handleErrors(); + return {}; } QByteArray out(static_cast(outLen), '\0'); - if (EVP_PKEY_encrypt(ctx, unsignedData(out), &outLen, (unsigned char *)data.constData(), data.size()) != 1) { - qCInfo(lcCse()) << "Could not encrypt key." << err; - exit(1); + if (EVP_PKEY_encrypt(ctx, unsignedData(out), &outLen, (unsigned char *)binaryData.constData(), binaryData.size()) != 1) { + qCInfo(lcCseEncryption()) << "Could not encrypt key." << handleErrors(); + return {}; } - qCInfo(lcCse()) << out.toBase64(); - return out; + // Transform the encrypted data into base64. + return out.toBase64(); +} + +} + +void debugOpenssl() +{ + if (ERR_peek_error() == 0) { + return; + } + + const char *file; + char errorMessage[255]; + int line; + while (const auto errorNumber = ERR_get_error_line(&file, &line)) { + ERR_error_string(errorNumber, errorMessage); + qCWarning(lcCse()) << errorMessage << file << line; + } +} + +} + + +ClientSideEncryption::ClientSideEncryption() +{ } +bool ClientSideEncryption::isInitialized() const +{ + return useTokenBasedEncryption() || !getMnemonic().isEmpty(); +} + +QSslKey ClientSideEncryption::getPublicKey() const +{ + return _encryptionCertificate.getSslPublicKey(); +} + +const QByteArray &ClientSideEncryption::getPrivateKey() const +{ + return _encryptionCertificate.getPrivateKeyData(); } -ClientSideEncryption::ClientSideEncryption() = default; +void ClientSideEncryption::setPrivateKey(const QByteArray &privateKey) +{ + _encryptionCertificate.setPrivateKeyData(privateKey); +} -void ClientSideEncryption::initialize(const AccountPtr &account) +const CertificateInformation &ClientSideEncryption::getCertificateInformation() const +{ + return _encryptionCertificate; +} + +CertificateInformation ClientSideEncryption::getCertificateInformationByFingerprint(const QByteArray &certificateFingerprint) const +{ + CertificateInformation result; + + if (_encryptionCertificate.sha256Fingerprint() == certificateFingerprint) { + result = _encryptionCertificate; + } else { + for(const auto &oneCertificate : _otherCertificates) { + if (oneCertificate.sha256Fingerprint() == certificateFingerprint) { + result = oneCertificate; + break; + } + } + } + + return result; +} + +int ClientSideEncryption::paddingMode() const +{ + return RSA_PKCS1_PADDING; +} + +CertificateInformation ClientSideEncryption::getTokenCertificateByFingerprint(const QByteArray &expectedFingerprint) const +{ + CertificateInformation result; + + if (_encryptionCertificate.sha256Fingerprint() == expectedFingerprint) { + result = _encryptionCertificate; + return result; + } + + const auto itCertificate = std::find_if(_otherCertificates.begin(), _otherCertificates.end(), [expectedFingerprint] (const auto &oneCertificate) { + return oneCertificate.sha256Fingerprint() == expectedFingerprint; + }); + if (itCertificate != _otherCertificates.end()) { + result = *itCertificate; + return result; + } + + return result; +} + +bool ClientSideEncryption::useTokenBasedEncryption() const +{ + return _encryptionCertificate.getPkcs11PrivateKey(); +} + +const QString &ClientSideEncryption::getMnemonic() const +{ + return _mnemonic; +} + +void ClientSideEncryption::setCertificate(const QSslCertificate &certificate) +{ + _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{certificate}}; +} + +const QSslCertificate& ClientSideEncryption::getCertificate() const +{ + return _encryptionCertificate.getCertificate(); +} + +ENGINE* ClientSideEncryption::sslEngine() const +{ + return ENGINE_get_default_RSA(); +} + +ClientSideEncryptionTokenSelector *ClientSideEncryption::usbTokenInformation() +{ + return &_usbTokenInformation; +} + +bool ClientSideEncryption::canEncrypt() const +{ + if (!isInitialized()) { + return false; + } + if (useTokenBasedEncryption()) { + return _encryptionCertificate.canEncrypt(); + } + + return true; +} + +bool ClientSideEncryption::canDecrypt() const +{ + return isInitialized(); +} + +bool ClientSideEncryption::userCertificateNeedsMigration() const +{ + if (!isInitialized()) { + return false; + } + if (useTokenBasedEncryption()) { + return _encryptionCertificate.userCertificateNeedsMigration(); + } + + return false; +} + +QByteArray ClientSideEncryption::certificateSha256Fingerprint() const +{ + if (useTokenBasedEncryption()) { + return _encryptionCertificate.sha256Fingerprint(); + } + + return {}; +} + +void ClientSideEncryption::initialize(QWidget *settingsDialog, + const AccountPtr &account) { Q_ASSERT(account); @@ -759,7 +962,227 @@ void ClientSideEncryption::initialize(const AccountPtr &account) return; } - fetchCertificateFromKeyChain(account); + if (account->enforceUseHardwareTokenEncryption()) { + if (_usbTokenInformation.isSetup()) { + initializeHardwareTokenEncryption(settingsDialog, account); + } else if (account->e2eEncryptionKeysGenerationAllowed() && account->askUserForMnemonic()) { + Q_EMIT startingDiscoveryEncryptionUsbToken(); + auto futureTokenDiscoveryResult = new QFutureWatcher(this); + auto tokenDiscoveryResult = _usbTokenInformation.searchForCertificates(account); + futureTokenDiscoveryResult->setFuture(tokenDiscoveryResult); + connect(futureTokenDiscoveryResult, &QFutureWatcher::finished, + this, [this, settingsDialog, account, futureTokenDiscoveryResult] () { + completeHardwareTokenInitialization(settingsDialog, account); + futureTokenDiscoveryResult->deleteLater(); + Q_EMIT finishedDiscoveryEncryptionUsbToken(); + }); + } else { + emit initializationFinished(); + } + } else { + fetchCertificateFromKeyChain(account); + } +} + +void ClientSideEncryption::initializeHardwareTokenEncryption(QWidget *settingsDialog, + const AccountPtr &account) +{ + auto ctx = Pkcs11Context{Pkcs11Context::State::CreateContext}; + _tokenSlots.reset(); + _encryptionCertificate.clear(); + _otherCertificates.clear(); + _context.clear(); + + if (PKCS11_CTX_load(ctx, account->encryptionHardwareTokenDriverPath().toLatin1().constData())) { + qCWarning(lcCse()) << "loading pkcs11 engine failed:" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + auto tokensCount = 0u; + PKCS11_SLOT *tempTokenSlots = nullptr; + /* get information on all slots */ + if (PKCS11_enumerate_slots(ctx, &tempTokenSlots, &tokensCount) < 0) { + qCWarning(lcCse()) << "no slots available" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + auto deleter = [ctx = static_cast(ctx), tokensCount] (PKCS11_SLOT* pointer) noexcept -> void { + qCWarning(lcCse()) << "destructor" << pointer << ctx; + if (pointer) { + qCWarning(lcCse()) << "destructor" << pointer << ctx; + PKCS11_release_all_slots(ctx, pointer, tokensCount); + } + }; + + auto tokenSlots = decltype(_tokenSlots){tempTokenSlots, deleter}; + + auto currentSlot = static_cast(nullptr); + for(auto i = 0u; i < tokensCount; ++i) { + currentSlot = PKCS11_find_next_token(ctx, tokenSlots.get(), tokensCount, currentSlot); + if (currentSlot == nullptr || currentSlot->token == nullptr) { + break; + } + + qCDebug(lcCse()) << "Slot manufacturer......:" << currentSlot->manufacturer; + qCDebug(lcCse()) << "Slot description.......:" << currentSlot->description; + qCDebug(lcCse()) << "Slot token label.......:" << currentSlot->token->label; + qCDebug(lcCse()) << "Slot token manufacturer:" << currentSlot->token->manufacturer; + qCDebug(lcCse()) << "Slot token model.......:" << currentSlot->token->model; + qCDebug(lcCse()) << "Slot token serialnr....:" << currentSlot->token->serialnr; + + if (PKCS11_open_session(currentSlot, 0) != 0) { + qCWarning(lcCse()) << "PKCS11_open_session failed" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + auto logged_in = 0; + if (PKCS11_is_logged_in(currentSlot, 0, &logged_in) != 0) { + qCWarning(lcCse()) << "PKCS11_is_logged_in failed" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + while (true) { + auto pinHasToBeCached = false; + auto newPin = _cachedPin; + + if (newPin.isEmpty()) { + /* perform pkcs #11 login */ + bool ok; + newPin = QInputDialog::getText(settingsDialog, + tr("PIN needed to login to token"), + tr("Enter Certificate USB Token PIN:"), + QLineEdit::Password, + {}, + &ok); + if (!ok || newPin.isEmpty()) { + qCWarning(lcCse()) << "an USER pin is required"; + + Q_EMIT initializationFinished(); + return; + } + + pinHasToBeCached = true; + } + + const auto newPinData = newPin.toLatin1(); + if (PKCS11_login(currentSlot, 0, newPinData.data()) != 0) { + QMessageBox::warning(settingsDialog, + tr("Invalid PIN. Login failed"), + tr("Login to the token failed after providing the user PIN. It may be invalid or wrong. Please try again !"), + QMessageBox::Ok); + _cachedPin.clear(); + continue; + } + + /* check if user is logged in */ + if (PKCS11_is_logged_in(currentSlot, 0, &logged_in) != 0) { + qCWarning(lcCse()) << "PKCS11_is_logged_in failed" << ERR_reason_error_string(ERR_get_error()); + + _cachedPin.clear(); + failedToInitialize(account); + return; + } + if (!logged_in) { + qCWarning(lcCse()) << "PKCS11_is_logged_in says user is not logged in, expected to be logged in"; + + _cachedPin.clear(); + failedToInitialize(account); + return; + } + + if (pinHasToBeCached) { + cacheTokenPin(newPin); + } + + break; + } + + auto keysCount = 0u; + auto certificatesFromToken = static_cast(nullptr); + if (PKCS11_enumerate_certs(currentSlot->token, &certificatesFromToken, &keysCount)) { + qCWarning(lcCse()) << "PKCS11_enumerate_certs failed" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + for (auto certificateIndex = 0u; certificateIndex < keysCount; ++certificateIndex) { + const auto currentCertificate = &certificatesFromToken[certificateIndex]; + + Bio out; + const auto ret = PEM_write_bio_X509(out, currentCertificate->x509); + if (ret <= 0){ + qCWarning(lcCse()) << "PEM_write_bio_X509 failed" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + const auto result = BIO2ByteArray(out); + auto sslCertificate = QSslCertificate{result, QSsl::Pem}; + + if (sslCertificate.isSelfSigned()) { + qCDebug(lcCse()) << "newly found certificate is self signed: goint to ignore it"; + continue; + } + + const auto certificateKey = PKCS11_find_key(currentCertificate); + if (!certificateKey) { + qCWarning(lcCse()) << "PKCS11_find_key failed" << ERR_reason_error_string(ERR_get_error()); + + failedToInitialize(account); + return; + } + + qCDebug(lcCse) << "checking the type of the key associated to the certificate"; + qCDebug(lcCse) << "key type" << Qt::hex << PKCS11_get_key_type(certificateKey); + + _otherCertificates.emplace_back(certificateKey, std::move(sslCertificate)); + } + } + + for (const auto &oneCertificateInformation : _otherCertificates) { + if (oneCertificateInformation.isSelfSigned()) { + qCDebug(lcCse()) << "newly found certificate is self signed: goint to ignore it"; + continue; + } + + if (!_usbTokenInformation.sha256Fingerprint().isEmpty() && oneCertificateInformation.sha256Fingerprint() != _usbTokenInformation.sha256Fingerprint()) { + qCDebug(lcCse()) << "skipping certificate from" << "with fingerprint" << oneCertificateInformation.sha256Fingerprint() << "different from" << _usbTokenInformation.sha256Fingerprint(); + continue; + } + + const auto &sslErrors = oneCertificateInformation.verify(); + for (const auto &sslError : sslErrors) { + qCInfo(lcCse()) << "certificate validation error" << sslError; + } + + setEncryptionCertificate(oneCertificateInformation); + + if (canEncrypt() && !checkEncryptionIsWorking()) { + qCWarning(lcCse()) << "encryption is not properly setup"; + + failedToInitialize(account); + return; + } + + sendPublicKey(account); + + _tokenSlots = std::move(tokenSlots); + _context = std::move(ctx); + + return; + } + + failedToInitialize(account); } void ClientSideEncryption::fetchCertificateFromKeyChain(const AccountPtr &account) @@ -811,18 +1234,24 @@ bool ClientSideEncryption::checkPublicKeyValidity(const AccountPtr &account) con QByteArray data = EncryptionHelper::generateRandom(64); Bio publicKeyBio; - QByteArray publicKeyPem = account->e2e()->_publicKey.toPem(); + QByteArray publicKeyPem = account->e2e()->getPublicKey().toPem(); BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); auto publicKey = PKey::readPublicKey(publicKeyBio); - auto encryptedData = EncryptionHelper::encryptStringAsymmetric(publicKey, data.toBase64()); + auto encryptedData = EncryptionHelper::encryptStringAsymmetric(account->e2e()->getCertificateInformation(), account->e2e()->paddingMode(), *account->e2e(), data.toBase64()); + if (!encryptedData) { + qCWarning(lcCse()) << "encryption error"; + return false; + } - Bio privateKeyBio; - QByteArray privateKeyPem = account->e2e()->_privateKey; - BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); - auto key = PKey::readPrivateKey(privateKeyBio); + auto key = _encryptionCertificate.getEvpPrivateKey(); - QByteArray decryptResult = QByteArray::fromBase64(EncryptionHelper::decryptStringAsymmetric(key, encryptedData)); + const auto decryptionResult = EncryptionHelper::decryptStringAsymmetric(account->e2e()->getCertificateInformation(), account->e2e()->paddingMode(), *account->e2e(), *encryptedData); + if (!decryptionResult) { + qCWarning(lcCse()) << "encryption error"; + return false; + } + const auto decryptResult = QByteArray::fromBase64(*decryptionResult); if (data != decryptResult) { qCInfo(lcCse()) << "invalid private key"; @@ -832,6 +1261,39 @@ bool ClientSideEncryption::checkPublicKeyValidity(const AccountPtr &account) con return true; } +bool ClientSideEncryption::checkEncryptionIsWorking() const +{ + qCInfo(lcCse) << "check encryption is working before enabling end-to-end encryption feature"; + QByteArray data = EncryptionHelper::generateRandom(64); + + auto encryptedData = EncryptionHelper::encryptStringAsymmetric(getCertificateInformation(), paddingMode(), *this, data); + if (!encryptedData) { + qCWarning(lcCse()) << "encryption error"; + return false; + } + + qCDebug(lcCse) << "encryption is working with" << getCertificateInformation().sha256Fingerprint(); + + const auto decryptionResult = EncryptionHelper::decryptStringAsymmetric(getCertificateInformation(), paddingMode(), *this, *encryptedData); + if (!decryptionResult) { + qCWarning(lcCse()) << "encryption error"; + return false; + } + + qCDebug(lcCse) << "decryption is working with" << getCertificateInformation().sha256Fingerprint(); + + QByteArray decryptResult = QByteArray::fromBase64(*decryptionResult); + + if (data != decryptResult) { + qCInfo(lcCse()) << "recovered data does not match the initial data after encryption and decryption of it"; + return false; + } + + qCInfo(lcCse) << "end-to-end encryption is working with" << getCertificateInformation().sha256Fingerprint(); + + return true; +} + bool ClientSideEncryption::checkServerPublicKeyValidity(const QByteArray &serverPublicKeyString) const { Bio serverPublicKeyBio; @@ -839,7 +1301,7 @@ bool ClientSideEncryption::checkServerPublicKeyValidity(const QByteArray &server const auto serverPublicKey = PKey::readPrivateKey(serverPublicKeyBio); Bio certificateBio; - const auto certificatePem = _certificate.toPem(); + const auto certificatePem = _encryptionCertificate.getCertificate().toPem(); BIO_write(certificateBio, certificatePem.constData(), certificatePem.size()); const auto x509Certificate = X509Certificate::readCertificate(certificateBio); if (!x509Certificate) { @@ -868,15 +1330,13 @@ void ClientSideEncryption::publicCertificateFetched(Job *incoming) return; } - _certificate = QSslCertificate(readJob->binaryData(), QSsl::Pem); + _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{readJob->binaryData(), QSsl::Pem}}; - if (_certificate.isNull()) { + if (_encryptionCertificate.getCertificate().isNull()) { fetchPublicKeyFromKeyChain(account); return; } - _publicKey = _certificate.publicKey(); - qCInfo(lcCse()) << "Public key fetched from keychain"; const QString kck = AbstractCredentials::keychainKey( @@ -896,7 +1356,7 @@ void ClientSideEncryption::publicCertificateFetched(Job *incoming) QByteArray ClientSideEncryption::generateSignatureCryptographicMessageSyntax(const QByteArray &data) const { Bio certificateBio; - const auto certificatePem = _certificate.toPem(); + const auto certificatePem = _encryptionCertificate.getCertificate().toPem(); BIO_write(certificateBio, certificatePem.constData(), certificatePem.size()); const auto x509Certificate = X509Certificate::readCertificate(certificateBio); if (!x509Certificate) { @@ -904,15 +1364,14 @@ QByteArray ClientSideEncryption::generateSignatureCryptographicMessageSyntax(con return {}; } - Bio privateKeyBio; - BIO_write(privateKeyBio, _privateKey.constData(), _privateKey.size()); - const auto privateKey = PKey::readPrivateKey(privateKeyBio); + const auto privateKey = _encryptionCertificate.getEvpPrivateKey(); Bio dataBio; BIO_write(dataBio, data.constData(), data.size()); const auto contentInfo = CMS_sign(x509Certificate, privateKey, nullptr, dataBio, CMS_DETACHED); + Q_ASSERT(contentInfo); if (!contentInfo) { return {}; } @@ -992,7 +1451,7 @@ void ClientSideEncryption::publicKeyFetched(QKeychain::Job *incoming) return; } - _publicKey = publicKey; + Q_UNUSED(publicKey) const QString kck = AbstractCredentials::keychainKey( account->url().toString(), @@ -1033,10 +1492,9 @@ void ClientSideEncryption::privateKeyFetched(Job *incoming) return; } - //_privateKey = QSslKey(readJob->binaryData(), QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); - _privateKey = readJob->binaryData(); + _encryptionCertificate.setPrivateKeyData(readJob->binaryData()); - if (_privateKey.isNull()) { + if (getPrivateKey().isNull()) { getPrivateKeyFromServer(account); return; } @@ -1070,9 +1528,9 @@ void ClientSideEncryption::mnemonicKeyFetched(QKeychain::Job *incoming) return; } - _mnemonic = readJob->textData(); + setMnemonic(readJob->textData()); - qCInfo(lcCse()) << "Mnemonic key fetched from keychain: " << _mnemonic; + qCInfo(lcCse()) << "Mnemonic key fetched from keychain"; checkServerHasSavedKeys(account); } @@ -1088,7 +1546,7 @@ void ClientSideEncryption::writePrivateKey(const AccountPtr &account) auto *job = new WritePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); job->setKey(kck); - job->setBinaryData(_privateKey); + job->setBinaryData(getPrivateKey()); connect(job, &WritePasswordJob::finished, [](Job *incoming) { Q_UNUSED(incoming); qCInfo(lcCse()) << "Private key stored in keychain"; @@ -1107,7 +1565,7 @@ void ClientSideEncryption::writeCertificate(const AccountPtr &account) auto *job = new WritePasswordJob(Theme::instance()->appName()); job->setInsecureFallback(false); job->setKey(kck); - job->setBinaryData(_certificate.toPem()); + job->setBinaryData(_encryptionCertificate.getCertificate().toPem()); connect(job, &WritePasswordJob::finished, [](Job *incoming) { Q_UNUSED(incoming); qCInfo(lcCse()) << "Certificate stored in keychain"; @@ -1131,10 +1589,50 @@ void ClientSideEncryption::writeCertificate(const AccountPtr &account, const QSt job->start(); } +void ClientSideEncryption::completeHardwareTokenInitialization(QWidget *settingsDialog, + const OCC::AccountPtr &account) +{ + if (_usbTokenInformation.isSetup()) { + initializeHardwareTokenEncryption(settingsDialog, account); + } else { + emit initializationFinished(); + } +} + +void ClientSideEncryption::setMnemonic(const QString &mnemonic) +{ + if (_mnemonic == mnemonic) { + return; + } + + _mnemonic = mnemonic; + + Q_EMIT canEncryptChanged(); + Q_EMIT canDecryptChanged(); +} + +void ClientSideEncryption::setEncryptionCertificate(CertificateInformation certificateInfo) +{ + if (_encryptionCertificate == certificateInfo) { + return; + } + + const auto oldValueForUserCertificateNeedsMigration = _encryptionCertificate.userCertificateNeedsMigration(); + + _encryptionCertificate = std::move(certificateInfo); + + Q_EMIT canEncryptChanged(); + Q_EMIT canDecryptChanged(); + + if (oldValueForUserCertificateNeedsMigration != _encryptionCertificate.userCertificateNeedsMigration()) { + Q_EMIT userCertificateNeedsMigrationChanged(); + } +} + void ClientSideEncryption::generateMnemonic() { const auto list = WordList::getRandomWords(12); - _mnemonic = list.join(' '); + setMnemonic(list.join(' ')); } template @@ -1176,6 +1674,10 @@ void ClientSideEncryption::forgetSensitiveData(const AccountPtr &account) return job; }; + if (!account->credentials()) { + return; + } + const auto user = account->credentials()->user(); const auto deletePrivateKeyJob = createDeleteJob(user + e2e_private); const auto deleteCertJob = createDeleteJob(user + e2e_cert); @@ -1187,6 +1689,15 @@ void ClientSideEncryption::forgetSensitiveData(const AccountPtr &account) deletePrivateKeyJob->start(); deleteCertJob->start(); deleteMnemonicJob->start(); + _usbTokenInformation.setSha256Fingerprint({}); + account->setEncryptionCertificateFingerprint({}); + _tokenSlots.reset(); + _encryptionCertificate.clear(); + _otherCertificates.clear(); + _context.clear(); + Q_EMIT canDecryptChanged(); + Q_EMIT canEncryptChanged(); + Q_EMIT userCertificateNeedsMigrationChanged(); } void ClientSideEncryption::getUsersPublicKeyFromServer(const AccountPtr &account, const QStringList &userIds) @@ -1218,6 +1729,11 @@ void ClientSideEncryption::getUsersPublicKeyFromServer(const AccountPtr &account job->start(); } +void ClientSideEncryption::migrateCertificate() +{ + _usbTokenInformation.clear(); +} + void ClientSideEncryption::handlePrivateKeyDeleted(const QKeychain::Job* const incoming) { const auto error = incoming->error(); @@ -1227,7 +1743,8 @@ void ClientSideEncryption::handlePrivateKeyDeleted(const QKeychain::Job* const i } qCDebug(lcCse) << "Private key successfully deleted from keychain. Clearing."; - _privateKey = QByteArray(); + _encryptionCertificate.clear(); + Q_EMIT privateKeyDeleted(); checkAllSensitiveDataDeleted(); } @@ -1241,7 +1758,7 @@ void ClientSideEncryption::handleCertificateDeleted(const QKeychain::Job* const } qCDebug(lcCse) << "Certificate successfully deleted from keychain. Clearing."; - _certificate = QSslCertificate(); + _encryptionCertificate.clear(); Q_EMIT certificateDeleted(); checkAllSensitiveDataDeleted(); } @@ -1255,7 +1772,7 @@ void ClientSideEncryption::handleMnemonicDeleted(const QKeychain::Job* const inc } qCDebug(lcCse) << "Mnemonic successfully deleted from keychain. Clearing."; - _mnemonic = QString(); + setMnemonic({}); Q_EMIT mnemonicDeleted(); checkAllSensitiveDataDeleted(); } @@ -1268,14 +1785,13 @@ void ClientSideEncryption::handlePublicKeyDeleted(const QKeychain::Job * const i return; } - _publicKey.clear(); Q_EMIT publicKeyDeleted(); checkAllSensitiveDataDeleted(); } bool ClientSideEncryption::sensitiveDataRemaining() const { - return !_privateKey.isEmpty() || !_certificate.isNull() || !_mnemonic.isEmpty(); + return !getPrivateKey().isEmpty() || !_encryptionCertificate.getCertificate().isNull() || !_mnemonic.isEmpty() || !_usbTokenInformation.sha256Fingerprint().isEmpty() || _encryptionCertificate.sensitiveDataRemaining(); } void ClientSideEncryption::failedToInitialize(const AccountPtr &account) @@ -1284,12 +1800,25 @@ void ClientSideEncryption::failedToInitialize(const AccountPtr &account) Q_EMIT initializationFinished(); } +void ClientSideEncryption::saveCertificateIdentification(const AccountPtr &account) const +{ + account->setEncryptionCertificateFingerprint(_usbTokenInformation.sha256Fingerprint()); +} + +void ClientSideEncryption::cacheTokenPin(const QString pin) +{ + _cachedPin = pin; + QTimer::singleShot(86400000, [this] () { + _cachedPin.clear(); + }); +} + void ClientSideEncryption::checkAllSensitiveDataDeleted() { if (sensitiveDataRemaining()) { qCWarning(lcCse) << "Some sensitive data emaining:" - << "Private key:" << (_privateKey.isEmpty() ? "is empty" : "is not empty") - << "Certificate is null:" << (_certificate.isNull() ? "true" : "false") + << "Private key:" << (getPrivateKey().isEmpty() ? "is empty" : "is not empty") + << "Certificate is null:" << (_encryptionCertificate.getCertificate().isNull() ? "true" : "false") << "Mnemonic:" << (_mnemonic.isEmpty() ? "is empty" : "is not empty"); return; } @@ -1335,7 +1864,7 @@ void ClientSideEncryption::generateKeyPair(const AccountPtr &account) return; } - _privateKey = BIO2ByteArray(privKey); + _encryptionCertificate.setPrivateKeyData(BIO2ByteArray(privKey)); } Bio privKey; @@ -1429,13 +1958,12 @@ void ClientSideEncryption::sendSignRequestCSR(const AccountPtr &account, auto job = new SignPublicKeyApiJob(account, e2eeBaseUrl(account) + "public-key", this); job->setCsr(csrContent); - connect(job, &SignPublicKeyApiJob::jsonReceived, [this, account, keyPair = std::move(keyPair)](const QJsonDocument& json, const int retCode) { + connect(job, &SignPublicKeyApiJob::jsonReceived, job, [this, account, keyPair = std::move(keyPair)](const QJsonDocument& json, const int retCode) { if (retCode == 200) { const auto cert = json.object().value("ocs").toObject().value("data").toObject().value("public-key").toString(); - _certificate = QSslCertificate(cert.toLocal8Bit(), QSsl::Pem); - _publicKey = _certificate.publicKey(); + _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{cert.toLocal8Bit(), QSsl::Pem}}; Bio certificateBio; - const auto certificatePem = _certificate.toPem(); + const auto certificatePem = _encryptionCertificate.getCertificate().toPem(); BIO_write(certificateBio, certificatePem.constData(), certificatePem.size()); const auto x509Certificate = X509Certificate::readCertificate(certificateBio); if (!X509_check_private_key(x509Certificate, keyPair)) { @@ -1457,6 +1985,28 @@ void ClientSideEncryption::sendSignRequestCSR(const AccountPtr &account, job->start(); } +void ClientSideEncryption::sendPublicKey(const AccountPtr &account) +{ + // Send public key to the server + auto job = new StorePublicKeyApiJob(account, e2eeBaseUrl(account) + "public-key", this); + job->setPublicKey(_encryptionCertificate.getCertificate().toPem()); + connect(job, &StorePublicKeyApiJob::jsonReceived, [this, account](const QJsonDocument& doc, int retCode) { + Q_UNUSED(doc); + switch(retCode) { + case 200: + case 409: + saveCertificateIdentification(account); + emit initializationFinished(); + + break; + default: + qCWarning(lcCse) << "Store certificate failed, return code:" << retCode; + failedToInitialize(account); + } + }); + job->start(); +} + void ClientSideEncryption::writeKeyPair(const AccountPtr &account, PKey keyPair, const QByteArray &csrContent) @@ -1581,7 +2131,7 @@ void ClientSideEncryption::encryptPrivateKey(const AccountPtr &account) auto salt = EncryptionHelper::generateRandom(40); auto secretKey = EncryptionHelper::generatePassword(passPhrase, salt); - auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(_privateKey), salt); + auto cryptedText = EncryptionHelper::encryptPrivateKey(secretKey, EncryptionHelper::privateKeyToPem(getPrivateKey()), salt); // Send private key to the server auto job = new StorePrivateKeyApiJob(account, e2eeBaseUrl(account) + "private-key", this); @@ -1633,7 +2183,7 @@ void ClientSideEncryption::decryptPrivateKey(const AccountPtr &account, const QB if (ok) { prev = dialog.textValue(); - _mnemonic = prev; + setMnemonic(prev); QString mnemonic = prev.split(" ").join(QString()).toLower(); // split off salt @@ -1641,11 +2191,10 @@ void ClientSideEncryption::decryptPrivateKey(const AccountPtr &account, const QB auto pass = EncryptionHelper::generatePassword(mnemonic, salt); - QByteArray privateKey = EncryptionHelper::decryptPrivateKey(pass, key); - //_privateKey = QSslKey(privateKey, QSsl::Rsa, QSsl::Pem, QSsl::PrivateKey); - _privateKey = privateKey; + const auto privateKey = EncryptionHelper::decryptPrivateKey(pass, key); + _encryptionCertificate.setPrivateKeyData(privateKey); - if (!_privateKey.isNull() && checkPublicKeyValidity(account)) { + if (!getPrivateKey().isNull() && checkPublicKeyValidity(account)) { writePrivateKey(account); writeCertificate(account); writeMnemonic(account, [] () {}); @@ -1687,8 +2236,7 @@ void ClientSideEncryption::getPublicKeyFromServer(const AccountPtr &account) 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(); + _encryptionCertificate = CertificateInformation{_encryptionCertificate.getPrivateKeyData(), QSslCertificate{publicKey.toLocal8Bit(), QSsl::Pem}}; fetchAndValidatePublicKeyFromServer(account); } else if (retCode == 404) { qCDebug(lcCse()) << "No public key on the server"; @@ -1713,7 +2261,7 @@ void ClientSideEncryption::fetchAndValidatePublicKeyFromServer(const AccountPtr if (retCode == 200) { const auto serverPublicKey = doc.object()["ocs"].toObject()["data"].toObject()["public-key"].toString().toLatin1(); if (checkServerPublicKeyValidity(serverPublicKey)) { - if (_privateKey.isEmpty()) { + if (getPrivateKey().isEmpty()) { getPrivateKeyFromServer(account); } else { encryptPrivateKey(account); @@ -2355,4 +2903,217 @@ OCC::NextcloudSslCertificate::operator QSslCertificate() const return _certificate; } +CertificateInformation::CertificateInformation() +{ + checkEncryptionCertificate(); +} + +CertificateInformation::CertificateInformation(PKCS11_KEY *hardwarePrivateKey, + QSslCertificate &&certificate) + : _hardwarePrivateKey(hardwarePrivateKey) + , _certificate(std::move(certificate)) +{ + checkEncryptionCertificate(); +} + +CertificateInformation::CertificateInformation(const QByteArray &privateKey, QSslCertificate &&certificate) + : _hardwarePrivateKey() + , _privateKeyData() + , _certificate(std::move(certificate)) +{ + if (!privateKey.isEmpty()) { + setPrivateKeyData(privateKey); + } + + checkEncryptionCertificate(); +} + +bool CertificateInformation::operator==(const CertificateInformation &other) const +{ + return _certificate.digest(QCryptographicHash::Sha256) == other._certificate.digest(QCryptographicHash::Sha256); +} + +void CertificateInformation::clear() +{ + _hardwarePrivateKey = nullptr; + _privateKeyData.clear(); + _certificate.clear(); + _certificateExpired = true; + _certificateNotYetValid = true; + _certificateRevoked = true; + _certificateInvalid = true; +} + +const QByteArray& CertificateInformation::getPrivateKeyData() const +{ + return _privateKeyData; +} + +void CertificateInformation::setPrivateKeyData(const QByteArray &privateKey) +{ + _privateKeyData = privateKey; +} + +QList CertificateInformation::verify() const +{ + auto result = QSslCertificate::verify({_certificate}); + + auto hasNeededExtendedKeyUsageExtension = false; + for (const auto &oneExtension : _certificate.extensions()) { + if (oneExtension.oid() == QStringLiteral("2.5.29.37")) { + const auto extendedKeyUsageList = oneExtension.value().toList(); + for (const auto &oneExtendedKeyUsageValue : extendedKeyUsageList) { + if (oneExtendedKeyUsageValue == QStringLiteral("E-mail Protection")) { + hasNeededExtendedKeyUsageExtension = true; + break; + } + } + } + } + if (!hasNeededExtendedKeyUsageExtension) { + result.append(QSslError{QSslError::InvalidPurpose}); + } + + return result; +} + +bool CertificateInformation::isSelfSigned() const +{ + return _certificate.isSelfSigned(); +} + +QSslKey CertificateInformation::getSslPublicKey() const +{ + return _certificate.publicKey(); +} + +PKey CertificateInformation::getEvpPublicKey() const +{ + const auto publicKey = _certificate.publicKey(); + Q_ASSERT(!publicKey.isNull()); + if (publicKey.isNull()) { + qCDebug(lcCse) << "Public key is null. Could not encrypt."; + } + Bio publicKeyBio; + const auto publicKeyPem = publicKey.toPem(); + BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); + return PKey::readPublicKey(publicKeyBio); +} + +PKCS11_KEY *CertificateInformation::getPkcs11PrivateKey() const +{ + return canDecrypt() ? _hardwarePrivateKey : nullptr; +} + +PKey CertificateInformation::getEvpPrivateKey() const +{ + if (_hardwarePrivateKey) { + return PKey::readHardwarePrivateKey(_hardwarePrivateKey); + } else { + const auto privateKeyPem = _privateKeyData; + Q_ASSERT(!privateKeyPem.isEmpty()); + if (privateKeyPem.isEmpty()) { + qCDebug(lcCse) << "Private key is empty. Could not encrypt."; + } + + Bio privateKeyBio; + BIO_write(privateKeyBio, privateKeyPem.constData(), privateKeyPem.size()); + return PKey::readPrivateKey(privateKeyBio); + } +} + +const QSslCertificate &CertificateInformation::getCertificate() const +{ + return _certificate; +} + +bool CertificateInformation::canEncrypt() const +{ + return (_hardwarePrivateKey || !_certificate.isNull()) && !_certificateExpired && !_certificateNotYetValid && !_certificateRevoked && !_certificateInvalid; +} + +bool CertificateInformation::canDecrypt() const +{ + return _hardwarePrivateKey || !_privateKeyData.isEmpty(); +} + +bool CertificateInformation::userCertificateNeedsMigration() const +{ + return _hardwarePrivateKey && + (_certificateExpired || _certificateNotYetValid || _certificateRevoked || _certificateInvalid); +} + +bool CertificateInformation::sensitiveDataRemaining() const +{ + return _hardwarePrivateKey && !_privateKeyData.isEmpty() && !_certificate.isNull(); +} + +QByteArray CertificateInformation::sha256Fingerprint() const +{ + return _certificate.digest(QCryptographicHash::Sha256).toBase64(); +} + +void CertificateInformation::checkEncryptionCertificate() +{ + _certificateExpired = false; + _certificateNotYetValid = false; + _certificateRevoked = false; + _certificateInvalid = false; + + const auto sslErrors = QSslCertificate::verify({_certificate}); + for (const auto &sslError : sslErrors) { + qCDebug(lcCse()) << "certificate validation error" << sslError; + switch (sslError.error()) + { + case QSslError::CertificateExpired: + _certificateExpired = true; + break; + case QSslError::CertificateNotYetValid: + _certificateNotYetValid = true; + break; + case QSslError::CertificateRevoked: + _certificateRevoked = true; + break; + case QSslError::UnableToGetIssuerCertificate: + case QSslError::UnableToDecryptCertificateSignature: + case QSslError::UnableToDecodeIssuerPublicKey: + case QSslError::CertificateSignatureFailed: + case QSslError::InvalidNotBeforeField: + case QSslError::InvalidNotAfterField: + case QSslError::SelfSignedCertificate: + case QSslError::SelfSignedCertificateInChain: + case QSslError::UnableToGetLocalIssuerCertificate: + case QSslError::UnableToVerifyFirstCertificate: + case QSslError::InvalidCaCertificate: + case QSslError::PathLengthExceeded: + case QSslError::InvalidPurpose: + case QSslError::CertificateUntrusted: + case QSslError::CertificateRejected: + case QSslError::SubjectIssuerMismatch: + case QSslError::AuthorityIssuerSerialNumberMismatch: + case QSslError::NoPeerCertificate: + case QSslError::HostNameMismatch: + case QSslError::NoSslSupport: + case QSslError::CertificateBlacklisted: + case QSslError::CertificateStatusUnknown: + case QSslError::OcspNoResponseFound: + case QSslError::OcspMalformedRequest: + case QSslError::OcspMalformedResponse: + case QSslError::OcspInternalError: + case QSslError::OcspTryLater: + case QSslError::OcspSigRequred: + case QSslError::OcspUnauthorized: + case QSslError::OcspResponseCannotBeTrusted: + case QSslError::OcspResponseCertIdUnknown: + case QSslError::OcspResponseExpired: + case QSslError::OcspStatusUnknown: + case QSslError::UnspecifiedError: + _certificateInvalid = true; + break; + case QSslError::NoError: + break; + } + } +} + } diff --git a/src/libsync/clientsideencryption.h b/src/libsync/clientsideencryption.h index 936b144423a9b..cf9527ba1c6a7 100644 --- a/src/libsync/clientsideencryption.h +++ b/src/libsync/clientsideencryption.h @@ -1,3 +1,17 @@ +/* + * Copyright © 2017, Tomaz Canabrava + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + #ifndef CLIENTSIDEENCRYPTION_H #define CLIENTSIDEENCRYPTION_H @@ -5,6 +19,11 @@ #include "clientsideencryptionprimitives.h" #include "accountfwd.h" +#include "networkjobs.h" +#include "clientsideencryptiontokenselector.h" + +#include "networkjobs.h" +#include "clientsideencryptiontokenselector.h" #include #include @@ -17,8 +36,17 @@ #include #include +#include + +#include + #include +#include +#include + +class QWidget; + namespace QKeychain { class Job; class WritePasswordJob; @@ -29,48 +57,109 @@ namespace OCC { QString e2eeBaseUrl(const OCC::AccountPtr &account); +class ClientSideEncryption; + +class CertificateInformation { +public: + CertificateInformation(); + + explicit CertificateInformation(PKCS11_KEY *hardwarePrivateKey, + QSslCertificate &&certificate); + + explicit CertificateInformation(const QByteArray& privateKey, + QSslCertificate &&certificate); + + [[nodiscard]] bool operator==(const CertificateInformation &other) const; + + void clear(); + + [[nodiscard]] const QByteArray& getPrivateKeyData() const; + + void setPrivateKeyData(const QByteArray& privateKey); + + [[nodiscard]] QList verify() const; + + [[nodiscard]] bool isSelfSigned() const; + + [[nodiscard]] QSslKey getSslPublicKey() const; + + [[nodiscard]] PKey getEvpPublicKey() const; + + [[nodiscard]] PKCS11_KEY* getPkcs11PrivateKey() const; + + [[nodiscard]] PKey getEvpPrivateKey() const; + + [[nodiscard]] const QSslCertificate& getCertificate() const; + + [[nodiscard]] bool canEncrypt() const; + + [[nodiscard]] bool canDecrypt() const; + + [[nodiscard]] bool userCertificateNeedsMigration() const; + + [[nodiscard]] bool sensitiveDataRemaining() const; + + [[nodiscard]] QByteArray sha256Fingerprint() const; + +private: + void checkEncryptionCertificate(); + + PKCS11_KEY* _hardwarePrivateKey = nullptr; + + QByteArray _privateKeyData; + + QSslCertificate _certificate; + + bool _certificateExpired = true; + + bool _certificateNotYetValid = true; + + bool _certificateRevoked = true; + + bool _certificateInvalid = true; +}; + namespace EncryptionHelper { - OWNCLOUDSYNC_EXPORT QByteArray generateRandomFilename(); - OWNCLOUDSYNC_EXPORT QByteArray generateRandom(int size); - QByteArray generatePassword(const QString &wordlist, const QByteArray& salt); - OWNCLOUDSYNC_EXPORT QByteArray encryptPrivateKey( - const QByteArray& key, - const QByteArray& privateKey, - const QByteArray &salt - ); - OWNCLOUDSYNC_EXPORT QByteArray decryptPrivateKey( - const QByteArray& key, - const QByteArray& data - ); - OWNCLOUDSYNC_EXPORT QByteArray extractPrivateKeySalt(const QByteArray &data); - OWNCLOUDSYNC_EXPORT QByteArray encryptStringSymmetric( - const QByteArray& key, - const QByteArray& data - ); - OWNCLOUDSYNC_EXPORT QByteArray decryptStringSymmetric( - const QByteArray& key, - const QByteArray& data - ); - OWNCLOUDSYNC_EXPORT QByteArray encryptStringAsymmetric(const QSslKey key, const QByteArray &data); - OWNCLOUDSYNC_EXPORT QByteArray decryptStringAsymmetric(const QByteArray &privateKeyPem, const QByteArray &data); - - QByteArray privateKeyToPem(const QByteArray key); - - //TODO: change those two EVP_PKEY into QSslKey. - QByteArray encryptStringAsymmetric( - EVP_PKEY *publicKey, - const QByteArray& data - ); - QByteArray decryptStringAsymmetric( - EVP_PKEY *privateKey, - const QByteArray& data - ); - - OWNCLOUDSYNC_EXPORT bool fileEncryption(const QByteArray &key, const QByteArray &iv, - QFile *input, QFile *output, QByteArray& returnTag); - - OWNCLOUDSYNC_EXPORT bool fileDecryption(const QByteArray &key, const QByteArray &iv, - QFile *input, QFile *output); + +OWNCLOUDSYNC_EXPORT QByteArray generateRandomFilename(); +OWNCLOUDSYNC_EXPORT QByteArray generateRandom(int size); +QByteArray generatePassword(const QString &wordlist, const QByteArray& salt); +OWNCLOUDSYNC_EXPORT QByteArray encryptPrivateKey( + const QByteArray& key, + const QByteArray& privateKey, + const QByteArray &salt +); +OWNCLOUDSYNC_EXPORT QByteArray decryptPrivateKey( + const QByteArray& key, + const QByteArray& data +); +OWNCLOUDSYNC_EXPORT QByteArray extractPrivateKeySalt(const QByteArray &data); +OWNCLOUDSYNC_EXPORT QByteArray encryptStringSymmetric( + const QByteArray& key, + const QByteArray& data +); +OWNCLOUDSYNC_EXPORT QByteArray decryptStringSymmetric( + const QByteArray& key, + const QByteArray& data +); + +[[nodiscard]] OWNCLOUDSYNC_EXPORT std::optional encryptStringAsymmetric(const CertificateInformation &selectedCertificate, + int paddingMode, + const ClientSideEncryption &encryptionEngine, + const QByteArray &binaryData); + +[[nodiscard]] OWNCLOUDSYNC_EXPORT std::optional decryptStringAsymmetric(const CertificateInformation &selectedCertificate, + int paddingMode, + const ClientSideEncryption &encryptionEngine, + const QByteArray &base64Data); + +QByteArray privateKeyToPem(const QByteArray key); + +OWNCLOUDSYNC_EXPORT bool fileEncryption(const QByteArray &key, const QByteArray &iv, + QFile *input, QFile *output, QByteArray& returnTag); + +OWNCLOUDSYNC_EXPORT bool fileDecryption(const QByteArray &key, const QByteArray &iv, + QFile *input, QFile *output); OWNCLOUDSYNC_EXPORT bool dataEncryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output, QByteArray &returnTag); OWNCLOUDSYNC_EXPORT bool dataDecryption(const QByteArray &key, const QByteArray &iv, const QByteArray &input, QByteArray &output); @@ -148,14 +237,54 @@ class OWNCLOUDSYNC_EXPORT NextcloudSslCertificate class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { Q_OBJECT + + Q_PROPERTY(bool canEncrypt READ canEncrypt NOTIFY canEncryptChanged FINAL) + Q_PROPERTY(bool canDecrypt READ canDecrypt NOTIFY canDecryptChanged FINAL) + Q_PROPERTY(bool userCertificateNeedsMigration READ userCertificateNeedsMigration NOTIFY userCertificateNeedsMigrationChanged FINAL) public: ClientSideEncryption(); - QByteArray _privateKey; - QSslKey _publicKey; - QSslCertificate _certificate; - QString _mnemonic; - bool _newMnemonicGenerated = false; + [[nodiscard]] bool isInitialized() const; + + [[nodiscard]] bool tokenIsSetup() const; + + [[nodiscard]] QSslKey getPublicKey() const; + + [[nodiscard]] const QByteArray& getPrivateKey() const; + + void setPrivateKey(const QByteArray &privateKey); + + [[nodiscard]] const CertificateInformation& getCertificateInformation() const; + + [[nodiscard]] CertificateInformation getCertificateInformationByFingerprint(const QByteArray &certificateFingerprint) const; + + [[nodiscard]] int paddingMode() const; + + [[nodiscard]] CertificateInformation getTokenCertificateByFingerprint(const QByteArray &expectedFingerprint) const; + + [[nodiscard]] bool useTokenBasedEncryption() const; + + [[nodiscard]] const QString &getMnemonic() const; + + void setCertificate(const QSslCertificate &certificate); + + [[nodiscard]] const QSslCertificate& getCertificate() const; + + [[nodiscard]] ENGINE* sslEngine() const; + + [[nodiscard]] QByteArray generateSignatureCryptographicMessageSyntax(const QByteArray &data) const; + + [[nodiscard]] bool verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector &certificatePems) const; + + [[nodiscard]] ClientSideEncryptionTokenSelector* usbTokenInformation(); + + [[nodiscard]] bool canEncrypt() const; + + [[nodiscard]] bool canDecrypt() const; + + [[nodiscard]] bool userCertificateNeedsMigration() const; + + [[nodiscard]] QByteArray certificateSha256Fingerprint() const; signals: void initializationFinished(bool isNewMnemonicGenerated = false); @@ -167,18 +296,27 @@ class OWNCLOUDSYNC_EXPORT ClientSideEncryption : public QObject { void certificateFetchedFromKeychain(QSslCertificate certificate); void certificatesFetchedFromServer(const QHash &results); void certificateWriteComplete(const QSslCertificate &certificate); + void displayTokenInitDialog(); -public: - [[nodiscard]] QByteArray generateSignatureCryptographicMessageSyntax(const QByteArray &data) const; - [[nodiscard]] bool verifySignatureCryptographicMessageSyntax(const QByteArray &cmsContent, const QByteArray &data, const QVector &certificatePems) const; + void startingDiscoveryEncryptionUsbToken(); + void finishedDiscoveryEncryptionUsbToken(); + + void canEncryptChanged(); + void canDecryptChanged(); + void userCertificateNeedsMigrationChanged(); public slots: - void initialize(const OCC::AccountPtr &account); + void initialize(QWidget *settingsDialog, + const OCC::AccountPtr &account); + void initializeHardwareTokenEncryption(QWidget* settingsDialog, + const OCC::AccountPtr &account); void forgetSensitiveData(const OCC::AccountPtr &account); void getUsersPublicKeyFromServer(const OCC::AccountPtr &account, const QStringList &userIds); void fetchCertificateFromKeyChain(const OCC::AccountPtr &account, const QString &userId); void writeCertificate(const OCC::AccountPtr &account, const QString &userId, const QSslCertificate &certificate); + void migrateCertificate(); + private slots: void generateKeyPair(const OCC::AccountPtr &account); void encryptPrivateKey(const OCC::AccountPtr &account); @@ -205,9 +343,16 @@ private slots: void writePrivateKey(const OCC::AccountPtr &account); void writeCertificate(const OCC::AccountPtr &account); + void completeHardwareTokenInitialization(QWidget *settingsDialog, + const OCC::AccountPtr &account); + + void setMnemonic(const QString &mnemonic); + private: void generateMnemonic(); + void setEncryptionCertificate(CertificateInformation certificateInfo); + [[nodiscard]] std::pair generateCSR(const AccountPtr &account, PKey keyPair, PKey privateKey); @@ -216,6 +361,8 @@ private slots: PKey keyPair, const QByteArray &csrContent); + void sendPublicKey(const AccountPtr &account); + void writeKeyPair(const AccountPtr &account, PKey keyPair, const QByteArray &csrContent); @@ -246,9 +393,27 @@ private slots: [[nodiscard]] bool checkServerPublicKeyValidity(const QByteArray &serverPublicKeyString) const; [[nodiscard]] bool sensitiveDataRemaining() const; + [[nodiscard]] bool checkEncryptionIsWorking() const; + void failedToInitialize(const AccountPtr &account); - bool isInitialized = false; + void saveCertificateIdentification(const AccountPtr &account) const; + void cacheTokenPin(const QString pin); + + QString _mnemonic; + bool _newMnemonicGenerated = false; + + QString _cachedPin; + + ClientSideEncryptionTokenSelector _usbTokenInformation; + + CertificateInformation _encryptionCertificate; + std::vector _otherCertificates; + + Pkcs11Context _context{Pkcs11Context::State::EmptyContext}; + std::unique_ptr> _tokenSlots; }; + } // namespace OCC + #endif diff --git a/src/libsync/clientsideencryptionjobs.cpp b/src/libsync/clientsideencryptionjobs.cpp index d39c393d1777b..c3325f325f178 100644 --- a/src/libsync/clientsideencryptionjobs.cpp +++ b/src/libsync/clientsideencryptionjobs.cpp @@ -20,6 +20,7 @@ #include "common/syncjournaldb.h" Q_LOGGING_CATEGORY(lcSignPublicKeyApiJob, "nextcloud.sync.networkjob.sendcsr", QtInfoMsg) +Q_LOGGING_CATEGORY(lcStorePublicKeyApiJob, "nextcloud.sync.networkjob.storepublickey", QtInfoMsg) Q_LOGGING_CATEGORY(lcStorePrivateKeyApiJob, "nextcloud.sync.networkjob.storeprivatekey", QtInfoMsg) Q_LOGGING_CATEGORY(lcCseJob, "nextcloud.sync.networkjob.clientsideencrypt", QtInfoMsg) @@ -77,17 +78,18 @@ bool GetMetadataApiJob::finished() } StoreMetaDataApiJob::StoreMetaDataApiJob(const AccountPtr& account, - const QByteArray& fileId, - const QByteArray &token, - const QByteArray& b64Metadata, - const QByteArray &signature, - QObject* parent) -: AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent), -_fileId(fileId), -_token(token), -_b64Metadata(b64Metadata), -_signature(signature) + const QByteArray& fileId, + const QByteArray &token, + const QByteArray& b64Metadata, + const QByteArray &signature, + QObject* parent) + : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("meta-data/") + fileId, parent), + _fileId(fileId), + _token(token), + _b64Metadata(b64Metadata), + _signature(signature) { + Q_ASSERT(!_signature.isEmpty()); } void StoreMetaDataApiJob::start() @@ -96,9 +98,8 @@ void StoreMetaDataApiJob::start() req.setRawHeader("OCS-APIREQUEST", "true"); req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { - if (!_signature.isEmpty()) { - req.setRawHeader(e2eeSignatureHeaderName, _signature); - } + Q_ASSERT(!_signature.isEmpty()); + req.setRawHeader(e2eeSignatureHeaderName, _signature); } QUrlQuery query; query.addQueryItem(QLatin1String("format"), QLatin1String("json")); @@ -121,14 +122,18 @@ void StoreMetaDataApiJob::start() bool StoreMetaDataApiJob::finished() { - int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - if (retCode != 200) { - qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode; - emit error(_fileId, retCode); - return false; - } - qCInfo(lcCseJob()) << "Metadata submitted to the server successfully"; - emit success(_fileId); + const auto retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (retCode != 200) { + qCInfo(lcCseJob()) << "error sending the metadata" << path() << errorString() << retCode; + qCInfo(lcCseJob()) << reply()->readAll(); + + emit error(_fileId, retCode); + return false; + } + + qCInfo(lcCseJob()) << "Metadata submitted to the server successfully"; + emit success(_fileId); + return true; } @@ -299,14 +304,16 @@ bool DeleteMetadataApiJob::finished() LockEncryptFolderApiJob::LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, + const QByteArray &certificateSha256Fingerprint, SyncJournalDb *journalDb, - const QSslKey publicKey, + const QSslKey &sslkey, QObject *parent) : AbstractNetworkJob(account, e2eeBaseUrl(account) + QStringLiteral("lock/") + fileId, parent) , _fileId(fileId) + , _certificateSha256Fingerprint(certificateSha256Fingerprint) , _journalDb(journalDb) - , _publicKey(publicKey) { + Q_UNUSED(sslkey) } void LockEncryptFolderApiJob::start() @@ -315,8 +322,12 @@ void LockEncryptFolderApiJob::start() if (!folderTokenEncrypted.isEmpty()) { qCInfo(lcCseJob()) << "lock folder started for:" << path() << " for fileId: " << _fileId << " but we need to first lift the previous lock"; - const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, folderTokenEncrypted); - const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, folderToken, _journalDb, this); + const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), folderTokenEncrypted); + if (!folderToken) { + qCWarning(lcCseJob()) << "decrypt failed"; + return; + } + const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, _fileId, *folderToken, _journalDb, this); unlockJob->setShouldRollbackMetadataChanges(true); connect(unlockJob, &UnlockEncryptFolderApiJob::done, this, [this]() { this->start(); @@ -364,9 +375,13 @@ bool LockEncryptFolderApiJob::finished() qCInfo(lcCseJob()) << "lock folder finished with code" << retCode << " for:" << path() << " for fileId: " << _fileId << " token:" << token; - if (!_publicKey.isNull()) { - const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_publicKey, token); - _journalDb->setE2EeLockedFolder(_fileId, folderTokenEncrypted); + if (!_account->e2e()->getPublicKey().isNull()) { + const auto folderTokenEncrypted = EncryptionHelper::encryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), token); + if (!folderTokenEncrypted) { + qCWarning(lcCseJob()) << "decrypt failed"; + return false; + } + _journalDb->setE2EeLockedFolder(_fileId, *folderTokenEncrypted); } //TODO: Parse the token and submit. @@ -410,6 +425,45 @@ bool SetEncryptionFlagApiJob::finished() return true; } +StorePublicKeyApiJob::StorePublicKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent) + : AbstractNetworkJob(account, path, parent) +{ +} + +void StorePublicKeyApiJob::setPublicKey(const QByteArray& publicKey) +{ + QByteArray data = "publicKey="; + data += QUrl::toPercentEncoding(publicKey); + _publicKey.setData(data); +} + +void StorePublicKeyApiJob::start() +{ + QNetworkRequest req; + req.setRawHeader("OCS-APIREQUEST", "true"); + req.setHeader(QNetworkRequest::ContentTypeHeader, QByteArrayLiteral("application/x-www-form-urlencoded")); + QUrlQuery query; + query.addQueryItem(QLatin1String("format"), QLatin1String("json")); + QUrl url = Utility::concatUrlPath(account()->url(), path()); + url.setQuery(query); + + qCDebug(lcStorePublicKeyApiJob) << "Sending the public key"; + sendRequest("PUT", url, req, &_publicKey); + AbstractNetworkJob::start(); +} + +bool StorePublicKeyApiJob::finished() +{ + int retCode = reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (retCode != 200) + qCInfo(lcStorePublicKeyApiJob()) << "Sending public key ended with" << path() << errorString() << retCode; + + QJsonParseError error{}; + auto json = QJsonDocument::fromJson(reply()->readAll(), &error); + emit jsonReceived(json, reply()->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt()); + return true; +} + StorePrivateKeyApiJob::StorePrivateKeyApiJob(const AccountPtr& account, const QString& path, QObject* parent) : AbstractNetworkJob(account, path, parent) { @@ -431,7 +485,7 @@ void StorePrivateKeyApiJob::start() QUrl url = Utility::concatUrlPath(account()->url(), path()); url.setQuery(query); - qCInfo(lcStorePrivateKeyApiJob) << "Sending the private key" << _privKey.data(); + qCDebug(lcStorePrivateKeyApiJob) << "Sending the private key"; sendRequest("POST", url, req, &_privKey); AbstractNetworkJob::start(); } @@ -470,7 +524,7 @@ void SignPublicKeyApiJob::start() QUrl url = Utility::concatUrlPath(account()->url(), path()); url.setQuery(query); - qCInfo(lcSignPublicKeyApiJob) << "Sending the CSR" << _csr.data(); + qCDebug(lcSignPublicKeyApiJob) << "Sending the CSR"; sendRequest("POST", url, req, &_csr); AbstractNetworkJob::start(); } diff --git a/src/libsync/clientsideencryptionjobs.h b/src/libsync/clientsideencryptionjobs.h index 9052e9bbcb420..6c1f5de91e925 100644 --- a/src/libsync/clientsideencryptionjobs.h +++ b/src/libsync/clientsideencryptionjobs.h @@ -3,9 +3,10 @@ #include "networkjobs.h" #include "accountfwd.h" + +#include #include #include -#include namespace OCC { /* Here are all of the network jobs for the client side encryption. @@ -57,6 +58,49 @@ public slots: QBuffer _csr; }; +/* + * @brief Job to upload the PublicKey that return JSON + * + * To be used like this: + * \code + * _job = new StorePublicKeyApiJob(account, QLatin1String("ocs/v1.php/foo/bar"), this); + * _job->setPublicKey( privKey ); + * connect(_job...); + * _job->start(); + * \encode + * + * @ingroup libsync + */ +class OWNCLOUDSYNC_EXPORT StorePublicKeyApiJob : public AbstractNetworkJob +{ + Q_OBJECT +public: + explicit StorePublicKeyApiJob(const AccountPtr &account, const QString &path, QObject *parent = nullptr); + + /** + * @brief setCsr - the CSR with the public key. + * This function needs to be called before start() obviously. + */ + void setPublicKey(const QByteArray& publicKey); + +public slots: + void start() override; + +protected: + bool finished() override; +signals: + + /** + * @brief jsonReceived - signal to report the json answer from ocs + * @param json - the parsed json document + * @param statusCode - the OCS status code: 100 (!) for success + */ + void jsonReceived(const QJsonDocument &json, int statusCode); + +private: + QBuffer _publicKey; +}; + /* * @brief Job to upload the PrivateKey that return JSON * @@ -145,7 +189,12 @@ class OWNCLOUDSYNC_EXPORT LockEncryptFolderApiJob : public AbstractNetworkJob { Q_OBJECT public: - explicit LockEncryptFolderApiJob(const AccountPtr &account, const QByteArray &fileId, SyncJournalDb *journalDb, const QSslKey publicKey, QObject *parent = nullptr); + explicit LockEncryptFolderApiJob(const AccountPtr &account, + const QByteArray &fileId, + const QByteArray &certificateSha256Fingerprint, + SyncJournalDb *journalDb, + const QSslKey &sslkey, + QObject *parent = nullptr); void setCounter(const quint64 counter); @@ -163,6 +212,7 @@ public slots: private: QByteArray _fileId; + QByteArray _certificateSha256Fingerprint; QPointer _journalDb; QSslKey _publicKey; quint64 _counter = 0; diff --git a/src/libsync/clientsideencryptionprimitives.cpp b/src/libsync/clientsideencryptionprimitives.cpp index 92210df5fc1ef..9f48d2720a54b 100644 --- a/src/libsync/clientsideencryptionprimitives.cpp +++ b/src/libsync/clientsideencryptionprimitives.cpp @@ -11,19 +11,18 @@ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ + #include "clientsideencryptionprimitives.h" + +#include + #include namespace OCC { -Bio::Bio() - : _bio(BIO_new(BIO_s_mem())) -{ -} -Bio::~Bio() -{ - BIO_free_all(_bio); -} + +Q_LOGGING_CATEGORY(lcCseUtility, "nextcloud.sync.clientsideencryption.utility", QtInfoMsg) + Bio::operator const BIO *() const { return _bio; @@ -53,6 +52,7 @@ PKeyCtx PKeyCtx::forKey(EVP_PKEY *pkey, ENGINE *e) { PKeyCtx ctx; ctx._ctx = EVP_PKEY_CTX_new(pkey, e); + Q_ASSERT(ctx._ctx); return ctx; } @@ -78,6 +78,13 @@ PKey PKey::readPublicKey(Bio &bio) return result; } +PKey PKey::readHardwarePublicKey(PKCS11_KEY *key) +{ + PKey result; + result._pkey = PKCS11_get_public_key(key); + return result; +} + PKey PKey::readPrivateKey(Bio &bio) { PKey result; @@ -85,6 +92,13 @@ PKey PKey::readPrivateKey(Bio &bio) return result; } +PKey PKey::readHardwarePrivateKey(PKCS11_KEY *key) +{ + PKey result; + result._pkey = PKCS11_get_private_key(key); + return result; +} + PKey PKey::generate(PKeyCtx &ctx) { PKey result; @@ -104,4 +118,53 @@ PKey::operator EVP_PKEY *() const return _pkey; } -} \ No newline at end of file +Pkcs11Context::Pkcs11Context(State initState) + : _pkcsS11Ctx(initState == State::CreateContext ? PKCS11_CTX_new() : nullptr) +{ + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] constructor" << this << _pkcsS11Ctx; +} + +Pkcs11Context::Pkcs11Context(Pkcs11Context &&otherContext) + : _pkcsS11Ctx(otherContext._pkcsS11Ctx) +{ + otherContext._pkcsS11Ctx = nullptr; + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] constructor" << this << _pkcsS11Ctx; +} + +Pkcs11Context::~Pkcs11Context() +{ + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] destructor" << this << _pkcsS11Ctx; + if (_pkcsS11Ctx) { + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] PKCS11_CTX_free" << _pkcsS11Ctx; + PKCS11_CTX_free(_pkcsS11Ctx); + _pkcsS11Ctx = nullptr; + } else { + qCDebug(lcCseUtility()) << "destructor" << this << "nullptr"; + } +} + +Pkcs11Context &Pkcs11Context::operator=(Pkcs11Context &&otherContext) +{ + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] operator=" << this << _pkcsS11Ctx; + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] operator=" << &otherContext << otherContext._pkcsS11Ctx; + if (&otherContext != this) { + if (_pkcsS11Ctx) { + qCDebug(lcCseUtility()) << "[[Pkcs11Context]] PKCS11_CTX_free" << _pkcsS11Ctx; + PKCS11_CTX_free(_pkcsS11Ctx); + _pkcsS11Ctx = nullptr; + } + std::swap(_pkcsS11Ctx, otherContext._pkcsS11Ctx); + } + + return *this; +} + +void Pkcs11Context::clear() +{ + if (_pkcsS11Ctx) { + PKCS11_CTX_free(_pkcsS11Ctx); + _pkcsS11Ctx = nullptr; + } +} + +} diff --git a/src/libsync/clientsideencryptionprimitives.h b/src/libsync/clientsideencryptionprimitives.h index 28efc9b29c3df..6d1bce28ea1cd 100644 --- a/src/libsync/clientsideencryptionprimitives.h +++ b/src/libsync/clientsideencryptionprimitives.h @@ -11,18 +11,28 @@ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * for more details. */ + #pragma once -#include + #include +#include +#include + namespace OCC { class Bio { public: - Bio(); + Bio() + : _bio(BIO_new(BIO_s_mem())) + { + } - ~Bio(); + ~Bio() + { + BIO_free_all(_bio); + } operator const BIO *() const; operator BIO *(); @@ -74,8 +84,12 @@ class PKey static PKey readPublicKey(Bio &bio); + static PKey readHardwarePublicKey(PKCS11_KEY *key); + static PKey readPrivateKey(Bio &bio); + static PKey readHardwarePrivateKey(PKCS11_KEY *key); + static PKey generate(PKeyCtx &ctx); operator EVP_PKEY *(); @@ -89,4 +103,40 @@ class PKey EVP_PKEY *_pkey = nullptr; }; -} \ No newline at end of file + +class Pkcs11Context { +public: + enum class State { + CreateContext, + EmptyContext, + }; + + explicit Pkcs11Context(State initState); + + Pkcs11Context(Pkcs11Context &&otherContext); + + Pkcs11Context(const Pkcs11Context&) = delete; + + ~Pkcs11Context(); + + Pkcs11Context& operator=(Pkcs11Context &&otherContext); + + Pkcs11Context& operator=(const Pkcs11Context&) = delete; + + operator const PKCS11_CTX*() const + { + return _pkcsS11Ctx; + } + + operator PKCS11_CTX*() + { + return _pkcsS11Ctx; + } + + void clear(); + +private: + PKCS11_CTX* _pkcsS11Ctx = nullptr; +}; + +} diff --git a/src/libsync/clientsideencryptiontokenselector.cpp b/src/libsync/clientsideencryptiontokenselector.cpp new file mode 100644 index 0000000000000..601b4de352339 --- /dev/null +++ b/src/libsync/clientsideencryptiontokenselector.cpp @@ -0,0 +1,319 @@ +/* + * Copyright © 2023, Matthieu Gallien + * Copyright (C) 2017 The Qt Company Ltd. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + * + * Commercial License Usage + * Licensees holding valid commercial Qt licenses may use this file in + * accordance with the commercial license agreement provided with the + * Software or, alternatively, in accordance with the terms contained in + * a written agreement between you and The Qt Company. For licensing terms + * and conditions see https://www.qt.io/terms-conditions. For further + * information use the contact form at https://www.qt.io/contact-us. + * + * GNU Lesser General Public License Usage + * Alternatively, this file may be used under the terms of the GNU Lesser + * General Public License version 3 as published by the Free Software + * Foundation and appearing in the file LICENSE.LGPL3 included in the + * packaging of this file. Please review the following information to + * ensure the GNU Lesser General Public License version 3 requirements + * will be met: https://www.gnu.org/licenses/lgpl-3.0.html. + * + * GNU General Public License Usage + * Alternatively, this file may be used under the terms of the GNU + * General Public License version 2.0 or (at your option) the GNU General + * Public license version 3 or any later version approved by the KDE Free + * Qt Foundation. The licenses are as published by the Free Software + * Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 + * included in the packaging of this file. Please review the following + * information to ensure the GNU General Public License requirements will + * be met: https://www.gnu.org/licenses/gpl-2.0.html and + * https://www.gnu.org/licenses/gpl-3.0.html. + */ + +#include "clientsideencryptiontokenselector.h" + +#include "clientsideencryptionprimitives.h" +#include "account.h" + +#include +#include +#include + +#if defined(Q_OS_WIN) +#include +#endif + +#include + +#include + +namespace { + +static unsigned char* unsignedData(QByteArray& array) +{ + return (unsigned char*)array.data(); +} + +static QByteArray BIO2ByteArray(OCC::Bio &b) { + auto pending = static_cast(BIO_ctrl_pending(b)); + QByteArray res(pending, '\0'); + BIO_read(b, unsignedData(res), pending); + return res; +} + +} + +namespace OCC +{ + +Q_LOGGING_CATEGORY(lcCseSelector, "nextcloud.sync.clientsideencryption.selector", QtInfoMsg) + +ClientSideEncryptionTokenSelector::ClientSideEncryptionTokenSelector(QObject *parent) + : QObject{parent} +{ + +} + +bool ClientSideEncryptionTokenSelector::isSetup() const +{ + return !_sha256Fingerprint.isEmpty(); +} + +QVariantList ClientSideEncryptionTokenSelector::discoveredCertificates() const +{ + return _discoveredCertificates; +} + +QByteArray ClientSideEncryptionTokenSelector::sha256Fingerprint() const +{ + return _sha256Fingerprint; +} + +void ClientSideEncryptionTokenSelector::clear() +{ + _discoveredCertificates.clear(); + _sha256Fingerprint.clear(); +} + +QFuture ClientSideEncryptionTokenSelector::searchForCertificates(const AccountPtr &account) +{ + return QtConcurrent::run([this, account] () -> void { + discoverCertificates(account); + }); +} + +void ClientSideEncryptionTokenSelector::setSha256Fingerprint(const QByteArray &sha256Fingerprint) +{ + if (_sha256Fingerprint == sha256Fingerprint) { + return; + } + + _sha256Fingerprint = sha256Fingerprint; + Q_EMIT sha256FingerprintChanged(); +} + +void ClientSideEncryptionTokenSelector::discoverCertificates(const AccountPtr &account) +{ +#if defined(Q_OS_WIN) + auto sslConfig = QSslConfiguration::defaultConfiguration(); + + for (const auto &storeName : std::vector{L"CA"}) { + auto systemStore = CertOpenSystemStore(0, storeName.data()); + if (systemStore) { + auto certificatePointer = PCCERT_CONTEXT{nullptr}; + while (true) { + certificatePointer = CertFindCertificateInStore(systemStore, X509_ASN_ENCODING, 0, CERT_FIND_ANY, nullptr, certificatePointer); + if (!certificatePointer) { + break; + } + const auto der = QByteArray{reinterpret_cast(certificatePointer->pbCertEncoded), + static_cast(certificatePointer->cbCertEncoded)}; + const auto cert = QSslCertificate{der, QSsl::Der}; + + qCDebug(lcCseSelector()) << "found certificate" << cert.subjectDisplayName() << cert.issuerDisplayName() << "from store" << storeName; + + sslConfig.addCaCertificate(cert); + } + CertCloseStore(systemStore, 0); + } + } + + QSslConfiguration::setDefaultConfiguration(sslConfig); +#endif + + qCDebug(lcCseSelector()) << "existing CA certificates"; + const auto currentSslConfig = QSslConfiguration::defaultConfiguration(); + const auto &caCertificates = currentSslConfig.caCertificates(); + for (const auto &oneCaCertificate : caCertificates) { + qCDebug(lcCseSelector()) << oneCaCertificate.subjectDisplayName() << oneCaCertificate.issuerDisplayName(); + } + + auto ctx = Pkcs11Context{Pkcs11Context::State::CreateContext}; + + auto rc = PKCS11_CTX_load(ctx, account->encryptionHardwareTokenDriverPath().toLatin1().constData()); + if (rc) { + qCWarning(lcCseSelector()) << "loading pkcs11 engine failed:" << ERR_reason_error_string(ERR_get_error()) << account->encryptionHardwareTokenDriverPath(); + + Q_EMIT failedToInitialize(account); + return; + } + + auto tokensCount = 0u; + PKCS11_SLOT *tempTokenSlots = nullptr; + /* get information on all slots */ + if (PKCS11_enumerate_slots(ctx, &tempTokenSlots, &tokensCount) < 0) { + qCWarning(lcCseSelector()) << "no slots available" << ERR_reason_error_string(ERR_get_error()); + + Q_EMIT failedToInitialize(account); + return; + } + + auto deleter = [&ctx, tokensCount] (PKCS11_SLOT* pointer) noexcept -> void { + PKCS11_release_all_slots(ctx, pointer, tokensCount); + }; + + auto tokenSlots = std::unique_ptr{tempTokenSlots, deleter}; + + if (!tokensCount) { + qCWarning(lcCseSelector()) << "no tokens found"; + + Q_EMIT failedToInitialize(account); + return; + } + + _discoveredCertificates.clear(); + auto currentSlot = static_cast(nullptr); + for(auto tokenIndex = 0u; tokenIndex < tokensCount; ++tokenIndex) { + currentSlot = PKCS11_find_next_token(ctx, tokenSlots.get(), tokensCount, currentSlot); + if (currentSlot == nullptr || currentSlot->token == nullptr) { + break; + } + + qCDebug(lcCseSelector()) << "Slot manufacturer......:" << currentSlot->manufacturer; + qCDebug(lcCseSelector()) << "Slot description.......:" << currentSlot->description; + qCDebug(lcCseSelector()) << "Slot token label.......:" << currentSlot->token->label; + qCDebug(lcCseSelector()) << "Slot token manufacturer:" << currentSlot->token->manufacturer; + qCDebug(lcCseSelector()) << "Slot token model.......:" << currentSlot->token->model; + qCDebug(lcCseSelector()) << "Slot token serialnr....:" << currentSlot->token->serialnr; + + auto keysCount = 0u; + auto certificatesFromToken = static_cast(nullptr); + if (PKCS11_enumerate_certs(currentSlot->token, &certificatesFromToken, &keysCount)) { + qCWarning(lcCseSelector()) << "PKCS11_enumerate_certs failed" << ERR_reason_error_string(ERR_get_error()); + + Q_EMIT failedToInitialize(account); + return; + } + + for (auto certificateIndex = 0u; certificateIndex < keysCount; ++certificateIndex) { + const auto currentCertificate = &certificatesFromToken[certificateIndex]; + qCInfo(lcCseSelector()) << "certificate metadata:" + << "label:" << currentCertificate->label; + + const auto certificateId = QByteArray{reinterpret_cast(currentCertificate->id), static_cast(currentCertificate->id_len)}; + qCInfo(lcCseSelector()) << "new certificate ID:" << certificateId.toBase64(); + + Bio out; + const auto ret = PEM_write_bio_X509(out, currentCertificate->x509); + if (ret <= 0){ + qCWarning(lcCseSelector()) << "PEM_write_bio_X509 failed" << ERR_reason_error_string(ERR_get_error()); + + Q_EMIT failedToInitialize(account); + return; + } + + const auto result = BIO2ByteArray(out); + const auto sslCertificate = QSslCertificate{result, QSsl::Pem}; + const auto certificateDigest = sslCertificate.digest(QCryptographicHash::Sha256).toBase64(); + + qCInfo(lcCseSelector()) << "newly found certificate" + << "subject:" << sslCertificate.subjectDisplayName() + << "issuer:" << sslCertificate.issuerDisplayName() + << "valid since:" << sslCertificate.effectiveDate() + << "valid until:" << sslCertificate.expiryDate() + << "serial number:" << sslCertificate.serialNumber() + << "SHA256 fingerprint:" << certificateDigest; + + if (sslCertificate.isSelfSigned()) { + qCDebug(lcCseSelector()) << "newly found certificate is self signed: goint to ignore it"; + continue; + } + + auto hasNeededExtendedKeyUsageExtension = false; + const auto &allExtensions = sslCertificate.extensions(); + for (const auto &oneExtension : allExtensions) { + qCDebug(lcCseSelector()) << "extension:" << (oneExtension.isCritical() ? "is critical" : "") << (oneExtension.isSupported() ? "is supported" : "") << oneExtension.name() << oneExtension.value() << oneExtension.oid(); + if (oneExtension.oid() == QStringLiteral("2.5.29.37")) { + const auto extendedKeyUsageList = oneExtension.value().toList(); + for (const auto &oneExtendedKeyUsageValue : extendedKeyUsageList) { + qCDebug(lcCseSelector()) << "EKU:" << oneExtendedKeyUsageValue; + if (oneExtendedKeyUsageValue == QStringLiteral("E-mail Protection")) { + hasNeededExtendedKeyUsageExtension = true; + break; + } + } + } + } + if (!hasNeededExtendedKeyUsageExtension) { + qCDebug(lcCseSelector()) << "newly found certificate is missing the required EKU extension: Secure Email (1.3.6.1.5.5.7.3.4)"; + continue; + } + + _discoveredCertificates.push_back(QVariantMap{ + {QStringLiteral("label"), QString::fromLatin1(currentCertificate->label)}, + {QStringLiteral("subject"), sslCertificate.subjectDisplayName()}, + {QStringLiteral("issuer"), sslCertificate.issuerDisplayName()}, + {QStringLiteral("serialNumber"), sslCertificate.serialNumber()}, + {QStringLiteral("validSince"), sslCertificate.effectiveDate()}, + {QStringLiteral("validUntil"), sslCertificate.expiryDate()}, + {QStringLiteral("sha256Fingerprint"), certificateDigest}, + {QStringLiteral("certificate"), QVariant::fromValue(sslCertificate)}, + }); + + std::sort(_discoveredCertificates.begin(), _discoveredCertificates.end(), [] (const auto &first, const auto &second) -> bool { + return first.toMap()[QStringLiteral("validSince")].toDateTime() > second.toMap()[QStringLiteral("validSince")].toDateTime(); + }); + } + } + + processDiscoveredCertificates(); +} + +void ClientSideEncryptionTokenSelector::processDiscoveredCertificates() +{ + const auto &allCertificates = discoveredCertificates(); + for (const auto &oneCertificate : allCertificates) { + const auto certificateData = oneCertificate.toMap(); + const auto sslCertificate = certificateData[QStringLiteral("certificate")].value(); + if (sslCertificate.isNull()) { + qCDebug(lcCseSelector()) << "null certificate"; + continue; + } + const auto sslErrors = QSslCertificate::verify({sslCertificate}); + if (!sslErrors.isEmpty()) { + for (const auto &oneError : sslErrors) { + qCInfo(lcCseSelector()) << oneError; + } + continue; + } + + const auto &sha256Fingerprint = sslCertificate.digest(QCryptographicHash::Sha256).toBase64(); + qCInfo(lcCseSelector()) << "selected certificate" << certificateData[QStringLiteral("subject")] << "from" << certificateData[QStringLiteral("issuer")] << "fingerprint" << sha256Fingerprint << "serialNumber" << sslCertificate.serialNumber(); + + setSha256Fingerprint(sha256Fingerprint); + Q_EMIT isSetupChanged(); + return; + } +} + +} diff --git a/src/libsync/clientsideencryptiontokenselector.h b/src/libsync/clientsideencryptiontokenselector.h new file mode 100644 index 0000000000000..1a82f3d3ec0d7 --- /dev/null +++ b/src/libsync/clientsideencryptiontokenselector.h @@ -0,0 +1,71 @@ +/* + * Copyright © 2023, Matthieu Gallien + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * for more details. + */ + +#ifndef CLIENTSIDETOKENSELECTOR_H +#define CLIENTSIDETOKENSELECTOR_H + +#include "accountfwd.h" +#include "owncloudlib.h" + +#include +#include + +namespace OCC +{ + +class OWNCLOUDSYNC_EXPORT ClientSideEncryptionTokenSelector : public QObject +{ + Q_OBJECT + + Q_PROPERTY(bool isSetup READ isSetup NOTIFY isSetupChanged) + + Q_PROPERTY(QByteArray sha256Fingerprint READ sha256Fingerprint WRITE setSha256Fingerprint NOTIFY sha256FingerprintChanged) + +public: + explicit ClientSideEncryptionTokenSelector(QObject *parent = nullptr); + + [[nodiscard]] bool isSetup() const; + + [[nodiscard]] QByteArray sha256Fingerprint() const; + + void clear(); + +public slots: + QFuture searchForCertificates(const OCC::AccountPtr &account); + + void setSha256Fingerprint(const QByteArray &sha256Fingerprint); + +signals: + + void isSetupChanged(); + + void sha256FingerprintChanged(); + + void failedToInitialize(const OCC::AccountPtr &account); + +private: + void discoverCertificates(const OCC::AccountPtr &account); + + [[nodiscard]] QVariantList discoveredCertificates() const; + + void processDiscoveredCertificates(); + + QVariantList _discoveredCertificates; + + QByteArray _sha256Fingerprint; +}; + +} + +#endif // CLIENTSIDETOKENSELECTOR_H diff --git a/src/libsync/discovery.cpp b/src/libsync/discovery.cpp index e842f82540808..9368eb8ef8d61 100644 --- a/src/libsync/discovery.cpp +++ b/src/libsync/discovery.cpp @@ -85,6 +85,16 @@ void ProcessDirectoryJob::start() { qCInfo(lcDisco) << "STARTING" << _currentFolder._server << _queryServer << _currentFolder._local << _queryLocal; + if (isInsideEncryptedTree()) { + auto folderDbRecord = SyncJournalFileRecord{}; + if (_discoveryData->_statedb->getFileRecord(_currentFolder._local, &folderDbRecord) && folderDbRecord.isValid()) { + if (_discoveryData->_account->encryptionCertificateFingerprint() != folderDbRecord._e2eCertificateFingerprint) { + qCDebug(lcDisco) << "encryption certificate needs update. Forcing full discovery"; + _queryServer = NormalQuery; + } + } + } + _discoveryData->_noCaseConflictRecordsInDb = _discoveryData->_statedb->caseClashConflictRecordPaths().isEmpty(); if (_queryServer == NormalQuery) { @@ -231,7 +241,7 @@ void ProcessDirectoryJob::process() continue; const auto isEncryptedFolderButE2eIsNotSetup = e.serverEntry.isValid() && e.serverEntry.isE2eEncrypted() && - _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->_publicKey.isNull() && _discoveryData->_account->e2e()->_privateKey.isNull(); + _discoveryData->_account->e2e() && !_discoveryData->_account->e2e()->isInitialized(); if (isEncryptedFolderButE2eIsNotSetup) { checkAndUpdateSelectiveSyncListsForE2eeFolders(path._server + "/"); @@ -697,6 +707,8 @@ void ProcessDirectoryJob::processFileAnalyzeRemoteInfo(const SyncFileItemPtr &it item->_e2eEncryptionStatus = serverEntry.isE2eEncrypted() ? SyncFileItem::EncryptionStatus::Encrypted : SyncFileItem::EncryptionStatus::NotEncrypted; if (serverEntry.isE2eEncrypted()) { item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); + item->_e2eCertificateFingerprint = serverEntry.e2eCertificateFingerprint; + //Q_ASSERT(item->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::NotEncrypted || !item->_e2eCertificateFingerprint.isEmpty()); } item->_encryptedFileName = [=] { if (serverEntry.e2eMangledName.isEmpty()) { @@ -1161,7 +1173,10 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( if (dbEntry.isValid()) { bool typeChange = localEntry.isDirectory != dbEntry.isDirectory(); - if (!typeChange && localEntry.isVirtualFile) { + if (localEntry.isDirectory && dbEntry.isValid() && dbEntry.isE2eEncrypted() && dbEntry._e2eCertificateFingerprint != _discoveryData->_account->encryptionCertificateFingerprint()) { + item->_instruction = CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA; + item->_direction = SyncFileItem::Up; + } else if (!typeChange && localEntry.isVirtualFile) { if (noServerEntry) { item->_instruction = CSYNC_INSTRUCTION_REMOVE; item->_direction = SyncFileItem::Down; @@ -1410,6 +1425,7 @@ void ProcessDirectoryJob::processFileAnalyzeLocalInfo( // 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->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(base._e2eEncryptionStatus); item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_discoveryData->_account->capabilities().clientSideEncryptionVersion()); + item->_e2eCertificateFingerprint = base._e2eCertificateFingerprint; } postProcessLocalNew(); finalize(); @@ -1702,6 +1718,12 @@ void ProcessDirectoryJob::processFileFinalize( } } + if (item->_direction == SyncFileItem::Up && item->isEncrypted() && !_discoveryData->_account->e2e()->canEncrypt()) { + item->_instruction = CSYNC_INSTRUCTION_ERROR; + item->_errorString = tr("Cannot modify encrypted item because the selected certificate is not valid."); + item->_status = SyncFileItem::Status::NormalError; + } + if (item->isDirectory() && item->_instruction == CSYNC_INSTRUCTION_SYNC) item->_instruction = CSYNC_INSTRUCTION_UPDATE_METADATA; bool removed = item->_instruction == CSYNC_INSTRUCTION_REMOVE; @@ -2089,6 +2111,8 @@ DiscoverySingleDirectoryJob *ProcessDirectoryJob::startAsyncServerQuery() const auto alreadyDownloaded = _discoveryData->_statedb->getFileRecord(_dirItem->_file, &record) && record.isValid(); // we need to make sure we first download all e2ee files/folders before migrating _dirItem->_isEncryptedMetadataNeedUpdate = alreadyDownloaded && serverJob->encryptedMetadataNeedUpdate(); + _dirItem->_e2eCertificateFingerprint = serverJob->certificateSha256Fingerprint(); + //Q_ASSERT(_dirItem->_e2eEncryptionStatus == SyncFileItem::EncryptionStatus::NotEncrypted || !_dirItem->_e2eCertificateFingerprint.isEmpty()); _dirItem->_e2eEncryptionStatus = serverJob->currentEncryptionStatus(); _dirItem->_e2eEncryptionStatusRemote = serverJob->currentEncryptionStatus(); _dirItem->_e2eEncryptionServerCapability = serverJob->requiredEncryptionStatus(); diff --git a/src/libsync/discoveryphase.cpp b/src/libsync/discoveryphase.cpp index 6cd226f2ee1a5..0114127ff7207 100644 --- a/src/libsync/discoveryphase.cpp +++ b/src/libsync/discoveryphase.cpp @@ -460,6 +460,11 @@ SyncFileItem::EncryptionStatus DiscoverySingleDirectoryJob::requiredEncryptionSt return _encryptionStatusRequired; } +QByteArray DiscoverySingleDirectoryJob::certificateSha256Fingerprint() const +{ + return _e2eCertificateFingerprint; +} + static void propertyMapToRemoteInfo(const QMap &map, RemotePermissions::MountedPermissionAlgorithm algorithm, RemoteInfo &result) { for (auto it = map.constBegin(); it != map.constEnd(); ++it) { @@ -728,6 +733,7 @@ void DiscoverySingleDirectoryJob::metadataReceived(const QJsonDocument &json, in } _isFileDropDetected = e2EeFolderMetadata->isFileDropPresent(); _encryptedMetadataNeedUpdate = e2EeFolderMetadata->encryptedMetadataNeedUpdate(); + _e2eCertificateFingerprint = e2EeFolderMetadata->certificateSha256Fingerprint(); _encryptionStatusRequired = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(_account->capabilities().clientSideEncryptionVersion()); _encryptionStatusCurrent = e2EeFolderMetadata->existingMetadataEncryptionStatus(); diff --git a/src/libsync/discoveryphase.h b/src/libsync/discoveryphase.h index bb932f568b4db..f1d1d3136dd65 100644 --- a/src/libsync/discoveryphase.h +++ b/src/libsync/discoveryphase.h @@ -71,6 +71,7 @@ struct RemoteInfo bool _isE2eEncrypted = false; bool isFileDropDetected = false; QString e2eMangledName; + QByteArray e2eCertificateFingerprint; bool sharedByMe = false; [[nodiscard]] bool isValid() const { return !name.isNull(); } @@ -162,6 +163,7 @@ class DiscoverySingleDirectoryJob : public QObject void abort(); [[nodiscard]] bool isFileDropDetected() const; [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + [[nodiscard]] QByteArray certificateSha256Fingerprint() const; [[nodiscard]] SyncFileItem::EncryptionStatus currentEncryptionStatus() const; [[nodiscard]] SyncFileItem::EncryptionStatus requiredEncryptionStatus() const; @@ -202,6 +204,8 @@ private slots: bool _isFileDropDetected = false; bool _encryptedMetadataNeedUpdate = false; SyncFileItem::EncryptionStatus _encryptionStatusRequired = SyncFileItem::EncryptionStatus::NotEncrypted; + QByteArray _e2eCertificateFingerprint; + // If set, the discovery will finish with an error int64_t _size = 0; QString _error; diff --git a/src/libsync/encryptedfoldermetadatahandler.cpp b/src/libsync/encryptedfoldermetadatahandler.cpp index 65a394646cc8c..7dfcb11e80c1b 100644 --- a/src/libsync/encryptedfoldermetadatahandler.cpp +++ b/src/libsync/encryptedfoldermetadatahandler.cpp @@ -97,7 +97,7 @@ void EncryptedFolderMetadataHandler::lockFolder() return; } - const auto lockJob = new LockEncryptFolderApiJob(_account, _folderId, _journalDb, _account->e2e()->_publicKey, this); + const auto lockJob = new LockEncryptFolderApiJob(_account, _folderId, _account->e2e()->certificateSha256Fingerprint(), _journalDb, _account->e2e()->getPublicKey(), this); connect(lockJob, &LockEncryptFolderApiJob::success, this, &EncryptedFolderMetadataHandler::slotFolderLockedSuccessfully); connect(lockJob, &LockEncryptFolderApiJob::error, this, &EncryptedFolderMetadataHandler::slotFolderLockedError); if (_account->capabilities().clientSideEncryptionVersion() >= 2.0) { @@ -265,13 +265,13 @@ void EncryptedFolderMetadataHandler::unlockFolder(const UnlockFolderWithResult r qCDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Calling Unlock"; const auto unlockJob = new UnlockEncryptFolderApiJob(_account, _folderId, _folderToken, _journalDb, this); - connect(unlockJob, &UnlockEncryptFolderApiJob::success, [this](const QByteArray &folderId) { + connect(unlockJob, &UnlockEncryptFolderApiJob::success, unlockJob, [this](const QByteArray &folderId) { qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Successfully Unlocked"; _isFolderLocked = false; emit folderUnlocked(folderId, 200); _isUnlockRunning = false; }); - connect(unlockJob, &UnlockEncryptFolderApiJob::error, [this](const QByteArray &folderId, int httpStatus) { + connect(unlockJob, &UnlockEncryptFolderApiJob::error, unlockJob, [this](const QByteArray &folderId, int httpStatus) { qDebug(lcFetchAndUploadE2eeFolderMetadataJob) << "Unlock Error"; emit folderUnlocked(folderId, httpStatus); _isUnlockRunning = false; diff --git a/src/libsync/encryptfolderjob.cpp b/src/libsync/encryptfolderjob.cpp index 295e24dd0c60f..e7740a85b6387 100644 --- a/src/libsync/encryptfolderjob.cpp +++ b/src/libsync/encryptfolderjob.cpp @@ -83,6 +83,7 @@ void EncryptFolderJob::slotEncryptionFlagSuccess(const QByteArray &fileId) if (!rec.isE2eEncrypted()) { rec._e2eEncryptionStatus = SyncJournalFileRecord::EncryptionStatus::Encrypted; + rec._e2eCertificateFingerprint = _account->e2e()->certificateSha256Fingerprint(); 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/foldermetadata.cpp b/src/libsync/foldermetadata.cpp index f3a21953d341d..34f9d8d2a50ed 100644 --- a/src/libsync/foldermetadata.cpp +++ b/src/libsync/foldermetadata.cpp @@ -24,7 +24,7 @@ namespace OCC { -Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.metadata", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCseMetadata, "nextcloud.sync.clientsideencryption.metadata", QtInfoMsg) namespace { @@ -187,23 +187,24 @@ void FolderMetadata::setupExistingMetadata(const QByteArray &metadata) return; } - if (!parseFileDropPart(metaDataDoc)) { - qCDebug(lcCseMetadata()) << "Could not parse filedrop part"; - return; - } - if (_folderUsers.contains(_account->davUser())) { const auto currentFolderUser = _folderUsers.value(_account->davUser()); - _metadataKeyForEncryption = decryptDataWithPrivateKey(currentFolderUser.encryptedMetadataKey); + _e2eCertificateFingerprint = QSslCertificate{currentFolderUser.certificatePem}.digest(QCryptographicHash::Sha256).toBase64(); + _metadataKeyForEncryption = QByteArray::fromBase64(decryptDataWithPrivateKey(currentFolderUser.encryptedMetadataKey, _e2eCertificateFingerprint)); _metadataKeyForDecryption = _metadataKeyForEncryption; } + if (!parseFileDropPart(metaDataDoc)) { + qCDebug(lcCseMetadata()) << "Could not parse filedrop part"; + return; + } + if (metadataKeyForDecryption().isEmpty() || metadataKeyForEncryption().isEmpty()) { qCDebug(lcCseMetadata()) << "Could not setup metadata key!"; _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; } - + const auto metadataObj = metaDataDoc.object()[metadataJsonKey].toObject(); _metadataNonce = QByteArray::fromBase64(metadataObj[nonceKey].toString().toLocal8Bit()); const auto cipherTextEncrypted = metadataObj[cipherTextKey].toString().toLocal8Bit(); @@ -284,7 +285,7 @@ void FolderMetadata::setupExistingMetadataLegacy(const QByteArray &metadata) const auto metadataKeyFromJson = metadataObj[metadataKeyKey].toString().toLocal8Bit(); if (!metadataKeyFromJson.isEmpty()) { // parse version 1.1 and 1.2 (both must have a single "metadataKey"), not "metadataKeys" as 1.0 - const auto decryptedMetadataKeyBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(metadataKeyFromJson)); + const auto decryptedMetadataKeyBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(metadataKeyFromJson), _account->e2e()->certificateSha256Fingerprint()); if (!decryptedMetadataKeyBase64.isEmpty()) { // fromBase64() multiple times just to stick with the old wrong way _metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(decryptedMetadataKeyBase64)); @@ -306,7 +307,7 @@ void FolderMetadata::setupExistingMetadataLegacy(const QByteArray &metadata) if (!lastMetadataKeyFromJson.isEmpty()) { const auto lastMetadataKeyValueFromJson = metadataKeys.value(lastMetadataKeyFromJson).toString().toLocal8Bit(); if (!lastMetadataKeyValueFromJson.isEmpty()) { - const auto lastMetadataKeyValueFromJsonBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(lastMetadataKeyValueFromJson)); + const auto lastMetadataKeyValueFromJsonBase64 = decryptDataWithPrivateKey(QByteArray::fromBase64(lastMetadataKeyValueFromJson), _account->e2e()->certificateSha256Fingerprint()); if (!lastMetadataKeyValueFromJsonBase64.isEmpty()) { _metadataKeyForDecryption = QByteArray::fromBase64(QByteArray::fromBase64(lastMetadataKeyValueFromJsonBase64)); } @@ -429,29 +430,25 @@ void FolderMetadata::emitSetupComplete() } // RSA/ECB/OAEPWithSHA-256AndMGF1Padding using private / public key. -QByteArray FolderMetadata::encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const +QByteArray FolderMetadata::encryptDataWithPublicKey(const QByteArray &binaryData, + const CertificateInformation &shareUserCertificate) const { - Bio publicKeyBio; - const auto publicKeyPem = key.toPem(); - BIO_write(publicKeyBio, publicKeyPem.constData(), publicKeyPem.size()); - const auto publicKey = PKey::readPublicKey(publicKeyBio); - - // The metadata key is binary so base64 encode it first - return EncryptionHelper::encryptStringAsymmetric(publicKey, data); + const auto encryptBase64Result = EncryptionHelper::encryptStringAsymmetric(shareUserCertificate, _account->e2e()->paddingMode(), *_account->e2e(), binaryData); + qCDebug(lcCseMetadata()) << "encryptDataWithPublicKey" << binaryData.toBase64() << *encryptBase64Result; + return *encryptBase64Result; } -QByteArray FolderMetadata::decryptDataWithPrivateKey(const QByteArray &data) const +QByteArray FolderMetadata::decryptDataWithPrivateKey(const QByteArray &base64Data, + const QByteArray &certificateFingerprint) const { - Bio privateKeyBio; - BIO_write(privateKeyBio, _account->e2e()->_privateKey.constData(), _account->e2e()->_privateKey.size()); - const auto privateKey = PKey::readPrivateKey(privateKeyBio); - - const auto decryptResult = EncryptionHelper::decryptStringAsymmetric(privateKey, data); - if (decryptResult.isEmpty()) { + qCDebug(lcCseMetadata()) << "decrypt metadata key with certificate" << certificateFingerprint; + const auto decryptBase64Result = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformationByFingerprint(certificateFingerprint), _account->e2e()->paddingMode(), *_account->e2e(), base64Data); + if (!decryptBase64Result) { qCDebug(lcCseMetadata()) << "ERROR. Could not decrypt the metadata key"; _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); + return {}; } - return decryptResult; + return *decryptBase64Result; } // AES/GCM/NoPadding (128 bit key size) @@ -476,7 +473,8 @@ QByteArray FolderMetadata::computeMetadataKeyChecksum(const QByteArray &metadata { auto hashAlgorithm = QCryptographicHash{QCryptographicHash::Sha256}; - hashAlgorithm.addData(_account->e2e()->_mnemonic.remove(' ').toUtf8()); + auto mnemonic = _account->e2e()->getMnemonic(); + hashAlgorithm.addData(mnemonic.remove(' ').toUtf8()); auto sortedFiles = _files; std::sort(sortedFiles.begin(), sortedFiles.end(), [](const auto &first, const auto &second) { return first.encryptedFilename < second.encryptedFilename; @@ -554,7 +552,7 @@ void FolderMetadata::initEmptyMetadata() } qCDebug(lcCseMetadata()) << "Setting up empty metadata v2"; if (_isRootEncryptedFolder) { - if (!addUser(_account->davUser(), _account->e2e()->_certificate)) { + if (!addUser(_account->davUser(), _account->e2e()->getCertificate())) { qCDebug(lcCseMetadata) << "Empty metadata setup failed. Could not add first user."; _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); return; @@ -571,7 +569,7 @@ void FolderMetadata::initEmptyMetadataLegacy() qCDebug(lcCseMetadata) << "Settint up legacy empty metadata"; _metadataKeyForEncryption = EncryptionHelper::generateRandom(metadataKeySize); _metadataKeyForDecryption = _metadataKeyForEncryption; - QString publicKey = _account->e2e()->_publicKey.toPem().toBase64(); + QString publicKey = _account->e2e()->getPublicKey().toPem().toBase64(); QString displayName = _account->displayName(); _isMetadataValid = true; @@ -703,7 +701,7 @@ QByteArray FolderMetadata::encryptedMetadataLegacy() } const auto version = _account->capabilities().clientSideEncryptionVersion(); // multiple toBase64() just to keep with the old (wrong way) - const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption().toBase64().toBase64(), _account->e2e()->_publicKey).toBase64(); + const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption().toBase64().toBase64(), _account->e2e()->getCertificateInformation()).toBase64(); const QJsonObject metadata{ {versionKey, version}, {metadataKeyKey, QJsonValue::fromVariant(encryptedMetadataKey)}, @@ -796,7 +794,7 @@ bool FolderMetadata::parseFileDropPart(const QJsonDocument &doc) if (userParsedId == _account->davUser()) { const auto fileDropEntryUser = UserWithFileDropEntryAccess{ userParsedId, - decryptDataWithPrivateKey(QByteArray::fromBase64(userParsed.value(usersEncryptedFiledropKey).toByteArray()))}; + decryptDataWithPrivateKey(QByteArray::fromBase64(userParsed.value(usersEncryptedFiledropKey).toByteArray()), _e2eCertificateFingerprint)}; if (!fileDropEntryUser.isValid()) { qCDebug(lcCseMetadata()) << "Could not parse filedrop data. encryptedFiledropKey decryption failed"; _account->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); @@ -957,6 +955,11 @@ bool FolderMetadata::encryptedMetadataNeedUpdate() const return !foundNestedFoldersOrIsNestedFolder; } +QByteArray FolderMetadata::certificateSha256Fingerprint() const +{ + return _e2eCertificateFingerprint; +} + bool FolderMetadata::moveFromFileDropToFiles() { if (_fileDropEntries.isEmpty()) { @@ -1037,13 +1040,14 @@ void FolderMetadata::slotRootE2eeFolderMetadataReceived(int statusCode, const QS bool FolderMetadata::addUser(const QString &userId, const QSslCertificate &certificate) { Q_ASSERT(_isRootEncryptedFolder); + Q_ASSERT(!certificate.isNull()); if (!_isRootEncryptedFolder) { qCWarning(lcCseMetadata()) << "Could not add a folder user to a non top level folder."; return false; } - const auto certificatePublicKey = certificate.publicKey(); - if (userId.isEmpty() || certificate.isNull() || certificatePublicKey.isNull()) { + const auto shareUserCertificate = CertificateInformation{{}, QSslCertificate{certificate}}; + if (userId.isEmpty() || certificate.isNull() || !shareUserCertificate.canEncrypt()) { qCWarning(lcCseMetadata()) << "Could not add a folder user. Invalid userId or certificate."; return false; } @@ -1052,7 +1056,7 @@ bool FolderMetadata::addUser(const QString &userId, const QSslCertificate &certi UserWithFolderAccess newFolderUser; newFolderUser.userId = userId; newFolderUser.certificatePem = certificate.toPem(); - newFolderUser.encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey); + newFolderUser.encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), shareUserCertificate); _folderUsers[userId] = newFolderUser; updateUsersEncryptedMetadataKey(); @@ -1095,13 +1099,9 @@ void FolderMetadata::updateUsersEncryptedMetadataKey() auto folderUser = it.value(); const QSslCertificate certificate(folderUser.certificatePem); - const auto certificatePublicKey = certificate.publicKey(); - if (certificate.isNull() || certificatePublicKey.isNull()) { - qCWarning(lcCseMetadata()) << "Could not update folder users with null certificatePublicKey!"; - continue; - } + CertificateInformation shareUserCertificate = CertificateInformation{{}, QSslCertificate{certificate}}; - const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), certificatePublicKey); + const auto encryptedMetadataKey = encryptDataWithPublicKey(metadataKeyForEncryption(), shareUserCertificate); if (encryptedMetadataKey.isEmpty()) { qCWarning(lcCseMetadata()) << "Could not update folder users with empty encryptedMetadataKey!"; continue; diff --git a/src/libsync/foldermetadata.h b/src/libsync/foldermetadata.h index 0cdba5236effd..34fda88e86e54 100644 --- a/src/libsync/foldermetadata.h +++ b/src/libsync/foldermetadata.h @@ -116,6 +116,8 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject [[nodiscard]] bool encryptedMetadataNeedUpdate() const; + [[nodiscard]] QByteArray certificateSha256Fingerprint() const; + [[nodiscard]] bool moveFromFileDropToFiles(); // adds a user to have access to this folder (always generates new metadata key) @@ -141,8 +143,8 @@ class OWNCLOUDSYNC_EXPORT FolderMetadata : public QObject [[nodiscard]] QByteArray initialMetadata() const; public slots: - void addEncryptedFile(const EncryptedFile &f); - void removeEncryptedFile(const EncryptedFile &f); + void addEncryptedFile(const FolderMetadata::EncryptedFile &f); + void removeEncryptedFile(const FolderMetadata::EncryptedFile &f); void removeAllEncryptedFiles(); private: @@ -150,8 +152,9 @@ public slots: [[nodiscard]] bool verifyMetadataKey(const QByteArray &metadataKey) const; - [[nodiscard]] QByteArray encryptDataWithPublicKey(const QByteArray &data, const QSslKey &key) const; - [[nodiscard]] QByteArray decryptDataWithPrivateKey(const QByteArray &data) const; + [[nodiscard]] QByteArray encryptDataWithPublicKey(const QByteArray &data, + const CertificateInformation &shareUserCertificate) const; + [[nodiscard]] QByteArray decryptDataWithPrivateKey(const QByteArray &data, const QByteArray &certificateFingerprint) const; [[nodiscard]] QByteArray encryptJsonObject(const QByteArray& obj, const QByteArray pass) const; [[nodiscard]] QByteArray decryptJsonObject(const QByteArray& encryptedJsonBlob, const QByteArray& pass) const; @@ -232,6 +235,8 @@ private slots: // signature from server-side metadata QByteArray _initialSignature; + QByteArray _e2eCertificateFingerprint; + // both files and folders info QVector _files; diff --git a/src/libsync/owncloudpropagator.cpp b/src/libsync/owncloudpropagator.cpp index fc166ecce8e90..1a1334cdcbf1f 100644 --- a/src/libsync/owncloudpropagator.cpp +++ b/src/libsync/owncloudpropagator.cpp @@ -394,10 +394,21 @@ PropagateItemJob *OwncloudPropagator::createJob(const SyncFileItemPtr &item) } case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA: return new PropagateVfsUpdateMetadataJob(this, item); + case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA: + { + const auto rootE2eeFolderPath = item->_file.split('/').first(); + const auto rootE2eeFolderPathFullRemotePath = fullRemotePath(rootE2eeFolderPath); + return new UpdateMigratedE2eeMetadataJob(this, item, rootE2eeFolderPathFullRemotePath, remotePath()); + } case CSYNC_INSTRUCTION_IGNORE: case CSYNC_INSTRUCTION_ERROR: return new PropagateIgnoreJob(this, item); - default: + case CSYNC_INSTRUCTION_NONE: + case CSYNC_INSTRUCTION_EVAL: + case CSYNC_INSTRUCTION_EVAL_RENAME: + case CSYNC_INSTRUCTION_STAT_ERROR: + case CSYNC_INSTRUCTION_UPDATE_METADATA: + case CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT: return nullptr; } return nullptr; @@ -1497,6 +1508,9 @@ void PropagateDirectory::slotSubJobsFinished(SyncFileItem::Status status) } #endif if (!_item->_isAnyCaseClashChild && !_item->_isAnyInvalidCharChild) { + if (_item->isEncrypted()) { + _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint(); + } const auto result = propagator()->updateMetadata(*_item); if (!result) { status = _item->_status = SyncFileItem::FatalError; diff --git a/src/libsync/progressdispatcher.cpp b/src/libsync/progressdispatcher.cpp index b656d6adadbe0..3272be55103ec 100644 --- a/src/libsync/progressdispatcher.cpp +++ b/src/libsync/progressdispatcher.cpp @@ -58,6 +58,8 @@ QString Progress::asResultString(const SyncFileItem &item) return QCoreApplication::translate("progress", "Updated local metadata"); case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA: return QCoreApplication::translate("progress", "Updated local virtual files metadata"); + case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA: + return QCoreApplication::translate("progress", "Updated end-to-end encryption metadata"); case CSYNC_INSTRUCTION_NONE: case CSYNC_INSTRUCTION_EVAL: return QCoreApplication::translate("progress", "Unknown"); @@ -91,6 +93,8 @@ QString Progress::asActionString(const SyncFileItem &item) return QCoreApplication::translate("progress", "Updating local metadata"); case CSYNC_INSTRUCTION_UPDATE_VFS_METADATA: return QCoreApplication::translate("progress", "Updating local virtual files metadata"); + case CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA: + return QCoreApplication::translate("progress", "Updating end-to-end encryption metadata"); case CSYNC_INSTRUCTION_NONE: case CSYNC_INSTRUCTION_EVAL: break; diff --git a/src/libsync/propagatedownload.cpp b/src/libsync/propagatedownload.cpp index d47061c9c1ca3..f183d202b7e62 100644 --- a/src/libsync/propagatedownload.cpp +++ b/src/libsync/propagatedownload.cpp @@ -1164,6 +1164,7 @@ void PropagateDownloadFile::finalizeDownload() { if (isEncrypted()) { if (_downloadEncryptedHelper->decryptFile(_tmpFile)) { + _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint(); downloadFinished(); } else { done(SyncFileItem::NormalError, _downloadEncryptedHelper->errorString(), ErrorCategory::GenericError); diff --git a/src/libsync/propagatedownloadencrypted.cpp b/src/libsync/propagatedownloadencrypted.cpp index 568aa911a91fe..52abaec5be0ee 100644 --- a/src/libsync/propagatedownloadencrypted.cpp +++ b/src/libsync/propagatedownloadencrypted.cpp @@ -18,7 +18,6 @@ PropagateDownloadEncrypted::PropagateDownloadEncrypted(OwncloudPropagator *propa const auto rootPath = Utility::noLeadingSlashPath(_propagator->remotePath()); const auto remoteFilename = _item->_encryptedFileName.isEmpty() ? _item->_file : _item->_encryptedFileName; const auto remotePath = QString(rootPath + remoteFilename); - const auto remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); _remoteParentPath = remotePath.left(remotePath.lastIndexOf('/')); const auto filenameInDb = _item->_file; @@ -115,4 +114,4 @@ QString PropagateDownloadEncrypted::errorString() const return _errorString; } -} \ No newline at end of file +} diff --git a/src/libsync/propagateremotemkdir.cpp b/src/libsync/propagateremotemkdir.cpp index e301beed5a723..868614e68f8f4 100644 --- a/src/libsync/propagateremotemkdir.cpp +++ b/src/libsync/propagateremotemkdir.cpp @@ -261,6 +261,7 @@ void PropagateRemoteMkdir::slotEncryptFolderFinished(int status, EncryptionStatu qCDebug(lcPropagateRemoteMkdir) << "Success making the new folder encrypted"; propagator()->_activeJobList.removeOne(this); _item->_e2eEncryptionStatus = encryptionStatus; + _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint(); _item->_e2eEncryptionStatusRemote = encryptionStatus; if (_item->isEncrypted()) { _item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion()); diff --git a/src/libsync/propagateupload.cpp b/src/libsync/propagateupload.cpp index 16e68c96b5ac6..3fea412308222 100644 --- a/src/libsync/propagateupload.cpp +++ b/src/libsync/propagateupload.cpp @@ -806,6 +806,10 @@ void PropagateUploadFileCommon::finalize() if (quotaIt != propagator()->_folderQuota.end()) quotaIt.value() -= _fileToUpload._size; + if (_item->isEncrypted() && _uploadingEncrypted) { + _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint(); + } + // Update the database entry const auto result = propagator()->updateMetadata(*_item, Vfs::DatabaseMetadata); if (!result) { diff --git a/src/libsync/propagateuploadencrypted.cpp b/src/libsync/propagateuploadencrypted.cpp index 6999459fbc686..0f156b0282d5f 100644 --- a/src/libsync/propagateuploadencrypted.cpp +++ b/src/libsync/propagateuploadencrypted.cpp @@ -186,4 +186,4 @@ void PropagateUploadEncrypted::slotUploadMetadataFinished(int statusCode, const FileSystem::getSize(_completeFileName)); } -} // namespace OCC \ No newline at end of file +} // namespace OCC diff --git a/src/libsync/syncengine.cpp b/src/libsync/syncengine.cpp index 0f8691cd05960..dc2d70556e2c3 100644 --- a/src/libsync/syncengine.cpp +++ b/src/libsync/syncengine.cpp @@ -423,6 +423,10 @@ void OCC::SyncEngine::slotItemDiscovered(const OCC::SyncFileItemPtr &item) } } + if (rec.isE2eEncrypted()) { + rec._e2eCertificateFingerprint = _account->encryptionCertificateFingerprint(); + } + // Updating the db happens on success if (!_journal->setFileRecord(rec)) { item->_status = SyncFileItem::Status::NormalError; @@ -513,9 +517,13 @@ void SyncEngine::startSync() for (const auto &e2EeLockedFolder : e2EeLockedFolders) { const auto folderId = e2EeLockedFolder.first; qCInfo(lcEngine()) << "start unlock job for folderId:" << folderId; - const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->_privateKey, e2EeLockedFolder.second); + const auto folderToken = EncryptionHelper::decryptStringAsymmetric(_account->e2e()->getCertificateInformation(), _account->e2e()->paddingMode(), *_account->e2e(), e2EeLockedFolder.second); + if (!folderToken) { + qCWarning(lcEngine()) << "decrypt failed"; + return; + } // TODO: We need to rollback changes done to metadata in case we have an active lock, this needs to be implemented on the server first - const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, folderToken, _journal, this); + const auto unlockJob = new OCC::UnlockEncryptFolderApiJob(_account, folderId, *folderToken, _journal, this); unlockJob->setShouldRollbackMetadataChanges(true); unlockJob->start(); } diff --git a/src/libsync/syncfileitem.cpp b/src/libsync/syncfileitem.cpp index 2746b192cd4bf..04bdba6df222c 100644 --- a/src/libsync/syncfileitem.cpp +++ b/src/libsync/syncfileitem.cpp @@ -118,6 +118,8 @@ SyncJournalFileRecord SyncFileItem::toSyncJournalFileRecordWithInode(const QStri rec._checksumHeader = _checksumHeader; rec._e2eMangledName = _encryptedFileName.toUtf8(); rec._e2eEncryptionStatus = EncryptionStatusEnums::toDbEncryptionStatus(_e2eEncryptionStatus); + rec._e2eCertificateFingerprint = _e2eCertificateFingerprint; + //Q_ASSERT(rec._e2eEncryptionStatus == SyncJournalFileRecord::EncryptionStatus::NotEncrypted || !rec._e2eCertificateFingerprint.isEmpty()); rec._lockstate._locked = _locked == LockStatus::LockedItem; rec._lockstate._lockOwnerDisplayName = _lockOwnerDisplayName; rec._lockstate._lockOwnerId = _lockOwnerId; @@ -158,6 +160,8 @@ SyncFileItemPtr SyncFileItem::fromSyncJournalFileRecord(const SyncJournalFileRec item->_encryptedFileName = rec.e2eMangledName(); item->_e2eEncryptionStatus = EncryptionStatusEnums::fromDbEncryptionStatus(rec._e2eEncryptionStatus); item->_e2eEncryptionServerCapability = item->_e2eEncryptionStatus; + //Q_ASSERT(rec._e2eEncryptionStatus == SyncJournalFileRecord::EncryptionStatus::NotEncrypted || !rec._e2eCertificateFingerprint.isEmpty()); + item->_e2eCertificateFingerprint = rec._e2eCertificateFingerprint; 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 46ee49621c688..71dc973875dac 100644 --- a/src/libsync/syncfileitem.h +++ b/src/libsync/syncfileitem.h @@ -286,6 +286,7 @@ class OWNCLOUDSYNC_EXPORT SyncFileItem EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted; // The file is E2EE or the content of the directory should be E2EE EncryptionStatus _e2eEncryptionServerCapability = EncryptionStatus::NotEncrypted; EncryptionStatus _e2eEncryptionStatusRemote = EncryptionStatus::NotEncrypted; + QByteArray _e2eCertificateFingerprint; quint16 _httpErrorCode = 0; RemotePermissions _remotePerm; QString _errorString; // Contains a string only in case of error diff --git a/src/libsync/updatee2eefoldermetadatajob.cpp b/src/libsync/updatee2eefoldermetadatajob.cpp index 5ea537c816261..ecfa7c5d104fe 100644 --- a/src/libsync/updatee2eefoldermetadatajob.cpp +++ b/src/libsync/updatee2eefoldermetadatajob.cpp @@ -80,7 +80,7 @@ void UpdateE2eeFolderMetadataJob::slotFetchMetadataJobFinished(int httpReturnCod qCDebug(lcUpdateFileDropMetadataJob()) << "Error Getting the encrypted metadata."; _item->_status = SyncFileItem::FatalError; _item->_errorString = message; - finished(SyncFileItem::FatalError); + emit finished(SyncFileItem::FatalError); return; } @@ -115,7 +115,7 @@ void UpdateE2eeFolderMetadataJob::slotUpdateMetadataFinished(int httpReturnCode, propagator()->_journal->schedulePathForRemoteDiscovery(_item->_file); propagator()->_anotherSyncNeeded = true; _item->_status = itemStatus; - finished(itemStatus); + emit finished(itemStatus); } void UpdateE2eeFolderMetadataJob::unlockFolder(const EncryptedFolderMetadataHandler::UnlockFolderWithResult result) @@ -143,17 +143,17 @@ void UpdateE2eeFolderMetadataJob::unlockFolder(const EncryptedFolderMetadataHand _item->_e2eEncryptionServerCapability = EncryptionStatusEnums::fromEndToEndEncryptionApiVersion(propagator()->account()->capabilities().clientSideEncryptionVersion()); } } - finished(itemStatus); + emit finished(itemStatus); return; } qCDebug(lcUpdateFileDropMetadataJob) << "Calling Unlock"; - connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, [this](const QByteArray &folderId, int httpStatus) { + connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, _encryptedFolderMetadataHandler.data(), [this](const QByteArray &folderId, int httpStatus) { if (httpStatus != 200) { qCWarning(lcUpdateFileDropMetadataJob) << "Unlock Error" << folderId << httpStatus; propagator()->account()->reportClientStatus(OCC::ClientStatusReportingStatus::E2EeError_GeneralError); _item->_errorString = tr("Failed to unlock encrypted folder."); - finished(SyncFileItem::FatalError); + emit finished(SyncFileItem::FatalError); return; } @@ -163,14 +163,14 @@ void UpdateE2eeFolderMetadataJob::unlockFolder(const EncryptedFolderMetadataHand || !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) { qCWarning(lcUpdateFileDropMetadataJob) << "Failed to finalize item. Invalid metadata."; _item->_errorString = tr("Failed to finalize item."); - finished(SyncFileItem::FatalError); + emit finished(SyncFileItem::FatalError); return; } _item->_e2eEncryptionStatus = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus(); _item->_e2eEncryptionStatusRemote = _encryptedFolderMetadataHandler->folderMetadata()->encryptedMetadataEncryptionStatus(); - finished(SyncFileItem::Success); + emit finished(SyncFileItem::Success); }); _encryptedFolderMetadataHandler->unlockFolder(result); } diff --git a/src/libsync/updatee2eefolderusersmetadatajob.cpp b/src/libsync/updatee2eefolderusersmetadatajob.cpp index c6bfc07c049b9..457fb19b1ff3d 100644 --- a/src/libsync/updatee2eefolderusersmetadatajob.cpp +++ b/src/libsync/updatee2eefolderusersmetadatajob.cpp @@ -62,7 +62,7 @@ void UpdateE2eeFolderUsersMetadataJob::start(const bool keepLock) if (keepLock) { connect(_encryptedFolderMetadataHandler.data(), &EncryptedFolderMetadataHandler::folderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::deleteLater); } else { - connect(this, &UpdateE2eeFolderUsersMetadataJob::slotFolderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::deleteLater); + connect(this, &UpdateE2eeFolderUsersMetadataJob::folderUnlocked, this, &UpdateE2eeFolderUsersMetadataJob::deleteLater); } _keepLock = keepLock; if (_operation != Operation::Add && _operation != Operation::Remove && _operation != Operation::ReEncrypt) { @@ -115,7 +115,7 @@ void UpdateE2eeFolderUsersMetadataJob::slotFetchMetadataJobFinished(int statusCo } if (!_encryptedFolderMetadataHandler->folderMetadata() || !_encryptedFolderMetadataHandler->folderMetadata()->isValid()) { - emit finished(403, tr("Could not add or remove user %1 to access folder %2").arg(_folderUserId).arg(_fullRemotePath)); + emit finished(403, tr("Could not add or remove user %1 to access folder %2").arg(_folderUserId, _fullRemotePath)); return; } startUpdate(); @@ -179,7 +179,7 @@ void UpdateE2eeFolderUsersMetadataJob::slotUpdateMetadataFinished(int code, cons unlockFolder(EncryptedFolderMetadataHandler::UnlockFolderWithResult::Success); } } else { - _subJobs.values().last()->start(); + (*_subJobs.begin())->start(); } } else { emit finished(200); @@ -276,7 +276,7 @@ void UpdateE2eeFolderUsersMetadataJob::slotSubJobFinished(int code, const QStrin if (_subJobs.isEmpty()) { subJobsFinished(true); } else { - _subJobs.values().last()->start(); + (*_subJobs.begin())->start(); } } diff --git a/src/libsync/updatemigratede2eemetadatajob.cpp b/src/libsync/updatemigratede2eemetadatajob.cpp index 1dbf5bdddf45e..744bc1691d06b 100644 --- a/src/libsync/updatemigratede2eemetadatajob.cpp +++ b/src/libsync/updatemigratede2eemetadatajob.cpp @@ -32,7 +32,7 @@ UpdateMigratedE2eeMetadataJob::UpdateMigratedE2eeMetadataJob(OwncloudPropagator const SyncFileItemPtr &syncFileItem, const QString &fullRemotePath, const QString &folderRemotePath) - : PropagatorJob(propagator) + : PropagateItemJob(propagator, syncFileItem) , _item(syncFileItem) , _fullRemotePath(fullRemotePath) , _folderRemotePath(Utility::noLeadingSlashPath(Utility::noTrailingSlashPath(folderRemotePath))) @@ -48,7 +48,7 @@ void UpdateMigratedE2eeMetadataJob::start() UpdateE2eeFolderUsersMetadataJob::Add, _fullRemotePath, propagator()->account()->davUser(), - propagator()->account()->e2e()->_certificate); + propagator()->account()->e2e()->getCertificate()); updateMedatadaAndSubfoldersJob->setParent(this); updateMedatadaAndSubfoldersJob->setSubJobSyncItems(_subJobItems); _subJobItems.clear(); @@ -57,6 +57,8 @@ void UpdateMigratedE2eeMetadataJob::start() if (code == 200) { _item->_e2eEncryptionStatus = updateMedatadaAndSubfoldersJob->encryptionStatus(); _item->_e2eEncryptionStatusRemote = updateMedatadaAndSubfoldersJob->encryptionStatus(); + _item->_e2eCertificateFingerprint = propagator()->account()->encryptionCertificateFingerprint(); + propagator()->updateMetadata(*_item, Vfs::UpdateMetadataType::DatabaseMetadata); emit finished(SyncFileItem::Status::Success); } else { _item->_errorString = message; diff --git a/src/libsync/updatemigratede2eemetadatajob.h b/src/libsync/updatemigratede2eemetadatajob.h index bf15973f2790c..6d2da78670786 100644 --- a/src/libsync/updatemigratede2eemetadatajob.h +++ b/src/libsync/updatemigratede2eemetadatajob.h @@ -22,12 +22,12 @@ namespace OCC { class FolderMetadata; -class OWNCLOUDSYNC_EXPORT UpdateMigratedE2eeMetadataJob : public PropagatorJob +class OWNCLOUDSYNC_EXPORT UpdateMigratedE2eeMetadataJob : public PropagateItemJob { Q_OBJECT public: - explicit UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &syncFileItem, const QString &path, const QString &folderRemotePath); + explicit UpdateMigratedE2eeMetadataJob(OwncloudPropagator *propagator, const SyncFileItemPtr &syncFileItem, const QString &fullRemotePath, const QString &folderRemotePath); [[nodiscard]] bool scheduleSelfOrChild() override; diff --git a/test/testclientsideencryptionv2.cpp b/test/testclientsideencryptionv2.cpp index 9b2304d0251b6..96ef34ddb3559 100644 --- a/test/testclientsideencryptionv2.cpp +++ b/test/testclientsideencryptionv2.cpp @@ -89,13 +89,11 @@ private slots: QVERIFY(!publicKey.isNull()); QVERIFY(!privateKey.isEmpty()); - _account->e2e()->_certificate = cert; - _account->e2e()->_publicKey = publicKey; - _account->e2e()->_privateKey = privateKey; + _account->e2e()->setCertificate(cert); + _account->e2e()->setPrivateKey(privateKey); - _secondAccount->e2e()->_certificate = cert; - _secondAccount->e2e()->_publicKey = publicKey; - _secondAccount->e2e()->_privateKey = privateKey; + _secondAccount->e2e()->setCertificate(cert); + _secondAccount->e2e()->setPrivateKey(privateKey); } @@ -137,10 +135,11 @@ private slots: } const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto certificate = QSslCertificate{certificatePem}; const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); if (!encryptedMetadataKey.isEmpty()) { - const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256)); if (decryptedMetadataKey.isEmpty()) { break; } @@ -246,11 +245,11 @@ private slots: encryptedFile.initializationVector = EncryptionHelper::generateRandom(16); metadata->addEncryptedFile(encryptedFile); - QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate)); + QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->getCertificate())); QVERIFY(metadata->removeUser(_secondAccount->davUser())); - QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->_certificate)); + QVERIFY(metadata->addUser(_secondAccount->davUser(), _secondAccount->e2e()->getCertificate())); const auto encryptedMetadata = metadata->encryptedMetadata(); QVERIFY(!encryptedMetadata.isEmpty()); @@ -273,10 +272,11 @@ private slots: } const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto certificate = QSslCertificate{certificatePem}; const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); if (!encryptedMetadataKey.isEmpty()) { - const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256)); if (decryptedMetadataKey.isEmpty()) { break; } @@ -371,10 +371,11 @@ private slots: } const auto certificatePem = folderUserObject.value("certificate").toString().toUtf8(); + const auto certificate = QSslCertificate{certificatePem}; const auto encryptedMetadataKey = QByteArray::fromBase64(folderUserObject.value("encryptedMetadataKey").toString().toUtf8()); if (!encryptedMetadataKey.isEmpty()) { - const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey); + const auto decryptedMetadataKey = metadata->decryptDataWithPrivateKey(encryptedMetadataKey, certificate.digest(QCryptographicHash::Sha256)); if (decryptedMetadataKey.isEmpty()) { break; } diff --git a/test/testsecurefiledrop.cpp b/test/testsecurefiledrop.cpp index a46871660d97d..a5fba860d20af 100644 --- a/test/testsecurefiledrop.cpp +++ b/test/testsecurefiledrop.cpp @@ -80,9 +80,8 @@ private slots: QVERIFY(!publicKey.isNull()); QVERIFY(!privateKey.isEmpty()); - _account->e2e()->_certificate = cert; - _account->e2e()->_publicKey = publicKey; - _account->e2e()->_privateKey = privateKey; + _account->e2e()->setCertificate(cert); + _account->e2e()->setPrivateKey(privateKey); QScopedPointer metadata(new FolderMetadata(_account, "/", FolderMetadata::FolderType::Root)); QSignalSpy metadataSetupCompleteSpy(metadata.data(), &FolderMetadata::setupComplete);