diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index f00e93531..ccdc955f2 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -132,6 +132,7 @@ set(keepassx_SOURCES gui/UnlockDatabaseWidget.cpp gui/UnlockDatabaseDialog.cpp gui/WelcomeWidget.cpp + gui/widgets/ElidedLabel.cpp gui/csvImport/CsvImportWidget.cpp gui/csvImport/CsvImportWizard.cpp gui/csvImport/CsvParserModel.cpp diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index 0a0ff2961..fb89e8bcb 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -188,10 +188,10 @@ QString BrowserService::storeKey(const QString& key) "If you would like to allow it access to your KeePassXC database,\n" "give it a unique name to identify and accept it.")); keyDialog.setOkButtonText(tr("Save and allow access")); + keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint); keyDialog.show(); keyDialog.activateWindow(); keyDialog.raise(); - keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint); auto ok = keyDialog.exec(); id = keyDialog.textValue(); diff --git a/src/core/Config.cpp b/src/core/Config.cpp index c83c1e720..461ab1f74 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -22,6 +22,7 @@ #include #include #include +#include Config* Config::m_instance(nullptr); diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index 629d8e802..772a56611 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -48,7 +48,7 @@ bool EntryAttributes::hasKey(const QString& key) const return m_attributes.contains(key); } -QList EntryAttributes::customKeys() +QList EntryAttributes::customKeys() const { QList customKeys; const QList keyList = keys(); diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index 3eca9171c..91cc50879 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -33,7 +33,7 @@ public: explicit EntryAttributes(QObject* parent = nullptr); QList keys() const; bool hasKey(const QString& key) const; - QList customKeys(); + QList customKeys() const; QString value(const QString& key) const; bool contains(const QString& key) const; bool containsValue(const QString& value) const; diff --git a/src/core/Group.cpp b/src/core/Group.cpp index c7c12bfea..bdcfeff73 100644 --- a/src/core/Group.cpp +++ b/src/core/Group.cpp @@ -439,11 +439,11 @@ void Group::setParent(Database* db) QObject::setParent(db); } -QStringList Group::hierarchy() +QStringList Group::hierarchy() const { QStringList hierarchy; - Group* group = this; - Group* parent = m_parent; + const Group* group = this; + const Group* parent = m_parent; hierarchy.prepend(group->name()); while (parent) { diff --git a/src/core/Group.h b/src/core/Group.h index 57d503a85..70f033196 100644 --- a/src/core/Group.h +++ b/src/core/Group.h @@ -117,7 +117,7 @@ public: Group* parentGroup(); const Group* parentGroup() const; void setParent(Group* parent, int index = -1); - QStringList hierarchy(); + QStringList hierarchy() const; Database* database(); const Database* database() const; diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index b7a622b19..cd57f6292 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -111,9 +111,13 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) "border-radius: 5px;"); m_detailsView = new DetailsWidget(this); - connect(m_detailsView, &DetailsWidget::errorOccurred, this, [this](const QString& error) { - showMessage(error, MessageWidget::MessageType::Error); - }); + m_detailsView->hide(); + connect(this, SIGNAL(pressedEntry(Entry*)), m_detailsView, SLOT(setEntry(Entry*))); + connect(this, SIGNAL(pressedGroup(Group*)), m_detailsView, SLOT(setGroup(Group*))); + connect(this, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), + m_detailsView, SLOT(setDatabaseMode(DatabaseWidget::Mode))); + connect(m_detailsView, SIGNAL(errorOccurred(QString)), this, SLOT(showErrorMessage(QString))); + QVBoxLayout* vLayout = new QVBoxLayout(rightHandSideWidget); vLayout->setMargin(0); @@ -1488,6 +1492,11 @@ void DatabaseWidget::showMessage(const QString& text, MessageWidget::MessageType m_messageWidget->showMessage(text, type, autoHideTimeout); } +void DatabaseWidget::showErrorMessage(const QString& errorMessage) +{ + showMessage(errorMessage, MessageWidget::MessageType::Error); +} + void DatabaseWidget::hideMessage() { if (m_messageWidget->isVisible()) { diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index ba799893a..464a543ab 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -178,6 +178,7 @@ public slots: void showMessage(const QString& text, MessageWidget::MessageType type, bool showClosebutton = true, int autoHideTimeout = MessageWidget::DefaultAutoHideTimeout); + void showErrorMessage(const QString& errorMessage); void hideMessage(); private slots: diff --git a/src/gui/DetailsWidget.cpp b/src/gui/DetailsWidget.cpp index 9f0d2bfd8..1ac20c9d8 100644 --- a/src/gui/DetailsWidget.cpp +++ b/src/gui/DetailsWidget.cpp @@ -28,153 +28,189 @@ #include "gui/Clipboard.h" #include "entry/EntryAttachmentsModel.h" +namespace { +constexpr int GeneralTabIndex = 0; +} + DetailsWidget::DetailsWidget(QWidget* parent) : QWidget(parent) , m_ui(new Ui::DetailsWidget()) , m_locked(false) , m_currentEntry(nullptr) , m_currentGroup(nullptr) - , m_timer(nullptr) - , m_attributesTabWidget(nullptr) - , m_attachmentsTabWidget(nullptr) - , m_autotypeTabWidget(nullptr) + , m_step(0) + , m_totpTimer(nullptr) , m_selectedTabEntry(0) , m_selectedTabGroup(0) { m_ui->setupUi(this); - connect(parent, SIGNAL(pressedEntry(Entry*)), SLOT(getSelectedEntry(Entry*))); - connect(parent, SIGNAL(pressedGroup(Group*)), SLOT(getSelectedGroup(Group*))); - connect(parent, SIGNAL(currentModeChanged(DatabaseWidget::Mode)), SLOT(setDatabaseMode(DatabaseWidget::Mode))); + // Entry + m_ui->entryTotpButton->setIcon(filePath()->icon("actions", "chronometer")); + m_ui->entryCloseButton->setIcon(filePath()->icon("actions", "dialog-close")); - m_ui->totpButton->setIcon(filePath()->icon("actions", "chronometer")); - m_ui->closeButton->setIcon(filePath()->icon("actions", "dialog-close")); + m_ui->entryAttachmentsWidget->setReadOnly(true); + m_ui->entryAttachmentsWidget->setButtonsVisible(false); - connect(m_ui->totpButton, SIGNAL(toggled(bool)), SLOT(showTotp(bool))); - connect(m_ui->closeButton, SIGNAL(toggled(bool)), SLOT(hideDetails())); - connect(m_ui->tabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndex(int))); + connect(m_ui->entryTotpButton, SIGNAL(toggled(bool)), m_ui->entryTotpWidget, SLOT(setVisible(bool))); + connect(m_ui->entryCloseButton, SIGNAL(toggled(bool)), SLOT(hide())); + connect(m_ui->entryTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection); - m_ui->attachmentsWidget->setReadOnly(true); - m_ui->attachmentsWidget->setButtonsVisible(false); - - m_attributesTabWidget = m_ui->tabWidget->widget(AttributesTab); - m_attachmentsTabWidget = m_ui->tabWidget->widget(AttachmentsTab); - m_autotypeTabWidget = m_ui->tabWidget->widget(AutotypeTab); - - this->hide(); + // Group + m_ui->groupCloseButton->setIcon(filePath()->icon("actions", "dialog-close")); + connect(m_ui->groupCloseButton, SIGNAL(toggled(bool)), SLOT(hide())); + connect(m_ui->groupTabWidget, SIGNAL(tabBarClicked(int)), SLOT(updateTabIndexes()), Qt::QueuedConnection); } DetailsWidget::~DetailsWidget() { - if (m_timer) { - delete m_timer; - } + deleteTotpTimer(); } -void DetailsWidget::getSelectedEntry(Entry* selectedEntry) +void DetailsWidget::setEntry(Entry* selectedEntry) { if (!selectedEntry) { - hideDetails(); + hide(); return; } m_currentEntry = selectedEntry; - if (!config()->get("GUI/HideDetailsView").toBool()) { - this->show(); + updateEntryHeaderLine(); + updateEntryTotp(); + updateEntryGeneralTab(); + updateEntryNotesTab(); + updateEntryAttributesTab(); + updateEntryAttachmentsTab(); + updateEntryAutotypeTab(); + + setVisible(!config()->get("GUI/HideDetailsView").toBool()); + + m_ui->stackedWidget->setCurrentWidget(m_ui->pageEntry); + const int tabIndex = m_ui->entryTabWidget->isTabEnabled(m_selectedTabEntry) ? m_selectedTabEntry + : GeneralTabIndex; + Q_ASSERT(m_ui->entryTabWidget->isTabEnabled(GeneralTabIndex)); + m_ui->entryTabWidget->setCurrentIndex(tabIndex); +} + +void DetailsWidget::setGroup(Group* selectedGroup) +{ + if (!selectedGroup) { + hide(); + return; } - m_ui->stackedWidget->setCurrentIndex(EntryPreview); + m_currentGroup = selectedGroup; + updateGroupHeaderLine(); + updateGroupGeneralTab(); + updateGroupNotesTab(); - if (m_ui->tabWidget->count() < 5) { - m_ui->tabWidget->insertTab(static_cast(AttributesTab), m_attributesTabWidget, tr("Attributes")); - m_ui->tabWidget->insertTab(static_cast(AttachmentsTab), m_attachmentsTabWidget, tr("Attachments")); - m_ui->tabWidget->insertTab(static_cast(AutotypeTab), m_autotypeTabWidget, tr("Autotype")); + setVisible(!config()->get("GUI/HideDetailsView").toBool()); + + m_ui->stackedWidget->setCurrentWidget(m_ui->pageGroup); + const int tabIndex = m_ui->groupTabWidget->isTabEnabled(m_selectedTabGroup) ? m_selectedTabGroup + : GeneralTabIndex; + Q_ASSERT(m_ui->groupTabWidget->isTabEnabled(GeneralTabIndex)); + m_ui->groupTabWidget->setCurrentIndex(tabIndex); +} + +void DetailsWidget::setDatabaseMode(DatabaseWidget::Mode mode) +{ + m_locked = mode == DatabaseWidget::LockedMode; + if (m_locked) { + return; } - m_ui->tabWidget->setTabEnabled(AttributesTab, false); - m_ui->tabWidget->setTabEnabled(NotesTab, false); - m_ui->tabWidget->setTabEnabled(AutotypeTab, false); - - m_ui->totpButton->hide(); - m_ui->totpWidget->hide(); - m_ui->totpButton->setChecked(false); - - auto icon = m_currentEntry->iconPixmap(); - if (icon.width() > 16 || icon.height() > 16) { - icon = icon.scaled(16, 16); - } - m_ui->entryIcon->setPixmap(icon); - - QString title = QString(" / "); - Group* entry_group = m_currentEntry->group(); - if (entry_group) { - QStringList hierarchy = entry_group->hierarchy(); - hierarchy.removeFirst(); - title += hierarchy.join(" / "); - if (hierarchy.size() > 0) { - title += " / "; + if (mode == DatabaseWidget::ViewMode) { + if (m_ui->stackedWidget->currentWidget() == m_ui->pageGroup) { + setGroup(m_currentGroup); + } else { + setEntry(m_currentEntry); } } - title.append(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title())); - m_ui->titleLabel->setText(title); +} - m_ui->usernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username())); +void DetailsWidget::updateEntryHeaderLine() +{ + Q_ASSERT(m_currentEntry); + const QString title = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->title()); + m_ui->entryTitleLabel->setText(hierarchy(m_currentEntry->group(), title)); + m_ui->entryIcon->setPixmap(preparePixmap(m_currentEntry->iconPixmap(), 16)); +} + +void DetailsWidget::updateEntryTotp() +{ + Q_ASSERT(m_currentEntry); + const bool hasTotp = m_currentEntry->hasTotp(); + m_ui->entryTotpButton->setVisible(hasTotp); + m_ui->entryTotpWidget->hide(); + m_ui->entryTotpButton->setChecked(false); + + if (hasTotp) { + deleteTotpTimer(); + m_totpTimer = new QTimer(m_currentEntry); + connect(m_totpTimer, SIGNAL(timeout()), this, SLOT(updateTotpLabel())); + m_totpTimer->start(1000); + + m_step = m_currentEntry->totpStep(); + updateTotpLabel(); + } else { + m_ui->entryTotpLabel->clear(); + stopTotpTimer(); + } +} + +void DetailsWidget::updateEntryGeneralTab() +{ + Q_ASSERT(m_currentEntry); + m_ui->entryUsernameLabel->setText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->username())); if (!config()->get("security/hidepassworddetails").toBool()) { - m_ui->passwordLabel->setText( - shortPassword(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password()))); - m_ui->passwordLabel->setToolTip(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password())); + const QString password = m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->password()); + m_ui->entryPasswordLabel->setRawText(password); + m_ui->entryPasswordLabel->setToolTip(password); } else { - m_ui->passwordLabel->setText(QString("\u25cf").repeated(6)); + m_ui->entryPasswordLabel->setRawText(QString("\u25cf").repeated(6)); + m_ui->entryPasswordLabel->setToolTip({}); } - QString url = m_currentEntry->webUrl(); + const QString url = m_currentEntry->webUrl(); if (!url.isEmpty()) { // URL is well formed and can be opened in a browser // create a new display url that masks password placeholders // the actual link will use the password - url = QString("%2").arg(url).arg(shortUrl(m_currentEntry->displayUrl())); - m_ui->urlLabel->setOpenExternalLinks(true); + m_ui->entryUrlLabel->setRawText(m_currentEntry->displayUrl()); + m_ui->entryUrlLabel->setUrl(url); } else { // Fallback to the raw url string - url = shortUrl(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->url())); - m_ui->urlLabel->setOpenExternalLinks(false); - } - m_ui->urlLabel->setText(url); - - TimeInfo entryTime = m_currentEntry->timeInfo(); - if (entryTime.expires()) { - m_ui->expirationLabel->setText(entryTime.expiryTime().toString(Qt::DefaultLocaleShortDate)); - } else { - m_ui->expirationLabel->setText(tr("Never")); + m_ui->entryUrlLabel->setRawText(m_currentEntry->resolveMultiplePlaceholders(m_currentEntry->url())); + m_ui->entryUrlLabel->setUrl({}); } - if (m_currentEntry->hasTotp()) { - m_step = m_currentEntry->totpStep(); + const TimeInfo entryTime = m_currentEntry->timeInfo(); + const QString expires = entryTime.expires() ? entryTime.expiryTime().toString(Qt::DefaultLocaleShortDate) + : tr("Never"); + m_ui->entryExpirationLabel->setText(expires); +} - if (m_timer) { - delete m_timer; - } - m_timer = new QTimer(selectedEntry); - connect(m_timer, SIGNAL(timeout()), this, SLOT(updateTotp())); - updateTotp(); - m_timer->start(m_step * 1000); - m_ui->totpButton->show(); - } +void DetailsWidget::updateEntryNotesTab() +{ + Q_ASSERT(m_currentEntry); + const QString notes = m_currentEntry->notes(); + setTabEnabled(m_ui->entryTabWidget, m_ui->entryNotesTab, !notes.isEmpty()); + m_ui->entryNotesEdit->setText(m_currentEntry->resolveMultiplePlaceholders(notes)); +} - QString notes = m_currentEntry->notes(); - if (!notes.isEmpty()) { - m_ui->tabWidget->setTabEnabled(NotesTab, true); - m_ui->notesEdit->setText(m_currentEntry->resolveMultiplePlaceholders(notes)); - } - - QStringList customAttributes = m_currentEntry->attributes()->customKeys(); - if (customAttributes.size() > 0) { - m_ui->tabWidget->setTabEnabled(AttributesTab, true); - m_ui->attributesEdit->clear(); - - QString attributesText = QString(); +void DetailsWidget::updateEntryAttributesTab() +{ + Q_ASSERT(m_currentEntry); + m_ui->entryAttributesEdit->clear(); + const EntryAttributes* attributes = m_currentEntry->attributes(); + const QStringList customAttributes = attributes->customKeys(); + const bool haveAttributes = customAttributes.size() > 0; + setTabEnabled(m_ui->entryTabWidget, m_ui->entryAttributesTab, haveAttributes); + if (haveAttributes) { + QString attributesText; for (const QString& key : customAttributes) { QString value = m_currentEntry->attributes()->value(key); if (m_currentEntry->attributes()->isProtected(key)) { @@ -182,174 +218,118 @@ void DetailsWidget::getSelectedEntry(Entry* selectedEntry) } attributesText.append(QString("%1: %2
").arg(key, value)); } - m_ui->attributesEdit->setText(attributesText); + m_ui->entryAttributesEdit->setText(attributesText); } +} +void DetailsWidget::updateEntryAttachmentsTab() +{ + Q_ASSERT(m_currentEntry); const bool hasAttachments = !m_currentEntry->attachments()->isEmpty(); - m_ui->tabWidget->setTabEnabled(AttachmentsTab, hasAttachments); - m_ui->attachmentsWidget->setEntryAttachments(m_currentEntry->attachments()); + setTabEnabled(m_ui->entryTabWidget, m_ui->entryAttachmentsTab, hasAttachments); + m_ui->entryAttachmentsWidget->setEntryAttachments(m_currentEntry->attachments()); +} - m_ui->autotypeTree->clear(); - AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations(); +void DetailsWidget::updateEntryAutotypeTab() +{ + Q_ASSERT(m_currentEntry); + m_ui->entryAutotypeTree->clear(); QList items; - for (auto assoc : autotypeAssociations->getAll()) { - QStringList association = QStringList() << assoc.window << assoc.sequence; - if (association.at(1).isEmpty()) { - association.replace(1, m_currentEntry->effectiveAutoTypeSequence()); - } - items.append(new QTreeWidgetItem(m_ui->autotypeTree, association)); - } - if (items.count() > 0) { - m_ui->autotypeTree->addTopLevelItems(items); - m_ui->tabWidget->setTabEnabled(AutotypeTab, true); + const AutoTypeAssociations* autotypeAssociations = m_currentEntry->autoTypeAssociations(); + const auto associations = autotypeAssociations->getAll(); + for (const auto& assoc : associations) { + const QString sequence = assoc.sequence.isEmpty() ? m_currentEntry->effectiveAutoTypeSequence() + : assoc.sequence; + items.append(new QTreeWidgetItem(m_ui->entryAutotypeTree, {assoc.window, sequence})); } - if (m_ui->tabWidget->isTabEnabled(m_selectedTabEntry)) { - m_ui->tabWidget->setCurrentIndex(m_selectedTabEntry); + m_ui->entryAutotypeTree->addTopLevelItems(items); + setTabEnabled(m_ui->entryTabWidget, m_ui->entryAutotypeTab, !items.isEmpty()); +} + +void DetailsWidget::updateGroupHeaderLine() +{ + Q_ASSERT(m_currentGroup); + m_ui->groupTitleLabel->setText(hierarchy(m_currentGroup, {})); + m_ui->groupIcon->setPixmap(preparePixmap(m_currentGroup->iconPixmap(), 32)); +} + +void DetailsWidget::updateGroupGeneralTab() +{ + Q_ASSERT(m_currentGroup); + const QString searchingText = m_currentGroup->resolveSearchingEnabled() ? tr("Enabled") : tr("Disabled"); + m_ui->groupSearchingLabel->setText(searchingText); + + const QString autotypeText = m_currentGroup->resolveAutoTypeEnabled() ? tr("Enabled") : tr("Disabled"); + m_ui->groupAutotypeLabel->setText(autotypeText); + + const TimeInfo groupTime = m_currentGroup->timeInfo(); + const QString expiresText = groupTime.expires() ? groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate) + : tr("Never"); + m_ui->groupExpirationLabel->setText(expiresText); +} + +void DetailsWidget::updateGroupNotesTab() +{ + Q_ASSERT(m_currentGroup); + const QString notes = m_currentGroup->notes(); + setTabEnabled(m_ui->groupTabWidget, m_ui->groupNotesTab, !notes.isEmpty()); + m_ui->groupNotesEdit->setText(notes); +} + +void DetailsWidget::stopTotpTimer() +{ + if (m_totpTimer) { + m_totpTimer->stop(); } } -void DetailsWidget::getSelectedGroup(Group* selectedGroup) +void DetailsWidget::deleteTotpTimer() { - if (!selectedGroup) { - hideDetails(); - return; + if (m_totpTimer) { + delete m_totpTimer; } +} - m_currentGroup = selectedGroup; - - if (!config()->get("GUI/HideDetailsView").toBool()) { - this->show(); +void DetailsWidget::updateTotpLabel() +{ + if (!m_locked && m_currentEntry) { + const QString totpCode = m_currentEntry->totp(); + const QString firstHalf = totpCode.left(totpCode.size() / 2); + const QString secondHalf = totpCode.mid(totpCode.size() / 2); + m_ui->entryTotpLabel->setText(firstHalf + " " + secondHalf); + } else { + m_ui->entryTotpLabel->clear(); + stopTotpTimer(); } +} - m_ui->stackedWidget->setCurrentIndex(GroupPreview); +void DetailsWidget::updateTabIndexes() +{ + m_selectedTabEntry = m_ui->entryTabWidget->currentIndex(); + m_selectedTabGroup = m_ui->groupTabWidget->currentIndex(); +} - if (m_ui->tabWidget->count() > 2) { - m_ui->tabWidget->removeTab(AutotypeTab); - m_ui->tabWidget->removeTab(AttachmentsTab); - m_ui->tabWidget->removeTab(AttributesTab); +void DetailsWidget::setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled) +{ + const int tabIndex = tabWidget->indexOf(widget); + Q_ASSERT(tabIndex != -1); + tabWidget->setTabEnabled(tabIndex, enabled); +} + +QPixmap DetailsWidget::preparePixmap(const QPixmap& pixmap, int size) +{ + if (pixmap.width() > size || pixmap.height() > size) { + return pixmap.scaled(size, size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); } + return pixmap; +} - m_ui->tabWidget->setTabEnabled(GroupNotesTab, false); - - m_ui->totpButton->hide(); - m_ui->totpWidget->hide(); - - auto icon = m_currentGroup->iconPixmap(); - if (icon.width() > 32 || icon.height() > 32) { - icon = icon.scaled(32, 32); - } - m_ui->entryIcon->setPixmap(icon); - - QString title = " / "; - QStringList hierarchy = m_currentGroup->hierarchy(); +QString DetailsWidget::hierarchy(const Group* group, const QString& title) +{ + const QString separator(" / "); + QStringList hierarchy = group->hierarchy(); hierarchy.removeFirst(); - title += hierarchy.join(" / "); - if (hierarchy.size() > 0) { - title += " / "; - } - m_ui->titleLabel->setText(title); - - QString notes = m_currentGroup->notes(); - if (!notes.isEmpty()) { - m_ui->tabWidget->setTabEnabled(GroupNotesTab, true); - m_ui->notesEdit->setText(notes); - } - - QString searching = tr("Disabled"); - if (m_currentGroup->resolveSearchingEnabled()) { - searching = tr("Enabled"); - } - m_ui->searchingLabel->setText(searching); - - QString autotype = tr("Disabled"); - if (m_currentGroup->resolveAutoTypeEnabled()) { - autotype = tr("Enabled"); - } - m_ui->autotypeLabel->setText(autotype); - - TimeInfo groupTime = m_currentGroup->timeInfo(); - if (groupTime.expires()) { - m_ui->groupExpirationLabel->setText(groupTime.expiryTime().toString(Qt::DefaultLocaleShortDate)); - } else { - m_ui->groupExpirationLabel->setText(tr("Never")); - } - - if (m_ui->tabWidget->isTabEnabled(m_selectedTabGroup)) { - m_ui->tabWidget->setCurrentIndex(m_selectedTabGroup); - } -} - -void DetailsWidget::updateTotp() -{ - if (!m_locked) { - QString totpCode = m_currentEntry->totp(); - QString firstHalf = totpCode.left(totpCode.size() / 2); - QString secondHalf = totpCode.mid(totpCode.size() / 2); - m_ui->totpLabel->setText(firstHalf + " " + secondHalf); - } else if (m_timer) { - m_timer->stop(); - } -} - -void DetailsWidget::showTotp(bool visible) -{ - if (visible) { - m_ui->totpWidget->show(); - } else { - m_ui->totpWidget->hide(); - } -} - -QString DetailsWidget::shortUrl(QString url) -{ - QString newurl = ""; - if (url.length() > 60) { - newurl.append(url.left(20)); - newurl.append("…"); - newurl.append(url.right(20)); - return newurl; - } - return url; -} - -QString DetailsWidget::shortPassword(QString password) -{ - QString newpassword = ""; - if (password.length() > 60) { - newpassword.append(password.left(50)); - newpassword.append("…"); - return newpassword; - } - return password; -} - -void DetailsWidget::hideDetails() -{ - this->hide(); -} - -void DetailsWidget::setDatabaseMode(DatabaseWidget::Mode mode) -{ - m_locked = false; - if (mode == DatabaseWidget::LockedMode) { - m_locked = true; - return; - } - if (mode == DatabaseWidget::ViewMode) { - if (m_ui->stackedWidget->currentIndex() == GroupPreview) { - getSelectedGroup(m_currentGroup); - } else { - getSelectedEntry(m_currentEntry); - } - } -} - -void DetailsWidget::updateTabIndex(int index) -{ - if (m_ui->stackedWidget->currentIndex() == GroupPreview) { - m_selectedTabGroup = index; - } else { - m_selectedTabEntry = index; - } + hierarchy.append(title); + return QString("%1%2").arg(separator, hierarchy.join(separator)); } diff --git a/src/gui/DetailsWidget.h b/src/gui/DetailsWidget.h index 2af184c48..abec33400 100644 --- a/src/gui/DetailsWidget.h +++ b/src/gui/DetailsWidget.h @@ -19,8 +19,8 @@ #define KEEPASSX_DETAILSWIDGET_H #include "gui/DatabaseWidget.h" + #include -#include namespace Ui { class DetailsWidget; @@ -34,48 +34,46 @@ public: explicit DetailsWidget(QWidget* parent = nullptr); ~DetailsWidget(); - enum StackedWidgetIndex - { - EntryPreview = 0, - GroupPreview = 1, - }; - - enum TabWidgetIndex - { - GeneralTab = 0, - AttributesTab = 1, - GroupNotesTab = 1, - AttachmentsTab = 2, - NotesTab = 3, - AutotypeTab = 4, - }; +public slots: + void setEntry(Entry* selectedEntry); + void setGroup(Group* selectedGroup); + void setDatabaseMode(DatabaseWidget::Mode mode); signals: void errorOccurred(const QString& error); private slots: - void getSelectedEntry(Entry* selectedEntry); - void getSelectedGroup(Group* selectedGroup); - void showTotp(bool visible); - void updateTotp(); - void hideDetails(); - void setDatabaseMode(DatabaseWidget::Mode mode); - void updateTabIndex(int index); + void updateEntryHeaderLine(); + void updateEntryTotp(); + void updateEntryGeneralTab(); + void updateEntryNotesTab(); + void updateEntryAttributesTab(); + void updateEntryAttachmentsTab(); + void updateEntryAutotypeTab(); + + void updateGroupHeaderLine(); + void updateGroupGeneralTab(); + void updateGroupNotesTab(); + + void stopTotpTimer(); + void deleteTotpTimer(); + void updateTotpLabel(); + void updateTabIndexes(); private: + void setTabEnabled(QTabWidget* tabWidget, QWidget* widget, bool enabled); + + static QPixmap preparePixmap(const QPixmap& pixmap, int size); + static QString hierarchy(const Group* group, const QString& title); + const QScopedPointer m_ui; bool m_locked; Entry* m_currentEntry; Group* m_currentGroup; quint8 m_step; - QPointer m_timer = nullptr; - QWidget* m_attributesTabWidget; - QWidget* m_attachmentsTabWidget; - QWidget* m_autotypeTabWidget; + QPointer m_totpTimer; quint8 m_selectedTabEntry; quint8 m_selectedTabGroup; - QString shortUrl(QString url); - QString shortPassword(QString password); }; #endif // KEEPASSX_DETAILSWIDGET_H diff --git a/src/gui/DetailsWidget.ui b/src/gui/DetailsWidget.ui index 9c705569d..5446f6510 100644 --- a/src/gui/DetailsWidget.ui +++ b/src/gui/DetailsWidget.ui @@ -2,521 +2,634 @@ DetailsWidget - + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + - - - QLayout::SetDefaultConstraint - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 0 - - - - - 12 - - - - Qt::AutoText - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - 0 - - - 0 - - - 0 - - - - - - 10 - 75 - true - - - - - - - - - - - - - - Generate TOTP Token - - - - - - true - - - - - - - Close - - - - - - true - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - + 0 - - false - - - false - - - false - - - - General - - + + - + + + QLayout::SetDefaultConstraint + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 12 + + + + Qt::AutoText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + + + 0 + + + 0 + + + + + + 10 + 75 + true + + + + + + + + + + + + + + Generate TOTP Token + + + + + + true + + + + + + + Close + + + + + + true + + + + + + + 0 - - - - - - Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse - + + false + + + false + + + false + + + + General + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Qt::LeftToRight + + + Username + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 100 + 0 + + + + Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Password + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 100 + 0 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + URL + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 100 + 0 + + + + PointingHandCursor + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Expiration + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + - - - - - 0 - 0 - + + + + + Attributes + + + + + + Qt::ClickFocus - - - 75 - true - - - - Password - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - URL - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - PointingHandCursor - - - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Expiration - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Qt::LeftToRight - - - Username - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true - - - - - - - 0 - 0 - + + + Attachments + + + + + + + + + + Notes + + + + + + Qt::ClickFocus - - - 75 - true - - - - Autotype - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true - - - - - 0 - 0 - + + + + + Autotype + + + + + + QFrame::Sunken + + true + + + true + + + false + + + 2 + + + false + + + 250 + + + 50 + + + true + + + + Window + + + + + Sequence + + - - - - Qt::Vertical - - - - 20 - 0 - - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Searching - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - - 0 - 0 - - - - - - - - - 0 - 0 - - - - - 75 - true - - - - Expiration - - - Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - - - - - - Qt::Horizontal - - - QSizePolicy::Fixed - - - - 20 - 20 - - - - - - - Attributes - - + + - - - Qt::ClickFocus + + + QLayout::SetDefaultConstraint - - true - - + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + + 12 + + + + Qt::AutoText + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + true + + + + - - - - - Attachments - - - - - - - - - Notes - - - - - - Qt::ClickFocus + + + 0 - - true - - - - - - - - Autotype - - - - - - QFrame::Sunken - - - true - - - true - - + false - - 2 - - + false - - - 250 - - - 50 - - - true - - - - Window - - - - - Sequence - - + + + false + + + + General + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Autotype + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Searching + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Expiration + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + + Qt::Vertical + + + + 20 + 10 + + + + + + + + + + + + Notes + + + + + + Qt::ClickFocus + + + true + + + + + @@ -542,6 +655,11 @@
gui/entry/EntryAttachmentsWidget.h
1 + + ElidedLabel + QLabel +
gui/widgets/ElidedLabel.h
+
diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index eef1b5ed8..c146da691 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -444,6 +444,10 @@ bool EditEntryWidget::getOpenSSHKey(OpenSSHKey& key) return false; } + if (key.comment().isEmpty()) { + key.setComment(m_entry->username()); + } + return true; } diff --git a/src/gui/widgets/ElidedLabel.cpp b/src/gui/widgets/ElidedLabel.cpp new file mode 100644 index 000000000..1642bdec8 --- /dev/null +++ b/src/gui/widgets/ElidedLabel.cpp @@ -0,0 +1,116 @@ +/* + * 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 "ElidedLabel.h" + +#include +#include + +namespace { +const QString htmlLinkTemplate("%2"); +} + +ElidedLabel::ElidedLabel(QWidget* parent, Qt::WindowFlags f) + : QLabel(parent, f) + , m_elideMode(Qt::ElideMiddle) +{ + connect(this, SIGNAL(elideModeChanged(Qt::TextElideMode)), this, SLOT(updateElidedText())); + connect(this, SIGNAL(rawTextChanged(QString)), this, SLOT(updateElidedText())); + connect(this, SIGNAL(urlChanged(QString)), this, SLOT(updateElidedText())); +} + +ElidedLabel::ElidedLabel(const QString& text, QWidget* parent, Qt::WindowFlags f) + : ElidedLabel(parent, f) +{ + setText(text); +} + +Qt::TextElideMode ElidedLabel::elideMode() const +{ + return m_elideMode; +} + +QString ElidedLabel::rawText() const +{ + return m_rawText; +} + +QString ElidedLabel::url() const +{ + return m_url; +} + +void ElidedLabel::setElideMode(Qt::TextElideMode elideMode) +{ + if (m_elideMode == elideMode) + return; + + if (m_elideMode != Qt::ElideNone) { + setWordWrap(false); + } + + m_elideMode = elideMode; + emit elideModeChanged(m_elideMode); +} + +void ElidedLabel::setRawText(const QString& elidedText) +{ + if (m_rawText == elidedText) + return; + + m_rawText = elidedText; + emit rawTextChanged(m_rawText); +} + +void ElidedLabel::setUrl(const QString& url) +{ + if (m_url == url) + return; + + m_url = url; + emit urlChanged(m_url); +} + +void ElidedLabel::clear() +{ + setRawText(QString()); + setElideMode(Qt::ElideMiddle); + setUrl(QString()); + QLabel::clear(); +} + +void ElidedLabel::updateElidedText() +{ + if (m_rawText.isEmpty()) { + QLabel::clear(); + return; + } + + QString displayText = m_rawText; + if (m_elideMode != Qt::ElideNone) { + const QFontMetrics metrix(font()); + displayText = metrix.elidedText(m_rawText, m_elideMode, width() - 2); + } + setText(m_url.isEmpty() ? displayText : htmlLinkTemplate.arg(m_url, displayText)); + setOpenExternalLinks(!m_url.isEmpty()); +} + +void ElidedLabel::resizeEvent(QResizeEvent* event) +{ + updateElidedText(); + QLabel::resizeEvent(event); +} diff --git a/src/gui/widgets/ElidedLabel.h b/src/gui/widgets/ElidedLabel.h new file mode 100644 index 000000000..c7694b5a1 --- /dev/null +++ b/src/gui/widgets/ElidedLabel.h @@ -0,0 +1,61 @@ +/* + * 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 . + */ + +#ifndef KEEPASSX_ELIDEDLABEL_H +#define KEEPASSX_ELIDEDLABEL_H + +#include + +class QResizeEvent; + +class ElidedLabel : public QLabel +{ + Q_OBJECT + Q_PROPERTY(Qt::TextElideMode elideMode READ elideMode WRITE setElideMode NOTIFY elideModeChanged) + Q_PROPERTY(QString rawText READ rawText WRITE setRawText NOTIFY rawTextChanged) + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) +public: + explicit ElidedLabel(QWidget *parent=nullptr, Qt::WindowFlags f=Qt::WindowFlags()); + explicit ElidedLabel(const QString &text, QWidget *parent=nullptr, Qt::WindowFlags f=Qt::WindowFlags()); + + Qt::TextElideMode elideMode() const; + QString rawText() const; + QString url() const; + +public slots: + void setElideMode(Qt::TextElideMode elideMode); + void setRawText(const QString& rawText); + void setUrl(const QString& url); + void clear(); + +signals: + void elideModeChanged(Qt::TextElideMode elideMode); + void rawTextChanged(QString rawText); + void urlChanged(QString url); + +private slots: + void updateElidedText(); + +private: + void resizeEvent(QResizeEvent* event); + + Qt::TextElideMode m_elideMode; + QString m_rawText; + QString m_url; +}; + +#endif // KEEPASSX_ELIDEDLABEL_H diff --git a/src/sshagent/ASN1Key.cpp b/src/sshagent/ASN1Key.cpp new file mode 100644 index 000000000..c46b8fa0b --- /dev/null +++ b/src/sshagent/ASN1Key.cpp @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2018 Toni Spets + * Copyright (C) 2018 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 "ASN1Key.h" +#include + +namespace { + constexpr quint8 TAG_INT = 0x02; + constexpr quint8 TAG_SEQUENCE = 0x30; + constexpr quint8 KEY_ZERO = 0x0; + + bool nextTag(BinaryStream& stream, quint8& tag, quint32& len) + { + stream.read(tag); + + quint8 lenByte; + stream.read(lenByte); + + if (lenByte & 0x80) { + quint32 bytes = lenByte & ~0x80; + if (bytes == 1) { + stream.read(lenByte); + len = lenByte; + } else if (bytes == 2) { + quint16 lenShort; + stream.read(lenShort); + len = lenShort; + } else if (bytes == 4) { + stream.read(len); + } else { + return false; + } + } else { + len = lenByte; + } + + return true; + } + + bool parseHeader(BinaryStream& stream, quint8 wantedType) + { + quint8 tag; + quint32 len; + + nextTag(stream, tag, len); + + if (tag != TAG_SEQUENCE) { + return false; + } + + nextTag(stream, tag, len); + + if (tag != TAG_INT || len != 1) { + return false; + } + + quint8 keyType; + stream.read(keyType); + + return (keyType == wantedType); + } + + bool readInt(BinaryStream& stream, QByteArray& target) + { + quint8 tag; + quint32 len; + + nextTag(stream, tag, len); + + if (tag != TAG_INT) { + return false; + } + + target.resize(len); + stream.read(target); + return true; + } + + QByteArray calculateIqmp(QByteArray& bap, QByteArray& baq) + { + gcry_mpi_t u, p, q; + QByteArray iqmp_hex; + + u = gcry_mpi_snew(bap.length() * 8); + gcry_mpi_scan(&p, GCRYMPI_FMT_HEX, bap.toHex().data(), 0, nullptr); + gcry_mpi_scan(&q, GCRYMPI_FMT_HEX, baq.toHex().data(), 0, nullptr); + + mpi_invm(u, q, p); + + iqmp_hex.resize((bap.length() + 1) * 2); + gcry_mpi_print(GCRYMPI_FMT_HEX, reinterpret_cast(iqmp_hex.data()), iqmp_hex.length(), nullptr, u); + + gcry_mpi_release(u); + gcry_mpi_release(p); + gcry_mpi_release(q); + + return QByteArray::fromHex(iqmp_hex); + } +} + +bool ASN1Key::parseDSA(QByteArray& ba, OpenSSHKey& key) +{ + BinaryStream stream(&ba); + + if (!parseHeader(stream, KEY_ZERO)) { + return false; + } + + QByteArray p,q,g,y,x; + readInt(stream, p); + readInt(stream, q); + readInt(stream, g); + readInt(stream, y); + readInt(stream, x); + + QList publicData; + publicData.append(p); + publicData.append(q); + publicData.append(g); + publicData.append(y); + + QList privateData; + privateData.append(p); + privateData.append(q); + privateData.append(g); + privateData.append(y); + privateData.append(x); + + key.setType("ssh-dss"); + key.setPublicData(publicData); + key.setPrivateData(privateData); + key.setComment(""); + return true; +} + +bool ASN1Key::parseRSA(QByteArray& ba, OpenSSHKey& key) +{ + BinaryStream stream(&ba); + + if (!parseHeader(stream, KEY_ZERO)) { + return false; + } + + QByteArray n,e,d,p,q,dp,dq,qinv; + readInt(stream, n); + readInt(stream, e); + readInt(stream, d); + readInt(stream, p); + readInt(stream, q); + readInt(stream, dp); + readInt(stream, dq); + readInt(stream, qinv); + + QList publicData; + publicData.append(e); + publicData.append(n); + + QList privateData; + privateData.append(n); + privateData.append(e); + privateData.append(d); + privateData.append(calculateIqmp(p, q)); + privateData.append(p); + privateData.append(q); + + key.setType("ssh-rsa"); + key.setPublicData(publicData); + key.setPrivateData(privateData); + key.setComment(""); + return true; +} diff --git a/src/sshagent/ASN1Key.h b/src/sshagent/ASN1Key.h new file mode 100644 index 000000000..59f8d4e81 --- /dev/null +++ b/src/sshagent/ASN1Key.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 Toni Spets + * Copyright (C) 2018 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 . + */ + +#ifndef ASN1KEY_H +#define ASN1KEY_H + +#include "OpenSSHKey.h" +#include + +namespace ASN1Key +{ + bool parseDSA(QByteArray& ba, OpenSSHKey& key); + bool parseRSA(QByteArray& ba, OpenSSHKey& key); +} + +#endif // ASN1KEY_H diff --git a/src/sshagent/CMakeLists.txt b/src/sshagent/CMakeLists.txt index 5d2ad6e3b..a612ff076 100644 --- a/src/sshagent/CMakeLists.txt +++ b/src/sshagent/CMakeLists.txt @@ -9,6 +9,7 @@ if(WITH_XC_SSHAGENT) BinaryStream.cpp KeeAgentSettings.cpp OpenSSHKey.cpp + ASN1Key.cpp SSHAgent.cpp ) diff --git a/src/sshagent/OpenSSHKey.cpp b/src/sshagent/OpenSSHKey.cpp index 2c51ee459..808408ab6 100644 --- a/src/sshagent/OpenSSHKey.cpp +++ b/src/sshagent/OpenSSHKey.cpp @@ -17,11 +17,16 @@ */ #include "OpenSSHKey.h" +#include "ASN1Key.h" #include #include #include #include "crypto/SymmetricCipher.h" +const QString OpenSSHKey::TYPE_DSA = "DSA PRIVATE KEY"; +const QString OpenSSHKey::TYPE_RSA = "RSA PRIVATE KEY"; +const QString OpenSSHKey::TYPE_OPENSSH = "OPENSSH PRIVATE KEY"; + // bcrypt_pbkdf.cpp int bcrypt_pbkdf(const QByteArray& pass, const QByteArray& salt, QByteArray& key, quint32 rounds); @@ -34,6 +39,7 @@ OpenSSHKey::OpenSSHKey(QObject *parent) , m_rawPrivateData(QByteArray()) , m_publicData(QList()) , m_privateData(QList()) + , m_privateType(QString()) , m_comment(QString()) , m_error(QString()) { @@ -178,14 +184,31 @@ bool OpenSSHKey::parsePEM(const QByteArray& in, QByteArray& out) return false; } - if (beginMatch.captured(1) != "OPENSSH PRIVATE KEY") { - m_error = tr("This is not an OpenSSH key, only modern keys are supported"); - return false; - } + m_privateType = beginMatch.captured(1); rows.removeFirst(); rows.removeLast(); + QRegularExpression keyValueExpr = QRegularExpression("^([A-Za-z0-9-]+): (.+)$"); + QMap pemOptions; + + do { + QRegularExpressionMatch keyValueMatch = keyValueExpr.match(rows.first()); + + if (!keyValueMatch.hasMatch()) { + break; + } + + pemOptions.insert(keyValueMatch.captured(1), keyValueMatch.captured(2)); + + rows.removeFirst(); + } while (!rows.isEmpty()); + + if (pemOptions.contains("Proc-Type")) { + m_error = tr("Encrypted keys are not yet supported"); + return false; + } + out = QByteArray::fromBase64(rows.join("").toLatin1()); if (out.isEmpty()) { @@ -204,51 +227,58 @@ bool OpenSSHKey::parse(const QByteArray& in) return false; } - BinaryStream stream(&data); + if (m_privateType == TYPE_DSA || m_privateType == TYPE_RSA) { + m_rawPrivateData = data; + } else if (m_privateType == TYPE_OPENSSH) { + BinaryStream stream(&data); - QByteArray magic; - magic.resize(15); + QByteArray magic; + magic.resize(15); - if (!stream.read(magic)) { - m_error = tr("Key file way too small."); - return false; - } - - if (QString::fromLatin1(magic) != "openssh-key-v1") { - m_error = tr("Key file magic header id invalid"); - return false; - } - - stream.readString(m_cipherName); - stream.readString(m_kdfName); - stream.readString(m_kdfOptions); - - quint32 numberOfKeys; - stream.read(numberOfKeys); - - if (numberOfKeys == 0) { - m_error = tr("Found zero keys"); - return false; - } - - for (quint32 i = 0; i < numberOfKeys; ++i) { - QByteArray publicKey; - if (!stream.readString(publicKey)) { - m_error = tr("Failed to read public key."); + if (!stream.read(magic)) { + m_error = tr("Key file way too small."); return false; } - if (i == 0) { - BinaryStream publicStream(&publicKey); - if (!readPublic(publicStream)) { + if (QString::fromLatin1(magic) != "openssh-key-v1") { + m_error = tr("Key file magic header id invalid"); + return false; + } + + stream.readString(m_cipherName); + stream.readString(m_kdfName); + stream.readString(m_kdfOptions); + + quint32 numberOfKeys; + stream.read(numberOfKeys); + + if (numberOfKeys == 0) { + m_error = tr("Found zero keys"); + return false; + } + + for (quint32 i = 0; i < numberOfKeys; ++i) { + QByteArray publicKey; + if (!stream.readString(publicKey)) { + m_error = tr("Failed to read public key."); return false; } - } - } - // padded list of keys - if (!stream.readString(m_rawPrivateData)) { - m_error = tr("Corrupted key file, reading private key failed"); + if (i == 0) { + BinaryStream publicStream(&publicKey); + if (!readPublic(publicStream)) { + return false; + } + } + } + + // padded list of keys + if (!stream.readString(m_rawPrivateData)) { + m_error = tr("Corrupted key file, reading private key failed"); + return false; + } + } else { + m_error = tr("Unsupported key type: %s").arg(m_privateType); return false; } @@ -283,7 +313,7 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) } else if (m_cipherName == "aes256-ctr") { cipher.reset(new SymmetricCipher(SymmetricCipher::Aes256, SymmetricCipher::Ctr, SymmetricCipher::Decrypt)); } else if (m_cipherName != "none") { - m_error = tr("Unknown cipher: ") + m_cipherName; + m_error = tr("Unknown cipher: %s").arg(m_cipherName); return false; } @@ -320,8 +350,13 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) ivData.setRawData(decryptKey.data() + cipher->keySize(), cipher->blockSize()); cipher->init(keyData, ivData); + + if (!cipher->init(keyData, ivData)) { + m_error = cipher->errorString(); + return false; + } } else if (m_kdfName != "none") { - m_error = tr("Unknown KDF: ") + m_kdfName; + m_error = tr("Unknown KDF: %s").arg(m_kdfName); return false; } @@ -336,20 +371,39 @@ bool OpenSSHKey::openPrivateKey(const QString& passphrase) } } - BinaryStream keyStream(&rawPrivateData); + if (m_privateType == TYPE_DSA) { + if (!ASN1Key::parseDSA(rawPrivateData, *this)) { + m_error = tr("Reading DSA private key failed, only unencrypted keys are supported at this time"); + return false; + } - quint32 checkInt1; - quint32 checkInt2; + return true; + } else if (m_privateType == TYPE_RSA) { + if (!ASN1Key::parseRSA(rawPrivateData, *this)) { + m_error = tr("Reading RSA private key failed, only unencrypted keys are supported at this time"); + return false; + } - keyStream.read(checkInt1); - keyStream.read(checkInt2); + return true; + } else if (m_privateType == TYPE_OPENSSH) { + BinaryStream keyStream(&rawPrivateData); - if (checkInt1 != checkInt2) { - m_error = tr("Decryption failed, wrong passphrase?"); - return false; + quint32 checkInt1; + quint32 checkInt2; + + keyStream.read(checkInt1); + keyStream.read(checkInt2); + + if (checkInt1 != checkInt2) { + m_error = tr("Decryption failed, wrong passphrase?"); + return false; + } + + return readPrivate(keyStream); } - return readPrivate(keyStream); + m_error = tr("Unsupported key type: %s").arg(m_privateType); + return false; } bool OpenSSHKey::readPublic(BinaryStream& stream) @@ -371,7 +425,7 @@ bool OpenSSHKey::readPublic(BinaryStream& stream) } else if (m_type == "ssh-ed25519") { keyParts = 1; } else { - m_error = tr("Unknown key type: ") + m_type; + m_error = tr("Unknown key type: %s").arg(m_type); return false; } @@ -408,7 +462,7 @@ bool OpenSSHKey::readPrivate(BinaryStream& stream) } else if (m_type == "ssh-ed25519") { keyParts = 2; } else { - m_error = tr("Unknown key type: ") + m_type; + m_error = tr("Unknown key type: %s").arg(m_type); return false; } diff --git a/src/sshagent/OpenSSHKey.h b/src/sshagent/OpenSSHKey.h index eca6c9edd..539d01892 100644 --- a/src/sshagent/OpenSSHKey.h +++ b/src/sshagent/OpenSSHKey.h @@ -55,6 +55,10 @@ public: bool writePrivate(BinaryStream& stream); private: + static const QString TYPE_DSA; + static const QString TYPE_RSA; + static const QString TYPE_OPENSSH; + bool parsePEM(const QByteArray& in, QByteArray& out); QString m_type; @@ -64,6 +68,7 @@ private: QByteArray m_rawPrivateData; QList m_publicData; QList m_privateData; + QString m_privateType; QString m_comment; QString m_error; }; diff --git a/src/sshagent/SSHAgent.cpp b/src/sshagent/SSHAgent.cpp index 2424bc97f..a65c6e4f1 100644 --- a/src/sshagent/SSHAgent.cpp +++ b/src/sshagent/SSHAgent.cpp @@ -260,6 +260,10 @@ void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) continue; } + if (key.comment().isEmpty()) { + key.setComment(e->username()); + } + if (settings.removeAtDatabaseClose()) { removeIdentityAtLock(key, uuid); } diff --git a/tests/TestOpenSSHKey.cpp b/tests/TestOpenSSHKey.cpp index 55033d7e1..7f94365e7 100644 --- a/tests/TestOpenSSHKey.cpp +++ b/tests/TestOpenSSHKey.cpp @@ -50,6 +50,7 @@ void TestOpenSSHKey::testParse() QCOMPARE(key.cipherName(), QString("none")); QCOMPARE(key.type(), QString("ssh-ed25519")); QCOMPARE(key.comment(), QString("opensshkey-test-parse@keepassxc")); + QCOMPARE(key.fingerprint(), QString("SHA256:D1fVmA15YXzaJ5sdO9dXxo5coHL/pnNaIfCvokHzTA4")); QByteArray publicKey, privateKey; BinaryStream publicStream(&publicKey), privateStream(&privateKey); @@ -61,6 +62,77 @@ void TestOpenSSHKey::testParse() QVERIFY(privateKey.length() == 154); } +void TestOpenSSHKey::testParseDSA() +{ + const QString keyString = QString( + "-----BEGIN DSA PRIVATE KEY-----\n" + "MIIBuwIBAAKBgQCudjbvSh8JxQOr2laCqZM1t4kNWBETVOXz5vgk9iw6Z5opB9/k\n" + "g4nFc1PVq7fdAIc8W/5WCAjugKcxPb9PIHfcwY2fimmiPWFK68/eHKLoCuIn2wxB\n" + "63ig2hAhx5U5aYG9QHkNCaT6VX7rc19nToSeZXlpja4x54/DaQaqOEWYsQIVAOer\n" + "UQWfccz7KXUu6+x7heGob6I3AoGAVDRFJIlL0DI/4nePIcgwgwbfgs2ojSu21g4w\n" + "dQoXvqU34XydPgPQ985XIIuiDkaomRw4yYd/Sh4ZapFcrP++iJ1V+WS6kLcWPHMq\n" + "poYwk8mq6GLbPFLEjr+n6HgX5ln15n3i4WAopNH7mEl0glY9L0rxmcN0XOpqw6Ux\n" + "ETGEfAwCgYAiOeYwblMkkTIGtVx5NvNsOlfrBYL4GqUP9oQMO5I+xLZLWQIf+7Jp\n" + "8t6mwxSBz0RHjNVQ11vZowNjq3587aLy57bVwf2lIm9KSvS6z9HoNbHgQimcBorR\n" + "J9l9RUrj7TnsZgiVw66j2r34nHRHRtggiO+qrMtw7MJc0Q7jiuTmzgIVAMXbk0T9\n" + "nBfSLWQz/L8RexU2GR4e\n" + "-----END DSA PRIVATE KEY-----\n" + ); + + const QByteArray keyData = keyString.toLatin1(); + + OpenSSHKey key; + QVERIFY(key.parse(keyData)); + QVERIFY(!key.encrypted()); + QCOMPARE(key.cipherName(), QString("none")); + QCOMPARE(key.type(), QString("ssh-dss")); + QCOMPARE(key.comment(), QString("")); + QCOMPARE(key.fingerprint(), QString("SHA256:tbbNuLN1hja8JNASDTlLOZQsbTlJDzJlz/oAGK3sX18")); +} + +void TestOpenSSHKey::testParseRSA() +{ + const QString keyString = QString( + "-----BEGIN RSA PRIVATE KEY-----\n" + "MIIEpAIBAAKCAQEAsCHtJicDPWnvHSIKbnTZaJkIB9vgE0pmLdK580JUqBuonVbB\n" + "y1QTy0ZQ7/TtqvLPgwPK88TR46OLO/QGCzo2+XxgJ85uy0xfuyUYRmSuw0drsErN\n" + "mH8vU91lSBxsGDp9LtBbgHKoR23vMWZ34IxFRc55XphrIH48ijsMaL6bXBwF/3tD\n" + "9T3lm2MpP1huyVNnIY9+GRRWCy4f9LMj/UGu/n4RtwwfpOZBBRwYkq5QkzA9lPm/\n" + "VzF3MP1rKTMkvAw+Nfb383mkmc6MRnsa6uh6iDa9aVB7naegM13UJQX/PY1Ks6pO\n" + "XDpy/MQ7iCh+HmYNq5dRmARyaNl9xIXJNhz1cQIDAQABAoIBAQCnEUc1LUQxeM5K\n" + "wANNCqE+SgoIClPdeHC7fmrLh1ttqe6ib6ybBUFRS31yXs0hnfefunVEDKlaV8K2\n" + "N52UAMAsngFHQNRvGh6kEWeZPd9Xc+N98TZbNCjcT+DGKc+Om8wqH5DrodZlCq4c\n" + "GaoT4HnE4TjWtZTH2XXrWF9I66PKFWf070R44nvyVcvaZi4pC2YmURRPuGF6K1iK\n" + "dH8zM6HHG1UGu2W6hLNn+K01IulG0Lb8eWNaNYMmtQWaxyp7I2IWkkecUs3nCuiR\n" + "byFOoomCjdh8r9yZFvwxjGUhgtkALN9GCU0Mwve+s11IB2gevruN+q9/Qejbyfdm\n" + "IlgLAeTRAoGBANRcVzW9CYeobCf+U9hKJFEOur8XO+J2mTMaELA0EjWpTJFAeIT7\n" + "KeRpCRG4/vOSklxxRF6vP1EACA4Z+5BlN+FTipHHs+bSEgqkPZiiANDH7Zot5Iqv\n" + "1q0fRyldNRZNZK7DWp08BPNVWGA/EnEuKJiURxnxBaxNXbUyMCdjxvMvAoGBANRT\n" + "utbrqS/bAa/DcHKn3V6DRqBl3TDOfvCNjiKC84a67F2uXgzLIdMktr4d1NyCZVJd\n" + "7/zVgWORLIdg1eAi6rYGoOvNV39wwga7CF+m9sBY0wAaKYCELe6L26r4aQHVCX6n\n" + "rnIgUv+4o4itmU2iP0r3wlmDC9pDRQP82vfvQPlfAoGASwhleANW/quvq2HdViq8\n" + "Mje2HBalfhrRfpDTHK8JUBSFjTzuWG42GxJRtgVbb8x2ElujAKGDCaetMO5VSGu7\n" + "Fs5hw6iAFCpdXY0yhl+XUi2R8kwM2EPQ4lKO3jqkq0ClNmqn9a5jQWcCVt9yMLNS\n" + "fLbHeI8EpiCf34ngIcrLXNkCgYEAzlcEZuKkC46xB+dNew8pMTUwSKZVm53BfPKD\n" + "44QRN6imFbBjU9mAaJnwQbfp6dWKs834cGPolyM4++MeVfB42iZ88ksesgmZdUMD\n" + "szkl6O0pOJs0I+HQZVdjRbadDZvD22MHQ3+oST1dJ3FVXz3Cdo9qPuT8esMO6f4r\n" + "qfDH2s8CgYAXC/lWWHQ//PGP0pH4oiEXisx1K0X1u0xMGgrChxBRGRiKZUwNMIvJ\n" + "TqUu7IKizK19cLHF/NBvxHYHFw+m7puNjn6T1RtRCUjRZT7Dx1VHfVosL9ih5DA8\n" + "tpbZA5KGKcvHtB5DDgT0MHwzBZnb4Q//Rhovzn+HXZPsJTTgHHy3NQ==\n" + "-----END RSA PRIVATE KEY-----\n" + ); + + const QByteArray keyData = keyString.toLatin1(); + + OpenSSHKey key; + QVERIFY(key.parse(keyData)); + QVERIFY(!key.encrypted()); + QCOMPARE(key.cipherName(), QString("none")); + QCOMPARE(key.type(), QString("ssh-rsa")); + QCOMPARE(key.comment(), QString("")); + QCOMPARE(key.fingerprint(), QString("SHA256:DYdaZciYNxCejr+/8x+OKYxeTU1D5UsuIFUG4PWRFkk")); +} + void TestOpenSSHKey::testDecryptAES256CBC() { const QString keyString = QString( diff --git a/tests/TestOpenSSHKey.h b/tests/TestOpenSSHKey.h index f2d6d1fb9..6c2041074 100644 --- a/tests/TestOpenSSHKey.h +++ b/tests/TestOpenSSHKey.h @@ -29,6 +29,8 @@ class TestOpenSSHKey : public QObject private slots: void initTestCase(); void testParse(); + void testParseDSA(); + void testParseRSA(); void testDecryptAES256CBC(); void testDecryptAES256CTR(); };