Branch data Line data Source code
1 : : #include "CredentialStore.h"
2 : :
3 : : #include <QEventLoop>
4 : : #include <QHash>
5 : : #include <QLoggingCategory>
6 : : #include <QTimer>
7 : : #include <QUuid>
8 : : #include <qt6keychain/keychain.h>
9 : :
10 [ + + + - : 27 : Q_LOGGING_CATEGORY(lcCredentialStore, "mailjd.credentials")
+ - - - ]
11 : :
12 : : namespace {
13 : :
14 : : #ifdef MAILJD_TEST_KEYRING
15 : : // Test-only seam: MAILJD_FAKE_KEYRING=1 replaces QtKeychain with an
16 : : // in-process store. The entire implementation is omitted from product
17 : : // targets, rather than merely guarded by a runtime environment variable.
18 : 856 : QHash<QString, QByteArray> &fakeKeyring() {
19 [ + + + - ]: 856 : static QHash<QString, QByteArray> store;
20 : 856 : return store;
21 : : }
22 : 570 : bool useFakeKeyring() {
23 : 570 : return qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING");
24 : : }
25 : : #else
26 : : void warnIfTestKeyringRequested() {
27 : : static bool warned = false;
28 : : if (!warned &&
29 : : (qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING") ||
30 : : qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING_FAIL_WRITES"))) {
31 : : warned = true;
32 : : qCCritical(lcCredentialStore)
33 : : << "Ignoring test-keyring environment in a production build";
34 : : }
35 : : }
36 : : #endif
37 : :
38 : : } // namespace
39 : :
40 : 521 : CredentialStore::CredentialStore(QObject *parent) : QObject(parent) {}
41 : :
42 : 574 : QString CredentialStore::keyringKey(const QString &service,
43 : : const QString &accountName) {
44 [ + - ]: 574 : return QStringLiteral("mailjd/%1/%2").arg(service, accountName);
45 : : }
46 : :
47 : 3 : bool CredentialStore::isAvailable() {
48 : : #ifdef MAILJD_TEST_KEYRING
49 [ + + ]: 3 : if (useFakeKeyring())
50 : 2 : return true;
51 : : #else
52 : : warnIfTestKeyringRequested();
53 : : #endif
54 [ + - ]: 1 : QKeychain::ReadPasswordJob job(QStringLiteral("mailjd"));
55 [ + - ]: 1 : job.setAutoDelete(false);
56 [ + - + - ]: 1 : job.setKey(keyringKey(
57 : 2 : QStringLiteral("availability-probe"),
58 [ + - + - ]: 2 : QUuid::createUuid().toString(QUuid::WithoutBraces)));
59 : :
60 [ + - ]: 1 : QEventLoop loop;
61 [ + - ]: 1 : QTimer timeout;
62 [ + - ]: 1 : timeout.setSingleShot(true);
63 : :
64 : 1 : bool finished = false;
65 : 1 : bool timedOut = false;
66 : 1 : QObject::connect(&job, &QKeychain::ReadPasswordJob::finished, &loop,
67 [ + - ]: 1 : [&](QKeychain::Job *) {
68 [ - + ]: 1 : if (timedOut)
69 : 0 : return;
70 : 1 : finished = true;
71 : 1 : loop.quit();
72 : : });
73 [ + - ]: 1 : QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
74 [ # # ]: 0 : if (finished)
75 : 0 : return;
76 : 0 : timedOut = true;
77 : 0 : loop.quit();
78 : : });
79 : :
80 [ + - ]: 1 : timeout.start(5000);
81 [ + - ]: 1 : job.start();
82 [ + - ]: 1 : loop.exec();
83 : :
84 [ - + ]: 1 : if (timedOut) {
85 [ # # # # : 0 : qCWarning(lcCredentialStore)
# # ]
86 [ # # ]: 0 : << "Keyring availability probe did not complete";
87 : 0 : return false;
88 : : }
89 : :
90 [ + - ]: 1 : const auto error = job.error();
91 [ + - - + ]: 1 : const bool available = error == QKeychain::NoError ||
92 : : error == QKeychain::EntryNotFound;
93 [ + - ]: 1 : if (!available) {
94 [ + - + - : 2 : qCWarning(lcCredentialStore)
+ + ]
95 [ + - + - : 1 : << "Keyring availability probe failed:" << job.errorString();
+ - ]
96 : : }
97 : 1 : return available;
98 : 1 : }
99 : :
100 : 370 : void CredentialStore::readPassword(const QString &service,
101 : : const QString &accountName,
102 : : ReadCallback callback) {
103 : : #ifdef MAILJD_TEST_KEYRING
104 [ + - ]: 370 : if (useFakeKeyring()) {
105 [ + - ]: 370 : const QString key = keyringKey(service, accountName);
106 : : // Deliver asynchronously to preserve the QtKeychain calling contract.
107 [ + - + - : 370 : QTimer::singleShot(0, this, [callback, key]() {
- - ]
108 : 348 : const bool found = fakeKeyring().contains(key);
109 [ + - ]: 348 : callback(found, fakeKeyring().value(key));
110 : 348 : });
111 : 370 : return;
112 : 370 : }
113 : : #else
114 : : warnIfTestKeyringRequested();
115 : : #endif
116 : : auto *job = new QKeychain::ReadPasswordJob(
117 [ # # # # : 0 : QStringLiteral("mailjd"), this);
# # ]
118 : 0 : job->setAutoDelete(true);
119 [ # # # # ]: 0 : job->setKey(keyringKey(service, accountName));
120 : :
121 : 0 : connect(job, &QKeychain::ReadPasswordJob::finished, this,
122 [ # # # # : 0 : [callback, service, accountName](QKeychain::Job *j) {
# # # # ]
123 : 0 : auto *readJob = qobject_cast<QKeychain::ReadPasswordJob *>(j);
124 [ # # # # : 0 : if (readJob && readJob->error() == QKeychain::NoError) {
# # ]
125 : : // QKeychain stores as QString — convert to QByteArray
126 [ # # # # ]: 0 : QByteArray pw = readJob->textData().toUtf8();
127 [ # # # # : 0 : qCDebug(lcCredentialStore)
# # ]
128 [ # # # # : 0 : << "Read password for" << service << accountName;
# # ]
129 [ # # ]: 0 : callback(true, pw);
130 : 0 : } else {
131 : 0 : QString errMsg = readJob ? readJob->errorString()
132 [ # # # # : 0 : : QStringLiteral("Unknown error");
# # # # ]
133 [ # # # # : 0 : qCWarning(lcCredentialStore)
# # ]
134 [ # # # # : 0 : << "Failed to read password for" << service << accountName
# # ]
135 [ # # # # ]: 0 : << ":" << errMsg;
136 [ # # ]: 0 : callback(false, QByteArray());
137 : 0 : }
138 : 0 : });
139 : :
140 : 0 : job->start();
141 : : }
142 : :
143 : 130 : void CredentialStore::writePassword(const QString &service,
144 : : const QString &accountName,
145 : : const QByteArray &password,
146 : : WriteCallback callback) {
147 : : #ifdef MAILJD_TEST_KEYRING
148 [ + + ]: 130 : if (useFakeKeyring()) {
149 [ + - ]: 106 : const QString key = keyringKey(service, accountName);
150 [ + - + - : 106 : QTimer::singleShot(0, this, [callback, key, password]() {
- - - - ]
151 [ + + ]: 106 : if (qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING_FAIL_WRITES")) {
152 [ + - ]: 22 : callback(false,
153 : 22 : QStringLiteral("Injected fake keyring write failure "
154 : : "[MAILJD_COMPILED_TEST_KEYRING]"));
155 : 11 : return;
156 : : }
157 : 95 : fakeKeyring().insert(key, password);
158 [ + - ]: 95 : callback(true, QString());
159 : : });
160 : 106 : return;
161 : 106 : }
162 : : #else
163 : : warnIfTestKeyringRequested();
164 : : #endif
165 : : auto *job = new QKeychain::WritePasswordJob(
166 [ + - - + : 48 : QStringLiteral("mailjd"), this);
- - ]
167 : 24 : job->setAutoDelete(true);
168 [ + - + - ]: 24 : job->setKey(keyringKey(service, accountName));
169 [ + - + - ]: 24 : job->setTextData(QString::fromUtf8(password));
170 : :
171 : 24 : connect(job, &QKeychain::WritePasswordJob::finished, this,
172 [ + - + - : 48 : [callback, service, accountName](QKeychain::Job *j) {
- - - - ]
173 [ - + ]: 24 : if (j->error() == QKeychain::NoError) {
174 [ # # # # : 0 : qCInfo(lcCredentialStore)
# # ]
175 [ # # # # : 0 : << "Stored password for" << service << accountName;
# # ]
176 [ # # ]: 0 : callback(true, QString());
177 : : } else {
178 [ + - + - : 48 : qCWarning(lcCredentialStore)
+ + ]
179 [ + - + - : 24 : << "Failed to write password for" << service << accountName
+ - ]
180 [ + - + - : 24 : << ":" << j->errorString();
+ - ]
181 [ + - + - ]: 24 : callback(false, j->errorString());
182 : : }
183 : 24 : });
184 : :
185 : 24 : job->start();
186 : : }
187 : :
188 : 67 : void CredentialStore::deletePassword(const QString &service,
189 : : const QString &accountName,
190 : : DeleteCallback callback) {
191 : : #ifdef MAILJD_TEST_KEYRING
192 [ + + ]: 67 : if (useFakeKeyring()) {
193 [ + - ]: 65 : const QString key = keyringKey(service, accountName);
194 [ + - + - : 65 : QTimer::singleShot(0, this, [callback, key]() {
- - ]
195 : 65 : callback(fakeKeyring().remove(key) > 0);
196 : 65 : });
197 : 65 : return;
198 : 65 : }
199 : : #else
200 : : warnIfTestKeyringRequested();
201 : : #endif
202 : : auto *job = new QKeychain::DeletePasswordJob(
203 [ + - - + : 4 : QStringLiteral("mailjd"), this);
- - ]
204 : 2 : job->setAutoDelete(true);
205 [ + - + - ]: 2 : job->setKey(keyringKey(service, accountName));
206 : :
207 : 2 : connect(job, &QKeychain::DeletePasswordJob::finished, this,
208 [ + - + - : 4 : [callback, service, accountName](QKeychain::Job *j) {
- - - - ]
209 [ - + ]: 2 : if (j->error() == QKeychain::NoError) {
210 [ # # # # : 0 : qCInfo(lcCredentialStore)
# # ]
211 [ # # # # : 0 : << "Deleted password for" << service << accountName;
# # ]
212 : 0 : callback(true);
213 : : } else {
214 [ + - + - : 4 : qCWarning(lcCredentialStore)
+ + ]
215 [ + - + - : 2 : << "Failed to delete password for" << service << accountName
+ - ]
216 [ + - + - : 2 : << ":" << j->errorString();
+ - ]
217 : 2 : callback(false);
218 : : }
219 : 2 : });
220 : :
221 : 2 : job->start();
222 : : }
|