keepassxc
681 строка · 24.2 Кб
1/*
2* Copyright (C) 2020 Aetf <aetf@unlimitedcode.works>
3*
4* This program is free software: you can redistribute it and/or modify
5* it under the terms of the GNU General Public License as published by
6* the Free Software Foundation, either version 2 or (at your option)
7* version 3 of the License.
8*
9* This program is distributed in the hope that it will be useful,
10* but WITHOUT ANY WARRANTY; without even the implied warranty of
11* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12* GNU General Public License for more details.
13*
14* You should have received a copy of the GNU General Public License
15* along with this program. If not, see <http://www.gnu.org/licenses/>.
16*/
17
18#include "DBusMgr.h"
19
20#include "fdosecrets/objects/Collection.h"
21#include "fdosecrets/objects/Item.h"
22#include "fdosecrets/objects/Prompt.h"
23#include "fdosecrets/objects/Service.h"
24#include "fdosecrets/objects/Session.h"
25
26#include "core/Entry.h"
27#include "core/Tools.h"
28
29#ifdef __FreeBSD__
30#include <string.h>
31#include <sys/sysctl.h>
32#endif
33
34namespace FdoSecrets
35{
36static const auto IntrospectionService = R"xml(
37<interface name="org.freedesktop.Secret.Service">
38<property name="Collections" type="ao" access="read"/>
39<signal name="CollectionCreated">
40<arg name="collection" type="o" direction="out"/>
41</signal>
42<signal name="CollectionDeleted">
43<arg name="collection" type="o" direction="out"/>
44</signal>
45<signal name="CollectionChanged">
46<arg name="collection" type="o" direction="out"/>
47</signal>
48<method name="OpenSession">
49<arg type="v" direction="out"/>
50<arg name="algorithm" type="s" direction="in"/>
51<arg name="input" type="v" direction="in"/>
52<arg name="result" type="o" direction="out"/>
53</method>
54<method name="CreateCollection">
55<arg type="o" direction="out"/>
56<arg name="properties" type="a{sv}" direction="in"/>
57<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
58<arg name="alias" type="s" direction="in"/>
59<arg name="prompt" type="o" direction="out"/>
60</method>
61<method name="SearchItems">
62<arg type="ao" direction="out"/>
63<arg name="attributes" type="a{ss}" direction="in"/>
64<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
65<arg name="locked" type="ao" direction="out"/>
66</method>
67<method name="Unlock">
68<arg type="ao" direction="out"/>
69<arg name="paths" type="ao" direction="in"/>
70<arg name="prompt" type="o" direction="out"/>
71</method>
72<method name="Lock">
73<arg type="ao" direction="out"/>
74<arg name="paths" type="ao" direction="in"/>
75<arg name="prompt" type="o" direction="out"/>
76</method>
77<method name="GetSecrets">
78<arg type="a{o(oayays)}" direction="out"/>
79<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="ObjectPathSecretMap"/>
80<arg name="items" type="ao" direction="in"/>
81<arg name="session" type="o" direction="in"/>
82</method>
83<method name="ReadAlias">
84<arg type="o" direction="out"/>
85<arg name="name" type="s" direction="in"/>
86</method>
87<method name="SetAlias">
88<arg name="name" type="s" direction="in"/>
89<arg name="collection" type="o" direction="in"/>
90</method>
91</interface>
92)xml";
93
94static const auto IntrospectionCollection = R"xml(
95<interface name="org.freedesktop.Secret.Collection">
96<property name="Items" type="ao" access="read"/>
97<property name="Label" type="s" access="readwrite"/>
98<property name="Locked" type="b" access="read"/>
99<property name="Created" type="t" access="read"/>
100<property name="Modified" type="t" access="read"/>
101<signal name="ItemCreated">
102<arg name="item" type="o" direction="out"/>
103</signal>
104<signal name="ItemDeleted">
105<arg name="item" type="o" direction="out"/>
106</signal>
107<signal name="ItemChanged">
108<arg name="item" type="o" direction="out"/>
109</signal>
110<method name="Delete">
111<arg type="o" direction="out"/>
112</method>
113<method name="SearchItems">
114<arg type="ao" direction="out"/>
115<arg name="attributes" type="a{ss}" direction="in"/>
116<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="StringStringMap"/>
117</method>
118<method name="CreateItem">
119<arg type="o" direction="out"/>
120<arg name="properties" type="a{sv}" direction="in"/>
121<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="QVariantMap"/>
122<arg name="secret" type="(oayays)" direction="in"/>
123<annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="FdoSecrets::wire::Secret"/>
124<arg name="replace" type="b" direction="in"/>
125<arg name="prompt" type="o" direction="out"/>
126</method>
127</interface>
128)xml";
129
130static const auto IntrospectionItem = R"xml(
131<interface name="org.freedesktop.Secret.Item">
132<property name="Locked" type="b" access="read"/>
133<property name="Attributes" type="a{ss}" access="readwrite">
134<annotation name="org.qtproject.QtDBus.QtTypeName" value="StringStringMap"/>
135</property>
136<property name="Label" type="s" access="readwrite"/>
137<property name="Created" type="t" access="read"/>
138<property name="Modified" type="t" access="read"/>
139<method name="Delete">
140<arg type="o" direction="out"/>
141</method>
142<method name="GetSecret">
143<arg type="(oayays)" direction="out"/>
144<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="FdoSecrets::wire::Secret"/>
145<arg name="session" type="o" direction="in"/>
146</method>
147<method name="SetSecret">
148<arg name="secret" type="(oayays)" direction="in"/>
149<annotation name="org.qtproject.QtDBus.QtTypeName.In0" value="FdoSecrets::wire::Secret"/>
150</method>
151</interface>
152)xml";
153
154static const auto IntrospectionSession = R"xml(
155<interface name="org.freedesktop.Secret.Session">
156<method name="Close">
157</method>
158</interface>
159)xml";
160
161static const auto IntrospectionPrompt = R"xml(
162<interface name="org.freedesktop.Secret.Prompt">
163<signal name="Completed">
164<arg name="dismissed" type="b" direction="out"/>
165<arg name="result" type="v" direction="out"/>
166</signal>
167<method name="Prompt">
168<arg name="windowId" type="s" direction="in"/>
169</method>
170<method name="Dismiss">
171</method>
172</interface>
173)xml";
174
175// read /proc, each field except pid may be empty
176ProcInfo readProc(uint pid)
177{
178ProcInfo info{};
179info.pid = pid;
180
181// The /proc/pid/exe link is more reliable than /proc/pid/cmdline
182// It's still weak and if the application does a prctl(PR_SET_DUMPABLE, 0) this link cannot be accessed.
183
184#ifdef __FreeBSD__
185const int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, static_cast<int>(info.pid)};
186char buffer[2048];
187size_t size = sizeof(buffer);
188if (sysctl(mib, 4, buffer, &size, NULL, 0) != 0) {
189strlcpy(buffer, "Invalid path", sizeof(buffer));
190}
191QFileInfo exe(buffer);
192#else
193QFileInfo exe(QStringLiteral("/proc/%1/exe").arg(pid));
194#endif
195info.exePath = exe.canonicalFilePath();
196
197// /proc/pid/cmdline gives full command line
198QFile cmdline(QStringLiteral("/proc/%1/cmdline").arg(pid));
199if (cmdline.open(QFile::ReadOnly)) {
200info.command = QString::fromLocal8Bit(cmdline.readAll().replace('\0', ' ')).trimmed();
201}
202
203// /proc/pid/stat gives ppid, name
204QFile stat(QStringLiteral("/proc/%1/stat").arg(pid));
205if (stat.open(QIODevice::ReadOnly)) {
206auto line = stat.readAll();
207// find comm field without looking in what's inside as it's user controlled
208auto commStart = line.indexOf('(');
209auto commEnd = line.lastIndexOf(')');
210if (commStart != -1 && commEnd != -1 && commStart < commEnd) {
211// start past '(', and subtract 2 from total length
212info.name = QString::fromLocal8Bit(line.mid(commStart + 1, commEnd + 1 - commStart - 2));
213
214auto parts = line.mid(commEnd + 1).trimmed().split(' ');
215if (parts.size() >= 2) {
216info.ppid = parts[1].toUInt();
217}
218}
219}
220
221return info;
222}
223
224DBusMgr::DBusMgr()
225: m_conn(QDBusConnection::sessionBus())
226{
227// remove client when it disappears on the bus
228m_watcher.setWatchMode(QDBusServiceWatcher::WatchForUnregistration);
229connect(&m_watcher, &QDBusServiceWatcher::serviceUnregistered, this, &DBusMgr::dbusServiceUnregistered);
230m_watcher.setConnection(m_conn);
231}
232
233void DBusMgr::populateMethodCache()
234{
235// these are the methods we expose on DBus
236populateMethodCache(Service::staticMetaObject);
237populateMethodCache(Collection::staticMetaObject);
238populateMethodCache(Item::staticMetaObject);
239populateMethodCache(PromptBase::staticMetaObject);
240populateMethodCache(Session::staticMetaObject);
241}
242
243DBusMgr::~DBusMgr() = default;
244
245void DBusMgr::overrideClient(const DBusClientPtr& fake)
246{
247m_overrideClient = fake;
248}
249
250QList<DBusClientPtr> DBusMgr::clients() const
251{
252return m_clients.values();
253}
254
255bool DBusMgr::serviceInfo(const QString& addr, PeerInfo& info) const
256{
257info.address = addr;
258auto pid = m_conn.interface()->servicePid(addr);
259if (!pid.isValid()) {
260return false;
261}
262info.pid = pid.value();
263
264// get the whole hierarchy
265uint ppid = info.pid;
266while (ppid != 0) {
267auto proc = readProc(ppid);
268info.hierarchy.append(proc);
269ppid = proc.ppid;
270}
271
272// check if the exe file is valid
273// (assuming for now that an accessible file is valid)
274info.valid = QFileInfo::exists(info.exePath());
275
276// ask again to double-check and protect against pid reuse
277auto newPid = m_conn.interface()->servicePid(addr);
278if (!newPid.isValid() || newPid.value() != pid.value()) {
279// either the peer already gone or the pid changed to something else
280return false;
281}
282return true;
283}
284
285bool DBusMgr::sendDBusSignal(const QString& path,
286const QString& interface,
287const QString& name,
288const QVariantList& arguments)
289{
290auto msg = QDBusMessage::createSignal(path, interface, name);
291msg.setArguments(arguments);
292return sendDBus(msg);
293}
294
295bool DBusMgr::sendDBus(const QDBusMessage& reply)
296{
297bool ok = m_conn.send(reply);
298if (!ok) {
299qDebug() << "Failed to send on DBus:" << reply;
300emit error(tr("Failed to send reply on DBus"));
301}
302return ok;
303}
304
305// `this` object is registered at multiple paths:
306// /org/freedesktop/secrets
307// /org/freedesktop/secrets/collection/xxx
308// /org/freedesktop/secrets/collection/xxx/yyy
309// /org/freedesktop/secrets/aliases/xxx
310// /org/freedesktop/secrets/session/xxx
311// /org/freedesktop/secrets/prompt/xxx
312//
313// The path validation is left to Qt, this method only do the minimum
314// required to differentiate the paths.
315DBusMgr::ParsedPath DBusMgr::parsePath(const QString& path)
316{
317Q_ASSERT(path.startsWith('/'));
318Q_ASSERT(path == "/" || !path.endsWith('/'));
319
320static const QString DBusPathSecrets = DBUS_PATH_SECRETS;
321
322if (!path.startsWith(DBusPathSecrets)) {
323return ParsedPath{};
324}
325auto parts = path.mid(DBusPathSecrets.size()).split('/');
326// the first part is always empty
327if (parts.isEmpty() || parts.first() != "") {
328return ParsedPath{};
329}
330parts.takeFirst();
331
332if (parts.isEmpty()) {
333return ParsedPath{PathType::Service};
334} else if (parts.size() == 2) {
335if (parts.at(0) == "collection") {
336return ParsedPath{PathType::Collection, parts.at(1)};
337} else if (parts.at(0) == "aliases") {
338return ParsedPath{PathType::Aliases, parts.at(1)};
339} else if (parts.at(0) == "prompt") {
340return ParsedPath{PathType::Prompt, parts.at(1)};
341} else if (parts.at(0) == "session") {
342return ParsedPath{PathType::Session, parts.at(1)};
343}
344} else if (parts.size() == 3) {
345if (parts.at(0) == "collection") {
346return ParsedPath{PathType::Item, parts.at(2), parts.at(1)};
347}
348}
349return ParsedPath{};
350}
351
352QString DBusMgr::introspect(const QString& path) const
353{
354auto parsed = parsePath(path);
355switch (parsed.type) {
356case PathType::Service:
357return IntrospectionService;
358case PathType::Collection:
359case PathType::Aliases:
360return IntrospectionCollection;
361case PathType::Prompt:
362return IntrospectionPrompt;
363case PathType::Session:
364return IntrospectionSession;
365case PathType::Item:
366return IntrospectionItem;
367case PathType::Unknown:
368default:
369return "";
370}
371}
372
373bool DBusMgr::serviceOccupied() const
374{
375auto reply = m_conn.interface()->isServiceRegistered(DBUS_SERVICE_SECRET);
376if (!reply.isValid()) {
377return false;
378}
379if (reply.value()) {
380auto pid = m_conn.interface()->servicePid(DBUS_SERVICE_SECRET);
381if (pid.isValid() && pid.value() != qApp->applicationPid()) {
382return true;
383}
384}
385return false;
386}
387
388QString DBusMgr::reportExistingService() const
389{
390auto pidStr = tr("Unknown", "Unknown PID");
391auto exeStr = tr("Unknown", "Unknown executable path");
392
393PeerInfo info{};
394if (serviceInfo(DBUS_SERVICE_SECRET, info)) {
395pidStr = QString::number(info.pid);
396if (!info.exePath().isEmpty()) {
397exeStr = info.exePath();
398}
399}
400
401auto otherService = tr("<i>PID: %1, Executable: %2</i>", "<i>PID: 1234, Executable: /path/to/exe</i>")
402.arg(pidStr, exeStr.toHtmlEscaped());
403return tr("Another secret service is running (%1).<br/>"
404"Please stop/remove it before re-enabling the Secret Service Integration.")
405.arg(otherService);
406}
407
408bool DBusMgr::registerObject(const QString& path, DBusObject* obj, bool primary)
409{
410if (!m_conn.registerVirtualObject(path, this)) {
411qDebug() << "failed to register" << obj << "at" << path;
412return false;
413}
414connect(obj, &DBusObject::destroyed, this, &DBusMgr::unregisterObject);
415m_objects.insert(path, obj);
416if (primary) {
417obj->setObjectPath(path);
418}
419return true;
420}
421
422bool DBusMgr::registerObject(Service* service)
423{
424if (!m_conn.registerService(DBUS_SERVICE_SECRET)) {
425const auto existing = reportExistingService();
426qDebug() << "Failed to register DBus service at " << DBUS_SERVICE_SECRET;
427qDebug() << existing;
428emit error(tr("Failed to register DBus service at %1.<br/>").arg(DBUS_SERVICE_SECRET) + existing);
429return false;
430}
431connect(service, &DBusObject::destroyed, this, [this]() { m_conn.unregisterService(DBUS_SERVICE_SECRET); });
432
433if (!registerObject(DBUS_PATH_SECRETS, service)) {
434qDebug() << "Failed to register service on DBus at path" << DBUS_PATH_SECRETS;
435emit error(tr("Failed to register service on DBus at path '%1'").arg(DBUS_PATH_SECRETS));
436return false;
437}
438
439connect(service, &Service::collectionCreated, this, &DBusMgr::emitCollectionCreated);
440connect(service, &Service::collectionChanged, this, &DBusMgr::emitCollectionChanged);
441connect(service, &Service::collectionDeleted, this, &DBusMgr::emitCollectionDeleted);
442
443return true;
444}
445
446bool DBusMgr::registerObject(Collection* coll)
447{
448auto name = encodePath(coll->name());
449auto path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name);
450if (!registerObject(path, coll)) {
451// try again with a suffix
452name.append(QString("_%1").arg(Tools::uuidToHex(QUuid::createUuid()).left(4)));
453path = DBUS_PATH_TEMPLATE_COLLECTION.arg(DBUS_PATH_SECRETS, name);
454
455if (!registerObject(path, coll)) {
456qDebug() << "Failed to register database on DBus under name" << name;
457emit error(tr("Failed to register database on DBus under the name '%1'").arg(name));
458return false;
459}
460}
461
462connect(coll, &Collection::itemCreated, this, &DBusMgr::emitItemCreated);
463connect(coll, &Collection::itemChanged, this, &DBusMgr::emitItemChanged);
464connect(coll, &Collection::itemDeleted, this, &DBusMgr::emitItemDeleted);
465
466return true;
467}
468
469bool DBusMgr::registerObject(Session* sess)
470{
471auto path = DBUS_PATH_TEMPLATE_SESSION.arg(DBUS_PATH_SECRETS, sess->id());
472if (!registerObject(path, sess)) {
473emit error(tr("Failed to register session on DBus at path '%1'").arg(path));
474return false;
475}
476return true;
477}
478
479bool DBusMgr::registerObject(Item* item)
480{
481auto path = DBUS_PATH_TEMPLATE_ITEM.arg(item->collection()->objectPath().path(), item->backend()->uuidToHex());
482if (!registerObject(path, item)) {
483emit error(tr("Failed to register item on DBus at path '%1'").arg(path));
484return false;
485}
486return true;
487}
488
489bool DBusMgr::registerObject(PromptBase* prompt)
490{
491auto path = DBUS_PATH_TEMPLATE_PROMPT.arg(DBUS_PATH_SECRETS, Tools::uuidToHex(QUuid::createUuid()));
492if (!registerObject(path, prompt)) {
493emit error(tr("Failed to register prompt object on DBus at path '%1'").arg(path));
494return false;
495}
496
497connect(prompt, &PromptBase::completed, this, &DBusMgr::emitPromptCompleted);
498
499return true;
500}
501
502void DBusMgr::unregisterObject(DBusObject* obj)
503{
504auto count = m_objects.remove(obj->objectPath().path());
505if (count > 0) {
506m_conn.unregisterObject(obj->objectPath().path());
507obj->setObjectPath("/");
508}
509}
510
511bool DBusMgr::registerAlias(Collection* coll, const QString& alias)
512{
513auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias);
514if (!registerObject(path, coll, false)) {
515qDebug() << "Failed to register database on DBus under alias" << alias;
516// usually this is reported back directly on dbus, so no need to show in UI
517return false;
518}
519// alias signals are handled together with collections' primary path in emitCollection*
520// but we need to handle object destroy here
521connect(coll, &DBusObject::destroyed, this, [this, alias]() { unregisterAlias(alias); });
522return true;
523}
524
525void DBusMgr::unregisterAlias(const QString& alias)
526{
527auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias);
528// DBusMgr::unregisterObject only handles primary path
529m_objects.remove(path);
530m_conn.unregisterObject(path);
531}
532
533void DBusMgr::emitCollectionCreated(Collection* coll)
534{
535QVariantList args;
536args += QVariant::fromValue(coll->objectPath());
537sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionCreated"), args);
538}
539
540void DBusMgr::emitCollectionChanged(Collection* coll)
541{
542QVariantList args;
543args += QVariant::fromValue(coll->objectPath());
544sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, "CollectionChanged", args);
545}
546
547void DBusMgr::emitCollectionDeleted(Collection* coll)
548{
549QVariantList args;
550args += QVariant::fromValue(coll->objectPath());
551sendDBusSignal(DBUS_PATH_SECRETS, DBUS_INTERFACE_SECRET_SERVICE, QStringLiteral("CollectionDeleted"), args);
552}
553
554void DBusMgr::emitItemCreated(Item* item)
555{
556auto coll = item->collection();
557QVariantList args;
558args += QVariant::fromValue(item->objectPath());
559// send on primary path
560sendDBusSignal(
561coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args);
562// also send on all alias path
563for (const auto& alias : coll->aliases()) {
564auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias);
565sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemCreated"), args);
566}
567}
568
569void DBusMgr::emitItemChanged(Item* item)
570{
571auto coll = item->collection();
572QVariantList args;
573args += QVariant::fromValue(item->objectPath());
574// send on primary path
575sendDBusSignal(
576coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args);
577// also send on all alias path
578for (const auto& alias : coll->aliases()) {
579auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias);
580sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemChanged"), args);
581}
582}
583
584void DBusMgr::emitItemDeleted(Item* item)
585{
586auto coll = item->collection();
587QVariantList args;
588args += QVariant::fromValue(item->objectPath());
589// send on primary path
590sendDBusSignal(
591coll->objectPath().path(), DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args);
592// also send on all alias path
593for (const auto& alias : coll->aliases()) {
594auto path = DBUS_PATH_TEMPLATE_ALIAS.arg(DBUS_PATH_SECRETS, alias);
595sendDBusSignal(path, DBUS_INTERFACE_SECRET_COLLECTION, QStringLiteral("ItemDeleted"), args);
596}
597}
598
599void DBusMgr::emitPromptCompleted(bool dismissed, QVariant result)
600{
601auto prompt = qobject_cast<PromptBase*>(sender());
602if (!prompt) {
603qDebug() << "Wrong sender in emitPromptCompleted";
604return;
605}
606
607// make sure the result contains a valid value, otherwise QDBusVariant refuses to marshall it.
608if (!result.isValid()) {
609result = QString{};
610}
611
612QVariantList args;
613args += QVariant::fromValue(dismissed);
614args += QVariant::fromValue(QDBusVariant(result));
615sendDBusSignal(prompt->objectPath().path(), DBUS_INTERFACE_SECRET_PROMPT, QStringLiteral("Completed"), args);
616}
617
618DBusClientPtr DBusMgr::findClient(const QString& addr)
619{
620if (m_overrideClient) {
621return m_overrideClient;
622}
623
624auto it = m_clients.find(addr);
625if (it == m_clients.end()) {
626auto client = createClient(addr);
627if (!client) {
628return {};
629}
630it = m_clients.insert(addr, client);
631}
632return it.value();
633}
634
635DBusClientPtr DBusMgr::createClient(const QString& addr)
636{
637PeerInfo info{};
638if (!serviceInfo(addr, info)) {
639return {};
640}
641
642auto client = DBusClientPtr(new DBusClient(this, info));
643
644emit clientConnected(client);
645m_watcher.addWatchedService(addr);
646
647return client;
648}
649
650void DBusMgr::removeClient(DBusClient* client)
651{
652if (!client) {
653return;
654}
655
656auto it = m_clients.find(client->address());
657if (it == m_clients.end()) {
658return;
659}
660
661emit clientDisconnected(*it);
662m_clients.erase(it);
663}
664
665void DBusMgr::dbusServiceUnregistered(const QString& service)
666{
667auto removed = m_watcher.removeWatchedService(service);
668if (!removed) {
669qDebug("FdoSecrets: Failed to remove service watcher");
670}
671
672auto it = m_clients.find(service);
673if (it == m_clients.end()) {
674return;
675}
676auto client = it.value();
677
678// client will notify DBusMgr and call DBusMgr::removeClient
679client->disconnectDBus();
680}
681} // namespace FdoSecrets
682