Add Polkit Quick Unlock Support

Closes #5991
Closes #3337 - Support fingerprint readers on Linux

Polkit allows for authentication of many means, including fingerprint scanning. Furthermore, a common interface for Quick Unlocking has been implemented, and has been replaced throughout to make implementing other quick unlock strategies easier.

Refactor QuickUnlock to use UUID stored in headers. This is a new feature using the KDBX 4 standard to store a randomly generated UUID in the public headers of the database. This enables identification of KDBX file without relying on path or filename and will eventually support persistent Quick Unlock.
This commit is contained in:
Thomas Hobson 2023-09-04 23:35:06 -04:00 committed by Jonathan White
parent ddd2fcecea
commit f93adaa854
27 changed files with 839 additions and 260 deletions

View File

@ -37,7 +37,7 @@ jobs:
run: | run: |
sudo apt update sudo apt update
sudo apt install build-essential cmake g++ sudo apt install build-essential cmake g++
sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev sudo apt install qtbase5-dev qtbase5-private-dev qttools5-dev qttools5-dev-tools libqt5svg5-dev libargon2-dev libkeyutils-dev libminizip-dev libbotan-2-dev libqrencode-dev zlib1g-dev asciidoctor libreadline-dev libpcsclite-dev libusb-1.0-0-dev libxi-dev libxtst-dev libqt5x11extras5-dev
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL

View File

@ -18,7 +18,7 @@ set(EXCLUDED_DIRS
src/thirdparty src/thirdparty
src/zxcvbn src/zxcvbn
# objective-c directories # objective-c directories
src/touchid src/quickunlock/touchid
src/autotype/mac src/autotype/mac
src/gui/osutils/macutils) src/gui/osutils/macutils)

View File

@ -58,7 +58,12 @@ if(UNIX AND NOT APPLE AND NOT HAIKU)
EXCLUDE PATTERN "actions" EXCLUDE PATTERN "categories" EXCLUDE) EXCLUDE PATTERN "actions" EXCLUDE PATTERN "categories" EXCLUDE)
endif(KEEPASSXC_DIST_FLATPAK) endif(KEEPASSXC_DIST_FLATPAK)
configure_file(linux/${APP_ID}.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop @ONLY) configure_file(linux/${APP_ID}.desktop.in ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop @ONLY)
configure_file(linux/${APP_ID}.policy.in ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.policy @ONLY)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.desktop DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
if("${CMAKE_SYSTEM}" MATCHES "Linux")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/linux/${APP_ID}.policy DESTINATION ${CMAKE_INSTALL_DATADIR}/polkit-1/actions)
endif()
install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo) install(FILES linux/${APP_ID}.appdata.xml DESTINATION ${CMAKE_INSTALL_DATADIR}/metainfo)
endif(UNIX AND NOT APPLE AND NOT HAIKU) endif(UNIX AND NOT APPLE AND NOT HAIKU)

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>KeePassXC Developers</vendor>
<vendor_url></vendor_url>
<icon_name>@APP_ICON_NAME@</icon_name>
<action id="org.keepassxc.KeePassXC.unlockDatabase">
<description>Quick Unlock for a KeePassXC Database</description>
<message>Authentication is required to unlock a KeePassXC Database</message>
<defaults>
<allow_inactive>no</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
</policyconfig>

View File

@ -1518,10 +1518,6 @@ To prevent this error from appearing, you must go to &quot;Database Settings / S
<source>Retry with empty password</source> <source>Retry with empty password</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Failed to authenticate with Touch ID</source>
<translation type="unfinished"></translation>
</message>
<message> <message>
<source>Failed to open key file: %1</source> <source>Failed to open key file: %1</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
@ -1576,11 +1572,7 @@ If you do not have a key file, please leave the field empty.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message> <message>
<source>Failed to authenticate with Windows Hello: %1</source> <source>Failed to authenticate with Quick Unlock: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Windows Hello setup was canceled or failed. Quick unlock has not been enabled.</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
@ -7983,6 +7975,62 @@ Kernel: %3 %4</source>
<source>allow screenshots and app recording (Windows/macOS)</source> <source>allow screenshots and app recording (Windows/macOS)</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>AES initialization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES encrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store in Linux Keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not locate key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES decrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Quick Unlock provider is available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>
@ -8998,25 +9046,6 @@ Example: JBSWY3DPEHPK3PXP</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
</context> </context>
<context>
<name>WindowsHello</name>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context> <context>
<name>YubiKey</name> <name>YubiKey</name>
<message> <message>

View File

@ -194,6 +194,7 @@ set(keepassx_SOURCES
streams/qtiocompressor.cpp streams/qtiocompressor.cpp
streams/StoreDataStream.cpp streams/StoreDataStream.cpp
streams/SymmetricCipherStream.cpp streams/SymmetricCipherStream.cpp
quickunlock/QuickUnlockInterface.cpp
totp/totp.cpp) totp/totp.cpp)
if(APPLE) if(APPLE)
set(keepassx_SOURCES set(keepassx_SOURCES
@ -209,6 +210,12 @@ if(UNIX AND NOT APPLE)
${keepassx_SOURCES} ${keepassx_SOURCES}
gui/osutils/nixutils/ScreenLockListenerDBus.cpp gui/osutils/nixutils/ScreenLockListenerDBus.cpp
gui/osutils/nixutils/NixUtils.cpp) gui/osutils/nixutils/NixUtils.cpp)
if("${CMAKE_SYSTEM}" MATCHES "Linux")
set(keepassx_SOURCES
${keepassx_SOURCES}
quickunlock/Polkit.cpp
quickunlock/PolkitDbusTypes.cpp)
endif()
if(WITH_XC_X11) if(WITH_XC_X11)
list(APPEND keepassx_SOURCES list(APPEND keepassx_SOURCES
gui/osutils/nixutils/X11Funcs.cpp) gui/osutils/nixutils/X11Funcs.cpp)
@ -217,6 +224,21 @@ if(UNIX AND NOT APPLE)
gui/org.keepassxc.KeePassXC.MainWindow.xml gui/org.keepassxc.KeePassXC.MainWindow.xml
gui/MainWindow.h gui/MainWindow.h
MainWindow) MainWindow)
set_source_files_properties(
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
PROPERTIES
INCLUDE "quickunlock/PolkitDbusTypes.h"
)
qt5_add_dbus_interface(keepassx_SOURCES
quickunlock/dbus/org.freedesktop.PolicyKit1.Authority.xml
polkit_dbus
)
find_library(KEYUTILS_LIBRARIES NAMES keyutils)
if(NOT KEYUTILS_LIBRARIES)
message(FATAL_ERROR "Could not find libkeyutils")
endif()
endif() endif()
if(WIN32) if(WIN32)
set(keepassx_SOURCES set(keepassx_SOURCES
@ -224,7 +246,7 @@ if(WIN32)
gui/osutils/winutils/ScreenLockListenerWin.cpp gui/osutils/winutils/ScreenLockListenerWin.cpp
gui/osutils/winutils/WinUtils.cpp) gui/osutils/winutils/WinUtils.cpp)
if (MSVC) if (MSVC)
list(APPEND keepassx_SOURCES winhello/WindowsHello.cpp) list(APPEND keepassx_SOURCES quickunlock/WindowsHello.cpp)
endif() endif()
endif() endif()
@ -316,9 +338,9 @@ if(WITH_XC_NETWORKING)
endif() endif()
if(APPLE) if(APPLE)
list(APPEND keepassx_SOURCES touchid/TouchID.mm) list(APPEND keepassx_SOURCES quickunlock/TouchID.mm)
# TODO: Remove -Wno-error once deprecation warnings have been resolved. # TODO: Remove -Wno-error once deprecation warnings have been resolved.
set_source_files_properties(touchid/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast") set_source_files_properties(quickunlock/TouchID.mm PROPERTY COMPILE_FLAGS "-Wno-old-style-cast")
endif() endif()
configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h) configure_file(config-keepassx.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-keepassx.h)
@ -344,6 +366,7 @@ target_link_libraries(keepassx_core
${ZXCVBN_LIBRARIES} ${ZXCVBN_LIBRARIES}
${ZLIB_LIBRARIES} ${ZLIB_LIBRARIES}
${ARGON2_LIBRARIES} ${ARGON2_LIBRARIES}
${KEYUTILS_LIBRARIES}
${thirdparty_LIBRARIES} ${thirdparty_LIBRARIES}
) )

