QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
gpgkeystate.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
8#include "gpgkeystate.h"
9#include "gpgkeystate_p.h"
10
11#include "util.h"
12
13#include <QDateTime>
14#include <QRegularExpression>
15
17constexpr int GPG_MIN_FIELDS = 10;
19constexpr int GPG_FIELD_VALIDITY = 1;
21constexpr int GPG_FIELD_KEY_ID = 4;
23constexpr int GPG_FIELD_CREATED = 5;
25constexpr int GPG_FIELD_EXPIRY = 6;
27constexpr int GPG_FIELD_USERID = 9;
32constexpr int GPG_FIELD_FINGERPRINT = 9;
33
34auto classifyRecord(const QString &record_type) -> GpgRecordType {
35 if (record_type == "pub") {
36 return GpgRecordType::Pub;
37 }
38 if (record_type == "sec") {
39 return GpgRecordType::Sec;
40 }
41 if (record_type == "uid") {
42 return GpgRecordType::Uid;
43 }
44 if (record_type == "fpr") {
45 return GpgRecordType::Fpr;
46 }
47 if (record_type == "sub") {
48 return GpgRecordType::Sub;
49 }
50 if (record_type == "ssb") {
51 return GpgRecordType::Ssb;
52 }
53 if (record_type == "grp") {
54 return GpgRecordType::Grp;
55 }
57}
58
59void handlePubSecRecord(const QStringList &props, bool secret,
60 UserInfo &current_user) {
61 if (props.size() < GPG_MIN_FIELDS) {
62 return;
63 }
64 current_user.key_id = props[GPG_FIELD_KEY_ID];
65 current_user.name = props[GPG_FIELD_USERID];
66 if (!props[GPG_FIELD_VALIDITY].isEmpty()) {
67 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 bool okCreated = false;
73 const qint64 createdSecs = props[GPG_FIELD_CREATED].toLongLong(&okCreated);
74 if (okCreated) {
75 current_user.created = QDateTime::fromSecsSinceEpoch(createdSecs);
76 }
77
78 bool okExpiry = false;
79 const qint64 expirySecs = props[GPG_FIELD_EXPIRY].toLongLong(&okExpiry);
80 if (okExpiry) {
81 current_user.expiry = QDateTime::fromSecsSinceEpoch(expirySecs);
82 }
83
84 current_user.have_secret = secret;
85}
86
92void handleUidRecord(const QStringList &props, UserInfo &current_user) {
93 current_user.name = props[GPG_FIELD_USERID];
94}
95
96void handleFprRecord(const QStringList &props, UserInfo &current_user) {
97 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 QStringLiteral("^(?:[0-9A-Fa-f]{40}|[0-9A-Fa-f]{64})$"));
105 const QString &fingerprint = props[GPG_FIELD_FINGERPRINT];
106 if (!current_user.key_id.isEmpty() && shape.match(fingerprint).hasMatch() &&
107 fingerprint.endsWith(current_user.key_id, Qt::CaseInsensitive)) {
108 current_user.key_id = fingerprint;
109 }
110}
111
112auto parseGpgColonOutput(const QString &output, bool secret)
113 -> QList<UserInfo> {
114 QList<UserInfo> users;
115
116 const QStringList lines =
117 output.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
118
119 UserInfo current_user;
120
121 for (const QString &key : lines) {
122 QStringList props = key.split(':');
123 if (props.size() < GPG_MIN_FIELDS) {
124 continue;
125 }
126
127 const QString &record_type = props[0];
128 const GpgRecordType type = classifyRecord(record_type);
129
130 switch (type) {
133 if (!current_user.key_id.isEmpty()) {
134 users.append(current_user);
135 }
136 current_user = UserInfo();
137 handlePubSecRecord(props, secret && (type == GpgRecordType::Sec),
138 current_user);
139 break;
141 if (current_user.name.isEmpty()) {
142 handleUidRecord(props, current_user);
143 }
144 break;
146 handleFprRecord(props, current_user);
147 break;
148 default:
149 break;
150 }
151 }
152
153 if (!current_user.key_id.isEmpty()) {
154 users.append(current_user);
155 }
156
157 return users;
158}
159
160auto primaryFingerprintOf(const QString &output, const QString &key)
161 -> QString {
162 QString wanted = key.trimmed().toUpper();
163 if (wanted.startsWith(QLatin1String("0X"))) {
164 wanted.remove(0, 2);
165 }
166 if (wanted.isEmpty()) {
167 return {};
168 }
169 const bool isId = wanted.size() == 8 || wanted.size() == 16;
170 QStringList holders;
171 QString primary;
172 bool expectPrimary = false;
173 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 const auto namesKeyId = [&](const QStringList &props) {
177 return isId && props[GPG_FIELD_KEY_ID].toUpper().endsWith(wanted);
178 };
179 const auto endBlock = [&] {
180 if (holds && !primary.isEmpty() && !holders.contains(primary)) {
181 holders << primary;
182 }
183 primary.clear();
184 holds = false;
185 };
186 const QStringList lines =
187 output.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
188 for (const QString &line : lines) {
189 const QStringList props = line.split(QLatin1Char(':'));
190 if (props.size() < GPG_MIN_FIELDS) {
191 continue;
192 }
193 switch (classifyRecord(props[0])) {
195 endBlock();
196 expectPrimary = true;
197 holds = holds || namesKeyId(props);
198 break;
200 expectPrimary = false;
201 holds = holds || namesKeyId(props);
202 break;
204 if (expectPrimary) {
205 primary = props[GPG_FIELD_FINGERPRINT].toUpper();
206 expectPrimary = false;
207 }
208 if (!isId && props[GPG_FIELD_FINGERPRINT].toUpper() == wanted) {
209 holds = true;
210 }
211 break;
212 default:
213 break;
214 }
215 }
216 endBlock();
217 return holders.size() == 1 ? holders.first() : QString();
218}
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
Definition util.cpp:656
void handlePubSecRecord(const QStringList &props, bool secret, UserInfo &current_user)
Start a key from a pub or sec record: key ID, the user ID the record may carry, validity,...
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse a gpg colon listing into one UserInfo per key.
auto primaryFingerprintOf(const QString &output, const QString &key) -> QString
The primary key fingerprint of the key that key names.
constexpr int GPG_FIELD_KEY_ID
Field index of the key ID.
constexpr int GPG_FIELD_VALIDITY
Field index of the validity letter (u, f, r, ...).
constexpr int GPG_FIELD_EXPIRY
Field index of the expiry time; empty for a key without one.
constexpr int GPG_FIELD_CREATED
Field index of the creation time, in seconds since the epoch.
constexpr int GPG_MIN_FIELDS
Fewest fields a record must have to be read at all.
auto classifyRecord(const QString &record_type) -> GpgRecordType
Classify a record of a gpg colon listing by its first field.
void handleFprRecord(const QStringList &props, UserInfo &current_user)
Replace the key ID of the key being built with its fingerprint.
void handleUidRecord(const QStringList &props, UserInfo &current_user)
Handle a uid record in GPG colon output.
constexpr int GPG_FIELD_USERID
Field index of the user ID.
constexpr int GPG_FIELD_FINGERPRINT
Field 9 of an fpr record: the fingerprint, in the slot the user ID occupies in a uid record.
Parsing of gpg --with-colons key listings into UserInfo records.
GpgRecordType
The record types of a gpg --with-colons listing that the parser tells apart; the first field of each ...
Definition gpgkeystate.h:21
Record handlers of the gpg colon-listing parser, declared separately only so the unit tests can call ...
Stores GPG key info including validity, creation date, and more.
Definition userinfo.h:19
bool have_secret
Whether secret key is available (can decrypt with this key).
Definition userinfo.h:61
QString key_id
Hexadecimal representation of the GnuPG key identifier.
Definition userinfo.h:52
QDateTime created
Date/time when key was created.
Definition userinfo.h:73
QString name
GPG user ID / full name.
Definition userinfo.h:48
UserInfo()
Construct a UserInfo with default (invalid/disabled) state.
Definition userinfo.h:23
char validity
GnuPG representation of validity. http://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg....
Definition userinfo.h:57
QDateTime expiry
Date/time when key expires.
Definition userinfo.h:69
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...