Add Pin Quick Unlock option

* Introduce QuickUnlockManager to fall back to pin unlock if OS native options are not available.
This commit is contained in:
Jonathan White 2023-11-22 22:44:11 -05:00
parent 8b29e4afdc
commit e6e4fefb82
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
8 changed files with 290 additions and 68 deletions

View File

@ -205,6 +205,7 @@ set(gui_SOURCES
gui/wizard/NewDatabaseWizardPageEncryption.cpp
gui/wizard/NewDatabaseWizardPageDatabaseKey.cpp
quickunlock/QuickUnlockInterface.cpp
quickunlock/PinUnlock.cpp
../share/icons/icons.qrc
../share/wizard/wizard.qrc)

View File

@ -374,13 +374,16 @@ QSharedPointer<CompositeKey> DatabaseOpenWidget::buildDatabaseKey()
{
auto databaseKey = QSharedPointer<CompositeKey>::create();
if (!m_db.isNull() && canPerformQuickUnlock()) {
// try to retrieve the stored password using Windows Hello
if (!m_db.isNull() && canPerformQuickUnlock(m_db->publicUuid())) {
// try to retrieve the stored password using quick unlock
QByteArray keyData;
if (!getQuickUnlock()->getKey(m_db->publicUuid(), keyData)) {
m_ui->messageWidget->showMessage(
tr("Failed to authenticate with Quick Unlock: %1").arg(getQuickUnlock()->errorString()),
MessageWidget::Error);
if (!getQuickUnlock()->hasKey(m_db->publicUuid())) {
resetQuickUnlock();
}
return {};
}
databaseKey->setRawKey(keyData);

View File

@ -441,6 +441,32 @@
<property name="bottomMargin">
<number>5</number>
</property>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="enableQuickUnlockCheckBox">
<property name="layoutDirection">
<enum>Qt::RightToLeft</enum>
</property>
<property name="text">
<string>Enable Quick Unlock</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item alignment="Qt::AlignRight">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="focusPolicy">

View File

@ -0,0 +1,171 @@
/*
* 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 "PinUnlock.h"
#include "crypto/CryptoHash.h"
#include "crypto/Random.h"
#include "crypto/SymmetricCipher.h"
#include <QInputDialog>
#include <QRegularExpression>
#define MIN_PIN_LENGTH 4
#define MAX_PIN_LENGTH 8
#define MAX_PIN_ATTEMPTS 3
bool PinUnlock::isAvailable() const
{
return true;
}
QString PinUnlock::errorString() const
{
return m_error;
}
bool PinUnlock::setKey(const QUuid& dbUuid, const QByteArray& data)
{
QString pin;
QRegularExpression pinRegex("^\\d+$");
while (true) {
bool ok = false;
pin = QInputDialog::getText(
nullptr,
QObject::tr("Quick Unlock Pin Entry"),
QObject::tr("Enter a %1 to %2 digit pin to use for quick unlock:").arg(MIN_PIN_LENGTH).arg(MAX_PIN_LENGTH),
QLineEdit::Password,
{},
&ok);
if (!ok) {
m_error = QObject::tr("Pin setup was canceled. Quick unlock has not been enabled.");
return false;
}
// Validate pin criteria
if (pin.length() >= MIN_PIN_LENGTH && pin.length() <= MAX_PIN_LENGTH && pinRegex.match(pin).hasMatch()) {
break;
}
}
// Hash the pin and use it as the key for the encryption
CryptoHash hash(CryptoHash::Sha256);
hash.addData(pin.toLatin1());
auto key = hash.result();
// Generate a random IV
auto iv = Random::instance()->randomArray(SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM));
// Encrypt the data using AES-256-CBC
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Encrypt, key, iv)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
QByteArray encrypted = data;
if (!cipher.finish(encrypted)) {
m_error = QObject::tr("Failed to encrypt key data.");
return false;
}
// Prepend the IV to the encrypted data
encrypted.prepend(iv);
// Store the encrypted data and pin attempts
m_encryptedKeys.insert(dbUuid, qMakePair(1, encrypted));
return true;
}
bool PinUnlock::getKey(const QUuid& dbUuid, QByteArray& data)
{
data.clear();
if (!hasKey(dbUuid)) {
m_error = QObject::tr("Failed to get credentials for quick unlock.");
return false;
}
const auto& pairData = m_encryptedKeys.value(dbUuid);
// Restrict pin attempts per database
for (int pinAttempts = pairData.first; pinAttempts <= MAX_PIN_ATTEMPTS; ++pinAttempts) {
bool ok = false;
auto pin = QInputDialog::getText(
nullptr,
QObject::tr("Quick Unlock Pin Entry"),
QObject::tr("Enter quick unlock pin (%1 of %2 attempts):").arg(pinAttempts).arg(MAX_PIN_ATTEMPTS),
QLineEdit::Password,
{},
&ok);
if (!ok) {
m_error = QObject::tr("Pin entry was canceled.");
return false;
}
// Hash the pin and use it as the key for the encryption
CryptoHash hash(CryptoHash::Sha256);
hash.addData(pin.toLatin1());
auto key = hash.result();
// Read the previously used challenge and encrypted data
auto ivSize = SymmetricCipher::defaultIvSize(SymmetricCipher::Aes256_GCM);
const auto& keydata = pairData.second;
auto challenge = keydata.left(ivSize);
auto encrypted = keydata.mid(ivSize);
// Decrypt the data using the generated key and IV from above
SymmetricCipher cipher;
if (!cipher.init(SymmetricCipher::Aes256_GCM, SymmetricCipher::Decrypt, key, challenge)) {
m_error = QObject::tr("Failed to init KeePassXC crypto.");
return false;
}
// Store the decrypted data into the passed parameter
data = encrypted;
if (cipher.finish(data)) {
// Reset the pin attempts
m_encryptedKeys.insert(dbUuid, qMakePair(1, keydata));
return true;
}
}
data.clear();
m_error = QObject::tr("Maximum pin attempts have been reached.");
reset(dbUuid);
return false;
}
bool PinUnlock::hasKey(const QUuid& dbUuid) const
{
return m_encryptedKeys.contains(dbUuid);
}
bool PinUnlock::canRemember() const
{
return false;
}
void PinUnlock::reset(const QUuid& dbUuid)
{
m_encryptedKeys.remove(dbUuid);
}
void PinUnlock::reset()
{
m_encryptedKeys.clear();
}

View File

@ -0,0 +1,49 @@
/*
* 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_PINUNLOCK_H
#define KEEPASSXC_PINUNLOCK_H
#include "QuickUnlockInterface.h"
#include <QHash>
class PinUnlock : public QuickUnlockInterface
{
public:
PinUnlock() = default;
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;
bool canRemember() const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
private:
QString m_error;
QHash<QUuid, QPair<int, QByteArray>> m_encryptedKeys;
Q_DISABLE_COPY(PinUnlock)
};
#endif // KEEPASSXC_PINUNLOCK_H

View File

@ -16,71 +16,46 @@
*/
#include "QuickUnlockInterface.h"
#include "PinUnlock.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};
QuickUnlockManager* quickUnlockManager = nullptr;
QuickUnlockInterface* getQuickUnlock()
QuickUnlockManager::QuickUnlockManager()
{
if (!quickUnlockInstance) {
quickUnlockInstance = new QUICKUNLOCK_IMPLEMENTATION();
}
return quickUnlockInstance;
#if defined(Q_OS_MACOS)
m_interfaces.append(new TouchId());
#elif defined(Q_CC_MSVC)
m_interfaces.append(new WindowsHello());
#elif defined(Q_OS_LINUX)
m_interfaces.append(new Polkit());
#endif
m_interfaces.append(new PinUnlock());
}
bool NoQuickUnlock::isAvailable() const
const QuickUnlockManager* QuickUnlockManager::get()
{
return false;
if (!quickUnlockManager) {
quickUnlockManager = new QuickUnlockManager();
}
return quickUnlockManager;
}
QString NoQuickUnlock::errorString() const
QuickUnlockInterface* QuickUnlockManager::getQuickUnlock()
{
return QObject::tr("No Quick Unlock provider is available");
for (auto* interface : m_interfaces) {
if (interface->isAvailable()) {
return interface;
}
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;
}
bool NoQuickUnlock::canRemember() const
{
return false;
}
void NoQuickUnlock::reset(const QUuid& dbUuid)
{
Q_UNUSED(dbUuid)
return nullptr;
}

View File

@ -18,6 +18,7 @@
#ifndef KEEPASSXC_QUICKUNLOCKINTERFACE_H
#define KEEPASSXC_QUICKUNLOCKINTERFACE_H
#include <QList>
#include <QUuid>
class QuickUnlockInterface
@ -41,22 +42,20 @@ public:
virtual void reset() = 0;
};
class NoQuickUnlock : public QuickUnlockInterface
class QuickUnlockManager final
{
Q_DISABLE_COPY(QuickUnlockManager)
public:
bool isAvailable() const override;
QString errorString() const override;
QuickUnlockManager();
~QuickUnlockManager();
bool setKey(const QUuid& dbUuid, const QByteArray& key) override;
bool getKey(const QUuid& dbUuid, QByteArray& key) override;
bool hasKey(const QUuid& dbUuid) const override;
bool canRemember() const override;
void reset(const QUuid& dbUuid) override;
void reset() override;
};
static const QuickUnlockManager* get();
QuickUnlockInterface* getQuickUnlock();
private:
QList<QuickUnlockInterface*> m_interfaces;
};
#endif // KEEPASSXC_QUICKUNLOCKINTERFACE_H

View File

@ -20,9 +20,6 @@
#include "QuickUnlockInterface.h"
#include <QHash>
#include <QObject>
class WindowsHello : public QuickUnlockInterface
{
public:
@ -42,7 +39,8 @@ public:
private:
QString m_error;
Q_DISABLE_COPY(WindowsHello);
Q_DISABLE_COPY(WindowsHello)
};
#endif // KEEPASSXC_WINDOWSHELLO_H