Skip to content

Commit e170b0c

Browse files
authored
Merge pull request #5877 from nextcloud/feature/e2eeUseHardwareTokenSecureStorage
Feature/e2ee use hardware token secure storage
2 parents b24626a + 8a88bfb commit e170b0c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2525
-456
lines changed

CMakeLists.txt

+16-11
Original file line numberDiff line numberDiff line change
@@ -233,20 +233,25 @@ if(BUILD_CLIENT)
233233
find_package(Sphinx)
234234
find_package(PdfLatex)
235235
find_package(OpenSSL 1.1 REQUIRED )
236+
find_package(PkgConfig REQUIRED)
237+
pkg_check_modules(OPENSC-LIBP11 libp11 REQUIRED IMPORTED_TARGET)
236238

237-
find_package(ZLIB REQUIRED)
238-
find_package(SQLite3 3.9.0 REQUIRED)
239+
set(ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "" CACHE PATH "Path to the driver for end-to-end encryption token")
240+
option(CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN "Enforce use of an hardware token for end-to-end encryption" false)
239241

240-
if(NOT WIN32 AND NOT APPLE)
241-
find_package(PkgConfig REQUIRED)
242-
pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET)
242+
find_package(ZLIB REQUIRED)
243+
find_package(SQLite3 3.9.0 REQUIRED)
243244

244-
if(CLOUDPROVIDERS_FOUND)
245-
pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET)
246-
pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET)
247-
pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET)
248-
endif()
249-
endif()
245+
if(NOT WIN32 AND NOT APPLE)
246+
find_package(PkgConfig REQUIRED)
247+
pkg_check_modules(CLOUDPROVIDERS cloudproviders IMPORTED_TARGET)
248+
249+
if(CLOUDPROVIDERS_FOUND)
250+
pkg_check_modules(DBUS-1 REQUIRED dbus-1 IMPORTED_TARGET)
251+
pkg_check_modules(GIO REQUIRED gio-2.0 IMPORTED_TARGET)
252+
pkg_check_modules(GLIB2 REQUIRED glib-2.0 IMPORTED_TARGET)
253+
endif()
254+
endif()
250255
endif()
251256

252257
option(BUILD_WITH_WEBENGINE "BUILD_WITH_WEBENGINE" ON)

NEXTCLOUD.cmake

-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@ set( APPLICATION_WIZARD_HEADER_BACKGROUND_COLOR ${NEXTCLOUD_BACKGROUND_COLOR} CA
6363
set( APPLICATION_WIZARD_HEADER_TITLE_COLOR "#ffffff" CACHE STRING "Hex color of the text in the wizard header")
6464
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 )
6565

66-
6766
#
6867
## Windows Shell Extensions & MSI - IMPORTANT: Generate new GUIDs for custom builds with "guidgen" or "uuidgen"
6968
#

config.h.in

+4
Original file line numberDiff line numberDiff line change
@@ -66,4 +66,8 @@
6666

6767
#cmakedefine WITH_WEBENGINE
6868

69+
#cmakedefine01 CLIENTSIDEENCRYPTION_ENFORCE_USB_TOKEN
70+
71+
#cmakedefine ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH "@ENCRYPTION_HARDWARE_TOKEN_DRIVER_PATH@"
72+
6973
#endif

resources.qrc

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
<file>src/gui/UserStatusSelectorButton.qml</file>
77
<file>src/gui/PredefinedStatusButton.qml</file>
88
<file>src/gui/ErrorBox.qml</file>
9+
<file>src/gui/EncryptionTokenSelectionWindow.qml</file>
910
<file>src/gui/filedetails/FileActivityView.qml</file>
1011
<file>src/gui/filedetails/FileDetailsPage.qml</file>
1112
<file>src/gui/filedetails/FileDetailsView.qml</file>
@@ -45,6 +46,7 @@
4546
<file>src/gui/tray/TalkReplyTextField.qml</file>
4647
<file>src/gui/tray/CallNotificationDialog.qml</file>
4748
<file>src/gui/tray/EditFileLocallyLoadingDialog.qml</file>
49+
<file>src/gui/tray/EncryptionTokenDiscoveryDialog.qml</file>
4850
<file>src/gui/tray/NCBusyIndicator.qml</file>
4951
<file>src/gui/tray/NCIconWithBackgroundImage.qml</file>
5052
<file>src/gui/tray/NCToolTip.qml</file>

