File indexing completed on 2024-05-19 05:26:26

0001 /*
0002     Copyright (c) 2009 Constantin Berzan <exit3219@gmail.com>
0003     Copyright (C) 2010 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com
0004     Copyright (c) 2010 Leo Franchi <lfranchi@kde.org>
0005     Copyright (c) 2017 Christian Mollekopf <mollekopf@kolabsys.com>
0006 
0007     This library is free software; you can redistribute it and/or modify it
0008     under the terms of the GNU Library General Public License as published by
0009     the Free Software Foundation; either version 2 of the License, or (at your
0010     option) any later version.
0011 
0012     This library is distributed in the hope that it will be useful, but WITHOUT
0013     ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
0014     FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Library General Public
0015     License for more details.
0016 
0017     You should have received a copy of the GNU Library General Public License
0018     along with this library; see the file COPYING.LIB.  If not, write to the
0019     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
0020     02110-1301, USA.
0021 */
0022 #include "crypto.h"
0023 
0024 #include "errors.h"
0025 
0026 #include <gpgme.h>
0027 
0028 #include <QDebug>
0029 #include <QDateTime>
0030 #include <QFile>
0031 
0032 #include <future>
0033 #include <utility>
0034 
0035 using namespace Crypto;
0036 
0037 QDebug operator<< (QDebug d, const Key &key)
0038 {
0039     d << key.fingerprint;
0040     return d;
0041 }
0042 
0043 QDebug operator<< (QDebug d, const Error &error)
0044 {
0045     d << error.error;
0046     return d;
0047 }
0048 
0049 namespace Crypto {
0050     struct Data {
0051         Data(const QByteArray &buffer)
0052         {
0053             const bool copy = false;
0054             const gpgme_error_t e = gpgme_data_new_from_mem(&data, buffer.constData(), buffer.size(), int(copy));
0055             if (e) {
0056                 qWarning() << "Failed to copy data?" << e;
0057             }
0058         }
0059 
0060         ~Data()
0061         {
0062             gpgme_data_release(data);
0063         }
0064         gpgme_data_t data;
0065     };
0066 }
0067 
0068 static gpgme_error_t checkEngine(CryptoProtocol protocol)
0069 {
0070     gpgme_check_version(0);
0071     const gpgme_protocol_t p = protocol == CMS ? GPGME_PROTOCOL_CMS : GPGME_PROTOCOL_OpenPGP;
0072     return gpgme_engine_check_version(p);
0073 }
0074 
0075 static std::pair<gpgme_error_t, gpgme_ctx_t> createForProtocol(CryptoProtocol proto)
0076 {
0077     if (auto e = checkEngine(proto)) {
0078         qWarning() << "GPG Engine check failed." << e;
0079         return std::make_pair(e, nullptr);
0080     }
0081     gpgme_ctx_t ctx = 0;
0082     if (auto e = gpgme_new(&ctx)) {
0083         return std::make_pair(e, nullptr);
0084     }
0085 
0086     switch (proto) {
0087         case OpenPGP:
0088             if (auto e = gpgme_set_protocol(ctx, GPGME_PROTOCOL_OpenPGP)) {
0089                 gpgme_release(ctx);
0090                 return std::make_pair(e, nullptr);
0091             }
0092         break;
0093         case CMS:
0094             if (auto e = gpgme_set_protocol(ctx, GPGME_PROTOCOL_CMS)) {
0095                 gpgme_release(ctx);
0096                 return std::make_pair(e, nullptr);
0097             }
0098         break;
0099         default:
0100             Q_ASSERT(false);
0101             return std::make_pair(1, nullptr);
0102     }
0103 
0104     //We want the output to always be ASCII armored
0105     gpgme_set_armor(ctx, 1);
0106 
0107     //Trust new keys
0108     if (auto e = gpgme_set_ctx_flag(ctx, "trust-model", "tofu+pgp")) {
0109         gpgme_release(ctx);
0110         return std::make_pair(e, nullptr);
0111     }
0112 
0113     //That's a great way to bring signature verification to a crawl
0114     if (auto e = gpgme_set_ctx_flag(ctx, "auto-key-retrieve", "0")) {
0115         gpgme_release(ctx);
0116         return std::make_pair(e, nullptr);
0117     }
0118 
0119     return std::make_pair(GPG_ERR_NO_ERROR, ctx);
0120 }
0121 
0122 gpgme_error_t gpgme_passphrase(void *hook, const char *uid_hint, const char *passphrase_info, int prev_was_bad, int fd)
0123 {
0124     Q_UNUSED(hook);
0125     Q_UNUSED(prev_was_bad);
0126     //uid_hint will be something like "CAA5183608F0FB50 Test1 Kolab <test1@kolab.org>" (CAA518... is the key)
0127     //pahhphrase_info will be something like "CAA5183608F0FB50 2E3B7787B1B75920 1 0"
0128     qInfo() << "Requested passphrase for " << (uid_hint ? QByteArray{uid_hint} : QByteArray{}) << (passphrase_info ? QByteArray{passphrase_info} : QByteArray{});
0129 
0130     QFile file;
0131     file.open(fd, QIODevice::WriteOnly);
0132     //FIXME hardcoded as a test
0133     auto passphrase = QByteArray{"test1"} + QByteArray{"\n"};
0134     file.write(passphrase);
0135     file.close();
0136 
0137     return 0;
0138 }
0139 
0140 
0141 namespace Crypto {
0142 struct Context {
0143     Context(CryptoProtocol protocol = OpenPGP)
0144     {
0145         gpgme_error_t code;
0146         std::tie(code, context) = createForProtocol(protocol);
0147         error = Error{code};
0148         //For enabling the loopback passphrase
0149         //if (!error) {
0150         //    gpgme_set_passphrase_cb (context, gpgme_passphrase, 0);
0151         //    //TODO requires allow-loopback-pinentry to be enabled in the gpg-agent.conf
0152         //    gpgme_set_pinentry_mode(context, GPGME_PINENTRY_MODE_LOOPBACK);
0153         //}
0154     }
0155 
0156     ~Context()
0157     {
0158         gpgme_release(context);
0159     }
0160 
0161     operator bool() const
0162     {
0163         return !error;
0164     }
0165     Error error;
0166     gpgme_ctx_t context;
0167 };
0168 }
0169 
0170 
0171 static QByteArray toBA(gpgme_data_t out)
0172 {
0173     size_t length = 0;
0174     auto data = gpgme_data_release_and_get_mem (out, &length);
0175     auto outdata = QByteArray{data, static_cast<int>(length)};
0176     gpgme_free(data);
0177     return outdata;
0178 }
0179 
0180 static std::vector<Recipient> copyRecipients(gpgme_decrypt_result_t result)
0181 {
0182     std::vector<Recipient> recipients;
0183     for (gpgme_recipient_t r = result->recipients ; r ; r = r->next) {
0184         recipients.push_back({QByteArray{r->keyid}, r->status != GPG_ERR_NO_SECKEY});
0185     }
0186     return recipients;
0187 }
0188 
0189 static std::vector<Signature> copySignatures(gpgme_verify_result_t result)
0190 {
0191     std::vector<Signature> signatures;
0192     for (gpgme_signature_t is = result->signatures ; is ; is = is->next) {
0193         Signature sig;
0194         sig.fingerprint = QByteArray{is->fpr};
0195         sig.creationTime.setTime_t(is->timestamp);
0196         if (is->summary & GPGME_SIGSUM_VALID) {
0197             sig.result = Signature::Ok;
0198         } else {
0199             sig.result = Signature::Invalid;
0200             if (is->summary & GPGME_SIGSUM_KEY_EXPIRED) {
0201                 sig.result = Signature::Expired;
0202             }
0203             if (is->summary & GPGME_SIGSUM_KEY_MISSING) {
0204                 sig.result = Signature::KeyNotFound;
0205             }
0206         }
0207         sig.status = {is->status};
0208         sig.isTrusted = is->validity == GPGME_VALIDITY_FULL || is->validity == GPGME_VALIDITY_ULTIMATE;
0209         signatures.push_back(sig);
0210     }
0211     return signatures;
0212 }
0213 
0214 
0215 VerificationResult Crypto::verifyDetachedSignature(CryptoProtocol protocol, const QByteArray &signature, const QByteArray &text)
0216 {
0217     Context context{protocol};
0218     if (!context) {
0219         qWarning() << "Failed to create context " << context.error;
0220         return {{}, context.error};
0221     }
0222     auto ctx = context.context;
0223 
0224     auto err = gpgme_op_verify(ctx, Data{signature}.data, Data{text}.data, 0);
0225     gpgme_verify_result_t res = gpgme_op_verify_result(ctx);
0226     return {copySignatures(res), {err}};
0227 }
0228 
0229 VerificationResult Crypto::verifyOpaqueSignature(CryptoProtocol protocol, const QByteArray &signature, QByteArray &outdata)
0230 {
0231     Context context{protocol};
0232     if (!context) {
0233         qWarning() << "Failed to create context " << context.error;
0234         return VerificationResult{{}, context.error};
0235     }
0236     auto ctx = context.context;
0237 
0238     gpgme_data_t out;
0239     const gpgme_error_t e = gpgme_data_new(&out);
0240     Q_ASSERT(!e);
0241     auto err = gpgme_op_verify(ctx, Data{signature}.data, 0, out);
0242 
0243     VerificationResult result{{}, {err}};
0244     if (gpgme_verify_result_t res = gpgme_op_verify_result(ctx)) {
0245         result.signatures = copySignatures(res);
0246     }
0247 
0248     outdata = toBA(out);
0249     return result;
0250 }
0251 
0252 static DecryptionResult::Result toResult(gpgme_error_t err)
0253 {
0254     if (err == GPG_ERR_NO_DATA) {
0255         return DecryptionResult::NotEncrypted;
0256     } else if (err == GPG_ERR_NO_SECKEY) {
0257         return DecryptionResult::NoSecretKeyError;
0258     } else if (err == GPG_ERR_CANCELED || err == GPG_ERR_INV_PASSPHRASE) {
0259         return DecryptionResult::PassphraseError;
0260     }
0261     qWarning() << "unknown error" << err << gpgme_strerror(err);
0262     return DecryptionResult::NoSecretKeyError;
0263 }
0264 
0265 std::pair<DecryptionResult,VerificationResult> Crypto::decryptAndVerify(CryptoProtocol protocol, const QByteArray &ciphertext, QByteArray &outdata)
0266 {
0267     Context context{protocol};
0268     if (!context) {
0269         qWarning() << "Failed to create context " << gpgme_strerror(context.error);
0270         qWarning() << "returning early";
0271         return std::make_pair(DecryptionResult{{}, {context.error}, DecryptionResult::NoSecretKeyError}, VerificationResult{{}, context.error});
0272     }
0273     auto ctx = context.context;
0274 
0275     gpgme_data_t out;
0276     if (gpgme_error_t e = gpgme_data_new(&out)) {
0277         qWarning() << "Failed to allocated data" << e;
0278     }
0279     auto err = gpgme_op_decrypt_verify(ctx, Data{ciphertext}.data, out);
0280     if (err) {
0281         qWarning() << "Failed to decrypt and verify" << Error{err};
0282         //We make sure we don't return any plain-text if the decryption failed to prevent EFAIL
0283         if (err == GPG_ERR_DECRYPT_FAILED) {
0284             return std::make_pair(DecryptionResult{{}, {err}, DecryptionResult::DecryptionError}, VerificationResult{{}, {err}});
0285         }
0286     }
0287 
0288     VerificationResult verificationResult{{}, {err}};
0289     if (gpgme_verify_result_t res = gpgme_op_verify_result(ctx)) {
0290         verificationResult.signatures = copySignatures(res);
0291     }
0292 
0293     DecryptionResult decryptionResult{{}, {err}};
0294     if (gpgme_decrypt_result_t res = gpgme_op_decrypt_result(ctx)) {
0295         decryptionResult.recipients = copyRecipients(res);
0296     }
0297     decryptionResult.result = toResult(err);
0298 
0299     outdata = toBA(out);
0300     return std::make_pair(decryptionResult, verificationResult);
0301 }
0302 
0303 static DecryptionResult decryptGPGME(CryptoProtocol protocol, const QByteArray &ciphertext, QByteArray &outdata)
0304 {
0305     Context context{protocol};
0306     if (!context) {
0307         qWarning() << "Failed to create context " << context.error;
0308         return DecryptionResult{{}, context.error};
0309     }
0310     auto ctx = context.context;
0311 
0312     gpgme_data_t out;
0313     if (gpgme_error_t e = gpgme_data_new(&out)) {
0314         qWarning() << "Failed to allocated data" << e;
0315     }
0316     auto err = gpgme_op_decrypt(ctx, Data{ciphertext}.data, out);
0317     if (err) {
0318         qWarning() << "Failed to decrypt" << gpgme_strerror(err);
0319         //We make sure we don't return any plain-text if the decryption failed to prevent EFAIL
0320         if (err == GPG_ERR_DECRYPT_FAILED) {
0321             return DecryptionResult{{}, {err}};
0322         }
0323     }
0324 
0325     DecryptionResult decryptionResult{{}, {err}};
0326     if (gpgme_decrypt_result_t res = gpgme_op_decrypt_result(ctx)) {
0327         decryptionResult.recipients = copyRecipients(res);
0328     }
0329 
0330     decryptionResult.result = toResult(err);
0331 
0332     outdata = toBA(out);
0333     return decryptionResult;
0334 }
0335 
0336 DecryptionResult Crypto::decrypt(CryptoProtocol protocol, const QByteArray &ciphertext, QByteArray &outdata)
0337 {
0338         return decryptGPGME(protocol, ciphertext, outdata);
0339 }
0340 
0341 ImportResult Crypto::importKey(CryptoProtocol protocol, const QByteArray &certData)
0342 {
0343     Context context{protocol};
0344     if (!context) {
0345         qWarning() << "Failed to create context " << context.error;
0346         return {0, 0, 0};
0347     }
0348     if (gpgme_op_import(context.context, Data{certData}.data)) {
0349         qWarning() << "Import failed";
0350         return {0, 0, 0};
0351     }
0352     if (auto result = gpgme_op_import_result(context.context)) {
0353         return {result->considered, result->imported, result->unchanged};
0354     } else {
0355         return {0, 0, 0};
0356     }
0357 }
0358 
0359 static bool validateKey(const gpgme_key_t key)
0360 {
0361     if (key->revoked) {
0362         qWarning() << "Key is revoked " << key->fpr;
0363         return false;
0364     }
0365     if (key->expired) {
0366         qWarning() << "Key is expired " << key->fpr;
0367         return false;
0368     }
0369     if (key->disabled) {
0370         qWarning() << "Key is disabled " << key->fpr;
0371         return false;
0372     }
0373     if (key->invalid) {
0374         qWarning() << "Key is invalid " << key->fpr;
0375         return false;
0376     }
0377     return true;
0378 }
0379 
0380 
0381 static KeyListResult listKeys(CryptoProtocol protocol, const std::vector<const char*> &patterns, bool secretOnly, int keyListMode, bool importKeys)
0382 {
0383     Context context{protocol};
0384     if (!context) {
0385         qWarning() << "Failed to create context " << context.error;
0386         return {{}, context.error};
0387     }
0388     auto ctx = context.context;
0389 
0390     gpgme_set_keylist_mode(ctx, keyListMode);
0391 
0392     KeyListResult result;
0393     result.error = {GPG_ERR_NO_ERROR};
0394     auto zeroTerminatedPatterns = patterns;
0395     zeroTerminatedPatterns.push_back(0);
0396     if (patterns.size() > 1) {
0397         if (auto err = gpgme_op_keylist_ext_start(ctx, const_cast<const char **>(zeroTerminatedPatterns.data()), int(secretOnly), 0)) {
0398             result.error = {err};
0399             qWarning() << "Error while listing keys:" << result.error;
0400         }
0401     } else if (patterns.size() == 1) {
0402         if (auto err = gpgme_op_keylist_start(ctx, zeroTerminatedPatterns[0], int(secretOnly))) {
0403             result.error = {err};
0404             qWarning() << "Error while listing keys:" << result.error;
0405         }
0406     } else {
0407         if (auto err = gpgme_op_keylist_start(ctx, 0, int(secretOnly))) {
0408             result.error = {err};
0409             qWarning() << "Error while listing keys:" << result.error;
0410         }
0411     }
0412 
0413 
0414     std::vector<gpgme_key_t> listedKeys;
0415     while (true) {
0416         gpgme_key_t key;
0417         if (auto err = gpgme_op_keylist_next(ctx, &key)) {
0418             if (gpgme_err_code(err) != GPG_ERR_EOF) {
0419                 qWarning() << "Error after listing keys" << result.error << gpgme_strerror(err);
0420             }
0421             break;
0422         }
0423 
0424         listedKeys.push_back(key);
0425 
0426         Key k;
0427         if (key->subkeys) {
0428             k.keyId = QByteArray{key->subkeys->keyid};
0429             k.shortKeyId = k.keyId.right(8);
0430             k.fingerprint = QByteArray{key->subkeys->fpr};
0431         }
0432         for (gpgme_user_id_t uid = key->uids ; uid ; uid = uid->next) {
0433             k.userIds.push_back(UserId{QByteArray{uid->name}, QByteArray{uid->email}, QByteArray{uid->uid}});
0434         }
0435         k.isUsable = validateKey(key);
0436         result.keys.push_back(k);
0437     }
0438     gpgme_op_keylist_end(ctx);
0439 
0440     if (importKeys && !listedKeys.empty()) {
0441         listedKeys.push_back(0);
0442         if (auto err = gpgme_op_import_keys(ctx, const_cast<gpgme_key_t*>(listedKeys.data()))) {
0443             qWarning() << "Error while importing keys" << gpgme_strerror(err);
0444         }
0445     }
0446     return result;
0447 }
0448 
0449 /**
0450  * Get the given `key` in the armor format.
0451  */
0452 Expected<Error, QByteArray> Crypto::exportPublicKey(const Key &key)
0453 {
0454     Context context;
0455     if (!context) {
0456         return makeUnexpected(Error{context.error});
0457     }
0458 
0459     gpgme_data_t out;
0460     const gpgme_error_t e = gpgme_data_new(&out);
0461     Q_ASSERT(!e);
0462 
0463     qDebug() << "Exporting public key:" << key.keyId;
0464     if (auto err = gpgme_op_export(context.context, key.keyId, 0, out)) {
0465         return makeUnexpected(Error{err});
0466     }
0467 
0468     return toBA(out);
0469 }
0470 
0471 Expected<Error, QByteArray> Crypto::signAndEncrypt(const QByteArray &content, const std::vector<Key> &encryptionKeys, const std::vector<Key> &signingKeys)
0472 {
0473     Context context;
0474     if (!context) {
0475         return makeUnexpected(Error{context.error});
0476     }
0477 
0478     for (const auto &signingKey : signingKeys) {
0479         qDebug() << "Signing with " << signingKey;
0480         //TODO do we have to free those again?
0481         gpgme_key_t key;
0482         if (auto e = gpgme_get_key(context.context, signingKey.fingerprint, &key, /*secret*/ false)) {
0483             qWarning() << "Failed to retrieve signing key " << signingKey.fingerprint << Error{e};
0484             return makeUnexpected(Error{e});
0485         } else {
0486             if (!key->can_sign || !validateKey(key)) {
0487                 qWarning() << "Key cannot be used for signing " << key->fpr;
0488                 return makeUnexpected(Error{e});
0489             }
0490             gpgme_signers_add(context.context, key);
0491         }
0492     }
0493 
0494     gpgme_key_t * const keys = new gpgme_key_t[encryptionKeys.size() + 1];
0495     gpgme_key_t * keys_it = keys;
0496     for (const auto &k : encryptionKeys) {
0497         qDebug() << "Encrypting to " << k;
0498         gpgme_key_t key;
0499         if (auto e = gpgme_get_key(context.context, k.fingerprint, &key, /*secret*/ false)) {
0500             qWarning() << "Failed to retrieve key " << k.fingerprint << Error{e};
0501             delete[] keys;
0502             return makeUnexpected(Error{e});
0503         } else {
0504             if (!key->can_encrypt || !validateKey(key)) {
0505                 qWarning() << "Key cannot be used for encryption " << k.fingerprint;
0506                 delete[] keys;
0507                 return makeUnexpected(Error{e});
0508             }
0509             *keys_it++ = key;
0510         }
0511     }
0512     *keys_it++ = 0;
0513 
0514     gpgme_data_t out;
0515     if (auto e = gpgme_data_new(&out)) {
0516         qWarning() << "Failed to allocate output buffer";
0517         delete[] keys;
0518         return makeUnexpected(Error{e});
0519     }
0520 
0521     gpgme_error_t err = !signingKeys.empty() ?
0522         gpgme_op_encrypt_sign(context.context, keys, GPGME_ENCRYPT_ALWAYS_TRUST, Data{content}.data, out) :
0523         gpgme_op_encrypt(context.context, keys, GPGME_ENCRYPT_ALWAYS_TRUST, Data{content}.data, out);
0524     delete[] keys;
0525     if (err) {
0526         qWarning() << "Encryption failed:" << gpgme_strerror(err);
0527         switch (gpgme_err_code(err)) {
0528             case GPG_ERR_UNUSABLE_PUBKEY:
0529                 for (const auto &k : encryptionKeys) {
0530                     qWarning() << "Encryption key:" << k;
0531                 }
0532                 break;
0533             case GPG_ERR_UNUSABLE_SECKEY:
0534                 for (const auto &k : signingKeys) {
0535                     qWarning() << "Signing key:" << k;
0536                 }
0537                 break;
0538             default:
0539                 break;
0540         }
0541         return makeUnexpected(Error{err});
0542     }
0543 
0544     return toBA(out);
0545 }
0546 
0547 Expected<Error, std::pair<QByteArray, QString>>
0548 Crypto::sign(const QByteArray &content, const std::vector<Key> &signingKeys)
0549 {
0550     Context context;
0551     if (!context) {
0552         return makeUnexpected(Error{context.error});
0553     }
0554 
0555     for (const auto &signingKey : signingKeys) {
0556         //TODO do we have to free those again?
0557         gpgme_key_t key;
0558         if (auto e = gpgme_get_key(context.context, signingKey.fingerprint, &key, /*secret*/ false)) {
0559             qWarning() << "Failed to retrieve signing key " << signingKey.fingerprint << Error{e};
0560             return makeUnexpected(Error{e});
0561         } else {
0562             gpgme_signers_add(context.context, key);
0563         }
0564     }
0565 
0566     gpgme_data_t out;
0567     const gpgme_error_t e = gpgme_data_new(&out);
0568     Q_ASSERT(!e);
0569 
0570     if (auto err = gpgme_op_sign(context.context, Data{content}.data, out, GPGME_SIG_MODE_DETACH)) {
0571         qWarning() << "Signing failed:" << Error{err};
0572         return makeUnexpected(Error{err});
0573     }
0574 
0575 
0576     const QByteArray algo = [&] {
0577         if (gpgme_sign_result_t res = gpgme_op_sign_result(context.context)) {
0578             if (gpgme_new_signature_t is = res->signatures) {
0579                 return QByteArray{gpgme_hash_algo_name(is->hash_algo)};
0580             }
0581         }
0582         return QByteArray{};
0583     }();
0584     // RFC 3156 Section 5:
0585     // Hash-symbols are constructed [...] by converting the text name to lower
0586     // case and prefixing it with the four characters "pgp-".
0587     const auto micAlg = (QString("pgp-") + algo).toLower();
0588 
0589     return std::pair<QByteArray, QString>{toBA(out), micAlg};
0590 }
0591 
0592 std::vector<Key> Crypto::findKeys(const QStringList &patterns, bool findPrivate, bool remote)
0593 {
0594     QByteArrayList list;
0595     std::transform(patterns.constBegin(), patterns.constEnd(), std::back_inserter(list), [] (const QString &s) { return s.toUtf8(); });
0596     std::vector<char const *> pattern;
0597     std::transform(list.constBegin(), list.constEnd(), std::back_inserter(pattern), [] (const QByteArray &s) { return s.constData(); });
0598 
0599     const KeyListResult res = listKeys(OpenPGP, pattern, findPrivate, remote ? GPGME_KEYLIST_MODE_EXTERN : GPGME_KEYLIST_MODE_LOCAL, remote);
0600     if (res.error) {
0601         qWarning() << "Failed to lookup keys: " << res.error;
0602         return {};
0603     }
0604     qDebug() << "Found " << res.keys.size() << " keys for the patterns: " << patterns;
0605 
0606     std::vector<Key> usableKeys;
0607     for (const auto &key : res.keys) {
0608         if (!key.isUsable) {
0609             qWarning() <<  "Key is not usable: " << key.fingerprint;
0610             continue;
0611         }
0612 
0613         qDebug() << "Key:" << key.fingerprint;
0614         for (const auto &userId : key.userIds) {
0615             qDebug() << "  userID:" << userId.email;
0616         }
0617         usableKeys.push_back(key);
0618     }
0619     return usableKeys;
0620 }