View File

@ -113,6 +113,8 @@ bool Database::open(QSharedPointer<const CompositeKey> key, QString* error)
* Unless `readOnly` is set to false, the database will be opened in * Unless `readOnly` is set to false, the database will be opened in
* read-write mode and fall back to read-only if that is not possible. * read-write mode and fall back to read-only if that is not possible.
* *
* If key is provided as null, only headers will be read.
*
* @param filePath path to the file * @param filePath path to the file
* @param key composite key for unlocking the database * @param key composite key for unlocking the database
* @param error error message in case of failure * @param error error message in case of failure
@ -996,3 +998,14 @@ void Database::stopModifiedTimer()
{ {
QMetaObject::invokeMethod(&m_modifiedTimer, "stop"); QMetaObject::invokeMethod(&m_modifiedTimer, "stop");
} }
QUuid Database::publicUuid()
{
if (!publicCustomData().contains("KPXC_PUBLIC_UUID")) {
publicCustomData().insert("KPXC_PUBLIC_UUID", QUuid::createUuid().toRfc4122());
markAsModified();
}
return QUuid::fromRfc4122(publicCustomData()["KPXC_PUBLIC_UUID"].toByteArray());
}

View File

@ -102,6 +102,7 @@ public:
bool hasNonDataChanges() const; bool hasNonDataChanges() const;
bool isSaving(); bool isSaving();
QUuid publicUuid();
QUuid uuid() const; QUuid uuid() const;
QString filePath() const; QString filePath() const;
QString canonicalFilePath() const; QString canonicalFilePath() const;

View File

