QtPass 1.6.0
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
tst_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
3#include <QtTest>
4#include <algorithm>
5
9
10class tst_gpgkeystate : public QObject {
11 Q_OBJECT
12
13private Q_SLOTS:
14 void parseMultiKeyPublic();
15 void parseMultiKeyPublic_data();
16 void parseSecretKeys();
17 void parseSecretKeys_data();
18 void parseSingleKey();
19 void parseSingleKey_data();
20 void parseKeyRollover();
21 void parseKeyRollover_data();
22 void classifyRecordTypes();
23 void classifyRecordEmpty();
24 void handlePubSecEmptyFields();
25 void handlePubSecShortList();
26 void handleFprEdgeCases();
27};
28
29void tst_gpgkeystate::parseMultiKeyPublic() {
30 QFETCH(QString, input);
31 QFETCH(int, expectedCount);
32
33 const bool includeSecretKeys = false;
34 QList<UserInfo> result = parseGpgColonOutput(input, includeSecretKeys);
35
36 QVERIFY2(result.size() == expectedCount,
37 qPrintable(QString("Expected %1 keys, got %2")
38 .arg(expectedCount)
39 .arg(result.size())));
40
41 for (const UserInfo &user : result) {
42 QVERIFY2(!user.have_secret,
43 "Public keys should not have secret capability");
44 }
45
46 if (expectedCount > 0) {
47 QVERIFY2(!result.at(0).key_id.isEmpty(), "First key should have key_id");
48 QVERIFY2(!result.at(0).name.isEmpty(), "First key should have name");
49 }
50 if (expectedCount > 1) {
51 QVERIFY2(!result.at(1).key_id.isEmpty(), "Second key should have key_id");
52 QVERIFY2(!result.at(1).name.isEmpty(), "Second key should have name");
53 }
54}
55
56void tst_gpgkeystate::parseMultiKeyPublic_data() {
57 QTest::addColumn<QString>("input");
58 QTest::addColumn<int>("expectedCount");
59
60 QString input = R"(tru::1:1775005973:0:3:1:5
61pub:u:4096:1:31850CF72D9CDDE9:1774947438:::u:::escarESCA::::::23::0:
62fpr:::::::::13A47CCE2B3DA3AC340A274A31850CF72D9CDDE9:
63uid:u::::1774947438::CBF23008234AA5F88824CE76140F482FAE34923E::Anne Jan Brouwer <henk@annejan.com>::::::::::0:
64sub:u:4096:1:6DF67C6BAD8383CB:1774947438::::::esa::::::23:
65pub:f:4096:1:693A0AF3FA364E76:1775005968:::f:::escarESCA::::::23::0:
66fpr:::::::::4EF2550F79F4E9E68B09F71D693A0AF3FA364E76:
67uid:f::::1775005968::8AA011711F27F6E08DF71653718C299A13B323A0::Harrie de Bot <harrie@annejan.com>::::::::::0:)";
68
69 QTest::newRow("two-public-keys") << input << 2;
70}
71
72void tst_gpgkeystate::parseSecretKeys() {
73 QFETCH(QString, input);
74 QFETCH(int, expectedCount);
75 QFETCH(bool, expectHaveSecret);
76
77 const bool includeSecretKeys = true;
78 QList<UserInfo> result = parseGpgColonOutput(input, includeSecretKeys);
79
80 QVERIFY2(result.size() == expectedCount,
81 qPrintable(QString("Expected %1 keys, got %2")
82 .arg(expectedCount)
83 .arg(result.size())));
84
85 if (expectedCount > 0) {
86 QVERIFY(!result.isEmpty());
87 for (int i = 0; i < result.size(); ++i) {
88 const UserInfo &user = result.at(i);
89 QVERIFY2(
90 user.have_secret == expectHaveSecret,
91 qPrintable(QString("Key at index %1 has have_secret=%2, expected %3")
92 .arg(i)
93 .arg(user.have_secret)
94 .arg(expectHaveSecret)));
95 }
96 }
97}
98
99void tst_gpgkeystate::parseSecretKeys_data() {
100 QTest::addColumn<QString>("input");
101 QTest::addColumn<int>("expectedCount");
102 QTest::addColumn<bool>("expectHaveSecret");
103
104 QString input =
105 R"(sec:u:4096:1:31850CF72D9CDDE9:1774947438:::u:::escarESCA:::+:::23::0:
106fpr:::::::::13A47CCE2B3DA3AC340A274A31850CF72D9CDDE9:
107uid:u::::1774947438::CBF23008234AA5F88824CE76140F482FAE34923E::Anne Jan Brouwer <henk@annejan.com>::::::::::0:
108ssb:u:4096:1:6DF67C6BAD8383CB:1774947438::::::esa:::+:::23:)";
109
110 QTest::newRow("single-secret-key") << input << 1 << true;
111}
112
113void tst_gpgkeystate::parseSingleKey() {
114 QFETCH(QString, input);
115 QFETCH(int, expectedCount);
116 QFETCH(QString, expectedKeyId);
117
118 const bool includeSecretKeys = false;
119 QList<UserInfo> result = parseGpgColonOutput(input, includeSecretKeys);
120 QVERIFY2(result.size() == expectedCount,
121 qPrintable(QString("Expected %1 keys, got %2")
122 .arg(expectedCount)
123 .arg(result.size())));
124
125 if (!result.isEmpty()) {
126 QVERIFY2(!result.first().key_id.isEmpty(),
127 "Parsed key should have a key_id");
128 if (!expectedKeyId.isEmpty()) {
129 QVERIFY2(result.first().key_id == expectedKeyId,
130 qPrintable(QString("Expected key_id %1, got %2")
131 .arg(expectedKeyId)
132 .arg(result.first().key_id)));
133 }
134 }
135}
136
137void tst_gpgkeystate::parseSingleKey_data() {
138 QTest::addColumn<QString>("input");
139 QTest::addColumn<int>("expectedCount");
140 QTest::addColumn<QString>("expectedKeyId");
141
142 QTest::newRow("pub-only")
143 << "pub:u:4096:1:ABC123:1774947438:::u::::::23::0:" << 1 << "ABC123";
144 QTest::newRow("pub-with-fpr") << "pub:u:4096:1:ABC123:1774947438:::u::::::23:"
145 ":0:\nfpr:::::::::FINGERPRINT123456789:"
146 << 1 << "ABC123";
147}
148
149void tst_gpgkeystate::parseKeyRollover() {
150 QFETCH(QString, input);
151 QFETCH(int, expectedCount);
152
153 const bool includeSecretKeys = false;
154 QList<UserInfo> result = parseGpgColonOutput(input, includeSecretKeys);
155 QVERIFY2(result.size() == expectedCount,
156 qPrintable(QString("Expected %1 keys, got %2")
157 .arg(expectedCount)
158 .arg(result.size())));
159
160 auto containsKeyId = [&](const QString &keyId) {
161 return std::any_of(
162 result.cbegin(), result.cend(),
163 [&](const UserInfo &user) { return user.key_id == keyId; });
164 };
165
166 QVERIFY2(containsKeyId("AAA111"), "Expected AAA111 key to be parsed");
167 QVERIFY2(containsKeyId("BBB222"), "Expected BBB222 key to be parsed");
168 QVERIFY2(containsKeyId("CCC333"), "Expected CCC333 key to be parsed");
169}
170
171void tst_gpgkeystate::parseKeyRollover_data() {
172 QTest::addColumn<QString>("input");
173 QTest::addColumn<int>("expectedCount");
174
175 QString input = R"(pub:u:4096:1:AAA111:1774947438:::u::::
176fpr:::::::::AAA111FINGERPRINT:
177uid:u::::1774947438::NAME1::user1@test.com:::::::0:
178pub:u:4096:1:BBB222:1774947438:::u::::
179fpr:::::::::BBB222FINGERPRINT:
180uid:u::::1774947438::NAME2::user2@test.com:::::::0:
181pub:u:4096:1:CCC333:1774947438:::u::::
182fpr:::::::::CCC333FINGERPRINT:
183uid:u::::1774947438::NAME3::user3@test.com:::::::0:)";
184
185 QTest::newRow("three-keys-rollover") << input << 3;
186}
187
188void tst_gpgkeystate::classifyRecordTypes() {
189 QVERIFY2(classifyRecord("pub") == GpgRecordType::Pub,
190 "Should classify pub record");
191 QVERIFY2(classifyRecord("sec") == GpgRecordType::Sec,
192 "Should classify sec record");
193 QVERIFY2(classifyRecord("uid") == GpgRecordType::Uid,
194 "Should classify uid record");
195 QVERIFY2(classifyRecord("fpr") == GpgRecordType::Fpr,
196 "Should classify fpr record");
197 QVERIFY2(classifyRecord("sub") == GpgRecordType::Sub,
198 "Should classify sub record");
199 QVERIFY2(classifyRecord("ssb") == GpgRecordType::Ssb,
200 "Should classify ssb record");
201 QVERIFY2(classifyRecord("grp") == GpgRecordType::Grp,
202 "Should classify grp record");
203 QVERIFY2(classifyRecord("unknown") == GpgRecordType::Unknown,
204 "Should classify unknown record types as Unknown");
205}
206
207void tst_gpgkeystate::classifyRecordEmpty() {
209 "Should classify empty as Unknown");
210 QVERIFY2(classifyRecord("PUB") == GpgRecordType::Unknown,
211 "Should be case-sensitive");
212 QVERIFY2(classifyRecord("pubx") == GpgRecordType::Unknown,
213 "Should not match partial");
214}
215
216void tst_gpgkeystate::handlePubSecEmptyFields() {
217 UserInfo user;
218 QStringList props;
219 props << "pub"
220 << "" // validity empty
221 << "4096"
222 << "1"
223 << "keyId00001"
224 << "" // created empty
225 << "" // expiry empty
226 << ""
227 << ""
228 << "Test User"; // name
229
230 handlePubSecRecord(props, false, user);
231
232 QVERIFY2(user.key_id == "keyId00001", "Should parse key_id");
233 QVERIFY2(user.name == "Test User", "Should parse name");
234 QVERIFY2(user.validity == '-', "Empty validity should be dash");
235 QVERIFY2(!user.created.isValid(), "Empty created should be invalid");
236 QVERIFY2(!user.expiry.isValid(), "Empty expiry should be invalid");
237 QVERIFY2(!user.have_secret, "Should not have secret");
238}
239
240void tst_gpgkeystate::handlePubSecShortList() {
241 QStringList shortProps;
242 shortProps.append("pub");
243 shortProps.append("");
244 shortProps.append("4096");
245 shortProps.append("1");
246 shortProps.append(""); // 5: created
247
248 auto verifyShortListIgnored = [](const UserInfo &u, const char *ctx) {
249 QVERIFY2(u.key_id.isEmpty(), "Short list should be ignored");
250 QVERIFY2(u.name.isEmpty(), "Short list ignored: name empty");
251 QVERIFY2(u.validity == '-', "Short list ignored: default validity");
252 QVERIFY2(!u.created.isValid(), "Short list ignored: created invalid");
253 QVERIFY2(!u.expiry.isValid(), "Short list ignored: expiry invalid");
254 QVERIFY2(!u.have_secret, ctx);
255 };
256
257 UserInfo publicUser;
258 handlePubSecRecord(shortProps, false, publicUser);
259 verifyShortListIgnored(publicUser,
260 "Short list ignored: no secret for public");
261
262 UserInfo secretUser;
263 handlePubSecRecord(shortProps, true, secretUser);
264 verifyShortListIgnored(secretUser,
265 "Short list ignored: no secret for secret input");
266}
267
268void tst_gpgkeystate::handleFprEdgeCases() {
269 UserInfo user;
270 user.key_id = "id123";
271
272 QStringList emptyProps;
273 for (int i = 0; i < 10; ++i)
274 emptyProps.append("");
275 handleFprRecord(emptyProps, user);
276 QVERIFY2(user.key_id == "id123", "Empty fpr should not change key_id");
277
278 QStringList nonMatchingProps;
279 for (int i = 0; i < 10; ++i)
280 nonMatchingProps.append("");
281 nonMatchingProps[9] = "otherFingerprint";
282 handleFprRecord(nonMatchingProps, user);
283 QVERIFY2(user.key_id == "id123", "Non-matching fpr should not change key_id");
284
285 user.key_id = "id456";
286 QStringList matchingProps;
287 for (int i = 0; i < 10; ++i)
288 matchingProps.append("");
289 matchingProps[9] = "full fingerprint id456";
290 handleFprRecord(matchingProps, user);
291 QVERIFY2(user.key_id == "full fingerprint id456",
292 "fpr ending with key_id should update to full fingerprint");
293}
294
295QTEST_MAIN(tst_gpgkeystate)
296#include "tst_gpgkeystate.moc"
void handlePubSecRecord(const QStringList &props, bool secret, UserInfo &current_user)
Handle a pub or sec record in GPG colon output.
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse GPG –with-colons output into a list of UserInfo.
auto classifyRecord(const QString &record_type) -> GpgRecordType
Classify a GPG colon output record type.
void handleFprRecord(const QStringList &props, UserInfo &current_user)
Handle an fpr (fingerprint) record in GPG colon output.
Stores key info lines including validity, creation date and more.
Definition userinfo.h:13
bool have_secret
UserInfo::have_secret whether secret key is available (can decrypt with this key).
Definition userinfo.h:51
QString key_id
UserInfo::key_id hexadecimal representation of the GnuPG key identifier.
Definition userinfo.h:41
QDateTime created
UserInfo::created date/time when key was created.
Definition userinfo.h:66
QString name
UserInfo::name GPG user ID / full name.
Definition userinfo.h:36
char validity
UserInfo::validity GnuPG representation of validity http://git.gnupg.org/cgi-bin/gitweb....
Definition userinfo.h:46
QDateTime expiry
UserInfo::expiry date/time when key expires.
Definition userinfo.h:62