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 [ + + + - : 25 : Q_LOGGING_CATEGORY(lcCredentialStore, "mailjd.credentials")
+ - - - ]
11 : :
12 : : // Test seam: MAILJD_FAKE_KEYRING=1 replaces QtKeychain with an in-process
13 : : // store. Required for headless CI — QtKeychain blocks (and times out) when
14 : : // no keyring daemon is available, which makes every credential code path
15 : : // untestable otherwise. Never enabled in production.
16 : : namespace {
17 : 603 : QHash<QString, QByteArray> &fakeKeyring() {
18 [ + + + - ]: 603 : static QHash<QString, QByteArray> store;
19 : 603 : return store;
20 : : }
21 : 389 : bool useFakeKeyring() {
22 : 389 : return qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING");
23 : : }
24 : : } // namespace
25 : :
26 : 339 : CredentialStore::CredentialStore(QObject *parent) : QObject(parent) {}
27 : :
28 : 393 : QString CredentialStore::keyringKey(const QString &service,
29 : : const QString &accountName) {
30 [ + - ]: 393 : return QStringLiteral("mailjd/%1/%2").arg(service, accountName);
31 : : }
32 : :
33 : 3 : bool CredentialStore::isAvailable() {
34 [ + + ]: 3 : if (useFakeKeyring())
35 : 2 : return true;
36 [ + - ]: 1 : QKeychain::ReadPasswordJob job(QStringLiteral("mailjd"));
37 [ + - ]: 1 : job.setAutoDelete(false);
38 [ + - + - ]: 1 : job.setKey(keyringKey(
39 : 2 : QStringLiteral("availability-probe"),
40 [ + - + - ]: 2 : QUuid::createUuid().toString(QUuid::WithoutBraces)));
41 : :
42 [ + - ]: 1 : QEventLoop loop;
43 [ + - ]: 1 : QTimer timeout;
44 [ + - ]: 1 : timeout.setSingleShot(true);
45 : :
46 : 1 : bool finished = false;
47 : 1 : bool timedOut = false;
48 : 1 : QObject::connect(&job, &QKeychain::ReadPasswordJob::finished, &loop,
49 [ + - ]: 1 : [&](QKeychain::Job *) {
50 [ - + ]: 1 : if (timedOut)
51 : 0 : return;
52 : 1 : finished = true;
53 : 1 : loop.quit();
54 : : });
55 [ + - ]: 1 : QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
56 [ # # ]: 0 : if (finished)
57 : 0 : return;
58 : 0 : timedOut = true;
59 : 0 : loop.quit();
60 : : });
61 : :
62 [ + - ]: 1 : timeout.start(5000);
63 [ + - ]: 1 : job.start();
64 [ + - ]: 1 : loop.exec();
65 : :
66 [ - + ]: 1 : if (timedOut) {
67 [ # # # # : 0 : qCWarning(lcCredentialStore)
# # ]
68 [ # # ]: 0 : << "Keyring availability probe did not complete";
69 : 0 : return false;
70 : : }
71 : :
72 [ + - ]: 1 : const auto error = job.error();
73 [ + - - + ]: 1 : const bool available = error == QKeychain::NoError ||
74 : : error == QKeychain::EntryNotFound;
75 [ + - ]: 1 : if (!available) {
76 [ + - + - : 2 : qCWarning(lcCredentialStore)
+ + ]
77 [ + - + - : 1 : << "Keyring availability probe failed:" << job.errorString();
+ - ]
78 : : }
79 : 1 : return available;
80 : 1 : }
81 : :
82 : 250 : void CredentialStore::readPassword(const QString &service,
83 : : const QString &accountName,
84 : : ReadCallback callback) {
85 [ + - ]: 250 : if (useFakeKeyring()) {
86 [ + - ]: 250 : const QString key = keyringKey(service, accountName);
87 : : // Deliver asynchronously to preserve the QtKeychain calling contract.
88 [ + - + - : 250 : QTimer::singleShot(0, this, [callback, key]() {
- - ]
89 : 250 : const bool found = fakeKeyring().contains(key);
90 [ + - ]: 250 : callback(found, fakeKeyring().value(key));
91 : 250 : });
92 : 250 : return;
93 : 250 : }
94 : : auto *job = new QKeychain::ReadPasswordJob(
95 [ # # # # : 0 : QStringLiteral("mailjd"), this);
# # ]
96 : 0 : job->setAutoDelete(true);
97 [ # # # # ]: 0 : job->setKey(keyringKey(service, accountName));
98 : :
99 : 0 : connect(job, &QKeychain::ReadPasswordJob::finished, this,
100 [ # # # # : 0 : [callback, service, accountName](QKeychain::Job *j) {
# # # # ]
101 : 0 : auto *readJob = qobject_cast<QKeychain::ReadPasswordJob *>(j);
102 [ # # # # : 0 : if (readJob && readJob->error() == QKeychain::NoError) {
# # ]
103 : : // QKeychain stores as QString — convert to QByteArray
104 [ # # # # ]: 0 : QByteArray pw = readJob->textData().toUtf8();
105 [ # # # # : 0 : qCDebug(lcCredentialStore)
# # ]
106 [ # # # # : 0 : << "Read password for" << service << accountName;
# # ]
107 [ # # ]: 0 : callback(true, pw);
108 : 0 : } else {
109 : 0 : QString errMsg = readJob ? readJob->errorString()
110 [ # # # # : 0 : : QStringLiteral("Unknown error");
# # # # ]
111 [ # # # # : 0 : qCWarning(lcCredentialStore)
# # ]
112 [ # # # # : 0 : << "Failed to read password for" << service << accountName
# # ]
113 [ # # # # ]: 0 : << ":" << errMsg;
114 [ # # ]: 0 : callback(false, QByteArray());
115 : 0 : }
116 : 0 : });
117 : :
118 : 0 : job->start();
119 : : }
120 : :
121 : 111 : void CredentialStore::writePassword(const QString &service,
122 : : const QString &accountName,
123 : : const QByteArray &password,
124 : : WriteCallback callback) {
125 [ + + ]: 111 : if (useFakeKeyring()) {
126 [ + - ]: 89 : const QString key = keyringKey(service, accountName);
127 [ + - + - : 89 : QTimer::singleShot(0, this, [callback, key, password]() {
- - - - ]
128 [ + + ]: 89 : if (qEnvironmentVariableIsSet("MAILJD_FAKE_KEYRING_FAIL_WRITES")) {
129 [ + - ]: 18 : callback(false, QStringLiteral("Injected fake keyring write failure"));
130 : 9 : return;
131 : : }
132 : 80 : fakeKeyring().insert(key, password);
133 [ + - ]: 80 : callback(true, QString());
134 : : });
135 : 89 : return;
136 : 89 : }
137 : : auto *job = new QKeychain::WritePasswordJob(
138 [ + - - + : 44 : QStringLiteral("mailjd"), this);
- - ]
139 : 22 : job->setAutoDelete(true);
140 [ + - + - ]: 22 : job->setKey(keyringKey(service, accountName));
141 [ + - + - ]: 22 : job->setTextData(QString::fromUtf8(password));
142 : :
143 : 22 : connect(job, &QKeychain::WritePasswordJob::finished, this,
144 [ + - + - : 44 : [callback, service, accountName](QKeychain::Job *j) {
- - - - ]
145 [ - + ]: 22 : if (j->error() == QKeychain::NoError) {
146 [ # # # # : 0 : qCInfo(lcCredentialStore)
# # ]
147 [ # # # # : 0 : << "Stored password for" << service << accountName;
# # ]
148 [ # # ]: 0 : callback(true, QString());
149 : : } else {
150 [ + - + - : 44 : qCWarning(lcCredentialStore)
+ + ]
151 [ + - + - : 22 : << "Failed to write password for" << service << accountName
+ - ]
152 [ + - + - : 22 : << ":" << j->errorString();
+ - ]
153 [ + - + - ]: 22 : callback(false, j->errorString());
154 : : }
155 : 22 : });
156 : :
157 : 22 : job->start();
158 : : }
159 : :
160 : 25 : void CredentialStore::deletePassword(const QString &service,
161 : : const QString &accountName,
162 : : DeleteCallback callback) {
163 [ + + ]: 25 : if (useFakeKeyring()) {
164 [ + - ]: 23 : const QString key = keyringKey(service, accountName);
165 [ + - + - : 23 : QTimer::singleShot(0, this, [callback, key]() {
- - ]
166 : 23 : callback(fakeKeyring().remove(key) > 0);
167 : 23 : });
168 : 23 : return;
169 : 23 : }
170 : : auto *job = new QKeychain::DeletePasswordJob(
171 [ + - - + : 4 : QStringLiteral("mailjd"), this);
- - ]
172 : 2 : job->setAutoDelete(true);
173 [ + - + - ]: 2 : job->setKey(keyringKey(service, accountName));
174 : :
175 : 2 : connect(job, &QKeychain::DeletePasswordJob::finished, this,
176 [ + - + - : 4 : [callback, service, accountName](QKeychain::Job *j) {
- - - - ]
177 [ - + ]: 2 : if (j->error() == QKeychain::NoError) {
178 [ # # # # : 0 : qCInfo(lcCredentialStore)
# # ]
179 [ # # # # : 0 : << "Deleted password for" << service << accountName;
# # ]
180 : 0 : callback(true);
181 : : } else {
182 [ + - + - : 4 : qCWarning(lcCredentialStore)
+ + ]
183 [ + - + - : 2 : << "Failed to delete password for" << service << accountName
+ - ]
184 [ + - + - : 2 : << ":" << j->errorString();
+ - ]
185 : 2 : callback(false);
186 : : }
187 : 2 : });
188 : :
189 : 2 : job->start();
190 : : }
|