mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
Add TouchID support on macOS
This commit is contained in:
parent
5aeb30e845
commit
d9fcdd2920
@ -47,6 +47,9 @@ option(WITH_XC_NETWORKING "Include networking code (e.g. for downlading website
|
||||
option(WITH_XC_BROWSER "Include browser integration with keepassxc-browser." OFF)
|
||||
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
|
||||
option(WITH_XC_SSHAGENT "Include SSH agent support." OFF)
|
||||
if(APPLE)
|
||||
option(WITH_XC_TOUCHID "Include TouchID support for macOS." OFF)
|
||||
endif()
|
||||
|
||||
if(WITH_XC_ALL)
|
||||
# Enable all options
|
||||
@ -55,6 +58,9 @@ if(WITH_XC_ALL)
|
||||
set(WITH_XC_BROWSER ON)
|
||||
set(WITH_XC_YUBIKEY ON)
|
||||
set(WITH_XC_SSHAGENT ON)
|
||||
if(APPLE)
|
||||
set(WITH_XC_TOUCHID ON)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# Process ui files automatically from source files
|
||||
|
@ -768,6 +768,14 @@ Please consider generating a new key file.</translation>
|
||||
<source>Select key file</source>
|
||||
<translation>Select key file</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>authenticate to access the database</source>
|
||||
<translation>authenticate to access the database</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>authenticate a privileged operation</source>
|
||||
<translation>authenticate a privileged operation</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
<name>DatabaseRepairWidget</name>
|
||||
|
@ -198,6 +198,9 @@ add_feature_info(Networking WITH_XC_NETWORKING "Compile KeePassXC with network a
|
||||
add_feature_info(KeePassXC-Browser WITH_XC_BROWSER "Browser integration with KeePassXC-Browser")
|
||||
add_feature_info(SSHAgent WITH_XC_SSHAGENT "SSH agent integration compatible with KeeAgent")
|
||||
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
|
||||
if(APPLE)
|
||||
add_feature_info(TouchID WITH_XC_TOUCHID "TouchID integration")
|
||||
endif()
|
||||
|
||||
set(BROWSER_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/browser)
|
||||
add_subdirectory(browser)
|
||||
@ -239,6 +242,10 @@ else()
|
||||
list(APPEND keepassx_SOURCES keys/drivers/YubiKeyStub.cpp)
|
||||
endif()
|
||||
|
||||
if(WITH_XC_TOUCHID)
|
||||
list(APPEND keepassx_SOURCES touchid/TouchID.mm)
|
||||
endif()
|
||||
|
||||
add_library(autotype STATIC ${autotype_SOURCES})
|
||||
target_link_libraries(autotype Qt5::Core Qt5::Widgets)
|
||||
|
||||
@ -266,6 +273,10 @@ if(APPLE)
|
||||
if(Qt5MacExtras_FOUND)
|
||||
target_link_libraries(keepassx_core Qt5::MacExtras)
|
||||
endif()
|
||||
if(WITH_XC_TOUCHID)
|
||||
target_link_libraries(keepassx_core "-framework Security")
|
||||
target_link_libraries(keepassx_core "-framework LocalAuthentication")
|
||||
endif()
|
||||
endif()
|
||||
if (UNIX AND NOT APPLE)
|
||||
target_link_libraries(keepassx_core Qt5::DBus)
|
||||
@ -297,6 +308,11 @@ if(APPLE AND WITH_APP_BUNDLE)
|
||||
MACOSX_BUNDLE ON
|
||||
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_BINARY_DIR}/Info.plist)
|
||||
|
||||
if(WITH_XC_TOUCHID)
|
||||
set_target_properties(${PROGNAME} PROPERTIES
|
||||
CPACK_BUNDLE_APPLE_ENTITLEMENTS "${CMAKE_SOURCE_DIR}/share/macosx/keepassxc.entitlements" )
|
||||
endif()
|
||||
|
||||
if(QT_MAC_USE_COCOA AND EXISTS "${QT_LIBRARY_DIR}/Resources/qt_menu.nib")
|
||||
install(DIRECTORY "${QT_LIBRARY_DIR}/Resources/qt_menu.nib"
|
||||
DESTINATION "${DATA_INSTALL_DIR}")
|
||||
|
@ -17,6 +17,7 @@
|
||||
#cmakedefine WITH_XC_BROWSER
|
||||
#cmakedefine WITH_XC_YUBIKEY
|
||||
#cmakedefine WITH_XC_SSHAGENT
|
||||
#cmakedefine WITH_XC_TOUCHID
|
||||
|
||||
#cmakedefine KEEPASSXC_BUILD_TYPE "@KEEPASSXC_BUILD_TYPE@"
|
||||
#cmakedefine KEEPASSXC_BUILD_TYPE_RELEASE
|
||||
|
@ -149,6 +149,9 @@ void Config::init(const QString& fileName)
|
||||
m_defaults.insert("security/hidepassworddetails", true);
|
||||
m_defaults.insert("security/autotypeask", true);
|
||||
m_defaults.insert("security/IconDownloadFallbackToGoogle", false);
|
||||
m_defaults.insert("security/resettouchid", false);
|
||||
m_defaults.insert("security/resettouchidtimeout", 30);
|
||||
m_defaults.insert("security/resettouchidscreenlock", true);
|
||||
m_defaults.insert("GUI/Language", "system");
|
||||
m_defaults.insert("GUI/HideToolbar", false);
|
||||
m_defaults.insert("GUI/ShowTrayIcon", false);
|
||||
|
@ -92,6 +92,9 @@ AboutDialog::AboutDialog(QWidget* parent)
|
||||
#ifdef WITH_XC_YUBIKEY
|
||||
extensions += "\n- " + tr("YubiKey");
|
||||
#endif
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
extensions += "\n- " + tr("TouchID");
|
||||
#endif
|
||||
|
||||
if (extensions.isEmpty())
|
||||
extensions = " " + tr("None");
|
||||
|
@ -30,6 +30,7 @@
|
||||
#include "keys/FileKey.h"
|
||||
#include "keys/PasswordKey.h"
|
||||
#include "keys/YkChallengeResponseKey.h"
|
||||
#include "touchid/TouchID.h"
|
||||
|
||||
#include "config-keepassx.h"
|
||||
|
||||
@ -82,6 +83,14 @@ DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
|
||||
m_ui->gridLayout->setContentsMargins(10, 0, 0, 0);
|
||||
m_ui->labelLayout->setContentsMargins(10, 0, 10, 0);
|
||||
#endif
|
||||
|
||||
#ifndef WITH_XC_TOUCHID
|
||||
m_ui->checkTouchID->setVisible(false);
|
||||
#else
|
||||
if (!TouchID::getInstance().isAvailable()) {
|
||||
m_ui->checkTouchID->setVisible(false);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
DatabaseOpenWidget::~DatabaseOpenWidget()
|
||||
@ -132,6 +141,9 @@ void DatabaseOpenWidget::load(const QString& filename)
|
||||
}
|
||||
}
|
||||
|
||||
QHash<QString, QVariant> useTouchID = config()->get("UseTouchID").toHash();
|
||||
m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
|
||||
|
||||
m_ui->editPassword->setFocus();
|
||||
}
|
||||
|
||||
@ -142,6 +154,7 @@ void DatabaseOpenWidget::clearForms()
|
||||
m_ui->checkPassword->setChecked(true);
|
||||
m_ui->checkKeyFile->setChecked(false);
|
||||
m_ui->checkChallengeResponse->setChecked(false);
|
||||
m_ui->checkTouchID->setChecked(false);
|
||||
m_ui->buttonTogglePassword->setChecked(false);
|
||||
m_db = nullptr;
|
||||
}
|
||||
@ -188,6 +201,24 @@ void DatabaseOpenWidget::openDatabase()
|
||||
QApplication::restoreOverrideCursor();
|
||||
|
||||
if (m_db) {
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
QHash<QString, QVariant> useTouchID = config()->get("UseTouchID").toHash();
|
||||
|
||||
// check if TouchID can & should be used to unlock the database next time
|
||||
if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable()) {
|
||||
// encrypt and store key blob
|
||||
if (TouchID::getInstance().storeKey(m_filename, PasswordKey(m_ui->editPassword->text()).rawKey())) {
|
||||
useTouchID.insert(m_filename, true);
|
||||
}
|
||||
} else {
|
||||
// when TouchID not available or unchecked, reset for the current database
|
||||
TouchID::getInstance().reset(m_filename);
|
||||
useTouchID.insert(m_filename, false);
|
||||
}
|
||||
|
||||
config()->set("UseTouchID", useTouchID);
|
||||
#endif
|
||||
|
||||
if (m_ui->messageWidget->isVisible()) {
|
||||
m_ui->messageWidget->animatedHide();
|
||||
}
|
||||
@ -196,6 +227,11 @@ void DatabaseOpenWidget::openDatabase()
|
||||
m_ui->messageWidget->showMessage(tr("Unable to open the database.").append("\n").append(reader.errorString()),
|
||||
MessageWidget::Error);
|
||||
m_ui->editPassword->clear();
|
||||
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
// unable to unlock database, reset TouchID for the current database
|
||||
TouchID::getInstance().reset(m_filename);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,6 +243,21 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
|
||||
masterKey->addKey(PasswordKey(m_ui->editPassword->text()));
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
// check if TouchID is available and enabled for unlocking the database
|
||||
if (m_ui->checkTouchID->isChecked() && TouchID::getInstance().isAvailable() && masterKey->isEmpty()) {
|
||||
// try to get, decrypt and use PasswordKey
|
||||
QSharedPointer<QByteArray> passwordKey = TouchID::getInstance().getKey(m_filename);
|
||||
if (passwordKey != NULL) {
|
||||
// check if the user cancelled the operation
|
||||
if (passwordKey.isNull())
|
||||
return QSharedPointer<CompositeKey>();
|
||||
|
||||
masterKey->addKey(PasswordKey::fromRawKey(*passwordKey));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
QHash<QString, QVariant> lastKeyFiles = config()->get("LastKeyFiles").toHash();
|
||||
QHash<QString, QVariant> lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
<height>302</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0,1,0,0,3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0,1,0,0,0">
|
||||
<property name="spacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
@ -142,7 +142,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="2">
|
||||
<item row="2" column="2">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
@ -153,7 +153,7 @@
|
||||
<property name="verticalSpacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="1" column="1">
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="buttonRedetectYubikey">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
@ -163,7 +163,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="0" column="0">
|
||||
<widget class="QComboBox" name="comboChallengeResponse">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@ -179,7 +179,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="1" column="0">
|
||||
<widget class="QProgressBar" name="yubikeyProgress">
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
@ -203,25 +203,35 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="checkChallengeResponse">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>2</number>
|
||||
<property name="text">
|
||||
<string>Challenge Response:</string>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkChallengeResponse">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Challenge Response:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="checkTouchID">
|
||||
<property name="text">
|
||||
<string>Use TouchID for quick unlock</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<spacer name="verticalSpacer_4">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
|
@ -55,6 +55,7 @@
|
||||
#include "gui/entry/EntryView.h"
|
||||
#include "gui/group/EditGroupWidget.h"
|
||||
#include "gui/group/GroupView.h"
|
||||
#include "touchid/TouchID.h"
|
||||
|
||||
#include "config-keepassx.h"
|
||||
|
||||
@ -820,6 +821,9 @@ void DatabaseWidget::updateMasterKey(bool accepted)
|
||||
if (accepted) {
|
||||
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
|
||||
bool result = m_db->setKey(m_changeMasterKeyWidget->newMasterKey(), true, true);
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
TouchID::getInstance().reset(m_filePath);
|
||||
#endif
|
||||
QApplication::restoreOverrideCursor();
|
||||
|
||||
if (!result) {
|
||||
|
@ -60,6 +60,8 @@
|
||||
#include "gui/PasswordGeneratorWidget.h"
|
||||
#include "gui/SettingsWidget.h"
|
||||
|
||||
#include "touchid/TouchID.h"
|
||||
|
||||
#ifdef WITH_XC_BROWSER
|
||||
class BrowserPlugin : public ISettingsPage
|
||||
{
|
||||
@ -182,6 +184,10 @@ MainWindow::MainWindow()
|
||||
|
||||
m_inactivityTimer = new InactivityTimer(this);
|
||||
connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity()));
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
m_touchIDinactivityTimer = new InactivityTimer(this);
|
||||
connect(m_touchIDinactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(forgetTouchIDAfterInactivity()));
|
||||
#endif
|
||||
applySettingsChanges();
|
||||
|
||||
m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N);
|
||||
@ -869,6 +875,21 @@ void MainWindow::applySettingsChanges()
|
||||
m_inactivityTimer->deactivate();
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
// forget TouchID (in minutes)
|
||||
timeout = config()->get("security/resettouchidtimeout").toInt() * 60 * 1000;
|
||||
if (timeout <= 0) {
|
||||
timeout = 30 * 60 * 1000;
|
||||
}
|
||||
|
||||
m_touchIDinactivityTimer->setInactivityTimeout(timeout);
|
||||
if (config()->get("security/resettouchid").toBool()) {
|
||||
m_touchIDinactivityTimer->activate();
|
||||
} else {
|
||||
m_touchIDinactivityTimer->deactivate();
|
||||
}
|
||||
#endif
|
||||
|
||||
m_ui->toolBar->setHidden(config()->get("GUI/HideToolbar").toBool());
|
||||
|
||||
updateTrayIcon();
|
||||
@ -935,6 +956,13 @@ void MainWindow::lockDatabasesAfterInactivity()
|
||||
m_ui->tabWidget->lockDatabases();
|
||||
}
|
||||
|
||||
void MainWindow::forgetTouchIDAfterInactivity()
|
||||
{
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
TouchID::getInstance().reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
void MainWindow::repairDatabase()
|
||||
{
|
||||
QString filter = QString("%1 (*.kdbx);;%2 (*)").arg(tr("KeePass 2 Database"), tr("All files"));
|
||||
@ -1031,6 +1059,12 @@ void MainWindow::handleScreenLock()
|
||||
if (config()->get("security/lockdatabasescreenlock").toBool()) {
|
||||
lockDatabasesAfterInactivity();
|
||||
}
|
||||
|
||||
#ifdef WITH_XC_TOUCHID
|
||||
if (config()->get("security/resettouchidscreenlock").toBool()) {
|
||||
forgetTouchIDAfterInactivity();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
QStringList MainWindow::kdbxFilesFromUrls(const QList<QUrl>& urls)
|
||||
|
@ -106,6 +106,7 @@ private slots:
|
||||
void hideWindow();
|
||||
void toggleWindow();
|
||||
void lockDatabasesAfterInactivity();
|
||||
void forgetTouchIDAfterInactivity();
|
||||
void repairDatabase();
|
||||
void hideTabMessage();
|
||||
void handleScreenLock();
|
||||
@ -133,6 +134,7 @@ private:
|
||||
QActionGroup* m_copyAdditionalAttributeActions;
|
||||
QStringList m_openDatabases;
|
||||
InactivityTimer* m_inactivityTimer;
|
||||
InactivityTimer* m_touchIDinactivityTimer;
|
||||
int m_countDefaultAttributes;
|
||||
QSystemTrayIcon* m_trayIcon;
|
||||
ScreenLockListener* m_screenLockListener;
|
||||
|
@ -27,6 +27,8 @@
|
||||
#include "core/Global.h"
|
||||
#include "core/Translator.h"
|
||||
|
||||
#include "touchid/TouchID.h"
|
||||
|
||||
class SettingsWidget::ExtraPage
|
||||
{
|
||||
public:
|
||||
@ -86,9 +88,25 @@ SettingsWidget::SettingsWidget(QWidget* parent)
|
||||
m_secUi->lockDatabaseIdleSpinBox,
|
||||
SLOT(setEnabled(bool)));
|
||||
|
||||
connect(m_secUi->touchIDResetCheckBox,
|
||||
SIGNAL(toggled(bool)),
|
||||
m_secUi->touchIDResetSpinBox,
|
||||
SLOT(setEnabled(bool)));
|
||||
|
||||
#ifndef WITH_XC_NETWORKING
|
||||
m_secUi->privacy->setVisible(false);
|
||||
#endif
|
||||
|
||||
#ifndef WITH_XC_TOUCHID
|
||||
bool hideTouchID = true;
|
||||
#else
|
||||
bool hideTouchID = !TouchID::getInstance().isAvailable();
|
||||
#endif
|
||||
if (hideTouchID) {
|
||||
m_secUi->touchIDResetCheckBox->setVisible(false);
|
||||
m_secUi->touchIDResetSpinBox->setVisible(false);
|
||||
m_secUi->touchIDResetOnScreenLockCheckBox->setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsWidget::~SettingsWidget()
|
||||
@ -175,6 +193,10 @@ void SettingsWidget::loadSettings()
|
||||
m_secUi->passwordRepeatCheckBox->setChecked(config()->get("security/passwordsrepeat").toBool());
|
||||
m_secUi->hideNotesCheckBox->setChecked(config()->get("security/hidenotes").toBool());
|
||||
|
||||
m_secUi->touchIDResetCheckBox->setChecked(config()->get("security/resettouchid").toBool());
|
||||
m_secUi->touchIDResetSpinBox->setValue(config()->get("security/resettouchidtimeout").toInt());
|
||||
m_secUi->touchIDResetOnScreenLockCheckBox->setChecked(config()->get("security/resettouchidscreenlock").toBool());
|
||||
|
||||
for (const ExtraPage& page : asConst(m_extraPages)) {
|
||||
page.loadSettings();
|
||||
}
|
||||
@ -241,6 +263,10 @@ void SettingsWidget::saveSettings()
|
||||
config()->set("security/passwordsrepeat", m_secUi->passwordRepeatCheckBox->isChecked());
|
||||
config()->set("security/hidenotes", m_secUi->hideNotesCheckBox->isChecked());
|
||||
|
||||
config()->set("security/resettouchid", m_secUi->touchIDResetCheckBox->isChecked());
|
||||
config()->set("security/resettouchidtimeout", m_secUi->touchIDResetSpinBox->value());
|
||||
config()->set("security/resettouchidscreenlock", m_secUi->touchIDResetOnScreenLockCheckBox->isChecked());
|
||||
|
||||
// Security: clear storage if related settings are disabled
|
||||
if (!config()->get("RememberLastDatabases").toBool()) {
|
||||
config()->set("LastDatabases", QVariant());
|
||||
|
@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>595</width>
|
||||
<height>446</height>
|
||||
<height>478</height>
|
||||
</rect>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
@ -61,7 +61,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<item row="2" column="0">
|
||||
<widget class="QCheckBox" name="lockDatabaseIdleCheckBox">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
@ -74,7 +74,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="lockDatabaseIdleSpinBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
@ -99,6 +99,35 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<widget class="QSpinBox" name="touchIDResetSpinBox">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> min</string>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>1440</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>30</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="touchIDResetCheckBox">
|
||||
<property name="text">
|
||||
<string>Forget TouchID after inactivity of</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@ -115,6 +144,13 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="touchIDResetOnScreenLockCheckBox">
|
||||
<property name="text">
|
||||
<string>Forget TouchID when session is locked or lid is closed</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="lockDatabaseMinimizeCheckBox">
|
||||
<property name="text">
|
||||
@ -122,7 +158,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="relockDatabaseAutoTypeCheckBox">
|
||||
<property name="text">
|
||||
<string>Re-lock previously locked database after performing Auto-Type</string>
|
||||
|
@ -28,6 +28,13 @@ PasswordKey::PasswordKey(const QString& password)
|
||||
setPassword(password);
|
||||
}
|
||||
|
||||
PasswordKey PasswordKey::fromRawKey(const QByteArray& rawKey)
|
||||
{
|
||||
PasswordKey result;
|
||||
result.m_key = rawKey;
|
||||
return result;
|
||||
}
|
||||
|
||||
QByteArray PasswordKey::rawKey() const
|
||||
{
|
||||
return m_key;
|
||||
|
@ -31,6 +31,7 @@ public:
|
||||
void setPassword(const QString& password);
|
||||
PasswordKey* clone() const;
|
||||
|
||||
static PasswordKey fromRawKey(const QByteArray& rawKey);
|
||||
private:
|
||||
QByteArray m_key;
|
||||
};
|
||||
|
55
src/touchid/TouchID.h
Normal file
55
src/touchid/TouchID.h
Normal file
@ -0,0 +1,55 @@
|
||||
#ifndef KEEPASSX_TOUCHID_H
|
||||
#define KEEPASSX_TOUCHID_H
|
||||
|
||||
#define TOUCHID_UNDEFINED -1
|
||||
#define TOUCHID_AVAILABLE 1
|
||||
#define TOUCHID_NOT_AVAILABLE 0
|
||||
|
||||
#include <QHash>
|
||||
#include <QString>
|
||||
#include <QByteArray>
|
||||
#include <QSharedPointer>
|
||||
|
||||
class TouchID
|
||||
{
|
||||
public:
|
||||
static TouchID& getInstance();
|
||||
|
||||
private:
|
||||
TouchID() {} // Constructor? (the {} brackets) are needed here.
|
||||
|
||||
// C++ 03
|
||||
// ========
|
||||
// Don't forget to declare these two. You want to make sure they
|
||||
// are unacceptable otherwise you may accidentally get copies of
|
||||
// your singleton appearing.
|
||||
|
||||
// TouchID(TouchID const&); // Don't Implement
|
||||
// void operator=(TouchID const&); // Don't implement
|
||||
|
||||
QHash<QString, QByteArray> m_encryptedMasterKeys;
|
||||
int m_available = TOUCHID_UNDEFINED;
|
||||
|
||||
public:
|
||||
// C++ 11
|
||||
// =======
|
||||
// We can use the better technique of deleting the methods
|
||||
// we don't want.
|
||||
|
||||
TouchID(TouchID const&) = delete;
|
||||
void operator=(TouchID const&) = delete;
|
||||
|
||||
// Note: Scott Meyers mentions in his Effective Modern
|
||||
// C++ book, that deleted functions should generally
|
||||
// be public as it results in better error messages
|
||||
// due to the compilers behavior to check accessibility
|
||||
// before deleted status
|
||||
|
||||
bool storeKey(const QString& databasePath, const QByteArray& passwordKey);
|
||||
QSharedPointer<QByteArray> getKey(const QString& databasePath) const;
|
||||
bool isAvailable();
|
||||
bool authenticate(const QString& message = "") const;
|
||||
void reset(const QString& databasePath = "");
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_TOUCHID_H
|
260
src/touchid/TouchID.mm
Normal file
260
src/touchid/TouchID.mm
Normal file
@ -0,0 +1,260 @@
|
||||
#define SECURITY_ACCOUNT_PREFIX QString("KeepassXC_TouchID_Keys_")
|
||||
|
||||
#include "touchid/TouchID.h"
|
||||
|
||||
#include "crypto/Random.h"
|
||||
#include "crypto/SymmetricCipher.h"
|
||||
#include "crypto/CryptoHash.h"
|
||||
|
||||
#include <Foundation/Foundation.h>
|
||||
#include <CoreFoundation/CoreFoundation.h>
|
||||
#include <LocalAuthentication/LocalAuthentication.h>
|
||||
#include <Security/Security.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
|
||||
inline void debug(const char* message, ...)
|
||||
{
|
||||
// qWarning(...);
|
||||
}
|
||||
|
||||
inline QString hash(const QString& value)
|
||||
{
|
||||
QByteArray result = CryptoHash::hash(value.toUtf8(), CryptoHash::Sha256).toHex();
|
||||
return QString(result);
|
||||
}
|
||||
|
||||
/* Singleton */
|
||||
TouchID& TouchID::getInstance()
|
||||
{
|
||||
static TouchID instance; // Guaranteed to be destroyed.
|
||||
// Instantiated on first use.
|
||||
return instance;
|
||||
}
|
||||
|
||||
/* Generates a random AES 256bit key and uses it to encrypt the PasswordKey that protects the database. The encrypted PasswordKey is kept in memory while the AES key is stored in the macOS KeyChain protected by TouchID. */
|
||||
bool TouchID::storeKey(const QString& databasePath, const QByteArray& passwordKey)
|
||||
{
|
||||
if (databasePath.isEmpty() || passwordKey.isEmpty()) {
|
||||
// illegal arguments
|
||||
debug("TouchID::storeKey - Illegal arguments: databasePath = %s, len(passwordKey) = %d", databasePath.toUtf8().constData(), passwordKey.length());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this->m_encryptedMasterKeys.contains(databasePath)) {
|
||||
// already stored key for this database
|
||||
debug("TouchID::storeKey - Already stored key for this database");
|
||||
return true;
|
||||
}
|
||||
|
||||
// generate random AES 256bit key and IV
|
||||
Random* random = randomGen();
|
||||
QByteArray randomKey = random->randomArray(32);
|
||||
QByteArray randomIV = random->randomArray(16);
|
||||
|
||||
bool ok;
|
||||
SymmetricCipher aes256Encrypt(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Encrypt);
|
||||
|
||||
if (!aes256Encrypt.init(randomKey, randomIV)) {
|
||||
debug("TouchID::storeKey - Error initializing encryption: %s", aes256Encrypt.errorString().toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
// encrypt and keep result in memory
|
||||
QByteArray encryptedMasterKey = aes256Encrypt.process(passwordKey, &ok);
|
||||
if (!ok) {
|
||||
debug("TouchID::storeKey - Error encrypting: %s", aes256Encrypt.errorString().toUtf8().constData());
|
||||
return false;
|
||||
}
|
||||
|
||||
// memorize which database the stored key is for
|
||||
this->m_encryptedMasterKeys.insert(databasePath, encryptedMasterKey);
|
||||
|
||||
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased
|
||||
|
||||
// try to delete an existing entry
|
||||
CFMutableDictionaryRef query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef)accountName);
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanFalse);
|
||||
|
||||
// get data from the KeyChain
|
||||
OSStatus status = SecItemDelete(query);
|
||||
|
||||
debug("TouchID::storeKey - Status deleting existing entry: %d", status);
|
||||
|
||||
// prepare adding secure entry to the macOS KeyChain
|
||||
CFErrorRef error = NULL;
|
||||
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
||||
kSecAccessControlTouchIDCurrentSet, // depr: kSecAccessControlBiometryCurrentSet,
|
||||
&error);
|
||||
|
||||
if (sacObject == NULL || error != NULL) {
|
||||
NSError* e = (__bridge NSError*) error;
|
||||
debug("TouchID::storeKey - Error creating security flags: %s", e.localizedDescription.UTF8String);
|
||||
return false;
|
||||
}
|
||||
|
||||
CFMutableDictionaryRef attributes = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
|
||||
// prepare data (key) to be stored
|
||||
QByteArray dataBytes = (randomKey + randomIV).toHex();
|
||||
|
||||
CFDataRef valueData = CFDataCreateWithBytesNoCopy(NULL, reinterpret_cast<UInt8*>(dataBytes.data()), dataBytes.length(), NULL);
|
||||
|
||||
CFDictionarySetValue(attributes, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(attributes, kSecAttrAccount, (__bridge CFStringRef)accountName);
|
||||
CFDictionarySetValue(attributes, kSecValueData, valueData);
|
||||
CFDictionarySetValue(attributes, kSecAttrSynchronizable, kCFBooleanFalse);
|
||||
CFDictionarySetValue(attributes, kSecUseAuthenticationUI, kSecUseAuthenticationUIAllow);
|
||||
CFDictionarySetValue(attributes, kSecAttrAccessControl, sacObject);
|
||||
|
||||
// add to KeyChain
|
||||
status = SecItemAdd(attributes, NULL);
|
||||
|
||||
debug("TouchID::storeKey - Status adding new entry: %d", status); // read w/ e.g. "security error -50" in shell
|
||||
|
||||
CFRelease(sacObject);
|
||||
CFRelease(attributes);
|
||||
|
||||
if (status != errSecSuccess) {
|
||||
debug("TouchID::storeKey - Not successful, resetting TouchID");
|
||||
this->m_encryptedMasterKeys.remove(databasePath);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Checks if an encrypted PasswordKey is available for the given database, tries to decrypt it using the KeyChain and if successful, returns it. */
|
||||
QSharedPointer<QByteArray> TouchID::getKey(const QString& databasePath) const
|
||||
{
|
||||
if (databasePath.isEmpty()) {
|
||||
// illegal arguments
|
||||
debug("TouchID::storeKey - Illegal argument: databasePath = %s", databasePath.toUtf8().constData());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// checks if encrypted PasswordKey is available and is stored for the given database
|
||||
if (!this->m_encryptedMasterKeys.contains(databasePath)) {
|
||||
debug("TouchID::getKey - No stored key found");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// query the KeyChain for the AES key
|
||||
CFMutableDictionaryRef query = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
|
||||
NSString* accountName = (SECURITY_ACCOUNT_PREFIX + hash(databasePath)).toNSString(); // autoreleased
|
||||
NSString* touchPromptMessage = QCoreApplication::translate("DatabaseOpenWidget", "authenticate to access the database").toNSString(); // autoreleased
|
||||
|
||||
CFDictionarySetValue(query, kSecClass, kSecClassGenericPassword);
|
||||
CFDictionarySetValue(query, kSecAttrAccount, (__bridge CFStringRef)accountName);
|
||||
CFDictionarySetValue(query, kSecReturnData, kCFBooleanTrue);
|
||||
CFDictionarySetValue(query, kSecUseOperationPrompt, (__bridge CFStringRef)touchPromptMessage);
|
||||
|
||||
// get data from the KeyChain
|
||||
CFTypeRef dataTypeRef = NULL;
|
||||
OSStatus status = SecItemCopyMatching(query, &dataTypeRef);
|
||||
CFRelease(query);
|
||||
|
||||
if (status == errSecUserCanceled) {
|
||||
// user canceled the authentication, need special return value
|
||||
debug("TouchID::getKey - User canceled authentication");
|
||||
return QSharedPointer<QByteArray>::create();
|
||||
} else if (status != errSecSuccess || dataTypeRef == NULL) {
|
||||
debug("TouchID::getKey - Error retrieving result: %d", status);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
CFDataRef valueData = static_cast<CFDataRef>(dataTypeRef);
|
||||
QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast<const char*>(CFDataGetBytePtr(valueData)), CFDataGetLength(valueData)));
|
||||
CFRelease(valueData);
|
||||
|
||||
// extract AES key and IV from data bytes
|
||||
QByteArray key = dataBytes.left(32);
|
||||
QByteArray iv = dataBytes.right(16);
|
||||
|
||||
bool ok;
|
||||
SymmetricCipher aes256Decrypt(SymmetricCipher::Aes256, SymmetricCipher::Cbc, SymmetricCipher::Decrypt);
|
||||
|
||||
if (!aes256Decrypt.init(key, iv)) {
|
||||
debug("TouchID::getKey - Error initializing decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// decrypt PasswordKey from memory using AES
|
||||
QByteArray result = aes256Decrypt.process(this->m_encryptedMasterKeys[databasePath], &ok);
|
||||
if (!ok) {
|
||||
debug("TouchID::getKey - Error decryption: %s", aes256Decrypt.errorString().toUtf8().constData());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return QSharedPointer<QByteArray>::create(result);
|
||||
}
|
||||
|
||||
/* Dynamic check if TouchID is available on the current machine. */
|
||||
bool TouchID::isAvailable()
|
||||
{
|
||||
// cache result
|
||||
if (this->m_available != TOUCHID_UNDEFINED)
|
||||
return (this->m_available == TOUCHID_AVAILABLE);
|
||||
|
||||
@try {
|
||||
LAContext* context = [[LAContext alloc] init];
|
||||
bool canAuthenticate = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:nil];
|
||||
[context release];
|
||||
this->m_available = canAuthenticate ? TOUCHID_AVAILABLE : TOUCHID_NOT_AVAILABLE;
|
||||
return canAuthenticate;
|
||||
}
|
||||
@catch(NSException*) {
|
||||
this->m_available = TOUCHID_NOT_AVAILABLE;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
typedef enum {
|
||||
kTouchIDResultNone,
|
||||
kTouchIDResultAllowed,
|
||||
kTouchIDResultFailed
|
||||
} TouchIDResult;
|
||||
|
||||
/* Performs a simple authentication using TouchID. */
|
||||
bool TouchID::authenticate(const QString& message) const
|
||||
{
|
||||
// message must not be an empty string
|
||||
QString msg = message;
|
||||
if (message.length() == 0)
|
||||
msg = QCoreApplication::translate("DatabaseOpenWidget", "authenticate a privileged operation");
|
||||
|
||||
@try {
|
||||
LAContext* context = [[LAContext alloc] init];
|
||||
__block TouchIDResult result = kTouchIDResultNone;
|
||||
NSString* authMessage = msg.toNSString(); // autoreleased
|
||||
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:authMessage reply:^(BOOL success, NSError* error) {
|
||||
result = success ? kTouchIDResultAllowed : kTouchIDResultFailed;
|
||||
CFRunLoopWakeUp(CFRunLoopGetCurrent());
|
||||
}];
|
||||
|
||||
while (result == kTouchIDResultNone)
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
|
||||
|
||||
[context release];
|
||||
return result == kTouchIDResultAllowed;
|
||||
}
|
||||
@catch(NSException*) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resets the inner state either for all or for the given database */
|
||||
void TouchID::reset(const QString& databasePath)
|
||||
{
|
||||
if (databasePath.isEmpty()) {
|
||||
this->m_encryptedMasterKeys.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this->m_encryptedMasterKeys.remove(databasePath);
|
||||
}
|
Loading…
Reference in New Issue
Block a user