MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - data - DavCredentials.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 89.7 % 194 174
Test Date: 2026-07-27 17:53:44 Functions: 100.0 % 16 16
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 54.0 % 378 204

             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
        

Generated by: LCOV version 2.0-1