diff --git a/README.md b/README.md index 4cb956bf2..2b6fd0315 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # KeePassXC - KeePass Cross-platform Community Edition [![Travis Build Status](https://travis-ci.org/keepassxreboot/keepassxc.svg?branch=develop)](https://travis-ci.org/keepassxreboot/keepassxc) [![Coverage Status](https://coveralls.io/repos/github/keepassxreboot/keepassxc/badge.svg)](https://coveralls.io/github/keepassxreboot/keepassxc) +KeePassXC Authenticode Certificate Campaign! ## About KeePassXC is a community fork of [KeePassX](https://www.keepassx.org/) with the goal to extend and improve it with new features and bugfixes to provide a feature-rich, fully cross-platform and modern open-source password manager. diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 99f119f66..a2e72f7fd 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -254,6 +254,17 @@ bool Entry::isExpired() const return m_data.timeInfo.expires() && m_data.timeInfo.expiryTime() < QDateTime::currentDateTimeUtc(); } +bool Entry::hasReferences() const +{ + const QList keyList = EntryAttributes::DefaultAttributes; + for (const QString& key : keyList) { + if (m_attributes->isReference(key)) { + return true; + } + } + return false; +} + EntryAttributes* Entry::attributes() { return m_attributes; @@ -679,20 +690,20 @@ QString Entry::resolvePlaceholder(const QString& str) const // using format from http://keepass.info/help/base/fieldrefs.html at the time of writing, // but supporting lookups of standard fields and references by UUID only - QRegExp tmpRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2); - if (tmpRegExp.indexIn(result) != -1) { + QRegExp* tmpRegExp = m_attributes->referenceRegExp(); + if (tmpRegExp->indexIn(result) != -1) { // cap(0) contains the whole reference // cap(1) contains which field is wanted // cap(2) contains the uuid of the referenced entry - Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp.cap(2).toLatin1()))); + Entry* tmpRefEntry = m_group->database()->resolveEntry(Uuid(QByteArray::fromHex(tmpRegExp->cap(2).toLatin1()))); if (tmpRefEntry) { // entry found, get the relevant field - QString tmpRefField = tmpRegExp.cap(1).toLower(); - if (tmpRefField == "t") result.replace(tmpRegExp.cap(0), tmpRefEntry->title(), Qt::CaseInsensitive); - else if (tmpRefField == "u") result.replace(tmpRegExp.cap(0), tmpRefEntry->username(), Qt::CaseInsensitive); - else if (tmpRefField == "p") result.replace(tmpRegExp.cap(0), tmpRefEntry->password(), Qt::CaseInsensitive); - else if (tmpRefField == "a") result.replace(tmpRegExp.cap(0), tmpRefEntry->url(), Qt::CaseInsensitive); - else if (tmpRefField == "n") result.replace(tmpRegExp.cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive); + QString tmpRefField = tmpRegExp->cap(1).toLower(); + if (tmpRefField == "t") result.replace(tmpRegExp->cap(0), tmpRefEntry->title(), Qt::CaseInsensitive); + else if (tmpRefField == "u") result.replace(tmpRegExp->cap(0), tmpRefEntry->username(), Qt::CaseInsensitive); + else if (tmpRefField == "p") result.replace(tmpRegExp->cap(0), tmpRefEntry->password(), Qt::CaseInsensitive); + else if (tmpRefField == "a") result.replace(tmpRegExp->cap(0), tmpRefEntry->url(), Qt::CaseInsensitive); + else if (tmpRefField == "n") result.replace(tmpRegExp->cap(0), tmpRefEntry->notes(), Qt::CaseInsensitive); } } diff --git a/src/core/Entry.h b/src/core/Entry.h index d08c7217c..38ec42d4e 100644 --- a/src/core/Entry.h +++ b/src/core/Entry.h @@ -79,6 +79,7 @@ public: QString password() const; QString notes() const; bool isExpired() const; + bool hasReferences() const; EntryAttributes* attributes(); const EntryAttributes* attributes() const; EntryAttachments* attachments(); diff --git a/src/core/EntryAttributes.cpp b/src/core/EntryAttributes.cpp index b633cae32..865e853f2 100644 --- a/src/core/EntryAttributes.cpp +++ b/src/core/EntryAttributes.cpp @@ -28,6 +28,7 @@ const QString EntryAttributes::RememberCmdExecAttr = "_EXEC_CMD"; EntryAttributes::EntryAttributes(QObject* parent) : QObject(parent) + , m_referenceRegExp("\\{REF:([TUPAN])@I:([^}]+)\\}", Qt::CaseInsensitive, QRegExp::RegExp2) { clear(); } @@ -69,6 +70,25 @@ bool EntryAttributes::isProtected(const QString& key) const return m_protectedAttributes.contains(key); } +bool EntryAttributes::isReference(const QString& key) const +{ + if (!m_attributes.contains(key)) { + Q_ASSERT(false); + return false; + } + + QString data = value(key); + if (m_referenceRegExp.indexIn(data) != -1) { + return true; + } + return false; +} + +QRegExp* EntryAttributes::referenceRegExp() +{ + return &m_referenceRegExp; +} + void EntryAttributes::set(const QString& key, const QString& value, bool protect) { bool emitModified = false; diff --git a/src/core/EntryAttributes.h b/src/core/EntryAttributes.h index 211b6d483..78afe5efa 100644 --- a/src/core/EntryAttributes.h +++ b/src/core/EntryAttributes.h @@ -35,6 +35,8 @@ public: QString value(const QString& key) const; bool contains(const QString& key) const; bool isProtected(const QString& key) const; + bool isReference(const QString& key) const; + QRegExp* referenceRegExp(); void set(const QString& key, const QString& value, bool protect = false); void remove(const QString& key); void rename(const QString& oldKey, const QString& newKey); @@ -71,6 +73,7 @@ Q_SIGNALS: private: QMap m_attributes; QSet m_protectedAttributes; + QRegExp m_referenceRegExp; }; #endif // KEEPASSX_ENTRYATTRIBUTES_H diff --git a/src/gui/entry/EditEntryWidget.cpp b/src/gui/entry/EditEntryWidget.cpp index 51e0bb735..a30325057 100644 --- a/src/gui/entry/EditEntryWidget.cpp +++ b/src/gui/entry/EditEntryWidget.cpp @@ -132,6 +132,8 @@ void EditEntryWidget::setupAdvanced() connect(m_advancedUi->addAttributeButton, SIGNAL(clicked()), SLOT(insertAttribute())); connect(m_advancedUi->editAttributeButton, SIGNAL(clicked()), SLOT(editCurrentAttribute())); connect(m_advancedUi->removeAttributeButton, SIGNAL(clicked()), SLOT(removeCurrentAttribute())); + connect(m_advancedUi->protectAttributeButton, SIGNAL(toggled(bool)), SLOT(protectCurrentAttribute(bool))); + connect(m_advancedUi->revealAttributeButton, SIGNAL(clicked(bool)), SLOT(revealCurrentAttribute())); connect(m_advancedUi->attributesView->selectionModel(), SIGNAL(currentChanged(QModelIndex,QModelIndex)), SLOT(updateCurrentAttribute())); @@ -351,6 +353,11 @@ void EditEntryWidget::setForms(const Entry* entry, bool restore) m_advancedUi->attributesEdit->setEnabled(false); } + QList sizes = m_advancedUi->attributesSplitter->sizes(); + sizes.replace(0, m_advancedUi->attributesSplitter->width() * 0.3); + sizes.replace(1, m_advancedUi->attributesSplitter->width() * 0.7); + m_advancedUi->attributesSplitter->setSizes(sizes); + IconStruct iconStruct; iconStruct.uuid = entry->iconUuid(); iconStruct.number = entry->iconNumber(); @@ -409,7 +416,7 @@ void EditEntryWidget::saveEntry() return; } - if (m_advancedUi->attributesView->currentIndex().isValid()) { + if (m_advancedUi->attributesView->currentIndex().isValid() && m_advancedUi->attributesEdit->isEnabled()) { QString key = m_attributesModel->keyByIndex(m_advancedUi->attributesView->currentIndex()); m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), m_entryAttributes->isProtected(key)); @@ -578,46 +585,96 @@ void EditEntryWidget::removeCurrentAttribute() QModelIndex index = m_advancedUi->attributesView->currentIndex(); if (index.isValid()) { - m_entryAttributes->remove(m_attributesModel->keyByIndex(index)); + if (MessageBox::question(this, tr("Confirm Remove"), tr("Are you sure you want to remove this attribute?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_entryAttributes->remove(m_attributesModel->keyByIndex(index)); + } } } void EditEntryWidget::updateCurrentAttribute() { QModelIndex newIndex = m_advancedUi->attributesView->currentIndex(); + QString newKey = m_attributesModel->keyByIndex(newIndex); - if (m_history) { - if (newIndex.isValid()) { - QString key = m_attributesModel->keyByIndex(newIndex); - m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key)); - m_advancedUi->attributesEdit->setEnabled(true); + if (!m_history && m_currentAttribute != newIndex) { + // Save changes to the currently selected attribute if editing is enabled + if (m_currentAttribute.isValid() && m_advancedUi->attributesEdit->isEnabled()) { + QString currKey = m_attributesModel->keyByIndex(m_currentAttribute); + m_entryAttributes->set(currKey, m_advancedUi->attributesEdit->toPlainText(), + m_entryAttributes->isProtected(currKey)); + } + } + + displayAttribute(newIndex, m_entryAttributes->isProtected(newKey)); + + m_currentAttribute = newIndex; +} + +void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected) +{ + // Block signals to prevent extra calls + m_advancedUi->protectAttributeButton->blockSignals(true); + + if (index.isValid()) { + QString key = m_attributesModel->keyByIndex(index); + if (showProtected) { + m_advancedUi->attributesEdit->setPlainText(tr("[PROTECTED] Press reveal to view or edit")); + m_advancedUi->attributesEdit->setEnabled(false); + m_advancedUi->revealAttributeButton->setEnabled(true); + m_advancedUi->protectAttributeButton->setChecked(true); } else { - m_advancedUi->attributesEdit->setPlainText(""); - m_advancedUi->attributesEdit->setEnabled(false); + m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key)); + m_advancedUi->attributesEdit->setEnabled(true); + m_advancedUi->revealAttributeButton->setEnabled(false); + m_advancedUi->protectAttributeButton->setChecked(false); } + + // Don't allow editing in history view + m_advancedUi->protectAttributeButton->setEnabled(!m_history); + m_advancedUi->editAttributeButton->setEnabled(!m_history); + m_advancedUi->removeAttributeButton->setEnabled(!m_history); } else { - if (m_currentAttribute != newIndex) { - if (m_currentAttribute.isValid()) { - QString key = m_attributesModel->keyByIndex(m_currentAttribute); - m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), - m_entryAttributes->isProtected(key)); - } + m_advancedUi->attributesEdit->setPlainText(""); + m_advancedUi->attributesEdit->setEnabled(false); + m_advancedUi->revealAttributeButton->setEnabled(false); + m_advancedUi->protectAttributeButton->setChecked(false); + m_advancedUi->protectAttributeButton->setEnabled(false); + m_advancedUi->editAttributeButton->setEnabled(false); + m_advancedUi->removeAttributeButton->setEnabled(false); + } - if (newIndex.isValid()) { - QString key = m_attributesModel->keyByIndex(newIndex); - m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key)); - m_advancedUi->attributesEdit->setEnabled(true); - } - else { - m_advancedUi->attributesEdit->setPlainText(""); - m_advancedUi->attributesEdit->setEnabled(false); - } + m_advancedUi->protectAttributeButton->blockSignals(false); +} - m_advancedUi->editAttributeButton->setEnabled(newIndex.isValid()); - m_advancedUi->removeAttributeButton->setEnabled(newIndex.isValid()); - m_currentAttribute = newIndex; +void EditEntryWidget::protectCurrentAttribute(bool state) +{ + QModelIndex index = m_advancedUi->attributesView->currentIndex(); + if (!m_history && index.isValid()) { + QString key = m_attributesModel->keyByIndex(index); + if (state) { + // Save the current text and protect the attribute + m_entryAttributes->set(key, m_advancedUi->attributesEdit->toPlainText(), true); + } else { + // Unprotect the current attribute value (don't save text as it is obscured) + m_entryAttributes->set(key, m_entryAttributes->value(key), false); + } + + // Display the attribute + displayAttribute(index, state); + } +} + +void EditEntryWidget::revealCurrentAttribute() +{ + if (! m_advancedUi->attributesEdit->isEnabled()) { + QModelIndex index = m_advancedUi->attributesView->currentIndex(); + if (index.isValid()) { + QString key = m_attributesModel->keyByIndex(index); + m_advancedUi->attributesEdit->setPlainText(m_entryAttributes->value(key)); + m_advancedUi->attributesEdit->setEnabled(true); } } } @@ -730,8 +787,13 @@ void EditEntryWidget::removeCurrentAttachment() return; } - QString key = m_attachmentsModel->keyByIndex(index); - m_entryAttachments->remove(key); + QMessageBox::StandardButton ans = MessageBox::question(this, tr("Confirm Remove"), + tr("Are you sure you want to remove this attachment?"), + QMessageBox::Yes | QMessageBox::No); + if (ans == QMessageBox::Yes) { + QString key = m_attachmentsModel->keyByIndex(index); + m_entryAttachments->remove(key); + } } void EditEntryWidget::updateAutoTypeEnabled() diff --git a/src/gui/entry/EditEntryWidget.h b/src/gui/entry/EditEntryWidget.h index c8045d93c..270542e8c 100644 --- a/src/gui/entry/EditEntryWidget.h +++ b/src/gui/entry/EditEntryWidget.h @@ -76,6 +76,8 @@ private Q_SLOTS: void editCurrentAttribute(); void removeCurrentAttribute(); void updateCurrentAttribute(); + void protectCurrentAttribute(bool state); + void revealCurrentAttribute(); void insertAttachment(); void saveCurrentAttachment(); void openAttachment(const QModelIndex& index); @@ -110,6 +112,8 @@ private: QMenu* createPresetsMenu(); void updateEntryData(Entry* entry) const; + void displayAttribute(QModelIndex index, bool showProtected); + Entry* m_entry; Database* m_database; diff --git a/src/gui/entry/EditEntryWidgetAdvanced.ui b/src/gui/entry/EditEntryWidgetAdvanced.ui index 61380bb90..2c7f95dde 100644 --- a/src/gui/entry/EditEntryWidgetAdvanced.ui +++ b/src/gui/entry/EditEntryWidgetAdvanced.ui @@ -28,19 +28,50 @@ Additional attributes - + - - - - - + + + Qt::Horizontal + + false + + + + 0 + 0 + + + + QAbstractScrollArea::AdjustToContents + + + QListView::Adjust + + + + + false + + + + 0 + 0 + + + + + 170 + 0 + + + - + @@ -48,16 +79,6 @@ - - - - false - - - Edit - - - @@ -68,6 +89,16 @@ + + + + false + + + Edit Name + + + @@ -81,6 +112,35 @@ + + + + true + + + margin-left:50%;margin-right:50% + + + Protect + + + true + + + + + + + false + + + Reveal + + + false + + + @@ -103,7 +163,7 @@ - + @@ -172,11 +232,12 @@ attributesView attributesEdit addAttributeButton - editAttributeButton removeAttributeButton + editAttributeButton attachmentsView addAttachmentButton removeAttachmentButton + openAttachmentButton saveAttachmentButton diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index d606a777e..323a55c82 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -19,6 +19,7 @@ #include #include +#include #include "core/DatabaseIcons.h" #include "core/Entry.h" @@ -127,8 +128,10 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } Entry* entry = entryFromIndex(index); + EntryAttributes* attr = entry->attributes(); if (role == Qt::DisplayRole) { + QString result; switch (index.column()) { case ParentGroup: if (entry->group()) { @@ -136,11 +139,23 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } break; case Title: - return entry->title(); + result = entry->resolvePlaceholder(entry->title()); + if (attr->isReference(EntryAttributes::TitleKey)) { + result.prepend(tr("Ref: ","Reference abbreviation")); + } + return result; case Username: - return entry->username(); + result = entry->resolvePlaceholder(entry->username()); + if (attr->isReference(EntryAttributes::UserNameKey)) { + result.prepend(tr("Ref: ","Reference abbreviation")); + } + return result; case Url: - return entry->url(); + result = entry->resolvePlaceholder(entry->url()); + if (attr->isReference(EntryAttributes::URLKey)) { + result.prepend(tr("Ref: ","Reference abbreviation")); + } + return result; } } else if (role == Qt::DecorationRole) { @@ -166,6 +181,12 @@ QVariant EntryModel::data(const QModelIndex& index, int role) const } return font; } + else if (role == Qt::TextColorRole) { + if (entry->hasReferences()) { + QPalette p; + return QVariant(p.color(QPalette::Active, QPalette::Mid)); + } + } return QVariant(); } diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index d5a16ebab..e0c8bb490 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -181,6 +181,12 @@ void TestEntryModel::testAttributesModel() QCOMPARE(spyAboutToRemove.count(), 1); QCOMPARE(spyRemoved.count(), 1); + // test attribute protection + QString value = entryAttributes->value("2nd"); + entryAttributes->set("2nd", value, true); + QVERIFY(entryAttributes->isProtected("2nd")); + QCOMPARE(entryAttributes->value("2nd"), value); + QSignalSpy spyReset(model, SIGNAL(modelReset())); entryAttributes->clear(); model->setEntryAttributes(0); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 3893038ba..7df5942e8 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -242,6 +243,19 @@ void TestGui::testEditEntry() QLineEdit* titleEdit = editEntryWidget->findChild("titleEdit"); QTest::keyClicks(titleEdit, "_test"); + // Test protected attributes + editEntryWidget->setCurrentPage(1); + QPlainTextEdit* attrTextEdit = editEntryWidget->findChild("attributesEdit"); + QTest::mouseClick(editEntryWidget->findChild("addAttributeButton"), Qt::LeftButton); + QString attrText = "TEST TEXT"; + QTest::keyClicks(attrTextEdit, attrText); + QCOMPARE(attrTextEdit->toPlainText(), attrText); + QTest::mouseClick(editEntryWidget->findChild("protectAttributeButton"), Qt::LeftButton); + QVERIFY(attrTextEdit->toPlainText().contains("PROTECTED")); + QTest::mouseClick(editEntryWidget->findChild("revealAttributeButton"), Qt::LeftButton); + QCOMPARE(attrTextEdit->toPlainText(), attrText); + editEntryWidget->setCurrentPage(0); + // Save the edit QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);