Merge pull request #4232 from Aetf/feature/fdo-secrets-unittests

FdoSecrets: Add unit tests for secret service integration
This commit is contained in:
Aetf 2020-05-28 21:37:42 -04:00 committed by GitHub
commit 229a756d84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 1706 additions and 65 deletions

View File

@ -21,8 +21,9 @@
#include "core/Group.h"
#include "core/Tools.h"
EntrySearcher::EntrySearcher(bool caseSensitive)
EntrySearcher::EntrySearcher(bool caseSensitive, bool skipProtected)
: m_caseSensitive(caseSensitive)
, m_skipProtected(skipProtected)
, m_termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re")
// Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string
{
@ -136,7 +137,7 @@ void EntrySearcher::setCaseSensitive(bool state)
m_caseSensitive = state;
}
bool EntrySearcher::isCaseSensitive()
bool EntrySearcher::isCaseSensitive() const
{
return m_caseSensitive;
}
@ -150,7 +151,9 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
// Build a group hierarchy to allow searching for e.g. /group1/subgroup*
auto hierarchy = entry->group()->hierarchy().join('/').prepend("/");
bool found;
// By default, empty term matches every entry.
// However when skipping protected fields, we will recject everything instead
bool found = !m_skipProtected;
for (const auto& term : m_searchTerms) {
switch (term.field) {
case Field::Title:
@ -160,6 +163,9 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
found = term.regex.match(entry->resolvePlaceholder(entry->username())).hasMatch();
break;
case Field::Password:
if (m_skipProtected) {
continue;
}
found = term.regex.match(entry->resolvePlaceholder(entry->password())).hasMatch();
break;
case Field::Url:
@ -175,8 +181,7 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
found = !attachments.filter(term.regex).empty();
break;
case Field::AttributeValue:
// skip protected attributes
if (entry->attributes()->isProtected(term.word)) {
if (m_skipProtected && entry->attributes()->isProtected(term.word)) {
continue;
}
found = entry->attributes()->contains(term.word)
@ -198,13 +203,18 @@ bool EntrySearcher::searchEntryImpl(const Entry* entry)
|| term.regex.match(entry->notes()).hasMatch();
}
// Short circuit if we failed to match or we matched and are excluding this term
if ((!found && !term.exclude) || (found && term.exclude)) {
// negate the result if exclude:
// * if found and not excluding, the entry matches
// * if didn't found but excluding, the entry also matches
found = (found && !term.exclude) || (!found && term.exclude);
// short circuit if we failed the match
if (!found) {
return false;
}
}
return true;
return found;
}
void EntrySearcher::parseSearchTerms(const QString& searchString)

View File

@ -51,7 +51,7 @@ public:
bool exclude;
};
explicit EntrySearcher(bool caseSensitive = false);
explicit EntrySearcher(bool caseSensitive = false, bool skipProtected = false);
QList<Entry*> search(const QList<SearchTerm>& searchTerms, const Group* baseGroup, bool forceSearch = false);
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
@ -62,13 +62,14 @@ public:
QList<Entry*> repeatEntries(const QList<Entry*>& entries);
void setCaseSensitive(bool state);
bool isCaseSensitive();
bool isCaseSensitive() const;
private:
bool searchEntryImpl(const Entry* entry);
void parseSearchTerms(const QString& searchString);
bool m_caseSensitive;
bool m_skipProtected;
QRegularExpression m_termParser;
QList<SearchTerm> m_searchTerms;

View File

@ -31,7 +31,8 @@
using FdoSecrets::Service;
FdoSecretsPlugin::FdoSecretsPlugin(DatabaseTabWidget* tabWidget)
: m_dbTabs(tabWidget)
: QObject(tabWidget)
, m_dbTabs(tabWidget)
{
FdoSecrets::registerDBusTypes();
}

View File

