Files
keepassxc/src/gui/DatabaseOpenWidget.cpp
Janek Bevendorff 5492b5c4f6 Redesign database unlock widget. (#3287)
With this change we get rid of the confusing key component checkboxes.
Now a component is either there or not (if left empty). There is
no redundant distinction between "unset" and "emtpy" anymore.
For compatibility with older databases that have "empty" passwords,
KeePassXC will ask if the user wants to retry with an empty password
if unlocking failed and the password field was left blank.

Besides these functional changes, the widget's layout has been
rearranged to be more compact, less stretched out (e.g. input fields
do not fill the full window width anymore), and more user-friendly
by providing a help tooltip for the hardware key field and accessible
descriptions for screen readers.
2019-06-22 18:00:31 +02:00

436 lines
15 KiB
C++

/*
* Copyright (C) 2011 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* 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 "DatabaseOpenWidget.h"
#include "ui_DatabaseOpenWidget.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/FilePath.h"
#include "crypto/Random.h"
#include "format/KeePass2Reader.h"
#include "gui/FileDialog.h"
#include "gui/MainWindow.h"
#include "gui/MessageBox.h"
#include "keys/FileKey.h"
#include "keys/PasswordKey.h"
#include "keys/YkChallengeResponseKey.h"
#include "touchid/TouchID.h"
#include "config-keepassx.h"
#include <QSharedPointer>
#include <QtConcurrentRun>
#include <QDesktopServices>
#include <QFont>
DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent)
: DialogyWidget(parent)
, m_ui(new Ui::DatabaseOpenWidget())
, m_db(nullptr)
{
m_ui->setupUi(this);
m_ui->messageWidget->setHidden(true);
QFont font;
font.setPointSize(font.pointSize() + 4);
font.setBold(true);
m_ui->labelHeadline->setFont(font);
m_ui->labelHeadline->setText(tr("Unlock KeePassXC Database"));
m_ui->comboKeyFile->lineEdit()->addAction(m_ui->keyFileClearIcon, QLineEdit::TrailingPosition);
m_ui->buttonTogglePassword->setIcon(filePath()->onOffIcon("actions", "password-show"));
connect(m_ui->buttonTogglePassword, SIGNAL(toggled(bool)), m_ui->editPassword, SLOT(setShowPassword(bool)));
connect(m_ui->buttonBrowseFile, SIGNAL(clicked()), SLOT(browseKeyFile()));
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(openDatabase()));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(reject()));
m_ui->hardwareKeyLabelHelp->setIcon(filePath()->icon("actions", "system-help").pixmap(QSize(12, 12)));
connect(m_ui->hardwareKeyLabelHelp, SIGNAL(clicked(bool)), SLOT(openHardwareKeyHelp()));
connect(m_ui->comboKeyFile->lineEdit(), SIGNAL(textChanged(QString)), SLOT(handleKeyFileComboEdited()));
connect(m_ui->comboKeyFile, SIGNAL(currentIndexChanged(int)), SLOT(handleKeyFileComboChanged()));
m_ui->keyFileClearIcon->setIcon(filePath()->icon("actions", "edit-clear-locationbar-rtl"));
m_ui->keyFileClearIcon->setVisible(false);
connect(m_ui->keyFileClearIcon, SIGNAL(triggered(bool)), SLOT(clearKeyFileEdit()));
#ifdef WITH_XC_YUBIKEY
m_ui->yubikeyProgress->setVisible(false);
QSizePolicy sp = m_ui->yubikeyProgress->sizePolicy();
sp.setRetainSizeWhenHidden(true);
m_ui->yubikeyProgress->setSizePolicy(sp);
connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollYubikey()));
#else
m_ui->checkChallengeResponse->setVisible(false);
m_ui->buttonRedetectYubikey->setVisible(false);
m_ui->comboChallengeResponse->setVisible(false);
m_ui->yubikeyProgress->setVisible(false);
#endif
#ifdef Q_OS_MACOS
// add random padding to layouts to align widgets properly
m_ui->dialogButtonsLayout->setContentsMargins(10, 0, 15, 0);
m_ui->gridLayout->setContentsMargins(10, 0, 0, 0);
#endif
#ifndef WITH_XC_TOUCHID
m_ui->touchIDContainer->setVisible(false);
#else
if (!TouchID::getInstance().isAvailable()) {
m_ui->checkTouchID->setVisible(false);
}
#endif
}
DatabaseOpenWidget::~DatabaseOpenWidget()
{
}
void DatabaseOpenWidget::showEvent(QShowEvent* event)
{
DialogyWidget::showEvent(event);
m_ui->editPassword->setFocus();
#ifdef WITH_XC_YUBIKEY
// showEvent() may be called twice, so make sure we are only polling once
if (!m_yubiKeyBeingPolled) {
// clang-format off
connect(YubiKey::instance(), SIGNAL(detected(int,bool)), SLOT(yubikeyDetected(int,bool)), Qt::QueuedConnection);
connect(YubiKey::instance(), SIGNAL(detectComplete()), SLOT(yubikeyDetectComplete()), Qt::QueuedConnection);
connect(YubiKey::instance(), SIGNAL(notFound()), SLOT(noYubikeyFound()), Qt::QueuedConnection);
// clang-format on
pollYubikey();
m_yubiKeyBeingPolled = true;
}
#endif
}
void DatabaseOpenWidget::hideEvent(QHideEvent* event)
{
DialogyWidget::hideEvent(event);
#ifdef WITH_XC_YUBIKEY
// Don't listen to any Yubikey events if we are hidden
disconnect(YubiKey::instance(), nullptr, this, nullptr);
m_yubiKeyBeingPolled = false;
#endif
if (isVisible()) {
return;
}
clearForms();
}
void DatabaseOpenWidget::load(const QString& filename)
{
m_filename = filename;
m_ui->fileNameLabel->setRawText(m_filename);
m_ui->comboKeyFile->addItem(tr("Select file..."), -1);
m_ui->comboKeyFile->setCurrentIndex(0);
m_ui->keyFileClearIcon->setVisible(false);
m_keyFileComboEdited = false;
if (config()->get("RememberLastKeyFiles").toBool()) {
QHash<QString, QVariant> lastKeyFiles = config()->get("LastKeyFiles").toHash();
if (lastKeyFiles.contains(m_filename)) {
m_ui->comboKeyFile->addItem(lastKeyFiles[m_filename].toString());
m_ui->comboKeyFile->setCurrentIndex(1);
}
}
QHash<QString, QVariant> useTouchID = config()->get("UseTouchID").toHash();
m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool());
m_ui->editPassword->setFocus();
}
void DatabaseOpenWidget::clearForms()
{
m_ui->editPassword->setText("");
m_ui->comboKeyFile->clear();
m_ui->comboKeyFile->setEditText("");
m_ui->checkTouchID->setChecked(false);
m_ui->buttonTogglePassword->setChecked(false);
m_db.reset();
}
QSharedPointer<Database> DatabaseOpenWidget::database()
{
return m_db;
}
void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile)
{
m_ui->editPassword->setText(pw);
m_ui->comboKeyFile->setCurrentIndex(-1);
m_ui->comboKeyFile->setEditText(keyFile);
openDatabase();
}
void DatabaseOpenWidget::openDatabase()
{
QSharedPointer<CompositeKey> masterKey = databaseKey();
if (!masterKey) {
return;
}
m_ui->editPassword->setShowPassword(false);
QCoreApplication::processEvents();
m_db.reset(new Database());
QString error;
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
bool ok = m_db->open(m_filename, masterKey, &error, false);
QApplication::restoreOverrideCursor();
if (!ok) {
if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) {
QScopedPointer<QMessageBox> msgBox(new QMessageBox(this));
msgBox->setIcon(QMessageBox::Critical);
msgBox->setWindowTitle(tr("Unlock failed and no password given"));
msgBox->setText(tr("Unlocking the database failed and you did not enter a password.\n"
"Do you want to retry with an \"empty\" password instead?\n\n"
"To prevent this error from appearing, you must go to "
"\"Database Settings / Security\" and reset your password."));
auto btn = msgBox->addButton(tr("Retry with empty password"), QMessageBox::ButtonRole::AcceptRole);
msgBox->setDefaultButton(btn);
msgBox->addButton(QMessageBox::Cancel);
msgBox->exec();
if (msgBox->clickedButton() == btn) {
m_retryUnlockWithEmptyPassword = true;
openDatabase();
return;
}
}
m_retryUnlockWithEmptyPassword = false;
m_ui->messageWidget->showMessage(error, MessageWidget::MessageType::Error);
return;
}
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();
}
emit dialogFinished(true);
} else {
m_ui->messageWidget->showMessage(error, MessageWidget::Error);
m_ui->editPassword->setText("");
#ifdef WITH_XC_TOUCHID
// unable to unlock database, reset TouchID for the current database
TouchID::getInstance().reset(m_filename);
#endif
}
}
QSharedPointer<CompositeKey> DatabaseOpenWidget::databaseKey()
{
auto masterKey = QSharedPointer<CompositeKey>::create();
if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) {
masterKey->addKey(QSharedPointer<PasswordKey>::create(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()
&& m_ui->editPassword->text().isEmpty()) {
// clear empty password from composite key
masterKey->clear();
// 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();
lastKeyFiles.remove(m_filename);
auto key = QSharedPointer<FileKey>::create();
QString keyFilename = m_ui->comboKeyFile->currentText();
if (!m_ui->comboKeyFile->currentText().isEmpty() && m_keyFileComboEdited) {
QString errorMsg;
if (!key->load(keyFilename, &errorMsg)) {
m_ui->messageWidget->showMessage(tr("Failed to open key file: %1").arg(errorMsg), MessageWidget::Error);
return {};
}
if (key->type() != FileKey::Hashed && !config()->get("Messages/NoLegacyKeyFileWarning").toBool()) {
QMessageBox legacyWarning;
legacyWarning.setWindowTitle(tr("Legacy key file format"));
legacyWarning.setText(tr("You are using a legacy key file format which may become\n"
"unsupported in the future.\n\n"
"Please consider generating a new key file."));
legacyWarning.setIcon(QMessageBox::Icon::Warning);
legacyWarning.addButton(QMessageBox::Ok);
legacyWarning.setDefaultButton(QMessageBox::Ok);
legacyWarning.setCheckBox(new QCheckBox(tr("Don't show this warning again")));
connect(legacyWarning.checkBox(), &QCheckBox::stateChanged, [](int state)
{
config()->set("Messages/NoLegacyKeyFileWarning", state == Qt::CheckState::Checked);
});
legacyWarning.exec();
}
masterKey->addKey(key);
lastKeyFiles[m_filename] = keyFilename;
}
if (config()->get("RememberLastKeyFiles").toBool()) {
config()->set("LastKeyFiles", lastKeyFiles);
}
#ifdef WITH_XC_YUBIKEY
QHash<QString, QVariant> lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
lastChallengeResponse.remove(m_filename);
int selectionIndex = m_ui->comboChallengeResponse->currentIndex();
if (selectionIndex > 0) {
int comboPayload = m_ui->comboChallengeResponse->itemData(selectionIndex).toInt();
// read blocking mode from LSB and slot index number from second LSB
bool blocking = comboPayload & 1;
int slot = comboPayload >> 1;
auto crKey = QSharedPointer<YkChallengeResponseKey>(new YkChallengeResponseKey(slot, blocking));
masterKey->addChallengeResponseKey(crKey);
lastChallengeResponse[m_filename] = true;
}
if (config()->get("RememberLastKeyFiles").toBool()) {
config()->set("LastChallengeResponse", lastChallengeResponse);
}
#endif
return masterKey;
}
void DatabaseOpenWidget::reject()
{
emit dialogFinished(false);
}
void DatabaseOpenWidget::browseKeyFile()
{
QString filters = QString("%1 (*);;%2 (*.key)").arg(tr("All files"), tr("Key files"));
if (!config()->get("RememberLastKeyFiles").toBool()) {
fileDialog()->setNextForgetDialog();
}
QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters);
if (!filename.isEmpty()) {
m_ui->comboKeyFile->setCurrentIndex(-1);
m_ui->comboKeyFile->setEditText(filename);
}
}
void DatabaseOpenWidget::clearKeyFileEdit()
{
m_ui->comboKeyFile->setCurrentIndex(0);
// make sure that handler is called even if 0 was the current index already
handleKeyFileComboChanged();
}
void DatabaseOpenWidget::handleKeyFileComboEdited()
{
m_keyFileComboEdited = true;
m_ui->keyFileClearIcon->setVisible(true);
}
void DatabaseOpenWidget::handleKeyFileComboChanged()
{
m_keyFileComboEdited = m_ui->comboKeyFile->currentIndex() != 0;
m_ui->keyFileClearIcon->setVisible(m_keyFileComboEdited);
}
void DatabaseOpenWidget::pollYubikey()
{
m_ui->buttonRedetectYubikey->setEnabled(false);
m_ui->comboChallengeResponse->setEnabled(false);
m_ui->comboChallengeResponse->clear();
m_ui->comboChallengeResponse->addItem(tr("Select slot..."), -1);
m_ui->yubikeyProgress->setVisible(true);
// YubiKey init is slow, detect asynchronously to not block the UI
QtConcurrent::run(YubiKey::instance(), &YubiKey::detect);
}
void DatabaseOpenWidget::yubikeyDetected(int slot, bool blocking)
{
YkChallengeResponseKey yk(slot, blocking);
// add detected YubiKey to combo box and encode blocking mode in LSB, slot number in second LSB
m_ui->comboChallengeResponse->addItem(yk.getName(), QVariant((slot << 1) | blocking));
if (config()->get("RememberLastKeyFiles").toBool()) {
QHash<QString, QVariant> lastChallengeResponse = config()->get("LastChallengeResponse").toHash();
if (lastChallengeResponse.contains(m_filename)) {
m_ui->comboChallengeResponse->setCurrentIndex(1);
}
}
}
void DatabaseOpenWidget::yubikeyDetectComplete()
{
m_ui->comboChallengeResponse->setEnabled(true);
m_ui->buttonRedetectYubikey->setEnabled(true);
m_ui->yubikeyProgress->setVisible(false);
m_yubiKeyBeingPolled = false;
}
void DatabaseOpenWidget::noYubikeyFound()
{
m_ui->buttonRedetectYubikey->setEnabled(true);
m_ui->yubikeyProgress->setVisible(false);
m_yubiKeyBeingPolled = false;
}
void DatabaseOpenWidget::openHardwareKeyHelp()
{
QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs#hwtoken"));
}