/* * Copyright (C) 2011 Felix Geyer * Copyright (C) 2017 KeePassXC Team * * 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 . */ #include "DatabaseOpenWidget.h" #include "ui_DatabaseOpenWidget.h" #include "config-keepassx.h" #include "gui/FileDialog.h" #include "gui/Icons.h" #include "gui/MainWindow.h" #include "gui/MessageBox.h" #include "keys/ChallengeResponseKey.h" #include "keys/FileKey.h" #ifdef Q_OS_MACOS #include "touchid/TouchID.h" #endif #include #include namespace { constexpr int clearFormsDelay = 30000; } DatabaseOpenWidget::DatabaseOpenWidget(QWidget* parent) : DialogyWidget(parent) , m_ui(new Ui::DatabaseOpenWidget()) , m_db(nullptr) { m_ui->setupUi(this); m_ui->messageWidget->setHidden(true); m_hideTimer.setInterval(clearFormsDelay); m_hideTimer.setSingleShot(true); connect(&m_hideTimer, &QTimer::timeout, this, [this] { // Reset the password field after being hidden for a set time m_ui->editPassword->setText(""); m_ui->editPassword->setShowPassword(false); }); QFont font; font.setPointSize(font.pointSize() + 4); font.setBold(true); m_ui->labelHeadline->setFont(font); m_ui->labelHeadline->setText(tr("Unlock KeePassXC Database")); 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(icons()->icon("system-help").pixmap(QSize(12, 12))); connect(m_ui->hardwareKeyLabelHelp, SIGNAL(clicked(bool)), SLOT(openHardwareKeyHelp())); m_ui->keyFileLabelHelp->setIcon(icons()->icon("system-help").pixmap(QSize(12, 12))); connect(m_ui->keyFileLabelHelp, SIGNAL(clicked(bool)), SLOT(openKeyFileHelp())); #ifdef WITH_XC_YUBIKEY m_ui->hardwareKeyProgress->setVisible(false); QSizePolicy sp = m_ui->hardwareKeyProgress->sizePolicy(); sp.setRetainSizeWhenHidden(true); m_ui->hardwareKeyProgress->setSizePolicy(sp); connect(m_ui->buttonRedetectYubikey, SIGNAL(clicked()), SLOT(pollHardwareKey())); connect(YubiKey::instance(), SIGNAL(detectComplete(bool)), SLOT(hardwareKeyResponse(bool)), Qt::QueuedConnection); connect(YubiKey::instance(), &YubiKey::userInteractionRequest, this, [this] { // Show the press notification if we are in an independent window (e.g., DatabaseOpenDialog) if (window() != getMainWindow()) { m_ui->messageWidget->showMessage(tr("Please present or touch your YubiKey to continue…"), MessageWidget::Information, MessageWidget::DisableAutoHide); } }); connect(YubiKey::instance(), &YubiKey::challengeCompleted, this, [this] { m_ui->messageWidget->hide(); }); #else m_ui->hardwareKeyLabel->setVisible(false); m_ui->hardwareKeyLabelHelp->setVisible(false); m_ui->buttonRedetectYubikey->setVisible(false); m_ui->challengeResponseCombo->setVisible(false); m_ui->hardwareKeyProgress->setVisible(false); #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(); m_hideTimer.stop(); } void DatabaseOpenWidget::hideEvent(QHideEvent* event) { DialogyWidget::hideEvent(event); // Schedule form clearing if we are hidden if (!isVisible()) { m_hideTimer.start(); } } void DatabaseOpenWidget::load(const QString& filename) { clearForms(); m_filename = filename; m_ui->fileNameLabel->setRawText(m_filename); if (config()->get(Config::RememberLastKeyFiles).toBool()) { auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash(); if (lastKeyFiles.contains(m_filename)) { m_ui->keyFileLineEdit->setText(lastKeyFiles[m_filename].toString()); } } QHash useTouchID = config()->get(Config::UseTouchID).toHash(); m_ui->checkTouchID->setChecked(useTouchID.value(m_filename, false).toBool()); #ifdef WITH_XC_YUBIKEY // Only auto-poll for hardware keys if we previously used one with this database file if (config()->get(Config::RememberLastKeyFiles).toBool()) { auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash(); if (lastChallengeResponse.contains(m_filename)) { pollHardwareKey(); } } #endif } void DatabaseOpenWidget::clearForms() { m_ui->editPassword->setText(""); m_ui->editPassword->setShowPassword(false); m_ui->keyFileLineEdit->clear(); m_ui->keyFileLineEdit->setShowPassword(false); m_ui->checkTouchID->setChecked(false); m_ui->challengeResponseCombo->clear(); m_db.reset(); } QSharedPointer DatabaseOpenWidget::database() { return m_db; } QString DatabaseOpenWidget::filename() { return m_filename; } void DatabaseOpenWidget::enterKey(const QString& pw, const QString& keyFile) { m_ui->editPassword->setText(pw); m_ui->keyFileLineEdit->setText(keyFile); openDatabase(); } void DatabaseOpenWidget::openDatabase() { m_ui->messageWidget->hide(); QSharedPointer databaseKey = buildDatabaseKey(); if (!databaseKey) { return; } m_ui->editPassword->setShowPassword(false); QCoreApplication::processEvents(); m_db.reset(new Database()); QString error; QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_ui->passwordFormFrame->setEnabled(false); QCoreApplication::processEvents(); bool ok = m_db->open(m_filename, databaseKey, &error, false); QApplication::restoreOverrideCursor(); m_ui->passwordFormFrame->setEnabled(true); if (ok && m_db->hasMinorVersionMismatch()) { QScopedPointer msgBox(new QMessageBox(this)); msgBox->setIcon(QMessageBox::Warning); msgBox->setWindowTitle(tr("Database Version Mismatch")); msgBox->setText(tr("The database you are trying to open was most likely\n" "created by a newer version of KeePassXC.\n\n" "You can try to open it anyway, but it may be incomplete\n" "and saving any changes may incur data loss.\n\n" "We recommend you update your KeePassXC installation.")); auto btn = msgBox->addButton(tr("Open database anyway"), QMessageBox::ButtonRole::AcceptRole); msgBox->setDefaultButton(btn); msgBox->addButton(QMessageBox::Cancel); msgBox->exec(); if (msgBox->clickedButton() != btn) { m_db.reset(new Database()); m_ui->messageWidget->showMessage(tr("Database unlock canceled."), MessageWidget::MessageType::Error); return; } } if (ok) { #ifdef WITH_XC_TOUCHID QHash useTouchID = config()->get(Config::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(Config::UseTouchID, useTouchID); #endif emit dialogFinished(true); clearForms(); } else { if (m_ui->editPassword->text().isEmpty() && !m_retryUnlockWithEmptyPassword) { QScopedPointer 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); // Focus on the password field and select the input for easy retry m_ui->editPassword->selectAll(); m_ui->editPassword->setFocus(); #ifdef WITH_XC_TOUCHID // unable to unlock database, reset TouchID for the current database TouchID::getInstance().reset(m_filename); #endif } } QSharedPointer DatabaseOpenWidget::buildDatabaseKey() { auto databaseKey = QSharedPointer::create(); if (!m_ui->editPassword->text().isEmpty() || m_retryUnlockWithEmptyPassword) { databaseKey->addKey(QSharedPointer::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 databaseKey->clear(); // try to get, decrypt and use PasswordKey QByteArray passwordKey; if (TouchID::getInstance().getKey(m_filename, passwordKey)) { // check if the user cancelled the operation if (passwordKey.isNull()) { return QSharedPointer(); } databaseKey->addKey(PasswordKey::fromRawKey(passwordKey)); } } #endif auto lastKeyFiles = config()->get(Config::LastKeyFiles).toHash(); lastKeyFiles.remove(m_filename); auto key = QSharedPointer::create(); QString keyFilename = m_ui->keyFileLineEdit->text(); if (!keyFilename.isEmpty()) { 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::KeePass2XMLv2 && key->type() != FileKey::Hashed && !config()->get(Config::Messages_NoLegacyKeyFileWarning).toBool()) { QMessageBox legacyWarning; legacyWarning.setWindowTitle(tr("Old key file format")); legacyWarning.setText(tr("You are using an old key file format which KeePassXC may
" "stop supporting in the future.