@ -27,6 +27,8 @@
/** /**
* Read KDBX magic header numbers from a device. * Read KDBX magic header numbers from a device.
* *
* Passing a null key will only read in the unprotected headers.
*
* @param device input device * @param device input device
* @param sig1 KDBX signature 1 * @param sig1 KDBX signature 1
* @param sig2 KDBX signature 2 * @param sig2 KDBX signature 2
@ -55,6 +57,8 @@ bool KdbxReader::readMagicNumbers(QIODevice* device, quint32& sig1, quint32& sig
* Read KDBX stream from device. * Read KDBX stream from device.
* The device will automatically be reset to 0 before reading. * The device will automatically be reset to 0 before reading.
* *
* Passing a null key will only read in the unprotected headers.
*
* @param device input device * @param device input device
* @param key database encryption composite key * @param key database encryption composite key
* @param db database to read into * @param db database to read into
@ -91,6 +95,11 @@ bool KdbxReader::readDatabase(QIODevice* device, QSharedPointer<const CompositeK
return false; return false;
} }
// No key provided - don't proceed to load payload
if (key.isNull()) {
return true;
}
// read payload // read payload
return readDatabaseImpl(device, headerStream.storedData(), std::move(key), db); return readDatabaseImpl(device, headerStream.storedData(), std::move(key), db);
} }

View File

@ -29,15 +29,10 @@
#include "gui/Icons.h" #include "gui/Icons.h"
#include "gui/MainWindow.h" #include "gui/MainWindow.h"
#include "gui/osutils/OSUtils.h" #include "gui/osutils/OSUtils.h"
#include "quickunlock/QuickUnlockInterface.h"
#include "FileDialog.h" #include "FileDialog.h"
#include "MessageBox.h" #include "MessageBox.h"
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
class ApplicationSettingsWidget::ExtraPage class ApplicationSettingsWidget::ExtraPage
{ {
@ -314,14 +309,7 @@ void ApplicationSettingsWidget::loadSettings()
m_secUi->EnableCopyOnDoubleClickCheckBox->setChecked( m_secUi->EnableCopyOnDoubleClickCheckBox->setChecked(
config()->get(Config::Security_EnableCopyOnDoubleClick).toBool()); config()->get(Config::Security_EnableCopyOnDoubleClick).toBool());
bool quickUnlockAvailable = false; m_secUi->quickUnlockCheckBox->setEnabled(getQuickUnlock()->isAvailable());
#if defined(Q_OS_MACOS)
quickUnlockAvailable = TouchID::getInstance().isAvailable();
#elif defined(Q_CC_MSVC)
quickUnlockAvailable = getWindowsHello()->isAvailable();
connect(getWindowsHello(), &WindowsHello::availableChanged, m_secUi->quickUnlockCheckBox, &QCheckBox::setEnabled);
#endif
m_secUi->quickUnlockCheckBox->setEnabled(quickUnlockAvailable);
m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool()); m_secUi->quickUnlockCheckBox->setChecked(config()->get(Config::Security_QuickUnlock).toBool());
for (const ExtraPage& page : asConst(m_extraPages)) { for (const ExtraPage& page : asConst(m_extraPages)) {

View File

@ -26,19 +26,12 @@
#include "gui/MessageBox.h" #include "gui/MessageBox.h"
#include "keys/ChallengeResponseKey.h" #include "keys/ChallengeResponseKey.h"
#include "keys/FileKey.h" #include "keys/FileKey.h"
#include "quickunlock/QuickUnlockInterface.h"
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
#include <QCheckBox> #include <QCheckBox>
#include <QCloseEvent> #include <QCloseEvent>
#include <QDesktopServices> #include <QDesktopServices>
#include <QFont> #include <QFont>
namespace namespace
{ {
constexpr int clearFormsDelay = 30000; constexpr int clearFormsDelay = 30000;
@ -46,25 +39,17 @@ namespace
bool isQuickUnlockAvailable() bool isQuickUnlockAvailable()
{ {
if (config()->get(Config::Security_QuickUnlock).toBool()) { if (config()->get(Config::Security_QuickUnlock).toBool()) {
#if defined(Q_CC_MSVC) return getQuickUnlock()->isAvailable();
return getWindowsHello()->isAvailable();
#elif defined(Q_OS_MACOS)
return TouchID::getInstance().isAvailable();
#endif
} }
return false; return false;
} }
bool canPerformQuickUnlock(const QString& filename) bool canPerformQuickUnlock(const QUuid& dbUuid)
{ {
if (isQuickUnlockAvailable()) { if (isQuickUnlockAvailable()) {
#if defined(Q_CC_MSVC) return getQuickUnlock()->hasKey(dbUuid);
return getWindowsHello()->hasKey(filename);
#elif defined(Q_OS_MACOS)
return TouchID::getInstance().containsKey(filename);
#endif
} }
Q_UNUSED(filename); Q_UNUSED(dbUuid);
return false; return false;
} }
} // namespace } // namespace
@ -149,7 +134,7 @@ void DatabaseOpenWidget::showEvent(QShowEvent* event)
DialogyWidget::showEvent(event); DialogyWidget::showEvent(event);
if (isOnQuickUnlockScreen()) { if (isOnQuickUnlockScreen()) {
m_ui->quickUnlockButton->setFocus(); m_ui->quickUnlockButton->setFocus();
if (!canPerformQuickUnlock(m_filename)) { if (m_db.isNull() || !canPerformQuickUnlock(m_db->publicUuid())) {
resetQuickUnlock(); resetQuickUnlock();
} }
} else { } else {
@ -178,6 +163,12 @@ void DatabaseOpenWidget::load(const QString& filename)
clearForms(); clearForms();
m_filename = filename; m_filename = filename;
// Read public headers
QString error;
m_db.reset(new Database());
m_db->open(m_filename, nullptr, &error);
m_ui->fileNameLabel->setRawText(m_filename); m_ui->fileNameLabel->setRawText(m_filename);
if (config()->get(Config::RememberLastKeyFiles).toBool()) { if (config()->get(Config::RememberLastKeyFiles).toBool()) {
@ -187,7 +178,7 @@ void DatabaseOpenWidget::load(const QString& filename)
} }
} }
if (canPerformQuickUnlock(m_filename)) { if (canPerformQuickUnlock(m_db->publicUuid())) {
m_ui->centralStack->setCurrentIndex(1); m_ui->centralStack->setCurrentIndex(1);
m_ui->quickUnlockButton->setFocus(); m_ui->quickUnlockButton->setFocus();
} else { } else {
@ -215,7 +206,10 @@ void DatabaseOpenWidget::clearForms()
m_ui->keyFileLineEdit->setClearButtonEnabled(true); m_ui->keyFileLineEdit->setClearButtonEnabled(true);
m_ui->challengeResponseCombo->clear(); m_ui->challengeResponseCombo->clear();
m_ui->centralStack->setCurrentIndex(0); m_ui->centralStack->setCurrentIndex(0);
m_db.reset();
QString error;
m_db.reset(new Database());
m_db->open(m_filename, nullptr, &error);
} }
QSharedPointer<Database> DatabaseOpenWidget::database() QSharedPointer<Database> DatabaseOpenWidget::database()
@ -274,6 +268,8 @@ void DatabaseOpenWidget::openDatabase()
msgBox->exec(); msgBox->exec();
if (msgBox->clickedButton() != btn) { if (msgBox->clickedButton() != btn) {
m_db.reset(new Database()); m_db.reset(new Database());
m_db->open(m_filename, nullptr, &error);
m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error); m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error);
setUserInteractionLock(false); setUserInteractionLock(false);
return; return;
@ -283,17 +279,7 @@ void DatabaseOpenWidget::openDatabase()
// Save Quick Unlock credentials if available // Save Quick Unlock credentials if available
if (!blockQuickUnlock && isQuickUnlockAvailable()) { if (!blockQuickUnlock && isQuickUnlockAvailable()) {
auto keyData = databaseKey->serialize(); auto keyData = databaseKey->serialize();
#if defined(Q_CC_MSVC) getQuickUnlock()->setKey(m_db->publicUuid(), keyData);
// Store the password using Windows Hello
if (!getWindowsHello()->storeKey(m_filename, keyData)) {
getMainWindow()->displayTabMessage(
tr("Windows Hello setup was canceled or failed. Quick unlock has not been enabled."),
MessageWidget::MessageType::Warning);
}
#elif defined(Q_OS_MACOS)
// Store the password using TouchID
TouchID::getInstance().storeKey(m_filename, keyData);
#endif
m_ui->messageWidget->hideMessage(); m_ui->messageWidget->hideMessage();
} }
@ -338,27 +324,15 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{ {
auto databaseKey = QSharedPointer<CompositeKey>::create(); auto databaseKey = QSharedPointer<CompositeKey>::create();
if (canPerformQuickUnlock(m_filename)) { if (!m_db.isNull() && canPerformQuickUnlock(m_db->publicUuid())) {
// try to retrieve the stored password using Windows Hello // try to retrieve the stored password using Windows Hello
QByteArray keyData; QByteArray keyData;
#ifdef Q_CC_MSVC if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) {
if (!getWindowsHello()->getKey(m_filename, keyData)) { m_ui->messageWidget->showMessage(
// Failed to retrieve Quick Unlock data tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()),
auto error = getWindowsHello()->errorString();
if (!error.isEmpty()) {
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Windows Hello: %1").arg(error),
MessageWidget::Error); MessageWidget::Error);
resetQuickUnlock();
}
return {}; return {};
} }
#elif defined(Q_OS_MACOS)
if (!TouchID::getInstance().getKey(m_filename, keyData)) {
// Failed to retrieve Quick Unlock data
m_ui->messageWidget->showMessage(tr("Failed to authenticate with Touch ID"), MessageWidget::Error);
return {};
}
#endif
databaseKey->setRawKey(keyData); databaseKey->setRawKey(keyData);
return databaseKey; return databaseKey;
} }
@ -553,10 +527,11 @@ void DatabaseOpenWidget::triggerQuickUnlock()
*/ */
void DatabaseOpenWidget::resetQuickUnlock() void DatabaseOpenWidget::resetQuickUnlock()
{ {
#if defined(Q_CC_MSVC) if (!isQuickUnlockAvailable()) {
getWindowsHello()->reset(m_filename); return;
#elif defined(Q_OS_MACOS) }
TouchID::getInstance().reset(m_filename); if (!m_db.isNull()) {
#endif getQuickUnlock()->reset(m_db->publicUuid());
}
load(m_filename); load(m_filename);
} }

