mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Add basic support for WebAuthn (Passkeys) (#8825)
--------- Co-authored-by: varjolintu <sami.vanttinen@protonmail.com> Co-authored-by: droidmonkey <support@dmapps.us>
This commit is contained in:
parent
378c2992cd
commit
6f2354c0e9
@ -53,6 +53,7 @@ set(WITH_XC_ALL OFF CACHE BOOL "Build in all available plugins")
|
||||
option(WITH_XC_AUTOTYPE "Include Auto-Type." ON)
|
||||
option(WITH_XC_NETWORKING "Include networking code (e.g. for downloading website icons)." OFF)
|
||||
option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF)
|
||||
option(WITH_XC_BROWSER_PASSKEYS "Passkeys support for browser integration." OFF)
|
||||
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
|
||||
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
|
||||
option(WITH_XC_KEESHARE "Sharing integration with KeeShare" OFF)
|
||||
@ -98,6 +99,7 @@ if(WITH_XC_ALL)
|
||||
set(WITH_XC_AUTOTYPE ON)
|
||||
set(WITH_XC_NETWORKING ON)
|
||||
set(WITH_XC_BROWSER ON)
|
||||
set(WITH_XC_BROWSER_PASSKEYS ON)
|
||||
set(WITH_XC_YUBIKEY ON)
|
||||
set(WITH_XC_SSHAGENT ON)
|
||||
set(WITH_XC_KEESHARE ON)
|
||||
@ -514,6 +516,12 @@ if(Qt5Core_VERSION VERSION_LESS "5.2.0")
|
||||
message(FATAL_ERROR "Qt version 5.2.0 or higher is required")
|
||||
endif()
|
||||
|
||||
# CBOR for Passkeys requires Qt 5.12
|
||||
if(Qt5Core_VERSION VERSION_LESS "5.12.0")
|
||||
message(STATUS "Qt version 5.12.0 or higher is required for Passkeys support")
|
||||
set(WITH_XC_BROWSER_PASSKEYS OFF)
|
||||
endif()
|
||||
|
||||
get_filename_component(Qt5_PREFIX ${Qt5_DIR}/../../.. REALPATH)
|
||||
if(APPLE)
|
||||
# Add includes under Qt5 Prefix in case Qt6 is also installed
|
||||
|
3
COPYING
3
COPYING
@ -1,5 +1,5 @@
|
||||
KeePassXC - http://www.keepassxc.org/
|
||||
Copyright (C) 2016-2020 KeePassXC Team <team@keepassxc.org>
|
||||
Copyright (C) 2016-2023 KeePassXC Team <team@keepassxc.org>
|
||||
|
||||
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
|
||||
@ -194,6 +194,7 @@ Files: share/icons/application/scalable/actions/application-exit.svg
|
||||
share/icons/application/scalable/actions/object-unlocked.svg
|
||||
share/icons/application/scalable/actions/paperclip.svg
|
||||
share/icons/application/scalable/actions/password-copy.svg
|
||||
share/icons/application/scalable/actions/passkey.svg
|
||||
share/icons/application/scalable/actions/password-generator.svg
|
||||
share/icons/application/scalable/actions/password-show-off.svg
|
||||
share/icons/application/scalable/actions/password-show-on.svg
|
||||
|
@ -112,6 +112,7 @@ KeePassXC comes with a variety of build options that can turn on/off features. M
|
||||
-DWITH_XC_AUTOTYPE=[ON|OFF] Enable/Disable Auto-Type (default: ON)
|
||||
-DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF)
|
||||
-DWITH_XC_BROWSER=[ON|OFF] Enable/Disable KeePassXC-Browser extension support (default: OFF)
|
||||
-DWITH_XC_BROWSER_PASSKEYS=[ON|OFF] Enable/Disable Passkeys support for browser integration (default: OFF)
|
||||
-DWITH_XC_NETWORKING=[ON|OFF] Enable/Disable Networking support (e.g., favicon downloading) (default: OFF)
|
||||
-DWITH_XC_SSHAGENT=[ON|OFF] Enable/Disable SSHAgent support (default: OFF)
|
||||
-DWITH_XC_FDOSECRETS=[ON|OFF] (Linux Only) Enable/Disable Freedesktop.org Secrets Service support (default:OFF)
|
||||
|
1
share/icons/application/scalable/actions/passkey.svg
Normal file
1
share/icons/application/scalable/actions/passkey.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21,11C21,16.55 17.16,21.74 12,23C6.84,21.74 3,16.55 3,11V5L12,1L21,5V11M12,21C15.75,20 19,15.54 19,11.22V6.3L12,3.18L5,6.3V11.22C5,15.54 8.25,20 12,21M12,6A3,3 0 0,1 15,9C15,10.31 14.17,11.42 13,11.83V14H15V16H13V18H11V11.83C9.83,11.42 9,10.31 9,9A3,3 0 0,1 12,6M12,8A1,1 0 0,0 11,9A1,1 0 0,0 12,10A1,1 0 0,0 13,9A1,1 0 0,0 12,8Z" /></svg>
|
After Width: | Height: | Size: 409 B |
@ -59,6 +59,7 @@
|
||||
<file>application/scalable/actions/object-locked.svg</file>
|
||||
<file>application/scalable/actions/object-unlocked.svg</file>
|
||||
<file>application/scalable/actions/paperclip.svg</file>
|
||||
<file>application/scalable/actions/passkey.svg</file>
|
||||
<file>application/scalable/actions/password-copy.svg</file>
|
||||
<file>application/scalable/actions/password-generator.svg</file>
|
||||
<file>application/scalable/actions/password-show-off.svg</file>
|
||||
|
@ -827,10 +827,6 @@ Ctrl+4 - Use Virtual Keyboard (Windows Only)</p></source>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserEntrySaveDialog</name>
|
||||
<message>
|
||||
<source>KeePassXC-Browser Save Entry</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Ok</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -844,6 +840,65 @@ Ctrl+4 - Use Virtual Keyboard (Windows Only)</p></source>
|
||||
Please select the correct database for saving credentials.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>KeePassXC - Select Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserPasskeysConfirmationDialog</name>
|
||||
<message>
|
||||
<source>KeePassXC: Passkey credentials</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Update</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Authenticate</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Register new</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Register</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Timeout in <b>%n</b> seconds...</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to register Passkey for:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>%1 (%2)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Existing Passkey found.
|
||||
Do you want to register a new Passkey for:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select the existing Passkey and press Update to replace it.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Authenticate Passkey credentials for:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserService</name>
|
||||
@ -900,6 +955,10 @@ Do you want to delete the entry?
|
||||
</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>%1 (Passkey)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>BrowserSettingsWidget</name>
|
||||
@ -5474,6 +5533,18 @@ We recommend you use the AppImage available on our downloads page.</source>
|
||||
<source>Allow Screen Capture</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passkeys…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passkeys</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Import Passkey</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ManageDatabase</name>
|
||||
@ -5859,6 +5930,152 @@ We recommend you use the AppImage available on our downloads page.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasskeyExportDialog</name>
|
||||
<message>
|
||||
<source>KeePassXC - Passkey Export</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export the following Passkey entries.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Filenames will be generated with title and .passkey file extension.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export entries</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export Selected</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Export to folder</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasskeyExporter</name>
|
||||
<message>
|
||||
<source>KeePassXC: Passkey Export</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>File "%1.passkey" already exists.
|
||||
Do you want to overwrite it?
|
||||
</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot open file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot open file "%1" for writing.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot write to file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasskeyImportDialog</name>
|
||||
<message>
|
||||
<source>KeePassXC - Passkey Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Do you want to import the Passkey?</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>URL: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Username: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Use default group (Imported Passkeys)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Group</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Select Database</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Import Passkey</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cancel</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Database: %1</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Group:</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasskeyImporter</name>
|
||||
<message>
|
||||
<source>Passkey file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>All files</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Open Passkey file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot open file</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot open file "%1" for reading.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot import Passkey</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot import Passkey file "%1". Data is missing.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Cannot import Passkey file "%1". Private key is missing or malformed.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>PasswordEditWidget</name>
|
||||
<message>
|
||||
@ -8031,6 +8248,10 @@ Kernel: %3 %4</source>
|
||||
<source>Failed to decrypt key data.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Passkeys</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>QtIOCompressor</name>
|
||||
@ -8068,18 +8289,6 @@ Kernel: %3 %4</source>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetBrowserStatistics</name>
|
||||
<message>
|
||||
<source>Exclude expired entries from the report</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show only entries which have URL set</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show only entries which have browser settings in custom data</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Double-click entries to edit.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -8147,17 +8356,25 @@ Kernel: %3 %4</source>
|
||||
<source>Exclude from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Only show entries that have a URL</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Only show entries that have been explicitly allowed or denied</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show expired entries</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source> (Expired)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetHealthcheck</name>
|
||||
<message>
|
||||
<source>Exclude expired entries from the report</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Also show entries that have been excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Hover over reason to show additional details. Double-click entries to edit.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
@ -8236,6 +8453,18 @@ Kernel: %3 %4</source>
|
||||
<source>Exclude from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show expired entries</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show entries that have been excluded from reports</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source> (Expired)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetHibp</name>
|
||||
@ -8335,6 +8564,68 @@ Kernel: %3 %4</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetPasskeys</name>
|
||||
<message>
|
||||
<source>Export</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Import</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>List of entry URLs</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Please wait, list of entries with Passkeys is being updated…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>No entries with Passkeys.</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Title</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Path</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Username</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>URLs</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Edit Entry…</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message numerus="yes">
|
||||
<source>Delete Entry(s)…</source>
|
||||
<translation type="unfinished">
|
||||
<numerusform></numerusform>
|
||||
<numerusform></numerusform>
|
||||
</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Relying Party</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Show expired entries</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
<message>
|
||||
<source> (Expired)</source>
|
||||
<translation type="unfinished"></translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>ReportsWidgetStatistics</name>
|
||||
<message>
|
||||
|
@ -259,6 +259,7 @@ set(keepassx_SOURCES_MAINEXE main.cpp)
|
||||
add_feature_info(Auto-Type WITH_XC_AUTOTYPE "Automatic password typing")
|
||||
add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network access code (e.g. for downloading website icons)")
|
||||
add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser")
|
||||
add_feature_info(Passkeys WITH_XC_BROWSER_PASSKEYS "Passkeys support for browser integration")
|
||||
add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent")
|
||||
add_feature_info(KeeShare WITH_XC_KEESHARE "Sharing integration with KeeShare")
|
||||
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
|
||||
@ -271,10 +272,21 @@ add_subdirectory(browser)
|
||||
add_subdirectory(proxy)
|
||||
if(WITH_XC_BROWSER)
|
||||
set(keepassxcbrowser_LIB keepassxcbrowser)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES} gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES} gui/entry/EntryURLModel.cpp)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES} gui/reports/ReportsWidgetBrowserStatistics.cpp)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES} gui/reports/ReportsPageBrowserStatistics.cpp)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES}
|
||||
gui/dbsettings/DatabaseSettingsWidgetBrowser.cpp
|
||||
gui/entry/EntryURLModel.cpp
|
||||
gui/reports/ReportsWidgetBrowserStatistics.cpp
|
||||
gui/reports/ReportsPageBrowserStatistics.cpp)
|
||||
endif()
|
||||
|
||||
if(WITH_XC_BROWSER_PASSKEYS)
|
||||
set(keepassx_SOURCES ${keepassx_SOURCES}
|
||||
gui/reports/ReportsWidgetPasskeys.cpp
|
||||
gui/reports/ReportsPagePasskeys.cpp
|
||||
gui/passkeys/PasskeyExporter.cpp
|
||||
gui/passkeys/PasskeyExportDialog.cpp
|
||||
gui/passkeys/PasskeyImporter.cpp
|
||||
gui/passkeys/PasskeyImportDialog.cpp)
|
||||
endif()
|
||||
|
||||
add_subdirectory(autotype)
|
||||
|
@ -16,8 +16,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERACCESSCONTROLDIALOG_H
|
||||
#define BROWSERACCESSCONTROLDIALOG_H
|
||||
#ifndef KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
|
||||
#define KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
@ -64,4 +64,4 @@ private:
|
||||
QList<Entry*> m_entriesToConfirm;
|
||||
};
|
||||
|
||||
#endif // BROWSERACCESSCONTROLDIALOG_H
|
||||
#endif // KEEPASSXC_BROWSERACCESSCONTROLDIALOG_H
|
||||
|
@ -16,7 +16,10 @@
|
||||
*/
|
||||
|
||||
#include "BrowserAction.h"
|
||||
#include "BrowserService.h"
|
||||
#include "BrowserMessageBuilder.h"
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
#include "BrowserPasskeys.h"
|
||||
#endif
|
||||
#include "BrowserSettings.h"
|
||||
#include "core/Global.h"
|
||||
#include "core/Tools.h"
|
||||
@ -36,6 +39,8 @@ static const QString BROWSER_REQUEST_GET_DATABASE_GROUPS = QStringLiteral("get-d
|
||||
static const QString BROWSER_REQUEST_GET_LOGINS = QStringLiteral("get-logins");
|
||||
static const QString BROWSER_REQUEST_GET_TOTP = QStringLiteral("get-totp");
|
||||
static const QString BROWSER_REQUEST_LOCK_DATABASE = QStringLiteral("lock-database");
|
||||
static const QString BROWSER_REQUEST_PASSKEYS_GET = QStringLiteral("passkeys-get");
|
||||
static const QString BROWSER_REQUEST_PASSKEYS_REGISTER = QStringLiteral("passkeys-register");
|
||||
static const QString BROWSER_REQUEST_REQUEST_AUTOTYPE = QStringLiteral("request-autotype");
|
||||
static const QString BROWSER_REQUEST_SET_LOGIN = QStringLiteral("set-login");
|
||||
static const QString BROWSER_REQUEST_TEST_ASSOCIATE = QStringLiteral("test-associate");
|
||||
@ -104,6 +109,12 @@ QJsonObject BrowserAction::handleAction(QLocalSocket* socket, const QJsonObject&
|
||||
return handleGlobalAutoType(json, action);
|
||||
} else if (action.compare("get-database-entries", Qt::CaseSensitive) == 0) {
|
||||
return handleGetDatabaseEntries(json, action);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
} else if (action.compare(BROWSER_REQUEST_PASSKEYS_GET) == 0) {
|
||||
return handlePasskeysGet(json, action);
|
||||
} else if (action.compare(BROWSER_REQUEST_PASSKEYS_REGISTER) == 0) {
|
||||
return handlePasskeysRegister(json, action);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Action was not recognized
|
||||
@ -226,18 +237,11 @@ QJsonObject BrowserAction::handleGetLogins(const QJsonObject& json, const QStrin
|
||||
return getErrorReply(action, ERROR_KEEPASS_NO_URL_PROVIDED);
|
||||
}
|
||||
|
||||
const auto keys = browserRequest.getArray("keys");
|
||||
|
||||
StringPairList keyList;
|
||||
for (const auto val : keys) {
|
||||
const auto keyObject = val.toObject();
|
||||
keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
|
||||
}
|
||||
|
||||
const auto id = browserRequest.getString("id");
|
||||
const auto formUrl = browserRequest.getString("submitUrl");
|
||||
const auto auth = browserRequest.getString("httpAuth");
|
||||
const bool httpAuth = auth.compare(TRUE_STR) == 0;
|
||||
const auto keyList = getConnectionKeys(browserRequest);
|
||||
|
||||
EntryParameters entryParameters;
|
||||
entryParameters.dbid = id;
|
||||
@ -384,10 +388,6 @@ QJsonObject BrowserAction::handleGetDatabaseGroups(const QJsonObject& json, cons
|
||||
|
||||
QJsonObject BrowserAction::handleGetDatabaseEntries(const QJsonObject& json, const QString& action)
|
||||
{
|
||||
const QString hash = browserService()->getDatabaseHash();
|
||||
const QString nonce = json.value("nonce").toString();
|
||||
const QString encrypted = json.value("message").toString();
|
||||
|
||||
if (!m_associated) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
|
||||
}
|
||||
@ -516,6 +516,74 @@ QJsonObject BrowserAction::handleGlobalAutoType(const QJsonObject& json, const Q
|
||||
return buildResponse(action, browserRequest.incrementedNonce);
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
QJsonObject BrowserAction::handlePasskeysGet(const QJsonObject& json, const QString& action)
|
||||
{
|
||||
if (!m_associated) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
|
||||
}
|
||||
|
||||
const auto browserRequest = decodeRequest(json);
|
||||
if (browserRequest.isEmpty()) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
|
||||
}
|
||||
|
||||
const auto command = browserRequest.getString("action");
|
||||
if (command.isEmpty() || command.compare(BROWSER_REQUEST_PASSKEYS_GET) != 0) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
|
||||
}
|
||||
|
||||
const auto publicKey = browserRequest.getObject("publicKey");
|
||||
if (publicKey.isEmpty()) {
|
||||
return getErrorReply(action, ERROR_PASSKEYS_EMPTY_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
const auto origin = browserRequest.getString("origin");
|
||||
if (!origin.startsWith("https://")) {
|
||||
return getErrorReply(action, ERROR_PASSKEYS_INVALID_URL_PROVIDED);
|
||||
}
|
||||
|
||||
const auto keyList = getConnectionKeys(browserRequest);
|
||||
const auto response = browserService()->showPasskeysAuthenticationPrompt(publicKey, origin, keyList);
|
||||
|
||||
const Parameters params{{"response", response}};
|
||||
return buildResponse(action, browserRequest.incrementedNonce, params);
|
||||
}
|
||||
|
||||
QJsonObject BrowserAction::handlePasskeysRegister(const QJsonObject& json, const QString& action)
|
||||
{
|
||||
if (!m_associated) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_ASSOCIATION_FAILED);
|
||||
}
|
||||
|
||||
const auto browserRequest = decodeRequest(json);
|
||||
if (browserRequest.isEmpty()) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_CANNOT_DECRYPT_MESSAGE);
|
||||
}
|
||||
|
||||
const auto command = browserRequest.getString("action");
|
||||
if (command.isEmpty() || command.compare(BROWSER_REQUEST_PASSKEYS_REGISTER) != 0) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_INCORRECT_ACTION);
|
||||
}
|
||||
|
||||
const auto publicKey = browserRequest.getObject("publicKey");
|
||||
if (publicKey.isEmpty()) {
|
||||
return getErrorReply(action, ERROR_PASSKEYS_EMPTY_PUBLIC_KEY);
|
||||
}
|
||||
|
||||
const auto origin = browserRequest.getString("origin");
|
||||
if (!origin.startsWith("https://")) {
|
||||
return getErrorReply(action, ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
|
||||
}
|
||||
|
||||
const auto keyList = getConnectionKeys(browserRequest);
|
||||
const auto response = browserService()->showPasskeysRegisterPrompt(publicKey, origin, keyList);
|
||||
|
||||
const Parameters params{{"response", response}};
|
||||
return buildResponse(action, browserRequest.incrementedNonce, params);
|
||||
}
|
||||
#endif
|
||||
|
||||
QJsonObject BrowserAction::decryptMessage(const QString& message, const QString& nonce)
|
||||
{
|
||||
return browserMessageBuilder()->decryptMessage(message, nonce, m_clientPublicKey, m_secretKey);
|
||||
@ -541,3 +609,16 @@ BrowserRequest BrowserAction::decodeRequest(const QJsonObject& json)
|
||||
browserMessageBuilder()->incrementNonce(nonce),
|
||||
decryptMessage(encrypted, nonce)};
|
||||
}
|
||||
|
||||
StringPairList BrowserAction::getConnectionKeys(const BrowserRequest& browserRequest)
|
||||
{
|
||||
const auto keys = browserRequest.getArray("keys");
|
||||
|
||||
StringPairList keyList;
|
||||
for (const auto val : keys) {
|
||||
const auto keyObject = val.toObject();
|
||||
keyList.push_back(qMakePair(keyObject.value("id").toString(), keyObject.value("key").toString()));
|
||||
}
|
||||
|
||||
return keyList;
|
||||
}
|
||||
|
@ -15,10 +15,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERACTION_H
|
||||
#define BROWSERACTION_H
|
||||
#ifndef KEEPASSXC_BROWSERACTION_H
|
||||
#define KEEPASSXC_BROWSERACTION_H
|
||||
|
||||
#include "BrowserMessageBuilder.h"
|
||||
#include "BrowserService.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
@ -43,6 +44,11 @@ struct BrowserRequest
|
||||
return decrypted.value(param).toArray();
|
||||
}
|
||||
|
||||
inline QJsonObject getObject(const QString& param) const
|
||||
{
|
||||
return decrypted.value(param).toObject();
|
||||
}
|
||||
|
||||
inline QString getString(const QString& param) const
|
||||
{
|
||||
return decrypted.value(param).toString();
|
||||
@ -73,12 +79,17 @@ private:
|
||||
QJsonObject handleGetTotp(const QJsonObject& json, const QString& action);
|
||||
QJsonObject handleDeleteEntry(const QJsonObject& json, const QString& action);
|
||||
QJsonObject handleGlobalAutoType(const QJsonObject& json, const QString& action);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
QJsonObject handlePasskeysGet(const QJsonObject& json, const QString& action);
|
||||
QJsonObject handlePasskeysRegister(const QJsonObject& json, const QString& action);
|
||||
#endif
|
||||
|
||||
private:
|
||||
QJsonObject buildResponse(const QString& action, const QString& nonce, const Parameters& params = {});
|
||||
QJsonObject getErrorReply(const QString& action, const int errorCode) const;
|
||||
QJsonObject decryptMessage(const QString& message, const QString& nonce);
|
||||
BrowserRequest decodeRequest(const QJsonObject& json);
|
||||
StringPairList getConnectionKeys(const BrowserRequest& browserRequest);
|
||||
|
||||
private:
|
||||
static const int MaxUrlLength;
|
||||
@ -91,4 +102,4 @@ private:
|
||||
friend class TestBrowser;
|
||||
};
|
||||
|
||||
#endif // BROWSERACTION_H
|
||||
#endif // KEEPASSXC_BROWSERACTION_H
|
||||
|
253
src/browser/BrowserCbor.cpp
Normal file
253
src/browser/BrowserCbor.cpp
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BrowserCbor.h"
|
||||
#include "BrowserMessageBuilder.h"
|
||||
#include <QCborStreamReader>
|
||||
#include <QCborStreamWriter>
|
||||
#include <QJsonDocument>
|
||||
|
||||
// https://w3c.github.io/webauthn/#sctn-none-attestation
|
||||
// https://w3c.github.io/webauthn/#sctn-generating-an-attestation-object
|
||||
QByteArray BrowserCbor::cborEncodeAttestation(const QByteArray& authData) const
|
||||
{
|
||||
QByteArray result;
|
||||
QCborStreamWriter writer(&result);
|
||||
|
||||
writer.startMap(3);
|
||||
|
||||
writer.append("fmt");
|
||||
writer.append("none");
|
||||
|
||||
writer.append("attStmt");
|
||||
writer.startMap(0);
|
||||
writer.endMap();
|
||||
|
||||
writer.append("authData");
|
||||
writer.appendByteString(authData.constData(), authData.size());
|
||||
|
||||
writer.endMap();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webauthn/#authdata-attestedcredentialdata-credentialpublickey
|
||||
QByteArray BrowserCbor::cborEncodePublicKey(int alg, const QByteArray& first, const QByteArray& second) const
|
||||
{
|
||||
QByteArray result;
|
||||
QCborStreamWriter writer(&result);
|
||||
|
||||
if (alg == WebAuthnAlgorithms::ES256) {
|
||||
writer.startMap(5);
|
||||
|
||||
// Key type
|
||||
writer.append(1);
|
||||
writer.append(getCoseKeyType(alg));
|
||||
|
||||
// Signature algorithm
|
||||
writer.append(3);
|
||||
writer.append(alg);
|
||||
|
||||
// Curve parameter
|
||||
writer.append(-1);
|
||||
writer.append(getCurveParameter(alg));
|
||||
|
||||
// Key x-coordinate
|
||||
writer.append(-2);
|
||||
writer.append(first);
|
||||
|
||||
// Key y-coordinate
|
||||
writer.append(-3);
|
||||
writer.append(second);
|
||||
|
||||
writer.endMap();
|
||||
} else if (alg == WebAuthnAlgorithms::RS256) {
|
||||
writer.startMap(4);
|
||||
|
||||
// Key type
|
||||
writer.append(1);
|
||||
writer.append(getCoseKeyType(alg));
|
||||
|
||||
// Signature algorithm
|
||||
writer.append(3);
|
||||
writer.append(alg);
|
||||
|
||||
// Key modulus
|
||||
writer.append(-1);
|
||||
writer.append(first);
|
||||
|
||||
// Key exponent
|
||||
writer.append(-2);
|
||||
writer.append(second);
|
||||
|
||||
writer.endMap();
|
||||
} else if (alg == WebAuthnAlgorithms::EDDSA) {
|
||||
// https://www.rfc-editor.org/rfc/rfc8152#section-13.2
|
||||
writer.startMap(3);
|
||||
|
||||
// Curve parameter
|
||||
writer.append(-1);
|
||||
writer.append(getCurveParameter(alg));
|
||||
|
||||
// Public key
|
||||
writer.append(-2);
|
||||
writer.append(first);
|
||||
|
||||
// Private key
|
||||
writer.append(-4);
|
||||
writer.append(second);
|
||||
|
||||
writer.endMap();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// See: https://fidoalliance.org/specs/common-specs/fido-registry-v2.1-ps-20191217.html#user-verification-methods
|
||||
QByteArray BrowserCbor::cborEncodeExtensionData(const QJsonObject& extensions) const
|
||||
{
|
||||
if (extensions.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray result;
|
||||
QCborStreamWriter writer(&result);
|
||||
|
||||
writer.startMap(extensions.keys().count());
|
||||
if (extensions["credProps"].toBool()) {
|
||||
writer.append("credProps");
|
||||
writer.startMap(1);
|
||||
writer.append("rk");
|
||||
writer.append(true);
|
||||
writer.endMap();
|
||||
}
|
||||
|
||||
if (extensions["uvm"].toBool()) {
|
||||
writer.append("uvm");
|
||||
|
||||
writer.startArray(1);
|
||||
writer.startArray(3);
|
||||
|
||||
// userVerificationMethod (USER_VERIFY_PRESENCE_INTERNAL "presence_internal", 0x00000001)
|
||||
writer.append(quint32(1));
|
||||
|
||||
// keyProtectionType (KEY_PROTECTION_SOFTWARE "software", 0x0001)
|
||||
writer.append(quint16(1));
|
||||
|
||||
// matcherProtectionType (MATCHER_PROTECTION_SOFTWARE "software", 0x0001)
|
||||
writer.append(quint16(1));
|
||||
|
||||
writer.endArray();
|
||||
writer.endArray();
|
||||
}
|
||||
writer.endMap();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QJsonObject BrowserCbor::getJsonFromCborData(const QByteArray& byteArray) const
|
||||
{
|
||||
auto reader = QCborStreamReader(byteArray);
|
||||
auto contents = QCborValue::fromCbor(reader);
|
||||
if (reader.lastError()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto ret = handleCborValue(contents);
|
||||
|
||||
// Parse variant result to QJsonDocument
|
||||
const auto jsonDocument = QJsonDocument::fromVariant(ret);
|
||||
if (jsonDocument.isNull() || jsonDocument.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return jsonDocument.object();
|
||||
}
|
||||
|
||||
QVariant BrowserCbor::handleCborArray(const QCborArray& array) const
|
||||
{
|
||||
QVariantList result;
|
||||
result.reserve(array.size());
|
||||
|
||||
for (auto a : array) {
|
||||
result.append(handleCborValue(a));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
QVariant BrowserCbor::handleCborMap(const QCborMap& map) const
|
||||
{
|
||||
QVariantMap result;
|
||||
for (auto pair : map) {
|
||||
result.insert(handleCborValue(pair.first).toString(), handleCborValue(pair.second));
|
||||
}
|
||||
|
||||
return QVariant::fromValue(result);
|
||||
}
|
||||
|
||||
QVariant BrowserCbor::handleCborValue(const QCborValue& value) const
|
||||
{
|
||||
if (value.isArray()) {
|
||||
return handleCborArray(value.toArray());
|
||||
} else if (value.isMap()) {
|
||||
return handleCborMap(value.toMap());
|
||||
} else if (value.isByteArray()) {
|
||||
auto ba = value.toByteArray();
|
||||
|
||||
// Return base64 instead of raw byte array
|
||||
auto base64Str = browserMessageBuilder()->getBase64FromArray(ba);
|
||||
return QVariant::fromValue(base64Str);
|
||||
}
|
||||
|
||||
return value.toVariant();
|
||||
}
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc8152#section-13.1
|
||||
unsigned int BrowserCbor::getCurveParameter(int alg) const
|
||||
{
|
||||
switch (alg) {
|
||||
case WebAuthnAlgorithms::ES256:
|
||||
return WebAuthnCurveKey::P256;
|
||||
case WebAuthnAlgorithms::ES384:
|
||||
return WebAuthnCurveKey::P384;
|
||||
case WebAuthnAlgorithms::ES512:
|
||||
return WebAuthnCurveKey::P521;
|
||||
case WebAuthnAlgorithms::EDDSA:
|
||||
return WebAuthnCurveKey::ED25519;
|
||||
default:
|
||||
return WebAuthnCurveKey::P256;
|
||||
}
|
||||
}
|
||||
|
||||
// See: https://www.rfc-editor.org/rfc/rfc8152
|
||||
// AES/HMAC/ChaCha20 etc. carries symmetric keys (4) and OKP not supported currently.
|
||||
unsigned int BrowserCbor::getCoseKeyType(int alg) const
|
||||
{
|
||||
switch (alg) {
|
||||
case WebAuthnAlgorithms::ES256:
|
||||
case WebAuthnAlgorithms::ES384:
|
||||
case WebAuthnAlgorithms::ES512:
|
||||
return WebAuthnCoseKeyType::EC2;
|
||||
case WebAuthnAlgorithms::EDDSA:
|
||||
return WebAuthnCoseKeyType::OKP;
|
||||
case WebAuthnAlgorithms::RS256:
|
||||
return WebAuthnCoseKeyType::RSA;
|
||||
default:
|
||||
return WebAuthnCoseKeyType::EC2;
|
||||
}
|
||||
}
|
70
src/browser/BrowserCbor.h
Normal file
70
src/browser/BrowserCbor.h
Normal file
@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_BROWSERCBOR_H
|
||||
#define KEEPASSXC_BROWSERCBOR_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QCborArray>
|
||||
#include <QCborMap>
|
||||
#include <QJsonObject>
|
||||
|
||||
enum WebAuthnAlgorithms : int
|
||||
{
|
||||
ES256 = -7,
|
||||
EDDSA = -8,
|
||||
ES384 = -35,
|
||||
ES512 = -36,
|
||||
RS256 = -257
|
||||
};
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc9053#section-7.1
|
||||
enum WebAuthnCurveKey : int
|
||||
{
|
||||
P256 = 1, // EC2, NIST P-256, also known as secp256r1
|
||||
P384 = 2, // EC2, NIST P-384, also known as secp384r1
|
||||
P521 = 3, // EC2, NIST P-521, also known as secp521r1
|
||||
X25519 = 4, // OKP, X25519 for use w/ ECDH only
|
||||
X448 = 5, // OKP, X448 for use w/ ECDH only
|
||||
ED25519 = 6, // OKP, Ed25519 for use w/ EdDSA only
|
||||
ED448 = 7 // OKP, Ed448 for use w/ EdDSA only
|
||||
};
|
||||
|
||||
// https://www.rfc-editor.org/rfc/rfc8152
|
||||
// For RSA: https://www.rfc-editor.org/rfc/rfc8230#section-4
|
||||
enum WebAuthnCoseKeyType : int
|
||||
{
|
||||
OKP = 1, // Octet Keypair
|
||||
EC2 = 2, // Elliptic Curve
|
||||
RSA = 3 // RSA
|
||||
};
|
||||
|
||||
class BrowserCbor
|
||||
{
|
||||
public:
|
||||
QByteArray cborEncodeAttestation(const QByteArray& authData) const;
|
||||
QByteArray cborEncodePublicKey(int alg, const QByteArray& first, const QByteArray& second) const;
|
||||
QByteArray cborEncodeExtensionData(const QJsonObject& extensions) const;
|
||||
QJsonObject getJsonFromCborData(const QByteArray& byteArray) const;
|
||||
QVariant handleCborArray(const QCborArray& array) const;
|
||||
QVariant handleCborMap(const QCborMap& map) const;
|
||||
QVariant handleCborValue(const QCborValue& value) const;
|
||||
unsigned int getCoseKeyType(int alg) const;
|
||||
unsigned int getCurveParameter(int alg) const;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_BROWSERCBOR_H
|
@ -16,8 +16,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERENTRYCONFIG_H
|
||||
#define BROWSERENTRYCONFIG_H
|
||||
#ifndef KEEPASSXC_BROWSERENTRYCONFIG_H
|
||||
#define KEEPASSXC_BROWSERENTRYCONFIG_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
@ -55,4 +55,4 @@ private:
|
||||
QString m_realm;
|
||||
};
|
||||
|
||||
#endif // BROWSERENTRYCONFIG_H
|
||||
#endif // KEEPASSXC_BROWSERENTRYCONFIG_H
|
||||
|
@ -16,8 +16,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERENTRYSAVEDIALOG_H
|
||||
#define BROWSERENTRYSAVEDIALOG_H
|
||||
#ifndef KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
|
||||
#define KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
|
||||
|
||||
#include "gui/DatabaseTabWidget.h"
|
||||
|
||||
@ -45,4 +45,4 @@ private:
|
||||
QScopedPointer<Ui::BrowserEntrySaveDialog> m_ui;
|
||||
};
|
||||
|
||||
#endif // BROWSERENTRYSAVEDIALOG_H
|
||||
#endif // KEEPASSXC_BROWSERENTRYSAVEDIALOG_H
|
||||
|
@ -11,7 +11,7 @@
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>KeePassXC-Browser Save Entry</string>
|
||||
<string>KeePassXC - Select Database</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
|
@ -15,8 +15,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef NATIVEMESSAGINGHOST_H
|
||||
#define NATIVEMESSAGINGHOST_H
|
||||
#ifndef KEEPASSXC_NATIVEMESSAGINGHOST_H
|
||||
#define KEEPASSXC_NATIVEMESSAGINGHOST_H
|
||||
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
@ -56,4 +56,4 @@ private:
|
||||
QList<QLocalSocket*> m_socketList;
|
||||
};
|
||||
|
||||
#endif // NATIVEMESSAGINGHOST_H
|
||||
#endif // KEEPASSXC_NATIVEMESSAGINGHOST_H
|
||||
|
@ -19,11 +19,14 @@
|
||||
#include "BrowserShared.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "core/Global.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#ifdef QT_DEBUG
|
||||
#include <QDebug>
|
||||
#endif
|
||||
|
||||
#include <botan/sodium.h>
|
||||
|
||||
@ -243,6 +246,11 @@ QJsonObject BrowserMessageBuilder::getJsonObject(const uchar* pArray, const uint
|
||||
QByteArray arr = getQByteArray(pArray, len);
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc(QJsonDocument::fromJson(arr, &err));
|
||||
#ifdef QT_DEBUG
|
||||
if (doc.isNull()) {
|
||||
qWarning() << "Cannot create QJsonDocument: " << err.errorString();
|
||||
}
|
||||
#endif
|
||||
return doc.object();
|
||||
}
|
||||
|
||||
@ -250,6 +258,12 @@ QJsonObject BrowserMessageBuilder::getJsonObject(const QByteArray& ba) const
|
||||
{
|
||||
QJsonParseError err;
|
||||
QJsonDocument doc(QJsonDocument::fromJson(ba, &err));
|
||||
#ifdef QT_DEBUG
|
||||
if (doc.isNull()) {
|
||||
qWarning() << "Cannot create QJsonDocument: " << err.errorString();
|
||||
}
|
||||
#endif
|
||||
|
||||
return doc.object();
|
||||
}
|
||||
|
||||
@ -266,3 +280,65 @@ QString BrowserMessageBuilder::incrementNonce(const QString& nonce)
|
||||
sodium_increment(n.data(), n.size());
|
||||
return getQByteArray(n.data(), n.size()).toBase64();
|
||||
}
|
||||
|
||||
QString BrowserMessageBuilder::getRandomBytesAsBase64(int bytes) const
|
||||
{
|
||||
if (bytes == 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::shared_ptr<unsigned char[]> buf(new unsigned char[bytes]);
|
||||
Botan::Sodium::randombytes_buf(buf.get(), bytes);
|
||||
|
||||
return getBase64FromArray(reinterpret_cast<const char*>(buf.get()), bytes);
|
||||
}
|
||||
|
||||
QString BrowserMessageBuilder::getBase64FromArray(const char* arr, int len) const
|
||||
{
|
||||
if (len < 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto data = QByteArray::fromRawData(arr, len);
|
||||
return getBase64FromArray(data);
|
||||
}
|
||||
|
||||
// Returns URL encoded base64 with trailing removed
|
||||
QString BrowserMessageBuilder::getBase64FromArray(const QByteArray& byteArray) const
|
||||
{
|
||||
if (byteArray.length() < 1) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return byteArray.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
}
|
||||
|
||||
QString BrowserMessageBuilder::getBase64FromJson(const QJsonObject& jsonObject) const
|
||||
{
|
||||
if (jsonObject.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto dataArray = QJsonDocument(jsonObject).toJson(QJsonDocument::Compact);
|
||||
return getBase64FromArray(dataArray);
|
||||
}
|
||||
|
||||
QByteArray BrowserMessageBuilder::getArrayFromHexString(const QString& hexString) const
|
||||
{
|
||||
return QByteArray::fromHex(hexString.toUtf8());
|
||||
}
|
||||
|
||||
QByteArray BrowserMessageBuilder::getArrayFromBase64(const QString& base64str) const
|
||||
{
|
||||
return QByteArray::fromBase64(base64str.toUtf8(), QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals);
|
||||
}
|
||||
|
||||
QByteArray BrowserMessageBuilder::getSha256Hash(const QString& str) const
|
||||
{
|
||||
return QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Sha256);
|
||||
}
|
||||
|
||||
QString BrowserMessageBuilder::getSha256HashAsBase64(const QString& str) const
|
||||
{
|
||||
return getBase64FromArray(QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Sha256));
|
||||
}
|
||||
|
@ -15,8 +15,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERMESSAGEBUILDER_H
|
||||
#define BROWSERMESSAGEBUILDER_H
|
||||
#ifndef KEEPASSXC_BROWSERMESSAGEBUILDER_H
|
||||
#define KEEPASSXC_BROWSERMESSAGEBUILDER_H
|
||||
|
||||
#include <QPair>
|
||||
#include <QString>
|
||||
@ -48,7 +48,13 @@ namespace
|
||||
ERROR_KEEPASS_NO_GROUPS_FOUND = 16,
|
||||
ERROR_KEEPASS_CANNOT_CREATE_NEW_GROUP = 17,
|
||||
ERROR_KEEPASS_NO_VALID_UUID_PROVIDED = 18,
|
||||
ERROR_KEEPASS_ACCESS_TO_ALL_ENTRIES_DENIED = 19
|
||||
ERROR_KEEPASS_ACCESS_TO_ALL_ENTRIES_DENIED = 19,
|
||||
ERROR_PASSKEYS_ATTESTATION_NOT_SUPPORTED = 20,
|
||||
ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED = 21,
|
||||
ERROR_PASSKEYS_REQUEST_CANCELED = 22,
|
||||
ERROR_PASSKEYS_INVALID_USER_VERIFICATION = 23,
|
||||
ERROR_PASSKEYS_EMPTY_PUBLIC_KEY = 24,
|
||||
ERROR_PASSKEYS_INVALID_URL_PROVIDED = 25
|
||||
};
|
||||
}
|
||||
|
||||
@ -84,6 +90,14 @@ public:
|
||||
QJsonObject getJsonObject(const QByteArray& ba) const;
|
||||
QByteArray base64Decode(const QString& str);
|
||||
QString incrementNonce(const QString& nonce);
|
||||
QString getRandomBytesAsBase64(int bytes) const;
|
||||
QString getBase64FromArray(const char* arr, int len) const;
|
||||
QString getBase64FromArray(const QByteArray& byteArray) const;
|
||||
QString getBase64FromJson(const QJsonObject& jsonObject) const;
|
||||
QByteArray getArrayFromHexString(const QString& hexString) const;
|
||||
QByteArray getArrayFromBase64(const QString& base64str) const;
|
||||
QByteArray getSha256Hash(const QString& str) const;
|
||||
QString getSha256HashAsBase64(const QString& str) const;
|
||||
|
||||
private:
|
||||
Q_DISABLE_COPY(BrowserMessageBuilder);
|
||||
@ -96,4 +110,4 @@ static inline BrowserMessageBuilder* browserMessageBuilder()
|
||||
return BrowserMessageBuilder::instance();
|
||||
}
|
||||
|
||||
#endif // BROWSERMESSAGEBUILDER_H
|
||||
#endif // KEEPASSXC_BROWSERMESSAGEBUILDER_H
|
||||
|
465
src/browser/BrowserPasskeys.cpp
Normal file
465
src/browser/BrowserPasskeys.cpp
Normal file
@ -0,0 +1,465 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BrowserPasskeys.h"
|
||||
#include "BrowserMessageBuilder.h"
|
||||
#include "BrowserService.h"
|
||||
#include "crypto/Random.h"
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QtEndian>
|
||||
|
||||
#include <botan/data_src.h>
|
||||
#include <botan/ec_group.h>
|
||||
#include <botan/ecdsa.h>
|
||||
#include <botan/ed25519.h>
|
||||
#include <botan/pkcs8.h>
|
||||
#include <botan/pubkey.h>
|
||||
#include <botan/rsa.h>
|
||||
#include <botan/sodium.h>
|
||||
|
||||
#include <bitset>
|
||||
|
||||
Q_GLOBAL_STATIC(BrowserPasskeys, s_browserPasskeys);
|
||||
|
||||
const QString BrowserPasskeys::PUBLIC_KEY = QStringLiteral("public-key");
|
||||
const QString BrowserPasskeys::REQUIREMENT_DISCOURAGED = QStringLiteral("discouraged");
|
||||
const QString BrowserPasskeys::REQUIREMENT_PREFERRED = QStringLiteral("preferred");
|
||||
const QString BrowserPasskeys::REQUIREMENT_REQUIRED = QStringLiteral("required");
|
||||
|
||||
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT = QStringLiteral("direct");
|
||||
const QString BrowserPasskeys::PASSKEYS_ATTESTATION_NONE = QStringLiteral("none");
|
||||
|
||||
const QString BrowserPasskeys::KPEX_PASSKEY_USERNAME = QStringLiteral("KPEX_PASSKEY_USERNAME");
|
||||
const QString BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");
|
||||
const QString BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM = QStringLiteral("KPEX_PASSKEY_PRIVATE_KEY_PEM");
|
||||
const QString BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX_PASSKEY_RELYING_PARTY");
|
||||
const QString BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
|
||||
|
||||
BrowserPasskeys* BrowserPasskeys::instance()
|
||||
{
|
||||
return s_browserPasskeys;
|
||||
}
|
||||
|
||||
PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
|
||||
const QString& origin,
|
||||
const TestingVariables& testingVariables)
|
||||
{
|
||||
QJsonObject publicKeyCredential;
|
||||
const auto id = testingVariables.credentialId.isEmpty() ? browserMessageBuilder()->getRandomBytesAsBase64(ID_BYTES)
|
||||
: testingVariables.credentialId;
|
||||
|
||||
// Extensions
|
||||
auto extensionObject = publicKeyCredentialOptions["extensions"].toObject();
|
||||
const auto extensionData = buildExtensionData(extensionObject);
|
||||
const auto extensions = browserMessageBuilder()->getBase64FromArray(extensionData);
|
||||
|
||||
// Response
|
||||
QJsonObject responseObject;
|
||||
const auto clientData = buildClientDataJson(publicKeyCredentialOptions, origin, false);
|
||||
const auto attestationObject = buildAttestationObject(publicKeyCredentialOptions, extensions, id, testingVariables);
|
||||
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromJson(clientData);
|
||||
responseObject["attestationObject"] = browserMessageBuilder()->getBase64FromArray(attestationObject.cborEncoded);
|
||||
|
||||
// PublicKeyCredential
|
||||
publicKeyCredential["authenticatorAttachment"] = QString("platform");
|
||||
publicKeyCredential["id"] = id;
|
||||
publicKeyCredential["response"] = responseObject;
|
||||
publicKeyCredential["type"] = PUBLIC_KEY;
|
||||
|
||||
return {id, publicKeyCredential, attestationObject.pem};
|
||||
}
|
||||
|
||||
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
|
||||
const QString& origin,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKeyPem)
|
||||
{
|
||||
const auto authenticatorData = buildGetAttestationObject(publicKeyCredentialRequestOptions);
|
||||
const auto clientData = buildClientDataJson(publicKeyCredentialRequestOptions, origin, true);
|
||||
const auto clientDataArray = QJsonDocument(clientData).toJson(QJsonDocument::Compact);
|
||||
const auto signature = buildSignature(authenticatorData, clientDataArray, privateKeyPem);
|
||||
|
||||
QJsonObject responseObject;
|
||||
responseObject["authenticatorData"] = browserMessageBuilder()->getBase64FromArray(authenticatorData);
|
||||
responseObject["clientDataJSON"] = browserMessageBuilder()->getBase64FromArray(clientDataArray);
|
||||
responseObject["signature"] = browserMessageBuilder()->getBase64FromArray(signature);
|
||||
responseObject["userHandle"] = userHandle;
|
||||
|
||||
QJsonObject publicKeyCredential;
|
||||
publicKeyCredential["authenticatorAttachment"] = QString("platform");
|
||||
publicKeyCredential["id"] = userId;
|
||||
publicKeyCredential["response"] = responseObject;
|
||||
publicKeyCredential["type"] = PUBLIC_KEY;
|
||||
|
||||
return publicKeyCredential;
|
||||
}
|
||||
|
||||
bool BrowserPasskeys::isUserVerificationValid(const QString& userVerification) const
|
||||
{
|
||||
return QStringList({REQUIREMENT_PREFERRED, REQUIREMENT_REQUIRED, REQUIREMENT_DISCOURAGED})
|
||||
.contains(userVerification);
|
||||
}
|
||||
|
||||
// See https://w3c.github.io/webauthn/#sctn-createCredential for default timeout values when not set in the request
|
||||
int BrowserPasskeys::getTimeout(const QString& userVerification, int timeout) const
|
||||
{
|
||||
if (timeout == 0) {
|
||||
return userVerification == REQUIREMENT_DISCOURAGED ? DEFAULT_DISCOURAGED_TIMEOUT : DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
return timeout;
|
||||
}
|
||||
|
||||
QStringList BrowserPasskeys::getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const
|
||||
{
|
||||
QStringList allowedCredentials;
|
||||
for (const auto& cred : publicKey["allowCredentials"].toArray()) {
|
||||
const auto c = cred.toObject();
|
||||
const auto id = c["id"].toString();
|
||||
|
||||
if (c["type"].toString() == PUBLIC_KEY && !id.isEmpty()) {
|
||||
allowedCredentials << id;
|
||||
}
|
||||
}
|
||||
|
||||
return allowedCredentials;
|
||||
}
|
||||
|
||||
QJsonObject BrowserPasskeys::buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get)
|
||||
{
|
||||
QJsonObject clientData;
|
||||
clientData["challenge"] = publicKey["challenge"];
|
||||
clientData["crossOrigin"] = false;
|
||||
clientData["origin"] = origin;
|
||||
clientData["type"] = get ? QString("webauthn.get") : QString("webauthn.create");
|
||||
|
||||
return clientData;
|
||||
}
|
||||
|
||||
// https://w3c.github.io/webauthn/#attestation-object
|
||||
PrivateKey BrowserPasskeys::buildAttestationObject(const QJsonObject& publicKey,
|
||||
const QString& extensions,
|
||||
const QString& id,
|
||||
const TestingVariables& testingVariables)
|
||||
{
|
||||
QByteArray result;
|
||||
|
||||
// Create SHA256 hash from rpId
|
||||
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rp"]["id"].toString());
|
||||
result.append(rpIdHash);
|
||||
|
||||
// Use default flags
|
||||
const auto flags =
|
||||
setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
|
||||
{"AT", true},
|
||||
{"BS", false},
|
||||
{"BE", false},
|
||||
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
|
||||
{"UP", true}}));
|
||||
result.append(flags);
|
||||
|
||||
// Signature counter (not supported, always 0
|
||||
const char counter[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
result.append(QByteArray::fromRawData(counter, 4));
|
||||
|
||||
// AAGUID (use the default/non-set)
|
||||
result.append("\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b");
|
||||
|
||||
// Credential length
|
||||
const char credentialLength[2] = {0x00, 0x20};
|
||||
result.append(QByteArray::fromRawData(credentialLength, 2));
|
||||
|
||||
// Credential Id
|
||||
result.append(QByteArray::fromBase64(
|
||||
testingVariables.credentialId.isEmpty() ? id.toUtf8() : testingVariables.credentialId.toUtf8(),
|
||||
QByteArray::Base64UrlEncoding));
|
||||
|
||||
// Credential private key
|
||||
const auto alg = getAlgorithmFromPublicKey(publicKey);
|
||||
const auto credentialPublicKey = buildCredentialPrivateKey(alg, testingVariables.first, testingVariables.second);
|
||||
result.append(credentialPublicKey.cborEncoded);
|
||||
|
||||
// Add extension data if available
|
||||
if (!extensions.isEmpty()) {
|
||||
result.append(browserMessageBuilder()->getArrayFromBase64(extensions));
|
||||
}
|
||||
|
||||
// The final result should be CBOR encoded
|
||||
return {m_browserCbor.cborEncodeAttestation(result), credentialPublicKey.pem};
|
||||
}
|
||||
|
||||
// Build a short version of the attestation object for webauthn.get
|
||||
QByteArray BrowserPasskeys::buildGetAttestationObject(const QJsonObject& publicKey)
|
||||
{
|
||||
QByteArray result;
|
||||
|
||||
const auto rpIdHash = browserMessageBuilder()->getSha256Hash(publicKey["rpId"].toString());
|
||||
result.append(rpIdHash);
|
||||
|
||||
const auto flags =
|
||||
setFlagsFromJson(QJsonObject({{"ED", false},
|
||||
{"AT", false},
|
||||
{"BS", false},
|
||||
{"BE", false},
|
||||
{"UV", publicKey["userVerification"].toString() != REQUIREMENT_DISCOURAGED},
|
||||
{"UP", true}}));
|
||||
result.append(flags);
|
||||
|
||||
// Signature counter (not supported, always 0
|
||||
const char counter[4] = {0x00, 0x00, 0x00, 0x00};
|
||||
result.append(QByteArray::fromRawData(counter, 4));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// See: https://w3c.github.io/webauthn/#sctn-encoded-credPubKey-examples
|
||||
PrivateKey
|
||||
BrowserPasskeys::buildCredentialPrivateKey(int alg, const QString& predefinedFirst, const QString& predefinedSecond)
|
||||
{
|
||||
// Only support -7, P256 (EC), -8 (EdDSA) and -257 (RSA) for now
|
||||
if (alg != WebAuthnAlgorithms::ES256 && alg != WebAuthnAlgorithms::RS256 && alg != WebAuthnAlgorithms::EDDSA) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QByteArray firstPart;
|
||||
QByteArray secondPart;
|
||||
QByteArray pem;
|
||||
|
||||
if (!predefinedFirst.isEmpty() && !predefinedSecond.isEmpty()) {
|
||||
firstPart = browserMessageBuilder()->getArrayFromBase64(predefinedFirst);
|
||||
secondPart = browserMessageBuilder()->getArrayFromBase64(predefinedSecond);
|
||||
} else {
|
||||
if (alg == WebAuthnAlgorithms::ES256) {
|
||||
try {
|
||||
Botan::ECDSA_PrivateKey privateKey(*randomGen()->getRng(), Botan::EC_Group("secp256r1"));
|
||||
const auto& publicPoint = privateKey.public_point();
|
||||
auto x = publicPoint.get_affine_x();
|
||||
auto y = publicPoint.get_affine_y();
|
||||
firstPart = bigIntToQByteArray(x);
|
||||
secondPart = bigIntToQByteArray(y);
|
||||
|
||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||||
pem = QByteArray::fromStdString(privateKeyPem);
|
||||
} catch (std::exception& e) {
|
||||
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EC2 private key: %s", e.what());
|
||||
return {};
|
||||
}
|
||||
} else if (alg == WebAuthnAlgorithms::RS256) {
|
||||
try {
|
||||
Botan::RSA_PrivateKey privateKey(*randomGen()->getRng(), RSA_BITS, RSA_EXPONENT);
|
||||
auto modulus = privateKey.get_n();
|
||||
auto exponent = privateKey.get_e();
|
||||
firstPart = bigIntToQByteArray(modulus);
|
||||
secondPart = bigIntToQByteArray(exponent);
|
||||
|
||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(privateKey);
|
||||
pem = QByteArray::fromStdString(privateKeyPem);
|
||||
} catch (std::exception& e) {
|
||||
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create RSA private key: %s", e.what());
|
||||
return {};
|
||||
}
|
||||
} else if (alg == WebAuthnAlgorithms::EDDSA) {
|
||||
try {
|
||||
Botan::Ed25519_PrivateKey key(*randomGen()->getRng());
|
||||
auto publicKey = key.get_public_key();
|
||||
auto privateKey = key.get_private_key();
|
||||
firstPart = browserMessageBuilder()->getQByteArray(publicKey.data(), publicKey.size());
|
||||
secondPart = browserMessageBuilder()->getQByteArray(privateKey.data(), privateKey.size());
|
||||
|
||||
auto privateKeyPem = Botan::PKCS8::PEM_encode(key);
|
||||
pem = QByteArray::fromStdString(privateKeyPem);
|
||||
} catch (std::exception& e) {
|
||||
qWarning("BrowserWebAuthn::buildCredentialPrivateKey: Could not create EdDSA private key: %s",
|
||||
e.what());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto result = m_browserCbor.cborEncodePublicKey(alg, firstPart, secondPart);
|
||||
return {result, pem};
|
||||
}
|
||||
|
||||
QByteArray BrowserPasskeys::buildSignature(const QByteArray& authenticatorData,
|
||||
const QByteArray& clientData,
|
||||
const QString& privateKeyPem)
|
||||
{
|
||||
const auto clientDataHash = browserMessageBuilder()->getSha256Hash(clientData);
|
||||
const auto attToBeSigned = authenticatorData + clientDataHash;
|
||||
|
||||
try {
|
||||
const auto privateKeyArray = privateKeyPem.toUtf8();
|
||||
Botan::DataSource_Memory dataSource(reinterpret_cast<const uint8_t*>(privateKeyArray.constData()),
|
||||
privateKeyArray.size());
|
||||
|
||||
const auto key = Botan::PKCS8::load_key(dataSource).release();
|
||||
const auto privateKeyBytes = key->private_key_bits();
|
||||
const auto algName = key->algo_name();
|
||||
const auto algId = key->algorithm_identifier();
|
||||
|
||||
std::vector<uint8_t> rawSignature;
|
||||
if (algName == "ECDSA") {
|
||||
Botan::ECDSA_PrivateKey privateKey(algId, privateKeyBytes);
|
||||
#ifdef WITH_XC_BOTAN3
|
||||
Botan::PK_Signer signer(
|
||||
privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::Signature_Format::DerSequence);
|
||||
#else
|
||||
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA1(SHA-256)", Botan::DER_SEQUENCE);
|
||||
#endif
|
||||
|
||||
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||||
rawSignature = signer.signature(*randomGen()->getRng());
|
||||
} else if (algName == "RSA") {
|
||||
Botan::RSA_PrivateKey privateKey(algId, privateKeyBytes);
|
||||
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "EMSA3(SHA-256)");
|
||||
|
||||
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||||
rawSignature = signer.signature(*randomGen()->getRng());
|
||||
} else if (algName == "Ed25519") {
|
||||
Botan::Ed25519_PrivateKey privateKey(algId, privateKeyBytes);
|
||||
Botan::PK_Signer signer(privateKey, *randomGen()->getRng(), "SHA-512");
|
||||
|
||||
signer.update(reinterpret_cast<const uint8_t*>(attToBeSigned.constData()), attToBeSigned.size());
|
||||
rawSignature = signer.signature(*randomGen()->getRng());
|
||||
} else {
|
||||
qWarning("BrowserWebAuthn::buildSignature: Algorithm not supported");
|
||||
return {};
|
||||
}
|
||||
|
||||
auto signature = QByteArray(reinterpret_cast<char*>(rawSignature.data()), rawSignature.size());
|
||||
return signature;
|
||||
} catch (std::exception& e) {
|
||||
qWarning("BrowserWebAuthn::buildSignature: Could not sign key: %s", e.what());
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray BrowserPasskeys::buildExtensionData(QJsonObject& extensionObject) const
|
||||
{
|
||||
// Only supports "credProps" and "uvm" for now
|
||||
const QStringList allowedKeys = {"credProps", "uvm"};
|
||||
|
||||
// Remove unsupported keys
|
||||
for (const auto& key : extensionObject.keys()) {
|
||||
if (!allowedKeys.contains(key)) {
|
||||
extensionObject.remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
auto extensionData = m_browserCbor.cborEncodeExtensionData(extensionObject);
|
||||
if (!extensionData.isEmpty()) {
|
||||
return extensionData;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
// Parse authentication data byte array to JSON
|
||||
// See: https://www.w3.org/TR/webauthn/images/fido-attestation-structures.svg
|
||||
// And: https://w3c.github.io/webauthn/#attested-credential-data
|
||||
QJsonObject BrowserPasskeys::parseAuthData(const QByteArray& authData) const
|
||||
{
|
||||
auto rpIdHash = authData.mid(AuthDataOffsets::RPIDHASH, HASH_BYTES);
|
||||
auto flags = authData.mid(AuthDataOffsets::FLAGS, 1);
|
||||
auto counter = authData.mid(AuthDataOffsets::SIGNATURE_COUNTER, 4);
|
||||
auto aaGuid = authData.mid(AuthDataOffsets::AAGUID, 16);
|
||||
auto credentialLength = authData.mid(AuthDataOffsets::CREDENTIAL_LENGTH, 2);
|
||||
auto credLen = qFromBigEndian<quint16>(credentialLength.data());
|
||||
auto credentialId = authData.mid(AuthDataOffsets::CREDENTIAL_ID, credLen);
|
||||
auto publicKey = authData.mid(AuthDataOffsets::CREDENTIAL_ID + credLen);
|
||||
|
||||
QJsonObject credentialDataJson({{"aaguid", browserMessageBuilder()->getBase64FromArray(aaGuid)},
|
||||
{"credentialId", browserMessageBuilder()->getBase64FromArray(credentialId)},
|
||||
{"publicKey", m_browserCbor.getJsonFromCborData(publicKey)}});
|
||||
|
||||
QJsonObject result({{"credentialData", credentialDataJson},
|
||||
{"flags", parseFlags(flags)},
|
||||
{"rpIdHash", browserMessageBuilder()->getBase64FromArray(rpIdHash)},
|
||||
{"signatureCounter", QJsonValue(qFromBigEndian<int>(counter))}});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// See: https://w3c.github.io/webauthn/#table-authData
|
||||
QJsonObject BrowserPasskeys::parseFlags(const QByteArray& flags) const
|
||||
{
|
||||
if (flags.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
auto flagsByte = static_cast<uint8_t>(flags[0]);
|
||||
std::bitset<8> flagBits(flagsByte);
|
||||
|
||||
return QJsonObject({{"ED", flagBits.test(AuthenticatorFlags::ED)},
|
||||
{"AT", flagBits.test(AuthenticatorFlags::AT)},
|
||||
{"BS", flagBits.test(AuthenticatorFlags::BS)},
|
||||
{"BE", flagBits.test(AuthenticatorFlags::BE)},
|
||||
{"UV", flagBits.test(AuthenticatorFlags::UV)},
|
||||
{"UP", flagBits.test(AuthenticatorFlags::UP)}});
|
||||
}
|
||||
|
||||
char BrowserPasskeys::setFlagsFromJson(const QJsonObject& flags) const
|
||||
{
|
||||
if (flags.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
char flagBits = 0x00;
|
||||
auto setFlag = [&](const char* key, unsigned char bit) {
|
||||
if (flags[key].toBool()) {
|
||||
flagBits |= 1 << bit;
|
||||
}
|
||||
};
|
||||
|
||||
setFlag("ED", AuthenticatorFlags::ED);
|
||||
setFlag("AT", AuthenticatorFlags::AT);
|
||||
setFlag("BS", AuthenticatorFlags::BS);
|
||||
setFlag("BE", AuthenticatorFlags::BE);
|
||||
setFlag("UV", AuthenticatorFlags::UV);
|
||||
setFlag("UP", AuthenticatorFlags::UP);
|
||||
|
||||
return flagBits;
|
||||
}
|
||||
|
||||
// Returns the first supported algorithm from the pubKeyCredParams list (only support ES256, RS256 and EdDSA for now)
|
||||
WebAuthnAlgorithms BrowserPasskeys::getAlgorithmFromPublicKey(const QJsonObject& publicKey) const
|
||||
{
|
||||
const auto pubKeyCredParams = publicKey["pubKeyCredParams"].toArray();
|
||||
if (!pubKeyCredParams.isEmpty()) {
|
||||
const auto alg = pubKeyCredParams.first()["alg"].toInt();
|
||||
if (alg == WebAuthnAlgorithms::ES256 || alg == WebAuthnAlgorithms::RS256 || alg == WebAuthnAlgorithms::EDDSA) {
|
||||
return static_cast<WebAuthnAlgorithms>(alg);
|
||||
}
|
||||
}
|
||||
|
||||
return WebAuthnAlgorithms::ES256;
|
||||
}
|
||||
|
||||
QByteArray BrowserPasskeys::bigIntToQByteArray(Botan::BigInt& bigInt) const
|
||||
{
|
||||
auto hexString = QString(bigInt.to_hex_string().c_str());
|
||||
|
||||
// Botan might add a leading "0x" to the hex string depending on the version. Remove it.
|
||||
if (hexString.startsWith(("0x"))) {
|
||||
hexString.remove(0, 2);
|
||||
}
|
||||
|
||||
return browserMessageBuilder()->getArrayFromHexString(hexString);
|
||||
}
|
143
src/browser/BrowserPasskeys.h
Normal file
143
src/browser/BrowserPasskeys.h
Normal file
@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERPASSKEYS_H
|
||||
#define BROWSERPASSKEYS_H
|
||||
|
||||
#include "BrowserCbor.h"
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
|
||||
#include <botan/asn1_obj.h>
|
||||
#include <botan/bigint.h>
|
||||
|
||||
#define ID_BYTES 32
|
||||
#define HASH_BYTES 32
|
||||
#define DEFAULT_TIMEOUT 300000
|
||||
#define DEFAULT_DISCOURAGED_TIMEOUT 120000
|
||||
#define RSA_BITS 2048
|
||||
#define RSA_EXPONENT 65537
|
||||
|
||||
enum AuthDataOffsets : int
|
||||
{
|
||||
RPIDHASH = 0,
|
||||
FLAGS = 32,
|
||||
SIGNATURE_COUNTER = 33,
|
||||
AAGUID = 37,
|
||||
CREDENTIAL_LENGTH = 53,
|
||||
CREDENTIAL_ID = 55
|
||||
};
|
||||
|
||||
enum AuthenticatorFlags
|
||||
{
|
||||
UP = 0,
|
||||
UV = 2,
|
||||
BE = 3,
|
||||
BS = 4,
|
||||
AT = 6,
|
||||
ED = 7
|
||||
};
|
||||
|
||||
struct PublicKeyCredential
|
||||
{
|
||||
QString id;
|
||||
QJsonObject response;
|
||||
QByteArray key;
|
||||
};
|
||||
|
||||
struct PrivateKey
|
||||
{
|
||||
QByteArray cborEncoded;
|
||||
QByteArray pem;
|
||||
};
|
||||
|
||||
// Predefined variables used for testing the class
|
||||
struct TestingVariables
|
||||
{
|
||||
QString credentialId;
|
||||
QString first;
|
||||
QString second;
|
||||
};
|
||||
|
||||
class BrowserPasskeys : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BrowserPasskeys() = default;
|
||||
~BrowserPasskeys() = default;
|
||||
static BrowserPasskeys* instance();
|
||||
|
||||
PublicKeyCredential buildRegisterPublicKeyCredential(const QJsonObject& publicKeyCredentialOptions,
|
||||
const QString& origin,
|
||||
const TestingVariables& predefinedVariables = {});
|
||||
QJsonObject buildGetPublicKeyCredential(const QJsonObject& publicKeyCredentialRequestOptions,
|
||||
const QString& origin,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKeyPem);
|
||||
bool isUserVerificationValid(const QString& userVerification) const;
|
||||
int getTimeout(const QString& userVerification, int timeout) const;
|
||||
QStringList getAllowedCredentialsFromPublicKey(const QJsonObject& publicKey) const;
|
||||
|
||||
static const QString PUBLIC_KEY;
|
||||
static const QString REQUIREMENT_DISCOURAGED;
|
||||
static const QString REQUIREMENT_PREFERRED;
|
||||
static const QString REQUIREMENT_REQUIRED;
|
||||
|
||||
static const QString PASSKEYS_ATTESTATION_DIRECT;
|
||||
static const QString PASSKEYS_ATTESTATION_NONE;
|
||||
|
||||
static const QString KPEX_PASSKEY_USERNAME;
|
||||
static const QString KPEX_PASSKEY_GENERATED_USER_ID;
|
||||
static const QString KPEX_PASSKEY_PRIVATE_KEY_PEM;
|
||||
static const QString KPEX_PASSKEY_RELYING_PARTY;
|
||||
static const QString KPEX_PASSKEY_USER_HANDLE;
|
||||
|
||||
private:
|
||||
QJsonObject buildClientDataJson(const QJsonObject& publicKey, const QString& origin, bool get);
|
||||
PrivateKey buildAttestationObject(const QJsonObject& publicKey,
|
||||
const QString& extensions,
|
||||
const QString& id,
|
||||
const TestingVariables& predefinedVariables = {});
|
||||
QByteArray buildGetAttestationObject(const QJsonObject& publicKey);
|
||||
PrivateKey buildCredentialPrivateKey(int alg,
|
||||
const QString& predefinedFirst = QString(),
|
||||
const QString& predefinedSecond = QString());
|
||||
QByteArray
|
||||
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);
|
||||
QByteArray buildExtensionData(QJsonObject& extensionObject) const;
|
||||
QJsonObject parseAuthData(const QByteArray& authData) const;
|
||||
QJsonObject parseFlags(const QByteArray& flags) const;
|
||||
char setFlagsFromJson(const QJsonObject& flags) const;
|
||||
WebAuthnAlgorithms getAlgorithmFromPublicKey(const QJsonObject& publicKey) const;
|
||||
QByteArray bigIntToQByteArray(Botan::BigInt& bigInt) const;
|
||||
|
||||
Q_DISABLE_COPY(BrowserPasskeys);
|
||||
|
||||
friend class TestPasskeys;
|
||||
|
||||
private:
|
||||
BrowserCbor m_browserCbor;
|
||||
};
|
||||
|
||||
static inline BrowserPasskeys* browserPasskeys()
|
||||
{
|
||||
return BrowserPasskeys::instance();
|
||||
}
|
||||
|
||||
#endif // BROWSERPASSKEYS_H
|
153
src/browser/BrowserPasskeysConfirmationDialog.cpp
Normal file
153
src/browser/BrowserPasskeysConfirmationDialog.cpp
Normal file
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "BrowserPasskeysConfirmationDialog.h"
|
||||
#include "ui_BrowserPasskeysConfirmationDialog.h"
|
||||
|
||||
#include "core/Entry.h"
|
||||
#include <QCloseEvent>
|
||||
#include <QUrl>
|
||||
|
||||
#define STEP 1000
|
||||
|
||||
BrowserPasskeysConfirmationDialog::BrowserPasskeysConfirmationDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::BrowserPasskeysConfirmationDialog())
|
||||
, m_passkeyUpdated(false)
|
||||
{
|
||||
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
|
||||
m_ui->setupUi(this);
|
||||
m_ui->updateButton->setVisible(false);
|
||||
|
||||
connect(m_ui->credentialsTable, SIGNAL(cellDoubleClicked(int, int)), this, SLOT(accept()));
|
||||
connect(m_ui->confirmButton, SIGNAL(clicked()), SLOT(accept()));
|
||||
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
|
||||
connect(m_ui->updateButton, SIGNAL(clicked()), SLOT(updatePasskey()));
|
||||
|
||||
connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
|
||||
connect(&m_timer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
|
||||
}
|
||||
|
||||
BrowserPasskeysConfirmationDialog::~BrowserPasskeysConfirmationDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::registerCredential(const QString& username,
|
||||
const QString& siteId,
|
||||
const QList<Entry*>& existingEntries,
|
||||
int timeout)
|
||||
{
|
||||
m_ui->firstLabel->setText(tr("Do you want to register Passkey for:"));
|
||||
m_ui->dataLabel->setText(tr("%1 (%2)").arg(username, siteId));
|
||||
m_ui->secondLabel->setText("");
|
||||
|
||||
if (!existingEntries.isEmpty()) {
|
||||
m_ui->firstLabel->setText(tr("Existing Passkey found.\nDo you want to register a new Passkey for:"));
|
||||
m_ui->secondLabel->setText(tr("Select the existing Passkey and press Update to replace it."));
|
||||
|
||||
m_ui->updateButton->setVisible(true);
|
||||
m_ui->confirmButton->setText(tr("Register new"));
|
||||
updateEntriesToTable(existingEntries);
|
||||
} else {
|
||||
m_ui->confirmButton->setText(tr("Register"));
|
||||
m_ui->credentialsTable->setVisible(false);
|
||||
}
|
||||
|
||||
startCounter(timeout);
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::authenticateCredential(const QList<Entry*>& entries,
|
||||
const QString& origin,
|
||||
int timeout)
|
||||
{
|
||||
m_ui->firstLabel->setText(tr("Authenticate Passkey credentials for:"));
|
||||
m_ui->dataLabel->setText(origin);
|
||||
m_ui->secondLabel->setText("");
|
||||
updateEntriesToTable(entries);
|
||||
startCounter(timeout);
|
||||
}
|
||||
|
||||
Entry* BrowserPasskeysConfirmationDialog::getSelectedEntry() const
|
||||
{
|
||||
auto selectedItem = m_ui->credentialsTable->currentItem();
|
||||
return selectedItem ? m_entries[selectedItem->row()] : nullptr;
|
||||
}
|
||||
|
||||
bool BrowserPasskeysConfirmationDialog::isPasskeyUpdated() const
|
||||
{
|
||||
return m_passkeyUpdated;
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::updatePasskey()
|
||||
{
|
||||
m_passkeyUpdated = true;
|
||||
emit accept();
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::updateProgressBar()
|
||||
{
|
||||
if (m_counter < m_ui->progressBar->maximum()) {
|
||||
m_ui->progressBar->setValue(m_ui->progressBar->maximum() - m_counter);
|
||||
m_ui->progressBar->update();
|
||||
} else {
|
||||
emit reject();
|
||||
}
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::updateSeconds()
|
||||
{
|
||||
++m_counter;
|
||||
updateTimeoutLabel();
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::startCounter(int timeout)
|
||||
{
|
||||
m_counter = 0;
|
||||
m_ui->progressBar->setMaximum(timeout / STEP);
|
||||
updateProgressBar();
|
||||
updateTimeoutLabel();
|
||||
m_timer.start(STEP);
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::updateTimeoutLabel()
|
||||
{
|
||||
m_ui->timeoutLabel->setText(tr("Timeout in <b>%n</b> seconds...", "", m_ui->progressBar->maximum() - m_counter));
|
||||
}
|
||||
|
||||
void BrowserPasskeysConfirmationDialog::updateEntriesToTable(const QList<Entry*>& entries)
|
||||
{
|
||||
m_entries = entries;
|
||||
m_ui->credentialsTable->setRowCount(entries.count());
|
||||
m_ui->credentialsTable->setColumnCount(1);
|
||||
|
||||
int row = 0;
|
||||
for (const auto& entry : entries) {
|
||||
auto item = new QTableWidgetItem();
|
||||
item->setText(entry->title() + " - " + entry->username());
|
||||
m_ui->credentialsTable->setItem(row, 0, item);
|
||||
|
||||
if (row == 0) {
|
||||
item->setSelected(true);
|
||||
}
|
||||
|
||||
++row;
|
||||
}
|
||||
|
||||
m_ui->credentialsTable->resizeColumnsToContents();
|
||||
m_ui->credentialsTable->horizontalHeader()->setStretchLastSection(true);
|
||||
}
|
66
src/browser/BrowserPasskeysConfirmationDialog.h
Normal file
66
src/browser/BrowserPasskeysConfirmationDialog.h
Normal file
@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H
|
||||
#define KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
#include <QTimer>
|
||||
|
||||
class Entry;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class BrowserPasskeysConfirmationDialog;
|
||||
}
|
||||
|
||||
class BrowserPasskeysConfirmationDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit BrowserPasskeysConfirmationDialog(QWidget* parent = nullptr);
|
||||
~BrowserPasskeysConfirmationDialog() override;
|
||||
|
||||
void registerCredential(const QString& username,
|
||||
const QString& siteId,
|
||||
const QList<Entry*>& existingEntries,
|
||||
int timeout);
|
||||
void authenticateCredential(const QList<Entry*>& entries, const QString& origin, int timeout);
|
||||
Entry* getSelectedEntry() const;
|
||||
bool isPasskeyUpdated() const;
|
||||
|
||||
private slots:
|
||||
void updatePasskey();
|
||||
void updateProgressBar();
|
||||
void updateSeconds();
|
||||
|
||||
private:
|
||||
void startCounter(int timeout);
|
||||
void updateTimeoutLabel();
|
||||
void updateEntriesToTable(const QList<Entry*>& entries);
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::BrowserPasskeysConfirmationDialog> m_ui;
|
||||
QList<Entry*> m_entries;
|
||||
QTimer m_timer;
|
||||
int m_counter;
|
||||
bool m_passkeyUpdated;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_BROWSERPASSKEYSCONFIRMATIONDIALOG_H
|
159
src/browser/BrowserPasskeysConfirmationDialog.ui
Executable file
159
src/browser/BrowserPasskeysConfirmationDialog.ui
Executable file
@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BrowserPasskeysConfirmationDialog</class>
|
||||
<widget class="QDialog" name="BrowserPasskeysConfirmationDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>405</width>
|
||||
<height>282</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>KeePassXC: Passkey credentials</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="firstLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="dataLabel">
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="secondLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Expanding</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>10</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="credentialsTable">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="cornerButtonEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QProgressBar" name="progressBar">
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="textVisible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="timeoutLabel"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="updateButton">
|
||||
<property name="text">
|
||||
<string>Update</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="confirmButton">
|
||||
<property name="text">
|
||||
<string>Authenticate</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -29,6 +29,10 @@
|
||||
#include "gui/MainWindow.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include "gui/osutils/OSUtils.h"
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
#include "BrowserPasskeys.h"
|
||||
#include "BrowserPasskeysConfirmationDialog.h"
|
||||
#endif
|
||||
#ifdef Q_OS_MACOS
|
||||
#include "gui/osutils/macutils/MacUtils.h"
|
||||
#endif
|
||||
@ -48,6 +52,9 @@ const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-
|
||||
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
|
||||
static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
|
||||
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
static int KEEPASSXCBROWSER_PASSKEY_ICON = 13;
|
||||
#endif
|
||||
// These are for the settings and password conversion
|
||||
static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
|
||||
static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
|
||||
@ -607,6 +614,177 @@ QString BrowserService::getKey(const QString& id)
|
||||
return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
// Passkey registration
|
||||
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKey,
|
||||
const QString& origin,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
auto db = selectedDatabase();
|
||||
if (!db) {
|
||||
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
|
||||
}
|
||||
|
||||
const auto userJson = publicKey["user"].toObject();
|
||||
const auto username = userJson["name"].toString();
|
||||
const auto userHandle = userJson["id"].toString();
|
||||
const auto rpId = publicKey["rp"]["id"].toString();
|
||||
const auto rpName = publicKey["rp"]["name"].toString();
|
||||
const auto timeoutValue = publicKey["timeout"].toInt();
|
||||
const auto excludeCredentials = publicKey["excludeCredentials"].toArray();
|
||||
const auto attestation = publicKey["attestation"].toString();
|
||||
|
||||
// Only support these two for now
|
||||
if (attestation != BrowserPasskeys::PASSKEYS_ATTESTATION_NONE
|
||||
&& attestation != BrowserPasskeys::PASSKEYS_ATTESTATION_DIRECT) {
|
||||
return getPasskeyError(ERROR_PASSKEYS_ATTESTATION_NOT_SUPPORTED);
|
||||
}
|
||||
|
||||
const auto authenticatorSelection = publicKey["authenticatorSelection"].toObject();
|
||||
const auto userVerification = authenticatorSelection["userVerification"].toString();
|
||||
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
|
||||
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
|
||||
}
|
||||
|
||||
if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, origin, keyList)) {
|
||||
return getPasskeyError(ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED);
|
||||
}
|
||||
|
||||
const auto existingEntries = getPasskeyEntries(rpId, keyList);
|
||||
const auto timeout = browserPasskeys()->getTimeout(userVerification, timeoutValue);
|
||||
|
||||
raiseWindow();
|
||||
BrowserPasskeysConfirmationDialog confirmDialog;
|
||||
confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
|
||||
|
||||
auto dialogResult = confirmDialog.exec();
|
||||
if (dialogResult == QDialog::Accepted) {
|
||||
const auto publicKeyCredentials = browserPasskeys()->buildRegisterPublicKeyCredential(publicKey, origin);
|
||||
|
||||
if (confirmDialog.isPasskeyUpdated()) {
|
||||
addPasskeyToEntry(confirmDialog.getSelectedEntry(),
|
||||
rpId,
|
||||
rpName,
|
||||
username,
|
||||
publicKeyCredentials.id,
|
||||
userHandle,
|
||||
publicKeyCredentials.key);
|
||||
} else {
|
||||
addPasskeyToGroup(
|
||||
nullptr, origin, rpId, rpName, username, publicKeyCredentials.id, userHandle, publicKeyCredentials.key);
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
return publicKeyCredentials.response;
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
|
||||
}
|
||||
|
||||
// Passkey authentication
|
||||
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
|
||||
const QString& origin,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
auto db = selectedDatabase();
|
||||
if (!db) {
|
||||
return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
|
||||
}
|
||||
|
||||
const auto userVerification = publicKey["userVerification"].toString();
|
||||
if (!browserPasskeys()->isUserVerificationValid(userVerification)) {
|
||||
return getPasskeyError(ERROR_PASSKEYS_INVALID_USER_VERIFICATION);
|
||||
}
|
||||
|
||||
// Parse "allowCredentials"
|
||||
const auto rpId = publicKey["rpId"].toString();
|
||||
const auto entries = getPasskeyAllowedEntries(publicKey, rpId, keyList);
|
||||
if (entries.isEmpty()) {
|
||||
return getPasskeyError(ERROR_KEEPASS_NO_LOGINS_FOUND);
|
||||
}
|
||||
|
||||
// With single entry, if no verification is needed, return directly
|
||||
if (entries.count() == 1 && userVerification == BrowserPasskeys::REQUIREMENT_DISCOURAGED) {
|
||||
return getPublicKeyCredentialFromEntry(entries.first(), publicKey, origin);
|
||||
}
|
||||
|
||||
const auto timeout = publicKey["timeout"].toInt();
|
||||
|
||||
raiseWindow();
|
||||
BrowserPasskeysConfirmationDialog confirmDialog;
|
||||
confirmDialog.authenticateCredential(entries, origin, timeout);
|
||||
auto dialogResult = confirmDialog.exec();
|
||||
if (dialogResult == QDialog::Accepted) {
|
||||
hideWindow();
|
||||
const auto selectedEntry = confirmDialog.getSelectedEntry();
|
||||
return getPublicKeyCredentialFromEntry(selectedEntry, publicKey, origin);
|
||||
}
|
||||
|
||||
hideWindow();
|
||||
return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
|
||||
}
|
||||
|
||||
void BrowserService::addPasskeyToGroup(Group* group,
|
||||
const QString& url,
|
||||
const QString& rpId,
|
||||
const QString& rpName,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey)
|
||||
{
|
||||
// If no group provided, use the default browser group of the selected database
|
||||
if (!group) {
|
||||
auto db = selectedDatabase();
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
group = getDefaultEntryGroup(db);
|
||||
}
|
||||
|
||||
auto* entry = new Entry();
|
||||
entry->setUuid(QUuid::createUuid());
|
||||
entry->setGroup(group);
|
||||
entry->setTitle(tr("%1 (Passkey)").arg(rpName));
|
||||
entry->setUsername(username);
|
||||
entry->setUrl(url);
|
||||
entry->setIcon(KEEPASSXCBROWSER_PASSKEY_ICON);
|
||||
|
||||
addPasskeyToEntry(entry, rpId, rpName, username, userId, userHandle, privateKey);
|
||||
|
||||
// Remove blank entry history
|
||||
entry->removeHistoryItems(entry->historyItems());
|
||||
}
|
||||
|
||||
void BrowserService::addPasskeyToEntry(Entry* entry,
|
||||
const QString& rpId,
|
||||
const QString& rpName,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey)
|
||||
{
|
||||
// Reserved for future use
|
||||
Q_UNUSED(rpName)
|
||||
|
||||
Q_ASSERT(entry);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry->beginUpdate();
|
||||
|
||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
|
||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID, userId, true);
|
||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
|
||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
|
||||
entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
|
||||
|
||||
entry->endUpdate();
|
||||
}
|
||||
#endif
|
||||
|
||||
void BrowserService::addEntry(const EntryParameters& entryParameters,
|
||||
const QString& group,
|
||||
const QString& groupUuid,
|
||||
@ -621,7 +799,7 @@ void BrowserService::addEntry(const EntryParameters& entryParameters,
|
||||
|
||||
auto* entry = new Entry();
|
||||
entry->setUuid(QUuid::createUuid());
|
||||
entry->setTitle(QUrl(entryParameters.siteUrl).host());
|
||||
entry->setTitle(entryParameters.title.isEmpty() ? QUrl(entryParameters.siteUrl).host() : entryParameters.title);
|
||||
entry->setUrl(entryParameters.siteUrl);
|
||||
entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
|
||||
entry->setUsername(entryParameters.login);
|
||||
@ -667,7 +845,7 @@ bool BrowserService::updateEntry(const EntryParameters& entryParameters, const Q
|
||||
return false;
|
||||
}
|
||||
|
||||
Entry* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
|
||||
auto entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
|
||||
if (!entry) {
|
||||
// If entry is not found for update, add a new one to the selected database
|
||||
addEntry(entryParameters, "", "", false, db);
|
||||
@ -746,8 +924,10 @@ bool BrowserService::deleteEntry(const QString& uuid)
|
||||
return true;
|
||||
}
|
||||
|
||||
QList<Entry*>
|
||||
BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl)
|
||||
QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
|
||||
const QString& siteUrl,
|
||||
const QString& formUrl,
|
||||
bool passkey)
|
||||
{
|
||||
QList<Entry*> entries;
|
||||
auto* rootGroup = db->rootGroup();
|
||||
@ -771,10 +951,17 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
|
||||
if (!passkey && !shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
// With Passkeys, check for the Relying Party instead of URL
|
||||
if (passkey && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
|
||||
continue;
|
||||
}
|
||||
#endif
|
||||
|
||||
// Additional URL check may have already inserted the entry to the list
|
||||
if (!entries.contains(entry)) {
|
||||
entries.append(entry);
|
||||
@ -785,8 +972,10 @@ BrowserService::searchEntries(const QSharedPointer<Database>& db, const QString&
|
||||
return entries;
|
||||
}
|
||||
|
||||
QList<Entry*>
|
||||
BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList)
|
||||
QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
|
||||
const QString& formUrl,
|
||||
const StringPairList& keyList,
|
||||
bool passkey)
|
||||
{
|
||||
// Check if database is connected with KeePassXC-Browser
|
||||
auto databaseConnected = [&](const QSharedPointer<Database>& db) {
|
||||
@ -820,7 +1009,7 @@ BrowserService::searchEntries(const QString& siteUrl, const QString& formUrl, co
|
||||
QList<Entry*> entries;
|
||||
do {
|
||||
for (const auto& db : databases) {
|
||||
entries << searchEntries(db, siteUrl, formUrl);
|
||||
entries << searchEntries(db, siteUrl, formUrl, passkey);
|
||||
}
|
||||
} while (entries.isEmpty() && removeFirstDomain(hostname));
|
||||
|
||||
@ -1094,6 +1283,74 @@ bool BrowserService::shouldIncludeEntry(Entry* entry,
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
// Returns all Passkey entries for the current Relying Party
|
||||
QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const StringPairList& keyList)
|
||||
{
|
||||
QList<Entry*> entries;
|
||||
for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
|
||||
if (entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)
|
||||
&& entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
|
||||
entries << entry;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Get all entries for the site that are allowed by the server
|
||||
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& publicKey,
|
||||
const QString& rpId,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
QList<Entry*> entries;
|
||||
const auto allowedCredentials = browserPasskeys()->getAllowedCredentialsFromPublicKey(publicKey);
|
||||
|
||||
for (const auto& entry : getPasskeyEntries(rpId, keyList)) {
|
||||
// If allowedCredentials.isEmpty() check if entry contains an extra attribute for user handle.
|
||||
// If that is found, the entry should be allowed.
|
||||
// See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
|
||||
if (allowedCredentials.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID))
|
||||
|| (allowedCredentials.isEmpty()
|
||||
&& entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
|
||||
entries << entry;
|
||||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
QJsonObject
|
||||
BrowserService::getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin)
|
||||
{
|
||||
const auto privateKeyPem = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
|
||||
const auto userId = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID);
|
||||
const auto userHandle = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
|
||||
return browserPasskeys()->buildGetPublicKeyCredential(publicKey, origin, userId, userHandle, privateKeyPem);
|
||||
}
|
||||
|
||||
// Checks if the same user ID already exists for the current site
|
||||
bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
|
||||
const QString& origin,
|
||||
const StringPairList& keyList)
|
||||
{
|
||||
QStringList allIds;
|
||||
for (const auto& cred : excludeCredentials) {
|
||||
allIds << cred["id"].toString();
|
||||
}
|
||||
|
||||
const auto passkeyEntries = getPasskeyEntries(origin, keyList);
|
||||
return std::any_of(passkeyEntries.begin(), passkeyEntries.end(), [&](const auto& entry) {
|
||||
return allIds.contains(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID));
|
||||
});
|
||||
}
|
||||
|
||||
QJsonObject BrowserService::getPasskeyError(int errorCode) const
|
||||
{
|
||||
return QJsonObject({{"errorCode", errorCode}});
|
||||
}
|
||||
#endif
|
||||
|
||||
bool BrowserService::handleURL(const QString& entryUrl,
|
||||
const QString& siteUrl,
|
||||
const QString& formUrl,
|
||||
|
@ -17,10 +17,11 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERSERVICE_H
|
||||
#define BROWSERSERVICE_H
|
||||
#ifndef KEEPASSXC_BROWSERSERVICE_H
|
||||
#define KEEPASSXC_BROWSERSERVICE_H
|
||||
|
||||
#include "BrowserAccessControlDialog.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "core/Entry.h"
|
||||
#include "gui/PasswordGeneratorWidget.h"
|
||||
|
||||
@ -45,6 +46,7 @@ struct KeyPairMessage
|
||||
struct EntryParameters
|
||||
{
|
||||
QString dbid;
|
||||
QString title;
|
||||
QString login;
|
||||
QString password;
|
||||
QString realm;
|
||||
@ -82,7 +84,30 @@ public:
|
||||
QString getCurrentTotp(const QString& uuid);
|
||||
void showPasswordGenerator(const KeyPairMessage& keyPairMessage);
|
||||
bool isPasswordGeneratorRequested() const;
|
||||
|
||||
bool isUrlIdentical(const QString& first, const QString& second) const;
|
||||
QSharedPointer<Database> selectedDatabase();
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
QJsonObject
|
||||
showPasskeysRegisterPrompt(const QJsonObject& publicKey, const QString& origin, const StringPairList& keyList);
|
||||
QJsonObject showPasskeysAuthenticationPrompt(const QJsonObject& publicKey,
|
||||
const QString& origin,
|
||||
const StringPairList& keyList);
|
||||
void addPasskeyToGroup(Group* group,
|
||||
const QString& url,
|
||||
const QString& rpId,
|
||||
const QString& rpName,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey);
|
||||
void addPasskeyToEntry(Entry* entry,
|
||||
const QString& rpId,
|
||||
const QString& rpName,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey);
|
||||
#endif
|
||||
void addEntry(const EntryParameters& entryParameters,
|
||||
const QString& group,
|
||||
const QString& groupUuid,
|
||||
@ -129,8 +154,12 @@ private:
|
||||
Hidden
|
||||
};
|
||||
|
||||
QList<Entry*> searchEntries(const QSharedPointer<Database>& db, const QString& siteUrl, const QString& formUrl);
|
||||
QList<Entry*> searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList);
|
||||
QList<Entry*> searchEntries(const QSharedPointer<Database>& db,
|
||||
const QString& siteUrl,
|
||||
const QString& formUrl,
|
||||
bool passkey = false);
|
||||
QList<Entry*>
|
||||
searchEntries(const QString& siteUrl, const QString& formUrl, const StringPairList& keyList, bool passkey = false);
|
||||
QList<Entry*> sortEntries(QList<Entry*>& entries, const QString& siteUrl, const QString& formUrl);
|
||||
QList<Entry*> confirmEntries(QList<Entry*>& entriesToConfirm,
|
||||
const EntryParameters& entryParameters,
|
||||
@ -148,12 +177,22 @@ private:
|
||||
bool removeFirstDomain(QString& hostname);
|
||||
bool
|
||||
shouldIncludeEntry(Entry* entry, const QString& url, const QString& submitUrl, const bool omitWwwSubdomain = false);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
QList<Entry*> getPasskeyEntries(const QString& rpId, const StringPairList& keyList);
|
||||
QList<Entry*>
|
||||
getPasskeyAllowedEntries(const QJsonObject& publicKey, const QString& rpId, const StringPairList& keyList);
|
||||
QJsonObject
|
||||
getPublicKeyCredentialFromEntry(const Entry* entry, const QJsonObject& publicKey, const QString& origin);
|
||||
bool isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
|
||||
const QString& origin,
|
||||
const StringPairList& keyList);
|
||||
QJsonObject getPasskeyError(int errorCode) const;
|
||||
#endif
|
||||
bool handleURL(const QString& entryUrl,
|
||||
const QString& siteUrl,
|
||||
const QString& formUrl,
|
||||
const bool omitWwwSubdomain = false);
|
||||
QSharedPointer<Database> getDatabase();
|
||||
QSharedPointer<Database> selectedDatabase();
|
||||
QString getDatabaseRootUuid();
|
||||
QString getDatabaseRecycleBinUuid();
|
||||
bool checkLegacySettings(QSharedPointer<Database> db);
|
||||
@ -176,6 +215,9 @@ private:
|
||||
Q_DISABLE_COPY(BrowserService);
|
||||
|
||||
friend class TestBrowser;
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
friend class TestPasskeys;
|
||||
#endif
|
||||
};
|
||||
|
||||
static inline BrowserService* browserService()
|
||||
@ -183,4 +225,4 @@ static inline BrowserService* browserService()
|
||||
return BrowserService::instance();
|
||||
}
|
||||
|
||||
#endif // BROWSERSERVICE_H
|
||||
#endif // KEEPASSXC_BROWSERSERVICE_H
|
||||
|
@ -17,8 +17,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BROWSERSETTINGS_H
|
||||
#define BROWSERSETTINGS_H
|
||||
#ifndef KEEPASSXC_BROWSERSETTINGS_H
|
||||
#define KEEPASSXC_BROWSERSETTINGS_H
|
||||
|
||||
#include "NativeMessageInstaller.h"
|
||||
|
||||
@ -92,4 +92,4 @@ inline BrowserSettings* browserSettings()
|
||||
return BrowserSettings::instance();
|
||||
}
|
||||
|
||||
#endif // BROWSERSETTINGS_H
|
||||
#endif // KEEPASSXC_BROWSERSETTINGS_H
|
||||
|
@ -1,5 +1,4 @@
|
||||
# Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
# Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
|
||||
#
|
||||
# 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
|
||||
@ -30,8 +29,14 @@ if(WITH_XC_BROWSER)
|
||||
BrowserSettings.cpp
|
||||
BrowserShared.cpp
|
||||
CustomTableWidget.cpp
|
||||
NativeMessageInstaller.cpp
|
||||
)
|
||||
NativeMessageInstaller.cpp)
|
||||
|
||||
if(WITH_XC_BROWSER_PASSKEYS)
|
||||
list(APPEND keepassxcbrowser_SOURCES
|
||||
BrowserCbor.cpp
|
||||
BrowserPasskeys.cpp
|
||||
BrowserPasskeysConfirmationDialog.cpp)
|
||||
endif()
|
||||
|
||||
add_library(keepassxcbrowser STATIC ${keepassxcbrowser_SOURCES})
|
||||
target_link_libraries(keepassxcbrowser Qt5::Core Qt5::Concurrent Qt5::Widgets Qt5::Network ${BOTAN_LIBRARIES})
|
||||
|
@ -15,6 +15,7 @@
|
||||
#cmakedefine WITH_XC_AUTOTYPE
|
||||
#cmakedefine WITH_XC_NETWORKING
|
||||
#cmakedefine WITH_XC_BROWSER
|
||||
#cmakedefine WITH_XC_BROWSER_PASSKEYS
|
||||
#cmakedefine WITH_XC_YUBIKEY
|
||||
#cmakedefine WITH_XC_SSHAGENT
|
||||
#cmakedefine WITH_XC_KEESHARE
|
||||
|
@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
|
||||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -17,7 +17,6 @@
|
||||
*/
|
||||
|
||||
#include "EntryAttributes.h"
|
||||
|
||||
#include "core/Global.h"
|
||||
|
||||
#include <QRegularExpression>
|
||||
@ -35,6 +34,7 @@ const QString EntryAttributes::SearchInGroupName = "SearchIn";
|
||||
const QString EntryAttributes::SearchTextGroupName = "SearchText";
|
||||
|
||||
const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD";
|
||||
const QString EntryAttributes::PasskeyAttribute = "KPEX_PASSKEY";
|
||||
|
||||
EntryAttributes::EntryAttributes(QObject* parent)
|
||||
: ModifiableObject(parent)
|
||||
@ -57,7 +57,7 @@ QList<QString> EntryAttributes::customKeys() const
|
||||
QList<QString> customKeys;
|
||||
const QList<QString> keyList = keys();
|
||||
for (const QString& key : keyList) {
|
||||
if (!isDefaultAttribute(key)) {
|
||||
if (!isDefaultAttribute(key) && !isPasskeyAttribute(key)) {
|
||||
customKeys.append(key);
|
||||
}
|
||||
}
|
||||
@ -321,3 +321,8 @@ bool EntryAttributes::isDefaultAttribute(const QString& key)
|
||||
{
|
||||
return DefaultAttributes.contains(key);
|
||||
}
|
||||
|
||||
bool EntryAttributes::isPasskeyAttribute(const QString& key)
|
||||
{
|
||||
return key.startsWith(PasskeyAttribute);
|
||||
}
|
||||
|
@ -61,7 +61,9 @@ public:
|
||||
static const QString NotesKey;
|
||||
static const QStringList DefaultAttributes;
|
||||
static const QString RememberCmdExecAttr;
|
||||
static const QString PasskeyAttribute;
|
||||
static bool isDefaultAttribute(const QString& key);
|
||||
static bool isPasskeyAttribute(const QString& key);
|
||||
|
||||
static const QString WantedFieldGroupName;
|
||||
static const QString SearchInGroupName;
|
||||
|
@ -131,6 +131,19 @@ QString Group::tags() const
|
||||
return m_data.tags;
|
||||
}
|
||||
|
||||
QString Group::fullPath() const
|
||||
{
|
||||
QString fullPath;
|
||||
auto group = this;
|
||||
|
||||
do {
|
||||
fullPath.insert(0, "/" + group->name());
|
||||
group = group->parentGroup();
|
||||
} while (group);
|
||||
|
||||
return fullPath;
|
||||
}
|
||||
|
||||
int Group::iconNumber() const
|
||||
{
|
||||
return m_data.iconNumber;
|
||||
|
@ -85,6 +85,7 @@ public:
|
||||
QString name() const;
|
||||
QString notes() const;
|
||||
QString tags() const;
|
||||
QString fullPath() const;
|
||||
int iconNumber() const;
|
||||
const QUuid& iconUuid() const;
|
||||
const TimeInfo& timeInfo() const;
|
||||
|
@ -94,6 +94,9 @@ namespace Tools
|
||||
#ifdef WITH_XC_BROWSER
|
||||
extensions += "\n- " + QObject::tr("Browser Integration");
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
extensions += "\n- " + QObject::tr("Passkeys");
|
||||
#endif
|
||||
#ifdef WITH_XC_SSHAGENT
|
||||
extensions += "\n- " + QObject::tr("SSH Agent");
|
||||
#endif
|
||||
@ -408,6 +411,16 @@ namespace Tools
|
||||
return subbed;
|
||||
}
|
||||
|
||||
QString cleanFilename(QString filename)
|
||||
{
|
||||
// Remove forward slash from title on all platforms
|
||||
filename.replace("/", "_");
|
||||
// Remove invalid characters
|
||||
filename.remove(QRegularExpression("[:*?\"<>|]"));
|
||||
|
||||
return filename.trimmed();
|
||||
}
|
||||
|
||||
QVariantMap qo2qvm(const QObject* object, const QStringList& ignoredProperties)
|
||||
{
|
||||
QVariantMap result;
|
||||
|
@ -43,6 +43,7 @@ namespace Tools
|
||||
bool isValidUuid(const QString& uuidStr);
|
||||
QString envSubstitute(const QString& filepath,
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment());
|
||||
QString cleanFilename(QString filename);
|
||||
|
||||
/**
|
||||
* Escapes all characters in regex such that they do not receive any special treatment when used
|
||||
|
@ -556,6 +556,18 @@ void DatabaseTabWidget::showDatabaseSettings()
|
||||
currentDatabaseWidget()->switchToDatabaseSettings();
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void DatabaseTabWidget::showPasskeys()
|
||||
{
|
||||
currentDatabaseWidget()->switchToPasskeys();
|
||||
}
|
||||
|
||||
void DatabaseTabWidget::importPasskey()
|
||||
{
|
||||
currentDatabaseWidget()->switchToImportPasskey();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool DatabaseTabWidget::isModified(int index) const
|
||||
{
|
||||
if (count() == 0) {
|
||||
|
@ -19,6 +19,7 @@
|
||||
#define KEEPASSX_DATABASETABWIDGET_H
|
||||
|
||||
#include "DatabaseOpenDialog.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "gui/MessageWidget.h"
|
||||
|
||||
#include <QTabWidget>
|
||||
@ -84,6 +85,10 @@ public slots:
|
||||
void showDatabaseSecurity();
|
||||
void showDatabaseReports();
|
||||
void showDatabaseSettings();
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void showPasskeys();
|
||||
void importPasskey();
|
||||
#endif
|
||||
void performGlobalAutoType(const QString& search);
|
||||
void performBrowserUnlock();
|
||||
|
||||
|
@ -64,6 +64,10 @@
|
||||
#include "sshagent/SSHAgent.h"
|
||||
#endif
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
#include "gui/passkeys/PasskeyImporter.h"
|
||||
#endif
|
||||
|
||||
DatabaseWidget::DatabaseWidget(QSharedPointer<Database> db, QWidget* parent)
|
||||
: QStackedWidget(parent)
|
||||
, m_db(std::move(db))
|
||||
@ -1396,6 +1400,20 @@ void DatabaseWidget::switchToDatabaseSecurity()
|
||||
m_databaseSettingDialog->showDatabaseKeySettings();
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void DatabaseWidget::switchToPasskeys()
|
||||
{
|
||||
switchToDatabaseReports();
|
||||
m_reportsDialog->activatePasskeysPage();
|
||||
}
|
||||
|
||||
void DatabaseWidget::switchToImportPasskey()
|
||||
{
|
||||
PasskeyImporter passkeyImporter;
|
||||
passkeyImporter.importPasskey(m_db);
|
||||
}
|
||||
#endif
|
||||
|
||||
void DatabaseWidget::performUnlockDatabase(const QString& password, const QString& keyfile)
|
||||
{
|
||||
if (password.isEmpty() && keyfile.isEmpty()) {
|
||||
|
@ -212,6 +212,10 @@ public slots:
|
||||
void switchToDatabaseSecurity();
|
||||
void switchToDatabaseReports();
|
||||
void switchToDatabaseSettings();
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void switchToPasskeys();
|
||||
void switchToImportPasskey();
|
||||
#endif
|
||||
void switchToOpenDatabase();
|
||||
void switchToOpenDatabase(const QString& filePath);
|
||||
void switchToOpenDatabase(const QString& filePath, const QString& password, const QString& keyFile);
|
||||
|
@ -438,6 +438,11 @@ MainWindow::MainWindow()
|
||||
m_ui->actionKeyboardShortcuts->setIcon(icons()->icon("keyboard-shortcuts"));
|
||||
m_ui->actionCheckForUpdates->setIcon(icons()->icon("system-software-update"));
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
m_ui->actionPasskeys->setIcon(icons()->icon("passkey"));
|
||||
m_ui->actionImportPasskey->setIcon(icons()->icon("document-import"));
|
||||
#endif
|
||||
|
||||
m_actionMultiplexer.connect(
|
||||
SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(setMenuActionState(DatabaseWidget::Mode)));
|
||||
m_actionMultiplexer.connect(SIGNAL(groupChanged()), this, SLOT(setMenuActionState()));
|
||||
@ -483,6 +488,10 @@ MainWindow::MainWindow()
|
||||
connect(m_ui->actionDatabaseSecurity, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseSecurity()));
|
||||
connect(m_ui->actionReports, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseReports()));
|
||||
connect(m_ui->actionDatabaseSettings, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showDatabaseSettings()));
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
connect(m_ui->actionPasskeys, SIGNAL(triggered()), m_ui->tabWidget, SLOT(showPasskeys()));
|
||||
connect(m_ui->actionImportPasskey, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importPasskey()));
|
||||
#endif
|
||||
connect(m_ui->actionImportCsv, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importCsv()));
|
||||
connect(m_ui->actionImportKeePass1, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importKeePass1Database()));
|
||||
connect(m_ui->actionImportOpVault, SIGNAL(triggered()), m_ui->tabWidget, SLOT(importOpVaultDatabase()));
|
||||
@ -977,6 +986,10 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
|
||||
m_ui->actionExportHtml->setEnabled(true);
|
||||
m_ui->actionExportXML->setEnabled(true);
|
||||
m_ui->actionDatabaseMerge->setEnabled(m_ui->tabWidget->currentIndex() != -1);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
m_ui->actionPasskeys->setEnabled(true);
|
||||
m_ui->actionImportPasskey->setEnabled(true);
|
||||
#endif
|
||||
#ifdef WITH_XC_SSHAGENT
|
||||
bool singleEntryHasSshKey =
|
||||
singleEntrySelected && sshAgent()->isEnabled() && dbWidget->currentEntryHasSshKey();
|
||||
@ -1044,6 +1057,14 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
|
||||
m_ui->actionEntryRemoveFromAgent->setVisible(false);
|
||||
m_ui->actionGroupEmptyRecycleBin->setVisible(false);
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
m_ui->actionPasskeys->setEnabled(false);
|
||||
m_ui->actionImportPasskey->setEnabled(false);
|
||||
#else
|
||||
m_ui->actionPasskeys->setVisible(false);
|
||||
m_ui->actionImportPasskey->setVisible(false);
|
||||
#endif
|
||||
|
||||
m_searchWidgetAction->setEnabled(false);
|
||||
break;
|
||||
}
|
||||
|
@ -262,6 +262,9 @@
|
||||
<addaction name="actionDatabaseSettings"/>
|
||||
<addaction name="actionDatabaseSecurity"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionPasskeys"/>
|
||||
<addaction name="actionImportPasskey"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionDatabaseMerge"/>
|
||||
<addaction name="menuImport"/>
|
||||
<addaction name="menuExport"/>
|
||||
@ -620,6 +623,34 @@
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionPasskeys">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Passkeys…</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Passkeys</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionImportPasskey">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import Passkey</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Import Passkey</string>
|
||||
</property>
|
||||
<property name="menuRole">
|
||||
<enum>QAction::NoRole</enum>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionEntryClone">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
|
91
src/gui/passkeys/PasskeyExportDialog.cpp
Normal file
91
src/gui/passkeys/PasskeyExportDialog.cpp
Normal file
@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PasskeyExportDialog.h"
|
||||
#include "ui_PasskeyExportDialog.h"
|
||||
|
||||
#include "core/Entry.h"
|
||||
#include "gui/FileDialog.h"
|
||||
|
||||
PasskeyExportDialog::PasskeyExportDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::PasskeyExportDialog())
|
||||
{
|
||||
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
|
||||
m_ui->setupUi(this);
|
||||
|
||||
connect(m_ui->exportButton, SIGNAL(clicked()), SLOT(accept()));
|
||||
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
|
||||
connect(m_ui->itemsTable->selectionModel(),
|
||||
SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
||||
this,
|
||||
SLOT(selectionChanged()));
|
||||
}
|
||||
|
||||
PasskeyExportDialog::~PasskeyExportDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void PasskeyExportDialog::setEntries(const QList<Entry*>& items)
|
||||
{
|
||||
m_ui->itemsTable->setRowCount(items.count());
|
||||
m_ui->itemsTable->setColumnCount(1);
|
||||
|
||||
int row = 0;
|
||||
for (const auto& entry : items) {
|
||||
auto item = new QTableWidgetItem();
|
||||
item->setText(entry->title() + " - " + entry->username());
|
||||
item->setData(Qt::UserRole, row);
|
||||
item->setFlags(item->flags() | Qt::ItemIsSelectable);
|
||||
m_ui->itemsTable->setItem(row, 0, item);
|
||||
|
||||
++row;
|
||||
}
|
||||
m_ui->itemsTable->resizeColumnsToContents();
|
||||
m_ui->itemsTable->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_ui->itemsTable->selectAll();
|
||||
m_ui->exportButton->setFocus();
|
||||
}
|
||||
|
||||
QList<QTableWidgetItem*> PasskeyExportDialog::getSelectedItems() const
|
||||
{
|
||||
QList<QTableWidgetItem*> selected;
|
||||
for (int i = 0; i < m_ui->itemsTable->rowCount(); ++i) {
|
||||
auto item = m_ui->itemsTable->item(i, 0);
|
||||
if (item->isSelected()) {
|
||||
selected.append(item);
|
||||
}
|
||||
}
|
||||
return selected;
|
||||
}
|
||||
|
||||
void PasskeyExportDialog::selectionChanged()
|
||||
{
|
||||
auto indexes = m_ui->itemsTable->selectionModel()->selectedIndexes();
|
||||
m_ui->exportButton->setEnabled(!indexes.isEmpty());
|
||||
|
||||
if (indexes.isEmpty()) {
|
||||
m_ui->exportButton->clearFocus();
|
||||
m_ui->cancelButton->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
QString PasskeyExportDialog::selectExportFolder()
|
||||
{
|
||||
return fileDialog()->getExistingDirectory(this, tr("Export to folder"), FileDialog::getLastDir("passkey"));
|
||||
}
|
52
src/gui/passkeys/PasskeyExportDialog.h
Normal file
52
src/gui/passkeys/PasskeyExportDialog.h
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PASSKEYEXPORTDIALOG_H
|
||||
#define KEEPASSXC_PASSKEYEXPORTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QTableWidget>
|
||||
|
||||
class Entry;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class PasskeyExportDialog;
|
||||
}
|
||||
|
||||
class PasskeyExportDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PasskeyExportDialog(QWidget* parent = nullptr);
|
||||
~PasskeyExportDialog() override;
|
||||
|
||||
void setEntries(const QList<Entry*>& items);
|
||||
QList<QTableWidgetItem*> getSelectedItems() const;
|
||||
QString selectExportFolder();
|
||||
|
||||
private slots:
|
||||
void selectionChanged();
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::PasskeyExportDialog> m_ui;
|
||||
QList<Entry*> m_entriesToConfirm;
|
||||
QList<Entry*> m_allowedEntries;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_PASSKEYEXPORTDIALOG_H
|
121
src/gui/passkeys/PasskeyExportDialog.ui
Executable file
121
src/gui/passkeys/PasskeyExportDialog.ui
Executable file
@ -0,0 +1,121 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PasskeyExportDialog</class>
|
||||
<widget class="QDialog" name="PasskeyExportDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>540</width>
|
||||
<height>320</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>KeePassXC - Passkey Export</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="exportLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export the following Passkey entries.</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="filenameLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Filenames will be generated with title and .passkey file extension.</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableWidget" name="itemsTable">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::ExtendedSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="cornerButtonEnabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="exportButton">
|
||||
<property name="accessibleName">
|
||||
<string>Export entries</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Export Selected</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
105
src/gui/passkeys/PasskeyExporter.cpp
Normal file
105
src/gui/passkeys/PasskeyExporter.cpp
Normal file
@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PasskeyExporter.h"
|
||||
#include "PasskeyExportDialog.h"
|
||||
|
||||
#include "browser/BrowserPasskeys.h"
|
||||
#include "core/Entry.h"
|
||||
#include "core/Tools.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
void PasskeyExporter::showExportDialog(const QList<Entry*>& items)
|
||||
{
|
||||
if (items.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PasskeyExportDialog passkeyExportDialog;
|
||||
passkeyExportDialog.setEntries(items);
|
||||
auto ret = passkeyExportDialog.exec();
|
||||
|
||||
if (ret == QDialog::Accepted) {
|
||||
// Select folder
|
||||
auto folder = passkeyExportDialog.selectExportFolder();
|
||||
if (folder.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auto selectedItems = passkeyExportDialog.getSelectedItems();
|
||||
for (const auto& item : selectedItems) {
|
||||
auto entry = items[item->row()];
|
||||
exportSelectedEntry(entry, folder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an export file for a Passkey credential
|
||||
*
|
||||
* File contents in JSON:
|
||||
* {
|
||||
* "privateKey": <private key>,
|
||||
* "relyingParty: <relying party>,
|
||||
* "url": <URL>,
|
||||
* "userHandle": <user handle>,
|
||||
* "userId": <generated user id>,
|
||||
* "username:" <username>
|
||||
* }
|
||||
*/
|
||||
void PasskeyExporter::exportSelectedEntry(const Entry* entry, const QString& folder)
|
||||
{
|
||||
const auto fullPath = QString("%1/%2.passkey").arg(folder, Tools::cleanFilename(entry->title()));
|
||||
if (QFile::exists(fullPath)) {
|
||||
auto dialogResult = MessageBox::warning(nullptr,
|
||||
tr("KeePassXC: Passkey Export"),
|
||||
tr("File \"%1.passkey\" already exists.\n"
|
||||
"Do you want to overwrite it?\n")
|
||||
.arg(entry->title()),
|
||||
MessageBox::Yes | MessageBox::No);
|
||||
|
||||
if (dialogResult != MessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
QFile passkeyFile(fullPath);
|
||||
if (!passkeyFile.open(QIODevice::WriteOnly)) {
|
||||
MessageBox::information(
|
||||
nullptr, tr("Cannot open file"), tr("Cannot open file \"%1\" for writing.").arg(fullPath));
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject passkeyObject;
|
||||
passkeyObject["relyingParty"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY);
|
||||
passkeyObject["url"] = entry->url();
|
||||
passkeyObject["username"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USERNAME);
|
||||
passkeyObject["userId"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_GENERATED_USER_ID);
|
||||
passkeyObject["userHandle"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
|
||||
passkeyObject["privateKey"] = entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
|
||||
|
||||
QJsonDocument document(passkeyObject);
|
||||
if (passkeyFile.write(document.toJson()) < 0) {
|
||||
MessageBox::information(
|
||||
nullptr, tr("Cannot write to file"), tr("Cannot open file \"%1\" for writing.").arg(fullPath));
|
||||
}
|
||||
|
||||
passkeyFile.close();
|
||||
}
|
39
src/gui/passkeys/PasskeyExporter.h
Normal file
39
src/gui/passkeys/PasskeyExporter.h
Normal file
@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PASSKEYEXPORTER_H
|
||||
#define KEEPASSXC_PASSKEYEXPORTER_H
|
||||
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
|
||||
class Entry;
|
||||
|
||||
class PasskeyExporter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PasskeyExporter() = default;
|
||||
|
||||
void showExportDialog(const QList<Entry*>& items);
|
||||
|
||||
private:
|
||||
void exportSelectedEntry(const Entry* entry, const QString& folder);
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_PASSKEYEXPORTER_H
|
121
src/gui/passkeys/PasskeyImportDialog.cpp
Normal file
121
src/gui/passkeys/PasskeyImportDialog.cpp
Normal file
@ -0,0 +1,121 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PasskeyImportDialog.h"
|
||||
#include "ui_PasskeyImportDialog.h"
|
||||
|
||||
#include "browser/BrowserService.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "gui/MainWindow.h"
|
||||
#include <QCloseEvent>
|
||||
#include <QFileInfo>
|
||||
|
||||
PasskeyImportDialog::PasskeyImportDialog(QWidget* parent)
|
||||
: QDialog(parent)
|
||||
, m_ui(new Ui::PasskeyImportDialog())
|
||||
, m_useDefaultGroup(true)
|
||||
{
|
||||
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
|
||||
|
||||
m_ui->setupUi(this);
|
||||
m_ui->useDefaultGroupCheckbox->setChecked(true);
|
||||
m_ui->selectGroupComboBox->setEnabled(false);
|
||||
|
||||
connect(m_ui->importButton, SIGNAL(clicked()), SLOT(accept()));
|
||||
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
|
||||
connect(m_ui->selectDatabaseButton, SIGNAL(clicked()), SLOT(selectDatabase()));
|
||||
connect(m_ui->selectGroupComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeGroup(int)));
|
||||
connect(m_ui->useDefaultGroupCheckbox, SIGNAL(stateChanged(int)), SLOT(useDefaultGroupChanged()));
|
||||
}
|
||||
|
||||
PasskeyImportDialog::~PasskeyImportDialog()
|
||||
{
|
||||
}
|
||||
|
||||
void PasskeyImportDialog::setInfo(const QString& url, const QString& username, const QSharedPointer<Database>& database)
|
||||
{
|
||||
m_ui->urlLabel->setText(tr("URL: %1").arg(url));
|
||||
m_ui->usernameLabel->setText(tr("Username: %1").arg(username));
|
||||
m_ui->selectDatabaseLabel->setText(tr("Database: %1").arg(getDatabaseName(database)));
|
||||
m_ui->selectGroupLabel->setText(tr("Group:"));
|
||||
|
||||
addGroups(database);
|
||||
|
||||
auto openDatabaseCount = 0;
|
||||
for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
|
||||
if (dbWidget && !dbWidget->isLocked()) {
|
||||
openDatabaseCount++;
|
||||
}
|
||||
}
|
||||
m_ui->selectDatabaseButton->setEnabled(openDatabaseCount > 1);
|
||||
}
|
||||
|
||||
QSharedPointer<Database> PasskeyImportDialog::getSelectedDatabase()
|
||||
{
|
||||
return m_selectedDatabase;
|
||||
}
|
||||
|
||||
QUuid PasskeyImportDialog::getSelectedGroupUuid()
|
||||
{
|
||||
return m_selectedGroupUuid;
|
||||
}
|
||||
|
||||
bool PasskeyImportDialog::useDefaultGroup()
|
||||
{
|
||||
return m_useDefaultGroup;
|
||||
}
|
||||
|
||||
QString PasskeyImportDialog::getDatabaseName(const QSharedPointer<Database>& database) const
|
||||
{
|
||||
return QFileInfo(database->filePath()).fileName();
|
||||
}
|
||||
|
||||
void PasskeyImportDialog::addGroups(const QSharedPointer<Database>& database)
|
||||
{
|
||||
m_ui->selectGroupComboBox->clear();
|
||||
for (const auto& group : database->rootGroup()->groupsRecursive(true)) {
|
||||
if (!group || group->isRecycled() || group == database->metadata()->recycleBin()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_ui->selectGroupComboBox->addItem(group->fullPath(), group->uuid());
|
||||
}
|
||||
}
|
||||
|
||||
void PasskeyImportDialog::selectDatabase()
|
||||
{
|
||||
auto selectedDatabase = browserService()->selectedDatabase();
|
||||
if (!selectedDatabase) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_selectedDatabase = selectedDatabase;
|
||||
m_ui->selectDatabaseLabel->setText(QString("Database: %1").arg(getDatabaseName(m_selectedDatabase)));
|
||||
|
||||
addGroups(m_selectedDatabase);
|
||||
}
|
||||
|
||||
void PasskeyImportDialog::changeGroup(int index)
|
||||
{
|
||||
m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value<QUuid>();
|
||||
}
|
||||
|
||||
void PasskeyImportDialog::useDefaultGroupChanged()
|
||||
{
|
||||
m_ui->selectGroupComboBox->setEnabled(!m_ui->useDefaultGroupCheckbox->isChecked());
|
||||
m_useDefaultGroup = m_ui->useDefaultGroupCheckbox->isChecked();
|
||||
}
|
60
src/gui/passkeys/PasskeyImportDialog.h
Normal file
60
src/gui/passkeys/PasskeyImportDialog.h
Normal file
@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PASSKEYIMPORTDIALOG_H
|
||||
#define KEEPASSXC_PASSKEYIMPORTDIALOG_H
|
||||
|
||||
#include "core/Database.h"
|
||||
#include "core/Group.h"
|
||||
#include <QDialog>
|
||||
#include <QUuid>
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class PasskeyImportDialog;
|
||||
}
|
||||
|
||||
class PasskeyImportDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PasskeyImportDialog(QWidget* parent = nullptr);
|
||||
~PasskeyImportDialog() override;
|
||||
|
||||
void setInfo(const QString& url, const QString& username, const QSharedPointer<Database>& database);
|
||||
QSharedPointer<Database> getSelectedDatabase();
|
||||
QUuid getSelectedGroupUuid();
|
||||
bool useDefaultGroup();
|
||||
|
||||
private:
|
||||
QString getDatabaseName(const QSharedPointer<Database>& database) const;
|
||||
void addGroups(const QSharedPointer<Database>& database);
|
||||
|
||||
private slots:
|
||||
void selectDatabase();
|
||||
void changeGroup(int index);
|
||||
void useDefaultGroupChanged();
|
||||
|
||||
private:
|
||||
QScopedPointer<Ui::PasskeyImportDialog> m_ui;
|
||||
QSharedPointer<Database> m_selectedDatabase;
|
||||
QUuid m_selectedGroupUuid;
|
||||
bool m_useDefaultGroup;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_PASSKEYIMPORTDIALOG_H
|
174
src/gui/passkeys/PasskeyImportDialog.ui
Executable file
174
src/gui/passkeys/PasskeyImportDialog.ui
Executable file
@ -0,0 +1,174 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>PasskeyImportDialog</class>
|
||||
<widget class="QDialog" name="PasskeyImportDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>405</width>
|
||||
<height>227</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>KeePassXC - Passkey Import</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="infoLabel">
|
||||
<property name="font">
|
||||
<font>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Do you want to import the Passkey?</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="urlLabel">
|
||||
<property name="text">
|
||||
<string>URL: %1</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="usernameLabel">
|
||||
<property name="text">
|
||||
<string>Username: %1</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="useDefaultGroupCheckbox">
|
||||
<property name="text">
|
||||
<string>Use default group (Imported Passkeys)</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="selectGroupHorLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="selectGroupLabel">
|
||||
<property name="text">
|
||||
<string>Group</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="selectGroupComboBox"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="selectDatabaseHorLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="selectDatabaseLabel">
|
||||
<property name="text">
|
||||
<string>Database</string>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="selectDatabaseButton">
|
||||
<property name="text">
|
||||
<string>Select Database</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="importButton">
|
||||
<property name="accessibleName">
|
||||
<string>Import Passkey</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
<property name="autoDefault">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
139
src/gui/passkeys/PasskeyImporter.cpp
Normal file
139
src/gui/passkeys/PasskeyImporter.cpp
Normal file
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PasskeyImporter.h"
|
||||
#include "PasskeyImportDialog.h"
|
||||
#include "browser/BrowserMessageBuilder.h"
|
||||
#include "browser/BrowserPasskeys.h"
|
||||
#include "browser/BrowserService.h"
|
||||
#include "core/Entry.h"
|
||||
#include "core/Group.h"
|
||||
#include "gui/FileDialog.h"
|
||||
#include "gui/MessageBox.h"
|
||||
#include <QFileInfo>
|
||||
#include <QUuid>
|
||||
|
||||
static const QString IMPORTED_PASSKEYS_GROUP = QStringLiteral("Imported Passkeys");
|
||||
|
||||
void PasskeyImporter::importPasskey(QSharedPointer<Database>& database)
|
||||
{
|
||||
auto filter = QString("%1 (*.passkey);;%2 (*)").arg(tr("Passkey file"), tr("All files"));
|
||||
auto fileName =
|
||||
fileDialog()->getOpenFileName(nullptr, tr("Open Passkey file"), FileDialog::getLastDir("passkey"), filter);
|
||||
if (fileName.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
FileDialog::saveLastDir("passkey", fileName, true);
|
||||
|
||||
QFile file(fileName);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
MessageBox::information(
|
||||
nullptr, tr("Cannot open file"), tr("Cannot open file \"%1\" for reading.").arg(fileName));
|
||||
return;
|
||||
}
|
||||
|
||||
importSelectedFile(file, database);
|
||||
}
|
||||
|
||||
void PasskeyImporter::importSelectedFile(QFile& file, QSharedPointer<Database>& database)
|
||||
{
|
||||
const auto fileData = file.readAll();
|
||||
const auto passkeyObject = browserMessageBuilder()->getJsonObject(fileData);
|
||||
if (passkeyObject.isEmpty()) {
|
||||
MessageBox::information(nullptr,
|
||||
tr("Cannot import Passkey"),
|
||||
tr("Cannot import Passkey file \"%1\". Data is missing.").arg(file.fileName()));
|
||||
return;
|
||||
}
|
||||
|
||||
const auto relyingParty = passkeyObject["relyingParty"].toString();
|
||||
const auto url = passkeyObject["url"].toString();
|
||||
const auto username = passkeyObject["username"].toString();
|
||||
const auto password = passkeyObject["userId"].toString();
|
||||
const auto userHandle = passkeyObject["userHandle"].toString();
|
||||
const auto privateKey = passkeyObject["privateKey"].toString();
|
||||
|
||||
if (relyingParty.isEmpty() || username.isEmpty() || password.isEmpty() || userHandle.isEmpty()
|
||||
|| privateKey.isEmpty()) {
|
||||
MessageBox::information(nullptr,
|
||||
tr("Cannot import Passkey"),
|
||||
tr("Cannot import Passkey file \"%1\". Data is missing.").arg(file.fileName()));
|
||||
} else if (!privateKey.startsWith("-----BEGIN PRIVATE KEY-----")
|
||||
|| !privateKey.trimmed().endsWith("-----END PRIVATE KEY-----")) {
|
||||
MessageBox::information(
|
||||
nullptr,
|
||||
tr("Cannot import Passkey"),
|
||||
tr("Cannot import Passkey file \"%1\". Private key is missing or malformed.").arg(file.fileName()));
|
||||
} else {
|
||||
showImportDialog(database, url, relyingParty, username, password, userHandle, privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
void PasskeyImporter::showImportDialog(QSharedPointer<Database>& database,
|
||||
const QString& url,
|
||||
const QString& relyingParty,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey)
|
||||
{
|
||||
PasskeyImportDialog passkeyImportDialog;
|
||||
passkeyImportDialog.setInfo(relyingParty, username, database);
|
||||
|
||||
auto ret = passkeyImportDialog.exec();
|
||||
if (ret != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto db = passkeyImportDialog.getSelectedDatabase();
|
||||
if (!db) {
|
||||
db = database;
|
||||
}
|
||||
|
||||
// Group settings. Use default group "Imported Passkeys" if user did not select a specific one.
|
||||
Group* group = nullptr;
|
||||
|
||||
// Attempt to use the selected group
|
||||
if (!passkeyImportDialog.useDefaultGroup()) {
|
||||
auto groupUuid = passkeyImportDialog.getSelectedGroupUuid();
|
||||
group = db->rootGroup()->findGroupByUuid(groupUuid);
|
||||
}
|
||||
|
||||
// Use default group if requested or if the selected group does not exist
|
||||
if (!group) {
|
||||
group = getDefaultGroup(db);
|
||||
}
|
||||
|
||||
browserService()->addPasskeyToGroup(
|
||||
group, url, relyingParty, relyingParty, username, userId, userHandle, privateKey);
|
||||
}
|
||||
|
||||
Group* PasskeyImporter::getDefaultGroup(QSharedPointer<Database>& database)
|
||||
{
|
||||
auto defaultGroup = database->rootGroup()->findGroupByPath(IMPORTED_PASSKEYS_GROUP);
|
||||
|
||||
// Create the default group if it does not exist
|
||||
if (!defaultGroup) {
|
||||
defaultGroup = new Group();
|
||||
defaultGroup->setName(IMPORTED_PASSKEYS_GROUP);
|
||||
defaultGroup->setUuid(QUuid::createUuid());
|
||||
defaultGroup->setParent(database->rootGroup());
|
||||
}
|
||||
|
||||
return defaultGroup;
|
||||
}
|
48
src/gui/passkeys/PasskeyImporter.h
Normal file
48
src/gui/passkeys/PasskeyImporter.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 3 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_PASSKEYIMPORTER_H
|
||||
#define KEEPASSXC_PASSKEYIMPORTER_H
|
||||
|
||||
#include "core/Database.h"
|
||||
#include <QFile>
|
||||
#include <QObject>
|
||||
|
||||
class Entry;
|
||||
|
||||
class PasskeyImporter : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PasskeyImporter() = default;
|
||||
|
||||
void importPasskey(QSharedPointer<Database>& database);
|
||||
|
||||
private:
|
||||
void importSelectedFile(QFile& file, QSharedPointer<Database>& database);
|
||||
void showImportDialog(QSharedPointer<Database>& database,
|
||||
const QString& url,
|
||||
const QString& relyingParty,
|
||||
const QString& username,
|
||||
const QString& userId,
|
||||
const QString& userHandle,
|
||||
const QString& privateKey);
|
||||
Group* getDefaultGroup(QSharedPointer<Database>& database);
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_PASSKEYIMPORTER_H
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -25,6 +25,10 @@
|
||||
#include "ReportsPageBrowserStatistics.h"
|
||||
#include "ReportsWidgetBrowserStatistics.h"
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
#include "ReportsPagePasskeys.h"
|
||||
#include "ReportsWidgetPasskeys.h"
|
||||
#endif
|
||||
#include "ReportsWidgetHealthcheck.h"
|
||||
#include "ReportsWidgetHibp.h"
|
||||
|
||||
@ -61,6 +65,9 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
, m_statPage(new ReportsPageStatistics())
|
||||
#ifdef WITH_XC_BROWSER
|
||||
, m_browserStatPage(new ReportsPageBrowserStatistics())
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
, m_passkeysPage(new ReportsPagePasskeys())
|
||||
#endif
|
||||
, m_editEntryWidget(new EditEntryWidget(this))
|
||||
{
|
||||
@ -68,10 +75,13 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
|
||||
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
|
||||
addPage(m_statPage);
|
||||
addPage(m_healthPage);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
addPage(m_passkeysPage);
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER
|
||||
addPage(m_browserStatPage);
|
||||
#endif
|
||||
addPage(m_healthPage);
|
||||
addPage(m_hibpPage);
|
||||
|
||||
m_ui->stackedWidget->setCurrentIndex(0);
|
||||
@ -88,6 +98,10 @@ ReportsDialog::ReportsDialog(QWidget* parent)
|
||||
connect(m_browserStatPage->m_browserWidget,
|
||||
SIGNAL(entryActivated(Entry*)),
|
||||
SLOT(entryActivationSignalReceived(Entry*)));
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
connect(
|
||||
m_passkeysPage->m_passkeysWidget, SIGNAL(entryActivated(Entry*)), SLOT(entryActivationSignalReceived(Entry*)));
|
||||
#endif
|
||||
connect(m_editEntryWidget, SIGNAL(editFinished(bool)), SLOT(switchToMainView(bool)));
|
||||
}
|
||||
@ -114,6 +128,15 @@ void ReportsDialog::addPage(QSharedPointer<IReportsPage> page)
|
||||
m_ui->categoryList->setCurrentCategory(category);
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void ReportsDialog::activatePasskeysPage()
|
||||
{
|
||||
m_ui->stackedWidget->setCurrentWidget(m_passkeysPage->m_passkeysWidget);
|
||||
auto index = m_ui->stackedWidget->currentIndex();
|
||||
m_ui->categoryList->setCurrentCategory(index);
|
||||
}
|
||||
#endif
|
||||
|
||||
void ReportsDialog::reject()
|
||||
{
|
||||
emit editFinished(true);
|
||||
@ -148,6 +171,11 @@ void ReportsDialog::switchToMainView(bool previousDialogAccepted)
|
||||
if (m_sender == m_browserStatPage->m_browserWidget) {
|
||||
m_browserStatPage->m_browserWidget->calculateBrowserStatistics();
|
||||
}
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
if (m_sender == m_passkeysPage->m_passkeysWidget) {
|
||||
m_passkeysPage->m_passkeysWidget->updateEntries();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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
|
||||
@ -32,6 +32,9 @@ class ReportsPageStatistics;
|
||||
#ifdef WITH_XC_BROWSER
|
||||
class ReportsPageBrowserStatistics;
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
class ReportsPagePasskeys;
|
||||
#endif
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
@ -60,6 +63,9 @@ public:
|
||||
|
||||
void load(const QSharedPointer<Database>& db);
|
||||
void addPage(QSharedPointer<IReportsPage> page);
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
void activatePasskeysPage();
|
||||
#endif
|
||||
|
||||
signals:
|
||||
void editFinished(bool accepted);
|
||||
@ -77,6 +83,9 @@ private:
|
||||
const QSharedPointer<ReportsPageStatistics> m_statPage;
|
||||
#ifdef WITH_XC_BROWSER
|
||||
const QSharedPointer<ReportsPageBrowserStatistics> m_browserStatPage;
|
||||
#endif
|
||||
#ifdef WITH_XC_BROWSER_PASSKEYS
|
||||
const QSharedPointer<ReportsPagePasskeys> m_passkeysPage;
|
||||
#endif
|
||||
QPointer<EditEntryWidget> m_editEntryWidget;
|
||||
QWidget* m_sender = nullptr;
|
||||
|
52
src/gui/reports/ReportsPagePasskeys.cpp
Normal file
52
src/gui/reports/ReportsPagePasskeys.cpp
Normal file
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ReportsPagePasskeys.h"
|
||||
#include "ReportsWidgetPasskeys.h"
|
||||
#include "gui/Icons.h"
|
||||
|
||||
ReportsPagePasskeys::ReportsPagePasskeys()
|
||||
: m_passkeysWidget(new ReportsWidgetPasskeys())
|
||||
{
|
||||
}
|
||||
|
||||
QString ReportsPagePasskeys::name()
|
||||
{
|
||||
return QObject::tr("Passkeys");
|
||||
}
|
||||
|
||||
QIcon ReportsPagePasskeys::icon()
|
||||
{
|
||||
return icons()->icon("passkey");
|
||||
}
|
||||
|
||||
QWidget* ReportsPagePasskeys::createWidget()
|
||||
{
|
||||
return m_passkeysWidget;
|
||||
}
|
||||
|
||||
void ReportsPagePasskeys::loadSettings(QWidget* widget, QSharedPointer<Database> db)
|
||||
{
|
||||
const auto settingsWidget = reinterpret_cast<ReportsWidgetPasskeys*>(widget);
|
||||
settingsWidget->loadSettings(db);
|
||||
}
|
||||
|
||||
void ReportsPagePasskeys::saveSettings(QWidget* widget)
|
||||
{
|
||||
const auto settingsWidget = reinterpret_cast<ReportsWidgetPasskeys*>(widget);
|
||||
settingsWidget->saveSettings();
|
||||
}
|
40
src/gui/reports/ReportsPagePasskeys.h
Normal file
40
src/gui/reports/ReportsPagePasskeys.h
Normal file
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_REPORTSPAGEPASSKEYS_H
|
||||
#define KEEPASSXC_REPORTSPAGEPASSKEYS_H
|
||||
|
||||
#include "ReportsDialog.h"
|
||||
#include "ReportsWidgetPasskeys.h"
|
||||
|
||||
class ReportsWidgetBrowserStatistics;
|
||||
|
||||
class ReportsPagePasskeys : public IReportsPage
|
||||
{
|
||||
public:
|
||||
ReportsWidgetPasskeys* m_passkeysWidget;
|
||||
|
||||
ReportsPagePasskeys();
|
||||
|
||||
QString name() override;
|
||||
QIcon icon() override;
|
||||
QWidget* createWidget() override;
|
||||
void loadSettings(QWidget* widget, QSharedPointer<Database> db) override;
|
||||
void saveSettings(QWidget* widget) override;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSPAGEPASSKEYS_H
|
@ -112,8 +112,8 @@ ReportsWidgetBrowserStatistics::ReportsWidgetBrowserStatistics(QWidget* parent)
|
||||
connect(
|
||||
m_ui->browserStatisticsTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||
connect(m_ui->showEntriesWithUrlOnlyCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
|
||||
connect(m_ui->showConnectedOnlyCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
|
||||
connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
|
||||
connect(m_ui->showAllowDenyCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
|
||||
connect(m_ui->showExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateBrowserStatistics()));
|
||||
|
||||
new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
|
||||
}
|
||||
@ -144,6 +144,9 @@ void ReportsWidgetBrowserStatistics::addStatisticsRow(bool hasUrls,
|
||||
if (excluded) {
|
||||
title.append(tr(" (Excluded)"));
|
||||
}
|
||||
if (entry->isExpired()) {
|
||||
title.append(tr(" (Expired)"));
|
||||
}
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem(Icons::entryIconPixmap(entry), title);
|
||||
@ -196,16 +199,15 @@ void ReportsWidgetBrowserStatistics::calculateBrowserStatistics()
|
||||
const QScopedPointer<BrowserStatistics> browserStatistics(
|
||||
AsyncTask::runAndWaitForFuture([this] { return new BrowserStatistics(m_db); }));
|
||||
|
||||
const auto showExcluded = m_ui->showConnectedOnlyCheckBox->isChecked();
|
||||
const auto showExpired = m_ui->showExpired->isChecked();
|
||||
const auto showEntriesWithUrlOnly = m_ui->showEntriesWithUrlOnlyCheckBox->isChecked();
|
||||
const auto showOnlyEntriesWithSettings = m_ui->showConnectedOnlyCheckBox->isChecked();
|
||||
const auto showOnlyEntriesWithSettings = m_ui->showAllowDenyCheckBox->isChecked();
|
||||
|
||||
// Display the entries
|
||||
m_rowToEntry.clear();
|
||||
for (const auto& item : browserStatistics->items()) {
|
||||
auto excluded = item->exclude || (item->entry->isExpired() && m_ui->excludeExpired->isChecked());
|
||||
if (excluded && !showExcluded) {
|
||||
// Exclude this entry from the report
|
||||
// Check if the entry should be displayed
|
||||
if (!showExpired && item->entry->isExpired()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
<height>379</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,0,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
@ -54,24 +54,27 @@
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="excludeExpired">
|
||||
<property name="text">
|
||||
<string>Exclude expired entries from the report</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showEntriesWithUrlOnlyCheckBox">
|
||||
<property name="text">
|
||||
<string>Show only entries which have URL set</string>
|
||||
<string>Only show entries that have a URL</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showConnectedOnlyCheckBox">
|
||||
<widget class="QCheckBox" name="showAllowDenyCheckBox">
|
||||
<property name="text">
|
||||
<string>Show only entries which have browser settings in custom data</string>
|
||||
<string>Only show entries that have been explicitly allowed or denied</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showExpired">
|
||||
<property name="text">
|
||||
<string>Show expired entries</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -91,9 +94,8 @@
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>browserStatisticsTableView</tabstop>
|
||||
<tabstop>excludeExpired</tabstop>
|
||||
<tabstop>showEntriesWithUrlOnlyCheckBox</tabstop>
|
||||
<tabstop>showConnectedOnlyCheckBox</tabstop>
|
||||
<tabstop>showAllowDenyCheckBox</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
@ -64,16 +64,16 @@ namespace
|
||||
return m_items;
|
||||
}
|
||||
|
||||
bool anyKnownBad() const
|
||||
bool anyExcludedEntries() const
|
||||
{
|
||||
return m_anyKnownBad;
|
||||
return m_anyExcludedEntries;
|
||||
}
|
||||
|
||||
private:
|
||||
QSharedPointer<Database> m_db;
|
||||
HealthChecker m_checker;
|
||||
QList<QSharedPointer<Item>> m_items;
|
||||
bool m_anyKnownBad = false;
|
||||
bool m_anyExcludedEntries = false;
|
||||
};
|
||||
|
||||
class ReportSortProxyModel : public QSortFilterProxyModel
|
||||
@ -121,7 +121,7 @@ Health::Health(QSharedPointer<Database> db)
|
||||
// Evaluate this entry
|
||||
const auto item = QSharedPointer<Item>(new Item(group, entry, m_checker.evaluate(entry)));
|
||||
if (item->exclude) {
|
||||
m_anyKnownBad = true;
|
||||
m_anyExcludedEntries = true;
|
||||
}
|
||||
|
||||
// Add entry if its password isn't at least "good"
|
||||
@ -152,8 +152,8 @@ ReportsWidgetHealthcheck::ReportsWidgetHealthcheck(QWidget* parent)
|
||||
|
||||
connect(m_ui->healthcheckTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
||||
connect(m_ui->healthcheckTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||
connect(m_ui->showKnownBadCheckBox, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||
connect(m_ui->excludeExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||
connect(m_ui->showExcluded, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||
connect(m_ui->showExpired, SIGNAL(stateChanged(int)), this, SLOT(calculateHealth()));
|
||||
|
||||
new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
|
||||
}
|
||||
@ -163,7 +163,7 @@ ReportsWidgetHealthcheck::~ReportsWidgetHealthcheck() = default;
|
||||
void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> health,
|
||||
Group* group,
|
||||
Entry* entry,
|
||||
bool knownBad)
|
||||
bool excluded)
|
||||
{
|
||||
QString descr, tip;
|
||||
QColor qualityColor;
|
||||
@ -195,9 +195,12 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> healt
|
||||
}
|
||||
|
||||
auto title = entry->title();
|
||||
if (knownBad) {
|
||||
if (excluded) {
|
||||
title.append(tr(" (Excluded)"));
|
||||
}
|
||||
if (entry->isExpired()) {
|
||||
title.append(tr(" (Expired)"));
|
||||
}
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem(descr);
|
||||
@ -215,7 +218,7 @@ void ReportsWidgetHealthcheck::addHealthRow(QSharedPointer<PasswordHealth> healt
|
||||
|
||||
// Set tooltips
|
||||
row[0]->setToolTip(tip);
|
||||
if (knownBad) {
|
||||
if (excluded) {
|
||||
row[1]->setToolTip(tr("This entry is being excluded from reports"));
|
||||
}
|
||||
row[4]->setToolTip(health->scoreDetails());
|
||||
@ -255,15 +258,12 @@ void ReportsWidgetHealthcheck::calculateHealth()
|
||||
// Perform the health check
|
||||
const QScopedPointer<Health> health(AsyncTask::runAndWaitForFuture([this] { return new Health(m_db); }));
|
||||
|
||||
// Display entries that are marked as "known bad"?
|
||||
const auto showExcluded = m_ui->showKnownBadCheckBox->isChecked();
|
||||
|
||||
// Display the entries
|
||||
m_rowToEntry.clear();
|
||||
for (const auto& item : health->items()) {
|
||||
auto excluded = item->exclude || (item->entry->isExpired() && m_ui->excludeExpired->isChecked());
|
||||
if (excluded && !showExcluded) {
|
||||
// Exclude this entry from the report
|
||||
// Check if the entry should be displayed
|
||||
if ((!m_ui->showExcluded->isChecked() && item->exclude)
|
||||
|| (!m_ui->showExpired->isChecked() && item->entry->isExpired())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -283,13 +283,8 @@ void ReportsWidgetHealthcheck::calculateHealth()
|
||||
m_ui->healthcheckTableView->resizeColumnsToContents();
|
||||
m_ui->healthcheckTableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::Fixed);
|
||||
|
||||
// Show the "show known bad entries" checkbox if there's any known
|
||||
// bad entry in the database.
|
||||
if (health->anyKnownBad()) {
|
||||
m_ui->showKnownBadCheckBox->show();
|
||||
} else {
|
||||
m_ui->showKnownBadCheckBox->hide();
|
||||
}
|
||||
// Only show the "show excluded" checkbox if there are any excluded entries in the database
|
||||
m_ui->showExcluded->setVisible(health->anyExcludedEntries());
|
||||
}
|
||||
|
||||
void ReportsWidgetHealthcheck::emitEntryActivated(const QModelIndex& index)
|
||||
|
@ -56,7 +56,7 @@ public slots:
|
||||
void deleteSelectedEntries();
|
||||
|
||||
private:
|
||||
void addHealthRow(QSharedPointer<PasswordHealth>, Group*, Entry*, bool knownBad);
|
||||
void addHealthRow(QSharedPointer<PasswordHealth>, Group*, Entry*, bool excluded);
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetHealthcheck> m_ui;
|
||||
|
||||
|
@ -55,16 +55,16 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="excludeExpired">
|
||||
<widget class="QCheckBox" name="showExpired">
|
||||
<property name="text">
|
||||
<string>Exclude expired entries from the report</string>
|
||||
<string>Show expired entries</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showKnownBadCheckBox">
|
||||
<widget class="QCheckBox" name="showExcluded">
|
||||
<property name="text">
|
||||
<string>Also show entries that have been excluded from reports</string>
|
||||
<string>Show entries that have been excluded from reports</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@ -84,7 +84,7 @@
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>healthcheckTableView</tabstop>
|
||||
<tabstop>showKnownBadCheckBox</tabstop>
|
||||
<tabstop>showExcluded</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
|
294
src/gui/reports/ReportsWidgetPasskeys.cpp
Normal file
294
src/gui/reports/ReportsWidgetPasskeys.cpp
Normal file
@ -0,0 +1,294 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "ReportsWidgetPasskeys.h"
|
||||
#include "ui_ReportsWidgetPasskeys.h"
|
||||
|
||||
#include "browser/BrowserPasskeys.h"
|
||||
#include "core/AsyncTask.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "gui/GuiTools.h"
|
||||
#include "gui/Icons.h"
|
||||
#include "gui/passkeys/PasskeyExporter.h"
|
||||
#include "gui/passkeys/PasskeyImporter.h"
|
||||
#include "gui/styles/StateColorPalette.h"
|
||||
|
||||
#include <QMenu>
|
||||
#include <QShortcut>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QStandardItemModel>
|
||||
|
||||
namespace
|
||||
{
|
||||
class PasskeyList
|
||||
{
|
||||
public:
|
||||
struct Item
|
||||
{
|
||||
QPointer<Group> group;
|
||||
QPointer<Entry> entry;
|
||||
|
||||
Item(Group* g, Entry* e)
|
||||
: group(g)
|
||||
, entry(e)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
explicit PasskeyList(const QSharedPointer<Database>&);
|
||||
|
||||
const QList<QSharedPointer<Item>>& items() const
|
||||
{
|
||||
return m_items;
|
||||
}
|
||||
|
||||
private:
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QSharedPointer<Item>> m_items;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
PasskeyList::PasskeyList(const QSharedPointer<Database>& db)
|
||||
: m_db(db)
|
||||
{
|
||||
for (auto group : db->rootGroup()->groupsRecursive(true)) {
|
||||
// Skip recycle bin
|
||||
if (group->isRecycled()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (auto entry : group->entries()) {
|
||||
if (entry->isRecycled() || !entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto item = QSharedPointer<Item>(new Item(group, entry));
|
||||
m_items.append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ReportsWidgetPasskeys::ReportsWidgetPasskeys(QWidget* parent)
|
||||
: QWidget(parent)
|
||||
, m_ui(new Ui::ReportsWidgetPasskeys())
|
||||
, m_referencesModel(new QStandardItemModel(this))
|
||||
, m_modelProxy(new QSortFilterProxyModel(this))
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
m_modelProxy->setSourceModel(m_referencesModel.data());
|
||||
m_modelProxy->setSortLocaleAware(true);
|
||||
m_ui->passkeysTableView->setModel(m_modelProxy.data());
|
||||
m_ui->passkeysTableView->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||
m_ui->passkeysTableView->verticalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
|
||||
connect(m_ui->passkeysTableView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(customMenuRequested(QPoint)));
|
||||
connect(m_ui->passkeysTableView, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex)));
|
||||
connect(m_ui->passkeysTableView->selectionModel(),
|
||||
SIGNAL(selectionChanged(QItemSelection, QItemSelection)),
|
||||
this,
|
||||
SLOT(selectionChanged()));
|
||||
connect(m_ui->showExpired, SIGNAL(stateChanged(int)), this, SLOT(updateEntries()));
|
||||
connect(m_ui->exportButton, SIGNAL(clicked(bool)), this, SLOT(exportPasskey()));
|
||||
connect(m_ui->importButton, SIGNAL(clicked(bool)), this, SLOT(importPasskey()));
|
||||
|
||||
m_ui->exportButton->setEnabled(false);
|
||||
|
||||
new QShortcut(Qt::Key_Delete, this, SLOT(deleteSelectedEntries()));
|
||||
}
|
||||
|
||||
ReportsWidgetPasskeys::~ReportsWidgetPasskeys()
|
||||
{
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::addPasskeyRow(Group* group, Entry* entry)
|
||||
{
|
||||
StateColorPalette statePalette;
|
||||
|
||||
auto urlList = entry->getAllUrls();
|
||||
auto urlToolTip = tr("List of entry URLs");
|
||||
|
||||
auto title = entry->title();
|
||||
if (entry->isExpired()) {
|
||||
title.append(tr(" (Expired)"));
|
||||
}
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem(Icons::entryIconPixmap(entry), title);
|
||||
row << new QStandardItem(Icons::groupIconPixmap(group), group->hierarchy().join("/"));
|
||||
row << new QStandardItem(entry->username());
|
||||
row << new QStandardItem(entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY));
|
||||
row << new QStandardItem(urlList.join('\n'));
|
||||
|
||||
// Set tooltips
|
||||
row[2]->setToolTip(urlToolTip);
|
||||
|
||||
// Store entry pointer per table row (used in double click handler)
|
||||
m_referencesModel->appendRow(row);
|
||||
m_rowToEntry.append({group, entry});
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::loadSettings(QSharedPointer<Database> db)
|
||||
{
|
||||
m_db = std::move(db);
|
||||
m_entriesUpdated = false;
|
||||
m_referencesModel->clear();
|
||||
m_rowToEntry.clear();
|
||||
|
||||
auto row = QList<QStandardItem*>();
|
||||
row << new QStandardItem(tr("Please wait, list of entries with Passkeys is being updated…"));
|
||||
m_referencesModel->appendRow(row);
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::showEvent(QShowEvent* event)
|
||||
{
|
||||
QWidget::showEvent(event);
|
||||
|
||||
if (!m_entriesUpdated) {
|
||||
// Perform stats calculation on next event loop to allow widget to appear
|
||||
m_entriesUpdated = true;
|
||||
QTimer::singleShot(0, this, SLOT(updateEntries()));
|
||||
}
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::updateEntries()
|
||||
{
|
||||
m_referencesModel->clear();
|
||||
|
||||
// Perform the statistics check
|
||||
const QScopedPointer<PasskeyList> browserStatistics(
|
||||
AsyncTask::runAndWaitForFuture([this] { return new PasskeyList(m_db); }));
|
||||
|
||||
// Display the entries
|
||||
m_rowToEntry.clear();
|
||||
for (const auto& item : browserStatistics->items()) {
|
||||
// Exclude expired entries from report if not requested
|
||||
if (!m_ui->showExpired->isChecked() && item->entry->isExpired()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
addPasskeyRow(item->group, item->entry);
|
||||
}
|
||||
|
||||
// Set the table header
|
||||
if (m_referencesModel->rowCount() == 0) {
|
||||
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("No entries with Passkeys."));
|
||||
} else {
|
||||
m_referencesModel->setHorizontalHeaderLabels(QStringList() << tr("Title") << tr("Path") << tr("Username")
|
||||
<< tr("Relying Party") << tr("URLs"));
|
||||
m_ui->passkeysTableView->sortByColumn(0, Qt::AscendingOrder);
|
||||
}
|
||||
|
||||
m_ui->passkeysTableView->resizeColumnsToContents();
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::emitEntryActivated(const QModelIndex& index)
|
||||
{
|
||||
if (!index.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto mappedIndex = m_modelProxy->mapToSource(index);
|
||||
const auto row = m_rowToEntry[mappedIndex.row()];
|
||||
const auto group = row.first;
|
||||
const auto entry = row.second;
|
||||
|
||||
if (group && entry) {
|
||||
emit entryActivated(entry);
|
||||
}
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::customMenuRequested(QPoint pos)
|
||||
{
|
||||
auto selected = m_ui->passkeysTableView->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create the context menu
|
||||
const auto menu = new QMenu(this);
|
||||
|
||||
// Create the "edit entry" menu item (only if 1 row is selected)
|
||||
if (selected.size() == 1) {
|
||||
const auto edit = new QAction(icons()->icon("entry-edit"), tr("Edit Entry…"), this);
|
||||
menu->addAction(edit);
|
||||
connect(edit, &QAction::triggered, edit, [this, selected] {
|
||||
auto row = m_modelProxy->mapToSource(selected[0]).row();
|
||||
auto entry = m_rowToEntry[row].second;
|
||||
emit entryActivated(entry);
|
||||
});
|
||||
}
|
||||
|
||||
// Create the "delete entry" menu item
|
||||
const auto delEntry = new QAction(icons()->icon("entry-delete"), tr("Delete Entry(s)…", "", selected.size()), this);
|
||||
menu->addAction(delEntry);
|
||||
connect(delEntry, &QAction::triggered, this, &ReportsWidgetPasskeys::deleteSelectedEntries);
|
||||
|
||||
// Show the context menu
|
||||
menu->popup(m_ui->passkeysTableView->viewport()->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::saveSettings()
|
||||
{
|
||||
// Nothing to do - the tab is passive
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::deleteSelectedEntries()
|
||||
{
|
||||
auto selectedEntries = getSelectedEntries();
|
||||
bool permanent = !m_db->metadata()->recycleBinEnabled();
|
||||
|
||||
if (GuiTools::confirmDeleteEntries(this, selectedEntries, permanent)) {
|
||||
GuiTools::deleteEntriesResolveReferences(this, selectedEntries, permanent);
|
||||
}
|
||||
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
QList<Entry*> ReportsWidgetPasskeys::getSelectedEntries()
|
||||
{
|
||||
QList<Entry*> selectedEntries;
|
||||
for (auto index : m_ui->passkeysTableView->selectionModel()->selectedRows()) {
|
||||
auto row = m_modelProxy->mapToSource(index).row();
|
||||
auto entry = m_rowToEntry[row].second;
|
||||
if (entry) {
|
||||
selectedEntries << entry;
|
||||
}
|
||||
}
|
||||
|
||||
return selectedEntries;
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::selectionChanged()
|
||||
{
|
||||
m_ui->exportButton->setEnabled(!m_ui->passkeysTableView->selectionModel()->selectedIndexes().isEmpty());
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::importPasskey()
|
||||
{
|
||||
PasskeyImporter passkeyImporter;
|
||||
passkeyImporter.importPasskey(m_db);
|
||||
|
||||
updateEntries();
|
||||
}
|
||||
|
||||
void ReportsWidgetPasskeys::exportPasskey()
|
||||
{
|
||||
PasskeyExporter passkeyExporter;
|
||||
passkeyExporter.showExportDialog(getSelectedEntries());
|
||||
}
|
76
src/gui/reports/ReportsWidgetPasskeys.h
Normal file
76
src/gui/reports/ReportsWidgetPasskeys.h
Normal file
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_REPORTSWIDGETPASSKEYS_H
|
||||
#define KEEPASSXC_REPORTSWIDGETPASSKEYS_H
|
||||
|
||||
#include "gui/entry/EntryModel.h"
|
||||
#include <QWidget>
|
||||
|
||||
class Database;
|
||||
class Entry;
|
||||
class Group;
|
||||
class PasswordHealth;
|
||||
class QSortFilterProxyModel;
|
||||
class QStandardItemModel;
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class ReportsWidgetPasskeys;
|
||||
}
|
||||
|
||||
class ReportsWidgetPasskeys : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ReportsWidgetPasskeys(QWidget* parent = nullptr);
|
||||
~ReportsWidgetPasskeys() override;
|
||||
|
||||
void loadSettings(QSharedPointer<Database> db);
|
||||
void saveSettings();
|
||||
|
||||
protected:
|
||||
void showEvent(QShowEvent* event) override;
|
||||
|
||||
signals:
|
||||
void entryActivated(Entry*);
|
||||
|
||||
public slots:
|
||||
void updateEntries();
|
||||
void emitEntryActivated(const QModelIndex& index);
|
||||
void customMenuRequested(QPoint);
|
||||
void deleteSelectedEntries();
|
||||
|
||||
private slots:
|
||||
void selectionChanged();
|
||||
void importPasskey();
|
||||
void exportPasskey();
|
||||
|
||||
private:
|
||||
void addPasskeyRow(Group*, Entry*);
|
||||
QList<Entry*> getSelectedEntries();
|
||||
|
||||
QScopedPointer<Ui::ReportsWidgetPasskeys> m_ui;
|
||||
|
||||
bool m_entriesUpdated = false;
|
||||
QScopedPointer<QStandardItemModel> m_referencesModel;
|
||||
QScopedPointer<QSortFilterProxyModel> m_modelProxy;
|
||||
QSharedPointer<Database> m_db;
|
||||
QList<QPair<Group*, Entry*>> m_rowToEntry;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_REPORTSWIDGETPASSKEYS_H
|
102
src/gui/reports/ReportsWidgetPasskeys.ui
Normal file
102
src/gui/reports/ReportsWidgetPasskeys.ui
Normal file
@ -0,0 +1,102 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>ReportsWidgetPasskeys</class>
|
||||
<widget class="QWidget" name="ReportsWidgetPasskeys">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>505</width>
|
||||
<height>379</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QTableView" name="passkeysTableView">
|
||||
<property name="contextMenuPolicy">
|
||||
<enum>Qt::CustomContextMenu</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QAbstractScrollArea::AdjustToContents</enum>
|
||||
</property>
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="showDropIndicator" stdset="0">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="textElideMode">
|
||||
<enum>Qt::ElideRight</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="showExpired">
|
||||
<property name="text">
|
||||
<string>Show expired entries</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="importButton">
|
||||
<property name="text">
|
||||
<string>Import</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="exportButton">
|
||||
<property name="text">
|
||||
<string>Export</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -25,6 +25,10 @@ QCheckBox, QRadioButton {
|
||||
spacing: 10px;
|
||||
}
|
||||
|
||||
ReportsDialog QTableView::item {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
DatabaseWidget, DatabaseWidget #groupView, DatabaseWidget #tagView {
|
||||
background-color: palette(window);
|
||||
border: none;
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
||||
# Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
# Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
@ -233,6 +233,17 @@ endif()
|
||||
if(WITH_XC_BROWSER)
|
||||
add_unit_test(NAME testbrowser SOURCES TestBrowser.cpp
|
||||
LIBS ${TEST_LIBRARIES})
|
||||
|
||||
if(WITH_XC_BROWSER_PASSKEYS)
|
||||
# Prevent duplicate linking with macOS
|
||||
if(APPLE)
|
||||
add_unit_test(NAME testpasskeys SOURCES TestPasskeys.cpp
|
||||
LIBS ${TEST_LIBRARIES})
|
||||
else()
|
||||
add_unit_test(NAME testpasskeys SOURCES TestPasskeys.cpp
|
||||
LIBS keepassxcbrowser ${TEST_LIBRARIES})
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
add_unit_test(NAME testcli SOURCES TestCli.cpp
|
||||
|
471
tests/TestPasskeys.cpp
Normal file
471
tests/TestPasskeys.cpp
Normal file
@ -0,0 +1,471 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "TestPasskeys.h"
|
||||
#include "browser/BrowserCbor.h"
|
||||
#include "browser/BrowserMessageBuilder.h"
|
||||
#include "browser/BrowserService.h"
|
||||
#include "crypto/Crypto.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QTest>
|
||||
#include <botan/sodium.h>
|
||||
|
||||
using namespace Botan::Sodium;
|
||||
|
||||
QTEST_GUILESS_MAIN(TestPasskeys)
|
||||
|
||||
// Register request
|
||||
// clang-format off
|
||||
const QString PublicKeyCredentialOptions = R"(
|
||||
{
|
||||
"attestation": "none",
|
||||
"authenticatorSelection": {
|
||||
"residentKey": "preferred",
|
||||
"requireResidentKey": false,
|
||||
"userVerification": "required"
|
||||
},
|
||||
"challenge": "lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw",
|
||||
"pubKeyCredParams": [
|
||||
{
|
||||
"type": "public-key",
|
||||
"alg": -7
|
||||
},
|
||||
{
|
||||
"type": "public-key",
|
||||
"alg": -257
|
||||
}
|
||||
],
|
||||
"rp": {
|
||||
"name": "webauthn.io",
|
||||
"id": "webauthn.io"
|
||||
},
|
||||
"timeout": 60000,
|
||||
"excludeCredentials": [],
|
||||
"user": {
|
||||
"displayName": "Test User",
|
||||
"id": "VkdWemRDQlZjMlZ5",
|
||||
"name": "Test User"
|
||||
}
|
||||
}
|
||||
)";
|
||||
|
||||
// Register response
|
||||
const QString PublicKeyCredential = R"(
|
||||
{
|
||||
"authenticatorAttachment": "platform",
|
||||
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
|
||||
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
|
||||
"response": {
|
||||
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAAECAwQFBgcIAQIDBAUGBwgAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIAbsrzRbYpFhbRlZA6ZQKsoxxJWoaeXwh-XUuDLNCIXdIlgg4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M",
|
||||
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
|
||||
},
|
||||
"type": "public-key"
|
||||
}
|
||||
)";
|
||||
|
||||
// Get request
|
||||
const QString PublicKeyCredentialRequestOptions = R"(
|
||||
{
|
||||
"allowCredentials": [
|
||||
{
|
||||
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
|
||||
"transports": ["internal"],
|
||||
"type": "public-key"
|
||||
}
|
||||
],
|
||||
"challenge": "9z36vTfQTL95Lf7WnZgyte7ohGeF-XRiLxkL-LuGU1zopRmMIUA1LVwzGpyIm1fOBn1QnRa0QH27ADAaJGHysQ",
|
||||
"rpId": "webauthn.io",
|
||||
"timeout": 60000,
|
||||
"userVerification": "required"
|
||||
}
|
||||
)";
|
||||
// clang-format on
|
||||
|
||||
void TestPasskeys::initTestCase()
|
||||
{
|
||||
QVERIFY(Crypto::init());
|
||||
}
|
||||
|
||||
void TestPasskeys::init()
|
||||
{
|
||||
}
|
||||
|
||||
void TestPasskeys::testBase64WithHexStrings()
|
||||
{
|
||||
const size_t bufSize = 64;
|
||||
unsigned char buf[bufSize] = {31, 141, 30, 29, 142, 73, 5, 239, 242, 84, 187, 202, 40, 54, 15, 223,
|
||||
201, 0, 108, 109, 209, 104, 207, 239, 160, 89, 208, 117, 134, 66, 42, 12,
|
||||
31, 66, 163, 248, 221, 88, 241, 164, 6, 55, 182, 97, 186, 243, 162, 162,
|
||||
81, 220, 55, 60, 93, 207, 170, 222, 56, 234, 227, 45, 115, 175, 138, 182};
|
||||
|
||||
auto base64FromArray = browserMessageBuilder()->getBase64FromArray(reinterpret_cast<const char*>(buf), bufSize);
|
||||
QCOMPARE(base64FromArray,
|
||||
QString("H40eHY5JBe_yVLvKKDYP38kAbG3RaM_voFnQdYZCKgwfQqP43VjxpAY3tmG686KiUdw3PF3Pqt446uMtc6-Ktg"));
|
||||
|
||||
auto arrayFromBase64 = browserMessageBuilder()->getArrayFromBase64(base64FromArray);
|
||||
QCOMPARE(arrayFromBase64.size(), bufSize);
|
||||
|
||||
for (size_t i = 0; i < bufSize; i++) {
|
||||
QCOMPARE(static_cast<unsigned char>(arrayFromBase64.at(i)), buf[i]);
|
||||
}
|
||||
|
||||
auto randomDataBase64 = browserMessageBuilder()->getRandomBytesAsBase64(24);
|
||||
QCOMPARE(randomDataBase64.isEmpty(), false);
|
||||
}
|
||||
|
||||
void TestPasskeys::testDecodeResponseData()
|
||||
{
|
||||
const auto publicKeyCredential = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
|
||||
auto response = publicKeyCredential["response"].toObject();
|
||||
auto clientDataJson = response["clientDataJSON"].toString();
|
||||
auto attestationObject = response["attestationObject"].toString();
|
||||
|
||||
QVERIFY(!clientDataJson.isEmpty());
|
||||
QVERIFY(!attestationObject.isEmpty());
|
||||
|
||||
// Parse clientDataJSON
|
||||
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
|
||||
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
|
||||
QCOMPARE(clientDataJsonObject["challenge"],
|
||||
QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
|
||||
QCOMPARE(clientDataJsonObject["origin"], QString("https://webauthn.io"));
|
||||
QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
|
||||
|
||||
// Parse attestationObject (CBOR decoding needed)
|
||||
BrowserCbor browserCbor;
|
||||
auto attestationByteArray = browserMessageBuilder()->getArrayFromBase64(attestationObject);
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(attestationByteArray);
|
||||
|
||||
// Parse authData
|
||||
auto authDataJsonObject = attestationJsonObject["authData"].toString();
|
||||
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
|
||||
QVERIFY(authDataArray.size() >= 37);
|
||||
|
||||
auto authData = browserPasskeys()->parseAuthData(authDataArray);
|
||||
auto credentialData = authData["credentialData"].toObject();
|
||||
auto flags = authData["flags"].toObject();
|
||||
auto publicKey = credentialData["publicKey"].toObject();
|
||||
|
||||
// The attestationObject should include the same ID after decoding with the response root
|
||||
QCOMPARE(credentialData["credentialId"].toString(), publicKeyCredential["id"].toString());
|
||||
QCOMPARE(credentialData["aaguid"].toString(), QString("AQIDBAUGBwgBAgMEBQYHCA"));
|
||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
QCOMPARE(flags["AT"], true);
|
||||
QCOMPARE(flags["UP"], true);
|
||||
QCOMPARE(publicKey["1"], 2);
|
||||
QCOMPARE(publicKey["3"], -7);
|
||||
QCOMPARE(publicKey["-1"], 1);
|
||||
QCOMPARE(publicKey["-2"], QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0"));
|
||||
QCOMPARE(publicKey["-3"], QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testLoadingECPrivateKeyFromPem()
|
||||
{
|
||||
const auto publicKeyCredentialRequestOptions =
|
||||
browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
|
||||
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
|
||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
|
||||
"XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
|
||||
"8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
|
||||
"-----END PRIVATE KEY-----");
|
||||
|
||||
const auto authenticatorData =
|
||||
browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
|
||||
const auto clientData = browserMessageBuilder()->getArrayFromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
|
||||
"1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
|
||||
"aW4iOmZhbHNlfQ");
|
||||
|
||||
const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
|
||||
QCOMPARE(
|
||||
browserMessageBuilder()->getBase64FromArray(signature),
|
||||
QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testLoadingRSAPrivateKeyFromPem()
|
||||
{
|
||||
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
|
||||
"MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC5OHjBHQaRfxxX\n4WHRmqq7e7JgT"
|
||||
"FRs1bd4dIOFAOZnhNE3vAg2IF5VurmeB+9ye9xh7frw8ubrL0cv\nsBWiJfN5CY3SYGRLbGTtBC0fZ6"
|
||||
"OhhhjwvVM1GW6nVeRU66atzuo4NBfYXJWIYECd\npRBU4+xsDL4vJnn1mj05+v/Tqp6Uo1HrEPx9+Dc"
|
||||
"oYJD+cw7+OQ83XeGmjD+Dtm5z\nNIyYdweaafVR4PEUlB3CYZuOq9xcpxay3ps2MuYT1zGoiQqk6fla"
|
||||
"d+0tBWGY8Lwp\nCVulXCv7ljNJ4gxgQtOqWX8j2hC0hBxeqNYDYbrkECid3TsMTEMcV5uaVJXULg4t"
|
||||
"\nn6UItA11AgMBAAECggEAC3B0WBxHuieIfllOOOC4H9/7S7fDH2f7+W2cFtQ6pqo9\nCq0WBmkYMmw"
|
||||
"Xx9hpHoq4TnhhHyL9WzPzuKYD0Vx4gvacV/ckkppFScnQKJ2hF+99\nLax1DbU+UImSknfDDFPYbYld"
|
||||
"b1CD2rpJG1i6X2fRQ6NuK+F7jE05mqcIyE+ZajK+\nIpx8XFmE+tI1EEWsn3CzxMLiTQfXyFt/drM9i"
|
||||
"GYfcDjYY+q5vzGU3Kxj68gjc96A\nOra79DGOmwX+4zIwo5sSzI3noHnhWPLsaRtE5jWu21Qkb+1BvB"
|
||||
"jPmbQfN274OQfy\n8/BNNR/NZM1mJm/8x4Mt+h5d946XlIo0AkyYZXY/UQKBgQDYI3G3BCwaYk6MDMe"
|
||||
"T\nIamRZo25phPtr3if47dhT2xFWJplIt35sW+6KjD6c1Qpb2aGOUh7JPmb57H54OgJ\nmojkS5tv9Y"
|
||||
"EQZFfgCCZoeuqBx+ArqtJdkXOiNEFS0dpt44I+eO3Do5pnwKRemH+Y\ncqJ/eMH96UMzYDO7WNsyOyo"
|
||||
"5UQKBgQDbYU0KbGbTrMEV4T9Q41rZ2TnWzs5moqn2\nCRtB8LOdKAZUG7FRsw5KgC1CvFn3Xuk+qphY"
|
||||
"GUQeJvv7FjxMRUv4BktNpXju6eUj\n3tWHzI2QOkHaeq/XibwbNomfkdyTjtLX2+v8DBHcZnCSlukxc"
|
||||
"JISyPqZ6CnTjXGE\nEGB+itBI5QKBgQCA+gWttOusguVkZWvivL+3aH9CPXy+5WsR3o1boE13xDu+Bm"
|
||||
"R3\n0A5gBTVc/t1GLJf9mMlL0vCwvD5UYoWU1YbC1OtYkCQIaBiYM8TXrCGseF2pMTJ/\na4CZVp10k"
|
||||
"o3J7W2XYgpgKIzHRQnQ+SeLDT0y3BjHMB9N1SaJsah7/RphQQKBgQCr\nL+4yKAzFOJUjQbVqpT8Lp5"
|
||||
"qeqJofNOdzef+vIOjHxafKkiF4I0UPlZ276cY6ZfGU\nWQKwHGcvMDSI5fz/d0OksySn3mvT4uhPaV8"
|
||||
"urMv6s7sXhY0Zn/0NLy2NOwDolBar\nIo2vDKwTVEyb1u75CWKzDemfl66ryj++Uhk6JZAKkQKBgQCc"
|
||||
"NYVe7m648DzD0nu9\n3lgetBTaAS1zZmMs8Cinj44v0ksfqxrRBzBZcO9kCQqiJZ7uCAaVYcQ+PwkY+"
|
||||
"05C\n+w1+KvdGcKM+8TQYTQM3s2B9IyKExRS/dbQf9F7stJL+k5vbt6OUerwfmbNI9R3t\ngDZ4DEfo"
|
||||
"pPivs9dnequ9wfaPOw=="
|
||||
"-----END PRIVATE KEY-----");
|
||||
|
||||
const auto authenticatorData =
|
||||
browserMessageBuilder()->getArrayFromBase64("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA");
|
||||
const auto clientData = browserMessageBuilder()->getArrayFromBase64(
|
||||
"eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtMLUx1R1Uxem9wUm"
|
||||
"1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmln"
|
||||
"aW4iOmZhbHNlfQ");
|
||||
|
||||
const auto signature = browserPasskeys()->buildSignature(authenticatorData, clientData, privateKeyPem);
|
||||
QCOMPARE(
|
||||
browserMessageBuilder()->getBase64FromArray(signature),
|
||||
QString("MOGw6KrerCgPf2mPig7FOTFIUDXYAU1v2uZj89_NgQTg2UddWnAB3JId3pa4zXghj8CkjjadVOI_LvweJGCEpmPQnRby71yFXnja6j"
|
||||
"Y3woX2b2klG2fB2alGZHHrVg6yVEmnAii4kYSdmoWxI7SmzLftoZfCJNFPFHujx2Pbr-6dIB02sZhtncetT0cpyWobtj9r7C5dIGfm"
|
||||
"J5n-LccP-F9gXGqtbN605VrIkC2WNztjdk3dAt5FGM_dlIwSe-vP1dKfIuNqAEbgr2IVZAUFn_ZfzUo-XbXTysksuz9JZfEopJBiUi"
|
||||
"9tjQDNvrYQFqB6wDPqkZAomkbRCohUb3TzCg"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testCreatingAttestationObjectWithEC()
|
||||
{
|
||||
// Predefined values for a desired outcome
|
||||
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
||||
const auto predefinedFirst = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
|
||||
const auto predefinedSecond = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
|
||||
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
|
||||
TestingVariables testingVariables = {id, predefinedFirst, predefinedSecond};
|
||||
auto result = browserPasskeys()->buildAttestationObject(publicKeyCredentialOptions, "", id, testingVariables);
|
||||
QCOMPARE(
|
||||
QString(result.cborEncoded),
|
||||
QString("\xA3"
|
||||
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
|
||||
"9\x7F)%\x0B`\x84\x1E\xF0"
|
||||
"E\x00\x00\x00\x01\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b\x00 \x8B\xB0\xCA"
|
||||
"6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:"
|
||||
"\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X "
|
||||
"e\xE2\xF2\x1F:cq\xD3G\xEA\xE0\xF7\x1F\xCF\xFA\\\xABO\xF6\x86\x88\x80\t\xAE\x81\x8BT\xB2\x9B\x15\x85~"
|
||||
"\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD"
|
||||
"5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7"));
|
||||
|
||||
// Double check that the result can be decoded
|
||||
BrowserCbor browserCbor;
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result.cborEncoded);
|
||||
|
||||
// Parse authData
|
||||
auto authDataJsonObject = attestationJsonObject["authData"].toString();
|
||||
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
|
||||
QVERIFY(authDataArray.size() >= 37);
|
||||
|
||||
auto authData = browserPasskeys()->parseAuthData(authDataArray);
|
||||
auto credentialData = authData["credentialData"].toObject();
|
||||
auto flags = authData["flags"].toObject();
|
||||
auto publicKey = credentialData["publicKey"].toObject();
|
||||
|
||||
// The attestationObject should include the same ID after decoding with the response root
|
||||
QCOMPARE(credentialData["credentialId"].toString(), QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
|
||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
QCOMPARE(flags["AT"], true);
|
||||
QCOMPARE(flags["UP"], true);
|
||||
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2);
|
||||
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256);
|
||||
QCOMPARE(publicKey["-1"], 1);
|
||||
QCOMPARE(publicKey["-2"], predefinedFirst);
|
||||
QCOMPARE(publicKey["-3"], predefinedSecond);
|
||||
}
|
||||
|
||||
void TestPasskeys::testCreatingAttestationObjectWithRSA()
|
||||
{
|
||||
// Predefined values for a desired outcome
|
||||
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
||||
const auto predefinedModulus = QString("vUhOZnyn8yn7U-nuHlsXZ6WDWLuYvevWWnwtoHxDEQq27vlp7yAfeVvAPkcvhxRcwoCEUespoa5"
|
||||
"5IDbkpp2Ypd6b15KbB4C-_4gM4r2FK9gfXghLPAXsMhstYv4keNFb4ghdlY5oUU3JCqUSMyOpmd"
|
||||
"HeX-RikLL0wgGv_tLT2DaDiWeyQCAtiDblr6COuTAU2kTpLc3Bn35geV9Iqw4iT8DwBQ-f8vjnI"
|
||||
"EDANXKUiRPojfy1q7WwEl-zMv6Ke2jFHxf68u82BSy3u9DOQaa24FAHoCm8Yd0n5IazMyoxyttl"
|
||||
"tRt8un8myVOGxcXMiR9_kQb9pu1RRLQMQLd-icE1Qw");
|
||||
const auto predefinedExponent = QString("AQAB");
|
||||
|
||||
// Force algorithm to RSA
|
||||
QJsonArray pubKeyCredParams;
|
||||
pubKeyCredParams.append(QJsonObject({{"type", "public-key"}, {"alg", -257}}));
|
||||
|
||||
auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
publicKeyCredentialOptions["pubKeyCredParams"] = pubKeyCredParams;
|
||||
|
||||
auto rpIdHash = browserMessageBuilder()->getSha256HashAsBase64(QString("webauthn.io"));
|
||||
QCOMPARE(rpIdHash, QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
|
||||
TestingVariables testingVariables = {id, predefinedModulus, predefinedExponent};
|
||||
auto result = browserPasskeys()->buildAttestationObject(publicKeyCredentialOptions, "", id, testingVariables);
|
||||
|
||||
// Double check that the result can be decoded
|
||||
BrowserCbor browserCbor;
|
||||
auto attestationJsonObject = browserCbor.getJsonFromCborData(result.cborEncoded);
|
||||
|
||||
// Parse authData
|
||||
auto authDataJsonObject = attestationJsonObject["authData"].toString();
|
||||
auto authDataArray = browserMessageBuilder()->getArrayFromBase64(authDataJsonObject);
|
||||
QVERIFY(authDataArray.size() >= 37);
|
||||
|
||||
auto authData = browserPasskeys()->parseAuthData(authDataArray);
|
||||
auto credentialData = authData["credentialData"].toObject();
|
||||
auto flags = authData["flags"].toObject();
|
||||
auto publicKey = credentialData["publicKey"].toObject();
|
||||
|
||||
// The attestationObject should include the same ID after decoding with the response root
|
||||
QCOMPARE(credentialData["credentialId"].toString(), QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
|
||||
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
|
||||
QCOMPARE(flags["AT"], true);
|
||||
QCOMPARE(flags["UP"], true);
|
||||
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA);
|
||||
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256);
|
||||
QCOMPARE(publicKey["-1"], predefinedModulus);
|
||||
QCOMPARE(publicKey["-2"], predefinedExponent);
|
||||
}
|
||||
|
||||
void TestPasskeys::testRegister()
|
||||
{
|
||||
// Predefined values for a desired outcome
|
||||
const auto predefinedId = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
||||
const auto predefinedX = QString("BuyvNFtikWFtGVkDplAqyjHElahp5fCH5dS4Ms0Ihd0");
|
||||
const auto predefinedY = QString("4u5_6Q8O6R0Hg0oDCdtCJLEL0yX_GDLhU5m3HUIE54M");
|
||||
const auto origin = QString("https://webauthn.io");
|
||||
const auto testDataPublicKey = browserMessageBuilder()->getJsonObject(PublicKeyCredential.toUtf8());
|
||||
const auto testDataResponse = testDataPublicKey["response"];
|
||||
const auto publicKeyCredentialOptions = browserMessageBuilder()->getJsonObject(PublicKeyCredentialOptions.toUtf8());
|
||||
|
||||
TestingVariables testingVariables = {predefinedId, predefinedX, predefinedY};
|
||||
auto result =
|
||||
browserPasskeys()->buildRegisterPublicKeyCredential(publicKeyCredentialOptions, origin, testingVariables);
|
||||
auto publicKeyCredential = result.response;
|
||||
QCOMPARE(publicKeyCredential["type"], QString("public-key"));
|
||||
QCOMPARE(publicKeyCredential["authenticatorAttachment"], QString("platform"));
|
||||
QCOMPARE(publicKeyCredential["id"], QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8"));
|
||||
|
||||
auto response = publicKeyCredential["response"].toObject();
|
||||
auto attestationObject = response["attestationObject"].toString();
|
||||
auto clientDataJson = response["clientDataJSON"].toString();
|
||||
QCOMPARE(attestationObject, testDataResponse["attestationObject"].toString());
|
||||
|
||||
// Parse clientDataJSON
|
||||
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
|
||||
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
|
||||
QCOMPARE(clientDataJsonObject["challenge"],
|
||||
QString("lVeHzVxWsr8MQxMkZF0ti6FXhdgMljqKzgA-q_zk2Mnii3eJ47VF97sqUoYktVC85WAZ1uIASm-a_lDFZwsLfw"));
|
||||
QCOMPARE(clientDataJsonObject["origin"], origin);
|
||||
QCOMPARE(clientDataJsonObject["type"], QString("webauthn.create"));
|
||||
}
|
||||
|
||||
void TestPasskeys::testGet()
|
||||
{
|
||||
const auto privateKeyPem = QString("-----BEGIN PRIVATE KEY-----"
|
||||
"MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg5DX2R6I37nMSZqCp"
|
||||
"XfHlE3UeitkGGE03FqGsdfxIBoOhRANCAAQG7K80W2KRYW0ZWQOmUCrKMcSVqGnl"
|
||||
"8Ifl1LgyzQiF3eLuf+kPDukdB4NKAwnbQiSxC9Ml/xgy4VOZtx1CBOeD"
|
||||
"-----END PRIVATE KEY-----");
|
||||
const auto origin = QString("https://webauthn.io");
|
||||
const auto id = QString("yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8");
|
||||
const auto publicKeyCredentialRequestOptions =
|
||||
browserMessageBuilder()->getJsonObject(PublicKeyCredentialRequestOptions.toUtf8());
|
||||
|
||||
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(
|
||||
publicKeyCredentialRequestOptions, origin, id, {}, privateKeyPem);
|
||||
QVERIFY(!publicKeyCredential.isEmpty());
|
||||
QCOMPARE(publicKeyCredential["id"].toString(), id);
|
||||
|
||||
auto response = publicKeyCredential["response"].toObject();
|
||||
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
|
||||
QCOMPARE(response["clientDataJSON"].toString(),
|
||||
QString("eyJjaGFsbGVuZ2UiOiI5ejM2dlRmUVRMOTVMZjdXblpneXRlN29oR2VGLVhSaUx4a0wtTHVHVTF6b3BSbU1JVUExTFZ3ekdwe"
|
||||
"UltMWZPQm4xUW5SYTBRSDI3QURBYUpHSHlzUSIsImNyb3NzT3JpZ2luIjpmYWxzZSwib3JpZ2luIjoiaHR0cHM6Ly93ZWJhdX"
|
||||
"Robi5pbyIsInR5cGUiOiJ3ZWJhdXRobi5nZXQifQ"));
|
||||
QCOMPARE(
|
||||
response["signature"].toString(),
|
||||
QString("MEUCIHFv0lOOGGloi_XoH5s3QDSs__8yAp9ZTMEjNiacMpOxAiEA04LAfO6TE7j12XNxd3zHQpn4kZN82jQFPntPiPBSD5c"));
|
||||
|
||||
auto clientDataJson = response["clientDataJSON"].toString();
|
||||
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);
|
||||
auto clientDataJsonObject = browserMessageBuilder()->getJsonObject(clientDataByteArray);
|
||||
QCOMPARE(clientDataJsonObject["challenge"].toString(), publicKeyCredentialRequestOptions["challenge"].toString());
|
||||
}
|
||||
|
||||
void TestPasskeys::testExtensions()
|
||||
{
|
||||
auto extensions = QJsonObject({{"credProps", true}, {"uvm", true}});
|
||||
auto result = browserPasskeys()->buildExtensionData(extensions);
|
||||
|
||||
BrowserCbor cbor;
|
||||
auto extensionJson = cbor.getJsonFromCborData(result);
|
||||
auto uvmArray = extensionJson["uvm"].toArray();
|
||||
QCOMPARE(extensionJson["credProps"].toObject()["rk"].toBool(), true);
|
||||
QCOMPARE(uvmArray.size(), 1);
|
||||
QCOMPARE(uvmArray.first().toArray().size(), 3);
|
||||
|
||||
auto partial = QJsonObject({{"props", true}, {"uvm", true}});
|
||||
auto faulty = QJsonObject({{"uvx", true}});
|
||||
auto partialData = browserPasskeys()->buildExtensionData(partial);
|
||||
auto faultyData = browserPasskeys()->buildExtensionData(faulty);
|
||||
|
||||
auto partialJson = cbor.getJsonFromCborData(partialData);
|
||||
QCOMPARE(partialJson["uvm"].toArray().size(), 1);
|
||||
|
||||
auto faultyJson = cbor.getJsonFromCborData(faultyData);
|
||||
QCOMPARE(faultyJson.size(), 0);
|
||||
}
|
||||
|
||||
void TestPasskeys::testParseFlags()
|
||||
{
|
||||
auto registerResult = browserPasskeys()->parseFlags("\x45");
|
||||
QCOMPARE(registerResult["ED"], false);
|
||||
QCOMPARE(registerResult["AT"], true);
|
||||
QCOMPARE(registerResult["BS"], false);
|
||||
QCOMPARE(registerResult["BE"], false);
|
||||
QCOMPARE(registerResult["UV"], true);
|
||||
QCOMPARE(registerResult["UP"], true);
|
||||
|
||||
auto getResult = browserPasskeys()->parseFlags("\x05"); // Only UP and UV
|
||||
QCOMPARE(getResult["ED"], false);
|
||||
QCOMPARE(getResult["AT"], false);
|
||||
QCOMPARE(getResult["BS"], false);
|
||||
QCOMPARE(getResult["BE"], false);
|
||||
QCOMPARE(getResult["UV"], true);
|
||||
QCOMPARE(getResult["UP"], true);
|
||||
}
|
||||
|
||||
void TestPasskeys::testSetFlags()
|
||||
{
|
||||
auto registerJson =
|
||||
QJsonObject({{"ED", false}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
|
||||
auto registerResult = browserPasskeys()->setFlagsFromJson(registerJson);
|
||||
QCOMPARE(registerResult, 0x45);
|
||||
|
||||
auto getJson =
|
||||
QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}});
|
||||
auto getResult = browserPasskeys()->setFlagsFromJson(getJson);
|
||||
QCOMPARE(getResult, 0x05);
|
||||
|
||||
// With "discouraged", so UV is false
|
||||
auto discouragedJson =
|
||||
QJsonObject({{"ED", false}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", false}, {"UP", true}});
|
||||
auto discouragedResult = browserPasskeys()->setFlagsFromJson(discouragedJson);
|
||||
QCOMPARE(discouragedResult, 0x01);
|
||||
}
|
47
tests/TestPasskeys.h
Normal file
47
tests/TestPasskeys.h
Normal file
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 or (at your option)
|
||||
* version 3 of the License.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_TESTPASSKEYS_H
|
||||
#define KEEPASSXC_TESTPASSKEYS_H
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "browser/BrowserPasskeys.h"
|
||||
|
||||
class TestPasskeys : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void init();
|
||||
|
||||
void testBase64WithHexStrings();
|
||||
void testDecodeResponseData();
|
||||
|
||||
void testLoadingECPrivateKeyFromPem();
|
||||
void testLoadingRSAPrivateKeyFromPem();
|
||||
void testCreatingAttestationObjectWithEC();
|
||||
void testCreatingAttestationObjectWithRSA();
|
||||
void testRegister();
|
||||
void testGet();
|
||||
|
||||
void testExtensions();
|
||||
void testParseFlags();
|
||||
void testSetFlags();
|
||||
};
|
||||
#endif // KEEPASSXC_TESTPASSKEYS_H
|
Loading…
Reference in New Issue
Block a user