Compare commits

...

9 commits

Author SHA1 Message Date
Max.-F. Helm
96a5bb9904
Merge 210cd13d5f into a1cddad37c 2026-03-08 17:05:46 +01:00
Sami Vänttinen
a1cddad37c
Add support for nested folders with Bitwarden import (#13081)
Some checks are pending
CodeQL / Analyze (push) Waiting to run
Co-authored-by: varjolintu <sami.vanttinen@ahmala.org>
2026-03-08 16:18:22 +01:00
Sami Vänttinen
2dd56c76cc
Fix showing correct checkbox value in entry Browser Integration settings (#12980)
Co-authored-by: varjolintu <sami.vanttinen@ahmala.org>
2026-03-08 16:17:27 +01:00
virgoparna
b45cea0d9c
Add uiAccess=true manifest to fix Auto-Type into elevated prompts (#13070)
Add uiAccess=true manifest (#13070)

Fixes #12956 (KeepassXC cannot autotype into windows password prompts)
2026-03-08 13:36:48 +01:00
Sami Vänttinen
1ce2ce9bb8
Passkeys: Set BE and BS flags to true (#13042)
Passkeys: Set BE flag to true

---------

Co-authored-by: varjolintu <sami.vanttinen@ahmala.org>
2026-03-08 13:08:15 +01:00
Sami Vänttinen
6f6076ab81
Fix setting browser related values to customData (#13026)
Co-authored-by: varjolintu <sami.vanttinen@ahmala.org>
2026-03-08 12:47:29 +01:00
Jonathan White
ec034eaf7d
Revert Auto-Type change that caused race condition (#12738)
* Fixes #12723
2026-03-08 11:58:52 +01:00
Janek Bevendorff
ada379fddd
Fix Linux test failures (#13113)
Some checks failed
CodeQL / Analyze (push) Has been cancelled
* Fix test failure introduced by ab31a748fa (#10993)

* Fix tray hiding test failure

Introduced by 43904d87b7 (#10928), but somehow hasn't posed an issue until now.

* Try to make attachment GUI test more robust

---------

Co-authored-by: Jonathan White <support@dmapps.us>
2026-03-06 17:59:02 -05:00
Maximilian-F. Helm
210cd13d5f add a button to copy the text of entry attributes 2026-02-06 15:21:39 +01:00
24 changed files with 666 additions and 148 deletions

View file

@ -3151,6 +3151,14 @@ Korrigieren?</translation>
<source>Reveal</source>
<translation>Zeigen</translation>
</message>
<message>
<source>Copy</source>
<translation>Kopieren</translation>
</message>
<message>
<source>Copy text into clipboard</source>
<translation>Text in die Zwischenablage kopieren</translation>
</message>
<message>
<source>Attachments</source>
<translation>Anhänge</translation>
@ -10671,4 +10679,4 @@ Beispiel: JBSWY3DPEHPK3PXP</translation>
<translation>Unbekannt</translation>
</message>
</context>
</TS>
</TS>

View file

@ -3150,6 +3150,14 @@ Would you like to correct it?</source>
<source>Reveal</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy text into clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachments</source>
<translation type="unfinished"></translation>

View file

@ -3150,6 +3150,14 @@ Would you like to correct it?</translation>
<source>Reveal</source>
<translation>Reveal</translation>
</message>
<message>
<source>Copy</source>
<translation>Copy</translation>
</message>
<message>
<source>Copy text into clipboard</source>
<translation>Copy text into clipboard</translation>
</message>
<message>
<source>Attachments</source>
<translation>Attachments</translation>
@ -10671,4 +10679,4 @@ Example: JBSWY3DPEHPK3PXP</translation>
<translation>Unknown</translation>
</message>
</context>
</TS>
</TS>

View file

@ -3150,6 +3150,14 @@ Would you like to correct it?</translation>
<source>Reveal</source>
<translation>Reveal</translation>
</message>
<message>
<source>Copy</source>
<translation>Copy</translation>
</message>
<message>
<source>Copy text into clipboard</source>
<translation>Copy text into clipboard</translation>
</message>
<message>
<source>Attachments</source>
<translation>Attachments</translation>
@ -10671,4 +10679,4 @@ Example: JBSWY3DPEHPK3PXP</translation>
<translation>Unknown</translation>
</message>
</context>
</TS>
</TS>

View file

@ -427,6 +427,7 @@ set_target_properties(${PROGNAME} PROPERTIES ENABLE_EXPORTS ON)
if(WIN32)
set_target_properties(${PROGNAME} PROPERTIES WIN32_EXECUTABLE ON)
set_target_properties(${PROGNAME} PROPERTIES LINK_FLAGS "/MANIFESTUAC:\"level='asInvoker' uiAccess='true'\"")
include(GenerateProductVersion)
generate_product_version(
WIN32_ResourceFiles

View file

@ -115,6 +115,8 @@ namespace
{"f14", Qt::Key_F14},
{"f15", Qt::Key_F15},
{"f16", Qt::Key_F16}};
constexpr int s_minWaitDelay = 100; // 100 ms
constexpr int s_maxWaitDelay = 10000; // 10 seconds
} // namespace
AutoType* AutoType::m_instance = nullptr;
@ -312,6 +314,9 @@ void AutoType::executeAutoTypeActions(const Entry* entry,
// Restore executor mode
m_executor->mode = mode;
// Initial Auto-Type delay to allow window to come to foreground
Tools::wait(qBound(s_minWaitDelay, config()->get(Config::AutoTypeStartDelay).toInt(), s_maxWaitDelay));
// Grab the current active window after everything settles
if (window == 0) {
window = m_plugin->activeWindow();
@ -543,16 +548,16 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
}
const int maxTypeDelay = 500;
const int maxWaitDelay = 10000;
const int maxRepetition = 100;
int currentTypingDelay = qBound(0, config()->get(Config::AutoTypeDelay).toInt(), maxTypeDelay);
int cumulativeDelay = qBound(0, config()->get(Config::AutoTypeStartDelay).toInt(), maxWaitDelay);
// Take into account the initial delay which is added before any actions are performed
int cumulativeDelay = qBound(s_minWaitDelay, config()->get(Config::AutoTypeStartDelay).toInt(), s_maxWaitDelay);
// Initial actions include start delay and initial inter-key delay
QList<QSharedPointer<AutoTypeAction>> actions;
actions << QSharedPointer<AutoTypeBegin>::create();
actions << QSharedPointer<AutoTypeDelay>::create(currentTypingDelay, true);
actions << QSharedPointer<AutoTypeDelay>::create(cumulativeDelay);
// Replace escaped braces with a template for easier regex
QString sequence = entrySequence;
@ -631,12 +636,12 @@ AutoType::parseSequence(const QString& entrySequence, const Entry* entry, QStrin
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, delay, maxTypeDelay), true);
} else if (placeholder == "delay") {
// Mid typing delay (wait), repeat represents the desired delay in milliseconds
if (repeat > maxWaitDelay) {
error = tr("Very long delay detected, max is %1: %2").arg(maxWaitDelay).arg(fullPlaceholder);
if (repeat > s_maxWaitDelay) {
error = tr("Very long delay detected, max is %1: %2").arg(s_maxWaitDelay).arg(fullPlaceholder);
return {};
}
cumulativeDelay += repeat;
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, maxWaitDelay));
actions << QSharedPointer<AutoTypeDelay>::create(qBound(0, repeat, s_maxWaitDelay));
} else if (placeholder == "clearfield") {
// Platform-specific field clearing
actions << QSharedPointer<AutoTypeClearField>::create();

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -125,14 +125,16 @@ PublicKeyCredential BrowserPasskeys::buildRegisterPublicKeyCredential(const QJso
QJsonObject BrowserPasskeys::buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
const QString& credentialId,
const QString& userHandle,
const QString& privateKeyPem)
const QString& privateKeyPem,
const bool beFlag,
const bool bsFlag)
{
if (!passkeyUtils()->checkCredentialAssertionOptions(assertionOptions)) {
return {};
}
const auto authenticatorData =
buildAuthenticatorData(assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString());
const auto authenticatorData = buildAuthenticatorData(
assertionOptions["rpId"].toString(), assertionOptions["extensions"].toString(), beFlag, bsFlag);
const auto clientDataJson = assertionOptions["clientDataJson"].toString();
const auto clientDataArray = clientDataJson.toUtf8();
@ -171,8 +173,12 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential
result.append(rpIdHash);
// Use default flags
const auto flags = setFlagsFromJson(QJsonObject(
{{"ED", !extensions.isEmpty()}, {"AT", true}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
const auto flags = setFlagsFromJson(QJsonObject({{"ED", !extensions.isEmpty()},
{"AT", true},
{"BS", DEFAULT_BS_FLAG},
{"BE", DEFAULT_BE_FLAG},
{"UV", true},
{"UP", true}}));
result.append(flags);
// Signature counter (not supported, always 0
@ -204,7 +210,10 @@ QByteArray BrowserPasskeys::buildAttestationObject(const QJsonObject& credential
}
// Build a short version of the attestation object for webauthn.get
QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QString& extensions)
QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId,
const QString& extensions,
const bool beFlag,
const bool bsFlag)
{
QByteArray result;
@ -212,7 +221,7 @@ QByteArray BrowserPasskeys::buildAuthenticatorData(const QString& rpId, const QS
result.append(rpIdHash);
const auto flags = setFlagsFromJson(QJsonObject(
{{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", false}, {"BE", false}, {"UV", true}, {"UP", true}}));
{{"ED", !extensions.isEmpty()}, {"AT", false}, {"BS", bsFlag}, {"BE", beFlag}, {"UV", true}, {"UP", true}}));
result.append(flags);
// Signature counter (not supported, always 0

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -25,6 +25,8 @@
#include <botan/asn1_obj.h>
#include <botan/bigint.h>
#define DEFAULT_BE_FLAG true
#define DEFAULT_BS_FLAG true
#define ID_BYTES 32
#define HASH_BYTES 32
#define RSA_BITS 2048
@ -87,7 +89,9 @@ public:
QJsonObject buildGetPublicKeyCredential(const QJsonObject& assertionOptions,
const QString& credentialId,
const QString& userHandle,
const QString& privateKeyPem);
const QString& privateKeyPem,
const bool beFlag = DEFAULT_BE_FLAG,
const bool bsFlag = DEFAULT_BE_FLAG);
static const QString AAGUID;
@ -113,7 +117,10 @@ private:
const QString& credentialId,
const QByteArray& cborEncodedPublicKey,
const TestingVariables& testingVariables = {});
QByteArray buildAuthenticatorData(const QString& rpId, const QString& extensions);
QByteArray buildAuthenticatorData(const QString& rpId,
const QString& extensions,
const bool beFlag = DEFAULT_BE_FLAG,
const bool bsFlag = DEFAULT_BE_FLAG);
AttestationKeyPair buildCredentialPrivateKey(int alg, const TestingVariables& testingVariables = {});
QByteArray
buildSignature(const QByteArray& authenticatorData, const QByteArray& clientData, const QString& privateKeyPem);

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
* Copyright (C) 2013 Francois Ferrand
*
@ -775,8 +775,16 @@ QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject&
const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
const auto userHandle = selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_USER_HANDLE);
auto publicKeyCredential =
browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
// Get BE and BS flags if present
const auto beFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BE)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BE) == TRUE_STR
: DEFAULT_BE_FLAG;
const auto bsFlag = selectedEntry->attributes()->hasKey(EntryAttributes::KPEX_PASSKEY_FLAG_BS)
? selectedEntry->attributes()->value(EntryAttributes::KPEX_PASSKEY_FLAG_BS) == TRUE_STR
: DEFAULT_BS_FLAG;
auto publicKeyCredential = browserPasskeys()->buildGetPublicKeyCredential(
assertionOptions, credentialId, userHandle, privateKeyPem, beFlag, bsFlag);
if (publicKeyCredential.isEmpty()) {
return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
}
@ -856,6 +864,8 @@ void BrowserService::addPasskeyToEntry(Entry* entry,
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_RELYING_PARTY, rpId);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BE, TRUE_STR);
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, TRUE_STR);
entry->addTag(tr("Passkey"));
entry->endUpdate();
@ -1013,8 +1023,8 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
}
for (const auto& group : rootGroup->groupsRecursive(true)) {
if (group->isRecycled()
|| group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable) {
const auto groupOptionHideEntry = group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY);
if (group->isRecycled() || groupOptionHideEntry == Group::Enable) {
continue;
}
@ -1029,7 +1039,8 @@ QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
for (auto* entry : group->entries()) {
if (entry->isRecycled()
|| (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
|| (groupOptionHideEntry == Group::Inherit
&& entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
&& entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
continue;
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -158,7 +158,6 @@ void CustomData::copyDataFrom(const CustomData* other)
m_data = other->m_data;
updateLastModified();
emit reset();
emitModified();
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@ -46,6 +46,8 @@ const QString EntryAttributes::KPEX_PASSKEY_RELYING_PARTY = QStringLiteral("KPEX
const QString EntryAttributes::KPEX_PASSKEY_USER_HANDLE = QStringLiteral("KPEX_PASSKEY_USER_HANDLE");
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_START = QStringLiteral("-----BEGIN PRIVATE KEY-----");
const QString EntryAttributes::KPEX_PASSKEY_PRIVATE_KEY_END = QStringLiteral("-----END PRIVATE KEY-----");
const QString EntryAttributes::KPEX_PASSKEY_FLAG_BE = QStringLiteral("KPEX_PASSKEY_FLAG_BE");
const QString EntryAttributes::KPEX_PASSKEY_FLAG_BS = QStringLiteral("KPEX_PASSKEY_FLAG_BS");
// For compatibility with StrongBox
const QString EntryAttributes::KPEX_PASSKEY_GENERATED_USER_ID = QStringLiteral("KPEX_PASSKEY_GENERATED_USER_ID");

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@ -75,6 +75,8 @@ public:
static const QString KPEX_PASSKEY_USER_HANDLE;
static const QString KPEX_PASSKEY_PRIVATE_KEY_START;
static const QString KPEX_PASSKEY_PRIVATE_KEY_END;
static const QString KPEX_PASSKEY_FLAG_BE;
static const QString KPEX_PASSKEY_FLAG_BS;
static bool isDefaultAttribute(const QString& key);
static bool isPasskeyAttribute(const QString& key);

View file

@ -261,6 +261,48 @@ namespace
return entry.take();
}
Group* createGroup(Group* rootGroup, const QString& folderName)
{
Group* currentParentGroup = rootGroup;
Group* result = nullptr;
const auto groups = folderName.split("/", Qt::SkipEmptyParts);
// Returns the group name based on depth
const auto getGroupName = [&](const int depth) {
QString groupName;
for (int i = 0; i < depth + 1; ++i) {
groupName.append((i == 0 ? "" : "/") + groups[i]);
}
return groupName;
};
// Create new group(s) always when the path is not found
for (int i = 0; i < groups.length(); ++i) {
const auto groupName = getGroupName(i);
const auto tempGroup = rootGroup->findGroupByPath(groupName);
if (!tempGroup) {
const auto newGroup = new Group();
newGroup->setName(groups[i]);
newGroup->setUuid(QUuid::createUuid());
newGroup->setParent(currentParentGroup);
currentParentGroup = newGroup;
if (groupName == folderName) {
result = newGroup;
}
continue;
}
if (groupName == folderName) {
result = tempGroup;
}
currentParentGroup = tempGroup;
}
return result;
}
void writeVaultToDatabase(const QJsonObject& vault, QSharedPointer<Database> db)
{
auto folderField = QString("folders");
@ -277,12 +319,12 @@ namespace
// Create groups from folders and store a temporary map of id -> uuid
QMap<QString, Group*> folderMap;
for (const auto& folder : vault.value(folderField).toArray()) {
auto group = new Group();
group->setUuid(QUuid::createUuid());
group->setName(folder.toObject().value("name").toString());
group->setParent(db->rootGroup());
const auto folderId = folder.toObject().value("id").toString();
const auto folderName = folder.toObject().value("name").toString();
folderMap.insert(folder.toObject().value("id").toString(), group);
if (const auto group = createGroup(db->rootGroup(), folderName)) {
folderMap.insert(folderId, group);
}
}
QString folderId;

View file

@ -246,6 +246,7 @@ void EditEntryWidget::setupAdvanced()
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(toggleCurrentAttributeVisibility()));
connect(m_advancedUi->copyTextButton, SIGNAL(clicked(bool)), SLOT(copyAttributeText()));
connect(m_advancedUi->attributesView->selectionModel(),
SIGNAL(currentChanged(QModelIndex,QModelIndex)),
SLOT(updateCurrentAttribute()));
@ -350,25 +351,31 @@ void EditEntryWidget::updateBrowser()
return;
}
auto changeValue = [&](const QString& option, const bool newValue) {
// If value is false and no customData exists, make no edits
if (!m_customData->hasKey(option) && !newValue) {
return;
}
// If customData exists, set the value
m_customData->set(option, (newValue ? TRUE_STR : FALSE_STR));
};
// Only update the custom data if no group level settings are used (checkbox is enabled)
if (m_browserUi->hideEntryCheckbox->isEnabled()) {
auto hide = m_browserUi->hideEntryCheckbox->isChecked();
m_customData->set(BrowserService::OPTION_HIDE_ENTRY, (hide ? TRUE_STR : FALSE_STR));
changeValue(BrowserService::OPTION_HIDE_ENTRY, m_browserUi->hideEntryCheckbox->isChecked());
}
if (m_browserUi->skipAutoSubmitCheckbox->isEnabled()) {
auto skip = m_browserUi->skipAutoSubmitCheckbox->isChecked();
m_customData->set(BrowserService::OPTION_SKIP_AUTO_SUBMIT, (skip ? TRUE_STR : FALSE_STR));
changeValue(BrowserService::OPTION_SKIP_AUTO_SUBMIT, m_browserUi->skipAutoSubmitCheckbox->isChecked());
}
if (m_browserUi->onlyHttpAuthCheckbox->isEnabled()) {
auto onlyHttpAuth = m_browserUi->onlyHttpAuthCheckbox->isChecked();
m_customData->set(BrowserService::OPTION_ONLY_HTTP_AUTH, (onlyHttpAuth ? TRUE_STR : FALSE_STR));
changeValue(BrowserService::OPTION_ONLY_HTTP_AUTH, m_browserUi->onlyHttpAuthCheckbox->isChecked());
}
if (m_browserUi->notHttpAuthCheckbox->isEnabled()) {
auto notHttpAuth = m_browserUi->notHttpAuthCheckbox->isChecked();
m_customData->set(BrowserService::OPTION_NOT_HTTP_AUTH, (notHttpAuth ? TRUE_STR : FALSE_STR));
changeValue(BrowserService::OPTION_NOT_HTTP_AUTH, m_browserUi->notHttpAuthCheckbox->isChecked());
}
}
@ -989,6 +996,7 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
m_advancedUi->addAttributeButton->setEnabled(!m_history);
m_advancedUi->editAttributeButton->setEnabled(false);
m_advancedUi->removeAttributeButton->setEnabled(false);
m_advancedUi->copyTextButton->setEnabled(false);
m_advancedUi->attributesEdit->setReadOnly(m_history);
QAbstractItemView::EditTriggers editTriggers;
if (m_history) {
@ -1080,55 +1088,26 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
setupBrowser();
}
auto hideEntriesCheckBoxEnabled = true;
auto skipAutoSubmitCheckBoxEnabled = true;
auto onlyHttpAuthCheckBoxEnabled = true;
auto notHttpAuthCheckBoxEnabled = true;
auto hideEntries = false;
auto skipAutoSubmit = false;
auto onlyHttpAuth = false;
auto notHttpAuth = false;
const auto group = m_entry->group();
if (group) {
hideEntries = group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable;
skipAutoSubmit = group->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == Group::Enable;
onlyHttpAuth = group->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Enable;
notHttpAuth = group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Enable;
m_browserUi->messageWidget->showMessage(
tr("Some Browser Integration settings are overridden by group settings."), MessageWidget::Information);
m_browserUi->messageWidget->setVisible(false);
hideEntriesCheckBoxEnabled =
group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Inherit;
skipAutoSubmitCheckBoxEnabled =
group->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == Group::Inherit;
onlyHttpAuthCheckBoxEnabled =
group->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Inherit;
notHttpAuthCheckBoxEnabled =
group->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Inherit;
}
auto updateCheckBoxValue = [&](QCheckBox* checkBox, const QString& option) {
const auto optionEnabledInGroup = group ? group->resolveBrowserOptionEnabled(option) : false;
const auto optionInherited = group ? group->resolveCustomDataTriState(option) == Group::Inherit : true;
// Show information about group level settings
if (!hideEntriesCheckBoxEnabled || !skipAutoSubmitCheckBoxEnabled || !onlyHttpAuthCheckBoxEnabled
|| !notHttpAuthCheckBoxEnabled) {
m_browserUi->messageWidget->showMessage(
tr("Some Browser Integration settings are overridden by group settings."), MessageWidget::Information);
m_browserUi->messageWidget->setVisible(true);
}
if (!optionInherited) {
m_browserUi->messageWidget->setVisible(true);
}
// Disable checkboxes based on group level settings
updateBrowserIntegrationCheckbox(
m_browserUi->hideEntryCheckbox, hideEntriesCheckBoxEnabled, hideEntries, BrowserService::OPTION_HIDE_ENTRY);
updateBrowserIntegrationCheckbox(m_browserUi->skipAutoSubmitCheckbox,
skipAutoSubmitCheckBoxEnabled,
skipAutoSubmit,
BrowserService::OPTION_SKIP_AUTO_SUBMIT);
updateBrowserIntegrationCheckbox(m_browserUi->onlyHttpAuthCheckbox,
onlyHttpAuthCheckBoxEnabled,
onlyHttpAuth,
BrowserService::OPTION_ONLY_HTTP_AUTH);
updateBrowserIntegrationCheckbox(m_browserUi->notHttpAuthCheckbox,
notHttpAuthCheckBoxEnabled,
notHttpAuth,
BrowserService::OPTION_NOT_HTTP_AUTH);
updateBrowserIntegrationCheckbox(checkBox, optionInherited, optionEnabledInGroup, option);
};
updateCheckBoxValue(m_browserUi->hideEntryCheckbox, BrowserService::OPTION_HIDE_ENTRY);
updateCheckBoxValue(m_browserUi->skipAutoSubmitCheckbox, BrowserService::OPTION_SKIP_AUTO_SUBMIT);
updateCheckBoxValue(m_browserUi->onlyHttpAuthCheckbox, BrowserService::OPTION_ONLY_HTTP_AUTH);
updateCheckBoxValue(m_browserUi->notHttpAuthCheckbox, BrowserService::OPTION_NOT_HTTP_AUTH);
m_browserUi->addURLButton->setEnabled(!m_history);
m_browserUi->removeURLButton->setEnabled(false);
@ -1518,12 +1497,15 @@ void EditEntryWidget::displayAttribute(QModelIndex index, bool showProtected)
m_advancedUi->protectAttributeButton->setEnabled(!m_history);
m_advancedUi->editAttributeButton->setEnabled(!m_history);
m_advancedUi->removeAttributeButton->setEnabled(!m_history);
m_advancedUi->copyTextButton->setEnabled(true);
} else {
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->copyTextButton->setEnabled(false);
m_advancedUi->editAttributeButton->setEnabled(false);
m_advancedUi->removeAttributeButton->setEnabled(false);
}
@ -1568,6 +1550,22 @@ void EditEntryWidget::toggleCurrentAttributeVisibility()
}
}
void EditEntryWidget::copyAttributeText()
{
QString value;
if (m_advancedUi->attributesEdit->isEnabled()) {
value = m_advancedUi->attributesEdit->toPlainText();
} else {
QModelIndex index = m_advancedUi->attributesView->currentIndex();
if (index.isValid()) {
QString key = m_attributesModel->keyByIndex(index);
value = m_entryAttributes->value(key);
}
}
clipboard()->setText(value);
}
void EditEntryWidget::updateAutoTypeEnabled()
{
bool autoTypeEnabled = m_autoTypeUi->enableButton->isChecked();

View file

@ -107,6 +107,7 @@ private slots:
void updateCurrentAttribute();
void protectCurrentAttribute(bool state);
void toggleCurrentAttributeVisibility();
void copyAttributeText();
void updateAutoTypeEnabled();
void openAutotypeHelp();
void insertAutoTypeAssoc();

View file

@ -32,7 +32,7 @@
<item>
<widget class="QSplitter" name="attributesSplitter">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="childrenCollapsible">
<bool>false</bool>
@ -48,10 +48,10 @@
<string>Attribute selection</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QAbstractScrollArea::AdjustToContents</enum>
<enum>QAbstractScrollArea::SizeAdjustPolicy::AdjustToContents</enum>
</property>
<property name="resizeMode">
<enum>QListView::Adjust</enum>
<enum>QListView::ResizeMode::Adjust</enum>
</property>
</widget>
<widget class="QPlainTextEdit" name="attributesEdit">
@ -117,7 +117,7 @@
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -146,6 +146,16 @@
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="copyTextButton">
<property name="accessibleName">
<string>Copy text into clipboard</string>
</property>
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="revealAttributeButton">
<property name="enabled">
@ -236,10 +246,10 @@
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Maximum</enum>
<enum>QSizePolicy::Policy::Maximum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
@ -284,7 +294,7 @@
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -854,3 +854,56 @@ void TestBrowser::testRestrictBrowserKey()
QCOMPARE(sorted[2]->url(), QString("https://example.com/2"));
QCOMPARE(sorted[3]->url(), QString("https://example.com/0"));
}
void TestBrowser::testHideEntry()
{
const auto db = QSharedPointer<Database>::create();
auto* root = db->rootGroup();
const auto entry = new Entry();
entry->setGroup(root);
entry->beginUpdate();
entry->setUrl(QString("https://github.com/"));
entry->setUsername(QString("User 1"));
entry->setUuid(QUuid::createUuid());
entry->setTitle(QString("Name_ 1"));
entry->endUpdate();
// Entry should be found normally
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/"));
// Hide entry from entry settings, group setting is inherited
entry->customData()->set(BrowserService::OPTION_HIDE_ENTRY, TRUE_STR);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 0);
// Disable hide from group settings, entry should be found
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Disable);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
// Enable hide from group setting, entry should not be found
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Enable);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 0);
// Remove the hide settings from entry, return group setting to inherit
entry->customData()->set(BrowserService::OPTION_HIDE_ENTRY, FALSE_STR);
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Inherit);
// Entry should be found again
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
// Enable hide from group setting, entry should not be found
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Enable);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 0);
// Disable hide from group settings, entry should be found
root->setCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY, Group::Disable);
result = m_browserService->searchEntries(db, "https://github.com", "https://github.com/session");
QCOMPARE(result.length(), 1);
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -51,6 +51,7 @@ private slots:
void testBestMatchingCredentials();
void testBestMatchingWithAdditionalURLs();
void testRestrictBrowserKey();
void testHideEntry();
private:
QList<Entry*> createEntries(QStringList& urls, Group* root, bool additionalUrl = false) const;

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -327,6 +327,106 @@ void TestImports::testBitwardenPasskey()
QStringLiteral("aTFtdmFnOHYtS2dxVEJ0by1rSFpLWGg0enlTVC1iUVJReDZ5czJXa3c2aw"));
}
void TestImports::testBitwardenNestedFolders()
{
auto bitwardenPath =
QStringLiteral("%1/%2").arg(KEEPASSX_TEST_DATA_DIR, QStringLiteral("/bitwarden_nested_export.json"));
BitwardenReader reader;
auto db = reader.convert(bitwardenPath);
QVERIFY2(!reader.hasError(), qPrintable(reader.errorString()));
QVERIFY(db);
/* The group tree should be:
/
- Example
- Test Authentication
/SecondTest
- GMail entry
/Test
- Gmail test 2
/Subfolder
- Webauthn.io test 2
/Subfolder
- Test Account
/SubFolder
- WebAuthn.io test
/AnotherSubFolder
- Another test account
- Webauthn.io test 3
*/
// Verify groups
auto secondTestGroup = db->rootGroup()->findGroupByPath("/SecondTest");
QVERIFY(secondTestGroup);
auto testGroup = db->rootGroup()->findGroupByPath("/Test");
QVERIFY(testGroup);
auto testSubfolderLowercaseGroup = db->rootGroup()->findGroupByPath("/Test/Subfolder");
QVERIFY(testSubfolderLowercaseGroup);
auto testSubFolderGroup = db->rootGroup()->findGroupByPath("/Test/SubFolder");
QVERIFY(testSubFolderGroup);
auto longGroup = db->rootGroup()->findGroupByPath("/Test/SubFolder/AnotherSubFolder");
QVERIFY(longGroup);
// Verify entries and the groups they belong to
// GMail entry
auto entry = db->rootGroup()->findEntryByPath("/SecondTest/GMail entry");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("example@gmail.com"));
QCOMPARE(entry->group(), secondTestGroup);
// Test Authentication
entry = db->rootGroup()->findEntryByPath("/Test Authentication");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("test@testauthentication.com"));
QCOMPARE(entry->group(), db->rootGroup());
// Gmail test 2
entry = db->rootGroup()->findEntryByPath("/Test/Gmail test 2");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("example2@gmail.com"));
QCOMPARE(entry->group(), testGroup);
// Example
entry = db->rootGroup()->findEntryByPath("/Example");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("user@example.com"));
QCOMPARE(entry->group(), db->rootGroup());
// WebAuthn.io test
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/WebAuthn.io test");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("testUser"));
QCOMPARE(entry->group(), testSubFolderGroup);
// Webauthn.io test 2
entry = db->rootGroup()->findEntryByPath("/Test/Subfolder/Webauthn.io test 2");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("testUser2"));
QCOMPARE(entry->group(), testSubfolderLowercaseGroup);
// Webauthn.io test 3
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/AnotherSubFolder/Webauthn.io test 3");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("testUser3"));
QCOMPARE(entry->group(), longGroup);
// Test Account
// There are two groups with an identical name. The group for this entry should not be the same group with the
// Webauthn.io test 2, but we cannot distinguish these.
entry = db->rootGroup()->findEntryByPath("/Test/Subfolder/Test Account");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("test-account"));
QCOMPARE(entry->group(), testSubfolderLowercaseGroup);
// Another test account
entry = db->rootGroup()->findEntryByPath("/Test/SubFolder/AnotherSubFolder/Another test account");
QVERIFY(entry);
QCOMPARE(entry->username(), QStringLiteral("anotherUser"));
QCOMPARE(entry->group(), longGroup);
}
void TestImports::testProtonPass()
{
auto protonPassPath =

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -31,6 +31,7 @@ private slots:
void testBitwarden();
void testBitwardenEncrypted();
void testBitwardenPasskey();
void testBitwardenNestedFolders();
void testProtonPass();
};