src/common/syncjournaldb.cpp

+35-31
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ Q_LOGGING_CATEGORY(lcDb, "nextcloud.sync.database", QtInfoMsg)
4848

4949
#define GET_FILE_RECORD_QUERY \
5050
"SELECT path, inode, modtime, type, md5, fileid, remotePerm, filesize," \
51-
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, " \
52-
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile" \
51+
" ignoredChildrenRemote, contentchecksumtype.name || ':' || contentChecksum, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, " \
52+
" lock, lockOwnerDisplayName, lockOwnerId, lockType, lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, " \
53+
" sharedByMe, isLivePhoto, livePhotoFile" \
5354
" FROM metadata" \
5455
" LEFT JOIN checksumtype as contentchecksumtype ON metadata.contentChecksumTypeId == contentchecksumtype.id"
5556

@@ -67,19 +68,20 @@ static void fillFileRecordFromGetQuery(SyncJournalFileRecord &rec, SqlQuery &que
6768
rec._checksumHeader = query.baValue(9);
6869
rec._e2eMangledName = query.baValue(10);
6970
rec._e2eEncryptionStatus = static_cast<SyncJournalFileRecord::EncryptionStatus>(query.intValue(11));
70-
rec._lockstate._locked = query.intValue(12) > 0;
71-
rec._lockstate._lockOwnerDisplayName = query.stringValue(13);
72-
rec._lockstate._lockOwnerId = query.stringValue(14);
73-
rec._lockstate._lockOwnerType = query.int64Value(15);
74-
rec._lockstate._lockEditorApp = query.stringValue(16);
75-
rec._lockstate._lockTime = query.int64Value(17);
76-
rec._lockstate._lockTimeout = query.int64Value(18);
77-
rec._lockstate._lockToken = query.stringValue(19);
78-
rec._isShared = query.intValue(20) > 0;
79-
rec._lastShareStateFetchedTimestamp = query.int64Value(21);
80-
rec._sharedByMe = query.intValue(22) > 0;
81-
rec._isLivePhoto = query.intValue(23) > 0;
82-
rec._livePhotoFile = query.stringValue(24);
71+
rec._e2eCertificateFingerprint = query.baValue(12);
72+
rec._lockstate._locked = query.intValue(13) > 0;
73+
rec._lockstate._lockOwnerDisplayName = query.stringValue(14);
74+
rec._lockstate._lockOwnerId = query.stringValue(15);
75+
rec._lockstate._lockOwnerType = query.int64Value(16);
76+
rec._lockstate._lockEditorApp = query.stringValue(17);
77+
rec._lockstate._lockTime = query.int64Value(18);
78+
rec._lockstate._lockTimeout = query.int64Value(19);
79+
rec._lockstate._lockToken = query.stringValue(20);
80+
rec._isShared = query.intValue(21) > 0;
81+
rec._lastShareStateFetchedTimestamp = query.int64Value(22);
82+
rec._sharedByMe = query.intValue(23) > 0;
83+
rec._isLivePhoto = query.intValue(24) > 0;
84+
rec._livePhotoFile = query.stringValue(25);
8385
}
8486

8587
static QByteArray defaultJournalMode(const QString &dbPath)
@@ -783,6 +785,7 @@ bool SyncJournalDb::updateMetadataTableStructure()
783785
addColumn(QStringLiteral("contentChecksumTypeId"), QStringLiteral("INTEGER"));
784786
addColumn(QStringLiteral("e2eMangledName"), QStringLiteral("TEXT"));
785787
addColumn(QStringLiteral("isE2eEncrypted"), QStringLiteral("INTEGER"));
788+
addColumn(QStringLiteral("e2eCertificateFingerprint"), QStringLiteral("TEXT"));
786789
addColumn(QStringLiteral("isShared"), QStringLiteral("INTEGER"));
787790
addColumn(QStringLiteral("lastShareStateFetchedTimestmap"), QStringLiteral("INTEGER"));
788791
addColumn(QStringLiteral("sharedByMe"), QStringLiteral("INTEGER"));
@@ -995,9 +998,9 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
995998