@ -75,6 +75,8 @@ namespace FdoSecrets
m_registered = false;
}
Q_ASSERT(m_backend);
// make sure we have updated copy of the filepath, which is used to identify the database.
m_backendPath = m_backend->database()->filePath();
@ -245,19 +247,11 @@ namespace FdoSecrets
QList<EntrySearcher::SearchTerm> terms;
for (auto it = attributes.constBegin(); it != attributes.constEnd(); ++it) {
if (it.key() == EntryAttributes::PasswordKey) {
continue;
}
terms << attributeToTerm(it.key(), it.value());
}
// empty terms causes EntrySearcher returns everything
if (terms.isEmpty()) {
return QList<Item*>{};
}
QList<Item*> items;
const auto foundEntries = EntrySearcher().search(terms, m_exposedGroup);
const auto foundEntries = EntrySearcher(false, true).search(terms, m_exposedGroup);
items.reserve(foundEntries.size());
for (const auto& entry : foundEntries) {
items << m_entryToItem.value(entry);
@ -310,13 +304,13 @@ namespace FdoSecrets
QString itemPath;
StringStringMap attributes;
// check existing item using attributes
auto iterAttr = properties.find(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes"));
if (iterAttr != properties.end()) {
attributes = qdbus_cast<StringStringMap>(iterAttr.value().value<QDBusArgument>());
attributes = iterAttr.value().value<StringStringMap>();
itemPath = attributes.value(ItemAttributes::PathKey);
// check existing item using attributes
auto existings = searchItems(attributes);
if (existings.isError()) {
return existings;
@ -440,6 +434,16 @@ namespace FdoSecrets
QString Collection::name() const
{
if (m_backendPath.isEmpty()) {
// This is a newly created db without saving to file.
// This name is also used to register dbus path.
// For simplicity, we don't monitor the name change.
// So the dbus object path is not updated if the db name
// changes. This should not be a problem because once the database
// gets saved, the dbus path will be updated to use filename and
// everything back to normal.
return m_backend->database()->metadata()->name();
}
return QFileInfo(m_backendPath).baseName();
}
@ -544,9 +548,10 @@ namespace FdoSecrets
}
// repopulate
Q_ASSERT(!backendLocked());
if (!backendLocked()) {
populateContents();
}
}
void Collection::onEntryAdded(Entry* entry, bool emitSignal)
{
@ -598,11 +603,11 @@ namespace FdoSecrets
return qobject_cast<Service*>(parent());
}
void Collection::doLock()
bool Collection::doLock()
{
Q_ASSERT(m_backend);
m_backend->lock();
return m_backend->lock();
}
void Collection::doUnlock()
@ -614,6 +619,11 @@ namespace FdoSecrets
void Collection::doDelete()
{
if (!m_backend) {
// I'm already deleted
return;
}
emit collectionAboutToDelete();
unregisterCurrentPath();
@ -623,7 +633,11 @@ namespace FdoSecrets
removeAlias(a).okOrDie();
}
// cleanup connection on Database
cleanupConnections();
// cleanup connection on Backend itself
m_backend->disconnect(this);
parent()->disconnect(this);
m_exposedGroup = nullptr;

View File

@ -59,9 +59,9 @@ namespace FdoSecrets
createItem(const QVariantMap& properties, const SecretStruct& secret, bool replace, PromptBase*& prompt);
signals:
void itemCreated(const Item* item);
void itemDeleted(const Item* item);
void itemChanged(const Item* item);
void itemCreated(Item* item);
void itemDeleted(Item* item);
void itemChanged(Item* item);
void collectionChanged();
void collectionAboutToDelete();
@ -102,7 +102,7 @@ namespace FdoSecrets
public slots:
// expose some methods for Prmopt to use
void doLock();
bool doLock();
void doUnlock();
// will remove self
void doDelete();

View File

@ -51,6 +51,11 @@ namespace FdoSecrets
return m_objectPath;
}
QDBusAbstractAdaptor& dbusAdaptor() const
{
return *m_dbusAdaptor;
}
protected:
void registerWithPath(const QString& path, QDBusAbstractAdaptor* adaptor);
@ -74,11 +79,6 @@ namespace FdoSecrets
QString callingPeerName() const;
template <typename Adaptor> Adaptor& dbusAdaptor() const
{
return *static_cast<Adaptor*>(m_dbusAdaptor);
}
DBusObject* p() const
{
return qobject_cast<DBusObject*>(parent());

View File

@ -35,6 +35,16 @@ namespace FdoSecrets
qRegisterMetaType<ObjectPathSecretMap>();
qDBusRegisterMetaType<ObjectPathSecretMap>();
QMetaType::registerConverter<QDBusArgument, StringStringMap>([](const QDBusArgument& arg) {
if (arg.currentSignature() != "a{ss}") {
return StringStringMap{};
}
// QDBusArgument is COW and qdbus_cast modifies it by detaching even it is const.
// we don't want to modify the instance (arg) stored in the qvariant so we create a copy
const auto copy = arg; // NOLINT(performance-unnecessary-copy-initialization)
return qdbus_cast<StringStringMap>(copy);
});
// NOTE: this is already registered by Qt in qtextratypes.h
// qRegisterMetaType<QList<QDBusObjectPath > >();
// qDBusRegisterMetaType<QList<QDBusObjectPath> >();

View File

@ -26,6 +26,7 @@
#include "core/Entry.h"
#include "core/EntryAttributes.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "core/Tools.h"
#include <QMimeDatabase>
@ -274,8 +275,8 @@ namespace FdoSecrets
return ret;
}
auto attributes = qdbus_cast<StringStringMap>(
properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value<QDBusArgument>());
auto attributes =
properties.value(QStringLiteral(DBUS_INTERFACE_SECRET_ITEM ".Attributes")).value<StringStringMap>();
ret = setAttributes(attributes);
if (ret.isError()) {
return ret;
@ -350,6 +351,13 @@ namespace FdoSecrets
return pathComponents.join('/');
}
bool Item::isDeletePermanent() const
{
auto recycleBin = backend()->database()->metadata()->recycleBin();
return (recycleBin && recycleBin->findEntryByUuid(backend()->uuid()))
|| !backend()->database()->metadata()->recycleBinEnabled();
}
void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType)
{
auto mimeName = contentType.split(';').takeFirst().trimmed();
@ -369,7 +377,8 @@ namespace FdoSecrets
}
if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) {
// we can't handle this content type, save the data as attachment
// we can't handle this content type, save the data as attachment, and clear the password field
entry->setPassword("");
entry->attachments()->set(FDO_SECRETS_DATA, data);
entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType);
return;
@ -393,8 +402,13 @@ namespace FdoSecrets
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
ss.value = entry->attachments()->value(FDO_SECRETS_DATA);
Q_ASSERT(entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE));
if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
ss.contentType = entry->attributes()->value(FDO_SECRETS_CONTENT_TYPE);
} else {
// the entry is somehow corrupted, maybe the user deleted it.
// set to binary and hope for the best...
ss.contentType = QStringLiteral("application/octet-stream");
}
return ss;
}