View File

@ -25,13 +25,7 @@
#include "keys/ChallengeResponseKey.h" #include "keys/ChallengeResponseKey.h"
#include "keys/FileKey.h" #include "keys/FileKey.h"
#include "keys/PasswordKey.h" #include "keys/PasswordKey.h"
#include "quickunlock/QuickUnlockInterface.h"
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
#ifdef Q_CC_MSVC
#include "winhello/WindowsHello.h"
#endif
#include <QLayout> #include <QLayout>
#include <QPushButton> #include <QPushButton>
@ -198,11 +192,7 @@ bool DatabaseSettingsWidgetDatabaseKey::save()
m_db->setKey(newKey, true, false, false); m_db->setKey(newKey, true, false, false);
#if defined(Q_OS_MACOS) getQuickUnlock()->reset(m_db->publicUuid());
TouchID::getInstance().reset(m_db->filePath());
#elif defined(Q_CC_MSVC)
getWindowsHello()->reset(m_db->filePath());
#endif
emit editFinished(true); emit editFinished(true);
if (m_isDirty) { if (m_isDirty) {

View File

@ -21,6 +21,7 @@
#include <QApplication> #include <QApplication>
#include <QDBusInterface> #include <QDBusInterface>
#include <QDebug>
#include <QDir> #include <QDir>
#include <QPointer> #include <QPointer>
#include <QStandardPaths> #include <QStandardPaths>
@ -323,3 +324,29 @@ void NixUtils::setColorScheme(QDBusVariant value)
m_systemColorschemePrefExists = true; m_systemColorschemePrefExists = true;
emit interfaceThemeChanged(); emit interfaceThemeChanged();
} }
quint64 NixUtils::getProcessStartTime() const
{
QString processStatPath = QString("/proc/%1/stat").arg(QCoreApplication::applicationPid());
QFile processStatFile(processStatPath);
if (!processStatFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
qDebug() << "nixutils: failed to open " << processStatPath;
return 0;
}
QTextStream processStatStream(&processStatFile);
QString processStatInfo = processStatStream.readLine();
processStatFile.close();
auto startIndex = processStatInfo.indexOf(')', -1);
if (startIndex != -1) {
auto tokens = processStatInfo.midRef(startIndex + 2).split(' ');
if (tokens.size() >= 20) {
return tokens[19].toULongLong();
}
}
qDebug() << "nixutils: failed to parse " << processStatPath;
return 0;
}

View File

@ -49,6 +49,8 @@ public:
return false; return false;
} }
quint64 getProcessStartTime() const;
private slots: private slots:
void handleColorSchemeRead(QDBusVariant value); void handleColorSchemeRead(QDBusVariant value);
void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value); void handleColorSchemeChanged(QString ns, QString key, QDBusVariant value);

View File

