Skip to content

Commit

Permalink
add support to use PKCS#11 harware token to store certifice for e2ee
Browse files Browse the repository at this point in the history
Close #5685

Signed-off-by: Matthieu Gallien <[email protected]>
  • Loading branch information
mgallien committed Apr 26, 2024
1 parent 810d8f1 commit de56d5f
Show file tree
Hide file tree
Showing 56 changed files with 2,207 additions and 328 deletions.
1 change: 0 additions & 1 deletion .drone.yml
Original file line number Diff line number Diff line change
Expand Up @@ -208,4 +208,3 @@ trigger:
kind: signature
hmac: 418915bcd7a880be3656a8c09aef0f6e7b3721d13f196838f8c5a9de6020f786

...
27 changes: 16 additions & 11 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -227,20 +227,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 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()

if (APPLE)
Expand Down
1 change: 0 additions & 1 deletion NEXTCLOUD.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -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"
#
Expand Down
4 changes: 4 additions & 0 deletions config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,8 @@

#cmakedefine CFAPI_SHELL_EXTENSIONS_LIB_NAME "@CFAPI_SHELL_EXTENSIONS_LIB_NAME@"

#cmakedefine01 CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN

#cmakedefine ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "@ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH@"

#endif
2 changes: 2 additions & 0 deletions resources.qrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<file>src/gui/PredefinedStatusButton.qml</file>
<file>src/gui/BasicComboBox.qml</file>
<file>src/gui/ErrorBox.qml</file>
<file>src/gui/EncryptionTokenSelectionWindow.qml</file>
<file>src/gui/filedetails/FileActivityView.qml</file>
<file>src/gui/filedetails/FileDetailsPage.qml</file>
<file>src/gui/filedetails/FileDetailsView.qml</file>
Expand Down Expand Up @@ -48,6 +49,7 @@
<file>src/gui/tray/TalkReplyTextField.qml</file>
<file>src/gui/tray/CallNotificationDialog.qml</file>
<file>src/gui/tray/EditFileLocallyLoadingDialog.qml</file>
<file>src/gui/tray/EncryptionTokenDiscoveryDialog.qml</file>
<file>src/gui/tray/NCBusyIndicator.qml</file>
<file>src/gui/tray/NCIconWithBackgroundImage.qml</file>
<file>src/gui/tray/NCToolTip.qml</file>
Expand Down
50 changes: 27 additions & 23 deletions src/common/syncjournaldb.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ 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, " \
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " \
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, isShared, lastShareStateFetchedTimestmap, sharedByMe" \
" FROM metadata" \
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
Expand All @@ -67,16 +67,18 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
rec._checksumHeader = query.baValue(9);
rec._e2eMangledName = query.baValue(10);
rec._e2eEncryptionStatus = static_cast<SyncJournalFileRecord::EncryptionStatus>(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._isShared = query.intValue(19) > 0;
rec._lastShareStateFetchedTimestamp = query.int64Value(20);
rec._sharedByMe = query.intValue(21) > 0;
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._isShared = query.intValue(20) > 0;
rec._lastShareStateFetchedTimestamp = query.int64Value(21);
rec._sharedByMe = query.intValue(22) > 0;
}

static QByteArray defaultJournalMode(const QString &dbPath)
Expand Down Expand Up @@ -777,6 +779,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"));
Expand Down Expand Up @@ -983,9 +986,9 @@ Result<void, QString> 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, isShared, lastShareStateFetchedTimestmap, sharedByMe) "
"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);"),
"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);"),
_db);
if (!query) {
return query->error();
Expand All @@ -1009,16 +1012,17 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
query->bindValue(16, contentChecksumTypeId);
query->bindValue(17, record._e2eMangledName);
query->bindValue(18, static_cast<int>(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._isShared);
query->bindValue(27, record._lastShareStateFetchedTimestamp);
query->bindValue(28, record._sharedByMe);
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._isShared);
query->bindValue(28, record._lastShareStateFetchedTimestamp);
query->bindValue(29, record._sharedByMe);

if (!query->exec()) {
return query->error();
Expand Down
1 change: 1 addition & 0 deletions src/common/syncjournalfilerecord.h
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ class OCSYNC_EXPORT SyncJournalFileRecord
QByteArray _checksumHeader;
QByteArray _e2eMangledName;
EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted;
QByteArray _e2eCertificateFingerprint;
SyncJournalFileLockInfo _lockstate;
bool _isShared = false;
qint64 _lastShareStateFetchedTimestamp = 0;
Expand Down
37 changes: 19 additions & 18 deletions src/csync/csync.h
Original file line number Diff line number Diff line change
Expand Up @@ -138,24 +138,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)
Expand Down
1 change: 1 addition & 0 deletions src/gui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ set(client_SRCS
syncrunfilelog.cpp
systray.h
systray.cpp
EncryptionTokenSelectionWindow.qml
thumbnailjob.h
thumbnailjob.cpp
userinfo.h
Expand Down
146 changes: 146 additions & 0 deletions src/gui/EncryptionTokenSelectionWindow.qml
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/*
* Copyright (C) 2023 by Matthieu Gallien <[email protected]>
*
* 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
}
}
4 changes: 4 additions & 0 deletions src/gui/accountmanager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ constexpr auto serverVersionC = "serverVersion";
constexpr auto serverColorC = "serverColor";
constexpr auto serverTextColorC = "serverTextColor";
constexpr auto skipE2eeMetadataChecksumValidationC = "skipE2eeMetadataChecksumValidation";
constexpr auto encryptionCertificateSha256FingerprintC = "encryptionCertificateSha256Fingerprint";
constexpr auto generalC = "General";

constexpr auto dummyAuthTypeC = "dummy";
Expand Down Expand Up @@ -322,6 +323,7 @@ void AccountManager::saveAccountHelper(Account *acc, QSettings &settings, bool s
settings.setValue(QLatin1String(serverVersionC), acc->_serverVersion);
settings.setValue(QLatin1String(serverColorC), acc->_serverColor);
settings.setValue(QLatin1String(serverTextColorC), acc->_serverTextColor);
settings.setValue(QLatin1String(encryptionCertificateSha256FingerprintC), acc->encryptionCertificateFingerprint());
if (!acc->_skipE2eeMetadataChecksumValidation) {
settings.remove(QLatin1String(skipE2eeMetadataChecksumValidationC));
} else {
Expand Down Expand Up @@ -448,6 +450,8 @@ AccountPtr AccountManager::loadAccountHelper(QSettings &settings)

acc->setCredentials(CredentialsFactory::create(authType));

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());
Expand Down
Loading

0 comments on commit de56d5f

Please sign in to comment.