" "Please consider generating a new key file by going to:
" "Database > Database Security > Change 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, this, [](int state) { config()->set(Config::Messages_NoLegacyKeyFileWarning, state == Qt::CheckState::Checked); }); legacyWarning.exec(); } databaseKey->addKey(key); lastKeyFiles.insert(m_filename, keyFilename); } if (config()->get(Config::RememberLastKeyFiles).toBool()) { config()->set(Config::LastKeyFiles, lastKeyFiles); } #ifdef WITH_XC_YUBIKEY auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash(); lastChallengeResponse.remove(m_filename); int selectionIndex = m_ui->challengeResponseCombo->currentIndex(); if (selectionIndex > 0) { auto slot = m_ui->challengeResponseCombo->itemData(selectionIndex).value(); auto crKey = QSharedPointer(new ChallengeResponseKey(slot)); databaseKey->addChallengeResponseKey(crKey); // Qt doesn't read custom types in settings so stuff into a QString lastChallengeResponse.insert(m_filename, QStringLiteral("%1:%2").arg(slot.first).arg(slot.second)); } if (config()->get(Config::RememberLastKeyFiles).toBool()) { config()->set(Config::LastChallengeResponse, lastChallengeResponse); } #endif return databaseKey; } void DatabaseOpenWidget::reject() { emit dialogFinished(false); } void DatabaseOpenWidget::browseKeyFile() { QString filters = QString("%1 (*);;%2 (*.keyx; *.key)").arg(tr("All files"), tr("Key files")); QString filename = fileDialog()->getOpenFileName(this, tr("Select key file"), QString(), filters); if (QFileInfo(filename).canonicalFilePath() == QFileInfo(m_filename).canonicalFilePath()) { MessageBox::warning(this, tr("Cannot use database file as key file"), tr("You cannot use your database file as a key file.\nIf you do not have a key file, " "please leave the field empty."), MessageBox::Button::Ok); filename = ""; } if (!filename.isEmpty()) { m_ui->keyFileLineEdit->setText(filename); } } void DatabaseOpenWidget::clearKeyFileText() { m_ui->keyFileLineEdit->clear(); m_ui->keyFileLineEdit->setShowPassword(false); } void DatabaseOpenWidget::pollHardwareKey() { if (m_pollingHardwareKey) { return; } m_ui->challengeResponseCombo->clear(); m_ui->challengeResponseCombo->addItem(tr("Detecting hardware keys…")); m_ui->buttonRedetectYubikey->setEnabled(false); m_ui->challengeResponseCombo->setEnabled(false); m_ui->hardwareKeyProgress->setVisible(true); m_pollingHardwareKey = true; YubiKey::instance()->findValidKeys(); } void DatabaseOpenWidget::hardwareKeyResponse(bool found) { m_ui->challengeResponseCombo->clear(); m_ui->buttonRedetectYubikey->setEnabled(true); m_ui->hardwareKeyProgress->setVisible(false); m_pollingHardwareKey = false; if (!found) { m_ui->challengeResponseCombo->addItem(tr("No hardware keys detected")); m_ui->challengeResponseCombo->setEnabled(false); return; } else { m_ui->challengeResponseCombo->addItem(tr("Select hardware key…")); } YubiKeySlot lastUsedSlot; if (config()->get(Config::RememberLastKeyFiles).toBool()) { auto lastChallengeResponse = config()->get(Config::LastChallengeResponse).toHash(); if (lastChallengeResponse.contains(m_filename)) { // Qt doesn't read custom types in settings so extract from QString auto split = lastChallengeResponse.value(m_filename).toString().split(":"); if (split.size() > 1) { lastUsedSlot = YubiKeySlot(split[0].toUInt(), split[1].toInt()); } } } int selectedIndex = 0; for (auto& slot : YubiKey::instance()->foundKeys()) { // add detected YubiKey to combo box m_ui->challengeResponseCombo->addItem(YubiKey::instance()->getDisplayName(slot), QVariant::fromValue(slot)); // Select this YubiKey + Slot if we used it in the past if (lastUsedSlot == slot) { selectedIndex = m_ui->challengeResponseCombo->count() - 1; } } m_ui->challengeResponseCombo->setCurrentIndex(selectedIndex); m_ui->challengeResponseCombo->setEnabled(true); } void DatabaseOpenWidget::openHardwareKeyHelp() { QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs#faq-cat-yubikey")); } void DatabaseOpenWidget::openKeyFileHelp() { QDesktopServices::openUrl(QUrl("https://keepassxc.org/docs#faq-cat-keyfile")); }