From 1672d29af31039b5fc2998439f3bc10be10084c4 Mon Sep 17 00:00:00 2001 From: Slava Aseev Date: Fri, 15 Jan 2021 16:13:41 +0300 Subject: [PATCH 1/3] Introduce Secret Service API Main implementation details: - Secret Service secret is actually the KWallet entry, so the secrets created using the Secret Service API can be accessed with KWallet interface. - Secrets created using the Secret Service API will be located in the "Secret Service" folder of the wallet. - Secret Service collection is actually the KWallet wallet (amazing). - Secret Service collection can have attributes, so each KWallet wallet can have an "attributes.json" to store them. A secret can only be accessed using the Secret Service API if it has the attribute record in the "attributes.json" file. - The "default" collection alias reference the same wallet as the "Default Wallet" in kwalletrc. Common changes: - Support unicode wallet/collection names (because the libsecret can use localized strings for the default collection name). - Support wallet/collection renaming. --- .../org.freedesktop.Secrets.Collection.xml | 46 + .../KWallet/org.freedesktop.Secrets.Item.xml | 37 + .../org.freedesktop.Secrets.Prompt.xml | 20 + .../org.freedesktop.Secrets.Service.xml | 72 ++ .../org.freedesktop.Secrets.Session.xml | 12 + src/runtime/kwalletd/CMakeLists.txt | 31 +- src/runtime/kwalletd/autotests/CMakeLists.txt | 78 ++ .../kwalletd/autotests/fdo_secrets_test.cpp | 486 +++++++++ .../kwalletd/autotests/fdo_secrets_test.h | 28 + .../kwalletd/autotests/mockkwalletd.cpp | 153 +++ .../kwalletd/autotests/static_mock.hpp | 304 ++++++ .../kwalletd/autotests/testhelpers.hpp | 100 ++ src/runtime/kwalletd/backend/CMakeLists.txt | 4 +- .../kwalletd/backend/kwalletbackend.cc | 75 +- src/runtime/kwalletd/backend/kwalletbackend.h | 9 +- src/runtime/kwalletd/kwalletd.cpp | 132 ++- src/runtime/kwalletd/kwalletd.h | 21 + src/runtime/kwalletd/kwalletdbuscontext.cpp | 17 + src/runtime/kwalletd/kwalletdbuscontext.h | 54 + .../kwalletd/kwalletfreedesktopattributes.cpp | 346 +++++++ .../kwalletd/kwalletfreedesktopattributes.h | 56 + .../kwalletd/kwalletfreedesktopcollection.cpp | 416 ++++++++ .../kwalletd/kwalletfreedesktopcollection.h | 104 ++ .../kwalletd/kwalletfreedesktopitem.cpp | 223 ++++ src/runtime/kwalletd/kwalletfreedesktopitem.h | 87 ++ .../kwalletd/kwalletfreedesktopprompt.cpp | 132 +++ .../kwalletd/kwalletfreedesktopprompt.h | 73 ++ .../kwalletd/kwalletfreedesktopservice.cpp | 956 ++++++++++++++++++ .../kwalletd/kwalletfreedesktopservice.h | 228 +++++ .../kwalletd/kwalletfreedesktopsession.cpp | 95 ++ .../kwalletd/kwalletfreedesktopsession.h | 72 ++ src/runtime/kwalletd/main.cpp | 5 + 32 files changed, 4431 insertions(+), 41 deletions(-) create mode 100644 src/api/KWallet/org.freedesktop.Secrets.Collection.xml create mode 100644 src/api/KWallet/org.freedesktop.Secrets.Item.xml create mode 100644 src/api/KWallet/org.freedesktop.Secrets.Prompt.xml create mode 100644 src/api/KWallet/org.freedesktop.Secrets.Service.xml create mode 100644 src/api/KWallet/org.freedesktop.Secrets.Session.xml create mode 100644 src/runtime/kwalletd/autotests/CMakeLists.txt create mode 100644 src/runtime/kwalletd/autotests/fdo_secrets_test.cpp create mode 100644 src/runtime/kwalletd/autotests/fdo_secrets_test.h create mode 100644 src/runtime/kwalletd/autotests/mockkwalletd.cpp create mode 100644 src/runtime/kwalletd/autotests/static_mock.hpp create mode 100644 src/runtime/kwalletd/autotests/testhelpers.hpp create mode 100644 src/runtime/kwalletd/kwalletdbuscontext.cpp create mode 100644 src/runtime/kwalletd/kwalletdbuscontext.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopattributes.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopattributes.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopcollection.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopcollection.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopitem.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopitem.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopprompt.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopprompt.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopservice.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopservice.h create mode 100644 src/runtime/kwalletd/kwalletfreedesktopsession.cpp create mode 100644 src/runtime/kwalletd/kwalletfreedesktopsession.h diff --git a/src/api/KWallet/org.freedesktop.Secrets.Collection.xml b/src/api/KWallet/org.freedesktop.Secrets.Collection.xml new file mode 100644 index 00000000..94dd9ab4 --- /dev/null +++ b/src/api/KWallet/org.freedesktop.Secrets.Collection.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api/KWallet/org.freedesktop.Secrets.Item.xml b/src/api/KWallet/org.freedesktop.Secrets.Item.xml new file mode 100644 index 00000000..8544bded --- /dev/null +++ b/src/api/KWallet/org.freedesktop.Secrets.Item.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api/KWallet/org.freedesktop.Secrets.Prompt.xml b/src/api/KWallet/org.freedesktop.Secrets.Prompt.xml new file mode 100644 index 00000000..7a6ab6f0 --- /dev/null +++ b/src/api/KWallet/org.freedesktop.Secrets.Prompt.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/api/KWallet/org.freedesktop.Secrets.Service.xml b/src/api/KWallet/org.freedesktop.Secrets.Service.xml new file mode 100644 index 00000000..ff68bbc4 --- /dev/null +++ b/src/api/KWallet/org.freedesktop.Secrets.Service.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/api/KWallet/org.freedesktop.Secrets.Session.xml b/src/api/KWallet/org.freedesktop.Secrets.Session.xml new file mode 100644 index 00000000..cba5b6de --- /dev/null +++ b/src/api/KWallet/org.freedesktop.Secrets.Session.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/src/runtime/kwalletd/CMakeLists.txt b/src/runtime/kwalletd/CMakeLists.txt index 81e4825a..a9ce5d81 100644 --- a/src/runtime/kwalletd/CMakeLists.txt +++ b/src/runtime/kwalletd/CMakeLists.txt @@ -1,5 +1,10 @@ project(kwalletd5) +include(CheckSymbolExists) + +check_symbol_exists(explicit_bzero "string.h" KWALLETD_HAVE_EXPLICIT_BZERO) +check_symbol_exists(RtlSecureZeroMemory "windows.h" KWALLETD_HAVE_RTLSECUREZEROMEMORY) + find_package(Qt${QT_MAJOR_VERSION} ${REQUIRED_QT_VERSION} CONFIG REQUIRED Gui) find_package(KF5Config ${KF_DEP_VERSION} REQUIRED) @@ -21,10 +26,12 @@ if (Gpgmepp_FOUND) include_directories(${GPGME_INCLUDES}) endif(Gpgmepp_FOUND) +find_package(Qca-qt${QT_MAJOR_VERSION} REQUIRED 2.3.1) include_directories(${CMAKE_CURRENT_BINARY_DIR}) ########### build backends ######### add_subdirectory(backend) +add_subdirectory(autotests) ########### kwalletd ############### @@ -48,6 +55,12 @@ target_sources(kwalletd5 PRIVATE kwalletwizard.cpp ktimeout.cpp kwalletsessionstore.cpp + kwalletfreedesktopservice.cpp + kwalletfreedesktopsession.cpp + kwalletfreedesktopcollection.cpp + kwalletfreedesktopitem.cpp + kwalletfreedesktopprompt.cpp + kwalletfreedesktopattributes.cpp ) ecm_qt_declare_logging_category(kwalletd5 HEADER kwalletd_debug.h @@ -85,9 +98,24 @@ else() # copy of org.kde.KWallet.xml, but with all deprecated API removed set(kwallet_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.kde.KWallet.nodeprecated.xml) endif() +set(fdo_service_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Service.xml) +set(fdo_session_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Session.xml) +set(fdo_collection_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Collection.xml) +set(fdo_item_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Item.xml) +set(fdo_prompt_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Prompt.xml) set(kwalletd_dbus_SRCS) qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${kwallet_xml} kwalletd.h KWalletD kwalletadaptor KWalletAdaptor) +qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${fdo_service_xml} kwalletfreedesktopservice.h KWalletFreedesktopService + kwalletfreedesktopserviceadaptor KWalletFreedesktopServiceAdaptor) +qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${fdo_session_xml} kwalletfreedesktopsession.h KWalletFreedesktopSession + kwalletfreedesktopsessionadaptor KWalletFreedesktopSessionAdaptor) +qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${fdo_collection_xml} kwalletfreedesktopcollection.h KWalletFreedesktopCollection + kwalletfreedesktopcollectionadaptor KWalletFreedesktopCollectionAdaptor) +qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${fdo_item_xml} kwalletfreedesktopitem.h KWalletFreedesktopItem + kwalletfreedesktopitemadaptor KWalletFreedesktopItemAdaptor) +qt_add_dbus_adaptor(kwalletd_dbus_SRCS ${fdo_prompt_xml} kwalletfreedesktopprompt.h KWalletFreedesktopPrompt + kwalletfreedesktoppromptadaptor KWalletFreedesktopPromptAdaptor) target_sources(kwalletd5 PRIVATE ${kwalletd_dbus_SRCS} ) @@ -115,7 +143,8 @@ target_link_libraries(kwalletd5 KF5::DBusAddons KF5::WidgetsAddons KF5::WindowSystem - KF5::Notifications) + KF5::Notifications + ${Qca_LIBRARY}) if (Gpgmepp_FOUND) target_link_libraries(kwalletd5 Gpgmepp) kde_target_enable_exceptions(kwalletd5 PRIVATE) diff --git a/src/runtime/kwalletd/autotests/CMakeLists.txt b/src/runtime/kwalletd/autotests/CMakeLists.txt new file mode 100644 index 00000000..9ebea969 --- /dev/null +++ b/src/runtime/kwalletd/autotests/CMakeLists.txt @@ -0,0 +1,78 @@ +set( EXECUTABLE_OUTPUT_PATH ${CMAKE_CURRENT_BINARY_DIR} ) + +find_package(Qt${QT_MAJOR_VERSION}Test REQUIRED) +find_package(KF5Config ${KF5_DEP_VERSION} REQUIRED) +set(CMAKE_INCLUDE_CURRENT_DIR ON) +set(CMAKE_AUTOMOC ON) + +include(ECMAddTests) + +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/..) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/..) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../backend) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/../backend) +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../../../api/KWallet) +include_directories(${CMAKE_CURRENT_BINARY_DIR}/../../../api/KWallet) + +add_definitions(-DFDO_ENABLE_DUMMY_MESSAGE_CONNECTION) +remove_definitions(-DQT_NO_CAST_FROM_ASCII) + +if (NOT EXCLUDE_DEPRECATED_BEFORE_AND_AT STREQUAL "CURRENT" AND + EXCLUDE_DEPRECATED_BEFORE_AND_AT VERSION_LESS 5.72.0) + set(kwallet_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.kde.KWallet.xml) +else() + # copy of org.kde.KWallet.xml, but with all deprecated API removed + set(kwallet_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.kde.KWallet.nodeprecated.xml) +endif() +set(fdo_service_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Service.xml) +set(fdo_session_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Session.xml) +set(fdo_collection_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Collection.xml) +set(fdo_item_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Item.xml) +set(fdo_prompt_xml ${CMAKE_SOURCE_DIR}/src/api/KWallet/org.freedesktop.Secrets.Prompt.xml) + +set(TEST_SRC + fdo_secrets_test.cpp + ../kwalletfreedesktopservice.cpp + ../kwalletfreedesktopitem.cpp + ../kwalletfreedesktopcollection.cpp + ../kwalletfreedesktopsession.cpp + ../kwalletfreedesktopprompt.cpp + ../kwalletfreedesktopattributes.cpp +) + +qt_add_dbus_adaptor( TEST_SRC ${kwallet_xml} ../kwalletd.h KWalletD kwalletadaptor KWalletAdaptor) +qt_add_dbus_adaptor( TEST_SRC ${fdo_service_xml} ../kwalletfreedesktopservice.h KWalletFreedesktopService + kwalletfreedesktopserviceadaptor KWalletFreedesktopServiceAdaptor) +qt_add_dbus_adaptor( TEST_SRC ${fdo_session_xml} ../kwalletfreedesktopsession.h KWalletFreedesktopSession + kwalletfreedesktopsessionadaptor KWalletFreedesktopSessionAdaptor) +qt_add_dbus_adaptor( TEST_SRC ${fdo_collection_xml} ../kwalletfreedesktopcollection.h KWalletFreedesktopCollection + kwalletfreedesktopcollectionadaptor KWalletFreedesktopCollectionAdaptor) +qt_add_dbus_adaptor( TEST_SRC ${fdo_item_xml} ../kwalletfreedesktopitem.h KWalletFreedesktopItem + kwalletfreedesktopitemadaptor KWalletFreedesktopItemAdaptor) +qt_add_dbus_adaptor( TEST_SRC ${fdo_prompt_xml} ../kwalletfreedesktopprompt.h KWalletFreedesktopPrompt + kwalletfreedesktoppromptadaptor KWalletFreedesktopPromptAdaptor) + +ecm_add_test( + ${TEST_SRC} + ../kwalletfreedesktopservice.h + ../kwalletfreedesktopcollection.h + ../kwalletfreedesktopitem.h + ../kwalletfreedesktopsession.h + ../kwalletfreedesktopprompt.h + ../kwalletd.h + ../ktimeout.h + kwalletfreedesktopserviceadaptor.cpp + kwalletfreedesktopcollectionadaptor.cpp + kwalletfreedesktopitemadaptor.cpp + kwalletfreedesktopsessionadaptor.cpp + kwalletfreedesktoppromptadaptor.cpp + TEST_NAME fdo_secrets_test + LINK_LIBRARIES + KF5Wallet + kwalletbackend5 + Qt${QT_MAJOR_VERSION}::Widgets + Qt${QT_MAJOR_VERSION}::Test + KF5::DBusAddons + KF5::ConfigCore + ${Qca_LIBRARY} +) diff --git a/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp b/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp new file mode 100644 index 00000000..1c324be6 --- /dev/null +++ b/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp @@ -0,0 +1,486 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "fdo_secrets_test.h" +#include "mockkwalletd.cpp" +#include "static_mock.hpp" + +void FdoSecretsTest::initTestCase() +{ + static QCA::Initializer init{}; +} + +void FdoSecretsTest::serviceStaticFunctions() +{ + auto labels = Testset{ + {"label", {"label", -1}}, + {"label__0_", {"label", 0}}, + {"label___1_", {"label_", 1}}, + {"__0_label___200_", {"__0_label_", 200}}, + }; + + runTestset(FdoUniqueLabel::fromName, labels); + runRevTestset( + [](const FdoUniqueLabel &l) { + return l.toName(); + }, + labels); + + runTestset(KWalletFreedesktopService::wrapToCollectionPath, + Testset{ + {"/org/freedesktop/secrets/collection/abcd", "/org/freedesktop/secrets/collection/abcd"}, + {"/org/freedesktop/secrets/collection/abcd/2", "/org/freedesktop/secrets/collection/abcd"}, + {"/org/freedesktop/secrets/collection/abcd/2/2/3/4", "/org/freedesktop/secrets/collection/abcd"}, + }); + + QCOMPARE(KWalletFreedesktopService::nextPromptPath().path(), "/org/freedesktop/secrets/prompt/p0"); + QCOMPARE(KWalletFreedesktopService::nextPromptPath().path(), "/org/freedesktop/secrets/prompt/p1"); +} + +void FdoSecretsTest::collectionStaticFunctions() +{ + auto dirNameTestset = Testset{ + {{FDO_SECRETS_DEFAULT_DIR, "entry1"}, {"entry1", -1}}, + {{FDO_SECRETS_DEFAULT_DIR, "entry__3_"}, {"entry", 3}}, + {{"Passwords", "password__"}, {"Passwords/password__", -1}}, + {{"Passwords__3_", "password__200_"}, {"Passwords__3_/password", 200}}, + {{"", "password"}, {"/password", -1}}, + }; + + runTestset( + [](const EntryLocation &l) { + return l.toUniqueLabel(); + }, + dirNameTestset); + runRevTestset( + [](const FdoUniqueLabel &l) { + return l.toEntryLocation(); + }, + dirNameTestset); +} + +void FdoSecretsTest::cleanup() +{ + SET_FUNCTION_RESULT(KWalletD::wallets, QStringList()); +} + +void FdoSecretsTest::precreatedWallets() +{ + const QStringList wallets = {"wallet1", "wallet2", "wallet2__0_", "wallet2__1_"}; + SET_FUNCTION_RESULT(KWalletD::wallets, wallets); + SET_FUNCTION_RESULT_OVERLOADED(KWalletD::isOpen, true, bool (KWalletD::*)(int)); + + std::unique_ptr kwalletd{new KWalletD}; + std::unique_ptr service{new KWalletFreedesktopService(kwalletd.get())}; + + QCOMPARE(wallets.size(), service->collections().size()); + for (const auto &walletName : wallets) { + auto collection = service->getCollectionByWalletName(walletName); + QVERIFY(collection); + QVERIFY(collection->label() == "wallet1" || collection->label() == "wallet2"); + } + + auto firstCollection = service->getCollectionByWalletName(wallets.front()); + auto &item1 = firstCollection->pushNewItem(FdoUniqueLabel{"item1", -1}, QDBusObjectPath(firstCollection->fdoObjectPath().path() + "/0")); + QCOMPARE(item1.fdoObjectPath().path(), (firstCollection->fdoObjectPath().path() + "/0")); + QCOMPARE(&item1, service->getItemByObjectPath(item1.fdoObjectPath())); +} + +void FdoSecretsTest::aliases() +{ + std::unique_ptr kwalletd{new KWalletD}; + std::unique_ptr service{new KWalletFreedesktopService(kwalletd.get())}; + + service->createCollectionAlias("alias", "walletName"); + service->createCollectionAlias("alias2", "walletName"); + service->createCollectionAlias("alias3", "walletName300"); + service->updateCollectionAlias("alias3", "walletName"); + QSet checkAliases = {"alias", "alias2", "alias3"}; + const QStringList aliases = service->readAliasesFor("walletName"); + for (const auto &alias : aliases) + checkAliases.remove(alias); + QVERIFY(checkAliases.isEmpty()); + + service->removeAlias("alias"); + service->removeAlias("alias2"); + service->removeAlias("alias3"); + QVERIFY(service->readAliasesFor("walletName").isEmpty()); +} + +struct SetupSessionT { + QDBusObjectPath sessionPath; + QCA::SymmetricKey symmetricKey; + QByteArray error; +}; + +#define SETUP_SESSION_VERIFY(cond) \ + do { \ + if (!(cond)) \ + return SetupSessionT{QDBusObjectPath(), QCA::SymmetricKey(), #cond}; \ + } while (false) + +SetupSessionT setupSession(KWalletFreedesktopService *service) +{ + SetupSessionT result; + QCA::KeyGenerator keygen; + auto dlGroup = QCA::DLGroup(keygen.createDLGroup(QCA::IETF_1024)); + if (dlGroup.isNull()) { + result.error = "createDLGroup failed, maybe libqca-ossl is missing"; + return result; + } + + auto privateKey = QCA::PrivateKey(keygen.createDH(dlGroup)); + auto publicKey = QCA::PublicKey(privateKey); + + auto connection = QDBusConnection::sessionBus(); + auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy"); + + auto pubKeyBytes = publicKey.toDH().y().toArray().toByteArray(); + auto sessionPubKeyVariant = service->OpenSession("dh-ietf1024-sha256-aes128-cbc-pkcs7", QDBusVariant(pubKeyBytes), result.sessionPath); + SETUP_SESSION_VERIFY(result.sessionPath.path() != "/"); + SETUP_SESSION_VERIFY(sessionPubKeyVariant.variant().canConvert()); + + auto servicePublicKeyBytes = sessionPubKeyVariant.variant().toByteArray(); + SETUP_SESSION_VERIFY(!servicePublicKeyBytes.isEmpty()); + + auto servicePublicKey = QCA::DHPublicKey(dlGroup, QCA::BigInteger(QCA::SecureArray(servicePublicKeyBytes))); + auto commonSecret = privateKey.deriveKey(servicePublicKey); + result.symmetricKey = QCA::HKDF().makeKey(commonSecret, {}, {}, FDO_SECRETS_CIPHER_KEY_SIZE); + + return result; +} + +void FdoSecretsTest::items() +{ + const QStringList wallets = {"wallet1"}; + const QStringList folders = {FDO_SECRETS_DEFAULT_DIR}; + const QStringList entries = {"item1", "item2", "item3"}; + SET_FUNCTION_RESULT(KWalletD::wallets, wallets); + SET_FUNCTION_RESULT(KWalletD::folderList, folders); + SET_FUNCTION_RESULT(KWalletD::entryList, entries); + + SET_FUNCTION_IMPL(KWalletD::entryType, [](int, const QString &, const QString &key, const QString &) -> int { + if (key == "item1") + return KWallet::Wallet::Password; + else if (key == "item2") + return KWallet::Wallet::Map; + else if (key == "item3") + return KWallet::Wallet::Stream; + else + QTEST_ASSERT(false); + }); + + QString _secretHolder1 = "It's a password"; + QByteArray _secretHolder2; + QByteArray _secretHolder3; + + { + QByteArray a = "It's a"; + QString b = "stream"; + + QDataStream ds{&_secretHolder2, QIODevice::WriteOnly}; + ds << a << b; + } + + { + StrStrMap map; + map["it's a"] = "map"; + + QDataStream ds{&_secretHolder3, QIODevice::WriteOnly}; + ds << map; + } + + SET_FUNCTION_IMPL(KWalletD::readPassword, [&](int, const QString &, const QString &key, const QString &) -> QString { + QTEST_ASSERT(key == "item1"); + return _secretHolder1; + }); + + SET_FUNCTION_IMPL(KWalletD::readEntry, [&](int, const QString &, const QString &key, const QString &) -> QByteArray { + QTEST_ASSERT(key == "item3" || key == "item2"); + if (key == "item2") + return _secretHolder2; + else + return _secretHolder3; + }); + + SET_FUNCTION_IMPL(KWalletD::writePassword, [&](int, const QString &, const QString &key, const QString &value, const QString &) -> int { + QTEST_ASSERT(key == "item1"); + _secretHolder1 = value; + return 0; + }); + + using writeEntryT = int (KWalletD::*)(int, const QString &, const QString &, const QByteArray &, int, const QString &); + SET_FUNCTION_IMPL_OVERLOADED(KWalletD::writeEntry, + writeEntryT, + [&](int, const QString &, const QString &key, const QByteArray &value, int, const QString &) -> int { + QTEST_ASSERT(key == "item3" || key == "item2"); + if (key == "item2") + _secretHolder2 = value; + else + _secretHolder3 = value; + return 0; + }); + + std::unique_ptr kwalletd{new KWalletD}; + std::unique_ptr service{new KWalletFreedesktopService(kwalletd.get())}; + + auto collection = service->getCollectionByWalletName("wallet1"); + QVERIFY(collection); + + /* Write some attributes */ + { + collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item1"}); + collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item2"}); + collection->itemAttributes().newItem({FDO_SECRETS_DEFAULT_DIR, "item3"}); + collection->itemAttributes().setParam({FDO_SECRETS_DEFAULT_DIR, "item3"}, FDO_KEY_CREATED, 100200300ULL); + collection->itemAttributes().setParam({FDO_SECRETS_DEFAULT_DIR, "item3"}, FDO_KEY_MODIFIED, 100200301ULL); + auto attribs = collection->itemAttributes().getAttributes({FDO_SECRETS_DEFAULT_DIR, "item3"}); + attribs["Attrib1"] = "value1"; + attribs["Attrib2"] = "value2"; + collection->itemAttributes().setAttributes({FDO_SECRETS_DEFAULT_DIR, "item3"}, attribs); + } + + /* Create collection */ + using OpenAsyncT = int (KWalletD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &); + bool openAsyncCalled = false; + SET_FUNCTION_IMPL_OVERLOADED(KWalletD::openAsync, + OpenAsyncT, + [&](const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &) -> int { + openAsyncCalled = true; + return 0; + }); + + QDBusObjectPath promptPath; + service->Unlock({collection->fdoObjectPath()}, promptPath); + auto prompt = service->getPromptByObjectPath(promptPath); + QVERIFY(prompt); + prompt->Prompt("wndid"); + Q_EMIT kwalletd->walletAsyncOpened(0, 0); + SET_FUNCTION_RESULT_OVERLOADED(KWalletD::isOpen, true, bool (KWalletD::*)(int)); + QVERIFY(!collection->locked()); + + auto item1 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item1"}); + auto item2 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item2"}); + auto item3 = collection->findItemByEntryLocation({FDO_SECRETS_DEFAULT_DIR, "item3"}); + QVERIFY(item1 && item2 && item3); + + auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy"); + auto [sessionPath, symmetricKey, errorStr] = setupSession(service.get()); + QVERIFY2(errorStr.isEmpty(), errorStr.constData()); + + /* Check secrets */ + auto secret1 = item1->GetSecret(sessionPath); + service->desecret(message, secret1); + QCOMPARE(secret1.note.toByteArray(), "It's a password"); + // QCOMPARE(secret1.mimeType, "text/plain"); + + auto secret2 = item2->GetSecret(sessionPath); + service->desecret(message, secret2); + QByteArray secretBytes = secret2.note.toByteArray(); + QDataStream ds{secretBytes}; + QByteArray a; + QString b; + ds >> a >> b; + + QCOMPARE(secret2.mimeType, "application/octet-stream"); + QCOMPARE(a, "It's a"); + QCOMPARE(b, "stream"); + + auto secret3 = item3->GetSecret(sessionPath); + service->desecret(message, secret3); + auto bytes3 = secret3.note.toByteArray(); + QDataStream ds2(bytes3); + StrStrMap map3; + ds2 >> map3; + + QVERIFY(map3.find("it's a") != map3.end() && map3["it's a"] == "map"); + QCOMPARE(item3->created(), 100200300); + QCOMPARE(item3->modified(), 100200301); + QCOMPARE(item3->attributes()["Attrib1"], "value1"); + QCOMPARE(item3->attributes()["Attrib2"], "value2"); + + /* Set new secrets */ + secret1.note = QByteArray("It's a new password"); + secret1.mimeType = "text/plain"; + service->ensecret(message, secret1); + item1->SetSecret(secret1); + secret1 = item1->GetSecret(sessionPath); + service->desecret(message, secret1); + QCOMPARE(secret1.note.toByteArray(), "It's a new password"); + QCOMPARE(secret1.mimeType, "text/plain"); + + secret2.note = QByteArray("It's a new secret"); + secret2.mimeType = "application/octet-stream"; + service->ensecret(message, secret2); + item2->SetSecret(secret2); + auto attribs = item2->attributes(); + attribs["newAttrib"] = ")))"; + item2->setAttributes(attribs); + + secret2 = item2->GetSecret(sessionPath); + service->desecret(message, secret2); + QCOMPARE(secret2.note.toByteArray(), "It's a new secret"); + QCOMPARE(item2->attributes()["newAttrib"], ")))"); + + /* Search items */ + attribs.clear(); + attribs["Attrib1"] = "value1"; + QList lockedItems; + auto unlockedItems = service->SearchItems(attribs, lockedItems); + QCOMPARE(unlockedItems.size(), 1); + QCOMPARE(unlockedItems.front(), item3->fdoObjectPath()); +} + +void FdoSecretsTest::createLockUnlockCollection() +{ + std::unique_ptr kwalletd{new KWalletD}; + std::unique_ptr service{new KWalletFreedesktopService(kwalletd.get())}; + + /* Create collection */ + using OpenAsyncT = int (KWalletD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &); + bool openAsyncCalled = false; + SET_FUNCTION_IMPL_OVERLOADED(KWalletD::openAsync, + OpenAsyncT, + [&](const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &) -> int { + openAsyncCalled = true; + return 0; + }); + + QVariantMap props; + props["org.freedesktop.Secret.Collection.Label"] = QString("walletName"); + QDBusObjectPath promptPath; + service->CreateCollection(props, "", promptPath); + auto prompt = service->getPromptByObjectPath(promptPath); + QVERIFY(prompt); + prompt->Prompt("wndid"); + QVERIFY(openAsyncCalled); + Q_EMIT kwalletd->walletAsyncOpened(0, 0); + + auto createdCollection = service->getCollectionByWalletName("walletName"); + QVERIFY(createdCollection); + QCOMPARE(createdCollection->label(), "walletName"); + + /* Check aliases */ + service->createCollectionAlias("alias", "walletName"); + service->createCollectionAlias("alias2", "walletName"); + service->createCollectionAlias("alias3", "walletName"); + + QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias"), createdCollection->fdoObjectPath().path()); + QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias2"), createdCollection->fdoObjectPath().path()); + QCOMPARE(service->resolveIfAlias(QStringLiteral(FDO_ALIAS_PATH) + "alias3"), createdCollection->fdoObjectPath().path()); + QCOMPARE(service->ReadAlias("alias"), createdCollection->fdoObjectPath()); + + service->removeAlias("alias"); + service->removeAlias("alias2"); + service->removeAlias("alias3"); + + /* Lock/Unlock */ + auto lockedObjects = service->Lock({createdCollection->fdoObjectPath()}, promptPath); + QCOMPARE(lockedObjects.size(), 1); + QCOMPARE(lockedObjects.front(), createdCollection->fdoObjectPath()); + SET_FUNCTION_RESULT_OVERLOADED(KWalletD::isOpen, false, bool (KWalletD::*)(int)); + QVERIFY(createdCollection->locked()); + + service->Unlock({createdCollection->fdoObjectPath()}, promptPath); + prompt = service->getPromptByObjectPath(promptPath); + QVERIFY(prompt); + openAsyncCalled = false; + prompt->Prompt("wndid"); + QVERIFY(openAsyncCalled); + Q_EMIT kwalletd->walletAsyncOpened(0, 0); + SET_FUNCTION_RESULT_OVERLOADED(KWalletD::isOpen, true, bool (KWalletD::*)(int)); + QVERIFY(!createdCollection->locked()); +} + +void FdoSecretsTest::session() +{ + std::unique_ptr kwalletd{new KWalletD}; + std::unique_ptr service{new KWalletFreedesktopService(kwalletd.get())}; + + auto message = QDBusMessage::createSignal("dummy", "dummy", "dummy"); + auto [sessionPath, symmetricKey, errorStr] = setupSession(service.get()); + + /* Generate secret */ + auto secret = FreedesktopSecret(sessionPath, QByteArray("It's a secret"), "text/plain"); + QVERIFY(service->ensecret(message, secret)); + + /* Try to decrypt by hand with symmetricKey */ + auto cipher = QCA::Cipher("aes128", QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Decode, symmetricKey, secret.initVector); + QCA::SecureArray result; + result.append(cipher.update(QCA::MemoryRegion(secret.note.toByteArray()))); + result.append(cipher.final()); + + QCOMPARE(QString::fromUtf8(result.toByteArray()), "It's a secret"); + + /* Try to decrypt by session */ + QVERIFY(service->desecret(message, secret)); + QCOMPARE(secret.note.toByteArray(), QByteArray("It's a secret")); +} + +void FdoSecretsTest::attributes() +{ + KWalletFreedesktopAttributes attribs{"test"}; + + attribs.newItem({"dir", "name"}); + + attribs.setParam({"dir", "name"}, "param1", 0xff00ff00ff00ff00); + attribs.setParam({"dir", "name"}, "param2", "string_param"); + + QCOMPARE(attribs.getULongLongParam({"dir", "name"}, "param1", 0), 0xff00ff00ff00ff00); + QCOMPARE(attribs.getStringParam({"dir", "name"}, "param2", ""), "string_param"); + + attribs.renameLabel({"dir", "name"}, {"newdir", "newname"}); + + QCOMPARE(attribs.getULongLongParam({"newdir", "newname"}, "param1", 0), 0xff00ff00ff00ff00); + QCOMPARE(attribs.getStringParam({"newdir", "newname"}, "param2", ""), "string_param"); + QCOMPARE(attribs.getULongLongParam({"dir", "name"}, "param1", 0xdef017), 0xdef017); + QCOMPARE(attribs.getStringParam({"dir", "name"}, "param2", "default"), "default"); + + attribs.setParam({"newdir", "newname"}, "param1", 100200300ULL); + attribs.setParam({"newdir", "newname"}, "param2", "another_string_param"); + + QCOMPARE(attribs.getULongLongParam({"newdir", "newname"}, "param1", 0), 100200300ULL); + QCOMPARE(attribs.getStringParam({"newdir", "newname"}, "param2", ""), "another_string_param"); + + QVERIFY(attribs.getAttributes({"newdir", "newname"}).empty()); + + StrStrMap attribMap; + attribMap["key1"] = "value1"; + attribMap["key2"] = "value2"; + + attribs.setAttributes({"newdir", "newname"}, attribMap); + QCOMPARE(attribs.getAttributes({"newdir", "newname"}), attribMap); + + attribs.setAttributes({"dir", "name"}, attribMap); + /* Item not exists - expects empty attributes map */ + QVERIFY(attribs.getAttributes({"dir", "name"}).empty()); + + attribs.setParam({"dir1", "name1"}, "param1", "some_param"); + QCOMPARE(attribs.getStringParam({"dir1", "name1"}, "param1", "default"), "default"); +} + +void FdoSecretsTest::walletNameEncodeDecode() +{ +#define ENCODE_DECODE_CHECK(DECODED, ENCODED) \ + do { \ + auto encodedResult = KWallet::Backend::encodeWalletName(DECODED); \ + auto decodedResult = KWallet::Backend::decodeWalletName(ENCODED); \ + QCOMPARE(encodedResult, ENCODED); \ + QCOMPARE(decodedResult, DECODED); \ + } while (false) + + ENCODE_DECODE_CHECK("/", ";2F"); + QString allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^&'@{}[],$=!-#()%.+_\r\n\t\f\v "; + ENCODE_DECODE_CHECK(allowedChars, allowedChars); + ENCODE_DECODE_CHECK("a/b/c\\", "a;2Fb;2Fc;5C"); + ENCODE_DECODE_CHECK("/\\/", ";2F;5C;2F"); + ENCODE_DECODE_CHECK(";;;", ";3B;3B;3B"); + ENCODE_DECODE_CHECK(";3B", ";3B3B"); + +#undef ENCODE_DECODE_CHECK +} + +QTEST_GUILESS_MAIN(FdoSecretsTest) diff --git a/src/runtime/kwalletd/autotests/fdo_secrets_test.h b/src/runtime/kwalletd/autotests/fdo_secrets_test.h new file mode 100644 index 00000000..9ab236c2 --- /dev/null +++ b/src/runtime/kwalletd/autotests/fdo_secrets_test.h @@ -0,0 +1,28 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "../kwalletd.h" +#include "testhelpers.hpp" + +class FdoSecretsTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanup(); + + void serviceStaticFunctions(); + void collectionStaticFunctions(); + + void precreatedWallets(); + void aliases(); + void createLockUnlockCollection(); + void items(); + void session(); + void attributes(); + void walletNameEncodeDecode(); +}; diff --git a/src/runtime/kwalletd/autotests/mockkwalletd.cpp b/src/runtime/kwalletd/autotests/mockkwalletd.cpp new file mode 100644 index 00000000..6264ce1f --- /dev/null +++ b/src/runtime/kwalletd/autotests/mockkwalletd.cpp @@ -0,0 +1,153 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2022 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "static_mock.hpp" + +#include "../kwalletd.h" +#include "../kwalletfreedesktopcollection.h" +#include "../kwalletfreedesktopitem.h" +#include "../kwalletfreedesktopprompt.h" +#include "../kwalletfreedesktopservice.h" +#include "../kwalletfreedesktopsession.h" + +KWalletD::KWalletD() + : _syncTime(0) +{ +} +KWalletD::~KWalletD() +{ +} + +KWalletSessionStore::KWalletSessionStore() +{ +} +KWalletSessionStore::~KWalletSessionStore() +{ +} + +KTimeout::KTimeout(QObject *) +{ +} +KTimeout::~KTimeout() +{ +} + +MOCK_FUNCTION(KWalletD, encodeWalletName, 1, ); +MOCK_FUNCTION(KWalletD, decodeWalletName, 1, ); + +MOCK_FUNCTION(KTimeout, clear, 0, ); +MOCK_FUNCTION(KTimeout, resetTimer, 2, ); +MOCK_FUNCTION(KTimeout, removeTimer, 1, ); +MOCK_FUNCTION(KTimeout, addTimer, 2, ); + +void KTimeout::timerEvent(QTimerEvent *) +{ +} + +MOCK_FUNCTION(KWalletD, readEntry, 4, ); +MOCK_FUNCTION(KWalletD, readMap, 4, ); +MOCK_FUNCTION(KWalletD, readPassword, 4, ); + +MOCK_FUNCTION_RES(KWalletD, removeEntry, 4, 0, ); +MOCK_FUNCTION_RES(KWalletD, writeMap, 5, 0, ); +MOCK_FUNCTION_RES(KWalletD, writePassword, 5, 0, ); + +using OVWriteEntry_6 = int (KWalletD::*)(int, const QString &, const QString &, const QByteArray &, int, const QString &); +MOCK_FUNCTION_OVERLOADED_RES(KWalletD, writeEntry, 6, 0, OVWriteEntry_6); + +using OVWriteEntry_5 = int (KWalletD::*)(int, const QString &, const QString &, const QByteArray &, const QString &); +MOCK_FUNCTION_OVERLOADED_RES(KWalletD, writeEntry, 5, 0, OVWriteEntry_5); + +MOCK_FUNCTION(KWalletD, entryType, 4, ); +MOCK_FUNCTION_RES(KWalletD, renameEntry, 5, 0, ); + +MOCK_FUNCTION(KWalletD, isEnabled, 0, const); +MOCK_FUNCTION(KWalletD, open, 3, ); +MOCK_FUNCTION(KWalletD, openPath, 3, ); +MOCK_FUNCTION(KWalletD, openPathAsync, 4, ); + +using OVOpenAsync4 = int (KWalletD::*)(const QString &, qlonglong, const QString &, bool); +MOCK_FUNCTION_OVERLOADED(KWalletD, openAsync, 4, OVOpenAsync4); + +using OVOpenAsync6 = int (KWalletD::*)(const QString &, qlonglong, const QString &, bool, const QDBusConnection &, const QDBusMessage &); +MOCK_FUNCTION_OVERLOADED(KWalletD, openAsync, 6, OVOpenAsync6); + +using OVClose4 = int (KWalletD::*)(int, bool, const QString &, const QDBusMessage &); +MOCK_FUNCTION_OVERLOADED(KWalletD, close, 4, OVClose4); + +using OVClose2 = int (KWalletD::*)(const QString &, bool); +MOCK_FUNCTION_OVERLOADED(KWalletD, close, 2, OVClose2); + +using OVClose3 = int (KWalletD::*)(int, bool, const QString &); +MOCK_FUNCTION_OVERLOADED(KWalletD, close, 3, OVClose3); + +MOCK_FUNCTION(KWalletD, deleteWallet, 1, ); + +MOCK_FUNCTION_OVERLOADED(KWalletD, isOpen, 1, bool (KWalletD::*)(const QString &)); +MOCK_FUNCTION_OVERLOADED(KWalletD, isOpen, 1, bool (KWalletD::*)(int)); + +MOCK_FUNCTION(KWalletD, users, 1, const); +MOCK_FUNCTION(KWalletD, wallets, 0, const); +MOCK_FUNCTION(KWalletD, folderList, 2, ); +MOCK_FUNCTION(KWalletD, hasFolder, 3, ); +MOCK_FUNCTION(KWalletD, createFolder, 3, ); +MOCK_FUNCTION(KWalletD, removeFolder, 3, ); +MOCK_FUNCTION(KWalletD, entryList, 3, ); + +#if KWALLET_BUILD_DEPRECATED_SINCE(5, 72) +MOCK_FUNCTION(KWalletD, readEntryList, 4, ); +MOCK_FUNCTION(KWalletD, readMapList, 4, ); +MOCK_FUNCTION(KWalletD, readPasswordList, 4, ); +#endif + +MOCK_FUNCTION(KWalletD, entriesList, 3, ); +MOCK_FUNCTION(KWalletD, mapList, 3, ); +MOCK_FUNCTION(KWalletD, passwordList, 3, ); +MOCK_FUNCTION(KWalletD, renameWallet, 2, ); +MOCK_FUNCTION(KWalletD, hasEntry, 4, ); +MOCK_FUNCTION(KWalletD, disconnectApplication, 2, ); +MOCK_FUNCTION(KWalletD, folderDoesNotExist, 2, ); +MOCK_FUNCTION(KWalletD, keyDoesNotExist, 3, ); +MOCK_FUNCTION(KWalletD, networkWallet, 0, ); +MOCK_FUNCTION(KWalletD, localWallet, 0, ); +MOCK_FUNCTION(KWalletD, pamOpen, 3, ); +MOCK_FUNCTION(KWalletD, sync, 2, ); +MOCK_FUNCTION(KWalletD, changePassword, 3, ); +MOCK_FUNCTION(KWalletD, reconfigure, 0, ); +MOCK_FUNCTION(KWalletD, closeAllWallets, 0, ); +MOCK_FUNCTION(KWalletD, screenSaverChanged, 1, ); + +void KWalletD::registerKWalletd4Service() +{ +} +void KWalletD::slotServiceOwnerChanged(const QString &, const QString &, const QString &) +{ +} +void KWalletD::emitWalletListDirty() +{ +} +void KWalletD::timedOutClose(int) +{ +} +void KWalletD::timedOutSync(int) +{ +} +void KWalletD::notifyFailures() +{ +} +void KWalletD::processTransactions() +{ +} +void KWalletD::activatePasswordDialog() +{ +} + +#include +const QLoggingCategory &KWALLETD_LOG() +{ + static const QLoggingCategory category("kf.wallet.kwalletd", QtFatalMsg); + return category; +} diff --git a/src/runtime/kwalletd/autotests/static_mock.hpp b/src/runtime/kwalletd/autotests/static_mock.hpp new file mode 100644 index 00000000..41094748 --- /dev/null +++ b/src/runtime/kwalletd/autotests/static_mock.hpp @@ -0,0 +1,304 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#ifndef __STATIC_MOCK__H__ +#define __STATIC_MOCK__H__ + +#include +#include +#include +#include + +/* + * Details + */ + +#define COMPTIME_HASH(x) details::fnv1a64_hash(x) + +namespace details +{ +template +constexpr uint64_t fnv1a64_hash(const char *str) { + return (fnv1a64_hash(str) ^ static_cast(str[I])) * 0x100000001b3; +} + +template <> +constexpr uint64_t fnv1a64_hash(const char*) { + return 0xcbf29ce484222325; +} + +template +struct ElementType_ { + using type = typename std::tuple_element>::type; +}; + +template +struct ElementType_ { + using type = std::nullptr_t; +}; + +template +struct ElementType : details::ElementType_<(N < sizeof...(ArgsT)), N, ArgsT...> {}; + +template +struct FunctionTraitsImpl { + using ReturnType = ReturnT; + + template using ArgT = typename ElementType::type; + + static constexpr size_t arity = sizeof...(ArgsT); +}; + +template struct FunctionTraits {}; + +template +struct FunctionTraits : FunctionTraitsImpl {}; + +template +struct FunctionTraits + : FunctionTraitsImpl {}; + +template +struct FunctionTraits : FunctionTraitsImpl {}; + +template +static T& returnStaticStorage() { + static T value; + return value; +} + +template +struct ReturnStaticStorageHelper { + using TT = typename std::decay::type; + + static T get() { return returnStaticStorage(); } + template + static void setValue(V&& v) { returnStaticStorage() = std::forward(v); } +}; + +template +struct ReturnStaticStorageHelper { + static void get() {} + template + static void setValue(V) {} +}; + +template +struct DefaultCtorOrNullForVoid { + using TT = typename std::decay::type; + static TT get() { return TT(); } +}; + +template <> +struct DefaultCtorOrNullForVoid { + static std::nullptr_t get() { return nullptr; } +}; + +template +struct FuncImplHelper { + using RetT = typename FunctionTraits::ReturnType; + + static typename FunctionTraits::ReturnType call(ArgsT... args) { + if (func()) + return func()(args...); + else + return ReturnStaticStorageHelper< + MemberF, NameHash, typename FunctionTraits::ReturnType>::get(); + } + + static std::function& func() { + static std::function holder; + return holder; + } +}; + +template +struct FuncImpl + : std::conditional< + (I < FunctionTraits::arity), + FuncImpl::template ArgT>, + FuncImplHelper>::type {}; + +} // namespace details + + +#define MM_MEMBER_TRAITS(NAME) details::FunctionTraits + +#define MM_MEMBER_ARG_0(NAME) +#define MM_MEMBER_ARG_1(NAME) typename details::FunctionTraits::ArgT<0> _0 +#define MM_MEMBER_ARG_2(NAME) \ + MM_MEMBER_ARG_1(NAME), typename details::FunctionTraits::ArgT<1> _1 +#define MM_MEMBER_ARG_3(NAME) \ + MM_MEMBER_ARG_2(NAME), typename details::FunctionTraits::ArgT<2> _2 +#define MM_MEMBER_ARG_4(NAME) \ + MM_MEMBER_ARG_3(NAME), typename details::FunctionTraits::ArgT<3> _3 +#define MM_MEMBER_ARG_5(NAME) \ + MM_MEMBER_ARG_4(NAME), typename details::FunctionTraits::ArgT<4> _4 +#define MM_MEMBER_ARG_6(NAME) \ + MM_MEMBER_ARG_5(NAME), typename details::FunctionTraits::ArgT<5> _5 +#define MM_MEMBER_ARG_7(NAME) \ + MM_MEMBER_ARG_6(NAME), typename details::FunctionTraits::ArgT<6> _6 + +#define MM_MEMBER_ARGNAME_0(NAME) +#define MM_MEMBER_ARGNAME_1(NAME) _0 +#define MM_MEMBER_ARGNAME_2(NAME) MM_MEMBER_ARGNAME_1(NAME), _1 +#define MM_MEMBER_ARGNAME_3(NAME) MM_MEMBER_ARGNAME_2(NAME), _2 +#define MM_MEMBER_ARGNAME_4(NAME) MM_MEMBER_ARGNAME_3(NAME), _3 +#define MM_MEMBER_ARGNAME_5(NAME) MM_MEMBER_ARGNAME_4(NAME), _4 +#define MM_MEMBER_ARGNAME_6(NAME) MM_MEMBER_ARGNAME_5(NAME), _5 +#define MM_MEMBER_ARGNAME_7(NAME) MM_MEMBER_ARGNAME_6(NAME), _6 + +#define MM_LINE_NAME(prefix) MM_JOIN_NAME(prefix, __LINE__) +#define MM_JOIN_NAME(NAME, LINE) MM_JOIN_NAME_1(NAME, LINE) +#define MM_JOIN_NAME_1(NAME, LINE) NAME##LINE + +#define MOCK_FUNCTION_RES_RAW_TYPE(CLASS, NAME, TYPE, ARGS_COUNT, INIT_VALUE, ...) \ + details::FunctionTraits::ReturnType CLASS::NAME(MM_MEMBER_ARG_##ARGS_COUNT(TYPE)) \ + __VA_ARGS__ { \ + return details::FuncImpl::call( \ + MM_MEMBER_ARGNAME_##ARGS_COUNT(TYPE)); \ + } \ + static int MM_LINE_NAME(_init_res_##CLASS##NAME##ARGS_COUNT) = []() { \ + details::ReturnStaticStorageHelper< \ + TYPE, COMPTIME_HASH(#CLASS "::" #NAME), \ + details::FunctionTraits::ReturnType>::setValue(INIT_VALUE); \ + return 0; \ + }() + + +/* + * Interface + */ + +/* + * Defines implementation for the function with specified return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * INIT_VALUE - the new result of the function + * ... - the optional const qualifier (must be empty if member function is non-const) + */ +#define MOCK_FUNCTION_RES(CLASS, NAME, ARGS_COUNT, INIT_VALUE, ...) \ + MOCK_FUNCTION_RES_RAW_TYPE(CLASS, NAME, decltype(&CLASS::NAME), ARGS_COUNT, INIT_VALUE, \ + __VA_ARGS__) + +/* + * Defines implementation for the function with default-constructed return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * ... - the optional const qualifier (must be empty if member function is non-const) + */ +#define MOCK_FUNCTION(CLASS, NAME, ARGS_COUNT, ...) \ + MOCK_FUNCTION_RES(CLASS, NAME, ARGS_COUNT, \ + details::DefaultCtorOrNullForVoid< \ + details::FunctionTraits::ReturnType>::get(), \ + __VA_ARGS__) + +/* + * Defines implementation for the overloaded function with specified return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * INIT_VALUE - the new result of the function + * ... - the signature of the overloaded function + */ +#define MOCK_FUNCTION_OVERLOADED_RES(CLASS, NAME, ARGS_COUNT, INIT_VALUE, ...) \ + MOCK_FUNCTION_RES_RAW_TYPE(CLASS, NAME, decltype((__VA_ARGS__)&CLASS::NAME), ARGS_COUNT, \ + INIT_VALUE, ) + +/* + * Defines implementation for the overloaded const member function with specified return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * INIT_VALUE - the new result of the function + * ... - the signature of the overloaded function + */ +#define MOCK_FUNCTION_OVERLOADED_RES_CONST(CLASS, NAME, ARGS_COUNT, INIT_VALUE, ...) \ + MOCK_FUNCTION_RES_RAW_TYPE(CLASS, NAME, decltype((__VA_ARGS__)&CLASS::NAME), ARGS_COUNT, \ + INIT_VALUE, const) + +/* + * Defines implementation for the overloaded function with default-constructed return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * ... - the signature of the overloaded function + */ +#define MOCK_FUNCTION_OVERLOADED(CLASS, NAME, ARGS_COUNT, ...) \ + MOCK_FUNCTION_OVERLOADED_RES( \ + CLASS, NAME, ARGS_COUNT, \ + details::DefaultCtorOrNullForVoid< \ + details::FunctionTraits::ReturnType>::get(), \ + __VA_ARGS__) + +/* + * Defines implementation for the overloaded const member function with default-constructed return value + * + * CLASS - the class name + * NAME - the function name + * ARGS_COUNT - the count of function arguments + * ... - the signature of the overloaded function + */ +#define MOCK_FUNCTION_OVERLOADED_CONST(CLASS, NAME, ARGS_COUNT, ...) \ + MOCK_FUNCTION_OVERLOADED_RES_CONST( \ + CLASS, NAME, ARGS_COUNT, \ + details::DefaultCtorOrNullForVoid< \ + details::FunctionTraits::ReturnType>::get(), \ + __VA_ARGS__) + +/* + * Sets return value for the specified function + * + * FULL_NAME - the full name of the function (e.g. SomeClass::functionName) + * ... - the value to be returned (or arguments to constructor) + */ +#define SET_FUNCTION_RESULT(FULL_NAME, ...) \ + details::ReturnStaticStorageHelper< \ + decltype(&FULL_NAME), COMPTIME_HASH(#FULL_NAME), \ + details::FunctionTraits::ReturnType>::setValue(__VA_ARGS__) + +/* + * Sets return value for the specified overloaded function + * + * FULL_NAME - the full name of the function (e.g. SomeClass::functionName) + * VALUE - the value to be returned + * ... - the signature of the overloaded function + */ +#define SET_FUNCTION_RESULT_OVERLOADED(FULL_NAME, VALUE, ...) \ + details::ReturnStaticStorageHelper< \ + decltype((__VA_ARGS__)&FULL_NAME), COMPTIME_HASH(#FULL_NAME), \ + details::FunctionTraits::ReturnType>::setValue(VALUE) + +/* + * Sets implementation for the specified function + * + * FULL_NAME - the full name of the function (e.g. SomeClass::functionName) + * ... - the lambda or function for implementation + */ +#define SET_FUNCTION_IMPL(FULL_NAME, ...) \ + details::FuncImpl::func() = __VA_ARGS__ + +/* + * Sets implementation for the specified overloaded function + * + * FULL_NAME - the full name of the function (e.g. SomeClass::functionName) + * FUNC_TYPE - the signature of the overloaded function (must be alias if it contains commas) + * ... - the lambda or function for implementation + */ +#define SET_FUNCTION_IMPL_OVERLOADED(FULL_NAME, FUNC_TYPE, ...) \ + details::FuncImpl::func() = \ + __VA_ARGS__ + +#endif // __STATIC_MOCK__H__ diff --git a/src/runtime/kwalletd/autotests/testhelpers.hpp b/src/runtime/kwalletd/autotests/testhelpers.hpp new file mode 100644 index 00000000..56c3c020 --- /dev/null +++ b/src/runtime/kwalletd/autotests/testhelpers.hpp @@ -0,0 +1,100 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef __TESTHELPERS_HPP__ +#define __TESTHELPERS_HPP__ + +#include +#include +#include +#include +#include <../kwalletfreedesktopservice.h> + +template +using Testset = std::vector>; + +template +struct EasyFormater { + std::string operator()(const T& v) const { + return std::string(v.toStdString()); + } +}; + +template +struct EasyFormater::value || + std::is_floating_point::value>::type> { + std::string operator()(T v) const { + return std::to_string(v); + } +}; + +template <> +struct EasyFormater { + std::string operator()(const FdoUniqueLabel& v) const { + return "{" + v.label.toStdString() + ", " + std::to_string(v.copyId) + "}"; + } +}; + +template <> +struct EasyFormater { + std::string operator()(const EntryLocation& v) const { + return "{" + v.folder.toStdString() + ", " + v.key.toStdString() + "}"; + } +}; + + +template +struct TestsetCmpHelper { + template + bool cmp(F&& function, const T& pair) { + return function(pair.first) == pair.second; + } + template + std::string format(F&& function, const T& pair) { + using RetT = decltype(function(pair.first)); + return EasyFormater()(pair.first) + " (evaluates to " + + EasyFormater()(function(pair.first)) + ") not equal with " + + EasyFormater()(pair.second); + } +}; +template <> +struct TestsetCmpHelper { + template + bool cmp(F&& function, const T& pair) { + return function(pair.second) == pair.first; + } + template + std::string format(F&& function, const T& pair) { + using RetT = decltype(function(pair.second)); + return EasyFormater()(pair.second) + " (evaluates to " + + EasyFormater()(function(pair.second)) + ") not equal with " + + EasyFormater()(pair.first); + } +}; + +template +void runTestsetTmpl(F&& function, const Testset& labelMap) { + for (auto& pair : labelMap) { + bool ok = TestsetCmpHelper().cmp(function, pair); + if (!ok) { + std::string str = TestsetCmpHelper().format(function, pair); + QVERIFY2(ok, str.c_str()); + } + } +} + +template +void runTestset(F&& function, const Testset& labelMap) { + return runTestsetTmpl(std::forward(function), labelMap); +} + +template +void runRevTestset(F&& function, const Testset& labelMap) { + return runTestsetTmpl(std::forward(function), labelMap); +} + +#endif // __TESTHELPERS_HPP__ + diff --git a/src/runtime/kwalletd/backend/CMakeLists.txt b/src/runtime/kwalletd/backend/CMakeLists.txt index 1523b6c0..6fbb7334 100644 --- a/src/runtime/kwalletd/backend/CMakeLists.txt +++ b/src/runtime/kwalletd/backend/CMakeLists.txt @@ -18,12 +18,14 @@ find_package(KF5CoreAddons ${KF_DEP_VERSION} REQUIRED) find_package(KF5I18n ${KF_DEP_VERSION} REQUIRED) find_package(KF5Notifications ${KF_DEP_VERSION} REQUIRED) find_package(KF5WidgetsAddons ${KF_DEP_VERSION} REQUIRED) +find_package(KF5Config ${KF_DEP_VERSION} REQUIRED) find_package(LibGcrypt 1.5.0 REQUIRED) set_package_properties(LibGcrypt PROPERTIES TYPE REQUIRED PURPOSE "kwalletd needs libgcrypt to perform PBKDF2-SHA512 hashing" ) +find_package(Qca-qt${QT_MAJOR_VERSION} REQUIRED 2.3.1) add_library(kwalletbackend5 SHARED) @@ -56,7 +58,7 @@ generate_export_header(kwalletbackend5) ecm_setup_version(${KF_VERSION} VARIABLE_PREFIX KWALLETBACKEND SOVERSION 5) -target_link_libraries(kwalletbackend5 Qt${QT_MAJOR_VERSION}::Widgets KF5::WidgetsAddons KF5::CoreAddons KF5::Notifications KF5::I18n ${LIBGCRYPT_LIBRARIES}) +target_link_libraries(kwalletbackend5 Qt${QT_MAJOR_VERSION}::Widgets KF5::WidgetsAddons KF5::CoreAddons KF5::Notifications KF5::I18n ${LIBGCRYPT_LIBRARIES} ${Qca_LIBRARY}) if(Gpgmepp_FOUND) target_link_libraries(kwalletbackend5 Gpgmepp) endif(Gpgmepp_FOUND) diff --git a/src/runtime/kwalletd/backend/kwalletbackend.cc b/src/runtime/kwalletd/backend/kwalletbackend.cc index 48131a58..e58da674 100644 --- a/src/runtime/kwalletd/backend/kwalletbackend.cc +++ b/src/runtime/kwalletd/backend/kwalletbackend.cc @@ -45,6 +45,22 @@ using namespace KWallet; #define KWMAGIC "KWALLET\n\r\0\r\n" +static const QByteArray walletAllowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789^&'@{}[],$=!-#()%.+_\r\n\t\f\v "; + +/* The encoding works if the name contains at least one unsupported character. + * Names that were allowed prior to the Secret Service API patch remain intact. + */ +QString Backend::encodeWalletName(const QString &name) { + /* Use a semicolon as "percent" because it does not conflict with already allowed characters for wallet names + * and is allowed for file names + */ + return QString::fromUtf8(name.toUtf8().toPercentEncoding(walletAllowedChars, {}, ';')); +} + +QString Backend::decodeWalletName(const QString &encodedName) { + return QString::fromUtf8(QByteArray::fromPercentEncoding(encodedName.toUtf8(), ';')); +} + class Backend::BackendPrivate { }; @@ -63,7 +79,7 @@ Backend::Backend(const QString &name, bool isPath) if (isPath) { _path = name; } else { - _path = getSaveLocation() + QDir::separator() + _name + ".kwl"; + _path = getSaveLocation() + '/' + encodeWalletName(_name) + ".kwl"; } _open = false; @@ -236,7 +252,7 @@ int Backend::deref() bool Backend::exists(const QString &wallet) { QString saveLocation = getSaveLocation(); - QString path = saveLocation + '/' + wallet + QLatin1String(".kwl"); + QString path = saveLocation + '/' + encodeWalletName(wallet) + QLatin1String(".kwl"); // Note: 60 bytes is presently the minimum size of a wallet file. // Anything smaller is junk. return QFile::exists(path) && QFileInfo(path).size() >= 60; @@ -449,7 +465,7 @@ int Backend::sync(WId w) return rc; } -int Backend::close(bool save) +int Backend::closeInternal(bool save) { // save if requested if (save) { @@ -466,13 +482,21 @@ int Backend::close(bool save) } } _entries.clear(); + _open = false; + + return 0; +} + +int Backend::close(bool save) +{ + int rc = closeInternal(save); + if (rc) + return rc; // empty the password hash _passhash.fill(0); _newPassHash.fill(0); - _open = false; - return 0; } @@ -481,6 +505,45 @@ const QString &Backend::walletName() const return _name; } +int Backend::renameWallet(const QString &newName, bool isPath) +{ + QString newPath; + const auto saveLocation = getSaveLocation(); + + if (isPath) { + newPath = newName; + } else { + newPath = saveLocation + QChar::fromLatin1('/') + encodeWalletName(newName) + QStringLiteral(".kwl"); + } + + if (newPath == _path) { + return 0; + } + + if (QFile::exists(newPath)) { + return -EEXIST; + } + + int rc = closeInternal(true); + if (rc) { + return rc; + } + + QFile::rename(_path, newPath); + QFile::rename(saveLocation + QChar::fromLatin1('/') + encodeWalletName(_name) + QStringLiteral(".salt"), + saveLocation + QChar::fromLatin1('/') + encodeWalletName(newName) + QStringLiteral(".salt")); + + _name = newName; + _path = newPath; + + rc = openInternal(); + if (rc) { + return rc; + } + + return 0; +} + bool Backend::isOpen() const { return _open; @@ -700,7 +763,7 @@ void Backend::setPassword(const QByteArray &password) password2hash(password, _passhash); QByteArray salt; - QFile saltFile(getSaveLocation() + QDir::separator() + _name + ".salt"); + QFile saltFile(getSaveLocation() + '/' + encodeWalletName(_name) + ".salt"); if (!saltFile.exists() || saltFile.size() == 0) { salt = createAndSaveSalt(saltFile.fileName()); } else { diff --git a/src/runtime/kwalletd/backend/kwalletbackend.h b/src/runtime/kwalletd/backend/kwalletbackend.h index 20121c54..9dffa3b1 100644 --- a/src/runtime/kwalletd/backend/kwalletbackend.h +++ b/src/runtime/kwalletd/backend/kwalletbackend.h @@ -99,6 +99,9 @@ public: // Returns the current wallet name. const QString &walletName() const; + // Rename the wallet + int renameWallet(const QString &newName, bool isPath = false); + // The list of folders. QStringList folderList() const; @@ -189,12 +192,14 @@ public: #endif static QString getSaveLocation(); + static QString encodeWalletName(const QString &name); + static QString decodeWalletName(const QString &encodedName); private: Q_DISABLE_COPY(Backend) class BackendPrivate; BackendPrivate *const d; - const QString _name; + QString _name; QString _path; bool _open; bool _useNewHash = false; @@ -209,6 +214,7 @@ private: QByteArray _passhash; // password hash used for saving the wallet QByteArray _newPassHash; // Modern hash using KWALLET_HASH_PBKDF2_SHA512 BackendCipherType _cipherType; // the kind of encryption used for this wallet + #ifdef HAVE_GPGMEPP GpgME::Key _gpgKey; #endif @@ -218,6 +224,7 @@ private: // open the wallet with the password already set. This is // called internally by both open and openPreHashed. int openInternal(WId w = 0); + int closeInternal(bool save); void swapToNewHash(); QByteArray createAndSaveSalt(const QString &path) const; }; diff --git a/src/runtime/kwalletd/kwalletd.cpp b/src/runtime/kwalletd/kwalletd.cpp index 36098b2e..b00c1a89 100644 --- a/src/runtime/kwalletd/kwalletd.cpp +++ b/src/runtime/kwalletd/kwalletd.cpp @@ -10,6 +10,11 @@ #include "kwalletd_debug.h" #include "kbetterthankdialog.h" +#include "kwalletfreedesktopcollection.h" +#include "kwalletfreedesktopitem.h" +#include "kwalletfreedesktopprompt.h" +#include "kwalletfreedesktopservice.h" +#include "kwalletfreedesktopsession.h" #include "kwalletwizard.h" #ifdef HAVE_GPGMEPP @@ -78,6 +83,11 @@ public: } } + static int getTransactionId() + { + return nextTransactionId; + } + ~KWalletTransaction() { } @@ -126,10 +136,17 @@ KWalletD::KWalletD() connect(&_closeTimers, &KTimeout::timedOut, this, &KWalletD::timedOutClose); connect(&_syncTimers, &KTimeout::timedOut, this, &KWalletD::timedOutSync); - (void)new KWalletAdaptor(this); - // register services - QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwalletd5")); - QDBusConnection::sessionBus().registerObject(QStringLiteral("/modules/kwalletd5"), this); + KConfig kwalletrc(QStringLiteral("kwalletrc")); + KConfigGroup cfgWallet(&kwalletrc, "Wallet"); + KConfigGroup cfgSecrets(&kwalletrc, "org.freedesktop.secrets"); + + if (cfgWallet.readEntry("apiEnabled", true)) { + (void)new KWalletAdaptor(this); + + // register services + QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwalletd5")); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/modules/kwalletd5"), this); + } #ifdef Q_WS_X11 screensaver = 0; @@ -148,6 +165,13 @@ KWalletD::KWalletD() _serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); connect(&_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &KWalletD::slotServiceOwnerChanged); + + if (cfgSecrets.readEntry("apiEnabled", true)) { + _fdoService.reset(new KWalletFreedesktopService(this)); + } else { + /* Do not keep dbus-daemon waiting for the org.freedesktop.secrets by registering the dummy-service */ + KWalletFreedesktopService(nullptr); + } } void KWalletD::registerKWalletd4Service() @@ -173,6 +197,16 @@ KWalletD::~KWalletD() qDeleteAll(_transactions); } +QString KWalletD::encodeWalletName(const QString &name) +{ + return KWallet::Backend::encodeWalletName(name); +} + +QString KWalletD::decodeWalletName(const QString &mangledName) +{ + return KWallet::Backend::decodeWalletName(mangledName); +} + #ifdef Q_WS_X11 void KWalletD::connectToScreenSaver() { @@ -212,7 +246,6 @@ QPair KWalletD::findWallet(const QString &walletName) c return qMakePair(-1, static_cast(nullptr)); } -static const QRegularExpression walletRegex(QStringLiteral("^[\\w\\^\\&\\'\\@\\{\\}\\[\\]\\,\\$\\=\\!\\-\\#\\(\\)\\%\\.\\+\\_\\s]+$")); bool KWalletD::_processing = false; void KWalletD::processTransactions() @@ -318,10 +351,6 @@ int KWalletD::open(const QString &wallet, qlonglong wId, const QString &appid) return -1; } - if (!walletRegex.match(wallet).hasMatch()) { - return -1; - } - KWalletTransaction *xact = new KWalletTransaction(connection()); _transactions.append(xact); @@ -342,17 +371,23 @@ int KWalletD::open(const QString &wallet, qlonglong wId, const QString &appid) return 0; } -int KWalletD::openAsync(const QString &wallet, qlonglong wId, const QString &appid, bool handleSession) +int KWalletD::nextTransactionId() const { - if (!_enabled) { // guard - return -1; - } + return KWalletTransaction::getTransactionId(); +} - if (!walletRegex.match(wallet).hasMatch()) { +int KWalletD::openAsync(const QString &wallet, + qlonglong wId, + const QString &appid, + bool handleSession, + const QDBusConnection &connection, + const QDBusMessage &message) +{ + if (!_enabled) { // guard return -1; } - KWalletTransaction *xact = new KWalletTransaction(connection()); + KWalletTransaction *xact = new KWalletTransaction(connection); _transactions.append(xact); xact->appid = appid; @@ -362,10 +397,10 @@ int KWalletD::openAsync(const QString &wallet, qlonglong wId, const QString &app xact->tType = KWalletTransaction::Open; xact->isPath = false; if (handleSession) { - qCDebug(KWALLETD_LOG) << "openAsync for " << message().service(); - _serviceWatcher.setConnection(connection()); - _serviceWatcher.addWatchedService(message().service()); - xact->service = message().service(); + qCDebug(KWALLETD_LOG) << "openAsync for " << message.service(); + _serviceWatcher.setConnection(connection); + _serviceWatcher.addWatchedService(message.service()); + xact->service = message.service(); } QTimer::singleShot(0, this, SLOT(processTransactions())); checkActiveDialog(); @@ -373,6 +408,11 @@ int KWalletD::openAsync(const QString &wallet, qlonglong wId, const QString &app return xact->tId; } +int KWalletD::openAsync(const QString &wallet, qlonglong wId, const QString &appid, bool handleSession) +{ + return openAsync(wallet, wId, appid, handleSession, connection(), message()); +} + int KWalletD::openPathAsync(const QString &path, qlonglong wId, const QString &appid, bool handleSession) { if (!_enabled) { // guard @@ -856,8 +896,8 @@ bool KWalletD::isAuthorizedApp(const QString &appid, const QString &wallet, WId int KWalletD::deleteWallet(const QString &wallet) { int result = -1; - QString path = KWallet::Backend::getSaveLocation() + "/" + wallet + ".kwl"; - QString pathSalt = KWallet::Backend::getSaveLocation() + "/" + wallet + ".salt"; + QString path = KWallet::Backend::getSaveLocation() + "/" + encodeWalletName(wallet) + ".kwl"; + QString pathSalt = KWallet::Backend::getSaveLocation() + "/" + encodeWalletName(wallet) + ".salt"; if (QFile::exists(path)) { const QPair walletInfo = findWallet(wallet); @@ -1010,14 +1050,14 @@ int KWalletD::internalClose(KWallet::Backend *const w, const int handle, const b return -1; } -int KWalletD::close(int handle, bool force, const QString &appid) +int KWalletD::close(int handle, bool force, const QString &appid, const QDBusMessage &message) { KWallet::Backend *w = _wallets.value(handle); if (w) { if (_sessions.hasSession(appid, handle)) { // remove one handle for the application - bool removed = _sessions.removeSession(appid, message().service(), handle); + bool removed = _sessions.removeSession(appid, message.service(), handle); // alternatively try sessionless if (removed || _sessions.removeSession(appid, QLatin1String(""), handle)) { w->deref(); @@ -1029,6 +1069,11 @@ int KWalletD::close(int handle, bool force, const QString &appid) return -1; // not open to begin with, or other error } +int KWalletD::close(int handle, bool force, const QString &appid) +{ + return close(handle, force, appid, message()); +} + bool KWalletD::isOpen(const QString &wallet) { const QPair walletInfo = findWallet(wallet); @@ -1067,7 +1112,7 @@ QStringList KWalletD::wallets() const if (fn.endsWith(QLatin1String(".kwl"))) { fn.truncate(fn.length() - 4); } - rc += fn; + rc += decodeWalletName(fn); } return rc; } @@ -1347,13 +1392,14 @@ int KWalletD::writeMap(int handle, const QString &folder, const QString &key, co b->writeEntry(&e); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); + emitEntryUpdated(b->walletName(), folder, key); return 0; } return -1; } -int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, const QByteArray &value, int entryType, const QString &appid) +int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, const QByteArray &value, const QString &appid) { KWallet::Backend *b; @@ -1362,17 +1408,18 @@ int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, KWallet::Entry e; e.setKey(key); e.setValue(value); - e.setType(KWallet::Wallet::EntryType(entryType)); + e.setType(KWallet::Wallet::Stream); b->writeEntry(&e); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); + emitEntryUpdated(b->walletName(), folder, key); return 0; } return -1; } -int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, const QByteArray &value, const QString &appid) +int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, const QByteArray &value, int entryType, const QString &appid) { KWallet::Backend *b; @@ -1381,7 +1428,7 @@ int KWalletD::writeEntry(int handle, const QString &folder, const QString &key, KWallet::Entry e; e.setKey(key); e.setValue(value); - e.setType(KWallet::Wallet::Stream); + e.setType(KWallet::Wallet::EntryType(entryType)); b->writeEntry(&e); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); @@ -1404,6 +1451,7 @@ int KWalletD::writePassword(int handle, const QString &folder, const QString &ke b->writeEntry(&e); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); + emitEntryUpdated(b->walletName(), folder, key); return 0; } @@ -1454,6 +1502,7 @@ int KWalletD::removeEntry(int handle, const QString &folder, const QString &key, bool rc = b->removeEntry(key); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); + emitEntryDeleted(b->walletName(), folder, key); return rc ? 0 : -3; } @@ -1567,12 +1616,19 @@ int KWalletD::renameEntry(int handle, const QString &folder, const QString &oldN int rc = b->renameEntry(oldName, newName); initiateSync(handle); emitFolderUpdated(b->walletName(), folder); + emitEntryRenamed(b->walletName(), folder, oldName, newName); return rc; } return -1; } +int KWalletD::renameWallet(const QString &oldName, const QString &newName) +{ + const QPair walletInfo = findWallet(oldName); + return walletInfo.second->renameWallet(newName); +} + QStringList KWalletD::users(const QString &wallet) const { const QPair walletInfo = findWallet(wallet); @@ -1605,6 +1661,21 @@ void KWalletD::emitFolderUpdated(const QString &wallet, const QString &folder) Q_EMIT folderUpdated(wallet, folder); } +void KWalletD::emitEntryUpdated(const QString &wallet, const QString &folder, const QString &key) +{ + Q_EMIT entryUpdated(wallet, folder, key); +} + +void KWalletD::emitEntryRenamed(const QString &wallet, const QString &folder, const QString &oldName, const QString &newName) +{ + Q_EMIT entryRenamed(wallet, folder, oldName, newName); +} + +void KWalletD::emitEntryDeleted(const QString &wallet, const QString &folder, const QString &key) +{ + Q_EMIT entryDeleted(wallet, folder, key); +} + void KWalletD::emitWalletListDirty() { const QStringList walletsInDisk = wallets(); @@ -1796,10 +1867,6 @@ int KWalletD::pamOpen(const QString &wallet, const QByteArray &passwordHash, int return -1; } - if (!walletRegex.match(wallet).hasMatch()) { - return -1; - } - // check if the wallet is already open QPair walletInfo = findWallet(wallet); int rc = walletInfo.first; @@ -1847,4 +1914,5 @@ int KWalletD::pamOpen(const QString &wallet, const QByteArray &passwordHash, int return handle; } + // vim: tw=220:ts=4 diff --git a/src/runtime/kwalletd/kwalletd.h b/src/runtime/kwalletd/kwalletd.h index 8f82a0ca..7638a1a5 100644 --- a/src/runtime/kwalletd/kwalletd.h +++ b/src/runtime/kwalletd/kwalletd.h @@ -27,6 +27,7 @@ class KTimeout; // @Private class KWalletTransaction; class KWalletSessionStore; +class KWalletFreedesktopService; class KWalletD : public QObject, protected QDBusContext { @@ -36,7 +37,17 @@ public: KWalletD(); ~KWalletD() override; + static QString encodeWalletName(const QString &name); + static QString decodeWalletName(const QString &mangledName); + + int nextTransactionId() const; + int + openAsync(const QString &wallet, qlonglong wId, const QString &appid, bool handleSession, const QDBusConnection &connection, const QDBusMessage &message); + // Close and lock the wallet + // Accepts "message" for working from other QDBusContexts + int close(int handle, bool force, const QString &appid, const QDBusMessage &message); public Q_SLOTS: + // Is the wallet enabled? If not, all open() calls fail. bool isEnabled() const; @@ -121,6 +132,8 @@ public Q_SLOTS: // Rename an entry. rc=0 on success. int renameEntry(int handle, const QString &folder, const QString &oldName, const QString &newName, const QString &appid); + // Rename the wallet + int renameWallet(const QString &oldName, const QString &newName); // Write an entry. rc=0 on success. int writeEntry(int handle, const QString &folder, const QString &key, const QByteArray &value, int entryType, const QString &appid); @@ -176,6 +189,9 @@ Q_SIGNALS: void allWalletsClosed(); void folderListUpdated(const QString &wallet); void folderUpdated(const QString &, const QString &); + void entryUpdated(const QString &, const QString &, const QString &); + void entryRenamed(const QString &, const QString &, const QString &, const QString &); + void entryDeleted(const QString &, const QString &, const QString &); void applicationDisconnected(const QString &wallet, const QString &application); private Q_SLOTS: @@ -205,6 +221,9 @@ private: // Emit signals about closing wallets void doCloseSignals(int, const QString &); void emitFolderUpdated(const QString &, const QString &); + void emitEntryUpdated(const QString &, const QString &, const QString &); + void emitEntryRenamed(const QString &, const QString &, const QString &, const QString &); + void emitEntryDeleted(const QString &, const QString &, const QString &); // Implicitly allow access for this application bool implicitAllow(const QString &wallet, const QString &app); bool implicitDeny(const QString &wallet, const QString &app); @@ -246,6 +265,8 @@ private: KWalletSessionStore _sessions; QDBusServiceWatcher _serviceWatcher; + std::unique_ptr _fdoService; + bool _useGpg; }; diff --git a/src/runtime/kwalletd/kwalletdbuscontext.cpp b/src/runtime/kwalletd/kwalletdbuscontext.cpp new file mode 100644 index 00000000..9f764458 --- /dev/null +++ b/src/runtime/kwalletd/kwalletdbuscontext.cpp @@ -0,0 +1,17 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2022 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletdbuscontext.h" + +const QDBusMessage &KWalletDBusContext::message() const +{ + return QDBusContext::message(); +} + +QDBusConnection KWalletDBusContext::connection() const +{ + return QDBusContext::connection(); +} diff --git a/src/runtime/kwalletd/kwalletdbuscontext.h b/src/runtime/kwalletd/kwalletdbuscontext.h new file mode 100644 index 00000000..87574f06 --- /dev/null +++ b/src/runtime/kwalletd/kwalletdbuscontext.h @@ -0,0 +1,54 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2022 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETDBUSCONTEXT_H_ +#define _KWALLETDBUSCONTEXT_H_ + +#include "qdbuserror.h" +#include +#include +#include + +/* FDO_DBUS_CONTEXT macro will be replaced by KWalletDBusContextDummy during + * preprocessing if FDO_ENABLE_DUMMY_MESSAGE_CONNECTION was defined, + * otherwise we get QDBusContext. + * + * This is used for mocking QDBusContext in autotests. + * + * QDBusContext's connection() and message() member functions can't be called + * without a real connection context (this cause segfault). + * So we need to use KWalletDBusContextDummy in case some DBus-related + * member functions may call connection()/message(). + * + * This header defines FDO_DBUS_CONTEXT macro that should be used instead of + * QDBusContext in all DBus-related which we want to use in autotests. + */ + +#ifdef FDO_ENABLE_DUMMY_MESSAGE_CONNECTION + +class KWalletDBusContextDummy : public QDBusContext +{ +public: + const QDBusMessage &message() + { + static auto msg = QDBusMessage::createSignal(QStringLiteral("dummy"), QStringLiteral("dummy"), QStringLiteral("dummy")); + return msg; + } + QDBusConnection connection() const + { + return QDBusConnection::sessionBus(); + } +}; + +#define FDO_DBUS_CONTEXT KWalletDBusContextDummy + +#else + +#define FDO_DBUS_CONTEXT QDBusContext + +#endif + +#endif // _KWALLETDBUSCONTEXT_H_ diff --git a/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp b/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp new file mode 100644 index 00000000..b0791cbc --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp @@ -0,0 +1,346 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopattributes.h" + +#include "kwalletd.h" +#include "kwalletd_debug.h" +#include "kwalletfreedesktopcollection.h" +#include +#include +#include + +KWalletFreedesktopAttributes::KWalletFreedesktopAttributes(const QString &walletName) +{ + QString writeLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (!writeLocation.isEmpty() && writeLocation.back() == QChar::fromLatin1('5')) { + writeLocation.resize(writeLocation.size() - 1); + } + _path = writeLocation + QChar::fromLatin1('/') + KWalletD::encodeWalletName(walletName) + QStringLiteral("_attributes.json"); + + read(); + + if (!_params.contains(FDO_KEY_CREATED)) { + const auto currentTime = QString::number(QDateTime::currentSecsSinceEpoch()); + _params[FDO_KEY_CREATED] = currentTime; + _params[FDO_KEY_MODIFIED] = currentTime; + } +} + +void KWalletFreedesktopAttributes::read() +{ + QByteArray content; + { + QFile file(_path); + file.open(QIODevice::ReadOnly | QIODevice::Text); + if (!file.isOpen()) { + qCDebug(KWALLETD_LOG) << "Can't read attributes file " << _path; + return; + } + content = file.readAll(); + } + + const auto jsonDoc = QJsonDocument::fromJson(content); + if (jsonDoc.isObject()) { + _params = jsonDoc.object(); + } else { + qCWarning(KWALLETD_LOG) << "Can't read attributes: the root element must be an JSON-object: " << _path; + _params = QJsonObject(); + } +} + +void KWalletFreedesktopAttributes::write() +{ + if (_params.empty()) { + QFile::remove(_path); + return; + } + + updateLastModified(); + + QSaveFile sf(_path); + if (!sf.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { + qCWarning(KWALLETD_LOG) << "Can't write attributes file: " << _path; + return; + } + sf.setPermissions(QSaveFile::ReadUser | QSaveFile::WriteUser); + + const QJsonDocument saveDoc(_params); + + const QByteArray jsonBytes = saveDoc.toJson(); + if (sf.write(jsonBytes) != jsonBytes.size()) { + sf.cancelWriting(); + qCWarning(KWALLETD_LOG) << "Cannot write attributes file " << _path; + return; + } + if (!sf.commit()) { + qCWarning(KWALLETD_LOG) << "Cannot commit attributes file " << _path; + } +} + +static QString entryLocationToStr(const EntryLocation &entryLocation) +{ + return entryLocation.folder + QChar::fromLatin1('/') + entryLocation.key; +} + +static EntryLocation splitToEntryLocation(const QString &entryLocation) +{ + const int slashPos = entryLocation.lastIndexOf(QChar::fromLatin1('/')); + if (slashPos == -1) { + qCWarning(KWALLETD_LOG) << "Entry location '" << entryLocation << "' has no slash '/'"; + return {}; + } else { + return {entryLocation.left(slashPos), entryLocation.right((entryLocation.size() - slashPos) - 1)}; + } +} + +void KWalletFreedesktopAttributes::remove(const EntryLocation &entryLocation) +{ + _params.remove(entryLocationToStr(entryLocation)); + if (_params.empty()) { + QFile::remove(_path); + } else { + write(); + } +} + +void KWalletFreedesktopAttributes::deleteFile() +{ + QFile::remove(_path); +} + +void KWalletFreedesktopAttributes::renameLabel(const EntryLocation &oldLocation, const EntryLocation &newLocation) +{ + const QString oldLoc = entryLocationToStr(oldLocation); + + const auto found = _params.find(oldLoc); + if (found == _params.end() || !found->isObject()) { + qCWarning(KWALLETD_LOG) << "Can't rename label (!?)"; + return; + } + const auto obj = found->toObject(); + _params.erase(found); + _params.insert(entryLocationToStr(newLocation), obj); + + write(); +} + +void KWalletFreedesktopAttributes::renameWallet(const QString &newName) +{ + QString writeLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + if (!writeLocation.isEmpty() && writeLocation.back() == QChar::fromLatin1('5')) { + writeLocation.resize(writeLocation.size() - 1); + } + const QString newPath = writeLocation + QChar::fromLatin1('/') + newName + QStringLiteral("_attributes.json"); + + QFile::rename(_path, newPath); + _path = newPath; +} + +void KWalletFreedesktopAttributes::newItem(const EntryLocation &entryLocation) +{ + _params[entryLocationToStr(entryLocation)] = QJsonObject(); +} + +QList KWalletFreedesktopAttributes::matchAttributes(const StrStrMap &attributes) const +{ + QList items; + + for (auto i = _params.constBegin(); i != _params.constEnd(); ++i) { + if (!i->isObject()) { + continue; + } + + bool match = true; + const auto itemParams = i->toObject(); + const auto foundItemAttribs = itemParams.find(QStringLiteral("attributes")); + if (foundItemAttribs == itemParams.end() || !foundItemAttribs->isObject()) { + continue; + } + const auto itemAttribs = foundItemAttribs->toObject(); + + for (auto i = attributes.constBegin(); i != attributes.constEnd(); ++i) { + const auto foundKey = itemAttribs.find(i.key()); + if (foundKey == itemAttribs.end() || !foundKey->isString() || foundKey->toString() != i.value()) { + match = false; + break; + } + } + + if (match) { + items += splitToEntryLocation(i.key()); + } + } + + return items; +} + +void KWalletFreedesktopAttributes::setAttributes(const EntryLocation &entryLocation, const StrStrMap &attributes) +{ + QJsonObject jsonAttrs; + for (auto i = attributes.constBegin(); i != attributes.constEnd(); ++i) { + jsonAttrs.insert(i.key(), i.value()); + } + + const QString strLocation = entryLocationToStr(entryLocation); + + const auto foundParams = _params.find(strLocation); + QJsonObject params; + if (foundParams != _params.end() && foundParams->isObject()) { + params = foundParams->toObject(); + } else { + return; + } + + if (jsonAttrs.empty()) { + params.remove(QStringLiteral("attributes")); + } else { + params[QStringLiteral("attributes")] = jsonAttrs; + } + + _params[strLocation] = params; + + write(); +} + +StrStrMap KWalletFreedesktopAttributes::getAttributes(const EntryLocation &entryLocation) const +{ + const auto foundObj = _params.find(entryLocationToStr(entryLocation)); + if (foundObj == _params.end() || !foundObj->isObject()) { + return StrStrMap(); + } + const auto jsonParams = foundObj->toObject(); + + const auto foundAttrs = jsonParams.find(QStringLiteral("attributes")); + if (foundAttrs == jsonParams.end() || !foundAttrs->isObject()) { + return StrStrMap(); + } + const auto jsonAttrs = foundAttrs->toObject(); + + StrStrMap itemAttrs; + + for (auto i = jsonAttrs.constBegin(); i != jsonAttrs.constEnd(); ++i) { + if (i.value().isString()) { + itemAttrs.insert(i.key(), i.value().toString()); + } + } + + return itemAttrs; +} + +QString KWalletFreedesktopAttributes::getStringParam(const EntryLocation &entryLocation, const QString ¶mName, const QString &defaultParam) const +{ + const auto foundParams = _params.find(entryLocationToStr(entryLocation)); + if (foundParams == _params.end() || !foundParams->isObject()) { + return defaultParam; + } + const auto params = foundParams->toObject(); + + const auto foundParam = params.find(paramName); + if (foundParam == params.end() || !foundParam->isString()) { + return defaultParam; + } + + return foundParam->toString(); +} + +qulonglong KWalletFreedesktopAttributes::getULongLongParam(const EntryLocation &entryLocation, const QString ¶mName, qulonglong defaultParam) const +{ + const auto str = getStringParam(entryLocation, paramName, QString::number(defaultParam)); + bool ok = false; + const auto result = str.toULongLong(&ok); + return ok ? result : defaultParam; +} + +void KWalletFreedesktopAttributes::setParam(const EntryLocation &entryLocation, const QString ¶mName, const QString ¶m) +{ + const auto entryLoc = entryLocationToStr(entryLocation); + const auto foundParams = _params.find(entryLoc); + if (foundParams == _params.end() || !foundParams->isObject()) { + return; + } + + auto params = foundParams->toObject(); + + params[paramName] = param; + _params[entryLoc] = params; + + write(); +} + +void KWalletFreedesktopAttributes::setParam(const EntryLocation &entryLocation, const QString ¶mName, qulonglong param) +{ + setParam(entryLocation, paramName, QString::number(param)); +} + +void KWalletFreedesktopAttributes::remove(const FdoUniqueLabel &itemUniqLabel) +{ + remove(itemUniqLabel.toEntryLocation()); +} + +void KWalletFreedesktopAttributes::setAttributes(const FdoUniqueLabel &itemUniqLabel, const StrStrMap &attributes) +{ + setAttributes(itemUniqLabel.toEntryLocation(), attributes); +} + +StrStrMap KWalletFreedesktopAttributes::getAttributes(const FdoUniqueLabel &itemUniqLabel) const +{ + return getAttributes(itemUniqLabel.toEntryLocation()); +} + +QString KWalletFreedesktopAttributes::getStringParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, const QString &defaultParam) const +{ + return getStringParam(itemUniqLabel.toEntryLocation(), paramName, defaultParam); +} + +qulonglong KWalletFreedesktopAttributes::getULongLongParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, qulonglong defaultParam) const +{ + return getULongLongParam(itemUniqLabel.toEntryLocation(), paramName, defaultParam); +} + +void KWalletFreedesktopAttributes::setParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, const QString ¶m) +{ + setParam(itemUniqLabel.toEntryLocation(), paramName, param); +} + +void KWalletFreedesktopAttributes::setParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, qulonglong param) +{ + setParam(itemUniqLabel.toEntryLocation(), paramName, param); +} + +QList KWalletFreedesktopAttributes::listItems() const +{ + QList items; + for (auto i = _params.constBegin(); i != _params.constEnd(); ++i) { + if (i->isObject()) { + items.push_back(splitToEntryLocation(i.key())); + } + } + return items; +} + +qulonglong KWalletFreedesktopAttributes::lastModified() const +{ + auto found = _params.constFind(FDO_KEY_MODIFIED); + if (found == _params.constEnd()) { + return 0; + } + return found->toString().toULongLong(); +} + +qulonglong KWalletFreedesktopAttributes::birthTime() const +{ + auto found = _params.constFind(FDO_KEY_CREATED); + if (found == _params.constEnd()) { + return 0; + } + return found->toString().toULongLong(); +} + +void KWalletFreedesktopAttributes::updateLastModified() +{ + _params[FDO_KEY_MODIFIED] = QString::number(QDateTime::currentSecsSinceEpoch()); +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopattributes.h b/src/runtime/kwalletd/kwalletfreedesktopattributes.h new file mode 100644 index 00000000..f273b5fd --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopattributes.h @@ -0,0 +1,56 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPATTRIBUTES_H_ +#define _KWALLETFREEDESKTOPATTRIBUTES_H_ + +#include "kwalletfreedesktopservice.h" + +class KWalletFreedesktopAttributes : public QObject +{ +public: + KWalletFreedesktopAttributes(const QString &walletName); + + void read(); + void write(); + void remove(const EntryLocation &entryLocation); + void remove(const FdoUniqueLabel &itemUniqLabel); + void renameLabel(const EntryLocation &oldLocation, const EntryLocation &newLocation); + void deleteFile(); + void renameWallet(const QString &newName); + void newItem(const EntryLocation &entryLocation); + + QList matchAttributes(const StrStrMap &attributes) const; + void setAttributes(const EntryLocation &entryLocation, const StrStrMap &attributes); + StrStrMap getAttributes(const EntryLocation &entryLocation) const; + + void setAttributes(const FdoUniqueLabel &itemUniqLabel, const StrStrMap &attributes); + StrStrMap getAttributes(const FdoUniqueLabel &itemUniqLabel) const; + + QString getStringParam(const EntryLocation &entryLocation, const QString ¶mName, const QString &defaultParam) const; + qulonglong getULongLongParam(const EntryLocation &entryLocation, const QString ¶mName, qulonglong defaultParam) const; + + QString getStringParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, const QString &defaultParam) const; + qulonglong getULongLongParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, qulonglong defaultParam) const; + + void setParam(const EntryLocation &entryLocation, const QString ¶mName, const QString ¶m); + void setParam(const EntryLocation &entryLocation, const QString ¶mName, qulonglong param); + + void setParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, const QString ¶m); + void setParam(const FdoUniqueLabel &itemUniqLabel, const QString ¶mName, qulonglong param); + + qulonglong lastModified() const; + qulonglong birthTime() const; + void updateLastModified(); + + QList listItems() const; + +private: + QString _path; + QJsonObject _params; +}; + +#endif diff --git a/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp b/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp new file mode 100644 index 00000000..3d5a5c2c --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp @@ -0,0 +1,416 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopcollection.h" + +#include "kwalletd.h" +#include "kwalletfreedesktopcollectionadaptor.h" +#include "kwalletfreedesktopitem.h" + +KWalletFreedesktopCollection::KWalletFreedesktopCollection(KWalletFreedesktopService *service, + int handle, + const QString &walletName, + QDBusObjectPath objectPath) + : m_service(service) + , m_handle(handle) + , m_uniqueLabel(FdoUniqueLabel::fromName(walletName)) + , m_objectPath(std::move(objectPath)) + , m_itemAttribs(walletName) +{ + (void)new KWalletFreedesktopCollectionAdaptor(this); + QDBusConnection::sessionBus().registerObject(fdoObjectPath().path(), this); + + const QStringList aliases = fdoService()->readAliasesFor(walletName); + for (const auto &alias : aliases) { + QDBusConnection::sessionBus().registerObject(QStringLiteral(FDO_ALIAS_PATH) + alias, this); + } + + onWalletChangeState(handle); + + /* Create items described in the attributes file */ + if (m_handle == -1) { + const auto items = itemAttributes().listItems(); + for (const auto &entryLocation : items) { + if (!findItemByEntryLocation(entryLocation)) { + pushNewItem(entryLocation.toUniqueLabel(), nextItemPath()); + } + } + } +} + +QDBusObjectPath KWalletFreedesktopCollection::nextItemPath() +{ + return QDBusObjectPath(fdoObjectPath().path() + QChar::fromLatin1('/') + QString::number(m_itemCounter++)); +} + +const QString &KWalletFreedesktopCollection::label() const +{ + return m_uniqueLabel.label; +} + +void KWalletFreedesktopCollection::setLabel(const QString &newLabel) +{ + if (newLabel == label()) { + return; + } + + const auto oldName = m_uniqueLabel.toName(); + const auto newUniqLabel = fdoService()->makeUniqueCollectionLabel(newLabel); + const auto newName = newUniqLabel.toName(); + + int rc = backend()->renameWallet(oldName, newName); + if (rc == 0) { + const QStringList aliases = fdoService()->readAliasesFor(walletName()); + m_uniqueLabel = newUniqLabel; + const QString newName = walletName(); + for (const auto &alias : aliases) { + fdoService()->updateCollectionAlias(alias, newName); + } + + itemAttributes().renameWallet(newName); + } +} + +bool KWalletFreedesktopCollection::locked() const +{ + return m_handle < 0 || !backend()->isOpen(m_handle); +} + +QList KWalletFreedesktopCollection::items() const +{ + QList items; + + for (const auto &item : m_items) { + items.push_back(item.second->fdoObjectPath()); + } + + return items; +} + +qulonglong KWalletFreedesktopCollection::created() const +{ + return itemAttributes().birthTime(); +} + +qulonglong KWalletFreedesktopCollection::modified() const +{ + return itemAttributes().lastModified(); +} + +QDBusObjectPath +KWalletFreedesktopCollection::CreateItem(const PropertiesMap &properties, const FreedesktopSecret &secret, bool replace, QDBusObjectPath &prompt) +{ + prompt = QDBusObjectPath("/"); + + if (m_handle == -1) { + sendErrorReply(QStringLiteral("org.freedesktop.Secret.Error.IsLocked"), + QStringLiteral("Collection ") + fdoObjectPath().path() + QStringLiteral(" is locked")); + return QDBusObjectPath("/"); + } + + const auto labelFound = properties.map.find(QStringLiteral("org.freedesktop.Secret.Item.Label")); + if (labelFound == properties.map.end()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Item label is missing (org.freedesktop.Secret.Item.Label)")); + return QDBusObjectPath("/"); + } + if (!labelFound->canConvert()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Item label is not a string (org.freedesktop.Secret.Item.Label)")); + return QDBusObjectPath("/"); + } + + const QString fdoLabel = labelFound->toString(); + QString dir, label; + QDBusObjectPath itemPath; + + StrStrMap attribs; + const auto attribsFound = properties.map.find(QStringLiteral("org.freedesktop.Secret.Item.Attributes")); + if (attribsFound != properties.map.end() && attribsFound->canConvert()) { + attribs = attribsFound->value(); + } + + if (replace) { + /* Try find item with same attributes */ + const auto matchedItems = itemAttributes().matchAttributes(attribs); + + if (!matchedItems.empty()) { + const auto &entryLoc = matchedItems.constFirst(); + const auto item = findItemByEntryLocation(entryLoc); + if (item) { + itemPath = item->fdoObjectPath(); + dir = entryLoc.folder; + label = entryLoc.key; + } + } + } + + if (dir.isEmpty() && label.isEmpty()) { + const auto entryLocation = makeUniqueEntryLocation(fdoLabel); + dir = entryLocation.folder; + label = entryLocation.key; + itemPath = nextItemPath(); + } + + if (label.isEmpty()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Item label is invalid (org.freedesktop.Secret.Item.Label)")); + return QDBusObjectPath("/"); + } + + const qulonglong createTime = QDateTime::currentSecsSinceEpoch(); + const EntryLocation entryLoc{dir, label}; + itemAttributes().newItem(entryLoc); + itemAttributes().setParam(entryLoc, FDO_KEY_MIME, secret.mimeType); + itemAttributes().setParam(entryLoc, FDO_KEY_CREATED, createTime); + itemAttributes().setParam(entryLoc, FDO_KEY_MODIFIED, createTime); + itemAttributes().setAttributes(entryLoc, attribs); + + pushNewItem(entryLoc.toUniqueLabel(), itemPath); + + { + auto decrypted = secret; + if (!fdoService()->desecret(message(), decrypted)) { + sendErrorReply(QDBusError::ErrorType::InvalidObjectPath, QStringLiteral("Can't find session ") + secret.session.path()); + return QDBusObjectPath("/"); + } + + QString xdgSchema = QStringLiteral("org.kde.KWallet.Stream"); + const auto found = attribs.find(FDO_KEY_XDG_SCHEMA); + if (found != attribs.end()) { + xdgSchema = found.value(); + } + + if (xdgSchema == QStringLiteral("org.kde.KWallet.Password") || secret.mimeType.startsWith(QStringLiteral("text/"))) { + auto bytes = decrypted.note.toByteArray(); + auto str = QString::fromUtf8(bytes); + backend()->writePassword(walletHandle(), dir, label, str, FDO_APPID); + explicit_zero_mem(bytes.data(), bytes.size()); + explicit_zero_mem(str.data(), str.size() * sizeof(QChar)); + } else { + auto bytes = decrypted.note.toByteArray(); + backend()->writeEntry(walletHandle(), dir, label, bytes, KWallet::Wallet::Stream, FDO_APPID); + explicit_zero_mem(bytes.data(), bytes.size()); + } + } + + onItemCreated(itemPath); + + return itemPath; +} + +QDBusObjectPath KWalletFreedesktopCollection::Delete() +{ + const auto name = walletName(); + + const QStringList aliases = fdoService()->readAliasesFor(name); + for (const QString &alias : aliases) { + fdoService()->removeAlias(alias); + } + + backend()->deleteWallet(name); + QDBusConnection::sessionBus().unregisterObject(fdoObjectPath().path()); + m_service->onCollectionDeleted(fdoObjectPath()); + + return QDBusObjectPath("/"); +} + +QList KWalletFreedesktopCollection::SearchItems(const StrStrMap &attributes) +{ + QList result; + + for (const auto &entryLoc : m_itemAttribs.matchAttributes(attributes)) { + auto *itm = findItemByEntryLocation(entryLoc); + if (itm) { + result.push_back(itm->fdoObjectPath()); + } + } + + return result; +} + +int KWalletFreedesktopCollection::walletHandle() const +{ + return m_handle; +} + +KWalletFreedesktopItem *KWalletFreedesktopCollection::getItemByObjectPath(const QString &objectPath) const +{ + const auto found = m_items.find(objectPath); + if (found != m_items.end()) { + return found->second.get(); + } else { + return nullptr; + } +} + +KWalletFreedesktopItem *KWalletFreedesktopCollection::findItemByEntryLocation(const EntryLocation &entryLocation) const +{ + const auto uniqLabel = FdoUniqueLabel::fromEntryLocation(entryLocation); + + for (const auto &itemPair : m_items) { + auto *item = itemPair.second.get(); + if (item->uniqueLabel() == uniqLabel) { + return item; + } + } + + return nullptr; +} + +EntryLocation KWalletFreedesktopCollection::makeUniqueEntryLocation(const QString &label) +{ + QString dir, name; + + const int slashPos = label.lastIndexOf(QChar::fromLatin1('/')); + if (slashPos == -1) { + dir = QStringLiteral(FDO_SECRETS_DEFAULT_DIR); + name = label; + } else { + dir = label.left(slashPos); + name = label.mid(slashPos + 1); + } + + int suffix = 0; + QString resultName = name; + while (backend()->hasEntry(m_handle, dir, resultName, FDO_APPID)) { + resultName = FdoUniqueLabel::makeName(name, suffix++); + } + + return {dir, resultName}; +} + +FdoUniqueLabel KWalletFreedesktopCollection::makeUniqueItemLabel(const QString &label) +{ + return makeUniqueEntryLocation(label).toUniqueLabel(); +} + +KWalletFreedesktopItem &KWalletFreedesktopCollection::pushNewItem(FdoUniqueLabel uniqLabel, const QDBusObjectPath &path) +{ + m_items.erase(path.path()); + auto item = std::make_unique(this, std::move(uniqLabel), path); + return *m_items.emplace(path.path(), std::move(item)).first->second; +} + +KWalletFreedesktopItem &KWalletFreedesktopCollection::pushNewItem(const QString &label, const QDBusObjectPath &path) +{ + return pushNewItem(makeUniqueItemLabel(label), path); +} + +KWalletFreedesktopService *KWalletFreedesktopCollection::fdoService() const +{ + return m_service; +} + +KWalletD *KWalletFreedesktopCollection::backend() const +{ + return fdoService()->backend(); +} + +QDBusObjectPath KWalletFreedesktopCollection::fdoObjectPath() const +{ + return m_objectPath; +} + +const FdoUniqueLabel &KWalletFreedesktopCollection::uniqueLabel() const +{ + return m_uniqueLabel; +} + +QString KWalletFreedesktopCollection::walletName() const +{ + return m_uniqueLabel.toName(); +} + +void KWalletFreedesktopCollection::onWalletChangeState(int handle) +{ + if (handle == m_handle) { + return; + } + + if (handle >= 0 && m_handle >= 0) { + m_handle = handle; + return; + } + + m_handle = handle; + + if (m_handle < 0 || !m_items.empty()) { + return; + } + + const QStringList folderList = backend()->folderList(m_handle, FDO_APPID); + for (const QString &folder : folderList) { + const QStringList entries = backend()->entryList(m_handle, folder, FDO_APPID); + + for (const auto &entry : entries) { + const EntryLocation entryLoc{folder, entry}; + const auto itm = findItemByEntryLocation(entryLoc); + if (!itm) { + auto &newItem = pushNewItem(entryLoc.toUniqueLabel(), nextItemPath()); + Q_EMIT ItemChanged(newItem.fdoObjectPath()); + } else { + Q_EMIT ItemChanged(itm->fdoObjectPath()); + } + } + } +} + +void KWalletFreedesktopCollection::onItemCreated(const QDBusObjectPath &item) +{ + itemAttributes().updateLastModified(); + Q_EMIT ItemCreated(item); + + QVariantMap props; + props[QStringLiteral("Items")] = QVariant::fromValue(items()); + onPropertiesChanged(props); +} + +void KWalletFreedesktopCollection::onItemChanged(const QDBusObjectPath &item) +{ + itemAttributes().updateLastModified(); + Q_EMIT ItemChanged(item); +} + +void KWalletFreedesktopCollection::onItemDeleted(const QDBusObjectPath &item) +{ + itemAttributes().updateLastModified(); + const auto itemMapPos = m_items.find(item.path()); + if (itemMapPos == m_items.end()) { + return; + } + auto *itemPtr = itemMapPos->second.get(); + + /* This can be called in the context of the item that is currently being + * deleted. Therefore we should schedule deletion on the next event loop iteration + */ + itemPtr->setDeleted(); + itemPtr->deleteLater(); + itemMapPos->second.release(); + m_items.erase(itemMapPos); + + Q_EMIT ItemDeleted(item); + + QVariantMap props; + props[QStringLiteral("Items")] = QVariant::fromValue(items()); + onPropertiesChanged(props); +} + +void KWalletFreedesktopCollection::onPropertiesChanged(const QVariantMap &properties) +{ + auto msg = QDBusMessage::createSignal(fdoObjectPath().path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged")); + auto args = QVariantList(); + args << QStringLiteral("org.freedesktop.Secret.Collection") << properties << QStringList(); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); +} + +KWalletFreedesktopAttributes &KWalletFreedesktopCollection::itemAttributes() +{ + return m_itemAttribs; +} + +const KWalletFreedesktopAttributes &KWalletFreedesktopCollection::itemAttributes() const +{ + return m_itemAttribs; +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopcollection.h b/src/runtime/kwalletd/kwalletfreedesktopcollection.h new file mode 100644 index 00000000..7f0c6c19 --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopcollection.h @@ -0,0 +1,104 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPCOLLECTION_H_ +#define _KWALLETFREEDESKTOPCOLLECTION_H_ +#include "kwalletfreedesktopattributes.h" +#include "kwalletfreedesktopservice.h" + +#define FDO_SECRETS_COLLECTION_PATH FDO_SECRETS_SERVICE_OBJECT "/collection/" +#define FDO_SECRETS_DEFAULT_DIR "Secret Service" +#define FDO_KEY_MODIFIED QStringLiteral("$fdo_modified") +#define FDO_KEY_CREATED QStringLiteral("$fdo_created") +#define FDO_KEY_MIME QStringLiteral("$fdo_mime_type") +#define FDO_KEY_XDG_SCHEMA QStringLiteral("xdg:schema") + +class KWalletFreedesktopItem; + +class KWalletFreedesktopCollection : public QObject, protected QDBusContext +{ + /* org.freedesktop.Secret.Collection properties */ +public: + Q_PROPERTY(qulonglong Created READ created) + qulonglong created() const; + + Q_PROPERTY(qulonglong Modified READ modified) + qulonglong modified() const; + + Q_PROPERTY(QList Items READ items) + QList items() const; + + Q_PROPERTY(QString Label READ label WRITE setLabel) + const QString &label() const; + void setLabel(const QString &value); + + Q_PROPERTY(bool Locked READ locked) + bool locked() const; + + Q_OBJECT + +public: + KWalletFreedesktopCollection(KWalletFreedesktopService *service, int handle, const QString &walletName, QDBusObjectPath objectPath); + + KWalletFreedesktopCollection(const KWalletFreedesktopCollection &) = delete; + KWalletFreedesktopCollection &operator=(const KWalletFreedesktopCollection &) = delete; + + KWalletFreedesktopCollection(KWalletFreedesktopCollection &&) = delete; + KWalletFreedesktopCollection &&operator=(KWalletFreedesktopCollection &&) = delete; + + EntryLocation makeUniqueEntryLocation(const QString &label); + FdoUniqueLabel makeUniqueItemLabel(const QString &label); + + QDBusObjectPath nextItemPath(); + + KWalletFreedesktopService *fdoService() const; + KWalletD *backend() const; + QDBusObjectPath fdoObjectPath() const; + const FdoUniqueLabel &uniqueLabel() const; + QString walletName() const; + int walletHandle() const; + + KWalletFreedesktopItem *getItemByObjectPath(const QString &objectPath) const; + KWalletFreedesktopItem *findItemByEntryLocation(const EntryLocation &entryLocation) const; + KWalletFreedesktopItem &pushNewItem(FdoUniqueLabel label, const QDBusObjectPath &path); + KWalletFreedesktopAttributes &itemAttributes(); + const KWalletFreedesktopAttributes &itemAttributes() const; + + /* Emitters */ + void onWalletChangeState(int handle); + void onItemCreated(const QDBusObjectPath &item); + void onItemChanged(const QDBusObjectPath &item); + void onItemDeleted(const QDBusObjectPath &item); + void onPropertiesChanged(const QVariantMap &properties); + +private: + KWalletFreedesktopItem &pushNewItem(const QString &label, const QDBusObjectPath &path); + +private: + KWalletFreedesktopService *m_service; + int m_handle; + FdoUniqueLabel m_uniqueLabel; + QDBusObjectPath m_objectPath; + KWalletFreedesktopAttributes m_itemAttribs; + std::map> m_items; + uint64_t m_itemCounter = 0; + + /* Freedesktop API */ + + /* org.freedesktop.Secret.Collection methods */ +public Q_SLOTS: + QDBusObjectPath CreateItem(const PropertiesMap &properties, const FreedesktopSecret &secret, bool replace, QDBusObjectPath &prompt); + QDBusObjectPath Delete(); + QList SearchItems(const StrStrMap &attributes); + + /* org.freedesktop.Secret.Service signals */ +Q_SIGNALS: + void ItemChanged(const QDBusObjectPath &item); + void ItemCreated(const QDBusObjectPath &item); + void ItemDeleted(const QDBusObjectPath &item); +}; + +#endif diff --git a/src/runtime/kwalletd/kwalletfreedesktopitem.cpp b/src/runtime/kwalletd/kwalletfreedesktopitem.cpp new file mode 100644 index 00000000..16d7a349 --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopitem.cpp @@ -0,0 +1,223 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopitem.h" + +#include "kwalletd.h" +#include "kwalletd_debug.h" +#include "kwalletfreedesktopcollection.h" +#include "kwalletfreedesktopitemadaptor.h" +#include "kwalletfreedesktopservice.h" + +KWalletFreedesktopItem::KWalletFreedesktopItem(KWalletFreedesktopCollection *collection, FdoUniqueLabel uniqLabel, QDBusObjectPath path) + : m_collection(collection) + , m_uniqueLabel(std::move(uniqLabel)) + , m_path(std::move(path)) +{ + (void)new KWalletFreedesktopItemAdaptor(this); + QDBusConnection::sessionBus().registerObject(fdoObjectPath().path(), this); +} + +KWalletFreedesktopItem::~KWalletFreedesktopItem() +{ + onPropertiesChanged(QVariantMap()); + + QDBusConnection::sessionBus().unregisterObject(fdoObjectPath().path()); + + if (!m_wasDeleted) { + m_collection->onItemChanged(fdoObjectPath()); + } +} + +StrStrMap KWalletFreedesktopItem::attributes() const +{ + return fdoCollection()->itemAttributes().getAttributes(m_uniqueLabel); +} + +void KWalletFreedesktopItem::setAttributes(const StrStrMap &value) +{ + fdoCollection()->itemAttributes().setAttributes(m_uniqueLabel, value); +} + +qulonglong KWalletFreedesktopItem::created() const +{ + return fdoCollection()->itemAttributes().getULongLongParam(m_uniqueLabel, FDO_KEY_CREATED, fdoCollection()->modified()); +} + +qulonglong KWalletFreedesktopItem::modified() const +{ + return fdoCollection()->itemAttributes().getULongLongParam(m_uniqueLabel, FDO_KEY_MODIFIED, fdoCollection()->modified()); +} + +QString KWalletFreedesktopItem::label() const +{ + return m_uniqueLabel.label; +} + +void KWalletFreedesktopItem::setLabel(const QString &value) +{ + const auto entryLocation = m_uniqueLabel.toEntryLocation(); + m_uniqueLabel = fdoCollection()->makeUniqueItemLabel(value); + const auto newEntryLocation = m_uniqueLabel.toEntryLocation(); + + if (newEntryLocation.folder != entryLocation.folder) { + const auto data = backend()->readEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + backend()->writeEntry(fdoCollection()->walletHandle(), newEntryLocation.folder, newEntryLocation.key, data, FDO_APPID); + backend()->removeEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + } else if (newEntryLocation.key != entryLocation.key) { + backend()->renameEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, newEntryLocation.key, FDO_APPID); + } + + fdoCollection()->itemAttributes().setParam(entryLocation, FDO_KEY_MODIFIED, static_cast(QDateTime::currentSecsSinceEpoch())); + fdoCollection()->itemAttributes().renameLabel(entryLocation, newEntryLocation); + + fdoCollection()->onItemChanged(fdoObjectPath()); +} + +bool KWalletFreedesktopItem::locked() const +{ + return m_collection->locked(); +} + +QString KWalletFreedesktopItem::type() const +{ + const auto attribs = fdoCollection()->itemAttributes().getAttributes(m_uniqueLabel); + const auto found = attribs.find(FDO_KEY_XDG_SCHEMA); + if (found != attribs.end()) { + return found.value(); + } else { + return QStringLiteral("org.freedesktop.Secret.Generic"); + } +} + +void KWalletFreedesktopItem::setType(const QString &value) +{ + auto attribs = fdoCollection()->itemAttributes().getAttributes(m_uniqueLabel); + attribs[FDO_KEY_XDG_SCHEMA] = value; + fdoCollection()->itemAttributes().setAttributes(m_uniqueLabel, attribs); +} + +QDBusObjectPath KWalletFreedesktopItem::Delete() +{ + const auto entryLocation = m_uniqueLabel.toEntryLocation(); + + backend()->removeEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + QDBusConnection::sessionBus().unregisterObject(fdoObjectPath().path()); + + m_collection->onItemDeleted(fdoObjectPath()); + + return QDBusObjectPath("/"); +} + +FreedesktopSecret KWalletFreedesktopItem::getSecret(const QDBusConnection &connection, const QDBusMessage &message, const QDBusObjectPath &session) +{ + const auto entryLocation = m_uniqueLabel.toEntryLocation(); + const auto mimeType = fdoCollection()->itemAttributes().getStringParam(entryLocation, FDO_KEY_MIME, QStringLiteral("application/octet-stream")); + + FreedesktopSecret fdoSecret; + + const auto entryType = backend()->entryType(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + if (entryType == KWallet::Wallet::Password) { + auto password = backend()->readPassword(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + auto bytes = password.toUtf8(); + fdoSecret = FreedesktopSecret(session, bytes, mimeType); + explicit_zero_mem(bytes.data(), bytes.size()); + explicit_zero_mem(password.data(), password.size() * sizeof(QChar)); + } else { + auto bytes = backend()->readEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, FDO_APPID); + fdoSecret = FreedesktopSecret(session, bytes, mimeType); + explicit_zero_mem(bytes.data(), bytes.size()); + } + + if (!fdoService()->ensecret(message, fdoSecret)) { + message.setDelayedReply(true); + connection.send(message.createErrorReply(QDBusError::ErrorType::UnknownObject, QStringLiteral("Can't find session ") + session.path())); + } + + return fdoSecret; +} + +FreedesktopSecret KWalletFreedesktopItem::GetSecret(const QDBusObjectPath &session) +{ + return getSecret(connection(), message(), session); +} + +void KWalletFreedesktopItem::SetSecret(const FreedesktopSecret &secret) +{ + const auto entryLocation = m_uniqueLabel.toEntryLocation(); + + fdoCollection()->itemAttributes().setParam(entryLocation, FDO_KEY_MIME, secret.mimeType); + fdoCollection()->itemAttributes().setParam(entryLocation, FDO_KEY_MODIFIED, static_cast(QDateTime::currentSecsSinceEpoch())); + + auto decrypted = secret; + if (!fdoService()->desecret(message(), decrypted)) { + sendErrorReply(QDBusError::ErrorType::UnknownObject, QStringLiteral("Can't find session ") + secret.session.path()); + return; + } + + QString xdgSchema = QStringLiteral("org.kde.KWallet.Stream"); + const auto attribs = fdoCollection()->itemAttributes().getAttributes(entryLocation); + const auto found = attribs.find(FDO_KEY_XDG_SCHEMA); + if (found != attribs.end()) { + xdgSchema = found.value(); + } + + if (xdgSchema == QStringLiteral("org.kde.KWallet.Password") || secret.mimeType.startsWith(QStringLiteral("text/"))) { + auto bytes = decrypted.note.toByteArray(); + auto str = QString::fromUtf8(bytes); + backend()->writePassword(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, str, FDO_APPID); + explicit_zero_mem(bytes.data(), bytes.size()); + explicit_zero_mem(str.data(), str.size() * sizeof(QChar)); + } else { + auto bytes = decrypted.note.toByteArray(); + backend()->writeEntry(fdoCollection()->walletHandle(), entryLocation.folder, entryLocation.key, bytes, KWallet::Wallet::Stream, FDO_APPID); + } +} + +KWalletFreedesktopCollection *KWalletFreedesktopItem::fdoCollection() const +{ + return m_collection; +} + +KWalletFreedesktopService *KWalletFreedesktopItem::fdoService() const +{ + return fdoCollection()->fdoService(); +} + +KWalletD *KWalletFreedesktopItem::backend() const +{ + return fdoCollection()->fdoService()->backend(); +} + +QDBusObjectPath KWalletFreedesktopItem::fdoObjectPath() const +{ + return m_path; +} + +const FdoUniqueLabel &KWalletFreedesktopItem::uniqueLabel() const +{ + return m_uniqueLabel; +} + +void KWalletFreedesktopItem::uniqueLabel(const FdoUniqueLabel &uniqueLabel) +{ + m_uniqueLabel = uniqueLabel; +} + +void KWalletFreedesktopItem::setDeleted() +{ + m_wasDeleted = true; + fdoCollection()->itemAttributes().remove(m_uniqueLabel); +} + +void KWalletFreedesktopItem::onPropertiesChanged(const QVariantMap &properties) +{ + auto msg = QDBusMessage::createSignal(fdoObjectPath().path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged")); + auto args = QVariantList(); + args << QStringLiteral("org.freedesktop.Secret.Item") << properties << QStringList(); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopitem.h b/src/runtime/kwalletd/kwalletfreedesktopitem.h new file mode 100644 index 00000000..8fd5b99d --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopitem.h @@ -0,0 +1,87 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPITEM_H_ +#define _KWALLETFREEDESKTOPITEM_H_ +#include "kwalletfreedesktopservice.h" + +static inline constexpr auto FDO_SS_MAGICK = 0x4950414f44465353ULL; + +class KWalletD; +class KWalletFreedesktopCollection; + +class KWalletFreedesktopItem : public QObject, protected FDO_DBUS_CONTEXT +{ + /* org.freedesktop.Secret.Item properties */ +public: + Q_PROPERTY(StrStrMap Attributes READ attributes WRITE setAttributes) + StrStrMap attributes() const; + void setAttributes(const StrStrMap &value); + + Q_PROPERTY(qulonglong Created READ created) + qulonglong created() const; + + Q_PROPERTY(QString Label READ label WRITE setLabel) + QString label() const; + void setLabel(const QString &value); + + Q_PROPERTY(bool Locked READ locked) + bool locked() const; + + Q_PROPERTY(qulonglong Modified READ modified) + qulonglong modified() const; + + Q_PROPERTY(QString Type READ type WRITE setType) + QString type() const; + void setType(const QString &value); + + Q_OBJECT + +public: + KWalletFreedesktopItem(KWalletFreedesktopCollection *collection, FdoUniqueLabel uniqLabel, QDBusObjectPath path); + ~KWalletFreedesktopItem(); + + KWalletFreedesktopItem(const KWalletFreedesktopItem &) = delete; + KWalletFreedesktopItem &operator=(const KWalletFreedesktopItem &) = delete; + + KWalletFreedesktopItem(KWalletFreedesktopItem &&) = delete; + KWalletFreedesktopItem &operator=(KWalletFreedesktopItem &&) = delete; + + KWalletFreedesktopCollection *fdoCollection() const; + KWalletFreedesktopService *fdoService() const; + KWalletD *backend() const; + QDBusObjectPath fdoObjectPath() const; + const FdoUniqueLabel &uniqueLabel() const; + void uniqueLabel(const FdoUniqueLabel &uniqLabel); + + /* + QVariantMap readMap() const; + void writeMap(const QVariantMap &data); + */ + + FreedesktopSecret getSecret(const QDBusConnection &connection, const QDBusMessage &message, const QDBusObjectPath &session); + void setDeleted(); + + /* Emitters */ + void onPropertiesChanged(const QVariantMap &properties); + void enableFdoFormat(); + +private: + KWalletFreedesktopCollection *m_collection; + FdoUniqueLabel m_uniqueLabel; + QDBusObjectPath m_path; + bool m_wasDeleted = false; + + /* Freedesktop API */ + + /* org.freedesktop.Secret.Item methods */ +public Q_SLOTS: + QDBusObjectPath Delete(); + FreedesktopSecret GetSecret(const QDBusObjectPath &session); + void SetSecret(const FreedesktopSecret &secret); +}; + +#endif diff --git a/src/runtime/kwalletd/kwalletfreedesktopprompt.cpp b/src/runtime/kwalletd/kwalletfreedesktopprompt.cpp new file mode 100644 index 00000000..0a5774f4 --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopprompt.cpp @@ -0,0 +1,132 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopprompt.h" + +#include "kwalletd.h" +#include "kwalletfreedesktopcollection.h" +#include "kwalletfreedesktoppromptadaptor.h" +#include "kwalletfreedesktopservice.h" + +KWalletFreedesktopPrompt::KWalletFreedesktopPrompt(KWalletFreedesktopService *service, QDBusObjectPath objectPath, PromptType type, QString responseBusName) + : QObject(nullptr) + , m_service(service) + , m_objectPath(std::move(objectPath)) + , m_type(type) + , m_responseBusName(std::move(responseBusName)) +{ + (void)new KWalletFreedesktopPromptAdaptor(this); +} + +KWalletFreedesktopService *KWalletFreedesktopPrompt::fdoService() const +{ + return m_service; +} + +KWalletD *KWalletFreedesktopPrompt::backend() const +{ + return fdoService()->backend(); +} + +QDBusObjectPath KWalletFreedesktopPrompt::fdoObjectPath() const +{ + return m_objectPath; +} + +void KWalletFreedesktopPrompt::Dismiss() +{ + auto msg = QDBusMessage::createTargetedSignal(m_responseBusName, + fdoObjectPath().path(), + QStringLiteral("org.freedesktop.Secret.Prompt"), + QStringLiteral("Completed")); + QVariantList args; + args << true << QVariant::fromValue(QDBusVariant(QVariant::fromValue(QList()))); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); + QDBusConnection::sessionBus().unregisterObject(fdoObjectPath().path()); +} + +void KWalletFreedesktopPrompt::Prompt(const QString &window_id) +{ + if (m_type != PromptType::Open && m_type != PromptType::Create) { + return; + } + + const int wId = window_id.toInt(); + for (auto properties : std::as_const(m_propertiesList)) { + /* When type is "PromptType::Open" the properties.label actually stores + * the wallet name + */ + QString walletName = properties.collectionLabel; + + if (m_type == PromptType::Create) { + walletName = fdoService()->makeUniqueWalletName(properties.collectionLabel); + properties.collectionLabel = walletName; + } + + if (!properties.alias.isEmpty()) { + fdoService()->createCollectionAlias(properties.alias, walletName); + } + + const int tId = backend()->openAsync(walletName, wId, FDO_APPID, false, connection(), message()); + m_transactionIds.insert(tId); + m_transactionIdToCollectionProperties.emplace(tId, std::move(properties)); + } +} + +void KWalletFreedesktopPrompt::walletAsyncOpened(int transactionId, int walletHandle) +{ + const auto found = m_transactionIds.find(transactionId); + + if (found != m_transactionIds.end()) { + const auto propertiesPos = m_transactionIdToCollectionProperties.find(transactionId); + if (walletHandle < 0 || propertiesPos == m_transactionIdToCollectionProperties.end()) { + Dismiss(); + fdoService()->deletePrompt(fdoObjectPath().path()); + return; + } + m_transactionIds.remove(transactionId); + + const QString &walletName = propertiesPos->second.collectionLabel; + const auto collectionPath = fdoService()->promptUnlockCollection(walletName, walletHandle); + m_result.push_back(propertiesPos->second.objectPath.path() == QStringLiteral("/") ? collectionPath : propertiesPos->second.objectPath); + } + + if (m_transactionIds.empty()) { + /* At this point there is no remaining transactions, so we able to complete prompt */ + + auto msg = QDBusMessage::createTargetedSignal(m_responseBusName, + fdoObjectPath().path(), + QStringLiteral("org.freedesktop.Secret.Prompt"), + QStringLiteral("Completed")); + QVariantList args; + args << false; + + if (m_type == PromptType::Create && m_result.size() < 2) { + /* Single object in dbus variant */ + args << QVariant::fromValue(QDBusVariant(QVariant::fromValue(m_result.empty() ? QDBusObjectPath("/") : m_result.front()))); + } else { + /* Object array in dbus variant */ + args << QVariant::fromValue(QDBusVariant(QVariant::fromValue(m_result))); + } + + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); + + fdoService()->deletePrompt(fdoObjectPath().path()); + } +} + +void KWalletFreedesktopPrompt::subscribeForWalletAsyncOpened() +{ + connect(backend(), &KWalletD::walletAsyncOpened, this, &KWalletFreedesktopPrompt::walletAsyncOpened); + QDBusConnection::sessionBus().registerObject(fdoObjectPath().path(), this); +} + +void KWalletFreedesktopPrompt::appendProperties(const QString &label, const QDBusObjectPath &objectPath, const QString &alias) +{ + m_propertiesList.push_back(CollectionProperties{label, objectPath, alias}); +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopprompt.h b/src/runtime/kwalletd/kwalletfreedesktopprompt.h new file mode 100644 index 00000000..a87ee323 --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopprompt.h @@ -0,0 +1,73 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPPROMPT_H_ +#define _KWALLETFREEDESKTOPPROMPT_H_ + +#include "kwalletfreedesktopservice.h" + +#define FDO_SECRET_SERVICE_PROMPT_PATH FDO_SECRETS_SERVICE_OBJECT "/prompt/" + +class KWalletD; + +enum class PromptType { + Open, + Create, +}; + +class KWalletFreedesktopPrompt : public QObject, protected FDO_DBUS_CONTEXT +{ + Q_OBJECT + + struct CollectionProperties { + QString collectionLabel; + QDBusObjectPath objectPath; + QString alias; + }; + +public: + KWalletFreedesktopPrompt(KWalletFreedesktopService *service, QDBusObjectPath objectPath, PromptType type, QString responseBusName); + + KWalletFreedesktopPrompt(const KWalletFreedesktopPrompt &) = delete; + KWalletFreedesktopPrompt &operator=(const KWalletFreedesktopPrompt &) = delete; + + KWalletFreedesktopPrompt(KWalletFreedesktopPrompt &&) = delete; + KWalletFreedesktopPrompt &operator=(KWalletFreedesktopPrompt &&) = delete; + + KWalletFreedesktopService *fdoService() const; + KWalletD *backend() const; + QDBusObjectPath fdoObjectPath() const; + + void subscribeForWalletAsyncOpened(); + void appendProperties(const QString &label, const QDBusObjectPath &objectPath = QDBusObjectPath("/"), const QString &alias = {}); + +public Q_SLOTS: + void walletAsyncOpened(int transactionId, int walletHandle); + +private: + KWalletFreedesktopService *m_service; + QDBusObjectPath m_objectPath; + PromptType m_type; + QSet m_transactionIds; + QList m_result; + QList m_propertiesList; + std::map m_transactionIdToCollectionProperties; + QString m_responseBusName; + + /* Freedesktop API */ + + /* org.freedesktop.Secret.Prompt methods */ +public Q_SLOTS: + void Dismiss(); + void Prompt(const QString &window_id); + + /* org.freedesktop.Secret.Prompt signals */ +Q_SIGNALS: + /* Emitted manually now */ + void Completed(bool dismissed, const QDBusVariant &result); +}; + +#endif diff --git a/src/runtime/kwalletd/kwalletfreedesktopservice.cpp b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp new file mode 100644 index 00000000..c6b4821d --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp @@ -0,0 +1,956 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopservice.h" + +#include "kwalletd.h" +#include "kwalletfreedesktopcollection.h" +#include "kwalletfreedesktopitem.h" +#include "kwalletfreedesktopprompt.h" +#include "kwalletfreedesktopserviceadaptor.h" +#include "kwalletfreedesktopsession.h" +#include +#include +#include +#include + +#ifdef Q_OS_WIN +#include +#endif + +[[maybe_unused]] int DBUS_SECRET_SERVICE_META_TYPE_REGISTER = []() { +#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0)) + qRegisterMetaTypeStreamOperators("StrStrMap"); + qRegisterMetaTypeStreamOperators>("QMap"); + qRegisterMetaTypeStreamOperators("QCA::SecureArray"); +#endif + + qDBusRegisterMetaType(); + qDBusRegisterMetaType>(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + qDBusRegisterMetaType(); + + return 0; +}(); + +namespace +{ +QString mangleInvalidObjectPathChars(const QString &str) +{ + const auto utf8Str = str.toUtf8(); + static constexpr char hex[] = "0123456789abcdef"; + static_assert(sizeof(hex) == 17); + + QString mangled; + mangled.reserve(utf8Str.size()); + + for (const auto &c : utf8Str) { + if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') && (c < '0' || c > '9') && c != '_') { + const auto cp = static_cast(c); + mangled.push_back(QChar::fromLatin1('_')); + mangled.push_back(QChar::fromLatin1(hex[cp >> 4])); + mangled.push_back(QChar::fromLatin1(hex[cp & 0x0f])); + } else { + mangled.push_back(QChar::fromLatin1(c)); + } + } + + return mangled; +} +} + +#define LABEL_NUMBER_PREFIX "__" +#define LABEL_NUMBER_POSTFIX "_" +#define LABEL_NUMBER_REGEX "(^.*)" LABEL_NUMBER_PREFIX "(\\d+)" LABEL_NUMBER_POSTFIX "$" + +EntryLocation EntryLocation::fromUniqueLabel(const FdoUniqueLabel &uniqLabel) +{ + QString dir; + QString name = uniqLabel.label; + + const int slashPos = uniqLabel.label.lastIndexOf(QChar::fromLatin1('/')); + if (slashPos == -1) { + dir = QStringLiteral(FDO_SECRETS_DEFAULT_DIR); + } else { + dir = uniqLabel.label.left(slashPos); + name = uniqLabel.label.right((uniqLabel.label.size() - dir.size()) - 1); + } + + return EntryLocation{dir, FdoUniqueLabel::makeName(name, uniqLabel.copyId)}; +} + +FdoUniqueLabel EntryLocation::toUniqueLabel() const +{ + return FdoUniqueLabel::fromEntryLocation(*this); +} + +FdoUniqueLabel FdoUniqueLabel::fromEntryLocation(const EntryLocation &entryLocation) +{ + const auto uniqLabel = FdoUniqueLabel::fromName(entryLocation.key); + + if (entryLocation.folder == QStringLiteral(FDO_SECRETS_DEFAULT_DIR)) { + return uniqLabel; + } else { + return {entryLocation.folder + QChar::fromLatin1('/') + uniqLabel.label, uniqLabel.copyId}; + } +} + +FdoUniqueLabel FdoUniqueLabel::fromName(const QString &name) +{ + static QRegularExpression regexp(QStringLiteral(LABEL_NUMBER_REGEX)); + + const auto match = regexp.match(name); + if (match.hasMatch()) { + const QString strNum = match.captured(2); + bool ok = false; + const int n = strNum.toInt(&ok); + if (ok) { + return FdoUniqueLabel{match.captured(1), n}; + } + } + return FdoUniqueLabel{name}; +} + +QString FdoUniqueLabel::makeName(const QString &label, int n) +{ + if (n == -1) { + return label; + } else { + return label + QStringLiteral(LABEL_NUMBER_PREFIX) + QString::number(n) + QStringLiteral(LABEL_NUMBER_POSTFIX); + } +} + +QString FdoUniqueLabel::toName() const +{ + return makeName(label, copyId); +} + +EntryLocation FdoUniqueLabel::toEntryLocation() const +{ + return EntryLocation::fromUniqueLabel(*this); +} + +QString KWalletFreedesktopService::wrapToCollectionPath(const QString &itemPath) +{ + /* Take only /org/freedesktop/secrets/collection/collection_name */ + return itemPath.section(QChar::fromLatin1('/'), 0, 5); +} + +KWalletFreedesktopService::KWalletFreedesktopService(KWalletD *parent) + : QObject(nullptr) + , m_parent(parent) + , m_kwalletrc(QStringLiteral("kwalletrc")) +{ + (void)new KWalletFreedesktopServiceAdaptor(this); + + /* register */ + QDBusConnection::sessionBus().registerService(QStringLiteral("org.freedesktop.secrets")); + QDBusConnection::sessionBus().registerObject(QStringLiteral(FDO_SECRETS_SERVICE_OBJECT), this); + + const KConfigGroup walletGroup(&m_kwalletrc, "Wallet"); + if (!parent || !walletGroup.readEntry("Enabled", true)) { + return; + } + + connect(m_parent, static_cast(&KWalletD::walletClosed), this, &KWalletFreedesktopService::lockCollection); + connect(m_parent, &KWalletD::entryUpdated, this, &KWalletFreedesktopService::entryUpdated); + connect(m_parent, &KWalletD::entryDeleted, this, &KWalletFreedesktopService::entryDeleted); + connect(m_parent, &KWalletD::entryRenamed, this, &KWalletFreedesktopService::entryRenamed); + connect(m_parent, &KWalletD::walletDeleted, this, &KWalletFreedesktopService::walletDeleted); + connect(m_parent, &KWalletD::walletCreated, this, &KWalletFreedesktopService::walletCreated); + + const auto walletNames = backend()->wallets(); + + /* Build collections */ + for (const QString &walletName : walletNames) { + const auto objectPath = makeUniqueObjectPath(walletName); + auto collection = std::make_unique(this, -1, walletName, objectPath); + + m_collections.emplace(objectPath.path(), std::move(collection)); + } +} + +KWalletFreedesktopService::~KWalletFreedesktopService() = default; + +QList KWalletFreedesktopService::collections() const +{ + QList result; + result.reserve(m_collections.size()); + + for (const auto &collectionPair : m_collections) { + result.push_back(QDBusObjectPath(collectionPair.first)); + } + + return result; +} + +QDBusObjectPath KWalletFreedesktopService::CreateCollection(const QVariantMap &properties, const QString &alias, QDBusObjectPath &prompt) +{ + prompt.setPath(QStringLiteral("/")); + + const auto labelIter = properties.find(QStringLiteral("org.freedesktop.Secret.Collection.Label")); + if (labelIter == properties.end()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Collection.Label property is missing")); + return QDBusObjectPath("/"); + } + if (!labelIter->canConvert()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Type of Collection.Label property is invalid")); + return QDBusObjectPath("/"); + } + + prompt = nextPromptPath(); + auto fdoPromptPtr = std::make_unique(this, prompt, PromptType::Create, message().service()); + auto &fdoPrompt = *m_prompts.emplace(prompt.path(), std::move(fdoPromptPtr)).first->second; + + fdoPrompt.appendProperties(labelIter->toString(), QDBusObjectPath("/"), alias); + fdoPrompt.subscribeForWalletAsyncOpened(); + + return QDBusObjectPath("/"); +} + +FreedesktopSecretMap KWalletFreedesktopService::GetSecrets(const QList &items, const QDBusObjectPath &session) +{ + FreedesktopSecretMap result; + + for (const QDBusObjectPath &itemPath : items) { + const auto item = getItemByObjectPath(itemPath); + + if (item) { + result.insert(itemPath, item->getSecret(connection(), message(), session)); + } else { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Can't find item at path ") + itemPath.path()); + break; + } + } + + return result; +} + +QList KWalletFreedesktopService::Lock(const QList &objects, QDBusObjectPath &prompt) +{ + prompt = QDBusObjectPath("/"); + QList result; + + /* Try find in active collections */ + for (const QDBusObjectPath &object : objects) { + const QString collectionPath = wrapToCollectionPath(resolveIfAlias(object.path())); + + const auto foundCollection = m_collections.find(collectionPath); + if (foundCollection != m_collections.end()) { + const int walletHandle = foundCollection->second->walletHandle(); + const int rc = m_parent->close(walletHandle, true, FDO_APPID, message()); + + if (rc == 0) { + result.push_back(QDBusObjectPath(collectionPath)); + } else { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Can't lock object at path ") + collectionPath); + } + } else { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Collection at path ") + collectionPath + QStringLiteral(" does not exist")); + } + } + + return result; +} + +QDBusVariant KWalletFreedesktopService::OpenSession(const QString &algorithm, const QDBusVariant &input, QDBusObjectPath &result) +{ + if (algorithm != QStringLiteral("dh-ietf1024-sha256-aes128-cbc-pkcs7")) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, + QStringLiteral("Algorithm ") + algorithm + QStringLiteral(" is not supported. (only dh-ietf1024-sha256-aes128-cbc-pkcs7 is supported)")); + return {}; + } + + if (!input.variant().canConvert()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Second input argument must be a byte array.")); + return {}; + } + + const auto sessionPath = createSession(input.variant().toByteArray()); + result.setPath(sessionPath); + + if (sessionPath != QStringLiteral("/")) { + return QDBusVariant(QVariant(m_sessions[sessionPath]->publicKey().toDH().y().toArray().toByteArray())); + } else { + return QDBusVariant(QVariant(QByteArray())); + } +} + +QDBusObjectPath KWalletFreedesktopService::ReadAlias(const QString &name) +{ + QString walletName; + + m_kwalletrc.reparseConfiguration(); + if (name == QStringLiteral("default")) { + KConfigGroup cfg(&m_kwalletrc, "Wallet"); + walletName = defaultWalletName(cfg); + + } else { + KConfigGroup cfg(&m_kwalletrc, "org.freedesktop.secrets.aliases"); + walletName = cfg.readEntry(name, QString()); + } + + if (!walletName.isEmpty()) { + const auto *collection = getCollectionByWalletName(walletName); + if (collection) { + return collection->fdoObjectPath(); + } + } + + return QDBusObjectPath("/"); +} + +QList KWalletFreedesktopService::SearchItems(const StrStrMap &attributes, QList &locked) +{ + QList unlocked; + + for (const auto &collectionPair : m_collections) { + auto &collection = *collectionPair.second; + + if (collection.locked()) { + locked += collection.SearchItems(attributes); + } else { + unlocked += collection.SearchItems(attributes); + } + } + + return unlocked; +} + +void KWalletFreedesktopService::SetAlias(const QString &name, const QDBusObjectPath &collectionPath) +{ + const auto foundCollection = m_collections.find(collectionPath.path()); + if (foundCollection == m_collections.end()) { + return; + } + + auto *collection = foundCollection->second.get(); + createCollectionAlias(name, collection); +} + +QString KWalletFreedesktopService::resolveIfAlias(QString alias) +{ + if (alias.startsWith(QStringLiteral(FDO_ALIAS_PATH))) { + const auto path = ReadAlias(alias.remove(0, QStringLiteral(FDO_ALIAS_PATH).size())).path(); + if (path != QStringLiteral("/")) { + alias = path; + } else { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Alias ") + alias + QStringLiteral(" does not exist")); + return {}; + } + } + + if (!alias.startsWith(QStringLiteral(FDO_SECRETS_COLLECTION_PATH))) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Collection object path is invalid")); + return {}; + } + + return alias; +} + +struct UnlockedObject { + QString walletName; + QDBusObjectPath objectPath; +}; + +QList KWalletFreedesktopService::Unlock(const QList &objects, QDBusObjectPath &prompt) +{ + prompt = QDBusObjectPath("/"); + + QList result; + QList needUnlock; + + /* Try find in active collections */ + for (const QDBusObjectPath &object : objects) { + const QString strPath = object.path(); + const QString collectionPath = wrapToCollectionPath(resolveIfAlias(strPath)); + + const auto foundCollection = m_collections.find(collectionPath); + if (foundCollection != m_collections.end()) { + if (foundCollection->second->locked()) { + needUnlock.push_back({foundCollection->second->walletName(), QDBusObjectPath(strPath)}); + } else { + result.push_back(QDBusObjectPath(strPath)); + } + } else { + sendErrorReply(QDBusError::ErrorType::InvalidObjectPath, QStringLiteral("Object ") + strPath + QStringLiteral(" does not exist")); + return {}; + } + } + + if (!needUnlock.empty()) { + const auto promptPath = nextPromptPath(); + auto fdoPromptPtr = std::make_unique(this, promptPath, PromptType::Open, message().service()); + auto &fdoPrompt = *m_prompts.emplace(promptPath.path(), std::move(fdoPromptPtr)).first->second; + + prompt = QDBusObjectPath(promptPath); + + for (const auto &[walletName, objectPath] : std::as_const(needUnlock)) { + fdoPrompt.appendProperties(walletName, objectPath); + } + + fdoPrompt.subscribeForWalletAsyncOpened(); + } + return result; +} + +QString KWalletFreedesktopService::createSession(const QByteArray &clientKey) +{ + if (clientKey.size() < FDO_DH_PUBLIC_KEY_SIZE) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("Client public key size is invalid")); + return QStringLiteral("/"); + } + + QCA::KeyGenerator keygen; + const auto dlGroup = QCA::DLGroup(keygen.createDLGroup(QCA::IETF_1024)); + if (dlGroup.isNull()) { + sendErrorReply(QDBusError::ErrorType::InvalidArgs, QStringLiteral("createDLGroup failed: maybe libqca-ossl is missing")); + return QStringLiteral("/"); + } + + auto privateKey = QCA::PrivateKey(keygen.createDH(dlGroup)); + const auto publicKey = QCA::PublicKey(privateKey); + const auto clientPublicKey = QCA::DHPublicKey(dlGroup, QCA::BigInteger(QCA::SecureArray(clientKey))); + const auto commonSecret = privateKey.deriveKey(clientPublicKey); + const auto symmetricKey = QCA::HKDF().makeKey(commonSecret, {}, {}, FDO_SECRETS_CIPHER_KEY_SIZE); + const QString sessionPath = QStringLiteral(FDO_SECRETS_SESSION_PATH) + QString::number(++m_session_counter); + + auto session = std::make_unique(this, publicKey, symmetricKey, sessionPath, connection(), message()); + m_sessions[sessionPath] = std::move(session); + return sessionPath; +} + +QString KWalletFreedesktopService::defaultWalletName(KConfigGroup &cfg) +{ + auto walletName = cfg.readEntry("Default Wallet", "kdewallet"); + if (walletName.isEmpty()) { + walletName = QStringLiteral("kdewallet"); + } + return walletName; +} + +QDBusObjectPath KWalletFreedesktopService::promptUnlockCollection(const QString &walletName, int handle) +{ + auto *collection = getCollectionByWalletName(walletName); + QString objectPath; + + if (collection) { + collection->onWalletChangeState(handle); + onCollectionChanged(collection->fdoObjectPath()); + objectPath = collection->fdoObjectPath().path(); + } else { + const auto path = makeUniqueObjectPath(walletName); + objectPath = path.path(); + auto newCollection = std::make_unique(this, handle, walletName, path); + m_collections[objectPath] = std::move(newCollection); + onCollectionCreated(path); + } + + return QDBusObjectPath(objectPath); +} + +/* Triggered after KWalletD::walletClosed signal */ +void KWalletFreedesktopService::lockCollection(const QString &name) +{ + auto *collection = getCollectionByWalletName(name); + if (collection) { + collection->onWalletChangeState(-1); + onCollectionChanged(collection->fdoObjectPath()); + } +} + +/* Triggered after KWalletD::entryUpdated signal */ +void KWalletFreedesktopService::entryUpdated(const QString &walletName, const QString &folder, const QString &entryName) +{ + auto *collection = getCollectionByWalletName(walletName); + if (!collection) { + return; + } + + const EntryLocation entryLocation{folder, entryName}; + const auto *item = collection->findItemByEntryLocation(entryLocation); + if (item) { + collection->onItemChanged(item->fdoObjectPath()); + } else { + auto objectPath = collection->nextItemPath(); + collection->pushNewItem(entryLocation.toUniqueLabel(), objectPath); + collection->onItemCreated(objectPath); + } +} + +/* Triggered after KWalletD::entryDeleted signal */ +void KWalletFreedesktopService::entryDeleted(const QString &walletName, const QString &folder, const QString &entryName) +{ + auto *collection = getCollectionByWalletName(walletName); + if (!collection) { + return; + } + + const auto *item = collection->findItemByEntryLocation({folder, entryName}); + if (item) { + collection->onItemDeleted(item->fdoObjectPath()); + } +} + +/* Triggered after KWalletD::entryRenamed signal */ +void KWalletFreedesktopService::entryRenamed(const QString &walletName, const QString &folder, const QString &oldName, const QString &newName) +{ + auto *collection = getCollectionByWalletName(walletName); + if (!collection) { + return; + } + + const EntryLocation oldLocation{folder, oldName}; + const EntryLocation newLocation{folder, newName}; + + auto *item = collection->findItemByEntryLocation(oldLocation); + if (!item) { + item = collection->findItemByEntryLocation(newLocation); + } + + if (item) { + collection->itemAttributes().renameLabel(oldLocation, newLocation); + item->uniqueLabel(newLocation.toUniqueLabel()); + collection->onItemChanged(item->fdoObjectPath()); + } +} + +/* Triggered after KWalletD::walletDeleted signal */ +void KWalletFreedesktopService::walletDeleted(const QString &walletName) +{ + auto *collection = getCollectionByWalletName(walletName); + if (collection) { + collection->Delete(); + } +} + +/* Triggered after KWalletD::walletCreated signal */ +void KWalletFreedesktopService::walletCreated(const QString &walletName) +{ + const auto objectPath = makeUniqueObjectPath(walletName); + auto collection = std::make_unique(this, -1, walletName, objectPath); + m_collections.emplace(objectPath.path(), std::move(collection)); + onCollectionCreated(objectPath); +} + +bool KWalletFreedesktopService::desecret(const QDBusMessage &message, FreedesktopSecret &secret) +{ + const auto foundSession = m_sessions.find(secret.session.path()); + + if (foundSession != m_sessions.end()) { + const KWalletFreedesktopSession &session = *foundSession->second; + auto decrypted = session.decrypt(message, secret.note, secret.initVector); + + if (decrypted.ok) { + secret.note = std::move(decrypted.bytes); + return true; + } + } + + return false; +} + +bool KWalletFreedesktopService::ensecret(const QDBusMessage &message, FreedesktopSecret &secret) +{ + const auto foundSession = m_sessions.find(secret.session.path()); + + if (foundSession != m_sessions.end()) { + const KWalletFreedesktopSession &session = *foundSession->second; + auto encrypted = session.encrypt(message, secret.note, secret.initVector); + + if (encrypted.ok) { + secret.note = std::move(encrypted.bytes); + return true; + } + } + + return false; +} + +QDBusObjectPath KWalletFreedesktopService::nextPromptPath() +{ + static uint64_t id = 0; + return QDBusObjectPath(QStringLiteral(FDO_SECRET_SERVICE_PROMPT_PATH) + QStringLiteral("p") + QString::number(id++)); +} + +QDBusArgument &operator<<(QDBusArgument &arg, const FreedesktopSecret &secret) +{ + arg.beginStructure(); + arg << secret.session; + arg << secret.initVector; + arg << secret.note; + arg << secret.mimeType; + arg.endStructure(); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, FreedesktopSecret &secret) +{ + arg.beginStructure(); + arg >> secret.session; + arg >> secret.initVector; + arg >> secret.note; + arg >> secret.mimeType; + arg.endStructure(); + return arg; +} + +QDataStream &operator<<(QDataStream &stream, const QCA::SecureArray &value) +{ + QByteArray bytes = value.toByteArray(); + stream << bytes; + explicit_zero_mem(bytes.data(), bytes.size()); + return stream; +} + +QDataStream &operator>>(QDataStream &stream, QCA::SecureArray &value) +{ + QByteArray bytes; + stream >> bytes; + value = QCA::SecureArray(bytes); + explicit_zero_mem(bytes.data(), bytes.size()); + return stream; +} + +QDBusArgument &operator<<(QDBusArgument &arg, const QCA::SecureArray &value) +{ + QByteArray bytes = value.toByteArray(); + arg << bytes; + explicit_zero_mem(bytes.data(), bytes.size()); + return arg; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, QCA::SecureArray &buf) +{ + QByteArray byteArray; + arg >> byteArray; + buf = QCA::SecureArray(byteArray); + explicit_zero_mem(byteArray.data(), byteArray.size()); + return arg; +} + +KWalletD *KWalletFreedesktopService::backend() const +{ + return m_parent; +} + +QDBusObjectPath KWalletFreedesktopService::fdoObjectPath() const +{ + return QDBusObjectPath(FDO_SECRETS_SERVICE_OBJECT); +} + +KWalletFreedesktopItem *KWalletFreedesktopService::getItemByObjectPath(const QDBusObjectPath &path) const +{ + const auto str = path.path(); + if (!str.startsWith(QStringLiteral(FDO_SECRETS_COLLECTION_PATH))) { + return nullptr; + } + + const QString collectionPath = wrapToCollectionPath(str); + const auto collectionPos = m_collections.find(collectionPath); + if (collectionPos == m_collections.end()) { + return nullptr; + } + + const auto &collection = collectionPos->second; + return collection->getItemByObjectPath(str); +} + +KWalletFreedesktopPrompt *KWalletFreedesktopService::getPromptByObjectPath(const QDBusObjectPath &path) const +{ + const auto foundPrompt = m_prompts.find(path.path()); + if (foundPrompt != m_prompts.end()) { + return foundPrompt->second.get(); + } else { + return nullptr; + } +} + +FdoUniqueLabel KWalletFreedesktopService::makeUniqueCollectionLabel(const QString &label) +{ + int n = -1; + auto walletName = label; + const QStringList wallets = backend()->wallets(); + + while (wallets.contains(walletName)) { + walletName = FdoUniqueLabel::makeName(label, ++n); + } + + return {label, n}; +} + +QString KWalletFreedesktopService::makeUniqueWalletName(const QString &labelPrefix) +{ + return makeUniqueCollectionLabel(labelPrefix).toName(); +} + +QDBusObjectPath KWalletFreedesktopService::makeUniqueObjectPath(const QString &walletName) const +{ + auto mangled = mangleInvalidObjectPathChars(walletName); + mangled.insert(0, QStringLiteral(FDO_SECRETS_COLLECTION_PATH)); + + QString result = mangled; + int postfix = 0; + while (m_collections.count(result)) { + result = mangled + QString::number(postfix++); + } + + return QDBusObjectPath(result); +} + +QStringList KWalletFreedesktopService::readAliasesFor(const QString &walletName) +{ + m_kwalletrc.reparseConfiguration(); + KConfigGroup cfg(&m_kwalletrc, "org.freedesktop.secrets.aliases"); + const auto map = cfg.entryMap(); + QStringList aliases; + + for (auto i = map.begin(); i != map.end(); ++i) { + if (i.value() == walletName) { + aliases.push_back(i.key()); + } + } + + KConfigGroup cfgWallet(&m_kwalletrc, "Wallet"); + if (defaultWalletName(cfgWallet) == walletName) { + aliases.push_back(QStringLiteral("default")); + } + + return aliases; +} + +void KWalletFreedesktopService::updateCollectionAlias(const QString &alias, const QString &walletName) +{ + QString sectName = QStringLiteral("org.freedesktop.secrets.aliases"); + QString sectKey = alias; + + if (alias == QStringLiteral("default")) { + sectName = QStringLiteral("Wallet"); + sectKey = QStringLiteral("Default Wallet"); + } + + KConfigGroup cfg(&m_kwalletrc, sectName); + cfg.writeEntry(sectKey, walletName); + m_kwalletrc.sync(); +} + +void KWalletFreedesktopService::createCollectionAlias(const QString &alias, const QString &walletName) +{ + QString sectName = QStringLiteral("org.freedesktop.secrets.aliases"); + QString sectKey = alias; + + if (alias == QStringLiteral("default")) { + sectName = QStringLiteral("Wallet"); + sectKey = QStringLiteral("Default Wallet"); + } + + m_kwalletrc.reparseConfiguration(); + KConfigGroup cfg(&m_kwalletrc, sectName); + + const QString prevWalletName = cfg.readEntry(sectKey, QString()); + if (!prevWalletName.isEmpty()) { + const auto *prevCollection = getCollectionByWalletName(prevWalletName); + if (prevCollection) { + QDBusConnection::sessionBus().unregisterObject(QStringLiteral(FDO_ALIAS_PATH) + alias); + } + } + + cfg.writeEntry(sectKey, walletName); + m_kwalletrc.sync(); + + auto *collection = getCollectionByWalletName(walletName); + if (collection) { + QDBusConnection::sessionBus().registerObject(QStringLiteral(FDO_ALIAS_PATH) + alias, collection); + } +} + +void KWalletFreedesktopService::createCollectionAlias(const QString &alias, KWalletFreedesktopCollection *collection) +{ + QString sectName = QStringLiteral("org.freedesktop.secrets.aliases"); + QString sectKey = alias; + + if (alias == QStringLiteral("default")) { + sectName = QStringLiteral("Wallet"); + sectKey = QStringLiteral("Default Wallet"); + } + + m_kwalletrc.reparseConfiguration(); + KConfigGroup cfg(&m_kwalletrc, sectName); + + const QString prevWalletName = cfg.readEntry(sectKey, ""); + if (!prevWalletName.isEmpty()) { + const auto *prevCollection = getCollectionByWalletName(prevWalletName); + if (prevCollection) { + QDBusConnection::sessionBus().unregisterObject(QStringLiteral(FDO_ALIAS_PATH) + alias); + } + } + + cfg.writeEntry(sectKey, collection->walletName()); + m_kwalletrc.sync(); + QDBusConnection::sessionBus().registerObject(QStringLiteral(FDO_ALIAS_PATH) + alias, collection); +} + +void KWalletFreedesktopService::removeAlias(const QString &alias) +{ + if (alias == QStringLiteral("default")) { + return; + } + + KConfigGroup cfg(&m_kwalletrc, "org.freedesktop.secrets.aliases"); + cfg.deleteEntry(alias); + m_kwalletrc.sync(); + QDBusConnection::sessionBus().unregisterObject(QStringLiteral(FDO_ALIAS_PATH) + alias); +} + +KWalletFreedesktopCollection *KWalletFreedesktopService::getCollectionByWalletName(const QString &walletName) const +{ + for (const auto &collectionKeyValue : m_collections) { + const auto collection = collectionKeyValue.second.get(); + if (collection->walletName() == walletName) { + return collection; + } + } + + return nullptr; +} + +void KWalletFreedesktopService::deletePrompt(const QString &objectPath) +{ + const auto foundPrompt = m_prompts.find(objectPath); + if (foundPrompt == m_prompts.end()) { + return; + } + + /* This can be called in the context of the prompt that is currently being + * deleted. Therefore, we should schedule deletion on the next event loop iteration + */ + foundPrompt->second->deleteLater(); + foundPrompt->second.release(); + m_prompts.erase(foundPrompt); +} + +void KWalletFreedesktopService::deleteSession(const QString &objectPath) +{ + const auto foundSession = m_sessions.find(objectPath); + if (foundSession == m_sessions.end()) { + return; + } + + /* This can be called in the context of the session that is currently being + * deleted. Therefore, we should schedule deletion on the next event loop iteration + */ + foundSession->second->deleteLater(); + foundSession->second.release(); + m_sessions.erase(foundSession); +} + +void KWalletFreedesktopService::onCollectionCreated(const QDBusObjectPath &path) +{ + Q_EMIT CollectionCreated(path); + + QVariantMap props; + props.insert(QStringLiteral("Collections"), QVariant::fromValue(collections())); + onPropertiesChanged(props); +} + +void KWalletFreedesktopService::onCollectionChanged(const QDBusObjectPath &path) +{ + Q_EMIT CollectionChanged(path); +} + +void KWalletFreedesktopService::onCollectionDeleted(const QDBusObjectPath &path) +{ + const auto collectionMapPos = m_collections.find(path.path()); + if (collectionMapPos == m_collections.end()) { + return; + } + auto &collectionPair = *collectionMapPos; + collectionPair.second->itemAttributes().deleteFile(); + + /* This can be called in the context of the collection that is currently being + * deleted. Therefore, we should schedule deletion on the next event loop iteration + */ + collectionPair.second->deleteLater(); + collectionPair.second.release(); + m_collections.erase(collectionMapPos); + + Q_EMIT CollectionDeleted(path); + + QVariantMap props; + props[QStringLiteral("Collections")] = QVariant::fromValue(collections()); + onPropertiesChanged(props); +} + +void KWalletFreedesktopService::onPropertiesChanged(const QVariantMap &properties) +{ + auto msg = QDBusMessage::createSignal(fdoObjectPath().path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("PropertiesChanged")); + auto args = QVariantList(); + args << QStringLiteral("org.freedesktop.Secret.Service") << properties << QStringList(); + msg.setArguments(args); + QDBusConnection::sessionBus().send(msg); +} + +QDataStream &operator<<(QDataStream &stream, const QDBusObjectPath &value) +{ + return stream << value.path(); +} + +QDataStream &operator>>(QDataStream &stream, QDBusObjectPath &value) +{ + QString str; + stream >> str; + value = QDBusObjectPath(str); + return stream; +} + +const QDBusArgument &operator>>(const QDBusArgument &arg, PropertiesMap &value) +{ + arg.beginMap(); + value.map.clear(); + + while (!arg.atEnd()) { + arg.beginMapEntry(); + QString key; + QVariant val; + arg >> key >> val; + + /* For org.freedesktop.Secret.Item.Attributes */ + if (val.canConvert()) { + auto metaArg = val.value(); + StrStrMap metaMap; + metaArg >> metaMap; + val = QVariant::fromValue(metaMap); + } + value.map.insert(key, val); + + arg.endMapEntry(); + } + arg.endMap(); + + return arg; +} + +QDBusArgument &operator<<(QDBusArgument &arg, const PropertiesMap &value) +{ + arg << value.map; + return arg; +} + +void explicit_zero_mem(void *data, size_t size) +{ +#if defined(KWALLETD_HAVE_EXPLICIT_BZERO) + explicit_bzero(data, size); +#elif defined(KWALLETD_HAVE_RTLSECUREZEROMEMORY) + RtlSecureZeroMemory(data, size); +#else + auto p = reinterpret_cast(data); + for (size_t i = 0; i < size; ++i) { + p[i] = 0; + } +#endif +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopservice.h b/src/runtime/kwalletd/kwalletfreedesktopservice.h new file mode 100644 index 00000000..6b05d56c --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopservice.h @@ -0,0 +1,228 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPSERVICE_H_ +#define _KWALLETFREEDESKTOPSERVICE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "kwalletdbuscontext.h" + +#define FDO_APPID QString() +#define FDO_SECRETS_SERVICE_OBJECT "/org/freedesktop/secrets" +#define FDO_ALIAS_PATH "/org/freedesktop/secrets/aliases/" + +static inline constexpr size_t FDO_SECRETS_CIPHER_KEY_SIZE = 16; +static inline constexpr int FDO_DH_PUBLIC_KEY_SIZE = 128; + +class KWalletD; + +class FreedesktopSecret +{ +public: + FreedesktopSecret() = default; + + FreedesktopSecret(QDBusObjectPath iSession, + const QCA::SecureArray &iNote, + QString iMimeType, + const QCA::SecureArray &iInitVector = QCA::InitializationVector(FDO_SECRETS_CIPHER_KEY_SIZE)) + : session(std::move(iSession)) + , note(iNote) + , mimeType(std::move(iMimeType)) + , initVector(iInitVector) + { + } + + friend QDBusArgument &operator<<(QDBusArgument &arg, const FreedesktopSecret &secret); + friend const QDBusArgument &operator>>(const QDBusArgument &arg, FreedesktopSecret &secret); + + QDBusObjectPath session; + QCA::SecureArray note; + QString mimeType; + QCA::SecureArray initVector; +}; + +struct PropertiesMap { + QVariantMap map; +}; + +struct EntryLocation { + static EntryLocation fromUniqueLabel(const struct FdoUniqueLabel &uniqLabel); + struct FdoUniqueLabel toUniqueLabel() const; + + bool operator==(const EntryLocation &rhs) const + { + return folder == rhs.folder && key == rhs.key; + } + + bool operator!=(const EntryLocation &rhs) const + { + return !(*this == rhs); + } + + QString folder; + QString key; +}; + +struct FdoUniqueLabel { + static FdoUniqueLabel fromEntryLocation(const EntryLocation &entryLocation); + static FdoUniqueLabel fromName(const QString &name); + static QString makeName(const QString &label, int copyId); + + bool operator==(const FdoUniqueLabel &rhs) const + { + return copyId == rhs.copyId && label == rhs.label; + } + + bool operator!=(const FdoUniqueLabel &rhs) const + { + return !(*this == rhs); + } + + QString toName() const; + EntryLocation toEntryLocation() const; + + QString label; + int copyId = -1; +}; + +typedef QMap FreedesktopSecretMap; +typedef QMap StrStrMap; + +Q_DECLARE_METATYPE(FreedesktopSecret) +Q_DECLARE_METATYPE(FreedesktopSecretMap) +Q_DECLARE_METATYPE(PropertiesMap) +Q_DECLARE_METATYPE(StrStrMap) +Q_DECLARE_METATYPE(QCA::SecureArray) + +class KWalletFreedesktopSession; +class KWalletFreedesktopCollection; +class KWalletFreedesktopPrompt; +class KWalletFreedesktopItem; + +class KWalletFreedesktopService : public QObject, protected FDO_DBUS_CONTEXT +{ + /* org.freedesktop.Secret.Service properties */ +public: + Q_PROPERTY(QList Collections READ collections) + QList collections() const; + + Q_OBJECT + +public: + explicit KWalletFreedesktopService(KWalletD *parent); + ~KWalletFreedesktopService(); + + KWalletFreedesktopService(const KWalletFreedesktopService &) = delete; + KWalletFreedesktopService &operator=(const KWalletFreedesktopService &) = delete; + + KWalletFreedesktopService(KWalletFreedesktopService &&) = delete; + KWalletFreedesktopService &operator=(KWalletFreedesktopService &&) = delete; + + static QString wrapToCollectionPath(const QString &itemPath); + + static QDBusObjectPath nextPromptPath(); + KWalletD *backend() const; + QDBusObjectPath fdoObjectPath() const; + + bool desecret(const QDBusMessage &message, FreedesktopSecret &secret); + bool ensecret(const QDBusMessage &message, FreedesktopSecret &secret); + KWalletFreedesktopItem *getItemByObjectPath(const QDBusObjectPath &path) const; + KWalletFreedesktopCollection *getCollectionByWalletName(const QString &walletName) const; + KWalletFreedesktopPrompt *getPromptByObjectPath(const QDBusObjectPath &path) const; + + FdoUniqueLabel makeUniqueCollectionLabel(const QString &label); + QString makeUniqueWalletName(const QString &labelPrefix); + QDBusObjectPath makeUniqueObjectPath(const QString &walletName) const; + + QString resolveIfAlias(QString alias); + QStringList readAliasesFor(const QString &walletName); + void createCollectionAlias(const QString &alias, KWalletFreedesktopCollection *collection); + void createCollectionAlias(const QString &alias, const QString &walletName); + void updateCollectionAlias(const QString &alias, const QString &walletName); + void removeAlias(const QString &alias); + + void deletePrompt(const QString &objectPath); + void deleteSession(const QString &objectPath); + QDBusObjectPath promptUnlockCollection(const QString &walletName, int handle); + + /* Emitters */ + void onCollectionCreated(const QDBusObjectPath &path); + void onCollectionChanged(const QDBusObjectPath &path); + void onCollectionDeleted(const QDBusObjectPath &path); + void onPropertiesChanged(const QVariantMap &properties); + +private Q_SLOTS: + void lockCollection(const QString &name); + void entryUpdated(const QString &walletName, const QString &folder, const QString &entryName); + void entryDeleted(const QString &walletName, const QString &folder, const QString &entryName); + void entryRenamed(const QString &walletName, const QString &folder, const QString &oldName, const QString &newName); + void walletDeleted(const QString &walletName); + void walletCreated(const QString &walletCreated); + /* + void slotServiceOwnerChanged(const QString &name, const QString &oldOwner, + const QString &newOwner); + */ + +private: + QString createSession(const QByteArray &clientKey); + QString defaultWalletName(KConfigGroup &cfg); + +private: + std::map> m_sessions; + std::map> m_collections; + std::map> m_prompts; + + uint64_t m_session_counter = 0; + + /* + QDBusServiceWatcher _serviceWatcher; + */ + KWalletD *m_parent; + QCA::Initializer m_init; + KConfig m_kwalletrc; + + /* Freedesktop API */ + + /* org.freedesktop.Secret.Service methods */ +public Q_SLOTS: + QDBusObjectPath CreateCollection(const QVariantMap &properties, const QString &alias, QDBusObjectPath &prompt); + FreedesktopSecretMap GetSecrets(const QList &items, const QDBusObjectPath &session); + QList Lock(const QList &objects, QDBusObjectPath &Prompt); + QDBusVariant OpenSession(const QString &algorithm, const QDBusVariant &input, QDBusObjectPath &result); + QDBusObjectPath ReadAlias(const QString &name); + QList SearchItems(const StrStrMap &attributes, QList &locked); + void SetAlias(const QString &name, const QDBusObjectPath &collection); + QList Unlock(const QList &objects, QDBusObjectPath &prompt); + + /* org.freedesktop.Secret.Service signals */ +Q_SIGNALS: + void CollectionChanged(const QDBusObjectPath &collection); + void CollectionCreated(const QDBusObjectPath &collection); + void CollectionDeleted(const QDBusObjectPath &collection); +}; + +QDataStream &operator<<(QDataStream &stream, const QDBusObjectPath &value); +QDataStream &operator>>(QDataStream &stream, QDBusObjectPath &value); + +const QDBusArgument &operator>>(const QDBusArgument &arg, PropertiesMap &value); +QDBusArgument &operator<<(QDBusArgument &arg, const PropertiesMap &value); + +QDataStream &operator<<(QDataStream &stream, const QCA::SecureArray &value); +QDataStream &operator>>(QDataStream &stream, QCA::SecureArray &value); +QDBusArgument &operator<<(QDBusArgument &arg, const QCA::SecureArray &value); +const QDBusArgument &operator>>(const QDBusArgument &arg, QCA::SecureArray &buf); + +void explicit_zero_mem(void *data, size_t size); + +#endif diff --git a/src/runtime/kwalletd/kwalletfreedesktopsession.cpp b/src/runtime/kwalletd/kwalletfreedesktopsession.cpp new file mode 100644 index 00000000..ad5e971f --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopsession.cpp @@ -0,0 +1,95 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#include "kwalletfreedesktopsession.h" + +#include "kwalletfreedesktopservice.h" +#include "kwalletfreedesktopsessionadaptor.h" +#include + +KWalletFreedesktopSession::KWalletFreedesktopSession(KWalletFreedesktopService *service, + const QCA::PublicKey &publicKey, + QCA::SymmetricKey symmetricKey, + QString sessionPath, + const QDBusConnection &connection, + const QDBusMessage &message) + : m_service(service) + , m_publicKey(publicKey) + , m_symmetricKey(std::move(symmetricKey)) + , m_sessionPath(std::move(sessionPath)) + , m_serviceBusName(message.service()) +{ + (void)new KWalletFreedesktopSessionAdaptor(this); + QDBusConnection::sessionBus().registerObject(m_sessionPath, this); + + m_serviceWatcher.setConnection(connection); + m_serviceWatcher.addWatchedService(m_serviceBusName); + m_serviceWatcher.setWatchMode(QDBusServiceWatcher::WatchForOwnerChange); + connect(&m_serviceWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this, &KWalletFreedesktopSession::slotServiceOwnerChanged); +} + +void KWalletFreedesktopSession::slotServiceOwnerChanged(const QString &, const QString &, const QString &) +{ + fdoService()->deleteSession(m_sessionPath); +} + +void KWalletFreedesktopSession::Close() +{ + if (message().service() != m_serviceBusName) { + sendErrorReply(QDBusError::ErrorType::UnknownObject, QStringLiteral("Can't find session ") + m_sessionPath); + } else { + fdoService()->deleteSession(m_sessionPath); + } +} + +CipherResult KWalletFreedesktopSession::encrypt(const QDBusMessage &message, const QCA::SecureArray &bytes, const QCA::SecureArray &initVector) const +{ + if (message.service() != m_serviceBusName) { + return {false, QByteArray()}; + } + + auto cipher = + QCA::Cipher(QStringLiteral("aes128"), QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Encode, m_symmetricKey, QCA::InitializationVector(initVector)); + QCA::SecureArray result; + result.append(cipher.update(QCA::MemoryRegion(bytes))); + if (cipher.ok()) { + result.append(cipher.final()); + } + + return {cipher.ok(), std::move(result)}; +} + +CipherResult KWalletFreedesktopSession::decrypt(const QDBusMessage &message, const QCA::SecureArray &bytes, const QCA::SecureArray &initVector) const +{ + if (message.service() != m_serviceBusName) { + return {false, QByteArray()}; + } + + auto cipher = + QCA::Cipher(QStringLiteral("aes128"), QCA::Cipher::CBC, QCA::Cipher::PKCS7, QCA::Decode, m_symmetricKey, QCA::InitializationVector(initVector)); + QCA::SecureArray result; + result.append(cipher.update(QCA::MemoryRegion(bytes))); + if (cipher.ok()) { + result.append(cipher.final()); + } + + return {cipher.ok(), std::move(result)}; +} + +KWalletFreedesktopService *KWalletFreedesktopSession::fdoService() const +{ + return m_service; +} + +KWalletD *KWalletFreedesktopSession::backend() const +{ + return fdoService()->backend(); +} + +QDBusObjectPath KWalletFreedesktopSession::fdoObjectPath() const +{ + return QDBusObjectPath(m_sessionPath); +} diff --git a/src/runtime/kwalletd/kwalletfreedesktopsession.h b/src/runtime/kwalletd/kwalletfreedesktopsession.h new file mode 100644 index 00000000..1860c878 --- /dev/null +++ b/src/runtime/kwalletd/kwalletfreedesktopsession.h @@ -0,0 +1,72 @@ +/* + This file is part of the KDE libraries + SPDX-FileCopyrightText: 2021 Slava Aseev + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ +#ifndef _KWALLETFREEDESKTOPSESSION_H_ +#define _KWALLETFREEDESKTOPSESSION_H_ + +#include "kwalletfreedesktopservice.h" +#include +#include +#include +#include + +#define FDO_SECRETS_SESSION_PATH FDO_SECRETS_SERVICE_OBJECT "/session/" + +class KWalletD; + +struct CipherResult { + bool ok; + QCA::SecureArray bytes; +}; + +class KWalletFreedesktopSession : public QObject, protected QDBusContext +{ + Q_OBJECT + +public: + KWalletFreedesktopSession(class KWalletFreedesktopService *parent, + const QCA::PublicKey &publicKey, + QCA::SymmetricKey symmetricKey, + QString sessionPath, + const QDBusConnection &connection, + const QDBusMessage &message); + + KWalletFreedesktopSession(const KWalletFreedesktopSession &) = delete; + KWalletFreedesktopSession &operator=(const KWalletFreedesktopSession &) = delete; + + KWalletFreedesktopSession(KWalletFreedesktopSession &&) = delete; + KWalletFreedesktopSession &operator=(KWalletFreedesktopSession &&) = delete; + + KWalletFreedesktopService *fdoService() const; + KWalletD *backend() const; + QDBusObjectPath fdoObjectPath() const; + + const QCA::PublicKey &publicKey() const + { + return m_publicKey; + } + CipherResult encrypt(const QDBusMessage &message, const QCA::SecureArray &bytes, const QCA::SecureArray &initVector) const; + CipherResult decrypt(const QDBusMessage &message, const QCA::SecureArray &bytes, const QCA::SecureArray &initVector) const; + +private Q_SLOTS: + void slotServiceOwnerChanged(const QString &name, const QString &oldOwner, const QString &newOwner); + +private: + class KWalletFreedesktopService *m_service; + QCA::PublicKey m_publicKey; + QCA::SymmetricKey m_symmetricKey; + QString m_sessionPath; + QString m_serviceBusName; + QDBusServiceWatcher m_serviceWatcher; + + /* Freedesktop API */ + + /* org.freedesktop.Secret.Session methods */ +public Q_SLOTS: + void Close(); +}; + +#endif diff --git a/src/runtime/kwalletd/main.cpp b/src/runtime/kwalletd/main.cpp index 99620347..541651d1 100644 --- a/src/runtime/kwalletd/main.cpp +++ b/src/runtime/kwalletd/main.cpp @@ -22,6 +22,7 @@ #include "backend/kwalletbackend.h" //For the hash size #include "kwalletd.h" #include "kwalletd_version.h" +#include "kwalletfreedesktopservice.h" #ifndef Q_OS_WIN #include @@ -194,6 +195,10 @@ int main(int argc, char **argv) // check if kwallet is disabled if (!isWalletEnabled()) { qCDebug(KWALLETD_LOG) << "kwalletd is disabled!"; + + /* Do not keep dbus-daemon waiting for the org.freedesktop.secrets if kwallet is disabled */ + KWalletFreedesktopService(nullptr); + return (0); } -- GitLab From da49ebd0493bff455d74247866c3332a2fe70dda Mon Sep 17 00:00:00 2001 From: Slava Aseev Date: Wed, 11 May 2022 18:40:21 +0300 Subject: [PATCH 2/3] Do not create EntryLocation with empty key Empty keys are not allowed, so the EntryLocation::fromUniqueLabel() function now identifies the KWallet folder by the first slash. Also a label with a single trailing slash will be treated as a key without a folder. --- src/runtime/kwalletd/autotests/fdo_secrets_test.cpp | 5 +++++ src/runtime/kwalletd/kwalletfreedesktopattributes.cpp | 2 +- src/runtime/kwalletd/kwalletfreedesktopcollection.cpp | 4 ++-- src/runtime/kwalletd/kwalletfreedesktopservice.cpp | 4 ++-- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp b/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp index 1c324be6..bc8b4243 100644 --- a/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp +++ b/src/runtime/kwalletd/autotests/fdo_secrets_test.cpp @@ -48,6 +48,11 @@ void FdoSecretsTest::collectionStaticFunctions() {{"Passwords", "password__"}, {"Passwords/password__", -1}}, {{"Passwords__3_", "password__200_"}, {"Passwords__3_/password", 200}}, {{"", "password"}, {"/password", -1}}, + {{"", "/"}, {"//", -1}}, + {{FDO_SECRETS_DEFAULT_DIR, "/"}, {"/", -1}}, + {{FDO_SECRETS_DEFAULT_DIR, "/__2_"}, {"/", 2}}, + {{"https:", "/foobar.org/"}, {"https://foobar.org/", -1}}, + {{"https:", "/foobar.org/__80_"}, {"https://foobar.org/", 80}}, }; runTestset( diff --git a/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp b/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp index b0791cbc..e1cba15c 100644 --- a/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp +++ b/src/runtime/kwalletd/kwalletfreedesktopattributes.cpp @@ -88,7 +88,7 @@ static QString entryLocationToStr(const EntryLocation &entryLocation) static EntryLocation splitToEntryLocation(const QString &entryLocation) { - const int slashPos = entryLocation.lastIndexOf(QChar::fromLatin1('/')); + const int slashPos = entryLocation.indexOf(QChar::fromLatin1('/')); if (slashPos == -1) { qCWarning(KWALLETD_LOG) << "Entry location '" << entryLocation << "' has no slash '/'"; return {}; diff --git a/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp b/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp index 3d5a5c2c..74bc1c5d 100644 --- a/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp +++ b/src/runtime/kwalletd/kwalletfreedesktopcollection.cpp @@ -262,8 +262,8 @@ EntryLocation KWalletFreedesktopCollection::makeUniqueEntryLocation(const QStrin { QString dir, name; - const int slashPos = label.lastIndexOf(QChar::fromLatin1('/')); - if (slashPos == -1) { + const int slashPos = label.indexOf(QChar::fromLatin1('/')); + if (slashPos == -1 || slashPos == label.size() - 1) { dir = QStringLiteral(FDO_SECRETS_DEFAULT_DIR); name = label; } else { diff --git a/src/runtime/kwalletd/kwalletfreedesktopservice.cpp b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp index c6b4821d..17c66ea9 100644 --- a/src/runtime/kwalletd/kwalletfreedesktopservice.cpp +++ b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp @@ -73,8 +73,8 @@ EntryLocation EntryLocation::fromUniqueLabel(const FdoUniqueLabel &uniqLabel) QString dir; QString name = uniqLabel.label; - const int slashPos = uniqLabel.label.lastIndexOf(QChar::fromLatin1('/')); - if (slashPos == -1) { + const int slashPos = uniqLabel.label.indexOf(QChar::fromLatin1('/')); + if (slashPos == -1 || slashPos == uniqLabel.label.size() - 1) { dir = QStringLiteral(FDO_SECRETS_DEFAULT_DIR); } else { dir = uniqLabel.label.left(slashPos); -- GitLab From 148e461d3c1460a387ee1573e05acb74c0e2d07b Mon Sep 17 00:00:00 2001 From: Slava Aseev Date: Thu, 12 May 2022 12:02:51 +0300 Subject: [PATCH 3/3] Do not try to rename label twice in entryRenamed() --- src/runtime/kwalletd/kwalletfreedesktopservice.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/runtime/kwalletd/kwalletfreedesktopservice.cpp b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp index 17c66ea9..ed6421f9 100644 --- a/src/runtime/kwalletd/kwalletfreedesktopservice.cpp +++ b/src/runtime/kwalletd/kwalletfreedesktopservice.cpp @@ -7,6 +7,7 @@ #include "kwalletfreedesktopservice.h" #include "kwalletd.h" +#include "kwalletd_debug.h" #include "kwalletfreedesktopcollection.h" #include "kwalletfreedesktopitem.h" #include "kwalletfreedesktopprompt.h" @@ -510,7 +511,11 @@ void KWalletFreedesktopService::entryRenamed(const QString &walletName, const QS auto *item = collection->findItemByEntryLocation(oldLocation); if (!item) { - item = collection->findItemByEntryLocation(newLocation); + /* Warn if label not found and not yet renamed */ + if (!collection->findItemByEntryLocation(newLocation)) { + qCWarning(KWALLETD_LOG) << "Cannot rename secret service label:" << FdoUniqueLabel::fromEntryLocation(oldLocation).label; + } + return; } if (item) { -- GitLab