@ -30,9 +30,6 @@
#include "core/Global.h" #include "core/Global.h"
#include "core/Group.h" #include "core/Group.h"
#ifdef Q_OS_MACOS
#include "touchid/TouchID.h"
#endif
class ReportsDialog::ExtraPage class ReportsDialog::ExtraPage
{ {

247
src/quickunlock/Polkit.cpp Normal file
View File

@ -0,0 +1,247 @@
/*
* 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 "Polkit.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include "gui/osutils/nixutils/NixUtils.h"
#include <QDebug>
#include <QFile>
#include <QtDBus>
#include <botan/mem_ops.h>
#include <cerrno>
extern "C" {
#include <keyutils.h>
}
const QString polkit_service = "org.freedesktop.PolicyKit1";
const QString polkit_object = "/org/freedesktop/PolicyKit1/Authority";
namespace
{
QString getKeyName(const QUuid& dbUuid)
{
static const QString keyPrefix = "keepassxc_polkit_keys_";
return keyPrefix + dbUuid.toString();
}
} // namespace
Polkit::Polkit()
{
PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overriden
through an environment variable to return an alternative bus path. This bus could have an application
pretending to be polkit running on it, which could approve every authentication request
Most Linux distros place the system bus at this exact path, so it is hard-coded.
For any other distros, this path will need to be patched before compilation.
*/
QDBusConnection bus =
QDBusConnection::connectToBus("unix:path=/run/dbus/system_bus_socket", "keepassxc_polkit_dbus");
m_available = bus.isConnected();
if (!m_available) {
qDebug() << "polkit: Failed to connect to system dbus (this may be due to a non-standard dbus path)";
return;
}
m_available = bus.interface()->isServiceRegistered(polkit_service);
if (!m_available) {
qDebug() << "polkit: Polkit is not registered on dbus";
return;
}
m_polkit.reset(new org::freedesktop::PolicyKit1::Authority(polkit_service, polkit_object, bus));
}
Polkit::~Polkit()
{
}
void Polkit::reset(const QUuid& dbUuid)
{
m_encryptedMasterKeys.remove(dbUuid);
}
bool Polkit::isAvailable() const
{
return m_available;
}
QString Polkit::errorString() const
{
return m_error;
}
void Polkit::reset()
{
m_encryptedMasterKeys.clear();
}
bool Polkit::setKey(const QUuid& dbUuid, const QByteArray& key)
{
reset(dbUuid);
// Generate a random iv/key pair to encrypt the master password with
QByteArray randomKey = randomGen()->randomArray(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray randomIV = randomGen()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
QByteArray keychainKeyValue = randomKey + randomIV;
SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
m_error = QObject::tr("AES initialization failed");
return false;
}
// Encrypt the master password
QByteArray encryptedMasterKey = key;
if (!aes256Encrypt.finish(encryptedMasterKey)) {
m_error = QObject::tr("AES encrypt failed");
qDebug() << "polkit aes encrypt failed: " << aes256Encrypt.errorString();
return false;
}
// Add the iv/key pair into the linux keyring
key_serial_t key_serial = add_key("user",
getKeyName(dbUuid).toStdString().c_str(),
keychainKeyValue.constData(),
keychainKeyValue.size(),
KEY_SPEC_PROCESS_KEYRING);
if (key_serial < 0) {
m_error = QObject::tr("Failed to store in Linux Keyring");
qDebug() << "polkit keyring failed to store: " << errno;
return false;
}
// Scrub the keys from ram
Botan::secure_scrub_memory(randomKey.data(), randomKey.size());
Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
Botan::secure_scrub_memory(keychainKeyValue.data(), keychainKeyValue.size());
// Store encrypted master password and return
m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
return true;
}
bool Polkit::getKey(const QUuid& dbUuid, QByteArray& key)
{
if (!m_polkit || !hasKey(dbUuid)) {
return false;
}
PolkitSubject subject;
subject.kind = "unix-process";
subject.details.insert("pid", static_cast<uint>(QCoreApplication::applicationPid()));
subject.details.insert("start-time", nixUtils()->getProcessStartTime());
QMap<QString, QString> details;
auto result = m_polkit->CheckAuthorization(
subject,
"org.keepassxc.KeePassXC.unlockDatabase",
details,
0x00000001,
// AllowUserInteraction - wait for user to authenticate
// https://www.freedesktop.org/software/polkit/docs/0.105/eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html#eggdbus-enum-CheckAuthorizationFlags
"");
// A general error occurred
if (result.isError()) {
auto msg = result.error().message();
m_error = QObject::tr("Polkit returned an error: %1").arg(msg);
qDebug() << "polkit returned an error: " << msg;
return false;
}
PolkitAuthorizationResults authResult = result.value();
if (authResult.is_authorized) {
QByteArray encryptedMasterKey = m_encryptedMasterKeys.value(dbUuid);
key_serial_t keySerial =
find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING);
if (keySerial == -1) {
m_error = QObject::tr("Could not locate key in keyring");
qDebug() << "polkit keyring failed to find: " << errno;
return false;
}
void* keychainBuffer;
long keychainDataSize = keyctl_read_alloc(keySerial, &keychainBuffer);
if (keychainDataSize == -1) {
m_error = QObject::tr("Could not read key in keyring");
qDebug() << "polkit keyring failed to read: " << errno;
return false;
}
QByteArray keychainBytes(static_cast<const char*>(keychainBuffer), keychainDataSize);
Botan::secure_scrub_memory(keychainBuffer, keychainDataSize);
free(keychainBuffer);
QByteArray keychainKey = keychainBytes.left(SymmetricCipher::keySize(SymmetricCipher::Aes256_GCM));
QByteArray keychainIv = keychainBytes.right(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
SymmetricCipher aes256Decrypt;
if (!aes256Decrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, keychainKey, keychainIv)) {
m_error = QObject::tr("AES initialization failed");
qDebug() << "polkit aes init failed";
return false;
}
key = encryptedMasterKey;
if (!aes256Decrypt.finish(key)) {
key.clear();
m_error = QObject::tr("AES decrypt failed");
qDebug() << "polkit aes decrypt failed: " << aes256Decrypt.errorString();
return false;
}
// Scrub the keys from ram
Botan::secure_scrub_memory(keychainKey.data(), keychainKey.size());
Botan::secure_scrub_memory(keychainIv.data(), keychainIv.size());
Botan::secure_scrub_memory(keychainBytes.data(), keychainBytes.size());
Botan::secure_scrub_memory(encryptedMasterKey.data(), encryptedMasterKey.size());
return true;
}
// Failed to authenticate
if (authResult.is_challenge) {
m_error = QObject::tr("No Polkit authentication agent was available");
} else {
m_error = QObject::tr("Polkit authorization failed");
}
return false;
}
bool Polkit::hasKey(const QUuid& dbUuid) const
{
if (!m_encryptedMasterKeys.contains(dbUuid)) {
return false;
}
return find_key_by_type_and_desc("user", getKeyName(dbUuid).toStdString().c_str(), KEY_SPEC_PROCESS_KEYRING) != -1;
}

50
src/quickunlock/Polkit.h Normal file
View File