View File

@ -79,6 +79,14 @@ namespace FdoSecrets
*/
QString path() const;
/**
* If the containing db does not have recycle bin enabled,
* or the entry is already in the recycle bin (not possible for item, though),
* the delete is permanent
* @return true if delete is permanent
*/
bool isDeletePermanent() const;
public slots:
void doDelete();

View File

@ -65,7 +65,7 @@ namespace FdoSecrets
DBusReturn<void> PromptBase::dismiss()
{
emit completed(true, {});
emit completed(true, "");
return {};
}
@ -95,7 +95,7 @@ namespace FdoSecrets
// only need to delete in backend, collection will react itself.
auto accepted = service()->doCloseDatabase(m_collection->backend());
emit completed(!accepted, {});
emit completed(!accepted, "");
return {};
}
@ -125,11 +125,17 @@ namespace FdoSecrets
}
emit collectionCreated(coll);
emit completed(false, coll->objectPath().path());
emit completed(false, QVariant::fromValue(coll->objectPath()));
return {};
}
DBusReturn<void> CreateCollectionPrompt::dismiss()
{
emit completed(true, QVariant::fromValue(QDBusObjectPath{"/"}));
return {};
}
LockCollectionsPrompt::LockCollectionsPrompt(Service* parent, const QList<Collection*>& colls)
: PromptBase(parent)
{
@ -153,19 +159,26 @@ namespace FdoSecrets
MessageBox::OverrideParent override(findWindow(windowId));
QList<QDBusObjectPath> locked;
for (const auto& c : asConst(m_collections)) {
if (c) {
c->doLock();
locked << c->objectPath();
if (!c->doLock()) {
return dismiss();
}
m_locked << c->objectPath();
}
}
emit completed(false, QVariant::fromValue(locked));
emit completed(false, QVariant::fromValue(m_locked));
return {};
}
DBusReturn<void> LockCollectionsPrompt::dismiss()
{
emit completed(true, QVariant::fromValue(m_locked));
return {};
}
UnlockCollectionsPrompt::UnlockCollectionsPrompt(Service* parent, const QList<Collection*>& colls)
: PromptBase(parent)
{
@ -191,6 +204,7 @@ namespace FdoSecrets
for (const auto& c : asConst(m_collections)) {
if (c) {
// doUnlock is nonblocking
connect(c, &Collection::doneUnlockCollection, this, &UnlockCollectionsPrompt::collectionUnlockFinished);
c->doUnlock();
}
@ -226,6 +240,11 @@ namespace FdoSecrets
emit completed(m_unlocked.isEmpty(), QVariant::fromValue(m_unlocked));
}
}
DBusReturn<void> UnlockCollectionsPrompt::dismiss()
{
emit completed(true, QVariant::fromValue(m_unlocked));
return {};
}
DeleteItemPrompt::DeleteItemPrompt(Service* parent, Item* item)
: PromptBase(parent)
@ -250,14 +269,18 @@ namespace FdoSecrets
// delete item's backend. Item will be notified after the backend is deleted.
if (m_item) {
if (FdoSecrets::settings()->noConfirmDeleteItem()) {
// based on permanent or not, different button is used
if (m_item->isDeletePermanent()) {
MessageBox::setNextAnswer(MessageBox::Delete);
} else {
MessageBox::setNextAnswer(MessageBox::Move);
}
}
m_item->collection()->doDeleteEntries({m_item->backend()});
}
emit completed(false, {});
emit completed(false, "");
return {};
}
} // namespace FdoSecrets

