mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2026-03-11 08:54:48 +00:00
Compare commits
13 commits
b370e21c0a
...
15c54f6c6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15c54f6c6a | ||
|
|
d9be1b0682 | ||
|
|
56521023b8 | ||
|
|
fd10c9dea1 | ||
|
|
5c91c9986a | ||
|
|
cc309ff813 | ||
|
|
a1cddad37c | ||
|
|
2dd56c76cc | ||
|
|
b45cea0d9c | ||
|
|
1ce2ce9bb8 | ||
|
|
6f6076ab81 | ||
|
|
ec034eaf7d | ||
|
|
f50d3f42f6 |
23 changed files with 646 additions and 110 deletions
|
|
@ -422,6 +422,23 @@ if (MSVC)
|
|||
endif()
|
||||
|
||||
if(WIN32)
|
||||
# Additional search paths for Windows if not building/running in Visual Studio environment (e.g. commandline or VSCode)
|
||||
|
||||
# 1.Resolve the ambiguity of using two names for one architecture
|
||||
if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64" OR CMAKE_SYSTEM_PROCESSOR STREQUAL "x64")
|
||||
set(ARCH_DIR "x64")
|
||||
else()
|
||||
set(ARCH_DIR "${CMAKE_SYSTEM_PROCESSOR}")
|
||||
endif()
|
||||
# 2. Locate Windows SDK Paths
|
||||
if (CMAKE_WINDOWS_KITS_10_DIR)
|
||||
set(WINSDKROOTC_INCLUDE "${CMAKE_WINDOWS_KITS_10_DIR}/Include/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/um")
|
||||
set(WINSDKROOTC_LIB "${CMAKE_WINDOWS_KITS_10_DIR}/LIB/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/um/${ARCH_DIR}")
|
||||
else()
|
||||
set(WINSDKROOTC_INCLUDE "$ENV{ProgramFiles\(x86\)}/Windows Kits/10/Include/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/um")
|
||||
set(WINSDKROOTC_LIB "$ENV{ProgramFiles\(x86\)}/Windows Kits/10/LIB/${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}/um/${ARCH_DIR}")
|
||||
endif()
|
||||
|
||||
set(CMAKE_RC_COMPILER_INIT windres)
|
||||
enable_language(RC)
|
||||
if(MINGW)
|
||||
|
|
@ -443,7 +460,11 @@ if(WIN32)
|
|||
endif()
|
||||
endif()
|
||||
# Determine if we can link against the Windows SDK, used for Windows Hello support
|
||||
find_library(WINSDK WindowsApp.lib)
|
||||
find_library(WINSDK WindowsApp.lib
|
||||
HINTS
|
||||
${CMAKE_C_IMPLICIT_LINK_DIRECTORIES}
|
||||
${WINSDKROOTC_LIB}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(APPLE AND WITH_APP_BUNDLE OR WIN32)
|
||||
|
|
|
|||
|
|
@ -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,16 +636,16 @@ 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();
|
||||
} else if (placeholder == "totp") {
|
||||
} else if (placeholder == "totp" || placeholder == "timeotp") {
|
||||
if (entry->hasValidTotp()) {
|
||||
// Calculate TOTP at the time of typing including delays
|
||||
bool isValid = false;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,20 @@ 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) == "1"
|
||||
|| 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) == "1"
|
||||
|| 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 +868,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, "1");
|
||||
entry->attributes()->set(EntryAttributes::KPEX_PASSKEY_FLAG_BS, "1");
|
||||
entry->addTag(tr("Passkey"));
|
||||
|
||||
entry->endUpdate();
|
||||
|
|
@ -1013,8 +1027,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 +1043,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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1594,6 +1594,7 @@ Entry::PlaceholderType Entry::placeholderType(const QString& placeholder) const
|
|||
{QStringLiteral("{PASSWORD}"), PlaceholderType::Password},
|
||||
{QStringLiteral("{NOTES}"), PlaceholderType::Notes},
|
||||
{QStringLiteral("{TOTP}"), PlaceholderType::Totp},
|
||||
{QStringLiteral("{TIMEOTP}"), PlaceholderType::Totp},
|
||||
{QStringLiteral("{URL}"), PlaceholderType::Url},
|
||||
{QStringLiteral("{UUID}"), PlaceholderType::Uuid},
|
||||
{QStringLiteral("{URL:RMVSCM}"), PlaceholderType::UrlWithoutScheme},
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -350,25 +350,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());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1080,55 +1086,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);
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#include <QLineEdit>
|
||||
#include <QMenu>
|
||||
#include <QMimeData>
|
||||
#include <QRegExp>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryFile>
|
||||
|
||||
|
|
@ -366,8 +367,9 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
|||
|
||||
QStringList errors;
|
||||
for (const QModelIndex& index : indexes) {
|
||||
const QString filename = m_attachmentsModel->keyByIndex(index);
|
||||
const QString attachmentPath = saveDir.absoluteFilePath(filename);
|
||||
QString attachmentKey = m_attachmentsModel->keyByIndex(index);
|
||||
const QString fileNameSanitized = attachmentKey.replace(QRegExp("[/\\\\]"), "");
|
||||
const QString attachmentPath = saveDir.absoluteFilePath(fileNameSanitized);
|
||||
|
||||
if (QFileInfo::exists(attachmentPath)) {
|
||||
|
||||
|
|
@ -380,7 +382,7 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
|||
tr("Are you sure you want to overwrite the existing file \"%1\" with the attachment?"));
|
||||
|
||||
auto result = MessageBox::question(
|
||||
this, tr("Confirm overwrite"), questionText.arg(filename), buttons, MessageBox::Cancel);
|
||||
this, tr("Confirm overwrite"), questionText.arg(fileNameSanitized), buttons, MessageBox::Cancel);
|
||||
|
||||
if (result == MessageBox::Skip) {
|
||||
continue;
|
||||
|
|
@ -390,11 +392,11 @@ void EntryAttachmentsWidget::saveSelectedAttachments()
|
|||
}
|
||||
|
||||
QFile file(attachmentPath);
|
||||
const QByteArray attachmentData = m_entryAttachments->value(filename);
|
||||
const QByteArray attachmentData = m_entryAttachments->value(attachmentKey);
|
||||
const bool saveOk = file.open(QIODevice::WriteOnly) && file.setPermissions(QFile::ReadUser | QFile::WriteUser)
|
||||
&& file.write(attachmentData) == attachmentData.size();
|
||||
if (!saveOk) {
|
||||
errors.append(QString("%1 - %2").arg(filename, file.errorString()));
|
||||
errors.append(QString("%1 - %2").arg(fileNameSanitized, file.errorString()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -218,7 +218,7 @@ add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp
|
|||
LIBS testsupport ${TEST_LIBRARIES})
|
||||
|
||||
add_unit_test(NAME testtools SOURCES TestTools.cpp
|
||||
LIBS ${TEST_LIBRARIES})
|
||||
LIBS testsupport ${TEST_LIBRARIES})
|
||||
|
||||
add_unit_test(NAME testconfig SOURCES TestConfig.cpp
|
||||
LIBS testsupport ${TEST_LIBRARIES})
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
#include "TestAutoType.h"
|
||||
|
||||
#include <QPluginLoader>
|
||||
#include <QRegularExpression>
|
||||
#include <QTest>
|
||||
|
||||
#include "autotype/AutoType.h"
|
||||
|
|
@ -475,7 +476,7 @@ void TestAutoType::testAutoTypeEmptyWindowAssociation()
|
|||
QVERIFY(assoc.isEmpty());
|
||||
}
|
||||
|
||||
void TestAutoType::testAutoTypeTotpDelay()
|
||||
void TestAutoType::testAutoTypeTotp()
|
||||
{
|
||||
// Get the TOTP time step in milliseconds
|
||||
auto totpStep = m_entry1->totpSettings()->step * 1000;
|
||||
|
|
@ -494,4 +495,24 @@ void TestAutoType::testAutoTypeTotpDelay()
|
|||
QString("Typed TOTP (%1) should differ from current TOTP (%2) due to delay")
|
||||
.arg(totpParts[0], totpParts[1])
|
||||
.toLatin1());
|
||||
|
||||
m_test->clearActions();
|
||||
|
||||
// Test TIMEOTP placeholder (KeePass2 Compatibility)
|
||||
m_autoType->performAutoTypeWithSequence(m_entry1, "{TIMEOTP}");
|
||||
typedChars = m_test->actionChars();
|
||||
QCOMPARE(typedChars.size(), m_entry1->totpSettings()->digits);
|
||||
// Verify that the typedchars are all numbers
|
||||
QRegularExpression re("^\\d+$");
|
||||
QVERIFY(re.match(typedChars).hasMatch());
|
||||
|
||||
m_test->clearActions();
|
||||
|
||||
// Test that TIMEOTP also works as an entry placeholder
|
||||
m_entry1->setPassword("{TIMEOTP}");
|
||||
m_autoType->performAutoTypeWithSequence(m_entry1, "{PASSWORD}");
|
||||
typedChars = m_test->actionChars();
|
||||
QCOMPARE(typedChars.size(), m_entry1->totpSettings()->digits);
|
||||
// Verify that the typedchars are all numbers
|
||||
QVERIFY(re.match(typedChars).hasMatch());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ private slots:
|
|||
void testAutoTypeSyntaxChecks();
|
||||
void testAutoTypeEffectiveSequences();
|
||||
void testAutoTypeEmptyWindowAssociation();
|
||||
void testAutoTypeTotpDelay();
|
||||
void testAutoTypeTotp();
|
||||
|
||||
private:
|
||||
AutoTypePlatformInterface* m_platform;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
#include "core/Clock.h"
|
||||
#include "core/Tools.h"
|
||||
#include "mock/MockClock.h"
|
||||
|
||||
#include <QFileInfo>
|
||||
#include <QRegularExpression>
|
||||
|
|
@ -33,8 +34,23 @@ namespace
|
|||
{
|
||||
return wholes + QLocale().decimalPoint() + fractions + " " + unit;
|
||||
}
|
||||
|
||||
MockClock* s_clock = nullptr;
|
||||
} // namespace
|
||||
|
||||
void TestTools::initTestCase()
|
||||
{
|
||||
Q_ASSERT(s_clock == nullptr);
|
||||
s_clock = new MockClock(2026, 3, 8, 21, 45, 05);
|
||||
MockClock::setup(s_clock);
|
||||
}
|
||||
|
||||
void TestTools::cleanupTestCase()
|
||||
{
|
||||
MockClock::teardown();
|
||||
s_clock = nullptr;
|
||||
}
|
||||
|
||||
void TestTools::testHumanReadableFileSize()
|
||||
{
|
||||
constexpr auto kibibyte = 1024u;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ class TestTools : public QObject
|
|||
{
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
void testHumanReadableFileSize();
|
||||
void testIsHex();
|
||||
void testIsBase64();
|
||||
|
|
|
|||
249
tests/data/bitwarden_nested_export.json
Normal file
249
tests/data/bitwarden_nested_export.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in a new issue