@ -0,0 +1,50 @@
/*
* 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 KEEPASSX_POLKIT_H
#define KEEPASSX_POLKIT_H
#include "QuickUnlockInterface.h"
#include "polkit_dbus.h"
#include <QHash>
#include <QScopedPointer>
class Polkit : public QuickUnlockInterface
{
public:
Polkit();
~Polkit() override;
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
bool m_available;
QString m_error;
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
QScopedPointer<org::freedesktop::PolicyKit1::Authority> m_polkit;
};
#endif // KEEPASSX_POLKIT_H

View File

@ -0,0 +1,45 @@
#include "PolkitDbusTypes.h"
void PolkitSubject::registerMetaType()
{
qRegisterMetaType<PolkitSubject>("PolkitSubject");
qDBusRegisterMetaType<PolkitSubject>();
}
QDBusArgument& operator<<(QDBusArgument& argument, const PolkitSubject& subject)
{
argument.beginStructure();
argument << subject.kind << subject.details;
argument.endStructure();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitSubject& subject)
{
argument.beginStructure();
argument >> subject.kind >> subject.details;
argument.endStructure();
return argument;
}
void PolkitAuthorizationResults::registerMetaType()
{
qRegisterMetaType<PolkitAuthorizationResults>("PolkitAuthorizationResults");
qDBusRegisterMetaType<PolkitAuthorizationResults>();
}
QDBusArgument& operator<<(QDBusArgument& argument, const PolkitAuthorizationResults& res)
{
argument.beginStructure();
argument << res.is_authorized << res.is_challenge << res.details;
argument.endStructure();
return argument;
}
const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& res)
{
argument.beginStructure();
argument >> res.is_authorized >> res.is_challenge >> res.details;
argument.endStructure();
return argument;
}

View File

@ -0,0 +1,36 @@
#ifndef KEEPASSX_POLKITDBUSTYPES_H
#define KEEPASSX_POLKITDBUSTYPES_H
#include <QtDBus>
class PolkitSubject
{
public:
QString kind;
QVariantMap details;
static void registerMetaType();
friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitSubject& subject);
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitSubject& subject);
};
class PolkitAuthorizationResults
{
public:
bool is_authorized;
bool is_challenge;
QMap<QString, QString> details;
static void registerMetaType();
friend QDBusArgument& operator<<(QDBusArgument& argument, const PolkitAuthorizationResults& subject);
friend const QDBusArgument& operator>>(const QDBusArgument& argument, PolkitAuthorizationResults& subject);
};
Q_DECLARE_METATYPE(PolkitSubject);
Q_DECLARE_METATYPE(PolkitAuthorizationResults);
#endif // KEEPASSX_POLKITDBUSTYPES_H

View File

@ -0,0 +1,81 @@
/*
* 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 "QuickUnlockInterface.h"
#include <QObject>
#if defined(Q_OS_MACOS)
#include "TouchID.h"
#define QUICKUNLOCK_IMPLEMENTATION TouchID
#elif defined(Q_CC_MSVC)
#include "WindowsHello.h"
#define QUICKUNLOCK_IMPLEMENTATION WindowsHello
#elif defined(Q_OS_LINUX)
#include "Polkit.h"
#define QUICKUNLOCK_IMPLEMENTATION Polkit
#else
#define QUICKUNLOCK_IMPLEMENTATION NoQuickUnlock
#endif
QUICKUNLOCK_IMPLEMENTATION* quickUnlockInstance = {nullptr};
QuickUnlockInterface* getQuickUnlock()
{
if (!quickUnlockInstance) {
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION();
}
return quickUnlockInstance;
}
bool NoQuickUnlock::isAvailable() const
{
return false;
}
QString NoQuickUnlock::errorString() const
{
return QObject::tr("No Quick Unlock provider is available");
}
void NoQuickUnlock::reset()
{
}
bool NoQuickUnlock::setKey(const QUuid& dbUuid, const QByteArray& key)
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
}
bool NoQuickUnlock::getKey(const QUuid& dbUuid, QByteArray& key)
{
Q_UNUSED(dbUuid)
Q_UNUSED(key)
return false;
}
bool NoQuickUnlock::hasKey(const QUuid& dbUuid) const
{
Q_UNUSED(dbUuid)
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
}

View File

@ -0,0 +1,58 @@
/*
* 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_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
#include <QUuid>
class QuickUnlockInterface
{
Q_DISABLE_COPY(QuickUnlockInterface)
public:
QuickUnlockInterface() = default;
virtual ~QuickUnlockInterface() = default;
virtual bool isAvailable() const = 0;
virtual QString errorString() const = 0;
virtual bool setKey(const QUuid& dbUuid, const QByteArray& key) = 0;
virtual bool getKey(const QUuid& dbUuid, QByteArray& key) = 0;
virtual bool hasKey(const QUuid& dbUuid) const = 0;
virtual void reset(const QUuid& dbUuid) = 0;
virtual void reset() = 0;
};
class NoQuickUnlock : public QuickUnlockInterface
{
public:
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
};
QuickUnlockInterface* getQuickUnlock();
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H

47
src/quickunlock/TouchID.h Normal file
View 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 KEEPASSX_TOUCHID_H
#define KEEPASSX_TOUCHID_H
#include "QuickUnlockInterface.h"
#include <QHash>
class TouchID : public QuickUnlockInterface
{
public:
bool isAvailable() const override;
QString errorString() const override;
bool setKey(const QUuid& dbUuid, const QByteArray& passwordKey) override;
bool getKey(const QUuid& dbUuid, QByteArray& passwordKey) override;
bool hasKey(const QUuid& dbUuid) const override;
void reset(const QUuid& dbUuid = "") override;
void reset() override;
private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QUuid& dbUuid);
QHash<QUuid, QByteArray> m_encryptedMasterKeys;
};
#endif // KEEPASSX_TOUCHID_H

View File

@ -1,4 +1,4 @@
#include "touchid/TouchID.h" #include "quickunlock/TouchID.h"
#include "crypto/Random.h" #include "crypto/Random.h"
#include "crypto/SymmetricCipher.h" #include "crypto/SymmetricCipher.h"
@ -13,6 +13,7 @@
#include <Security/Security.h> #include <Security/Security.h>
#include <QCoreApplication> #include <QCoreApplication>
#include <QString>
#define TOUCH_ID_ENABLE_DEBUG_LOGS() 0 #define TOUCH_ID_ENABLE_DEBUG_LOGS() 0
#if TOUCH_ID_ENABLE_DEBUG_LOGS() #if TOUCH_ID_ENABLE_DEBUG_LOGS()
@ -54,16 +55,6 @@ inline CFMutableDictionaryRef makeDictionary() {
return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); return CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
} }
/**
* Singleton
*/
TouchID& TouchID::getInstance()
{
static TouchID instance; // Guaranteed to be destroyed.
// Instantiated on first use.
return instance;
}
//! Try to delete an existing keychain entry //! Try to delete an existing keychain entry
void TouchID::deleteKeyEntry(const QString& accountName) void TouchID::deleteKeyEntry(const QString& accountName)
{ {
@ -77,14 +68,24 @@ void TouchID::deleteKeyEntry(const QString& accountName)
// get data from the KeyChain // get data from the KeyChain
OSStatus status = SecItemDelete(query); OSStatus status = SecItemDelete(query);
LogStatusError("TouchID::storeKey - Status deleting existing entry", status); LogStatusError("TouchID::deleteKeyEntry - Status deleting existing entry", status);
} }
QString TouchID::databaseKeyName(const QString &databasePath) QString TouchID::databaseKeyName(const QUuid& dbUuid)
{ {
static const QString keyPrefix = "KeepassXC_TouchID_Keys_"; static const QString keyPrefix = "KeepassXC_TouchID_Keys_";
const QByteArray pathHash = CryptoHash::hash(databasePath.toUtf8(), CryptoHash::Sha256).toHex(); return keyPrefix + dbUuid.toString();
return keyPrefix + pathHash; }
QString TouchID::errorString() const
{
// TODO
return "";
}
void TouchID::reset()
{
m_encryptedMasterKeys.clear();
} }
/** /**
@ -92,15 +93,15 @@ QString TouchID::databaseKeyName(const QString &databasePath)
* protects the database. The encrypted PasswordKey is kept in memory while the * protects the database. The encrypted PasswordKey is kept in memory while the
* AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch. * AES key is stored in the macOS KeyChain protected by either TouchID or Apple Watch.
*/ */
bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey) bool TouchID::setKey(const QUuid& dbUuid, const QByteArray& passwordKey)
{ {
if (databasePath.isEmpty() || passwordKey.isEmpty()) { if (passwordKey.isEmpty()) {
debug("TouchID::storeKey - illegal arguments"); debug("TouchID::setKey - illegal arguments");
return false; return false;
} }
if (m_encryptedMasterKeys.contains(databasePath)) { if (m_encryptedMasterKeys.contains(dbUuid)) {
debug("TouchID::storeKey - Already stored key for this database"); debug("TouchID::setKey - Already stored key for this database");
return true; return true;
} }
@ -110,7 +111,7 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
SymmetricCipher aes256Encrypt; SymmetricCipher aes256Encrypt;
if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) { if (!aes256Encrypt.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, randomKey, randomIV)) {
debug("TouchID::storeKey - AES initialisation failed"); debug("TouchID::setKey - AES initialisation failed");
return false; return false;
} }
@ -121,7 +122,7 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
return false; return false;
} }
const QString keyName = databaseKeyName(databasePath); const QString keyName = databaseKeyName(dbUuid);
deleteKeyEntry(keyName); // Try to delete the existing key entry deleteKeyEntry(keyName); // Try to delete the existing key entry
@ -152,7 +153,7 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
if (sacObject == NULL || error != NULL) { if (sacObject == NULL || error != NULL) {
NSError* e = (__bridge NSError*) error; NSError* e = (__bridge NSError*) error;
debug("TouchID::storeKey - Error creating security flags: %s", e.localizedDescription.UTF8String); debug("TouchID::setKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
return false; return false;
} }
@ -174,7 +175,7 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
// add to KeyChain // add to KeyChain
OSStatus status = SecItemAdd(attributes, NULL); OSStatus status = SecItemAdd(attributes, NULL);
LogStatusError("TouchID::storeKey - Status adding new entry", status); LogStatusError("TouchID::setKey - Status adding new entry", status);
CFRelease(sacObject); CFRelease(sacObject);
CFRelease(attributes); CFRelease(attributes);
@ -188,8 +189,8 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
Botan::secure_scrub_memory(randomIV.data(), randomIV.size()); Botan::secure_scrub_memory(randomIV.data(), randomIV.size());
// memorize which database the stored key is for // memorize which database the stored key is for
m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey); m_encryptedMasterKeys.insert(dbUuid, encryptedMasterKey);
debug("TouchID::storeKey - Success!"); debug("TouchID::setKey - Success!");
return true; return true;
} }
@ -197,15 +198,11 @@ bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKe
* Checks if an encrypted PasswordKey is available for the given database, tries to * Checks if an encrypted PasswordKey is available for the given database, tries to
* decrypt it using the KeyChain and if successful, returns it. * decrypt it using the KeyChain and if successful, returns it.
*/ */
bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const bool TouchID::getKey(const QUuid& dbUuid, QByteArray& passwordKey)
{ {
passwordKey.clear(); passwordKey.clear();
if (databasePath.isEmpty()) {
debug("TouchID::getKey - missing database path");
return false;
}
if (!containsKey(databasePath)) { if (!hasKey(dbUuid)) {
debug("TouchID::getKey - No stored key found"); debug("TouchID::getKey - No stored key found");
return false; return false;
} }
@ -213,7 +210,7 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
// query the KeyChain for the AES key // query the KeyChain for the AES key
CFMutableDictionaryRef query = makeDictionary(); CFMutableDictionaryRef query = makeDictionary();
const QString keyName = databaseKeyName(databasePath); const QString keyName = databaseKeyName(dbUuid);
NSString* accountName = keyName.toNSString(); // The NSString is released by Qt NSString* accountName = keyName.toNSString(); // The NSString is released by Qt
NSString* touchPromptMessage = NSString* touchPromptMessage =
QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database") QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database")
@ -254,7 +251,7 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
} }
// decrypt PasswordKey from memory using AES // decrypt PasswordKey from memory using AES
passwordKey = m_encryptedMasterKeys[databasePath]; passwordKey = m_encryptedMasterKeys[dbUuid];
if (!aes256Decrypt.finish(passwordKey)) { if (!aes256Decrypt.finish(passwordKey)) {
passwordKey.clear(); passwordKey.clear();
debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData()); debug("TouchID::getKey - AES decrypt failed: %s", aes256Decrypt.errorString().toUtf8().constData());
@ -268,9 +265,9 @@ bool TouchID::getKey(const QString& databasePath, QByteArray& passwordKey) const
return true; return true;
} }
bool TouchID::containsKey(const QString& dbPath) const bool TouchID::hasKey(const QUuid& dbUuid) const
{ {
return m_encryptedMasterKeys.contains(dbPath); return m_encryptedMasterKeys.contains(dbUuid);
} }
// TODO: Both functions below should probably handle the returned errors to // TODO: Both functions below should probably handle the returned errors to
@ -336,7 +333,7 @@ bool TouchID::isTouchIdAvailable()
} }
//! @return true if either TouchID or Apple Watch is available at the moment. //! @return true if either TouchID or Apple Watch is available at the moment.
bool TouchID::isAvailable() bool TouchID::isAvailable() const
{ {
// note: we cannot cache the check results because the configuration // note: we cannot cache the check results because the configuration
// is dynamic in its nature. User can close the laptop lid or take off // is dynamic in its nature. User can close the laptop lid or take off
@ -349,12 +346,7 @@ bool TouchID::isAvailable()
/** /**
* Resets the inner state either for all or for the given database * Resets the inner state either for all or for the given database
*/ */
void TouchID::reset(const QString& databasePath) void TouchID::reset(const QUuid& dbUuid)
{ {
if (databasePath.isEmpty()) { m_encryptedMasterKeys.remove(dbUuid);
m_encryptedMasterKeys.clear();
return;
}
m_encryptedMasterKeys.remove(databasePath);
} }

View File

@ -99,28 +99,10 @@ namespace
} }
} // namespace } // namespace
WindowsHello* WindowsHello::m_instance{nullptr};
WindowsHello* WindowsHello::instance()
{
if (!m_instance) {
m_instance = new WindowsHello();
}
return m_instance;
}
WindowsHello::WindowsHello(QObject* parent)
: QObject(parent)
{
concurrency::create_task([this] {
bool state = KeyCredentialManager::IsSupportedAsync().get();
m_available = state;
emit availableChanged(m_available);
});
}
bool WindowsHello::isAvailable() const bool WindowsHello::isAvailable() const
{ {
return m_available; auto task = concurrency::create_task([] { return KeyCredentialManager::IsSupportedAsync().get(); });
return task.get();
} }
QString WindowsHello::errorString() const QString WindowsHello::errorString() const
@ -128,7 +110,7 @@ QString WindowsHello::errorString() const
return m_error; return m_error;
} }
bool WindowsHello::storeKey(const QString& dbPath, const QByteArray& data) bool WindowsHello::setKey(const QUuid& dbUuid, const QByteArray& data)
{ {
queueSecurityPromptFocus(); queueSecurityPromptFocus();
@ -144,26 +126,26 @@ bool WindowsHello::storeKey(const QString& dbPath, const QByteArray& data)
// Encrypt the data using AES-256-CBC // Encrypt the data using AES-256-CBC
SymmetricCipher cipher; SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, challenge)) { if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, challenge)) {
m_error = tr("Failed to init KeePassXC crypto."); m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false; return false;
} }
QByteArray encrypted = data; QByteArray encrypted = data;
if (!cipher.finish(encrypted)) { if (!cipher.finish(encrypted)) {
m_error = tr("Failed to encrypt key data."); m_error = QObject::tr("Failed to encrypt key data.");
return false; return false;
} }
// Prepend the challenge/IV to the encrypted data // Prepend the challenge/IV to the encrypted data
encrypted.prepend(challenge); encrypted.prepend(challenge);
m_encryptedKeys.insert(dbPath, encrypted); m_encryptedKeys.insert(dbUuid, encrypted);
return true; return true;
} }
bool WindowsHello::getKey(const QString& dbPath, QByteArray& data) bool WindowsHello::getKey(const QUuid& dbUuid, QByteArray& data)
{ {
data.clear(); data.clear();
if (!hasKey(dbPath)) { if (!hasKey(dbUuid)) {
m_error = tr("Failed to get Windows Hello credential."); m_error = QObject::tr("Failed to get Windows Hello credential.");
return false; return false;
} }
@ -171,7 +153,7 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data)
// Read the previously used challenge and encrypted data // Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM); auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = m_encryptedKeys.value(dbPath); const auto& keydata = m_encryptedKeys.value(dbUuid);
auto challenge = keydata.left(ivSize); auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize); auto encrypted = keydata.mid(ivSize);
QByteArray key; QByteArray key;
@ -183,7 +165,7 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data)
// Decrypt the data using the generated key and IV from above // Decrypt the data using the generated key and IV from above
SymmetricCipher cipher; SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) { if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) {
m_error = tr("Failed to init KeePassXC crypto."); m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false; return false;
} }
@ -191,21 +173,21 @@ bool WindowsHello::getKey(const QString& dbPath, QByteArray& data)
data = encrypted; data = encrypted;
if (!cipher.finish(data)) { if (!cipher.finish(data)) {
data.clear(); data.clear();
m_error = tr("Failed to decrypt key data."); m_error = QObject::tr("Failed to decrypt key data.");
return false; return false;
} }
return true; return true;
} }
void WindowsHello::reset(const QString& dbPath) void WindowsHello::reset(const QUuid& dbUuid)
{ {
m_encryptedKeys.remove(dbPath); m_encryptedKeys.remove(dbUuid);
} }
bool WindowsHello::hasKey(const QString& dbPath) const bool WindowsHello::hasKey(const QUuid& dbUuid) const
{ {
return m_encryptedKeys.contains(dbPath); return m_encryptedKeys.contains(dbUuid);
} }
void WindowsHello::reset() void WindowsHello::reset()