View File

@ -73,6 +73,7 @@ namespace FdoSecrets
explicit CreateCollectionPrompt(Service* parent);
DBusReturn<void> prompt(const QString& windowId) override;
DBusReturn<void> dismiss() override;
signals:
void collectionCreated(Collection* coll);
@ -85,9 +86,11 @@ namespace FdoSecrets
explicit LockCollectionsPrompt(Service* parent, const QList<Collection*>& colls);
DBusReturn<void> prompt(const QString& windowId) override;
DBusReturn<void> dismiss() override;
private:
QList<QPointer<Collection>> m_collections;
QList<QDBusObjectPath> m_locked;
};
class UnlockCollectionsPrompt : public PromptBase
@ -97,6 +100,7 @@ namespace FdoSecrets
explicit UnlockCollectionsPrompt(Service* parent, const QList<Collection*>& coll);
DBusReturn<void> prompt(const QString& windowId) override;
DBusReturn<void> dismiss() override;
private slots:
void collectionUnlockFinished(bool accepted);

View File

@ -68,9 +68,9 @@ namespace FdoSecrets
incomplete = false;
std::unique_ptr<CipherPair> cipher{};
if (algorithm == QLatin1Literal("plain")) {
if (algorithm == QLatin1String(PlainCipher::Algorithm)) {
cipher.reset(new PlainCipher);
} else if (algorithm == QLatin1Literal("dh-ietf1024-sha256-aes128-cbc-pkcs7")) {
} else if (algorithm == QLatin1String(DhIetf1024Sha256Aes128CbcPkcs7::Algorithm)) {
QByteArray clientPublicKey = input.toByteArray();
cipher.reset(new DhIetf1024Sha256Aes128CbcPkcs7(clientPublicKey));
} else {

View File

@ -43,6 +43,9 @@ namespace
namespace FdoSecrets
{
// XXX: remove the redundant definitions once we are at C++17
constexpr char PlainCipher::Algorithm[];
constexpr char DhIetf1024Sha256Aes128CbcPkcs7::Algorithm[];
DhIetf1024Sha256Aes128CbcPkcs7::DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes)
: m_valid(false)
@ -51,13 +54,24 @@ namespace FdoSecrets
auto clientPub = MpiFromBytes(clientPublicKeyBytes, false);
// generate server side private, 128 bytes
GcryptMPI serverPrivate(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
GcryptMPI serverPrivate = nullptr;
if (NextPrivKey) {
serverPrivate = std::move(NextPrivKey);
} else {
serverPrivate.reset(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
gcry_mpi_randomize(serverPrivate.get(), KEY_SIZE_BYTES * 8, GCRY_STRONG_RANDOM);
}
// generate server side public key
GcryptMPI serverPublic(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
GcryptMPI serverPublic = nullptr;
if (NextPubKey) {
serverPublic = std::move(NextPubKey);
} else {
serverPublic.reset(gcry_mpi_snew(KEY_SIZE_BYTES * 8));
// the generator of Second Oakley Group is 2
gcry_mpi_powm(serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
gcry_mpi_powm(
serverPublic.get(), GCRYMPI_CONST_TWO, serverPrivate.get(), IETF1024_SECOND_OAKLEY_GROUP_P.get());
}
initialize(std::move(clientPub), std::move(serverPublic), std::move(serverPrivate));
}
@ -216,4 +230,13 @@ namespace FdoSecrets
return m_publicKey;
}
void DhIetf1024Sha256Aes128CbcPkcs7::fixNextServerKeys(GcryptMPI priv, GcryptMPI pub)
{
NextPrivKey = std::move(priv);
NextPubKey = std::move(pub);
}
GcryptMPI DhIetf1024Sha256Aes128CbcPkcs7::NextPrivKey = nullptr;
GcryptMPI DhIetf1024Sha256Aes128CbcPkcs7::NextPubKey = nullptr;
} // namespace FdoSecrets

View File

@ -22,6 +22,7 @@
#include "fdosecrets/objects/Session.h"
class TestFdoSecrets;
class TestGuiFdoSecrets;
namespace FdoSecrets
{
@ -42,6 +43,8 @@ namespace FdoSecrets
{
Q_DISABLE_COPY(PlainCipher)
public:
static constexpr const char Algorithm[] = "plain";
PlainCipher() = default;
SecretStruct encrypt(const SecretStruct& input) override
{
@ -113,6 +116,8 @@ namespace FdoSecrets
}
public:
static constexpr const char Algorithm[] = "dh-ietf1024-sha256-aes128-cbc-pkcs7";
explicit DhIetf1024Sha256Aes128CbcPkcs7(const QByteArray& clientPublicKeyBytes);
SecretStruct encrypt(const SecretStruct& input) override;
@ -123,9 +128,18 @@ namespace FdoSecrets
QVariant negotiationOutput() const override;
private:
/**
* For test only, fix the server side private and public key.
*/
static void fixNextServerKeys(GcryptMPI priv, GcryptMPI pub);
static GcryptMPI NextPrivKey;
static GcryptMPI NextPubKey;
private:
Q_DISABLE_COPY(DhIetf1024Sha256Aes128CbcPkcs7);
friend class ::TestFdoSecrets;
friend class ::TestGuiFdoSecrets;
};
} // namespace FdoSecrets

View File

@ -335,9 +335,7 @@ namespace FdoSecrets
{
switch (role) {
case Qt::EditRole: {
auto v = QVariant::fromValue(sess);
qDebug() << v << v.type() << v.userType();
return v;
return QVariant::fromValue(sess);
}
default:
return {};

View File

@ -49,7 +49,7 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
: QTabWidget(parent)
, m_dbWidgetStateSync(new DatabaseWidgetStateSync(this))
, m_dbWidgetPendingLock(nullptr)
, m_databaseOpenDialog(new DatabaseOpenDialog())
, m_databaseOpenDialog(new DatabaseOpenDialog(this))
{
auto* tabBar = new DragTabBar(this);
setTabBar(tabBar);

View File

@ -108,7 +108,7 @@ private:
QPointer<DatabaseWidgetStateSync> m_dbWidgetStateSync;
QPointer<DatabaseWidget> m_dbWidgetPendingLock;
QScopedPointer<DatabaseOpenDialog> m_databaseOpenDialog;
QPointer<DatabaseOpenDialog> m_databaseOpenDialog;
};
#endif // KEEPASSX_DATABASETABWIDGET_H

View File

@ -57,8 +57,9 @@ macro(parse_arguments prefix arg_names option_names)
endmacro(parse_arguments)
macro(add_unit_test)
parse_arguments(TEST "NAME;SOURCES;LIBS" "" ${ARGN})
parse_arguments(TEST "NAME;SOURCES;LIBS;LAUNCHER" "" ${ARGN})
set(_test_NAME ${TEST_NAME})
set(_test_LAUNCHER ${TEST_LAUNCHER})
set(_srcList ${TEST_SOURCES})
add_executable(${_test_NAME} ${_srcList})
target_link_libraries(${_test_NAME} ${TEST_LIBS})
@ -69,9 +70,9 @@ macro(add_unit_test)
set(TEST_OUTPUT ${TEST_OUTPUT} CACHE STRING "The output to generate when running the QTest unit tests")
if(KDE4_TEST_OUTPUT STREQUAL "xml")
add_test(${_test_NAME} ${_test_NAME} -xml -o ${_test_NAME}.tml)
add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME} -xml -o ${_test_NAME}.tml)
else(KDE4_TEST_OUTPUT STREQUAL "xml")
add_test(${_test_NAME} ${_test_NAME})
add_test(${_test_NAME} ${_test_LAUNCHER} ${_test_NAME})
endif(KDE4_TEST_OUTPUT STREQUAL "xml")
if(NOT MSVC_IDE) #not needed for the ide

View File

@ -23,6 +23,7 @@ QTEST_GUILESS_MAIN(TestEntrySearcher)
void TestEntrySearcher::init()
{
m_rootGroup = new Group();
m_entrySearcher = EntrySearcher();
}
void TestEntrySearcher::cleanup()
@ -259,6 +260,7 @@ void TestEntrySearcher::testCustomAttributesAreSearched()
QCOMPARE(m_searchResult.count(), 2);
// protected attributes are ignored
m_entrySearcher = EntrySearcher(false, true);
m_searchResult = m_entrySearcher.search("_testAttribute:test _testProtected:testP2", m_rootGroup);
QCOMPARE(m_searchResult.count(), 2);
}
@ -319,3 +321,52 @@ void TestEntrySearcher::testGroup()
m_searchResult = m_entrySearcher.search("g:/group1 search", m_rootGroup);
QCOMPARE(m_searchResult.count(), 1);
}
void TestEntrySearcher::testSkipProtected()
{
QScopedPointer<Entry> e1(new Entry());
e1->setGroup(m_rootGroup);
e1->attributes()->set("testAttribute", "testE1");
e1->attributes()->set("testProtected", "apple", true);
QScopedPointer<Entry> e2(new Entry());
e2->setGroup(m_rootGroup);
e2->attributes()->set("testAttribute", "testE2");
e2->attributes()->set("testProtected", "banana", true);
const QList<Entry*> expectE1{e1.data()};
const QList<Entry*> expectE2{e2.data()};
const QList<Entry*> expectBoth{e1.data(), e2.data()};
// when not skipping protected, empty term matches everything
m_searchResult = m_entrySearcher.search("", m_rootGroup);
QCOMPARE(m_searchResult, expectBoth);
// now test the searcher with skipProtected = true
m_entrySearcher = EntrySearcher(false, true);
// when skipping protected, empty term matches nothing
m_searchResult = m_entrySearcher.search("", m_rootGroup);
QCOMPARE(m_searchResult, {});
// having a protected entry in terms should not affect the results in anyways
m_searchResult = m_entrySearcher.search("_testProtected:apple", m_rootGroup);
QCOMPARE(m_searchResult, {});
m_searchResult = m_entrySearcher.search("_testProtected:apple _testAttribute:testE2", m_rootGroup);
QCOMPARE(m_searchResult, expectE2);
m_searchResult = m_entrySearcher.search("_testProtected:apple _testAttribute:testE1", m_rootGroup);
QCOMPARE(m_searchResult, expectE1);
m_searchResult =
m_entrySearcher.search("_testProtected:apple _testAttribute:testE1 _testAttribute:testE2", m_rootGroup);
QCOMPARE(m_searchResult, {});
// also move the protected term around to execurise the short-circut logic
m_searchResult = m_entrySearcher.search("_testAttribute:testE2 _testProtected:apple", m_rootGroup);
QCOMPARE(m_searchResult, expectE2);
m_searchResult = m_entrySearcher.search("_testAttribute:testE1 _testProtected:apple", m_rootGroup);
QCOMPARE(m_searchResult, expectE1);
m_searchResult =
m_entrySearcher.search("_testAttribute:testE1 _testProtected:apple _testAttribute:testE2", m_rootGroup);
QCOMPARE(m_searchResult, {});
}

View File

@ -37,6 +37,7 @@ private slots:
void testSearchTermParser();
void testCustomAttributesAreSearched();
void testGroup();
void testSkipProtected();
private:
Group* m_rootGroup;

View File

@ -0,0 +1,33 @@
<interface name="org.freedesktop.Secret.Collection">
<property name="Items" type="ao" access="read"/>
<property name="Label" type="s" access="readwrite"/>
<property name="Locked" type="b" access="read"/>
<property name="Created" type="t" access="read"/>
<property name="Modified" type="t" access="read"/>
<signal name="ItemCreated">
<arg name="item" type="o" direction="out"/>
</signal>
<signal name="ItemDeleted">
<arg name="item" type="o" direction="out"/>
</signal>
<signal name="ItemChanged">
<arg name="item" type="o" direction="out"/>
</signal>
<method name="Delete">
<arg type="o" direction="out"/>
</method>
<method name="SearchItems">
<arg type="ao" direction="out"/>
<arg name="attributes" type="a{ss}" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
</method>
<method name="CreateItem">
<arg type="o" direction="out"/>
<arg name="properties" type="a{sv}" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg name="secret" type="(oayays)" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="FdoSecrets::SecretStruct"/>
<arg name="replace" type="b" direction="in"/>
<arg name="prompt" type="o" direction="out"/>
</method>
</interface>

View File

@ -0,0 +1,21 @@
<interface name="org.freedesktop.Secret.Item">
<property name="Locked" type="b" access="read"/>
<property name="Attributes" type="a{ss}" access="readwrite">
<annotation name="org.qtproject.QtDBus.QtTypeName" value="StringStringMap"/>
</property>
<property name="Label" type="s" access="readwrite"/>
<property name="Created" type="t" access="read"/>
<property name="Modified" type="t" access="read"/>
<method name="Delete">
<arg type="o" direction="out"/>
</method>
<method name="GetSecret">
<arg type="(oayays)" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FdoSecrets::SecretStruct"/>
<arg name="session" type="o" direction="in"/>
</method>
<method name="SetSecret">
<arg name="secret" type="(oayays)" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="FdoSecrets::SecretStruct"/>
</method>
</interface>

View File

@ -0,0 +1,11 @@
<interface name="org.freedesktop.Secret.Prompt">
<signal name="Completed">
<arg name="dismissed" type="b" direction="out"/>
<arg name="result" type="v" direction="out"/>
</signal>
<method name="Prompt">
<arg name="windowId" type="s" direction="in"/>
</method>
<method name="Dismiss">
</method>
</interface>

View File

@ -0,0 +1,55 @@
<interface name="org.freedesktop.Secret.Service">
<property name="Collections" type="ao" access="read"/>
<signal name="CollectionCreated">
<arg name="collection" type="o" direction="out"/>
</signal>
<signal name="CollectionDeleted">
<arg name="collection" type="o" direction="out"/>
</signal>
<signal name="CollectionChanged">
<arg name="collection" type="o" direction="out"/>
</signal>
<method name="OpenSession">
<arg type="v" direction="out"/>
<arg name="algorithm" type="s" direction="in"/>
<arg name="input" type="v" direction="in"/>
<arg name="result" type="o" direction="out"/>
</method>
<method name="CreateCollection">
<arg type="o" direction="out"/>
<arg name="properties" type="a{sv}" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
<arg name="alias" type="s" direction="in"/>
<arg name="prompt" type="o" direction="out"/>
</method>
<method name="SearchItems">
<arg type="ao" direction="out"/>
<arg name="attributes" type="a{ss}" direction="in"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
<arg name="locked" type="ao" direction="out"/>
</method>
<method name="Unlock">
<arg type="ao" direction="out"/>
<arg name="paths" type="ao" direction="in"/>
<arg name="prompt" type="o" direction="out"/>
</method>
<method name="Lock">
<arg type="ao" direction="out"/>
<arg name="paths" type="ao" direction="in"/>
<arg name="prompt" type="o" direction="out"/>
</method>
<method name="GetSecrets">
<arg type="a{o(oayays)}" direction="out"/>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ObjectPathSecretMap"/>
<arg name="items" type="ao" direction="in"/>
<arg name="session" type="o" direction="in"/>
</method>
<method name="ReadAlias">
<arg type="o" direction="out"/>
<arg name="name" type="s" direction="in"/>
</method>
<method name="SetAlias">
<arg name="name" type="s" direction="in"/>
<arg name="collection" type="o" direction="in"/>
</method>
</interface>

View File

@ -0,0 +1,4 @@
<interface name="org.freedesktop.Secret.Session">
<method name="Close">
</method>
</interface>

View File

@ -0,0 +1,39 @@
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
<busconfig>
<type>session</type>
<keep_umask/>
<listen>unix:tmpdir=/tmp</listen>
<auth>EXTERNAL</auth>
<standard_session_servicedirs />
<policy context="default">
<allow send_destination="*" eavesdrop="true"/>
<allow eavesdrop="true"/>
<allow own="*"/>
</policy>
<include ignore_missing="yes">/etc/dbus-1/session.conf</include>
<includedir>session.d</includedir>
<includedir>/etc/dbus-1/session.d</includedir>
<include ignore_missing="yes">/etc/dbus-1/session-local.conf</include>
<include if_selinux_enabled="yes" selinux_root_relative="yes">contexts/dbus_contexts</include>
<limit name="max_incoming_bytes">1000000000</limit>
<limit name="max_incoming_unix_fds">250000000</limit>
<limit name="max_outgoing_bytes">1000000000</limit>
<limit name="max_outgoing_unix_fds">250000000</limit>
<limit name="max_message_size">1000000000</limit>
<limit name="auth_timeout">240000</limit>
<limit name="pending_fd_timeout">150000</limit>
<limit name="max_completed_connections">100000</limit>
<limit name="max_incomplete_connections">10000</limit>
<limit name="max_connections_per_user">100000</limit>
<limit name="max_pending_service_starts">10000</limit>
<limit name="max_names_per_connection">50000</limit>
<limit name="max_match_rules_per_connection">50000</limit>
<limit name="max_replies_per_connection">50000</limit>
<!-- The above is copied from session bus conf.
Our only intent here is to set a low service_start_timeout,
such that ctest can exit sooner when dbus-run-session is used
to launch tests and some service fails to start.
-->
<limit name="service_start_timeout">500</limit>
</busconfig>

View File

@ -21,3 +21,12 @@ add_unit_test(NAME testguipixmaps SOURCES TestGuiPixmaps.cpp LIBS ${TEST_LIBRARI
if(WITH_XC_BROWSER)
add_unit_test(NAME testguibrowser SOURCES TestGuiBrowser.cpp ../util/TemporaryFile.cpp LIBS ${TEST_LIBRARIES})
endif()
if(WITH_XC_FDOSECRETS)
add_unit_test(NAME testguifdosecrets
SOURCES TestGuiFdoSecrets.cpp ../util/TemporaryFile.cpp
LIBS ${TEST_LIBRARIES}
# The following doesn't work because dbus-run-session expects execname to be in PATH
# dbus-run-session -- execname
LAUNCHER dbus-run-session --config-file ${CMAKE_CURRENT_SOURCE_DIR}/../data/dbus/session.conf -- sh -c "exec ./$0")
endif()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,121 @@
/*
* Copyright (C) 2019 Aetf <aetf@unlimitedcodeworks.xyz>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_TESTGUIFDOSECRETS_H
#define KEEPASSXC_TESTGUIFDOSECRETS_H
#include <QByteArray>
#include <QObject>
#include <QPointer>
#include <QScopedPointer>
#include <QSharedPointer>
#include <QString>
#include "fdosecrets/GcryptMPI.h"
#include "fdosecrets/objects/DBusTypes.h"
class MainWindow;
class Database;
class DatabaseTabWidget;
class DatabaseWidget;
class TemporaryFile;
class FdoSecretsPlugin;
namespace FdoSecrets
{
class Service;
class Session;
class Collection;
class Item;
class Prompt;
class DhIetf1024Sha256Aes128CbcPkcs7;
} // namespace FdoSecrets
class QAbstractItemView;
class TestGuiFdoSecrets : public QObject
{
Q_OBJECT
public:
~TestGuiFdoSecrets() override;
private slots:
void initTestCase();
void init();
void cleanup();
void cleanupTestCase();
void testDBusSpec();
void testServiceEnable();
void testServiceEnableNoExposedDatabase();
void testServiceSearch();
void testServiceUnlock();
void testServiceLock();
void testSessionOpen();
void testSessionClose();
void testCollectionCreate();
void testCollectionDelete();
void testItemCreate();
void testItemReplace();
void testItemSecret();
void testItemDelete();
void testAlias();
void testDefaultAliasAlwaysPresent();
void testExposeSubgroup();
void testModifiyingExposedGroup();
protected slots:
void createDatabaseCallback();
private:
void lockDatabaseInBackend();
void unlockDatabaseInBackend();
QPointer<FdoSecrets::Service> enableService();
QPointer<FdoSecrets::Session> openSession(FdoSecrets::Service* service, const QString& algo);
QPointer<FdoSecrets::Collection> getDefaultCollection(FdoSecrets::Service* service);
QPointer<FdoSecrets::Item> getFirstItem(FdoSecrets::Collection* coll);
QPointer<FdoSecrets::Item> createItem(FdoSecrets::Session* sess,
FdoSecrets::Collection* coll,
const QString& label,
const QString& pass,
const StringStringMap& attr,
bool replace);
private:
QScopedPointer<MainWindow> m_mainWindow;
QPointer<DatabaseTabWidget> m_tabWidget;
QPointer<DatabaseWidget> m_dbWidget;
QSharedPointer<Database> m_db;
QPointer<FdoSecretsPlugin> m_plugin;
// For DH session tests
GcryptMPI m_serverPrivate;
GcryptMPI m_serverPublic;
std::unique_ptr<FdoSecrets::DhIetf1024Sha256Aes128CbcPkcs7> m_cipher;
QByteArray m_dbData;
QScopedPointer<TemporaryFile> m_dbFile;
};
#endif // KEEPASSXC_TESTGUIFDOSECRETS_H