diff --git a/CMakeLists.txt b/CMakeLists.txt
index dfba167889..9922e99016 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -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
diff --git a/share/translations/keepassx_en_US.ts b/share/translations/keepassx_en_US.ts
index 97d1014f08..a7a9627e3f 100644
--- a/share/translations/keepassx_en_US.ts
+++ b/share/translations/keepassx_en_US.ts
@@ -768,6 +768,14 @@ Please consider generating a new key file.
Select key file
Select key file
+
+ authenticate to access the database
+ authenticate to access the database
+
+
+ authenticate a privileged operation
+ authenticate a privileged operation
+
DatabaseRepairWidget
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 012fc29dbd..fa1bc7b7e1 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -199,6 +199,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)
@@ -240,6 +243,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)
@@ -267,6 +274,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)
@@ -298,6 +309,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}")
diff --git a/src/config-keepassx.h.cmake b/src/config-keepassx.h.cmake
index 8bbea9eaee..d1f0723b42 100644
--- a/src/config-keepassx.h.cmake
+++ b/src/config-keepassx.h.cmake
@@ -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
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
index c57361b28d..d05662f4e7 100644
--- a/src/core/Config.cpp
+++ b/src/core/Config.cpp
@@ -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/ShowTrayIcon", false);
m_defaults.insert("GUI/DarkTrayIcon", false);
diff --git a/src/gui/AboutDialog.cpp b/src/gui/AboutDialog.cpp
index 5a082851fb..c7c75a11eb 100644
--- a/src/gui/AboutDialog.cpp
+++ b/src/gui/AboutDialog.cpp
@@ -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");
diff --git a/src/gui/DatabaseOpenWidget.cpp b/src/gui/DatabaseOpenWidget.cpp
index 78fa08587b..3553c07315 100644
--- a/src/gui/DatabaseOpenWidget.cpp
+++ b/src/gui/DatabaseOpenWidget.cpp
@@ -30,6 +30,7 @@
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#include "keys/YkChallengeResponseKey.h"
+#include "touchid/TouchID.h"
#include "config-keepassx.h"
@@ -81,6 +82,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()
@@ -131,6 +140,9 @@ void DatabaseOpenWidget::load(const QString& filename)
}
}
+ QHash useTouchID = config()->get("UseTouchID").toHash();
+ m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
+
m_ui->editPassword->setFocus();
}
@@ -141,6 +153,7 @@ void DatabaseOpenWidget::clearForms()
m_ui->checkPassword->setChecked(false);
m_ui->checkKeyFile->setChecked(false);
m_ui->checkChallengeResponse->setChecked(false);
+ m_ui->checkTouchID->setChecked(false);
m_ui->buttonTogglePassword->setChecked(false);
m_db = nullptr;
}
@@ -187,6 +200,24 @@ void DatabaseOpenWidget::openDatabase()
QApplication::restoreOverrideCursor();
if (m_db) {
+#ifdef WITH_XC_TOUCHID
+ QHash 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();
}
@@ -195,6 +226,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
}
}
@@ -206,6 +242,21 @@ QSharedPointer 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 passwordKey = TouchID::getInstance().getKey(m_filename);
+ if (passwordKey != NULL) {
+ // check if the user cancelled the operation
+ if (passwordKey.isNull())
+ return QSharedPointer();
+
+ masterKey->addKey(PasswordKey::fromRawKey(*passwordKey));
+ }
+ }
+#endif
+
QHash lastKeyFiles = config()->get("LastKeyFiles").toHash();
QHash lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
diff --git a/src/gui/DatabaseOpenWidget.ui b/src/gui/DatabaseOpenWidget.ui
index dba71d0fac..f0e9057ec4 100644
--- a/src/gui/DatabaseOpenWidget.ui
+++ b/src/gui/DatabaseOpenWidget.ui
@@ -10,7 +10,7 @@
302
-
+
8
@@ -225,6 +225,23 @@
+ -
+
+
+ 5
+
+
+ 8
+
+
-
+
+
+ Use TouchID for quick unlock
+
+
+
+
+
-
diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp
index f1eb1793b7..c45c08747a 100644
--- a/src/gui/DatabaseWidget.cpp
+++ b/src/gui/DatabaseWidget.cpp
@@ -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"
@@ -828,6 +829,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) {
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp
index 9859e0fdc5..b6e5d5db4d 100644
--- a/src/gui/MainWindow.cpp
+++ b/src/gui/MainWindow.cpp
@@ -59,6 +59,8 @@
#include "gui/PasswordGeneratorWidget.h"
#include "gui/SettingsWidget.h"
+#include "touchid/TouchID.h"
+
#ifdef WITH_XC_BROWSER
class BrowserPlugin : public ISettingsPage
{
@@ -183,6 +185,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);
@@ -841,6 +847,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
+
updateTrayIcon();
}
@@ -905,6 +926,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"));
@@ -1001,6 +1029,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& urls)
diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h
index 08e264e768..7e74298ba7 100644
--- a/src/gui/MainWindow.h
+++ b/src/gui/MainWindow.h
@@ -104,6 +104,7 @@ private slots:
void hideWindow();
void toggleWindow();
void lockDatabasesAfterInactivity();
+ void forgetTouchIDAfterInactivity();
void repairDatabase();
void hideTabMessage();
void handleScreenLock();
@@ -131,6 +132,7 @@ private slots:
QActionGroup* m_copyAdditionalAttributeActions;
QStringList m_openDatabases;
InactivityTimer* m_inactivityTimer;
+ InactivityTimer* m_touchIDinactivityTimer;
int m_countDefaultAttributes;
QSystemTrayIcon* m_trayIcon;
ScreenLockListener* m_screenLockListener;
diff --git a/src/gui/SettingsWidget.cpp b/src/gui/SettingsWidget.cpp
index 751b032867..9ba2521806 100644
--- a/src/gui/SettingsWidget.cpp
+++ b/src/gui/SettingsWidget.cpp
@@ -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()
@@ -174,6 +192,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();
}
@@ -239,6 +261,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());
diff --git a/src/gui/SettingsWidgetSecurity.ui b/src/gui/SettingsWidgetSecurity.ui
index da3def8689..05929e5953 100644
--- a/src/gui/SettingsWidgetSecurity.ui
+++ b/src/gui/SettingsWidgetSecurity.ui
@@ -7,7 +7,7 @@
0
0
595
- 446
+ 478
@@ -61,7 +61,7 @@
- -
+
-
@@ -74,7 +74,7 @@
- -
+
-
false
@@ -99,6 +99,35 @@
+ -
+
+
+ false
+
+
+
+ 0
+ 0
+
+
+
+ min
+
+
+ 1440
+
+
+ 30
+
+
+
+ -
+
+
+ Forget TouchID after inactivity of
+
+
+
@@ -115,6 +144,13 @@
+ -
+
+
+ Forget TouchID when session is locked or lid is closed
+
+
+
-
@@ -122,7 +158,7 @@
- -
+
-
Re-lock previously locked database after performing Auto-Type
diff --git a/src/keys/PasswordKey.cpp b/src/keys/PasswordKey.cpp
index ce216c2169..ea14404808 100644
--- a/src/keys/PasswordKey.cpp
+++ b/src/keys/PasswordKey.cpp
@@ -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;
diff --git a/src/keys/PasswordKey.h b/src/keys/PasswordKey.h
index 8c40f7735c..821510703f 100644
--- a/src/keys/PasswordKey.h
+++ b/src/keys/PasswordKey.h
@@ -31,6 +31,7 @@ class PasswordKey : public Key
void setPassword(const QString& password);
PasswordKey* clone() const;
+ static PasswordKey fromRawKey(const QByteArray& rawKey);
private:
QByteArray m_key;
};
diff --git a/src/touchid/TouchID.h b/src/touchid/TouchID.h
new file mode 100644
index 0000000000..cae108b645
--- /dev/null
+++ b/src/touchid/TouchID.h
@@ -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
+#include
+#include
+#include
+
+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 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 getKey(const QString& databasePath) const;
+ bool isAvailable();
+ bool authenticate(const QString& message = "") const;
+ void reset(const QString& databasePath = "");
+};
+
+#endif // KEEPASSX_TOUCHID_H
diff --git a/src/touchid/TouchID.mm b/src/touchid/TouchID.mm
new file mode 100644
index 0000000000..45f2de0f75
--- /dev/null
+++ b/src/touchid/TouchID.mm
@@ -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
+#include
+#include
+#include
+
+#include
+
+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(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 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::create();
+ } else if (status != errSecSuccess || dataTypeRef == NULL) {
+ debug("TouchID::getKey - Error retrieving result: %d", status);
+ return NULL;
+ }
+
+ CFDataRef valueData = static_cast(dataTypeRef);
+ QByteArray dataBytes = QByteArray::fromHex(QByteArray(reinterpret_cast(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::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);
+}