View File

@ -18,41 +18,28 @@
#ifndef KEEPASSXC_WINDOWSHELLO_H #ifndef KEEPASSXC_WINDOWSHELLO_H
#define KEEPASSXC_WINDOWSHELLO_H #define KEEPASSXC_WINDOWSHELLO_H
#include "QuickUnlockInterface.h";
#include <QHash> #include <QHash>
#include <QObject> #include <QObject>
class WindowsHello : public QObject class WindowsHello : public QuickUnlockInterface
{ {
Q_OBJECT
public: public:
static WindowsHello* instance(); WindowsHello() = default;
bool isAvailable() const; bool isAvailable() const override;
QString errorString() const; QString errorString() const override;
void reset(); void reset() override;
bool storeKey(const QString& dbPath, const QByteArray& key); bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QString& dbPath, QByteArray& key); bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QString& dbPath) const; bool hasKey(const QUuid& dbUuid) const override;
void reset(const QString& dbPath); void reset(const QUuid& dbUuid) override;
signals:
void availableChanged(bool state);
private: private:
bool m_available = false;
QString m_error; QString m_error;
QHash<QString, QByteArray> m_encryptedKeys; QHash<QUuid, QByteArray> m_encryptedKeys;
static WindowsHello* m_instance;
WindowsHello(QObject* parent = nullptr);
~WindowsHello() override = default;
Q_DISABLE_COPY(WindowsHello); Q_DISABLE_COPY(WindowsHello);
}; };
inline WindowsHello* getWindowsHello()
{
return WindowsHello::instance();
}
#endif // KEEPASSXC_WINDOWSHELLO_H #endif // KEEPASSXC_WINDOWSHELLO_H

