QtPass Developer documentation
QtPass test coverage
Current view: top level - src/gpgkeystate.cpp (source / functions) Coverage Total Hit
Test: QtPass Lines: 100.0 % 89 89
Test Date: 2026-11-14 13:19:18 Functions: 100.0 % 8 8

            Line data    Source code
       1              : // SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
       2              : // SPDX-License-Identifier: GPL-3.0-or-later
       3              : /**
       4              :  * @file
       5              :  * @brief Implements the colon-listing parser: record classification, one
       6              :  * handler per record type, and primary-fingerprint lookup.
       7              :  */
       8              : #include "gpgkeystate.h"
       9              : #include "gpgkeystate_p.h"
      10              : 
      11              : #include "util.h"
      12              : 
      13              : #include <QDateTime>
      14              : #include <QRegularExpression>
      15              : 
      16              : /** @brief Fewest fields a record must have to be read at all. */
      17              : constexpr int GPG_MIN_FIELDS = 10;
      18              : /** @brief Field index of the validity letter (`u`, `f`, `r`, ...). */
      19              : constexpr int GPG_FIELD_VALIDITY = 1;
      20              : /** @brief Field index of the key ID. */
      21              : constexpr int GPG_FIELD_KEY_ID = 4;
      22              : /** @brief Field index of the creation time, in seconds since the epoch. */
      23              : constexpr int GPG_FIELD_CREATED = 5;
      24              : /** @brief Field index of the expiry time; empty for a key without one. */
      25              : constexpr int GPG_FIELD_EXPIRY = 6;
      26              : /** @brief Field index of the user ID. */
      27              : constexpr int GPG_FIELD_USERID = 9;
      28              : /**
      29              :  * @brief Field 9 of an `fpr` record: the fingerprint, in the slot the user
      30              :  * ID occupies in a `uid` record.
      31              :  */
      32              : constexpr int GPG_FIELD_FINGERPRINT = 9;
      33              : 
      34          624 : auto classifyRecord(const QString &record_type) -> GpgRecordType {
      35          624 :   if (record_type == "pub") {
      36              :     return GpgRecordType::Pub;
      37              :   }
      38          452 :   if (record_type == "sec") {
      39              :     return GpgRecordType::Sec;
      40              :   }
      41          427 :   if (record_type == "uid") {
      42              :     return GpgRecordType::Uid;
      43              :   }
      44          240 :   if (record_type == "fpr") {
      45              :     return GpgRecordType::Fpr;
      46              :   }
      47           41 :   if (record_type == "sub") {
      48              :     return GpgRecordType::Sub;
      49              :   }
      50           20 :   if (record_type == "ssb") {
      51              :     return GpgRecordType::Ssb;
      52              :   }
      53           13 :   if (record_type == "grp") {
      54            7 :     return GpgRecordType::Grp;
      55              :   }
      56              :   return GpgRecordType::Unknown;
      57              : }
      58              : 
      59          165 : void handlePubSecRecord(const QStringList &props, bool secret,
      60              :                         UserInfo &current_user) {
      61          165 :   if (props.size() < GPG_MIN_FIELDS) {
      62            2 :     return;
      63              :   }
      64          163 :   current_user.key_id = props[GPG_FIELD_KEY_ID];
      65          163 :   current_user.name = props[GPG_FIELD_USERID];
      66          163 :   if (!props[GPG_FIELD_VALIDITY].isEmpty()) {
      67          162 :     current_user.validity = props[GPG_FIELD_VALIDITY][0].toLatin1();
      68              :   }
      69              : 
      70              :   // An empty field (GnuPG's "no expiry") leaves the QDateTime null, so
      71              :   // callers distinguish "unset" from "set" with isValid() alone.
      72          163 :   bool okCreated = false;
      73              :   const qint64 createdSecs = props[GPG_FIELD_CREATED].toLongLong(&okCreated);
      74          163 :   if (okCreated) {
      75          162 :     current_user.created = QDateTime::fromSecsSinceEpoch(createdSecs);
      76              :   }
      77              : 
      78          163 :   bool okExpiry = false;
      79              :   const qint64 expirySecs = props[GPG_FIELD_EXPIRY].toLongLong(&okExpiry);
      80          163 :   if (okExpiry) {
      81            9 :     current_user.expiry = QDateTime::fromSecsSinceEpoch(expirySecs);
      82              :   }
      83              : 
      84          163 :   current_user.have_secret = secret;
      85              : }
      86              : 
      87              : /**
      88              :  * @brief Handle a uid record in GPG colon output
      89              :  * @param props The colon-split fields of the record
      90              :  * @param current_user UserInfo to populate with user name
      91              :  */
      92          159 : void handleUidRecord(const QStringList &props, UserInfo &current_user) {
      93          159 :   current_user.name = props[GPG_FIELD_USERID];
      94          159 : }
      95              : 
      96          160 : void handleFprRecord(const QStringList &props, UserInfo &current_user) {
      97          160 :   if (props.size() < GPG_MIN_FIELDS) {
      98              :     return;
      99              :   }
     100              :   // The key ID from the pub/sec record is the fingerprint's tail; take the
     101              :   // whole fingerprint, but only something shaped like one (40 hex for v4,
     102              :   // 64 for v5/v6): it becomes a recipient and a signing identity later.
     103              :   static const QRegularExpression shape(
     104          166 :       QStringLiteral("^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$"));
     105              :   const QString &fingerprint = props[GPG_FIELD_FINGERPRINT];
     106          326 :   if (!current_user.key_id.isEmpty() && shape.match(fingerprint).hasMatch() &&
     107          149 :       fingerprint.endsWith(current_user.key_id, Qt::CaseInsensitive)) {
     108          142 :     current_user.key_id = fingerprint;
     109              :   }
     110              : }
     111              : 
     112          159 : auto parseGpgColonOutput(const QString &output, bool secret)
     113              :     -> QList<UserInfo> {
     114          159 :   QList<UserInfo> users;
     115              : 
     116              :   const QStringList lines =
     117          159 :       output.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
     118              : 
     119          159 :   UserInfo current_user;
     120              : 
     121          656 :   for (const QString &key : lines) {
     122          497 :     QStringList props = key.split(':');
     123          497 :     if (props.size() < GPG_MIN_FIELDS) {
     124              :       continue;
     125              :     }
     126              : 
     127              :     const QString &record_type = props[0];
     128          492 :     const GpgRecordType type = classifyRecord(record_type);
     129              : 
     130          492 :     switch (type) {
     131              :     case GpgRecordType::Pub:
     132              :     case GpgRecordType::Sec:
     133          162 :       if (!current_user.key_id.isEmpty()) {
     134              :         users.append(current_user);
     135              :       }
     136          162 :       current_user = UserInfo();
     137          162 :       handlePubSecRecord(props, secret && (type == GpgRecordType::Sec),
     138              :                          current_user);
     139              :       break;
     140              :     case GpgRecordType::Uid:
     141          160 :       if (current_user.name.isEmpty()) {
     142          159 :         handleUidRecord(props, current_user);
     143              :       }
     144              :       break;
     145          153 :     case GpgRecordType::Fpr:
     146          153 :       handleFprRecord(props, current_user);
     147              :       break;
     148              :     default:
     149              :       break;
     150              :     }
     151              :   }
     152              : 
     153          159 :   if (!current_user.key_id.isEmpty()) {
     154              :     users.append(current_user);
     155              :   }
     156              : 
     157          159 :   return users;
     158          159 : }
     159              : 
     160           21 : auto primaryFingerprintOf(const QString &output, const QString &key)
     161              :     -> QString {
     162           21 :   QString wanted = key.trimmed().toUpper();
     163           21 :   if (wanted.startsWith(QLatin1String("0X"))) {
     164            1 :     wanted.remove(0, 2);
     165              :   }
     166           21 :   if (wanted.isEmpty()) {
     167            1 :     return {};
     168              :   }
     169           20 :   const bool isId = wanted.size() == 8 || wanted.size() == 16;
     170           20 :   QStringList holders;
     171           20 :   QString primary;
     172              :   bool expectPrimary = false;
     173           20 :   bool holds = false;
     174              :   // A pub or sub record carries the key's own ID: the fingerprint's tail
     175              :   // for v4 keys, its head for v5/v6. A short ID is the long one's tail.
     176           41 :   const auto namesKeyId = [&](const QStringList &props) {
     177           71 :     return isId && props[GPG_FIELD_KEY_ID].toUpper().endsWith(wanted);
     178           20 :   };
     179           51 :   const auto endBlock = [&] {
     180           51 :     if (holds && !primary.isEmpty() && !holders.contains(primary)) {
     181           17 :       holders << primary;
     182              :     }
     183           51 :     primary.clear();
     184           51 :     holds = false;
     185           71 :   };
     186              :   const QStringList lines =
     187           20 :       output.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
     188          142 :   for (const QString &line : lines) {
     189          122 :     const QStringList props = line.split(QLatin1Char(':'));
     190          122 :     if (props.size() < GPG_MIN_FIELDS) {
     191              :       continue;
     192              :     }
     193          113 :     switch (classifyRecord(props[0])) {
     194           31 :     case GpgRecordType::Pub:
     195           31 :       endBlock();
     196              :       expectPrimary = true;
     197           31 :       holds = holds || namesKeyId(props);
     198           31 :       break;
     199           13 :     case GpgRecordType::Sub:
     200              :       expectPrimary = false;
     201           13 :       holds = holds || namesKeyId(props);
     202           13 :       break;
     203           44 :     case GpgRecordType::Fpr:
     204           44 :       if (expectPrimary) {
     205           31 :         primary = props[GPG_FIELD_FINGERPRINT].toUpper();
     206              :         expectPrimary = false;
     207              :       }
     208           73 :       if (!isId && props[GPG_FIELD_FINGERPRINT].toUpper() == wanted) {
     209           10 :         holds = true;
     210              :       }
     211              :       break;
     212              :     default:
     213              :       break;
     214              :     }
     215              :   }
     216           20 :   endBlock();
     217           20 :   return holders.size() == 1 ? holders.first() : QString();
     218              : }
        

Generated by: LCOV version 2.0-1