Branch data Line data Source code
1 : : #include "DavCredentials.h"
2 : :
3 : : #include "CredentialStore.h"
4 : :
5 : : #include <QCryptographicHash>
6 : : #include <QEventLoop>
7 : : #include <QSettings>
8 : : #include <QTimer>
9 : :
10 : : namespace {
11 : :
12 : : constexpr int kKeyringTimeoutMs = 30000;
13 : : constexpr auto kDavService = "carddav";
14 : :
15 : 221 : QString digestMaterial(const QString &accountId, const QString &origin,
16 : : const QString &username) {
17 [ + - + - ]: 442 : const QString material = accountId.trimmed() + QLatin1Char('\n') +
18 [ + - + - ]: 663 : origin + QLatin1Char('\n') +
19 [ + - + - ]: 663 : username.trimmed();
20 : : return QString::fromLatin1(
21 [ + - + - ]: 442 : QCryptographicHash::hash(material.toUtf8(),
22 : : QCryptographicHash::Sha256)
23 [ + - + - ]: 442 : .toHex());
24 : 221 : }
25 : :
26 : 87 : QString legacyAccountName(const QString &accountId, const QString &serverUrl,
27 : : const QString &username) {
28 [ + - ]: 174 : const QString id = accountId.trimmed().isEmpty()
29 [ + + - - ]: 87 : ? QStringLiteral("unknown")
30 [ + + + - ]: 91 : : accountId.trimmed();
31 : : // Pre-2026-07 endpoint-bound format. Keep this exact raw-URL digest solely
32 : : // for a safe one-way migration; unlike the later account-ID-only format it
33 : : // cannot move a secret to another endpoint.
34 : 174 : return QStringLiteral("%1/%2").arg(
35 [ + - + - : 174 : id, digestMaterial(accountId, serverUrl.trimmed(), username));
+ - ]
36 : 87 : }
37 : :
38 : 193 : QByteArray readPasswordKeyBlocking(const QString &accountName, bool &success,
39 : : QString *error) {
40 : 193 : success = false;
41 : :
42 [ + - ]: 193 : CredentialStore store;
43 [ + - ]: 193 : QEventLoop loop;
44 [ + - ]: 193 : QTimer timeout;
45 [ + - ]: 193 : timeout.setSingleShot(true);
46 : :
47 : 193 : bool completed = false;
48 : 193 : QByteArray password;
49 : 193 : QString readError;
50 : :
51 [ + - ]: 193 : QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
52 [ # # ]: 0 : if (completed)
53 : 0 : return;
54 : 0 : completed = true;
55 : 0 : readError = QStringLiteral("Timed out reading DAV password from keyring");
56 : 0 : loop.quit();
57 : : });
58 : :
59 [ + - + - ]: 193 : store.readPassword(QString::fromLatin1(kDavService), accountName,
60 [ + - ]: 193 : [&](bool ok, const QByteArray &pw) {
61 [ - + ]: 193 : if (completed)
62 : 0 : return;
63 : 193 : completed = true;
64 : 193 : success = ok;
65 : 193 : password = pw;
66 : 193 : loop.quit();
67 : : });
68 : :
69 [ + - ]: 193 : timeout.start(kKeyringTimeoutMs);
70 [ + - ]: 193 : loop.exec();
71 : :
72 [ + + + + ]: 193 : if (!success && error) {
73 : 47 : *error = readError.isEmpty()
74 [ + - + - ]: 141 : ? QStringLiteral("Could not read DAV password from keyring")
75 : 47 : : readError;
76 : : }
77 [ + + ]: 386 : return success ? password : QByteArray();
78 : 193 : }
79 : :
80 : 31 : bool writePasswordKeyBlocking(const QString &accountName,
81 : : const QByteArray &password, QString *error) {
82 [ + - ]: 31 : CredentialStore store;
83 [ + - ]: 31 : QEventLoop loop;
84 [ + - ]: 31 : QTimer timeout;
85 [ + - ]: 31 : timeout.setSingleShot(true);
86 : :
87 : 31 : bool completed = false;
88 : 31 : bool success = false;
89 : 31 : QString writeError;
90 : :
91 [ + - ]: 31 : QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
92 [ # # ]: 0 : if (completed)
93 : 0 : return;
94 : 0 : completed = true;
95 : 0 : writeError = QStringLiteral("Timed out writing DAV password to keyring");
96 : 0 : loop.quit();
97 : : });
98 : :
99 [ + - + - ]: 31 : store.writePassword(QString::fromLatin1(kDavService), accountName, password,
100 [ + - ]: 31 : [&](bool ok, const QString &err) {
101 [ - + ]: 31 : if (completed)
102 : 0 : return;
103 : 31 : completed = true;
104 : 31 : success = ok;
105 : 31 : writeError = err;
106 : 31 : loop.quit();
107 : : });
108 : :
109 [ + - ]: 31 : timeout.start(kKeyringTimeoutMs);
110 [ + - ]: 31 : loop.exec();
111 : :
112 [ + + + + ]: 31 : if (!success && error) {
113 : 5 : *error = writeError.isEmpty()
114 [ - + - + ]: 10 : ? QStringLiteral("Could not write DAV password to keyring")
115 : 5 : : writeError;
116 : : }
117 : 31 : return success;
118 : 31 : }
119 : :
120 : 17 : bool deletePasswordKeyBlocking(const QString &accountName) {
121 [ + - ]: 17 : CredentialStore store;
122 [ + - ]: 17 : QEventLoop loop;
123 [ + - ]: 17 : QTimer timeout;
124 [ + - ]: 17 : timeout.setSingleShot(true);
125 : :
126 : 17 : bool completed = false;
127 : 17 : bool success = false;
128 : :
129 [ + - ]: 17 : QObject::connect(&timeout, &QTimer::timeout, &loop, [&]() {
130 [ # # ]: 0 : if (completed)
131 : 0 : return;
132 : 0 : completed = true;
133 : 0 : loop.quit();
134 : : });
135 : :
136 [ + - + - ]: 17 : store.deletePassword(QString::fromLatin1(kDavService), accountName,
137 [ + - ]: 17 : [&](bool ok) {
138 [ - + ]: 17 : if (completed)
139 : 0 : return;
140 : 17 : completed = true;
141 : 17 : success = ok;
142 : 17 : loop.quit();
143 : : });
144 : :
145 [ + - ]: 17 : timeout.start(kKeyringTimeoutMs);
146 [ + - ]: 17 : loop.exec();
147 : 17 : return success;
148 : 17 : }
149 : :
150 : 75 : bool requiresLocalAuthorization(const QString &accountId,
151 : : const QString &serverUrl,
152 : : const QString &username) {
153 [ + - ]: 75 : QSettings settings;
154 [ + - ]: 75 : const int count = settings.beginReadArray(QStringLiteral("carddav/accounts"));
155 : 75 : bool required = false;
156 [ + + ]: 76 : for (int i = 0; i < count; ++i) {
157 [ + - ]: 49 : settings.setArrayIndex(i);
158 [ + - + - : 98 : if (settings.value(QStringLiteral("id")).toString().trimmed() !=
+ - ]
159 [ + - + + ]: 147 : accountId.trimmed()) {
160 : 1 : continue;
161 : : }
162 : : const bool sameIdentity =
163 [ + - - - ]: 96 : DavCredentials::canonicalOrigin(
164 [ + - + - : 96 : settings.value(QStringLiteral("serverUrl")).toString()) ==
+ - + - +
- + - - -
- - - - -
- ]
165 [ + - + + : 238 : DavCredentials::canonicalOrigin(serverUrl) &&
+ - + - -
- ]
166 [ + - + - : 95 : settings.value(QStringLiteral("username")).toString().trimmed() ==
+ - + + +
+ + + + +
- - - - -
- - - -
- ]
167 [ + - + + : 143 : username.trimmed();
+ + + - -
- ]
168 [ + + ]: 95 : required = sameIdentity &&
169 [ + - + + : 142 : settings.value(QStringLiteral("requiresLocalAuthorization"),
+ + + + +
+ - - - -
- - - - ]
170 : : false)
171 [ + - + + ]: 47 : .toBool();
172 : 48 : break;
173 : : }
174 [ + - ]: 75 : settings.endArray();
175 : 75 : return required;
176 : 75 : }
177 : :
178 : : } // namespace
179 : :
180 : : namespace DavCredentials {
181 : :
182 : 134 : QString accountName(const QString &accountId, const QString &serverUrl,
183 : : const QString &username) {
184 [ + - ]: 134 : const QString id = accountId.trimmed().isEmpty()
185 [ + + - - ]: 134 : ? QStringLiteral("unknown")
186 [ + + + - ]: 141 : : accountId.trimmed();
187 [ + - ]: 134 : const QString canonical = canonicalOrigin(serverUrl);
188 : 134 : const QString origin = canonical.isEmpty()
189 [ + - + + : 144 : ? QStringLiteral("invalid:") + serverUrl.trimmed()
+ + + + -
- - - -
- ]
190 [ + + + - ]: 144 : : canonical;
191 : : const QString digest =
192 [ + - + - ]: 134 : digestMaterial(accountId, origin, username.trimmed());
193 [ + - ]: 268 : return QStringLiteral("%1/v2/%2").arg(id, digest);
194 : 134 : }
195 : :
196 : 75 : QByteArray readPasswordBlocking(const QString &accountId,
197 : : const QString &serverUrl,
198 : : const QString &username,
199 : : QString *error) {
200 [ + - + + ]: 75 : if (requiresLocalAuthorization(accountId, serverUrl, username)) {
201 [ + - ]: 1 : if (error)
202 : 1 : *error = QStringLiteral("DAV account requires local authorization");
203 : 1 : return {};
204 : : }
205 : :
206 : 74 : bool success = false;
207 [ + - ]: 74 : const QString stableName = accountName(accountId, serverUrl, username);
208 [ + - ]: 74 : QByteArray password = readPasswordKeyBlocking(stableName, success, error);
209 [ + + ]: 74 : if (success)
210 : 21 : return password;
211 : :
212 [ + - ]: 53 : const QString legacyName = legacyAccountName(accountId, serverUrl, username);
213 [ - + ]: 53 : if (legacyName == stableName)
214 : 0 : return {};
215 : :
216 [ + - ]: 53 : password = readPasswordKeyBlocking(legacyName, success, nullptr);
217 [ + + ]: 53 : if (!success)
218 : 50 : return {};
219 : :
220 : 3 : QString migrationError;
221 [ + - + + ]: 3 : if (writePasswordKeyBlocking(stableName, password, &migrationError))
222 [ + - ]: 2 : deletePasswordKeyBlocking(legacyName);
223 [ + + ]: 3 : if (error)
224 : 2 : error->clear();
225 : 3 : return password;
226 : 74 : }
227 : :
228 : 29 : bool writePasswordBlocking(const QString &accountId, const QString &serverUrl,
229 : : const QString &username,
230 : : const QByteArray &password,
231 : : QString *error) {
232 [ + + ]: 29 : if (password.isEmpty())
233 : 1 : return true;
234 [ + - ]: 28 : const QString boundName = accountName(accountId, serverUrl, username);
235 [ + - + + ]: 28 : if (!writePasswordKeyBlocking(boundName, password, error))
236 : 5 : return false;
237 : :
238 : : // SEC-2026-07-21-24: Delete ALL legacy key forms after an explicit write,
239 : : // not just the ID-only key. The endpoint-bound legacy key may still hold
240 : : // an OLD password (e.g. after re-authorization against a changed endpoint);
241 : : // leaving it orphaned creates a valid stale credential in the keyring.
242 : : const QString legacyName =
243 [ + - ]: 23 : legacyAccountName(accountId, serverUrl, username);
244 [ + - ]: 23 : if (legacyName != boundName) {
245 : 23 : bool legacyExists = false;
246 [ + - ]: 23 : readPasswordKeyBlocking(legacyName, legacyExists, nullptr);
247 [ - + ]: 23 : if (legacyExists)
248 [ # # ]: 0 : deletePasswordKeyBlocking(legacyName);
249 : : }
250 : :
251 [ + - ]: 23 : const QString legacyStableName = accountId.trimmed();
252 [ + + + - : 45 : if (!legacyStableName.isEmpty() && legacyStableName != boundName &&
+ + ]
253 [ + - ]: 22 : legacyStableName != legacyName) {
254 : 22 : bool legacyStableExists = false;
255 [ + - ]: 22 : readPasswordKeyBlocking(legacyStableName, legacyStableExists, nullptr);
256 [ - + ]: 22 : if (legacyStableExists)
257 [ # # ]: 0 : deletePasswordKeyBlocking(legacyStableName);
258 : : }
259 : 23 : return true;
260 : 28 : }
261 : :
262 : 11 : bool deletePasswordBlocking(const QString &accountId, const QString &serverUrl,
263 : : const QString &username,
264 : : QString *error) {
265 [ + - ]: 11 : const QString stableName = accountName(accountId, serverUrl, username);
266 [ + - ]: 11 : bool success = deletePasswordKeyBlocking(stableName);
267 : :
268 : : const QString legacyName =
269 [ + - ]: 11 : legacyAccountName(accountId, serverUrl, username);
270 [ + - ]: 11 : if (legacyName != stableName) {
271 : 11 : bool legacyExists = false;
272 [ + - ]: 11 : readPasswordKeyBlocking(legacyName, legacyExists, nullptr);
273 [ + + ]: 11 : if (legacyExists)
274 [ + - - + : 2 : success = deletePasswordKeyBlocking(legacyName) || success;
- - ]
275 : : }
276 : :
277 [ + - ]: 11 : const QString legacyStableName = accountId.trimmed();
278 [ + + + - : 21 : if (!legacyStableName.isEmpty() && legacyStableName != stableName &&
+ + ]
279 [ + - ]: 10 : legacyStableName != legacyName) {
280 : 10 : bool legacyStableExists = false;
281 [ + - ]: 10 : readPasswordKeyBlocking(legacyStableName, legacyStableExists, nullptr);
282 [ + + ]: 10 : if (legacyStableExists)
283 [ + - - + : 2 : success = deletePasswordKeyBlocking(legacyStableName) || success;
- - ]
284 : : }
285 : :
286 [ + + + + ]: 11 : if (!success && error)
287 : 2 : *error = QStringLiteral("Could not delete DAV password from keyring");
288 : 11 : return success;
289 : 11 : }
290 : :
291 : : } // namespace DavCredentials
|