996999
const auto query = _queryManager.get(PreparedSqlQueryManager::SetFileRecordQuery, QByteArrayLiteral("INSERT OR REPLACE INTO metadata "
9971000
"(phash, pathlen, path, inode, uid, gid, mode, modtime, type, md5, fileid, remotePerm, filesize, ignoredChildrenRemote, "
998-
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
1001+
"contentChecksum, contentChecksumTypeId, e2eMangledName, isE2eEncrypted, e2eCertificateFingerprint, lock, lockType, lockOwnerDisplayName, lockOwnerId, "
9991002
"lockOwnerEditor, lockTime, lockTimeout, lockToken, isShared, lastShareStateFetchedTimestmap, sharedByMe, isLivePhoto, livePhotoFile) "
1000-
"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);"),
1003+
"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);"),
10011004
_db);
10021005
if (!query) {
10031006
qCDebug(lcDb) << "database error:" << query->error();
@@ -1022,19 +1025,20 @@ Result<void, QString> SyncJournalDb::setFileRecord(const SyncJournalFileRecord &
10221025
query->bindValue(16, contentChecksumTypeId);
10231026
query->bindValue(17, record._e2eMangledName);
10241027
query->bindValue(18, static_cast<int>(record._e2eEncryptionStatus));
1025-
query->bindValue(19, record._lockstate._locked ? 1 : 0);
1026-
query->bindValue(20, record._lockstate._lockOwnerType);
1027-
query->bindValue(21, record._lockstate._lockOwnerDisplayName);
1028-
query->bindValue(22, record._lockstate._lockOwnerId);
1029-
query->bindValue(23, record._lockstate._lockEditorApp);
1030-
query->bindValue(24, record._lockstate._lockTime);
1031-
query->bindValue(25, record._lockstate._lockTimeout);
1032-
query->bindValue(26, record._lockstate._lockToken);
1033-
query->bindValue(27, record._isShared);
1034-
query->bindValue(28, record._lastShareStateFetchedTimestamp);
1035-
query->bindValue(29, record._sharedByMe);
1036-
query->bindValue(30, record._isLivePhoto);
1037-
query->bindValue(31, record._livePhotoFile);
1028+
query->bindValue(19, record._e2eCertificateFingerprint);
1029+
query->bindValue(20, record._lockstate._locked ? 1 : 0);
1030+
query->bindValue(21, record._lockstate._lockOwnerType);
1031+
query->bindValue(22, record._lockstate._lockOwnerDisplayName);
1032+
query->bindValue(23, record._lockstate._lockOwnerId);
1033+
query->bindValue(24, record._lockstate._lockEditorApp);
1034+
query->bindValue(25, record._lockstate._lockTime);
1035+
query->bindValue(26, record._lockstate._lockTimeout);
1036+
query->bindValue(27, record._lockstate._lockToken);
1037+
query->bindValue(28, record._isShared);
1038+
query->bindValue(29, record._lastShareStateFetchedTimestamp);
1039+
query->bindValue(30, record._sharedByMe);
1040+
query->bindValue(31, record._isLivePhoto);
1041+
query->bindValue(32, record._livePhotoFile);
10381042

10391043
if (!query->exec()) {
10401044
qCDebug(lcDb) << "database error:" << query->error();
@@ -3035,7 +3039,7 @@ SyncJournalDb::PinStateInterface::rawList()
30353039

30363040
SyncJournalDb::PinStateInterface SyncJournalDb::internalPinStates()
30373041
{
3038-
return {this};
3042+
return PinStateInterface{this};
30393043
}
30403044

30413045
void SyncJournalDb::commit(const QString &context, bool startTrans)

src/common/syncjournaldb.h

+5
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,11 @@ class OCSYNC_EXPORT SyncJournalDb : public QObject
304304
*/
305305
struct OCSYNC_EXPORT PinStateInterface
306306
{
307+
explicit PinStateInterface(SyncJournalDb *db)
308+
: _db(db)
309+
{
310+
}
311+
307312
PinStateInterface(const PinStateInterface &) = delete;
308313
PinStateInterface(PinStateInterface &&) = delete;
309314

src/common/syncjournalfilerecord.h

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class OCSYNC_EXPORT SyncJournalFileRecord
8484
QByteArray _checksumHeader;
8585
QByteArray _e2eMangledName;
8686
EncryptionStatus _e2eEncryptionStatus = EncryptionStatus::NotEncrypted;
87+
QByteArray _e2eCertificateFingerprint;
8788
SyncJournalFileLockInfo _lockstate;
8889
bool _isShared = false;
8990
qint64 _lastShareStateFetchedTimestamp = 0;

src/csync/csync.h

+19-18
Original file line numberDiff line numberDiff line change
@@ -140,24 +140,25 @@ Q_ENUM_NS(csync_status_codes_e)
140140
* the csync state of a file.
141141
*/
142142
enum SyncInstructions {
143-
CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
144-
CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
145-
CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
146-
CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
147-
CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
148-
CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
149-
CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
150-
CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
151-
CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
152-
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
153-
CSYNC_INSTRUCTION_ERROR = 1 << 8,
154-
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
155-
Used when the type of something changes from directory to file
156-
or back. */
157-
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
158-
but without any propagation (UPDATE|RECONCILE) */
159-
CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
160-
CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */
143+
CSYNC_INSTRUCTION_NONE = 0, /* Nothing to do (UPDATE|RECONCILE) */
144+
CSYNC_INSTRUCTION_EVAL = 1 << 0, /* There was changed compared to the DB (UPDATE) */
145+
CSYNC_INSTRUCTION_REMOVE = 1 << 1, /* The file need to be removed (RECONCILE) */
146+
CSYNC_INSTRUCTION_RENAME = 1 << 2, /* The file need to be renamed (RECONCILE) */
147+
CSYNC_INSTRUCTION_EVAL_RENAME = 1 << 11, /* The file is new, it is the destination of a rename (UPDATE) */
148+
CSYNC_INSTRUCTION_NEW = 1 << 3, /* The file is new compared to the db (UPDATE) */
149+
CSYNC_INSTRUCTION_CONFLICT = 1 << 4, /* The file need to be downloaded because it is a conflict (RECONCILE) */
150+
CSYNC_INSTRUCTION_IGNORE = 1 << 5, /* The file is ignored (UPDATE|RECONCILE) */
151+
CSYNC_INSTRUCTION_SYNC = 1 << 6, /* The file need to be pushed to the other remote (RECONCILE) */
152+
CSYNC_INSTRUCTION_STAT_ERROR = 1 << 7,
153+
CSYNC_INSTRUCTION_ERROR = 1 << 8,
154+
CSYNC_INSTRUCTION_TYPE_CHANGE = 1 << 9, /* Like NEW, but deletes the old entity first (RECONCILE)
155+
Used when the type of something changes from directory to file
156+
or back. */
157+
CSYNC_INSTRUCTION_UPDATE_METADATA = 1 << 10, /* If the etag has been updated and need to be writen to the db,
158+
but without any propagation (UPDATE|RECONCILE) */
159+
CSYNC_INSTRUCTION_CASE_CLASH_CONFLICT = 1 << 12, /* The file need to be downloaded because it is a case clash conflict (RECONCILE) */
160+
CSYNC_INSTRUCTION_UPDATE_VFS_METADATA = 1 << 13, /* vfs item metadata are out of sync and we need to tell operating system about it */
161+
CSYNC_INSTRUCTION_UPDATE_ENCRYPTION_METADATA = 1 << 14, /* encryption metadata needs update after certificate was migrated */
161162
};
162163

163164
Q_ENUM_NS(SyncInstructions)

src/gui/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ set(client_SRCS
146146
syncrunfilelog.cpp
147147
systray.h
148148
systray.cpp
149+
EncryptionTokenSelectionWindow.qml
149150
thumbnailjob.h
150151
thumbnailjob.cpp
151152
userinfo.h
+146
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright (C) 2023 by Matthieu Gallien <[email protected]>
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation; either version 2 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but
10+
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
11+
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12+
* for more details.
13+
*/
14+
15+
import QtQuick 2.15
16+
import QtQuick.Layouts 1.15
17+
import QtQuick.Controls 2.15
18+
import QtQml.Models 2.15
19+
20+
import com.nextcloud.desktopclient 1.0
21+
import Style 1.0
22+
23+
import "./tray"
24+
25+
ApplicationWindow {
26+
id: encryptionKeyChooserDialog
27+
28+
required property var certificatesInfo
29+
required property ClientSideEncryptionTokenSelector certificateSelector
30+
property string selectedSerialNumber: ''
31+
32+
flags: Qt.Window | Qt.Dialog
33+
visible: true
34+
modality: Qt.ApplicationModal
35+
36+
width: 400
37+
height: 600
38+
minimumWidth: 400
39+
minimumHeight: 600
40+
41+
title: qsTr('Token Encryption Key Chooser')
42+
43+
// TODO: Rather than setting all these palette colours manually,
44+
// create a custom style and do it for all components globally
45+
palette {
46+
text: Style.ncTextColor
47+
windowText: Style.ncTextColor
48+
buttonText: Style.ncTextColor
49+
brightText: Style.ncTextBrightColor
50+
highlight: Style.lightHover
51+
highlightedText: Style.ncTextColor
52+
light: Style.lightHover
53+
midlight: Style.ncSecondaryTextColor
54+
mid: Style.darkerHover
55+
dark: Style.menuBorder
56+
button: Style.buttonBackgroundColor
57+
window: Style.backgroundColor
58+
base: Style.backgroundColor
59+
toolTipBase: Style.backgroundColor
60+
toolTipText: Style.ncTextColor
61+
}
62+
63+
onClosing: function(close) {
64+
Systray.destroyDialog(self);
65+
close.accepted = true
66+
}
67+
68+
ColumnLayout {
69+
anchors.fill: parent
70+
anchors.leftMargin: 20
71+
anchors.rightMargin: 20
72+
anchors.bottomMargin: 20
73+
anchors.topMargin: 20
74+
spacing: 15
75+
z: 2
76+
77+
EnforcedPlainTextLabel {
78+
text: qsTr("Available Keys for end-to-end Encryption:")
79+
font.bold: true
80+
font.pixelSize: Style.bigFontPixelSizeResolveConflictsDialog
81+
Layout.fillWidth: true
82+
}
83+
84+
ScrollView {
85+
Layout.fillWidth: true
86+
Layout.fillHeight: true
87+
88+
clip: true
89+
90+
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
91+
92+
ListView {
93+
id: tokensListView
94+
95+
currentIndex: -1
96+
97+
model: DelegateModel {
98+
model: certificatesInfo
99+
100+
delegate: ItemDelegate {
101+
width: tokensListView.contentItem.width
102+
103+
text: modelData.subject
104+
105+
highlighted: tokensListView.currentIndex === index
106+
107+
onClicked: function()
108+
{
109+
tokensListView.currentIndex = index
110+
selectedSerialNumber = modelData.serialNumber
111+
}
112+
}
113+
}
114+
}
115+
}
116+
117+
DialogButtonBox {
118+
Layout.fillWidth: true
119+
120+
Button {
121+
text: qsTr("Choose")
122+
DialogButtonBox.buttonRole: DialogButtonBox.AcceptRole
123+
}
124+
Button {
125+
text: qsTr("Cancel")
126+
DialogButtonBox.buttonRole: DialogButtonBox.RejectRole
127+
}
128+
129+
onAccepted: function() {
130+
Systray.destroyDialog(encryptionKeyChooserDialog)
131+
certificateSelector.serialNumber = selectedSerialNumber
132+
}
133+
134+
onRejected: function() {
135+
Systray.destroyDialog(encryptionKeyChooserDialog)
136+
certificateSelector.serialNumber = ''
137+
}
138+
}
139+
}
140+
141+
Rectangle {
142+
color: Style.backgroundColor
143+
anchors.fill: parent
144+
z: 1
145+
}
146+
}

0 commit comments

Comments
 (0)