/src/neomutt/ncrypt/gnupgparse.c
Line | Count | Source (jump to first uncovered line) |
1 | | /** |
2 | | * @file |
3 | | * Parse the output of CLI PGP program |
4 | | * |
5 | | * @authors |
6 | | * Copyright (C) 2017-2021 Pietro Cerutti <gahr@gahr.ch> |
7 | | * Copyright (C) 2017-2023 Richard Russon <rich@flatcap.org> |
8 | | * Copyright (C) 2019 Ian Zimmerman <itz@no-use.mooo.com> |
9 | | * |
10 | | * @copyright |
11 | | * This program is free software: you can redistribute it and/or modify it under |
12 | | * the terms of the GNU General Public License as published by the Free Software |
13 | | * Foundation, either version 2 of the License, or (at your option) any later |
14 | | * version. |
15 | | * |
16 | | * This program is distributed in the hope that it will be useful, but WITHOUT |
17 | | * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
18 | | * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more |
19 | | * details. |
20 | | * |
21 | | * You should have received a copy of the GNU General Public License along with |
22 | | * this program. If not, see <http://www.gnu.org/licenses/>. |
23 | | */ |
24 | | |
25 | | /** |
26 | | * @page crypt_gnupg Parse the output of CLI PGP program |
27 | | * |
28 | | * Parse the output of CLI PGP program |
29 | | * |
30 | | * @note This code used to be the parser for GnuPG's output. |
31 | | * |
32 | | * Nowadays, we are using an external pubring lister with PGP which mimics |
33 | | * gpg's output format. |
34 | | */ |
35 | | |
36 | | #include "config.h" |
37 | | #include <ctype.h> |
38 | | #include <fcntl.h> |
39 | | #include <iconv.h> |
40 | | #include <stdbool.h> |
41 | | #include <stdio.h> |
42 | | #include <string.h> |
43 | | #include <sys/types.h> |
44 | | #include <time.h> |
45 | | #include <unistd.h> |
46 | | #include "mutt/lib.h" |
47 | | #include "config/lib.h" |
48 | | #include "email/lib.h" |
49 | | #include "core/lib.h" |
50 | | #include "gnupgparse.h" |
51 | | #include "lib.h" |
52 | | #include "pgpinvoke.h" |
53 | | #include "pgpkey.h" |
54 | | #ifdef CRYPT_BACKEND_CLASSIC_PGP |
55 | | #include "pgplib.h" |
56 | | #endif |
57 | | |
58 | | /**************** |
59 | | * Read the GNUPG keys. For now we read the complete keyring by |
60 | | * calling gnupg in a special mode. |
61 | | * |
62 | | * The output format of gpgm is colon delimited with these fields: |
63 | | * - record type ("pub","uid","sig","rev" etc.) |
64 | | * - trust info |
65 | | * - key length |
66 | | * - pubkey algo |
67 | | * - 16 hex digits with the long keyid |
68 | | * - timestamp (1998-02-28) |
69 | | * - Local id |
70 | | * - ownertrust |
71 | | * - name |
72 | | * - signature class |
73 | | */ |
74 | | |
75 | | /// Cached copy of $charset |
76 | | static char *Charset = NULL; |
77 | | |
78 | | /** |
79 | | * fix_uid - Decode backslash-escaped user ids (in place) |
80 | | * @param uid String to decode |
81 | | */ |
82 | | static void fix_uid(char *uid) |
83 | 0 | { |
84 | 0 | char *s = NULL, *d = NULL; |
85 | 0 | iconv_t cd = ICONV_T_INVALID; |
86 | |
|
87 | 0 | for (s = uid, d = uid; *s;) |
88 | 0 | { |
89 | 0 | if ((s[0] == '\\') && (s[1] == 'x') && isxdigit((unsigned char) s[2]) && |
90 | 0 | isxdigit((unsigned char) s[3])) |
91 | 0 | { |
92 | 0 | *d++ = (hexval(s[2]) << 4) | hexval(s[3]); |
93 | 0 | s += 4; |
94 | 0 | } |
95 | 0 | else |
96 | 0 | { |
97 | 0 | *d++ = *s++; |
98 | 0 | } |
99 | 0 | } |
100 | 0 | *d = '\0'; |
101 | |
|
102 | 0 | if (Charset && iconv_t_valid(cd = mutt_ch_iconv_open(Charset, "utf-8", MUTT_ICONV_NO_FLAGS))) |
103 | 0 | { |
104 | 0 | int n = s - uid + 1; /* chars available in original buffer */ |
105 | |
|
106 | 0 | char *buf = MUTT_MEM_MALLOC(n + 1, char); |
107 | 0 | const char *ib = uid; |
108 | 0 | size_t ibl = d - uid + 1; |
109 | 0 | char *ob = buf; |
110 | 0 | size_t obl = n; |
111 | 0 | iconv(cd, (ICONV_CONST char **) &ib, &ibl, &ob, &obl); |
112 | 0 | if (ibl == 0) |
113 | 0 | { |
114 | 0 | if (ob - buf < n) |
115 | 0 | { |
116 | 0 | memcpy(uid, buf, ob - buf); |
117 | 0 | uid[ob - buf] = '\0'; |
118 | 0 | } |
119 | 0 | else if ((n >= 0) && ((ob - buf) == n) && (buf[n] = 0, (strlen(buf) < (size_t) n))) |
120 | 0 | { |
121 | 0 | memcpy(uid, buf, n); |
122 | 0 | } |
123 | 0 | } |
124 | 0 | FREE(&buf); |
125 | 0 | } |
126 | 0 | } |
127 | | |
128 | | /** |
129 | | * parse_pub_line - Parse the 'pub' line from the pgp output |
130 | | * @param[in] buf Buffer containing string to parse |
131 | | * @param[out] is_subkey Is this a subkey of another key? |
132 | | * @param[in] k Key to from which to merge info (optional) |
133 | | * @retval ptr PgpKeyInfo containing the (merged) results |
134 | | * @retval NULL Error |
135 | | */ |
136 | | static struct PgpKeyInfo *parse_pub_line(char *buf, bool *is_subkey, struct PgpKeyInfo *k) |
137 | 0 | { |
138 | 0 | struct PgpUid *uid = NULL; |
139 | 0 | int field = 0; |
140 | 0 | bool is_uid = false; |
141 | 0 | bool is_pub = false; |
142 | 0 | bool is_fpr = false; |
143 | 0 | char *pend = NULL, *p = NULL; |
144 | 0 | int trust = 0; |
145 | 0 | KeyFlags flags = KEYFLAG_NO_FLAGS; |
146 | 0 | char tstr[11] = { 0 }; |
147 | |
|
148 | 0 | *is_subkey = false; |
149 | 0 | if (*buf == '\0') |
150 | 0 | return NULL; |
151 | | |
152 | | /* if we're given a key, merge our parsing results, else |
153 | | * start with a fresh one to work with so that we don't |
154 | | * mess up the real key in case we find parsing errors. */ |
155 | 0 | struct PgpKeyInfo tmp = { 0 }; |
156 | 0 | if (k) |
157 | 0 | memcpy(&tmp, k, sizeof(tmp)); |
158 | |
|
159 | 0 | mutt_debug(LL_DEBUG2, "buf = '%s'\n", buf); |
160 | |
|
161 | 0 | const bool c_pgp_ignore_subkeys = cs_subset_bool(NeoMutt->sub, "pgp_ignore_subkeys"); |
162 | 0 | for (p = buf; p; p = pend) |
163 | 0 | { |
164 | 0 | pend = strchr(p, ':'); |
165 | 0 | if (pend) |
166 | 0 | *pend++ = 0; |
167 | 0 | field++; |
168 | 0 | if ((*p == '\0') && (field != 1) && (field != 10)) |
169 | 0 | continue; |
170 | | |
171 | 0 | if (is_fpr && (field != 10)) |
172 | 0 | continue; |
173 | | |
174 | 0 | switch (field) |
175 | 0 | { |
176 | 0 | case 1: /* record type */ |
177 | 0 | { |
178 | 0 | mutt_debug(LL_DEBUG2, "record type: %s\n", p); |
179 | |
|
180 | 0 | if (mutt_str_equal(p, "pub")) |
181 | 0 | is_pub = true; |
182 | 0 | else if (mutt_str_equal(p, "sub")) |
183 | 0 | *is_subkey = true; |
184 | 0 | else if (mutt_str_equal(p, "sec")) |
185 | 0 | ; // do nothing |
186 | 0 | else if (mutt_str_equal(p, "ssb")) |
187 | 0 | *is_subkey = true; |
188 | 0 | else if (mutt_str_equal(p, "uid")) |
189 | 0 | is_uid = true; |
190 | 0 | else if (mutt_str_equal(p, "fpr")) |
191 | 0 | is_fpr = true; |
192 | 0 | else |
193 | 0 | return NULL; |
194 | | |
195 | 0 | if (!(is_uid || is_fpr || (*is_subkey && c_pgp_ignore_subkeys))) |
196 | 0 | memset(&tmp, 0, sizeof(tmp)); |
197 | |
|
198 | 0 | break; |
199 | 0 | } |
200 | 0 | case 2: /* trust info */ |
201 | 0 | { |
202 | 0 | mutt_debug(LL_DEBUG2, "trust info: %s\n", p); |
203 | |
|
204 | 0 | switch (*p) |
205 | 0 | { /* look only at the first letter */ |
206 | 0 | case 'd': |
207 | 0 | flags |= KEYFLAG_DISABLED; |
208 | 0 | break; |
209 | 0 | case 'e': |
210 | 0 | flags |= KEYFLAG_EXPIRED; |
211 | 0 | break; |
212 | 0 | case 'f': |
213 | 0 | trust = 3; |
214 | 0 | break; |
215 | 0 | case 'm': |
216 | 0 | trust = 2; |
217 | 0 | break; |
218 | 0 | case 'n': |
219 | 0 | trust = 1; |
220 | 0 | break; |
221 | 0 | case 'r': |
222 | 0 | flags |= KEYFLAG_REVOKED; |
223 | 0 | break; |
224 | 0 | case 'u': |
225 | 0 | trust = 3; |
226 | 0 | break; |
227 | 0 | } |
228 | | |
229 | 0 | if (!is_uid && !(*is_subkey && c_pgp_ignore_subkeys)) |
230 | 0 | tmp.flags |= flags; |
231 | |
|
232 | 0 | break; |
233 | 0 | } |
234 | 0 | case 3: /* key length */ |
235 | 0 | { |
236 | 0 | mutt_debug(LL_DEBUG2, "key len: %s\n", p); |
237 | |
|
238 | 0 | if (!(*is_subkey && c_pgp_ignore_subkeys) && !mutt_str_atos_full(p, &tmp.keylen)) |
239 | 0 | { |
240 | 0 | goto bail; |
241 | 0 | } |
242 | 0 | break; |
243 | 0 | } |
244 | 0 | case 4: /* pubkey algo */ |
245 | 0 | { |
246 | 0 | mutt_debug(LL_DEBUG2, "pubkey algorithm: %s\n", p); |
247 | |
|
248 | 0 | if (!(*is_subkey && c_pgp_ignore_subkeys)) |
249 | 0 | { |
250 | 0 | int x = 0; |
251 | 0 | if (!mutt_str_atoi_full(p, &x)) |
252 | 0 | goto bail; |
253 | 0 | tmp.numalg = x; |
254 | 0 | tmp.algorithm = pgp_pkalgbytype(x); |
255 | 0 | } |
256 | 0 | break; |
257 | 0 | } |
258 | 0 | case 5: /* 16 hex digits with the long keyid. */ |
259 | 0 | { |
260 | 0 | mutt_debug(LL_DEBUG2, "key id: %s\n", p); |
261 | |
|
262 | 0 | if (!(*is_subkey && c_pgp_ignore_subkeys)) |
263 | 0 | mutt_str_replace(&tmp.keyid, p); |
264 | 0 | break; |
265 | 0 | } |
266 | 0 | case 6: /* timestamp (1998-02-28) */ |
267 | 0 | { |
268 | 0 | mutt_debug(LL_DEBUG2, "time stamp: %s\n", p); |
269 | |
|
270 | 0 | if (strchr(p, '-')) /* gpg pre-2.0.10 used format (yyyy-mm-dd) */ |
271 | 0 | { |
272 | 0 | struct tm time = { 0 }; |
273 | |
|
274 | 0 | time.tm_sec = 0; |
275 | 0 | time.tm_min = 0; |
276 | 0 | time.tm_hour = 12; |
277 | 0 | strncpy(tstr, p, 11); |
278 | 0 | tstr[4] = '\0'; |
279 | 0 | tstr[7] = '\0'; |
280 | 0 | if (!mutt_str_atoi_full(tstr, &time.tm_year)) |
281 | 0 | { |
282 | 0 | p = tstr; |
283 | 0 | goto bail; |
284 | 0 | } |
285 | 0 | time.tm_year -= 1900; |
286 | 0 | if (!mutt_str_atoi_full(tstr + 5, &time.tm_mon)) |
287 | 0 | { |
288 | 0 | p = tstr + 5; |
289 | 0 | goto bail; |
290 | 0 | } |
291 | 0 | time.tm_mon -= 1; |
292 | 0 | if (!mutt_str_atoi_full(tstr + 8, &time.tm_mday)) |
293 | 0 | { |
294 | 0 | p = tstr + 8; |
295 | 0 | goto bail; |
296 | 0 | } |
297 | 0 | tmp.gen_time = mutt_date_make_time(&time, false); |
298 | 0 | } |
299 | 0 | else /* gpg 2.0.10+ uses seconds since 1970-01-01 */ |
300 | 0 | { |
301 | 0 | unsigned long long secs; |
302 | |
|
303 | 0 | if (!mutt_str_atoull(p, &secs)) |
304 | 0 | goto bail; |
305 | 0 | tmp.gen_time = (time_t) secs; |
306 | 0 | } |
307 | 0 | break; |
308 | 0 | } |
309 | 0 | case 7: /* valid for n days */ |
310 | 0 | break; |
311 | 0 | case 8: /* Local id */ |
312 | 0 | break; |
313 | 0 | case 9: /* ownertrust */ |
314 | 0 | break; |
315 | 0 | case 10: /* name */ |
316 | 0 | { |
317 | | /* Empty field or no trailing colon. |
318 | | * We allow an empty field for a pub record type because it is |
319 | | * possible for a primary uid record to have an empty User-ID |
320 | | * field. Without any address records, it is not possible to |
321 | | * use the key in neomutt. */ |
322 | 0 | if (!(pend && (*p || is_pub))) |
323 | 0 | break; |
324 | | |
325 | 0 | if (is_fpr) |
326 | 0 | { |
327 | | /* don't let a subkey fpr overwrite an existing primary key fpr */ |
328 | 0 | if (!tmp.fingerprint) |
329 | 0 | tmp.fingerprint = mutt_str_dup(p); |
330 | 0 | break; |
331 | 0 | } |
332 | | |
333 | | /* ignore user IDs on subkeys */ |
334 | 0 | if (!is_uid && (*is_subkey && c_pgp_ignore_subkeys)) |
335 | 0 | break; |
336 | | |
337 | 0 | mutt_debug(LL_DEBUG2, "user ID: %s\n", NONULL(p)); |
338 | |
|
339 | 0 | uid = MUTT_MEM_CALLOC(1, struct PgpUid); |
340 | 0 | fix_uid(p); |
341 | 0 | uid->addr = mutt_str_dup(p); |
342 | 0 | uid->trust = trust; |
343 | 0 | uid->flags |= flags; |
344 | 0 | uid->next = tmp.address; |
345 | 0 | tmp.address = uid; |
346 | |
|
347 | 0 | if (strstr(p, "ENCR")) |
348 | 0 | tmp.flags |= KEYFLAG_PREFER_ENCRYPTION; |
349 | 0 | if (strstr(p, "SIGN")) |
350 | 0 | tmp.flags |= KEYFLAG_PREFER_SIGNING; |
351 | |
|
352 | 0 | break; |
353 | 0 | } |
354 | 0 | case 11: /* signature class */ |
355 | 0 | break; |
356 | 0 | case 12: /* key capabilities */ |
357 | 0 | mutt_debug(LL_DEBUG2, "capabilities info: %s\n", p); |
358 | |
|
359 | 0 | while (*p) |
360 | 0 | { |
361 | 0 | switch (*p++) |
362 | 0 | { |
363 | 0 | case 'D': |
364 | 0 | flags |= KEYFLAG_DISABLED; |
365 | 0 | break; |
366 | | |
367 | 0 | case 'e': |
368 | 0 | flags |= KEYFLAG_CANENCRYPT; |
369 | 0 | break; |
370 | | |
371 | 0 | case 's': |
372 | 0 | flags |= KEYFLAG_CANSIGN; |
373 | 0 | break; |
374 | 0 | } |
375 | 0 | } |
376 | | |
377 | 0 | if (!is_uid && (!*is_subkey || !c_pgp_ignore_subkeys || |
378 | 0 | !((flags & KEYFLAG_DISABLED) || (flags & KEYFLAG_REVOKED) || |
379 | 0 | (flags & KEYFLAG_EXPIRED)))) |
380 | 0 | { |
381 | 0 | tmp.flags |= flags; |
382 | 0 | } |
383 | |
|
384 | 0 | break; |
385 | | |
386 | 0 | default: |
387 | 0 | break; |
388 | 0 | } |
389 | 0 | } |
390 | | |
391 | | /* merge temp key back into real key */ |
392 | 0 | if (!(is_uid || is_fpr || (*is_subkey && c_pgp_ignore_subkeys))) |
393 | 0 | k = MUTT_MEM_MALLOC(1, struct PgpKeyInfo); |
394 | 0 | if (!k) |
395 | 0 | return NULL; |
396 | 0 | memcpy(k, &tmp, sizeof(*k)); |
397 | | /* fixup parentship of uids after merging the temp key into |
398 | | * the real key */ |
399 | 0 | if (tmp.address) |
400 | 0 | { |
401 | 0 | for (uid = k->address; uid; uid = uid->next) |
402 | 0 | uid->parent = k; |
403 | 0 | } |
404 | |
|
405 | 0 | return k; |
406 | | |
407 | 0 | bail: |
408 | 0 | mutt_debug(LL_DEBUG1, "invalid number: '%s'\n", p); |
409 | 0 | return NULL; |
410 | 0 | } |
411 | | |
412 | | /** |
413 | | * pgp_get_candidates - Find PGP keys matching a list of hints |
414 | | * @param keyring PGP Keyring |
415 | | * @param hints List of strings to match |
416 | | * @retval ptr Key list |
417 | | * @retval NULL Error |
418 | | */ |
419 | | struct PgpKeyInfo *pgp_get_candidates(enum PgpRing keyring, struct ListHead *hints) |
420 | 0 | { |
421 | 0 | FILE *fp = NULL; |
422 | 0 | pid_t pid; |
423 | 0 | char buf[1024] = { 0 }; |
424 | 0 | struct PgpKeyInfo *db = NULL, **kend = NULL, *k = NULL, *kk = NULL, *mainkey = NULL; |
425 | 0 | bool is_sub = false; |
426 | |
|
427 | 0 | int fd_null = open("/dev/null", O_RDWR); |
428 | 0 | if (fd_null == -1) |
429 | 0 | return NULL; |
430 | | |
431 | 0 | mutt_str_replace(&Charset, cc_charset()); |
432 | |
|
433 | 0 | pid = pgp_invoke_list_keys(NULL, &fp, NULL, -1, -1, fd_null, keyring, hints); |
434 | 0 | if (pid == -1) |
435 | 0 | { |
436 | 0 | close(fd_null); |
437 | 0 | return NULL; |
438 | 0 | } |
439 | | |
440 | 0 | kend = &db; |
441 | 0 | k = NULL; |
442 | 0 | while (fgets(buf, sizeof(buf) - 1, fp)) |
443 | 0 | { |
444 | 0 | kk = parse_pub_line(buf, &is_sub, k); |
445 | 0 | if (!kk) |
446 | 0 | continue; |
447 | | |
448 | | /* Only append kk to the list if it's new. */ |
449 | 0 | if (kk != k) |
450 | 0 | { |
451 | 0 | if (k) |
452 | 0 | kend = &k->next; |
453 | 0 | *kend = kk; |
454 | 0 | k = kk; |
455 | |
|
456 | 0 | if (is_sub) |
457 | 0 | { |
458 | 0 | struct PgpUid **l = NULL; |
459 | |
|
460 | 0 | k->flags |= KEYFLAG_SUBKEY; |
461 | 0 | k->parent = mainkey; |
462 | 0 | for (l = &k->address; *l; l = &(*l)->next) |
463 | 0 | ; // do nothing |
464 | |
|
465 | 0 | *l = pgp_copy_uids(mainkey->address, k); |
466 | 0 | } |
467 | 0 | else |
468 | 0 | { |
469 | 0 | mainkey = k; |
470 | 0 | } |
471 | 0 | } |
472 | 0 | } |
473 | |
|
474 | 0 | if (ferror(fp)) |
475 | 0 | mutt_perror("fgets"); |
476 | |
|
477 | 0 | mutt_file_fclose(&fp); |
478 | 0 | filter_wait(pid); |
479 | |
|
480 | 0 | close(fd_null); |
481 | |
|
482 | 0 | return db; |
483 | 0 | } |