keepassxc/tests/TestBrowser.cpp

743 lines
31 KiB
C++
Raw Normal View History

2018-12-10 07:20:58 -05:00
/*
* Copyright (C) 2023 KeePassXC Team <team@keepassxc.org>
2018-12-10 07:20:58 -05:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "TestBrowser.h"
#include "browser/BrowserMessageBuilder.h"
2019-06-18 18:23:12 -04:00
#include "browser/BrowserSettings.h"
2021-07-11 22:10:29 -04:00
#include "core/Group.h"
2019-11-22 06:54:28 -05:00
#include "core/Tools.h"
2018-12-10 07:20:58 -05:00
#include "crypto/Crypto.h"
2021-07-11 22:10:29 -04:00
#include <QJsonObject>
#include <QTest>
Replace all crypto libraries with Botan Selected the [Botan crypto library](https://github.com/randombit/botan) due to its feature list, maintainer support, availability across all deployment platforms, and ease of use. Also evaluated Crypto++ as a viable candidate, but the additional features of Botan (PKCS#11, TPM, etc) won out. The random number generator received a backend upgrade. Botan prefers hardware-based RNG's and will provide one if available. This is transparent to KeePassXC and a significant improvement over gcrypt. Replaced Argon2 library with built-in Botan implementation that supports i, d, and id. This requires Botan 2.11.0 or higher. Also simplified the parameter test across KDF's. Aligned SymmetricCipher parameters with available modes. All encrypt and decrypt operations are done in-place instead of returning new objects. This allows use of secure vectors in the future with no additional overhead. Took this opportunity to decouple KeeShare from SSH Agent. Removed leftover code from OpenSSHKey and consolidated the SSH Agent code into the same directory. Removed bcrypt and blowfish inserts since they are provided by Botan. Additionally simplified KeeShare settings interface by removing raw certificate byte data from the user interface. KeeShare will be further refactored in a future PR. NOTE: This PR breaks backwards compatibility with KeeShare certificates due to different RSA key storage with Botan. As a result, new "own" certificates will need to be generated and trust re-established. Removed YKChallengeResponseKeyCLI in favor of just using the original implementation with signal/slots. Removed TestRandom stub since it was just faking random numbers and not actually using the backend. TestRandomGenerator now uses the actual RNG. Greatly simplified Secret Service plugin's use of crypto functions with Botan.
2021-04-04 08:56:00 -04:00
#include <botan/sodium.h>
using namespace Botan::Sodium;
2018-12-10 07:20:58 -05:00
QTEST_GUILESS_MAIN(TestBrowser)
const QString PUBLICKEY = "UIIPObeoya1G8g1M5omgyoPR/j1mR1HlYHu0wHCgMhA=";
const QString SECRETKEY = "B8ei4ZjQJkWzZU2SK/tBsrYRwp+6ztEMf5GFQV+i0yI=";
const QString SERVERPUBLICKEY = "lKnbLhrVCOqzEjuNoUz1xj9EZlz8xeO4miZBvLrUPVQ=";
const QString SERVERSECRETKEY = "tbPQcghxfOgbmsnEqG2qMIj1W2+nh+lOJcNsHncaz1Q=";
const QString NONCE = "zBKdvTjL5bgWaKMCTut/8soM/uoMrFoZ";
const QString INCREMENTEDNONCE = "zRKdvTjL5bgWaKMCTut/8soM/uoMrFoZ";
2018-12-10 07:20:58 -05:00
const QString CLIENTID = "testClient";
void TestBrowser::initTestCase()
{
QVERIFY(Crypto::init());
m_browserService = browserService();
browserSettings()->setBestMatchOnly(false);
2018-12-10 07:20:58 -05:00
}
void TestBrowser::init()
2018-12-10 07:20:58 -05:00
{
m_browserAction.reset(new BrowserAction());
2018-12-10 07:20:58 -05:00
}
/**
* Tests for BrowserAction
*/
void TestBrowser::testChangePublicKeys()
{
QJsonObject json;
json["action"] = "change-public-keys";
json["publicKey"] = PUBLICKEY;
json["nonce"] = NONCE;
auto response = m_browserAction->processClientMessage(nullptr, json);
2018-12-10 07:20:58 -05:00
QCOMPARE(response["action"].toString(), QString("change-public-keys"));
QCOMPARE(response["publicKey"].toString() == PUBLICKEY, false);
QCOMPARE(response["success"].toString(), TRUE_STR);
2018-12-10 07:20:58 -05:00
}
void TestBrowser::testEncryptMessage()
{
QJsonObject message;
message["action"] = "test-action";
m_browserAction->m_publicKey = SERVERPUBLICKEY;
m_browserAction->m_secretKey = SERVERSECRETKEY;
m_browserAction->m_clientPublicKey = PUBLICKEY;
auto encrypted = browserMessageBuilder()->encryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
2018-12-10 07:20:58 -05:00
QCOMPARE(encrypted, QString("+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP"));
}
void TestBrowser::testDecryptMessage()
{
QString message = "+zjtntnk4rGWSl/Ph7Vqip/swvgeupk4lNgHEm2OO3ujNr0OMz6eQtGwjtsj+/rP";
m_browserAction->m_publicKey = SERVERPUBLICKEY;
m_browserAction->m_secretKey = SERVERSECRETKEY;
m_browserAction->m_clientPublicKey = PUBLICKEY;
auto decrypted = browserMessageBuilder()->decryptMessage(message, NONCE, PUBLICKEY, SERVERSECRETKEY);
2018-12-10 07:20:58 -05:00
QCOMPARE(decrypted["action"].toString(), QString("test-action"));
}
void TestBrowser::testGetBase64FromKey()
{
unsigned char pk[crypto_box_PUBLICKEYBYTES];
2019-06-18 18:23:12 -04:00
2018-12-10 07:20:58 -05:00
for (unsigned int i = 0; i < crypto_box_PUBLICKEYBYTES; ++i) {
pk[i] = i;
}
auto response = browserMessageBuilder()->getBase64FromKey(pk, crypto_box_PUBLICKEYBYTES);
2018-12-10 07:20:58 -05:00
QCOMPARE(response, QString("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="));
}
void TestBrowser::testIncrementNonce()
{
auto result = browserMessageBuilder()->incrementNonce(NONCE);
QCOMPARE(result, INCREMENTEDNONCE);
}
void TestBrowser::testBuildResponse()
{
const auto object = QJsonObject{{"test", true}};
const QJsonArray arr = {QJsonObject{{"test", true}}};
const auto val = QString("value1");
// Note: Passing a const QJsonObject will fail
const Parameters params{
{"test-param-1", val}, {"test-param-2", 2}, {"test-param-3", false}, {"object", object}, {"arr", arr}};
const auto action = QString("test-action");
const auto message = browserMessageBuilder()->buildResponse(action, NONCE, params, PUBLICKEY, SERVERSECRETKEY);
QVERIFY(!message.isEmpty());
QCOMPARE(message["action"].toString(), action);
QCOMPARE(message["nonce"].toString(), NONCE);
const auto decrypted =
browserMessageBuilder()->decryptMessage(message["message"].toString(), NONCE, PUBLICKEY, SERVERSECRETKEY);
QVERIFY(!decrypted.isEmpty());
QCOMPARE(decrypted["test-param-1"].toString(), QString("value1"));
QCOMPARE(decrypted["test-param-2"].toInt(), 2);
QCOMPARE(decrypted["test-param-3"].toBool(), false);
const auto objectResult = decrypted["object"].toObject();
QCOMPARE(objectResult["test"].toBool(), true);
const auto arrResult = decrypted["arr"].toArray();
QCOMPARE(arrResult.size(), 1);
const auto firstArr = arrResult[0].toObject();
QCOMPARE(firstArr["test"].toBool(), true);
2018-12-10 07:20:58 -05:00
}
void TestBrowser::testSortPriority()
{
QFETCH(QString, entryUrl);
QFETCH(QString, siteUrl);
QFETCH(QString, formUrl);
QFETCH(int, expectedScore);
QScopedPointer<Entry> entry(new Entry());
entry->setUrl(entryUrl);
2022-11-12 04:27:28 -05:00
QCOMPARE(m_browserService->sortPriority(entry->getAllUrls(), siteUrl, formUrl), expectedScore);
}
void TestBrowser::testSortPriority_data()
{
const QString siteUrl = "https://github.com/login";
const QString formUrl = "https://github.com/session";
QTest::addColumn<QString>("entryUrl");
QTest::addColumn<QString>("siteUrl");
QTest::addColumn<QString>("formUrl");
QTest::addColumn<int>("expectedScore");
QTest::newRow("Exact Match") << siteUrl << siteUrl << siteUrl << 100;
QTest::newRow("Exact Match (site)") << siteUrl << siteUrl << formUrl << 100;
QTest::newRow("Exact Match (form)") << siteUrl << "https://github.net" << siteUrl << 100;
QTest::newRow("Exact Match No Trailing Slash") << "https://github.com"
<< "https://github.com/" << formUrl << 100;
QTest::newRow("Exact Match No Scheme") << "github.com/login" << siteUrl << formUrl << 100;
QTest::newRow("Exact Match with Query") << "https://github.com/login?test=test#fragment"
<< "https://github.com/login?test=test" << formUrl << 100;
QTest::newRow("Site Query Mismatch") << siteUrl << siteUrl + "?test=test" << formUrl << 90;
QTest::newRow("Path Mismatch (site)") << "https://github.com/" << siteUrl << formUrl << 85;
QTest::newRow("Path Mismatch (site) No Scheme") << "github.com" << siteUrl << formUrl << 85;
QTest::newRow("Path Mismatch (form)") << "https://github.com/"
<< "https://github.net" << formUrl << 85;
QTest::newRow("Path Mismatch (diff parent)") << "https://github.com/keepassxreboot" << siteUrl << formUrl << 80;
QTest::newRow("Path Mismatch (diff parent, form)") << "https://github.com/keepassxreboot"
<< "https://github.net" << formUrl << 70;
QTest::newRow("Subdomain Mismatch (site)") << siteUrl << "https://sub.github.com/"
<< "https://github.net/" << 60;
QTest::newRow("Subdomain Mismatch (form)") << siteUrl << "https://github.net/"
<< "https://sub.github.com/" << 50;
QTest::newRow("Scheme Mismatch") << "http://github.com" << siteUrl << formUrl << 0;
QTest::newRow("Scheme Mismatch w/path") << "http://github.com/login" << siteUrl << formUrl << 0;
QTest::newRow("Invalid URL") << "http://github" << siteUrl << formUrl << 0;
2018-12-10 07:20:58 -05:00
}
void TestBrowser::testSearchEntries()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
2019-11-18 01:57:04 -05:00
QStringList urls = {"https://github.com/login_page",
"https://github.com/login",
"https://github.com/",
"github.com/login",
"http://github.com",
"http://github.com/login",
"github.com",
"github.com/login",
"https://github", // Invalid URL
"github.com"};
2019-11-12 15:38:20 -05:00
createEntries(urls, root);
2018-12-10 07:20:58 -05:00
browserSettings()->setMatchUrlScheme(false);
2019-11-18 01:57:04 -05:00
auto result =
m_browserService->searchEntries(db, "https://github.com", "https://github.com/session"); // db, url, submitUrl
2019-11-01 14:13:12 -04:00
QCOMPARE(result.length(), 9);
2018-12-10 07:20:58 -05:00
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->url(), QString("https://github.com/login"));
QCOMPARE(result[2]->url(), QString("https://github.com/"));
2019-11-01 14:13:12 -04:00
QCOMPARE(result[3]->url(), QString("github.com/login"));
QCOMPARE(result[4]->url(), QString("http://github.com"));
QCOMPARE(result[5]->url(), QString("http://github.com/login"));
2018-12-10 07:20:58 -05:00
2019-11-01 14:13:12 -04:00
// With matching there should be only 3 results + 4 without a scheme
2018-12-10 07:20:58 -05:00
browserSettings()->setMatchUrlScheme(true);
2019-11-12 15:38:20 -05:00
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
2019-11-01 14:13:12 -04:00
QCOMPARE(result.length(), 7);
2018-12-10 07:20:58 -05:00
QCOMPARE(result[0]->url(), QString("https://github.com/login_page"));
QCOMPARE(result[1]->url(), QString("https://github.com/login"));
QCOMPARE(result[2]->url(), QString("https://github.com/"));
2019-11-01 14:13:12 -04:00
QCOMPARE(result[3]->url(), QString("github.com/login"));
2018-12-10 07:20:58 -05:00
}
2020-10-17 10:05:02 -04:00
void TestBrowser::testSearchEntriesByPath()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QStringList urlsRoot = {"https://root.example.com/", "root.example.com/login"};
auto entriesRoot = createEntries(urlsRoot, root);
auto* groupLevel1 = new Group();
groupLevel1->setParent(root);
groupLevel1->setName("TestGroup1");
QStringList urlsLevel1 = {"https://1.example.com/", "1.example.com/login"};
auto entriesLevel1 = createEntries(urlsLevel1, groupLevel1);
auto* groupLevel2 = new Group();
groupLevel2->setParent(groupLevel1);
groupLevel2->setName("TestGroup2");
QStringList urlsLevel2 = {"https://2.example.com/", "2.example.com/login"};
auto entriesLevel2 = createEntries(urlsLevel2, groupLevel2);
compareEntriesByPath(db, entriesRoot, "");
compareEntriesByPath(db, entriesLevel1, "TestGroup1/");
compareEntriesByPath(db, entriesLevel2, "TestGroup1/TestGroup2/");
}
void TestBrowser::compareEntriesByPath(QSharedPointer<Database> db, QList<Entry*> entries, QString path)
{
for (Entry* entry : entries) {
QString testUrl = "keepassxc://by-path/" + path + entry->title();
/* Look for an entry with that path. First using handleEntry, then through the search */
QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
2020-10-17 10:05:02 -04:00
auto result = m_browserService->searchEntries(db, testUrl, "");
QCOMPARE(result.length(), 1);
QCOMPARE(result[0], entry);
}
}
void TestBrowser::testSearchEntriesByUUID()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
/* The URLs don't really matter for this test, we just need some entries */
QStringList urls = {"https://github.com/login_page",
"https://github.com/login",
"https://github.com/",
"github.com/login",
"http://github.com",
"http://github.com/login",
"github.com",
"github.com/login",
"https://github",
"github.com",
"",
"not an URL"};
auto entries = createEntries(urls, root);
for (Entry* entry : entries) {
QString testUrl = "keepassxc://by-uuid/" + entry->uuidToHex();
2022-11-12 04:27:28 -05:00
/* Look for an entry with that UUID. First using shouldIncludeEntry, then through the search */
QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), true);
auto result = m_browserService->searchEntries(db, testUrl, "");
QCOMPARE(result.length(), 1);
QCOMPARE(result[0], entry);
}
/* Test for entries that don't exist */
QStringList uuids = {"00000000000000000000000000000000",
"00000000000000000000000000000001",
"00000000000000000000000000000002/",
"invalid uuid",
"000000000000000000000000000000000000000"
"00000000000000000000000"};
for (QString uuid : uuids) {
QString testUrl = "keepassxc://by-uuid/" + uuid;
for (Entry* entry : entries) {
QCOMPARE(m_browserService->shouldIncludeEntry(entry, testUrl, ""), false);
}
auto result = m_browserService->searchEntries(db, testUrl, "");
QCOMPARE(result.length(), 0);
}
}
2022-11-12 04:27:28 -05:00
void TestBrowser::testSearchEntriesByReference()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
/* The URLs don't really matter for this test, we just need some entries */
QStringList urls = {"https://subdomain.example.com",
"example.com", // Only includes a partial URL for references
"https://another.domain.com", // Additional URL as full reference
"https://subdomain.somesite.com", // Additional URL as partial reference
"", // Full reference will be added to https://subdomain.example.com
"" // Partial reference will be added to https://subdomain.example.com
"https://www.notincluded.com"}; // Should not show in search
auto entries = createEntries(urls, root);
auto firstEntryUuid = entries.first()->uuidToHex();
auto secondEntryUuid = entries[1]->uuidToHex();
auto fullReference = QString("{REF:A@I:%1}").arg(firstEntryUuid);
auto partialReference = QString("https://subdomain.{REF:A@I:%1}").arg(secondEntryUuid);
entries[2]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, fullReference);
entries[3]->attributes()->set(EntryAttributes::AdditionalUrlAttribute, partialReference);
2022-11-12 04:27:28 -05:00
entries[4]->setUrl(fullReference);
entries[5]->setUrl(partialReference);
auto result = m_browserService->searchEntries(db, "https://subdomain.example.com", "");
QCOMPARE(result.length(), 6);
QCOMPARE(result[0]->url(), urls[0]);
QCOMPARE(result[1]->url(), urls[1]);
QCOMPARE(result[2]->url(), urls[2]);
QCOMPARE(
result[2]->resolveMultiplePlaceholders(result[2]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
urls[0]);
2022-11-12 04:27:28 -05:00
QCOMPARE(result[3]->url(), urls[3]);
QCOMPARE(
result[3]->resolveMultiplePlaceholders(result[3]->attributes()->value(EntryAttributes::AdditionalUrlAttribute)),
urls[0]);
2022-11-12 04:27:28 -05:00
QCOMPARE(result[4]->url(), fullReference);
QCOMPARE(result[4]->resolveMultiplePlaceholders(result[4]->url()), urls[0]); // Should be resolved to the main entry
QCOMPARE(result[5]->url(), partialReference);
QCOMPARE(result[5]->resolveMultiplePlaceholders(result[5]->url()), urls[0]); // Should be resolved to the main entry
}
2018-12-10 07:20:58 -05:00
void TestBrowser::testSearchEntriesWithPort()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
2019-11-18 01:57:04 -05:00
QStringList urls = {"http://127.0.0.1:443", "http://127.0.0.1:80"};
2018-12-10 07:20:58 -05:00
2019-11-12 15:38:20 -05:00
createEntries(urls, root);
2018-12-10 07:20:58 -05:00
2019-11-12 15:38:20 -05:00
auto result = m_browserService->searchEntries(db, "http://127.0.0.1:443", "http://127.0.0.1");
2018-12-10 07:20:58 -05:00
QCOMPARE(result.length(), 1);
2019-11-12 15:38:20 -05:00
QCOMPARE(result[0]->url(), QString("http://127.0.0.1:443"));
2018-12-10 07:20:58 -05:00
}
void TestBrowser::testSearchEntriesWithAdditionalURLs()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
2019-11-18 01:57:04 -05:00
QStringList urls = {"https://github.com/", "https://www.example.com", "http://domain.com"};
2019-11-12 15:38:20 -05:00
auto entries = createEntries(urls, root);
// Add an additional URL to the first entry
entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://keepassxc.org");
2019-11-12 15:38:20 -05:00
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
2019-11-12 15:38:20 -05:00
QCOMPARE(result[0]->url(), QString("https://github.com/"));
// Search the additional URL. It should return the same entry
2019-11-12 15:38:20 -05:00
auto additionalResult = m_browserService->searchEntries(db, "https://keepassxc.org", "https://keepassxc.org");
QCOMPARE(additionalResult.length(), 1);
2019-11-12 15:38:20 -05:00
QCOMPARE(additionalResult[0]->url(), QString("https://github.com/"));
2019-11-01 14:13:12 -04:00
}
void TestBrowser::testInvalidEntries()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
2019-11-12 15:38:20 -05:00
const QString url("https://github.com");
const QString submitUrl("https://github.com/session");
QStringList urls = {
"https://github.com/login",
"https:///github.com/", // Extra '/'
"http://github.com/**//*",
"http://*.github.com/login",
"//github.com", // fromUserInput() corrects this one.
"github.com/{}<>",
"http:/example.com",
};
2019-11-18 01:57:04 -05:00
2019-11-12 15:38:20 -05:00
createEntries(urls, root);
2019-11-01 14:13:12 -04:00
browserSettings()->setMatchUrlScheme(true);
2019-11-12 15:38:20 -05:00
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
2019-11-01 14:13:12 -04:00
QCOMPARE(result.length(), 2);
QCOMPARE(result[0]->url(), QString("https://github.com/login"));
QCOMPARE(result[1]->url(), QString("//github.com"));
// Test the URL's directly
2019-11-12 15:38:20 -05:00
QCOMPARE(m_browserService->handleURL(urls[0], url, submitUrl), true);
QCOMPARE(m_browserService->handleURL(urls[1], url, submitUrl), false);
QCOMPARE(m_browserService->handleURL(urls[2], url, submitUrl), false);
QCOMPARE(m_browserService->handleURL(urls[3], url, submitUrl), false);
QCOMPARE(m_browserService->handleURL(urls[4], url, submitUrl), true);
QCOMPARE(m_browserService->handleURL(urls[5], url, submitUrl), false);
2019-11-01 14:13:12 -04:00
}
void TestBrowser::testSubdomainsAndPaths()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
2019-11-12 15:38:20 -05:00
QStringList urls = {
"https://www.github.com/login/page.xml",
"https://login.github.com/",
"https://github.com",
"http://www.github.com",
"http://login.github.com/pathtonowhere",
".github.com", // Invalid URL
"www.github.com/",
2020-01-13 04:26:45 -05:00
"https://github", // Invalid URL
"https://hub.com" // Should not return
2019-11-12 15:38:20 -05:00
};
2019-11-01 14:13:12 -04:00
2019-11-12 15:38:20 -05:00
createEntries(urls, root);
2019-11-01 14:13:12 -04:00
browserSettings()->setMatchUrlScheme(false);
2019-11-12 15:38:20 -05:00
auto result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com"));
2019-11-01 14:13:12 -04:00
2019-11-12 15:38:20 -05:00
// With www subdomain
result = m_browserService->searchEntries(db, "https://www.github.com", "https://www.github.com/session");
QCOMPARE(result.length(), 4);
QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
QCOMPARE(result[1]->url(), QString("https://github.com")); // Accepts any subdomain
QCOMPARE(result[2]->url(), QString("http://www.github.com"));
QCOMPARE(result[3]->url(), QString("www.github.com/"));
2019-11-01 14:13:12 -04:00
// With www subdomain omitted
root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Enable);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
root->setCustomDataTriState(BrowserService::OPTION_OMIT_WWW, Group::Inherit);
QCOMPARE(result.length(), 4);
QCOMPARE(result[0]->url(), QString("https://www.github.com/login/page.xml"));
QCOMPARE(result[1]->url(), QString("https://github.com"));
QCOMPARE(result[2]->url(), QString("http://www.github.com"));
QCOMPARE(result[3]->url(), QString("www.github.com/"));
2019-11-12 15:38:20 -05:00
// With scheme matching there should be only 1 result
2019-11-01 14:13:12 -04:00
browserSettings()->setMatchUrlScheme(true);
2019-11-12 15:38:20 -05:00
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
QCOMPARE(result[0]->url(), QString("https://github.com"));
// Test site with subdomain in the site URL
QStringList entryURLs = {
"https://accounts.example.com",
"https://accounts.example.com/path",
"https://subdomain.example.com/",
"https://another.accounts.example.com/",
"https://another.subdomain.example.com/",
"https://example.com/",
"https://example" // Invalid URL
};
createEntries(entryURLs, root);
result = m_browserService->searchEntries(db, "https://accounts.example.com/", "https://accounts.example.com/");
2019-11-12 15:38:20 -05:00
QCOMPARE(result.length(), 3);
QCOMPARE(result[0]->url(), QString("https://accounts.example.com"));
QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
QCOMPARE(result[2]->url(), QString("https://example.com/")); // Accepts any subdomain
2019-11-18 01:57:04 -05:00
result = m_browserService->searchEntries(
db, "https://another.accounts.example.com/", "https://another.accounts.example.com/");
2019-11-01 14:13:12 -04:00
QCOMPARE(result.length(), 4);
2019-11-18 01:57:04 -05:00
QCOMPARE(result[0]->url(),
QString("https://accounts.example.com")); // Accepts any subdomain under accounts.example.com
2019-11-12 15:38:20 -05:00
QCOMPARE(result[1]->url(), QString("https://accounts.example.com/path"));
QCOMPARE(result[2]->url(), QString("https://another.accounts.example.com/"));
QCOMPARE(result[3]->url(), QString("https://example.com/")); // Accepts one or more subdomains
// Test local files. It should be a direct match.
2019-11-18 01:57:04 -05:00
QStringList localFiles = {"file:///Users/testUser/tests/test.html"};
2019-11-12 15:38:20 -05:00
createEntries(localFiles, root);
// With local files, url is always set to the file scheme + ://. Submit URL holds the actual URL.
result = m_browserService->searchEntries(db, "file://", "file:///Users/testUser/tests/test.html");
QCOMPARE(result.length(), 1);
}
2019-11-12 15:38:20 -05:00
QList<Entry*> TestBrowser::createEntries(QStringList& urls, Group* root) const
{
QList<Entry*> entries;
for (int i = 0; i < urls.length(); ++i) {
auto entry = new Entry();
entry->setGroup(root);
entry->beginUpdate();
entry->setUrl(urls[i]);
entry->setUsername(QString("User %1").arg(i));
entry->setUuid(QUuid::createUuid());
2020-10-17 10:05:02 -04:00
entry->setTitle(QString("Name_%1").arg(entry->uuidToHex()));
2019-11-12 15:38:20 -05:00
entry->endUpdate();
entries.push_back(entry);
}
return entries;
2019-11-22 06:54:28 -05:00
}
void TestBrowser::testBestMatchingCredentials()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
// Test with simple URL entries
QStringList urls = {"https://github.com/loginpage", "https://github.com/justsomepage", "https://github.com/"};
auto entries = createEntries(urls, root);
browserSettings()->setBestMatchOnly(true);
QString siteUrl = "https://github.com/loginpage";
auto result = m_browserService->searchEntries(db, siteUrl, siteUrl);
auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), siteUrl);
siteUrl = "https://github.com/justsomepage";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), siteUrl);
siteUrl = "https://github.com/";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(entries, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), siteUrl);
// Without best-matching the URL with the path should be returned first
browserSettings()->setBestMatchOnly(false);
siteUrl = "https://github.com/loginpage";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 3);
QCOMPARE(sorted[0]->url(), siteUrl);
// Test with subdomains
QStringList subdomainsUrls = {"https://sub.github.com/loginpage",
"https://sub.github.com/justsomepage",
"https://bus.github.com/justsomepage",
"https://subdomain.example.com/",
"https://subdomain.example.com",
"https://example.com"};
entries = createEntries(subdomainsUrls, root);
browserSettings()->setBestMatchOnly(true);
siteUrl = "https://sub.github.com/justsomepage";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), siteUrl);
siteUrl = "https://github.com/justsomepage";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), siteUrl);
siteUrl = "https://sub.github.com/justsomepage?wehavesomeextra=here";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), QString("https://sub.github.com/justsomepage"));
// The matching should not care if there's a / path or not.
siteUrl = "https://subdomain.example.com/";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 2);
QCOMPARE(sorted[0]->url(), QString("https://subdomain.example.com"));
QCOMPARE(sorted[1]->url(), QString("https://subdomain.example.com/"));
// Entries with https://example.com should be still returned even if the site URL has a subdomain. Those have the
// best match.
db = QSharedPointer<Database>::create();
root = db->rootGroup();
QStringList domainUrls = {"https://example.com", "https://example.com", "https://other.example.com"};
entries = createEntries(domainUrls, root);
siteUrl = "https://subdomain.example.com";
result = m_browserService->searchEntries(db, siteUrl, siteUrl);
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 2);
QCOMPARE(sorted[0]->url(), QString("https://example.com"));
QCOMPARE(sorted[1]->url(), QString("https://example.com"));
// https://github.com/keepassxreboot/keepassxc/issues/4754
db = QSharedPointer<Database>::create();
root = db->rootGroup();
QStringList fooUrls = {"https://example.com/foo", "https://example.com/bar"};
entries = createEntries(fooUrls, root);
for (const auto& url : fooUrls) {
result = m_browserService->searchEntries(db, url, url);
sorted = m_browserService->sortEntries(result, url, url);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), QString(url));
}
// https://github.com/keepassxreboot/keepassxc/issues/4734
db = QSharedPointer<Database>::create();
root = db->rootGroup();
QStringList testUrls = {"http://some.domain.tld/somePath", "http://some.domain.tld/otherPath"};
entries = createEntries(testUrls, root);
for (const auto& url : testUrls) {
result = m_browserService->searchEntries(db, url, url);
sorted = m_browserService->sortEntries(result, url, url);
QCOMPARE(sorted.size(), 1);
QCOMPARE(sorted[0]->url(), QString(url));
}
}
void TestBrowser::testBestMatchingWithAdditionalURLs()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
QStringList urls = {"https://github.com/loginpage", "https://test.github.com/", "https://github.com/"};
auto entries = createEntries(urls, root);
browserSettings()->setBestMatchOnly(true);
// Add an additional URL to the first entry
entries.first()->attributes()->set(EntryAttributes::AdditionalUrlAttribute, "https://test.github.com/anotherpage");
// The first entry should be triggered
auto result = m_browserService->searchEntries(
db, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
auto sorted = m_browserService->sortEntries(
result, "https://test.github.com/anotherpage", "https://test.github.com/anotherpage");
QCOMPARE(sorted.length(), 1);
QCOMPARE(sorted[0]->url(), urls[0]);
}
void TestBrowser::testRestrictBrowserKey()
{
auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
// Group 0 (root): No browser key restriction given
QStringList urlsRoot = {"https://example.com/0"};
auto entriesRoot = createEntries(urlsRoot, root);
// Group 1: restricted to browser with 'key1'
auto* group1 = new Group();
group1->setParent(root);
group1->setName("TestGroup1");
group1->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key1");
QStringList urls1 = {"https://example.com/1"};
auto entries1 = createEntries(urls1, group1);
// Group 2: restricted to browser with 'key2'
auto* group2 = new Group();
group2->setParent(root);
group2->setName("TestGroup2");
group2->customData()->set(BrowserService::OPTION_RESTRICT_KEY, "key2");
QStringList urls2 = {"https://example.com/2"};
auto entries2 = createEntries(urls2, group2);
// Group 2b: inherits parent group (2) restriction
auto* group2b = new Group();
group2b->setParent(group2);
group2b->setName("TestGroup2b");
QStringList urls2b = {"https://example.com/2b"};
auto entries2b = createEntries(urls2b, group2b);
// Group 3: inherits parent group (root) - any browser can see
auto* group3 = new Group();
group3->setParent(root);
group3->setName("TestGroup3");
QStringList urls3 = {"https://example.com/3"};
auto entries3 = createEntries(urls3, group3);
// Browser 'key0': Groups 1 and 2 are excluded, so entries 0 and 3 will be found
auto siteUrl = QString("https://example.com");
auto result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key0"});
auto sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 2);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/0"));
// Browser 'key1': Group 2 will be excluded, so entries 0, 1, and 3 will be found
result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key1"});
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 3);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/1"));
QCOMPARE(sorted[2]->url(), QString("https://example.com/0"));
// Browser 'key2': Group 1 will be excluded, so entries 0, 2, 2b, 3 will be found
result = m_browserService->searchEntries(db, siteUrl, siteUrl, {"key2"});
sorted = m_browserService->sortEntries(result, siteUrl, siteUrl);
QCOMPARE(sorted.size(), 4);
QCOMPARE(sorted[0]->url(), QString("https://example.com/3"));
QCOMPARE(sorted[1]->url(), QString("https://example.com/2b"));
QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
}