View file

@ -1,5 +1,5 @@
/*
* Copyright (C) 2025 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2026 KeePassXC Team <team@keepassxc.org>
*
* 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
@ -77,7 +77,7 @@ const QString PublicKeyCredential = R"(
"id": "yrzFJ5lwcpTwYMOdXSmxF5b5cYQlqBMzbbU_d-oFLO8",
"rawId": "cabcc52799707294f060c39d5d29b11796f9718425a813336db53f77ea052cef",
"response": {
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBFAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU",
"attestationObject": "o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVikdKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvBdAAAAAP2xQbJdhEQ-ijVGmMIFpQIAIMq8xSeZcHKU8GDDnV0psReW-XGEJagTM221P3fqBSzvpQECAyYgASFYIHK1iVimeR02UYipyiEKrKhhfhJRMew8EbDWGKtMZ2wUIlggbtZ70X11SLx17QFDWVAR3_qqk5OqrRS--Whc7hyw9YU",
"clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoibFZlSHpWeFdzcjhNUXhNa1pGMHRpNkZYaGRnTWxqcUt6Z0EtcV96azJNbmlpM2VKNDdWRjk3c3FVb1lrdFZDODVXQVoxdUlBU20tYV9sREZad3NMZnciLCJvcmlnaW4iOiJodHRwczovL3dlYmF1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"
},
"type": "public-key"
@ -185,6 +185,8 @@ void TestPasskeys::testDecodeResponseData()
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
QCOMPARE(flags["AT"], true);
QCOMPARE(flags["UP"], true);
QCOMPARE(flags["BE"], true);
QCOMPARE(flags["BS"], true);
QCOMPARE(publicKey["1"], 2);
QCOMPARE(publicKey["3"], -7);
QCOMPARE(publicKey["-1"], 1);
@ -285,13 +287,18 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
result,
QString("\xA3"
"cfmtdnonegattStmt\xA0hauthDataX\xA4t\xA6\xEA\x92\x13\xC9\x9C/t\xB2$\x92\xB3 \xCF@&*\x94\xC1\xA9P\xA0"
"9\x7F)%\x0B`\x84\x1E\xF0"
"E\x00\x00\x00\x01\x01\x02\x03\x04\x05\x06\x07\b\x01\x02\x03\x04\x05\x06\x07\b\x00 \x8B\xB0\xCA"
"6\x17\xD6\xDE\x01\x11|\xEA\x94\r\xA0R\xC0\x80_\xF3r\xFBr\xB5\x02\x03:"
"\xBAr\x0Fi\x81\xFE\xA5\x01\x02\x03& \x01!X "
"e\xE2\xF2\x1F:cq\xD3G\xEA\xE0\xF7\x1F\xCF\xFA\\\xABO\xF6\x86\x88\x80\t\xAE\x81\x8BT\xB2\x9B\x15\x85~"
"\"X \\\x8E\x1E@\xDB\x97T-\xF8\x9B\xB0\xAD"
"5\xDC\x12^\xC3\x95\x05\xC6\xDF^\x03\xCB\xB4Q\x91\xFF|\xDB\x94\xB7"));
"9\x7F)%\x0B`\x84\x1E\xF0]\x00\x00\x00\x00\xFD\xB1"
"A\xB2]\x84"
"D>\x8A"
"5F\x98\xC2\x05\xA5\x02\x00 \xCA\xBC\xC5'\x99pr\x94\xF0`\xC3\x9D])\xB1\x17\x96\xF9q\x84%\xA8\x13"
"3m\xB5?w\xEA\x05,\xEF\xA5\x01\x02\x03& \x01!X \x06\xEC\xAF"
"4[b\x91"
"am\x19Y\x03\xA6P*\xCA"
"1\xC4\x95\xA8i\xE5\xF0\x87\xE5\xD4\xB8"
"2\xCD\b\x85\xDD\"X \xE2\xEE\x7F\xE9\x0F\x0E\xE9\x1D\x07\x83J\x03\t\xDB"
"B$\xB1\x0B\xD3%\xFF\x18"
"2\xE1S\x99\xB7\x1D"
"B\x04\xE7\x83"));
// Double check that the result can be decoded
BrowserCbor browserCbor;
@ -312,6 +319,8 @@ void TestPasskeys::testCreatingAttestationObjectWithEC()
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
QCOMPARE(flags["AT"], true);
QCOMPARE(flags["UP"], true);
QCOMPARE(flags["BE"], true);
QCOMPARE(flags["BS"], true);
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::EC2);
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::ES256);
QCOMPARE(publicKey["-1"], 1);
@ -368,6 +377,8 @@ void TestPasskeys::testCreatingAttestationObjectWithRSA()
QCOMPARE(authData["rpIdHash"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvA"));
QCOMPARE(flags["AT"], true);
QCOMPARE(flags["UP"], true);
QCOMPARE(flags["BE"], true);
QCOMPARE(flags["BS"], true);
QCOMPARE(publicKey["1"], WebAuthnCoseKeyType::RSA);
QCOMPARE(publicKey["3"], WebAuthnAlgorithms::RS256);
QCOMPARE(publicKey["-1"], predefinedModulus);
@ -438,14 +449,14 @@ void TestPasskeys::testGet()
QCOMPARE(publicKeyCredential["id"].toString(), id);
auto response = publicKeyCredential["response"].toObject();
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAFAAAAAA"));
QCOMPARE(response["authenticatorData"].toString(), QString("dKbqkhPJnC90siSSsyDPQCYqlMGpUKA5fyklC2CEHvAdAAAAAA"));
QCOMPARE(response["clientDataJSON"].toString(),
QString("eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiOXozNnZUZlFUTDk1TGY3V25aZ3l0ZTdvaEdlRi1YUmlMeGtML"
"Ux1R1Uxem9wUm1NSVVBMUxWd3pHcHlJbTFmT0JuMVFuUmEwUUgyN0FEQWFKR0h5c1EiLCJvcmlnaW4iOiJodHRwczovL3dlYm"
"F1dGhuLmlvIiwiY3Jvc3NPcmlnaW4iOmZhbHNlfQ"));
QCOMPARE(
response["signature"].toString(),
QString("MEYCIQCpbDaYJ4b2ofqWBxfRNbH3XCpsyao7Iui5lVuJRU9HIQIhAPl5moNZgJu5zmurkKK_P900Ct6wd3ahVIqCEqTeeRdE"));
QString("MEUCIQCvg3nXO2fiNK9ockxscgPtoM9_u6ERaW2-F1L99YasOAIgNhYOjPJyKJ-W8roV531kC59ss1USas7jy8TfRnbJLtg"));
auto clientDataJson = response["clientDataJSON"].toString();
auto clientDataByteArray = browserMessageBuilder()->getArrayFromBase64(clientDataJson);

View file

@ -0,0 +1,249 @@
{
"encrypted": false,
"folders": [
{
"id": "53f3c6e7-a167-47e2-91bb-b3f900a377a3",
"name": "SecondTest"
},
{
"id": "14f22922-b8ed-4e9c-814b-b3f900a36a92",
"name": "Test"
},
{
"id": "da442766-39b4-4fb1-a2f3-b3f900a3a36d",
"name": "Test/Subfolder"
},
{
"id": "0504d89b-00aa-41a5-9355-b3f900a3b43f",
"name": "Test/Subfolder"
},
{
"id": "c96cf0e9-fd44-4a7e-9619-b3f900a58e59",
"name": "Test/SubFolder"
},
{
"id": "5d262faf-329d-4197-9e8d-b3f900a3934c",
"name": "Test/SubFolder/AnotherSubFolder"
}
],
"items": [
{
"passwordHistory": [],
"revisionDate": "2026-02-22T09:57:23.033Z",
"creationDate": "2026-02-17T16:55:23.210Z",
"id": "6b154a7d-4b62-44aa-ae0d-b3f40116e266",
"folderId": "53f3c6e7-a167-47e2-91bb-b3f900a377a3",
"type": 1,
"reprompt": 0,
"name": "GMail entry",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://accounts.google.com"
}
],
"fido2Credentials": [],
"username": "example@gmail.com",
"password": "examplePassword",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-20T17:45:32.670Z",
"creationDate": "2026-02-20T17:45:32.670Z",
"id": "6ccafb74-ecab-482f-88b2-b3f70124a91b",
"type": 1,
"reprompt": 0,
"name": "Test Authentication",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://testauthentication.com/login"
}
],
"fido2Credentials": [],
"username": "test@testauthentication.com",
"password": "testPassword",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-22T09:57:15.540Z",
"creationDate": "2026-02-06T19:22:48.860Z",
"id": "a9f00893-346e-4be6-ac4e-b3e9013f6065",
"folderId": "14f22922-b8ed-4e9c-814b-b3f900a36a92",
"type": 1,
"reprompt": 0,
"name": "Gmail test 2",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://accounts.google.com"
}
],
"fido2Credentials": [],
"username": "example2@gmail.com",
"password": "examplePassword2",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-09T13:15:16.370Z",
"creationDate": "2026-02-09T13:15:16.113Z",
"id": "b375fe89-756a-41be-bcec-b3ec00da6d55",
"type": 1,
"reprompt": 0,
"name": "Example",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://www.example.com/"
}
],
"username": "user@example.com",
"password": "examplePassword",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-22T10:03:05.596Z",
"creationDate": "2026-02-09T13:15:45.020Z",
"id": "b42284e2-a103-4dd0-982c-b3ec00da8f36",
"folderId": "c96cf0e9-fd44-4a7e-9619-b3f900a58e59",
"type": 1,
"reprompt": 0,
"name": "WebAuthn.io test",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://webauthn.io/"
}
],
"username": "testUser",
"password": "testPassword",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-22T09:56:58.923Z",
"creationDate": "2024-10-23T16:38:08.606Z",
"id": "a8e579f0-98c2-4ac9-a126-b212011225f8",
"folderId": "da442766-39b4-4fb1-a2f3-b3f900a3a36d",
"type": 1,
"reprompt": 0,
"name": "Webauthn.io test 2",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://webauthn.io/"
}
],
"username": "testUser2",
"password": "testPassword2",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-22T09:56:46.026Z",
"creationDate": "2025-10-29T06:13:55.333Z",
"id": "a88363cf-9fea-43d8-a3dd-b3850066b36a",
"folderId": "5d262faf-329d-4197-9e8d-b3f900a3934c",
"type": 1,
"reprompt": 0,
"name": "Webauthn.io test 3",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://webauthn.io/"
}
],
"username": "testUser3",
"password": "testPassword3",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2025-09-12T16:25:15.850Z",
"creationDate": "2025-09-12T16:25:15.850Z",
"id": "d2946603-1bfc-4eee-8805-b356010e9c65",
"folderId": "0504d89b-00aa-41a5-9355-b3f900a3b43f",
"type": 1,
"reprompt": 0,
"name": "Test Account",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://testsite.com/"
}
],
"fido2Credentials": [],
"username": "test-account",
"password": "test",
"totp": null
},
"collectionIds": null
},
{
"passwordHistory": [],
"revisionDate": "2026-02-22T09:56:52.673Z",
"creationDate": "2024-10-23T19:17:45.433Z",
"id": "4e3b570e-3ead-4557-b5be-b212013dfcd0",
"folderId": "5d262faf-329d-4197-9e8d-b3f900a3934c",
"type": 1,
"reprompt": 0,
"name": "Another test account",
"notes": null,
"favorite": false,
"fields": [],
"login": {
"uris": [
{
"uri": "https://anothertestsite.org/"
}
],
"username": "anotherUser",
"password": "anotherPassword",
"totp": null
},
"collectionIds": null
}
]
}

View file

@ -357,9 +357,8 @@ void TestGui::testMergeDatabase()
fileDialog()->setNextFileName(QString(KEEPASSX_TEST_DATA_DIR).append("/MergeDatabase.kdbx"));
triggerAction("actionDatabaseMerge");
auto* editPasswordMerge = QApplication::focusWidget();
QVERIFY(editPasswordMerge);
QTRY_COMPARE(editPasswordMerge->objectName(), QString("passwordEdit"));
QWidget* editPasswordMerge;
QTRY_VERIFY((editPasswordMerge = QApplication::focusWidget()) && editPasswordMerge->objectName() == "passwordEdit");
QVERIFY(editPasswordMerge->isVisible());
QTest::keyClicks(editPasswordMerge, "a");
@ -462,9 +461,8 @@ void TestGui::testRemoteSyncDatabaseRequiresPassword()
// need to process more events as opening with the same key did not work and more events have been fired
QApplication::processEvents(QEventLoop::WaitForMoreEvents);
auto* editPasswordSync = QApplication::focusWidget();
QVERIFY(editPasswordSync);
QTRY_COMPARE(editPasswordSync->objectName(), QString("passwordEdit"));
QWidget* editPasswordSync;
QTRY_VERIFY((editPasswordSync = QApplication::focusWidget()) && editPasswordSync->objectName() == "passwordEdit");
QVERIFY(editPasswordSync->isVisible());
QTest::keyClicks(editPasswordSync, "b");
@ -2095,18 +2093,28 @@ void TestGui::testTrayRestoreHide()
trayIcon->activated(QSystemTrayIcon::Trigger);
QTRY_VERIFY(m_mainWindow->isVisible());
// Wait out window hide grace period before triggering tray icon again
int gracePeriod = 250;
#ifdef Q_OS_WIN
// Windows requires a shorter grace period
gracePeriod = 50;
#endif
Tools::wait(gracePeriod);
trayIcon->activated(QSystemTrayIcon::Trigger);
QTRY_VERIFY(!m_mainWindow->isVisible());
trayIcon->activated(QSystemTrayIcon::MiddleClick);
QTRY_VERIFY(m_mainWindow->isVisible());
Tools::wait(gracePeriod);
trayIcon->activated(QSystemTrayIcon::MiddleClick);
QTRY_VERIFY(!m_mainWindow->isVisible());
trayIcon->activated(QSystemTrayIcon::DoubleClick);
QTRY_VERIFY(m_mainWindow->isVisible());
Tools::wait(gracePeriod);
trayIcon->activated(QSystemTrayIcon::DoubleClick);
QTRY_VERIFY(!m_mainWindow->isVisible());

View file

@ -43,10 +43,7 @@ void TestImageAttachmentsWidget::testFitInView()
auto zoomFactor = m_imageAttachmentsView->transform();
m_widget->setMinimumSize(m_widget->size() + QSize{100, 100});
QCoreApplication::processEvents();
QVERIFY(zoomFactor != m_imageAttachmentsView->transform());
QTRY_VERIFY(zoomFactor != m_imageAttachmentsView->transform());
}
void TestImageAttachmentsWidget::testZoomCombobox()
@ -56,10 +53,7 @@ void TestImageAttachmentsWidget::testZoomCombobox()
QVERIFY(index != -1);
m_zoomCombobox->setCurrentIndex(index);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(zoom, zoom));
}
}
@ -67,10 +61,7 @@ void TestImageAttachmentsWidget::testEditZoomCombobox()
{
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString::number(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
}
@ -79,19 +70,13 @@ void TestImageAttachmentsWidget::testEditWithPercentZoomCombobox()
// Example 100 %
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString("%1 %").arg(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
// Example 100%
for (double i = 0.25; i < 5; i += 0.25) {
m_zoomCombobox->setCurrentText(QString("%1%").arg(i * 100));
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
QTRY_COMPARE(m_imageAttachmentsView->transform(), QTransform::fromScale(i, i));
}
}
@ -108,10 +93,7 @@ void TestImageAttachmentsWidget::testInvalidValueZoomCombobox()
for (const auto& invalidValue : {"Help", "3,4", "", ".", "% 100"}) {
m_zoomCombobox->setCurrentText(invalidValue);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectedTransform);
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectedTransform);
}
}
@ -208,10 +190,7 @@ void TestImageAttachmentsWidget::testZoomLowerBound()
true);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectTransform);
}
void TestImageAttachmentsWidget::testZoomUpperBound()
@ -237,8 +216,5 @@ void TestImageAttachmentsWidget::testZoomUpperBound()
true);
QCoreApplication::sendEvent(m_imageAttachmentsView->viewport(), &event);
QCoreApplication::processEvents();
QCOMPARE(m_imageAttachmentsView->transform(), expectTransform);
QTRY_COMPARE(m_imageAttachmentsView->transform(), expectTransform);
}