QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCKeychain.cc
Go to the documentation of this file.
1#include "QGCKeychain.h"
2
3#include <QtCore/QCoreApplication>
4#include <QtCore/QEventLoop>
5#include <QtCore/QLatin1StringView>
6#include <QtCore/QSettings>
7#include <atomic>
8#include <mutex>
9#include <qtkeychain/keychain.h>
10
11#include "QGCLoggingCategory.h"
12
13QGC_LOGGING_CATEGORY(QGCKeychainLog, "Utilities.Platform.QGCKeychain")
14
15namespace {
16
17constexpr QLatin1StringView kSettingsGroup{"KeychainFallback"};
18
19QString serviceName()
20{
21 return QCoreApplication::applicationName();
22}
23
24QSettings settingsStore()
25{
26 return QSettings(QSettings::IniFormat, QSettings::UserScope, QCoreApplication::organizationName(),
27 QCoreApplication::applicationName());
28}
29
30bool fallbackWrite(const QString& key, const QByteArray& data)
31{
32 QSettings s = settingsStore();
33 s.beginGroup(kSettingsGroup);
34 s.setValue(key, data);
35 s.endGroup();
36 s.sync();
37 return s.status() == QSettings::NoError;
38}
39
40QByteArray fallbackRead(const QString& key)
41{
42 QSettings s = settingsStore();
43 s.beginGroup(kSettingsGroup);
44 const QByteArray result = s.value(key).toByteArray();
45 s.endGroup();
46 return result;
47}
48
49bool fallbackRemove(const QString& key)
50{
51 QSettings s = settingsStore();
52 s.beginGroup(kSettingsGroup);
53 s.remove(key);
54 s.endGroup();
55 s.sync();
56 return s.status() == QSettings::NoError;
57}
58
59// Once the backend proves unavailable, skip all future probes for the session.
60std::atomic<bool> g_qtkeychainUnavailable{false};
61
62// One-time probe: avoids per-call DBus round-trip and KDE unlock prompt on a doomed session.
63void probeBackendOnce()
64{
65 static std::once_flag flag;
66 std::call_once(flag, [] {
67 if (!QKeychain::isAvailable()) {
68 qCInfo(QGCKeychainLog) << "qtkeychain reports no available backend; using QSettings fallback";
69 g_qtkeychainUnavailable.store(true, std::memory_order_relaxed);
70 }
71 });
72}
73
74QKeychain::Error runJob(QKeychain::Job* job)
75{
76 if (!QCoreApplication::instance()) {
77 qCWarning(QGCKeychainLog) << "No QCoreApplication — skipping keychain operation";
78 return QKeychain::OtherError;
79 }
80 QEventLoop loop;
81 QObject::connect(job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
82 job->start();
83 loop.exec();
84 return job->error();
85}
86
87bool indicatesMissingBackend(QKeychain::Error err)
88{
89 // NoBackendAvailable = headless; AccessDenied = dismissed prompt — both treated as missing.
90 return err == QKeychain::NoBackendAvailable || err == QKeychain::AccessDenied;
91}
92
93// Linux libsecret backend reports DBus ServiceUnknown as OtherError — recognize it as "no backend".
94bool isMissingSecretService(QKeychain::Error err, const QString& errorString)
95{
96 if (err != QKeychain::OtherError) {
97 return false;
98 }
99 return errorString.contains(QLatin1String("org.freedesktop.secrets")) ||
100 errorString.contains(QLatin1String("ServiceUnknown"));
101}
102
103} // namespace
104
105bool QGCKeychain::writeBinary(const QString& key, const QByteArray& data)
106{
107 probeBackendOnce();
108 if (!g_qtkeychainUnavailable.load(std::memory_order_relaxed)) {
109 QKeychain::WritePasswordJob job(serviceName());
110 job.setInsecureFallback(false); // we manage our own fallback to keep one canonical store
111 job.setKey(key);
112 job.setBinaryData(data);
113 const auto err = runJob(&job);
114 if (err == QKeychain::NoError) {
115 return true;
116 }
117 if (indicatesMissingBackend(err) || isMissingSecretService(err, job.errorString())) {
118 qCInfo(QGCKeychainLog) << "qtkeychain unavailable, using QSettings fallback:" << job.errorString();
119 g_qtkeychainUnavailable.store(true, std::memory_order_relaxed);
120 } else {
121 qCWarning(QGCKeychainLog) << "Keychain write error:" << job.errorString();
122 return false;
123 }
124 }
125 return fallbackWrite(key, data);
126}
127
128QByteArray QGCKeychain::readBinary(const QString& key)
129{
130 probeBackendOnce();
131 if (!g_qtkeychainUnavailable.load(std::memory_order_relaxed)) {
132 QKeychain::ReadPasswordJob job(serviceName());
133 job.setInsecureFallback(false);
134 job.setKey(key);
135 const auto err = runJob(&job);
136 if (err == QKeychain::NoError) {
137 return job.binaryData();
138 }
139 if (indicatesMissingBackend(err) || isMissingSecretService(err, job.errorString())) {
140 qCInfo(QGCKeychainLog) << "qtkeychain unavailable, using QSettings fallback:" << job.errorString();
141 g_qtkeychainUnavailable.store(true, std::memory_order_relaxed);
142 // fall through to QSettings
143 } else if (err == QKeychain::EntryNotFound) {
144 // Entry may have been written by a prior session with no backend — try fallback store.
145 } else {
146 qCWarning(QGCKeychainLog) << "Keychain read error:" << job.errorString();
147 return {};
148 }
149 }
150 return fallbackRead(key);
151}
152
153bool QGCKeychain::remove(const QString& key)
154{
155 probeBackendOnce();
156 bool keychainOk = false;
157 if (!g_qtkeychainUnavailable.load(std::memory_order_relaxed)) {
158 QKeychain::DeletePasswordJob job(serviceName());
159 job.setInsecureFallback(false);
160 job.setKey(key);
161 const auto err = runJob(&job);
162 if (err == QKeychain::NoError || err == QKeychain::EntryNotFound) {
163 keychainOk = true;
164 } else if (indicatesMissingBackend(err) || isMissingSecretService(err, job.errorString())) {
165 qCInfo(QGCKeychainLog) << "qtkeychain unavailable, using QSettings fallback:" << job.errorString();
166 g_qtkeychainUnavailable.store(true, std::memory_order_relaxed);
167 } else {
168 qCWarning(QGCKeychainLog) << "Keychain remove error:" << job.errorString();
169 }
170 }
171 // Always clear the QSettings fallback too — entry may live there from a prior session.
172 const bool fallbackOk = fallbackRemove(key);
173 return keychainOk || fallbackOk;
174}
QString errorString
#define QGC_LOGGING_CATEGORY(name, categoryStr)
QByteArray readBinary(const QString &key)
bool writeBinary(const QString &key, const QByteArray &data)
bool remove(const QString &key)