File indexing completed on 2023-12-03 09:19:00

0001 /*
0002     SPDX-FileCopyrightText: 2009-2022 Rolf Eike Beer <kde@opensource.sf-tec.de>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "caff.h"
0007 #include "caff_p.h"
0008 
0009 #include "kgpg_general_debug.h"
0010 #include "kgpginterface.h"
0011 #include "kgpgsettings.h"
0012 #include "core/KGpgKeyNode.h"
0013 #include "transactions/kgpgdeluid.h"
0014 #include "transactions/kgpgencrypt.h"
0015 #include "transactions/kgpgexport.h"
0016 #include "transactions/kgpgimport.h"
0017 #include "transactions/kgpgsignuid.h"
0018 #include "gpgproc.h"
0019 
0020 #include <KLocalizedString>
0021 #include <KMessageBox>
0022 
0023 
0024 #include <QDesktopServices>
0025 #include <QDir>
0026 #include <QFileInfo>
0027 #include <QTemporaryDir>
0028 
0029 KGpgCaffPrivate::KGpgCaffPrivate(KGpgCaff *parent, const KGpgSignableNode::List &ids, const QStringList &signers,
0030         const KGpgCaff::OperationFlags flags, const KGpgSignTransactionHelper::carefulCheck checklevel)
0031     : QObject(parent),
0032     q_ptr(parent),
0033     m_signers(signers),
0034     m_flags(flags),
0035     m_checklevel(checklevel),
0036     m_gpgVersion(GPGProc::gpgVersion(GPGProc::gpgVersionString(QString()))),
0037     m_allids(ids)
0038 {
0039     const QString gpgCfg = KGpgSettings::gpgConfigPath();
0040     const QString secring = KgpgInterface::getGpgSetting(QLatin1String( "secret-keyring" ), gpgCfg);
0041 
0042     QFileInfo fn(gpgCfg);
0043     if (!secring.isEmpty()) {
0044         m_secringfile = secring;
0045     } else {
0046         fn.setFile(fn.dir(), QLatin1String("secring.gpg"));
0047         m_secringfile = QDir::toNativeSeparators(fn.absoluteFilePath());
0048     }
0049 
0050     fn.setFile(fn.dir(), QLatin1String("private-keys-v1.d"));
0051     m_secringdir = QDir::toNativeSeparators(fn.absoluteFilePath());
0052 }
0053 
0054 void
0055 KGpgCaffPrivate::reexportKey(const KGpgSignableNode *key)
0056 {
0057     Q_Q(KGpgCaff);
0058 
0059     Q_ASSERT(m_tempdir.isNull());
0060 
0061     // find out if the given id can be used for encryption
0062     const KGpgKeyNode *k;
0063     if (key->getType() & KgpgCore::ITYPE_PAIR)
0064         k = key->toKeyNode();
0065     else
0066         k = key->getParentKeyNode()->toKeyNode();
0067 
0068     // skip if not
0069     if (!k->canEncrypt()) {
0070         m_noEncIds << key;
0071         m_allids.removeFirst();
0072         checkNextLoop();
0073         return;
0074     }
0075 
0076     m_tempdir.reset(new QTemporaryDir());
0077 
0078     if (m_gpgVersion >= 0x20100) {
0079         /* see https://lists.gnupg.org/pipermail/gnupg-devel/2014-December/029296.html */
0080         QFile seclink(m_secringdir);
0081 
0082         if (!seclink.link(m_tempdir->path() + QLatin1String("private-keys-v1.d"))) {
0083             KMessageBox::error(qobject_cast<QWidget *>(q->parent()),
0084                     i18n("This function is not available on this system. The symbolic link to the private GnuPG keys cannot be created."));
0085             return;
0086         }
0087     }
0088 
0089     // export all keys necessary for signing
0090     QStringList exportkeys(m_signers);
0091     exportkeys << key->getKeyNode()->getId();
0092 
0093     KGpgImport *imp = new KGpgImport(this);
0094 
0095     QStringList expOptions(QLatin1String( "--export-options" ));
0096     expOptions << QLatin1String( "export-clean,export-attribute" );
0097     KGpgExport *exp = new KGpgExport(this, exportkeys, expOptions);
0098     exp->setOutputTransaction(imp);
0099 
0100     imp->setGnuPGHome(m_tempdir->path());
0101 
0102     connect(imp, &KGpgImport::done, this, &KGpgCaffPrivate::slotReimportDone);
0103     imp->start();
0104 }
0105 
0106 void
0107 KGpgCaffPrivate::slotReimportDone(int result)
0108 {
0109     KGpgImport *imp = qobject_cast<KGpgImport *>(sender());
0110 
0111     if (result != KGpgTransaction::TS_OK) {
0112         abortOperation(result);
0113     } else {
0114         if (imp->getImportedIds(0x1).count() != 1 + m_signers.count()) {
0115             abortOperation(-1);
0116         } else {
0117             KGpgSignUid *signuid = new KGpgSignUid(this, m_signers.first(), m_allids.first(), false, m_checklevel);
0118             signuid->setGnuPGHome(m_tempdir->path());
0119             if (m_gpgVersion < 0x20100)
0120                 signuid->setSecringFile(m_secringfile);
0121             connect(signuid, &KGpgSignUid::done, this, &KGpgCaffPrivate::slotSigningFinished);
0122 
0123             signuid->start();
0124         }
0125     }
0126 
0127     sender()->deleteLater();
0128 }
0129 
0130 void
0131 KGpgCaffPrivate::abortOperation(int result)
0132 {
0133     Q_Q(KGpgCaff);
0134 
0135     qCDebug(KGPG_LOG_GENERAL) << "transaction" << sender() << "failed, result" << result;
0136     m_tempdir.reset();
0137 
0138     Q_EMIT q->aborted();
0139 }
0140 
0141 void
0142 KGpgCaffPrivate::checkNextLoop()
0143 {
0144     Q_Q(KGpgCaff);
0145 
0146     m_tempdir.reset();
0147 
0148     if (m_allids.isEmpty()) {
0149         if (!m_noEncIds.isEmpty()) {
0150             QStringList ids;
0151 
0152             for (const KGpgSignableNode *nd : qAsConst(m_noEncIds))
0153                 if (nd->getEmail().isEmpty())
0154                     ids << i18nc("%1 is the key id, %2 is the name and comment of the key or uid",
0155                             "%1: %2", nd->getId(), nd->getNameComment());
0156                 else
0157                     ids << i18nc("%1 is the key id, %2 is the name and comment of the key or uid, %3 is the email address of the uid",
0158                             "%1: %2 &lt;%3&gt;", nd->getId(), nd->getNameComment(), nd->getEmail());
0159 
0160             KMessageBox::detailedError(qobject_cast<QWidget *>(q->parent()),
0161                     i18np("No mail was sent for the following user id because it belongs to a key without encryption capability:",
0162                             "No mail was sent for the following user ids because they belong to keys without encryption capability:",
0163                             m_noEncIds.count()),
0164                     ids.join(QLatin1String("\n")));
0165         }
0166 
0167         if (!m_alreadyIds.isEmpty()) {
0168             QStringList ids;
0169 
0170             for (const KGpgSignableNode *nd : qAsConst(m_alreadyIds))
0171                 if (nd->getEmail().isEmpty())
0172                     ids << i18nc("%1 is the key id, %2 is the name and comment of the key or uid",
0173                             "%1: %2", nd->getId(), nd->getNameComment());
0174                 else
0175                     ids << i18nc("%1 is the key id, %2 is the name and comment of the key or uid, %3 is the email address of the uid",
0176                             "%1: %2 &lt;%3&gt;", nd->getId(), nd->getNameComment(), nd->getEmail());
0177 
0178             KMessageBox::detailedError(qobject_cast<QWidget *>(q->parent()),
0179                     i18np("No mail was sent for the following user id because it was already signed:",
0180                             "No mail was sent for the following user ids because they were already signed:",
0181                             m_alreadyIds.count()),
0182                     ids.join(QLatin1String("\n")));
0183         }
0184 
0185         Q_EMIT q->done();
0186     } else {
0187         reexportKey(m_allids.first());
0188     }
0189 }
0190 
0191 void
0192 KGpgCaffPrivate::slotSigningFinished(int result)
0193 {
0194     sender()->deleteLater();
0195 
0196     if (result != KGpgTransaction::TS_OK) {
0197         if ((result == KGpgSignTransactionHelper::TS_ALREADY_SIGNED) && (m_flags & KGpgCaff::IgnoreAlreadySigned)) {
0198             m_alreadyIds << m_allids.takeFirst();
0199             checkNextLoop();
0200         } else {
0201             abortOperation(result);
0202         }
0203         return;
0204     }
0205 
0206     const KGpgSignableNode *uid = m_allids.first();
0207 
0208     // if there is no email address we can't send this out anyway, so don't bother.
0209     // could be improved: if this is the only selected uid from this key go and select
0210     // a proper mail address to send this to
0211     if (uid->getEmail().isEmpty()) {
0212         m_allids.removeFirst();
0213         checkNextLoop();
0214     }
0215 
0216     const KGpgKeyNode *key = uid->getKeyNode();
0217 
0218     int uidnum;
0219 
0220     if (uid == key) {
0221         uidnum = -1;
0222     } else {
0223         uidnum = -uid->getId().toInt();
0224     }
0225 
0226     KGpgDelUid::RemoveMode removeMode;
0227     switch (KGpgSettings::mailUats()) {
0228     case 0:
0229         removeMode = KGpgDelUid::RemoveWithEmail;
0230         break;
0231     case 1:
0232         if (uid == key) {
0233             removeMode = KGpgDelUid::RemoveWithEmail;
0234         } else {
0235             // check if this is the first uid with email address
0236             const KGpgSignableNode *otherUid;
0237             int index = 1;
0238             removeMode = KGpgDelUid::RemoveAllOther;
0239 
0240             while ( (otherUid = key->getUid(index++)) != nullptr) {
0241                 if (otherUid == uid) {
0242                     removeMode = KGpgDelUid::RemoveWithEmail;
0243                     break;
0244                 }
0245                 if (!otherUid->getEmail().isEmpty())
0246                     break;
0247             }
0248         }
0249         break;
0250     case 2:
0251         removeMode = KGpgDelUid::RemoveAllOther;
0252         break;
0253     default:
0254         Q_ASSERT(0);
0255         return;
0256     }
0257 
0258     KGpgDelUid *deluid = new KGpgDelUid(this, key, uidnum, removeMode);
0259 
0260     deluid->setGnuPGHome(m_tempdir->path());
0261 
0262     connect(deluid, &KGpgDelUid::done, this, &KGpgCaffPrivate::slotDelUidFinished);
0263 
0264     deluid->start();
0265 }
0266 
0267 void
0268 KGpgCaffPrivate::slotDelUidFinished(int result)
0269 {
0270     sender()->deleteLater();
0271 
0272     const KGpgSignableNode *uid = m_allids.first();
0273     const KGpgKeyNode *key = uid->getKeyNode();
0274 
0275     if (result != KGpgTransaction::TS_OK) {
0276         // it's no error if we tried to delete all other ids but there is no other id
0277         if ((uid != key) || (result != KGpgDelUid::TS_NO_SUCH_UID)) {
0278             abortOperation(result);
0279             return;
0280         }
0281     }
0282 
0283     QStringList expOptions(QLatin1String( "--export-options" ));
0284     expOptions << QLatin1String( "export-attribute" );
0285 
0286     KGpgExport *exp = new KGpgExport(this, QStringList(key->getId()), expOptions);
0287 
0288     exp->setGnuPGHome(m_tempdir->path());
0289 
0290     connect(exp, &KGpgExport::done, this, &KGpgCaffPrivate::slotExportFinished);
0291 
0292     exp->start();
0293 }
0294 
0295 void
0296 KGpgCaffPrivate::slotExportFinished(int result)
0297 {
0298     sender()->deleteLater();
0299 
0300     if (result != KGpgTransaction::TS_OK) {
0301         abortOperation(result);
0302         return;
0303     }
0304 
0305     const KGpgSignableNode *uid = m_allids.first();
0306     const KGpgKeyNode *key = uid->getKeyNode();
0307 
0308     KGpgExport *exp = qobject_cast<KGpgExport *>(sender());
0309     Q_ASSERT(exp != nullptr);
0310 
0311     QString body = KGpgSettings::emailTemplate();
0312     body.replace(QLatin1Char( '%' ) + i18nc("Email template placeholder for key id", "KEYID") + QLatin1Char( '%' ), key->getId());
0313     body.replace(QLatin1Char( '%' ) + i18nc("Email template placeholder for key id", "UIDNAME") + QLatin1Char( '%' ), uid->getNameComment());
0314 
0315     body += QLatin1Char( '\n' ) + QLatin1String( exp->getOutputData() );
0316 
0317     KGpgEncrypt *enc = new KGpgEncrypt(this, QStringList(key->getId()), body, KGpgEncrypt::AsciiArmored | KGpgEncrypt::AllowUntrustedEncryption);
0318 
0319     // Set the home directory to make sure custom encrypt options
0320     // as well as the "always encrypt to" setting are not honored.
0321     enc->setGnuPGHome(m_tempdir->path());
0322 
0323     connect(enc, &KGpgEncrypt::done, this, &KGpgCaffPrivate::slotTextEncrypted);
0324 
0325     enc->start();
0326 }
0327 
0328 void
0329 KGpgCaffPrivate::slotTextEncrypted(int result)
0330 {
0331     sender()->deleteLater();
0332 
0333     switch (result) {
0334     case KGpgTransaction::TS_OK: {
0335         KGpgEncrypt *enc = qobject_cast<KGpgEncrypt *>(sender());
0336         Q_ASSERT(enc != nullptr);
0337 
0338         const QString text = enc->encryptedText().join(QLatin1String("\n"));
0339 
0340         const KGpgSignableNode *uid = m_allids.takeFirst();
0341 
0342         const QString email = uid->getEmail();
0343         const QString keyid = uid->getKeyNode()->getId();
0344 
0345         QDesktopServices::openUrl(QUrl(QLatin1String("mailto:") + email +
0346                 QLatin1String("?subject=") + i18nc("%1 is 64 bit key id (in hex), text is used as email subject", "Your key %1", keyid) +
0347                 QLatin1String("&body=") + text));
0348         break;
0349         }
0350     default:
0351         abortOperation(result);
0352         break;
0353     case KGpgTransaction::TS_USER_ABORTED:
0354         m_allids.clear();
0355         break;
0356     }
0357 
0358     checkNextLoop();
0359 }
0360 
0361 KGpgCaff::KGpgCaff(QObject *parent, const KGpgSignableNode::List &ids, const QStringList &signids,
0362         const int checklevel, const OperationFlags flags)
0363     : QObject(parent),
0364     d_ptr(new KGpgCaffPrivate(this, ids, signids, flags, static_cast<KGpgSignTransactionHelper::carefulCheck>(checklevel)))
0365 {
0366 }
0367 
0368 void
0369 KGpgCaff::run()
0370 {
0371     Q_D(KGpgCaff);
0372 
0373     d->reexportKey(d->m_allids.first());
0374 }