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 }