View File

@ -0,0 +1,16 @@
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN" "http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
<node>
<interface name="org.freedesktop.PolicyKit1.Authority">
<method name="CheckAuthorization">
<arg type="(sa{sv})" name="subject" direction="in" />
<arg type="s" name="action_id" direction="in" />
<arg type="a{ss}" name="details" direction="in" />
<arg type="u" name="flags" direction="in" />
<arg type="s" name="cancellation_id" direction="in" />>
<arg type="(bba{ss})" name="result" direction="out" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="PolkitAuthorizationResults"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="PolkitSubject"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="QMap&lt;QString, QString&gt;"/>
</method>
</interface>
</node>

View File

@ -1,39 +0,0 @@
#ifndef KEEPASSX_TOUCHID_H
#define KEEPASSX_TOUCHID_H
#include <QHash>
class TouchID
{
public:
static TouchID& getInstance();
private:
TouchID()
{
// Nothing to do here
}
public:
TouchID(TouchID const&) = delete;
void operator=(TouchID const&) = delete;
bool storeKey(const QString& databasePath, const QByteArray& passwordKey);
bool getKey(const QString& databasePath, QByteArray& passwordKey) const;
bool containsKey(const QString& databasePath) const;
void reset(const QString& databasePath = "");
bool isAvailable();
private:
static bool isWatchAvailable();
static bool isTouchIdAvailable();
static void deleteKeyEntry(const QString& accountName);
static QString databaseKeyName(const QString& databasePath);
private:
QHash<QString, QByteArray> m_encryptedMasterKeys;
};
#endif // KEEPASSX_TOUCHID_H