QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
rnpcrypto.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 "rnpcrypto.h"
9#include "util.h"
10
11#include <QDir>
12#include <QFileInfo>
13#include <QJsonArray>
14#include <QJsonDocument>
15#include <QJsonObject>
16#include <QRegularExpression>
17#include <QTimeZone>
18#include <algorithm>
19#include <cstring>
20#include <limits>
21#include <memory>
22#include <optional>
23#include <rnp/rnp.h>
24#include <rnp/rnp_err.h>
25
26namespace {
27
29auto rnpError(const QString &what, rnp_result_t result) -> QString {
30 return QStringLiteral("%1: %2").arg(
31 what, QString::fromUtf8(rnp_result_to_string(result)));
32}
33
34auto rnpError(const char *what, rnp_result_t result) -> QString {
35 return rnpError(QString::fromUtf8(what), result);
36}
37
38void setError(QString *error, const QString &message) {
39 if (error != nullptr) {
40 *error = message;
41 }
42}
43
44struct InputDeleter {
45 void operator()(rnp_input_st *input) const { rnp_input_destroy(input); }
46};
47struct OutputDeleter {
48 void operator()(rnp_output_st *output) const { rnp_output_destroy(output); }
49};
50struct KeyDeleter {
51 void operator()(rnp_key_handle_st *key) const { rnp_key_handle_destroy(key); }
52};
53using Input = std::unique_ptr<rnp_input_st, InputDeleter>;
54using Output = std::unique_ptr<rnp_output_st, OutputDeleter>;
55using Key = std::unique_ptr<rnp_key_handle_st, KeyDeleter>;
56
58auto memoryInput(const QByteArray &data, QString *error, const char *what)
59 -> Input {
60 rnp_input_t input = nullptr;
61 const rnp_result_t result = rnp_input_from_memory(
62 &input, reinterpret_cast<const uint8_t *>(data.constData()),
63 static_cast<size_t>(data.size()), false);
64 if (result != RNP_SUCCESS) {
65 setError(error, rnpError(what, result));
66 return nullptr;
67 }
68 return Input(input);
69}
70
73auto memoryOutput(QString *error, const char *what, qint64 maxBytes = 0)
74 -> Output {
75 rnp_output_t output = nullptr;
76 const rnp_result_t result =
77 rnp_output_to_memory(&output, static_cast<size_t>(maxBytes));
78 if (result != RNP_SUCCESS) {
79 setError(error, rnpError(what, result));
80 return nullptr;
81 }
82 return Output(output);
83}
84
86auto outputBytes(rnp_output_t output) -> std::optional<QByteArray> {
87 uint8_t *buf = nullptr;
88 size_t len = 0;
89 if (rnp_output_memory_get_buf(output, &buf, &len, false) != RNP_SUCCESS) {
90 return std::nullopt;
91 }
92 return QByteArray(reinterpret_cast<const char *>(buf),
93 static_cast<qsizetype>(len));
94}
95
97auto isValidKey(rnp_key_handle_t key) -> bool {
98 bool valid = false;
99 return rnp_key_is_valid(key, &valid) == RNP_SUCCESS && valid;
100}
101
102auto allowsEncryption(rnp_key_handle_t key) -> bool {
103 bool allowed = false;
104 return rnp_key_allows_usage(key, "encrypt", &allowed) == RNP_SUCCESS &&
105 allowed;
106}
107
114auto canEncryptTo(rnp_key_handle_t key) -> bool {
115 if (!isValidKey(key)) {
116 return false;
117 }
118 if (allowsEncryption(key)) {
119 return true;
120 }
121 size_t subkeys = 0;
122 rnp_key_get_subkey_count(key, &subkeys);
123 for (size_t i = 0; i < subkeys; ++i) {
124 rnp_key_handle_t handle = nullptr;
125 if (rnp_key_get_subkey_at(key, i, &handle) != RNP_SUCCESS ||
126 handle == nullptr) {
127 continue;
128 }
129 const Key subkey(handle);
130 if (isValidKey(handle) && allowsEncryption(handle)) {
131 return true;
132 }
133 }
134 return false;
135}
136
143auto secretIsHere(rnp_key_handle_t key) -> bool {
144 bool secret = false;
145 if (rnp_key_have_secret(key, &secret) != RNP_SUCCESS || !secret) {
146 return false;
147 }
148 char *type = nullptr;
149 if (rnp_key_get_protection_type(key, &type) != RNP_SUCCESS ||
150 type == nullptr) {
151 return false;
152 }
153 const QString protection = QString::fromLatin1(type);
154 rnp_buffer_destroy(type);
155 return protection != QLatin1String("GPG-None") &&
156 protection != QLatin1String("GPG-Smartcard") &&
157 protection != QLatin1String("Unknown");
158}
159
160auto isProtected(rnp_key_handle_t key) -> bool {
161 bool locked = false;
162 return rnp_key_is_protected(key, &locked) == RNP_SUCCESS && locked;
163}
164
171template <typename Visit>
172auto forEachSecretPart(rnp_key_handle_t key, Visit visit) -> int {
173 int visited = 0;
174 if (secretIsHere(key)) {
175 ++visited;
176 if (!visit(key)) {
177 return visited;
178 }
179 }
180 size_t subkeys = 0;
181 rnp_key_get_subkey_count(key, &subkeys);
182 for (size_t i = 0; i < subkeys; ++i) {
183 rnp_key_handle_t handle = nullptr;
184 if (rnp_key_get_subkey_at(key, i, &handle) != RNP_SUCCESS ||
185 handle == nullptr) {
186 continue;
187 }
188 const Key subkey(handle);
189 if (secretIsHere(handle)) {
190 ++visited;
191 if (!visit(handle)) {
192 return visited;
193 }
194 }
195 }
196 return visited;
197}
198
204auto signingKeyOf(rnp_key_handle_t key) -> Key {
205 if (!isValidKey(key)) {
206 return nullptr;
207 }
208 rnp_key_handle_t handle = nullptr;
209 if (rnp_key_get_default_key(key, "sign", 0, &handle) != RNP_SUCCESS ||
210 handle == nullptr) {
211 return nullptr;
212 }
213 Key signer(handle);
214 if (!isValidKey(handle) || !secretIsHere(handle)) {
215 return nullptr;
216 }
217 return signer;
218}
219
221enum class Usage { Encrypt, Sign };
222
223auto usableFor(rnp_key_handle_t primary, Usage usage) -> bool {
224 return usage == Usage::Encrypt ? canEncryptTo(primary)
225 : signingKeyOf(primary) != nullptr;
226}
227
229auto primaryFingerprint(rnp_key_handle_t key) -> QString {
230 bool primary = false;
231 rnp_key_is_primary(key, &primary);
232 char *fpr = nullptr;
233 if ((primary ? rnp_key_get_fprint(key, &fpr)
234 : rnp_key_get_primary_fprint(key, &fpr)) != RNP_SUCCESS) {
235 return {};
236 }
237 QString result = QString::fromLatin1(fpr);
238 rnp_buffer_destroy(fpr);
239 return result;
240}
241
248auto liveUserIds(rnp_key_handle_t key) -> QStringList {
249 QStringList live;
250 size_t uids = 0;
251 rnp_key_get_uid_count(key, &uids);
252 for (size_t i = 0; i < uids; ++i) {
253 rnp_uid_handle_t handle = nullptr;
254 if (rnp_key_get_uid_handle_at(key, i, &handle) != RNP_SUCCESS ||
255 handle == nullptr) {
256 continue;
257 }
258 // Both: in RNP 0.17 a revoked user ID can still be "valid".
259 bool valid = false;
260 bool revoked = true;
261 const bool known = rnp_uid_is_valid(handle, &valid) == RNP_SUCCESS &&
262 rnp_uid_is_revoked(handle, &revoked) == RNP_SUCCESS;
263 rnp_uid_handle_destroy(handle);
264 char *uid = nullptr;
265 if (!known || !valid || revoked ||
266 rnp_key_get_uid_at(key, i, &uid) != RNP_SUCCESS) {
267 continue;
268 }
269 live << QString::fromUtf8(uid);
270 rnp_buffer_destroy(uid);
271 }
272 return live;
273}
274
276template <typename Hit>
277auto anyUserIdMatches(rnp_key_handle_t key, Hit hit) -> bool {
278 const QStringList uids = liveUserIds(key);
279 return std::any_of(uids.cbegin(), uids.cend(), hit);
280}
281
283auto mailboxOf(const QString &uid) -> QString {
284 const qsizetype open = uid.lastIndexOf(QLatin1Char('<'));
285 const qsizetype close = uid.lastIndexOf(QLatin1Char('>'));
286 return open >= 0 && close > open ? uid.mid(open + 1, close - open - 1)
287 : uid.trimmed();
288}
289
295template <typename Match>
296auto matchingPrimaries(rnp_ffi_t ffi, Usage usage, Match match) -> QStringList {
297 QStringList primaries;
298 rnp_identifier_iterator_t it = nullptr;
299 if (rnp_identifier_iterator_create(ffi, &it, "fingerprint") != RNP_SUCCESS) {
300 return primaries;
301 }
302 const char *fpr = nullptr;
303 while (rnp_identifier_iterator_next(it, &fpr) == RNP_SUCCESS &&
304 fpr != nullptr) {
305 rnp_key_handle_t handle = nullptr;
306 if (rnp_locate_key(ffi, "fingerprint", fpr, &handle) != RNP_SUCCESS ||
307 handle == nullptr) {
308 continue;
309 }
310 const Key key(handle);
311 if (!match(handle)) {
312 continue;
313 }
314 const QString primary = primaryFingerprint(handle);
315 rnp_key_handle_t primaryHandle = nullptr;
316 if (primary.isEmpty() || primaries.contains(primary) ||
317 rnp_locate_key(ffi, "fingerprint", primary.toLatin1().constData(),
318 &primaryHandle) != RNP_SUCCESS ||
319 primaryHandle == nullptr) {
320 continue;
321 }
322 const Key primaryKey(primaryHandle);
323 if (usableFor(primaryHandle, usage)) {
324 primaries << primary;
325 }
326 }
327 rnp_identifier_iterator_destroy(it);
328 return primaries;
329}
330
338auto locateKey(rnp_ffi_t ffi, const QString &recipient, QString *why,
339 Usage usage = Usage::Encrypt) -> Key {
340 const QString id = recipient.trimmed();
341 if (id.isEmpty()) {
342 setError(why, QStringLiteral("Empty recipient"));
343 return nullptr;
344 }
345 // A key ID or fingerprint: hex digits after an optional 0x, which marks
346 // one only when hex follows (gpg reads "0xAlice" as a user ID, and so
347 // does this). Blanks (spaces or tabs) between groups are allowed in a
348 // fingerprint only, as gpg allows them: "dead beef" is a user ID.
349 static const QRegularExpression blanks(QStringLiteral("\\s+"));
350 static const QRegularExpression hex(QStringLiteral("^[0-9A-Fa-f]+$"));
351 const auto withoutPrefix = [](const QString &text) {
352 return text.startsWith(QLatin1String("0x"), Qt::CaseInsensitive) &&
353 hex.match(text.mid(2)).hasMatch()
354 ? text.mid(2)
355 : text;
356 };
357 QString fingerprint = id;
358 fingerprint.remove(blanks);
359 fingerprint = withoutPrefix(fingerprint);
360 const QString keyId = withoutPrefix(id);
361 if (keyId.compare(QLatin1String("0x"), Qt::CaseInsensitive) == 0) {
362 setError(why, QStringLiteral("Empty recipient"));
363 return nullptr;
364 }
365 const bool isHexId = hex.match(keyId).hasMatch();
366 const char *type = nullptr;
367 QString lookup;
368 // 40 digits for a v4 key, 64 for v5 and v6 (RFC 9580).
369 if (hex.match(fingerprint).hasMatch() &&
370 (fingerprint.size() == 40 || fingerprint.size() == 64)) {
371 type = "fingerprint";
372 lookup = fingerprint;
373 } else if (isHexId && keyId.size() == 16) {
374 type = "keyid";
375 lookup = keyId;
376 }
377 if (type != nullptr) {
378 rnp_key_handle_t handle = nullptr;
379 rnp_locate_key(ffi, type, lookup.toUpper().toLatin1().constData(), &handle);
380 if (handle == nullptr) {
381 setError(why, (usage == Usage::Encrypt
382 ? QStringLiteral("No public key for %1")
383 : QStringLiteral("No secret key for %1"))
384 .arg(recipient));
385 return nullptr;
386 }
387 Key key(handle);
388 // Named exactly, so no other key is tried; say why it cannot be used
389 // rather than leaving RNP's generic error for later.
390 const QString primary = primaryFingerprint(handle);
391 rnp_key_handle_t primaryHandle = nullptr;
392 rnp_locate_key(ffi, "fingerprint", primary.toLatin1().constData(),
393 &primaryHandle);
394 const Key primaryKey(primaryHandle);
395 if (primaryHandle == nullptr || !usableFor(primaryHandle, usage)) {
396 setError(why,
397 (usage == Usage::Encrypt
398 ? QStringLiteral("The key %1 cannot be encrypted to: it "
399 "is expired or revoked, or has no valid "
400 "encryption subkey")
401 : QStringLiteral("The key %1 cannot sign: it is expired "
402 "or revoked, has no valid signing key, "
403 "or its secret key is not here"))
404 .arg(recipient));
405 return nullptr;
406 }
407 return key;
408 }
409
410 QStringList primaries;
411 if (isHexId && keyId.size() == 8) {
412 // A short key ID: the last eight digits of a key's (or subkey's) ID.
413 const QString shortId = keyId.toUpper();
414 primaries = matchingPrimaries(ffi, usage, [&shortId](rnp_key_handle_t key) {
415 char *keyId = nullptr;
416 if (rnp_key_get_keyid(key, &keyId) != RNP_SUCCESS) {
417 return false;
418 }
419 const bool hit = QString::fromLatin1(keyId).endsWith(shortId);
420 rnp_buffer_destroy(keyId);
421 return hit;
422 });
423 } else if (static const QRegularExpression address(QStringLiteral(
424 R"(^(?:<[^\s<>@]+@[^\s<>@]+>|[^\s<>@]+@[^\s<>@]+)$)"));
425 address.match(id).hasMatch()) {
426 // An e-mail address (a .gpg-id often lists just that) names the key
427 // whose user ID has that address, compared without regard to case, not
428 // every user ID that contains it: as part of a name, alice@example.org
429 // would also find malice@example.org. Stricter than gpg -r, which
430 // searches for a substring.
431 QString mailbox = id;
432 mailbox.remove(QLatin1Char('<')).remove(QLatin1Char('>'));
433 primaries = matchingPrimaries(ffi, usage, [&mailbox](rnp_key_handle_t key) {
434 return anyUserIdMatches(key, [&mailbox](const QString &uid) {
435 return mailboxOf(uid).compare(mailbox, Qt::CaseInsensitive) == 0;
436 });
437 });
438 } else {
439 // The whole user ID first, then part of one (gpg -r takes both). Both
440 // through matchingPrimaries, so an expired key is skipped and two keys
441 // with the same user ID (after a key rotation, say) are an error, not a
442 // pick.
443 primaries = matchingPrimaries(ffi, usage, [&id](rnp_key_handle_t key) {
444 return anyUserIdMatches(key,
445 [&id](const QString &uid) { return uid == id; });
446 });
447 if (primaries.isEmpty()) {
448 primaries = matchingPrimaries(ffi, usage, [&id](rnp_key_handle_t key) {
449 return anyUserIdMatches(key, [&id](const QString &uid) {
450 return uid.contains(id, Qt::CaseInsensitive);
451 });
452 });
453 }
454 }
455 if (primaries.size() > 1) {
456 setError(why,
457 QStringLiteral("%1 matches more than one key").arg(recipient));
458 return nullptr;
459 }
460 if (primaries.isEmpty()) {
461 setError(why, (usage == Usage::Encrypt
462 ? QStringLiteral("No usable public key for %1")
463 : QStringLiteral("No usable secret key for %1"))
464 .arg(recipient));
465 return nullptr;
466 }
467 rnp_key_handle_t key = nullptr;
468 rnp_locate_key(ffi, "fingerprint", primaries.first().toLatin1().constData(),
469 &key);
470 return Key(key);
471}
472
473// The signature is RNP's rnp_password_cb.
474auto passwordCallback(rnp_ffi_t /*ffi*/, void *appCtx, rnp_key_handle_t key,
475 const char * /*pgpContext*/,
476 char buf[], // NOLINT(modernize-avoid-c-arrays)
477 size_t bufLen) -> bool {
478 const auto *provider =
479 static_cast<const RnpCrypto::PassphraseProvider *>(appCtx);
480 if (provider == nullptr || !*provider || bufLen == 0) {
481 return false;
482 }
483 QString keyId;
484 char *rawId = nullptr;
485 if (rnp_key_get_keyid(key, &rawId) == RNP_SUCCESS) {
486 keyId = QString::fromLatin1(rawId);
487 rnp_buffer_destroy(rawId);
488 }
489 QByteArray passphrase = (*provider)(keyId).toUtf8();
490 if (passphrase.isEmpty() ||
491 static_cast<size_t>(passphrase.size()) >= bufLen) {
492 passphrase.fill('\0');
493 return false;
494 }
495 std::memcpy(buf, passphrase.constData(),
496 static_cast<size_t>(passphrase.size()));
497 buf[passphrase.size()] = '\0';
498 passphrase.fill('\0');
499 return true;
500}
501
502} // namespace
503
505 rnp_ffi_t ffi = nullptr;
506 if (rnp_ffi_create(&ffi, "GPG", "GPG") == RNP_SUCCESS) {
507 m_ffi = ffi;
508 rnp_ffi_set_pass_provider(m_ffi, passwordCallback, &m_passphrase);
509 }
510}
511
513 if (m_ffi != nullptr) {
514 rnp_ffi_destroy(m_ffi);
515 }
516}
517
518auto RnpCrypto::isValid() const -> bool { return m_ffi != nullptr; }
519
521 // RNP reads 0 as "no limit", and a negative value would wrap to a huge
522 // size_t: either would switch the guard off without a word. RNP 0.17 and
523 // 0.18 also keep the limit in 32 bits, so 4 GiB would become 0 (no limit)
524 // and 4 GiB + 5 become 5: anything wider is capped to what fits.
525 if (bytes > 0) {
527 std::min<qint64>(bytes, std::numeric_limits<quint32>::max());
528 }
529}
530
532 m_passphrase = std::move(provider);
533}
534
535auto RnpCrypto::importKeys(const QByteArray &keyData, QString *error)
536 -> QStringList {
537 if (m_ffi == nullptr) {
538 setError(error, QStringLiteral("RNP is not available"));
539 return {};
540 }
541 Input input = memoryInput(keyData, error, "Cannot read the key data");
542 if (!input) {
543 return {};
544 }
545 char *json = nullptr;
546 const rnp_result_t result = rnp_import_keys(
547 m_ffi, input.get(), RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS,
548 &json);
549 if (result != RNP_SUCCESS) {
550 setError(error, rnpError("Cannot import the keys", result));
551 return {};
552 }
553 QJsonParseError parseError{};
554 const QJsonDocument doc = QJsonDocument::fromJson(json, &parseError);
555 rnp_buffer_destroy(json);
556 const QJsonValue keysValue = doc.object().value(QStringLiteral("keys"));
557 if (parseError.error != QJsonParseError::NoError || !keysValue.isArray()) {
558 // The keys may well be in the keyring; only which ones is unknown.
559 setError(error, QStringLiteral("Cannot read RNP's import report: %1")
560 .arg(parseError.error != QJsonParseError::NoError
561 ? parseError.errorString()
562 : QStringLiteral("no key list")));
563 return {};
564 }
565 QStringList primaries;
566 const QJsonArray keys = keysValue.toArray();
567 for (const auto &entry : keys) {
568 const QString fpr =
569 entry.toObject().value(QStringLiteral("fingerprint")).toString();
570 rnp_key_handle_t key = nullptr;
571 if (rnp_locate_key(m_ffi, "fingerprint", fpr.toLatin1().constData(),
572 &key) != RNP_SUCCESS ||
573 key == nullptr) {
574 continue;
575 }
576 bool primary = false;
577 rnp_key_is_primary(key, &primary);
578 rnp_key_handle_destroy(key);
579 if (primary && !primaries.contains(fpr)) {
580 primaries << fpr;
581 }
582 }
583 return primaries;
584}
585
586auto RnpCrypto::encrypt(const QByteArray &plaintext,
587 const QStringList &recipients, QString *error)
588 -> QByteArray {
589 if (m_ffi == nullptr) {
590 setError(error, QStringLiteral("RNP is not available"));
591 return {};
592 }
593 if (recipients.isEmpty()) {
594 setError(error, QStringLiteral("No recipients"));
595 return {};
596 }
597 Input input = memoryInput(plaintext, error, "Cannot read the entry");
598 Output output = memoryOutput(error, "Cannot encrypt");
599 if (!input || !output) {
600 return {};
601 }
602 rnp_op_encrypt_t op = nullptr;
603 rnp_result_t result =
604 rnp_op_encrypt_create(&op, m_ffi, input.get(), output.get());
605 if (result != RNP_SUCCESS) {
606 setError(error, rnpError("Cannot encrypt", result));
607 return {};
608 }
609 const std::unique_ptr<rnp_op_encrypt_st, rnp_result_t (*)(rnp_op_encrypt_t)>
610 guard(op, rnp_op_encrypt_destroy);
611 for (const QString &recipient : recipients) {
612 QString why;
613 const Key key = locateKey(m_ffi, recipient, &why);
614 if (!key) {
615 setError(error, why);
616 return {};
617 }
618 result = rnp_op_encrypt_add_recipient(op, key.get());
619 if (result != RNP_SUCCESS) {
620 setError(error,
621 rnpError(QStringLiteral("Cannot encrypt to %1").arg(recipient),
622 result));
623 return {};
624 }
625 }
626 rnp_op_encrypt_set_armor(op, false);
627 result = rnp_op_encrypt_execute(op);
628 if (result != RNP_SUCCESS) {
629 setError(error, rnpError("Cannot encrypt", result));
630 return {};
631 }
632 std::optional<QByteArray> cipher = outputBytes(output.get());
633 if (!cipher) {
634 setError(error, QStringLiteral("Cannot encrypt: no output from RNP"));
635 return {};
636 }
637 return *cipher;
638}
639
640auto RnpCrypto::decrypt(const QByteArray &ciphertext, QString *error, bool *ok)
641 -> QByteArray {
642 if (ok != nullptr) {
643 *ok = false;
644 }
645 if (m_ffi == nullptr) {
646 setError(error, QStringLiteral("RNP is not available"));
647 return {};
648 }
649 Input input = memoryInput(ciphertext, error, "Cannot read the entry");
650 Output output = memoryOutput(error, "Cannot decrypt", m_maxPlaintext);
651 if (!input || !output) {
652 return {};
653 }
654 // Through a verify operation rather than rnp_decrypt(): only that reports
655 // how the message was protected, and rnp_decrypt() returns success for
656 // CFB without MDC, the kind EFAIL abuses.
657 rnp_op_verify_t op = nullptr;
658 rnp_result_t result =
659 rnp_op_verify_create(&op, m_ffi, input.get(), output.get());
660 if (result != RNP_SUCCESS) {
661 setError(error, rnpError("Cannot decrypt", result));
662 return {};
663 }
664 const std::unique_ptr<rnp_op_verify_st, rnp_result_t (*)(rnp_op_verify_t)>
665 guard(op, rnp_op_verify_destroy);
666 // A signature by a key not in this keyring does not make an entry
667 // unreadable; pass does not check signatures on entries either.
668 rnp_op_verify_set_flags(op, RNP_VERIFY_IGNORE_SIGS_ON_DECRYPT);
669 result = rnp_op_verify_execute(op);
670 if (result != RNP_SUCCESS) {
671 setError(error, rnpError("Cannot decrypt", result));
672 return {};
673 }
674 char *mode = nullptr;
675 bool protectedOk = false;
676 if (rnp_op_verify_get_protection_info(op, &mode, nullptr, &protectedOk) !=
677 RNP_SUCCESS ||
678 !protectedOk) {
679 setError(error,
680 QStringLiteral("Refusing a message without integrity "
681 "protection (%1)")
682 .arg(QString::fromLatin1(mode != nullptr ? mode : "unknown")));
683 rnp_buffer_destroy(mode);
684 return {};
685 }
686 rnp_buffer_destroy(mode);
687 std::optional<QByteArray> plain = outputBytes(output.get());
688 if (!plain) {
689 setError(error, QStringLiteral("Cannot decrypt: no output from RNP"));
690 return {};
691 }
692 if (ok != nullptr) {
693 *ok = true;
694 }
695 return *plain;
696}
697
698namespace {
699
701auto fingerprintOf(rnp_key_handle_t key) -> QString {
702 char *fpr = nullptr;
703 if (rnp_key_get_fprint(key, &fpr) != RNP_SUCCESS) {
704 return {};
705 }
706 QString result = QString::fromLatin1(fpr);
707 rnp_buffer_destroy(fpr);
708 return result;
709}
710
711auto isRevoked(rnp_key_handle_t key) -> bool {
712 bool revoked = false;
713 return rnp_key_is_revoked(key, &revoked) == RNP_SUCCESS && revoked;
714}
715
722auto firstSecretOfUnknownProtection(rnp_ffi_t ffi) -> std::optional<QString> {
723 QString found;
724 rnp_identifier_iterator_t it = nullptr;
725 if (rnp_identifier_iterator_create(ffi, &it, "fingerprint") != RNP_SUCCESS) {
726 return std::nullopt;
727 }
728 bool failed = false;
729 const char *fpr = nullptr;
730 while (found.isEmpty() && !failed) {
731 if (rnp_identifier_iterator_next(it, &fpr) != RNP_SUCCESS) {
732 failed = true;
733 break;
734 }
735 if (fpr == nullptr) {
736 break; // the end
737 }
738 rnp_key_handle_t handle = nullptr;
739 if (rnp_locate_key(ffi, "fingerprint", fpr, &handle) != RNP_SUCCESS ||
740 handle == nullptr) {
741 failed = true;
742 break;
743 }
744 const Key key(handle);
745 bool secret = false;
746 if (rnp_key_have_secret(handle, &secret) != RNP_SUCCESS) {
747 failed = true;
748 break;
749 }
750 if (!secret) {
751 continue;
752 }
753 char *type = nullptr;
754 if (rnp_key_get_protection_type(handle, &type) != RNP_SUCCESS ||
755 type == nullptr) {
756 failed = true;
757 break;
758 }
759 if (QString::fromLatin1(type) == QLatin1String("Unknown")) {
760 found = QString::fromLatin1(fpr);
761 }
762 rnp_buffer_destroy(type);
763 }
764 rnp_identifier_iterator_destroy(it);
765 if (failed) {
766 return std::nullopt;
767 }
768 return found;
769}
770
772auto primaryOf(rnp_ffi_t ffi, rnp_key_handle_t key) -> Key {
773 const QString primary = primaryFingerprint(key);
774 rnp_key_handle_t handle = nullptr;
775 if (primary.isEmpty() ||
776 rnp_locate_key(ffi, "fingerprint", primary.toLatin1().constData(),
777 &handle) != RNP_SUCCESS) {
778 return nullptr;
779 }
780 return Key(handle);
781}
782
783} // namespace
784
785auto RnpCrypto::listKeys(bool secretOnly) const -> QList<KeyInfo> {
786 QList<KeyInfo> keys;
787 if (m_ffi == nullptr) {
788 return keys;
789 }
790 rnp_identifier_iterator_t it = nullptr;
791 if (rnp_identifier_iterator_create(m_ffi, &it, "fingerprint") !=
792 RNP_SUCCESS) {
793 return keys;
794 }
795 const char *fpr = nullptr;
796 while (rnp_identifier_iterator_next(it, &fpr) == RNP_SUCCESS &&
797 fpr != nullptr) {
798 rnp_key_handle_t handle = nullptr;
799 if (rnp_locate_key(m_ffi, "fingerprint", fpr, &handle) != RNP_SUCCESS ||
800 handle == nullptr) {
801 continue;
802 }
803 const Key key(handle);
804 bool primary = false;
805 if (rnp_key_is_primary(handle, &primary) != RNP_SUCCESS || !primary) {
806 continue;
807 }
808 KeyInfo info;
809 info.fingerprint = fingerprintOf(handle);
810 char *keyId = nullptr;
811 if (rnp_key_get_keyid(handle, &keyId) == RNP_SUCCESS) {
812 info.keyId = QString::fromLatin1(keyId);
813 rnp_buffer_destroy(keyId);
814 }
815 info.userIds = liveUserIds(handle);
816 uint32_t created = 0;
817 if (rnp_key_get_creation(handle, &created) == RNP_SUCCESS) {
818 info.created = QDateTime::fromSecsSinceEpoch(created, QTimeZone::UTC);
819 }
820 uint32_t lifetime = 0;
821 if (rnp_key_get_expiration(handle, &lifetime) == RNP_SUCCESS &&
822 lifetime > 0) {
823 info.expires = info.created.addSecs(lifetime);
824 }
825 info.revoked = isRevoked(handle);
826 bool expired = false;
827 info.expired =
828 rnp_key_is_expired(handle, &expired) == RNP_SUCCESS && expired;
829 // A secret part may be a subkey's only (gpg --export-secret-subkeys).
830 info.hasSecret = forEachSecretPart(handle, [&info](rnp_key_handle_t part) {
831 info.secretProtected =
832 info.secretProtected && isProtected(part);
833 return true;
834 }) > 0;
835 info.canEncrypt = canEncryptTo(handle);
836 info.canSign = signingKeyOf(handle) != nullptr;
837 if (!secretOnly || info.hasSecret) {
838 keys << info;
839 }
840 }
841 rnp_identifier_iterator_destroy(it);
842 return keys;
843}
844
845auto RnpCrypto::sign(const QByteArray &data, const QString &signer,
846 QString *error) -> QByteArray {
847 if (m_ffi == nullptr) {
848 setError(error, QStringLiteral("RNP is not available"));
849 return {};
850 }
851 // As gpg --local-user: exactly the key named, never another one that
852 // happens to have a secret key here.
853 QString why;
854 const Key named = locateKey(m_ffi, signer, &why, Usage::Sign);
855 if (!named) {
856 setError(error, why);
857 return {};
858 }
859 const Key primary = primaryOf(m_ffi, named.get());
860 const Key signing = primary ? signingKeyOf(primary.get()) : nullptr;
861 if (!signing) {
862 setError(error, QStringLiteral("The key %1 cannot sign").arg(signer));
863 return {};
864 }
865 Input input = memoryInput(data, error, "Cannot read the data to sign");
866 Output output = memoryOutput(error, "Cannot sign");
867 if (!input || !output) {
868 return {};
869 }
870 rnp_op_sign_t op = nullptr;
871 rnp_result_t result =
872 rnp_op_sign_detached_create(&op, m_ffi, input.get(), output.get());
873 if (result != RNP_SUCCESS) {
874 setError(error, rnpError("Cannot sign", result));
875 return {};
876 }
877 const std::unique_ptr<rnp_op_sign_st, rnp_result_t (*)(rnp_op_sign_t)> guard(
878 op, rnp_op_sign_destroy);
879 rnp_op_sign_set_armor(op, false);
880 result = rnp_op_sign_add_signature(op, signing.get(), nullptr);
881 if (result == RNP_SUCCESS) {
882 // Unlocks the secret key through the passphrase provider.
883 result = rnp_op_sign_execute(op);
884 }
885 if (result != RNP_SUCCESS) {
886 setError(error, rnpError("Cannot sign", result));
887 return {};
888 }
889 std::optional<QByteArray> signature = outputBytes(output.get());
890 if (!signature || signature->isEmpty()) {
891 setError(error, QStringLiteral("Cannot sign: no output from RNP"));
892 return {};
893 }
894 return *signature;
895}
896
897auto RnpCrypto::verify(const QByteArray &data, const QByteArray &signature,
898 QString *error) -> QStringList {
899 if (m_ffi == nullptr) {
900 setError(error, QStringLiteral("RNP is not available"));
901 return {};
902 }
903 Input input = memoryInput(data, error, "Cannot read the signed data");
904 Input sigInput = memoryInput(signature, error, "Cannot read the signature");
905 if (!input || !sigInput) {
906 return {};
907 }
908 rnp_op_verify_t op = nullptr;
909 rnp_result_t result =
910 rnp_op_verify_detached_create(&op, m_ffi, input.get(), sigInput.get());
911 if (result != RNP_SUCCESS) {
912 setError(error, rnpError("Cannot verify", result));
913 return {};
914 }
915 const std::unique_ptr<rnp_op_verify_st, rnp_result_t (*)(rnp_op_verify_t)>
916 guard(op, rnp_op_verify_destroy);
917 // Fails when no signature verifies; each one's own status says why.
918 result = rnp_op_verify_execute(op);
919 size_t count = 0;
920 rnp_op_verify_get_signature_count(op, &count);
921 for (size_t i = 0; i < count; ++i) {
922 rnp_op_verify_signature_t sig = nullptr;
923 if (rnp_op_verify_get_signature_at(op, i, &sig) != RNP_SUCCESS ||
924 rnp_op_verify_signature_get_status(sig) != RNP_SUCCESS) {
925 continue;
926 }
927 rnp_key_handle_t handle = nullptr;
928 if (rnp_op_verify_signature_get_key(sig, &handle) != RNP_SUCCESS ||
929 handle == nullptr) {
930 continue;
931 }
932 const Key key(handle);
933 const Key primary = primaryOf(m_ffi, handle);
934 // The first good signature decides, as the first VALIDSIG does in
935 // GpgIdSigner: by a revoked key (a leaked one, say) it counts for
936 // nothing. RNP 0.17 and 0.18 already give such a signature a failed
937 // status; checked here too, so that holds whatever RNP decides.
938 if (isRevoked(handle) || !primary || isRevoked(primary.get())) {
939 setError(error, QStringLiteral("The signature is by a revoked key (%1)")
940 .arg(primaryFingerprint(handle)));
941 return {};
942 }
943 return {fingerprintOf(handle), fingerprintOf(primary.get())};
944 }
945 setError(error, result != RNP_SUCCESS ? rnpError("No valid signature", result)
946 : QStringLiteral("No valid signature"));
947 return {};
948}
949
950auto RnpCrypto::protectKey(const QString &fingerprint,
951 const QString &passphrase, QString *error) -> bool {
952 if (m_ffi == nullptr) {
953 setError(error, QStringLiteral("RNP is not available"));
954 return false;
955 }
956 if (passphrase.isEmpty()) {
957 setError(error, QStringLiteral("An empty passphrase protects nothing"));
958 return false;
959 }
960 rnp_key_handle_t handle = nullptr;
961 rnp_locate_key(m_ffi, "fingerprint",
962 fingerprint.toUpper().toLatin1().constData(), &handle);
963 const Key key(handle);
964 // Every part that is still unprotected gets the passphrase; the ones that
965 // are protected keep theirs. So a key left half protected (an imported
966 // one, or a run that failed part-way) can be finished, rather than only
967 // saved with consent, unprotected parts and all.
968 int unprotected = 0;
969 const int parts =
970 handle == nullptr
971 ? 0
972 : forEachSecretPart(handle, [&unprotected](rnp_key_handle_t part) {
973 unprotected += isProtected(part) ? 0 : 1;
974 return true;
975 });
976 if (parts == 0) {
977 setError(error, QStringLiteral("No secret key for %1").arg(fingerprint));
978 return false;
979 }
980 if (unprotected == 0) {
981 setError(error,
982 QStringLiteral("The secret key %1 is protected already; its "
983 "passphrase is not changed here")
984 .arg(fingerprint));
985 return false;
986 }
987 QByteArray secret = passphrase.toUtf8();
988 bool ok = true;
989 forEachSecretPart(handle, [&](rnp_key_handle_t part) {
990 if (isProtected(part)) {
991 return true;
992 }
993 // RNP's defaults: AES-256 and a SHA-256 S2K with its iteration count.
994 const rnp_result_t result =
995 rnp_key_protect(part, secret.constData(), nullptr, nullptr, nullptr, 0);
996 if (result != RNP_SUCCESS) {
997 setError(
998 error,
999 rnpError(QStringLiteral("Cannot protect %1").arg(fingerprintOf(part)),
1000 result));
1001 ok = false;
1002 }
1003 return ok;
1004 });
1005 secret.fill('\0');
1006 return ok;
1007}
1008
1009auto RnpCrypto::saveKeyring(const QString &dir, bool allowUnprotected,
1010 QString *error) const -> bool {
1011 if (m_ffi == nullptr) {
1012 setError(error, QStringLiteral("RNP is not available"));
1013 return false;
1014 }
1015 // QtPass's own folder, not wherever a link would lead: cleaned first, so
1016 // "dir/" or "dir/." is not judged by its target, and a junction counts.
1017 if (Util::isLinkedFolder(dir)) {
1018 setError(error,
1019 QStringLiteral("%1 is a link, not a keyring folder").arg(dir));
1020 return false;
1021 }
1022 // Checked before anything is written: a secret key goes to disk only
1023 // protected by its own passphrase, unless the user said otherwise.
1024 if (!allowUnprotected) {
1025 for (const KeyInfo &key : listKeys(true)) {
1026 if (!key.secretProtected) {
1027 setError(error, QStringLiteral("The secret key %1 is not protected "
1028 "by a passphrase")
1029 .arg(key.fingerprint));
1030 return false;
1031 }
1032 }
1033 // A secret part whose protection RNP cannot name is not counted as
1034 // here (secretIsHere()), yet it would be written: what is in it is not
1035 // known, so it is refused like an unprotected one.
1036 const std::optional<QString> unknown =
1037 firstSecretOfUnknownProtection(m_ffi);
1038 if (!unknown) {
1039 setError(error, QStringLiteral("Cannot tell how every secret key is "
1040 "protected; nothing is written"));
1041 return false;
1042 }
1043 if (!unknown->isEmpty()) {
1044 setError(error, QStringLiteral("The secret key %1 has a protection RNP "
1045 "cannot name")
1046 .arg(*unknown));
1047 return false;
1048 }
1049 }
1050 // One file with public and secret keys, replaced whole: two files could
1051 // not be swapped together, and a save failing between them would leave a
1052 // new public ring next to an old secret one, which the next load would
1053 // combine (a secret key removed since back in). RNP refuses to save an
1054 // empty set; an empty file says the same.
1055 std::optional<QByteArray> ring = QByteArray();
1056 if (!listKeys().isEmpty()) {
1057 Output output = memoryOutput(error, "Cannot save the keyring");
1058 if (!output) {
1059 return false;
1060 }
1061 const rnp_result_t result =
1062 rnp_save_keys(m_ffi, "GPG", output.get(),
1063 RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS);
1064 if (result != RNP_SUCCESS) {
1065 setError(error, rnpError("Cannot save the keyring", result));
1066 return false;
1067 }
1068 ring = outputBytes(output.get());
1069 if (!ring) {
1070 setError(error, QStringLiteral("Cannot save the keyring: no output"));
1071 return false;
1072 }
1073 }
1074 // What loadKeyring() would not read back is not written.
1075 if (ring->size() > kMaxKeyringBytes) {
1076 setError(error, QStringLiteral("The keyring is larger than %1 bytes")
1077 .arg(kMaxKeyringBytes));
1078 return false;
1079 }
1080 // QtPass's own folder, owner-only whatever the umask, also when it
1081 // existed already.
1082 if (!QDir().mkpath(dir) ||
1083 !QFile::setPermissions(dir, QFile::ReadOwner | QFile::WriteOwner |
1084 QFile::ExeOwner)) {
1085 setError(error, QStringLiteral("Cannot create %1").arg(dir));
1086 return false;
1087 }
1088 // Staged and replaced whole (owner-only), as an entry is written: a
1089 // reader sees the old keyring or the new one, never a mix.
1090 QString why;
1092 QDir(dir).filePath(QStringLiteral("keyring.gpg")), *ring, true,
1093 &why)) {
1094 setError(error, QStringLiteral("Cannot write the keyring: %1").arg(why));
1095 return false;
1096 }
1097 return true;
1098}
1099
1100auto RnpCrypto::loadKeyring(const QString &dir, QString *error) -> bool {
1101 if (m_ffi == nullptr) {
1102 setError(error, QStringLiteral("RNP is not available"));
1103 return false;
1104 }
1105 // As saveKeyring(): the folder itself is not read through a link either.
1106 if (Util::isLinkedFolder(dir)) {
1107 setError(error,
1108 QStringLiteral("%1 is a link, not a keyring folder").arg(dir));
1109 return false;
1110 }
1111 const QString path = QDir(dir).filePath(QStringLiteral("keyring.gpg"));
1112 const QFileInfo info(path);
1113 if (!info.exists() && !info.isSymLink()) {
1114 return true; // no keyring yet
1115 }
1116 const std::optional<QByteArray> bytes =
1118 if (!bytes) {
1119 setError(error, QStringLiteral("Cannot read %1 as a keyring (a regular "
1120 "file of at most %2 bytes)")
1121 .arg(path)
1122 .arg(kMaxKeyringBytes));
1123 return false;
1124 }
1125 if (bytes->isEmpty()) {
1126 return true; // saved without keys
1127 }
1128 Input input = memoryInput(*bytes, error, "Cannot read the keyring");
1129 if (!input) {
1130 return false;
1131 }
1132 const rnp_result_t result =
1133 rnp_load_keys(m_ffi, "GPG", input.get(),
1134 RNP_LOAD_SAVE_PUBLIC_KEYS | RNP_LOAD_SAVE_SECRET_KEYS);
1135 if (result != RNP_SUCCESS) {
1136 setError(error,
1137 rnpError(QStringLiteral("Cannot load %1").arg(path), result));
1138 return false;
1139 }
1140 return true;
1141}
static constexpr qint64 kMaxKeyringBytes
Most bytes of a keyring file: saveKeyring() writes no larger one, loadKeyring() reads no larger one.
Definition rnpcrypto.h:104
auto sign(const QByteArray &data, const QString &signer, QString *error=nullptr) -> QByteArray
Makes a detached signature over data, as gpg –detach-sign –local-user signer does (for ....
void setPassphraseProvider(PassphraseProvider provider)
Sets how secret-key passphrases are obtained.
std::function< QString(const QString &keyId)> PassphraseProvider
Asks for the passphrase of a secret key.
Definition rnpcrypto.h:86
RnpCrypto()
Creates an empty keyring.
auto importKeys(const QByteArray &keyData, QString *error=nullptr) -> QStringList
Imports public and secret keys from armored or binary key data.
qint64 m_maxPlaintext
Largest plaintext decrypt() returns, in bytes.
Definition rnpcrypto.h:297
auto verify(const QByteArray &data, const QByteArray &signature, QString *error=nullptr) -> QStringList
Checks a detached signature over data, as QtPass reads gpg's VALIDSIG.
void setMaxPlaintextBytes(qint64 bytes)
Sets the most bytes of plaintext decrypt() may produce.
~RnpCrypto()
Frees the keyring and everything RNP holds for it.
rnp_ffi_st * m_ffi
RNP's handle holding this object's keyring; null when RNP could not be initialised,...
Definition rnpcrypto.h:290
auto protectKey(const QString &fingerprint, const QString &passphrase, QString *error=nullptr) -> bool
Protects the secret parts of a key with passphrase.
auto decrypt(const QByteArray &ciphertext, QString *error=nullptr, bool *ok=nullptr) -> QByteArray
Decrypts an OpenPGP message with any secret key in the keyring.
auto listKeys(bool secretOnly=false) const -> QList< KeyInfo >
The primary keys in the keyring.
auto isValid() const -> bool
Whether the RNP context could be created at all.
auto loadKeyring(const QString &dir, QString *error=nullptr) -> bool
Adds the keyring in dir (as saveKeyring() wrote it) to this one. Secret keys stay protected until an ...
auto encrypt(const QByteArray &plaintext, const QStringList &recipients, QString *error=nullptr) -> QByteArray
Encrypts plaintext to every recipient, as one message.
PassphraseProvider m_passphrase
Asked for the passphrase of a locked secret key during decryption; set with setPassphraseProvider(),...
Definition rnpcrypto.h:295
auto saveKeyring(const QString &dir, bool allowUnprotected=false, QString *error=nullptr) const -> bool
Writes the keyring to dir: keyring.gpg, public and secret keys in one file (OpenPGP keys,...
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
Definition util.cpp:1165
static auto readRegularFile(const QString &path, qint64 maxBytes) -> std::optional< QByteArray >
Read path whole, as the regular file it is (openRegularFile()), when it holds at most maxBytes.
Definition util.cpp:973
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
Definition util.cpp:761
RnpCrypto, in-process OpenPGP encryption and decryption through the RNP library.
One primary key in the keyring, as the Users dialog lists keys.
Definition rnpcrypto.h:46
QStringList userIds
Its user IDs that are still its own (valid, not revoked), in the keyring's order.
Definition rnpcrypto.h:55
QDateTime expires
When it expires; invalid when it does not.
Definition rnpcrypto.h:59
bool canSign
Whether it can sign now, with a secret key that is here.
Definition rnpcrypto.h:77
bool canEncrypt
Whether a message can be encrypted to it now.
Definition rnpcrypto.h:75
bool secretProtected
Whether every secret part of it that is here is protected by a passphrase; true when none is here.
Definition rnpcrypto.h:73
bool expired
Whether it has expired.
Definition rnpcrypto.h:63
QString fingerprint
The primary key's fingerprint, upper-case hex.
Definition rnpcrypto.h:48
QString keyId
The primary key's 16-hex-digit key ID.
Definition rnpcrypto.h:50
QDateTime created
When it was created.
Definition rnpcrypto.h:57
bool hasSecret
Whether a secret part of it (primary or subkey) is here to use: a GnuPG stub (a key left at home,...
Definition rnpcrypto.h:68
bool revoked
Whether it has been revoked.
Definition rnpcrypto.h:61
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...