File indexing completed on 2024-04-28 09:46:06

0001 /*
0002     SPDX-FileCopyrightText: 2012-2022 Rolf Eike Beer <kde@opensource.sf-tec.de>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "kgpgverify.h"
0007 
0008 #include "gpgproc.h"
0009 #include "core/KGpgKeyNode.h"
0010 #include "model/kgpgitemmodel.h"
0011 
0012 #include <QLocale>
0013 #include <QRegularExpression>
0014 
0015 #include <KLocalizedString>
0016 
0017 KGpgVerify::KGpgVerify(QObject *parent, const QString &text)
0018     : KGpgTextOrFileTransaction(parent, text),
0019     m_fileIndex(-1)
0020 {
0021 }
0022 
0023 KGpgVerify::KGpgVerify(QObject *parent, const QList<QUrl> &files)
0024     : KGpgTextOrFileTransaction(parent, files),
0025     m_fileIndex(0)
0026 {
0027 }
0028 
0029 QStringList
0030 KGpgVerify::command() const
0031 {
0032     QStringList ret(QLatin1String("--verify"));
0033 
0034     return ret;
0035 }
0036 
0037 bool
0038 KGpgVerify::nextLine(const QString &line)
0039 {
0040     if (line.startsWith(QLatin1String("[GNUPG:] NO_PUBKEY "))) {
0041         setSuccess(TS_MISSING_KEY);
0042         m_missingId = line.mid(19).simplified();
0043         return false;
0044     }
0045 
0046     if (line.startsWith(QLatin1String("[GNUPG:] ")) &&
0047             line.contains(QLatin1String("SIG"))) {
0048         if (line.startsWith(QLatin1String("[GNUPG:] BADSIG")))
0049             setSuccess(KGpgVerify::TS_BAD_SIGNATURE);
0050         else
0051             setSuccess(KGpgTransaction::TS_OK);
0052     }
0053 
0054     return KGpgTextOrFileTransaction::nextLine(line);
0055 }
0056 
0057 void
0058 KGpgVerify::finish()
0059 {
0060     // GnuPG will return error code 2 if it wasn't able to verify the file.
0061     // If it complained about a missing signature before that is fine.
0062     if (((getProcess()->exitCode() == 2) && (getSuccess() == TS_MISSING_KEY)) ||
0063             ((getProcess()->exitCode() == 1) && (getSuccess() == TS_BAD_SIGNATURE)))
0064         return;
0065 
0066     KGpgTextOrFileTransaction::finish();
0067 }
0068 
0069 static QString
0070 sigTimeMessage(const QString &sigtime)
0071 {
0072     QDateTime stamp;
0073     if (sigtime.contains(QLatin1Char('T'))) {
0074         stamp = QDateTime::fromString(sigtime, Qt::ISODate);
0075     } else {
0076         bool ok;
0077         qint64 secs = sigtime.toLongLong(&ok);
0078         if (ok)
0079             stamp = QDateTime::fromMSecsSinceEpoch(secs * 1000);
0080     }
0081 
0082     if (!stamp.isValid())
0083         return QString();
0084 
0085     return xi18nc("@info first argument is formatted date, second argument is formatted time",
0086                     "<para>The signature was created at %1 %2</para>",
0087                     QLocale().toString(stamp.date(), QLocale::ShortFormat),
0088                     QLocale().toString(stamp.time(), QLocale::ShortFormat));
0089 }
0090 
0091 QString
0092 KGpgVerify::getReport(const QStringList &log, const KGpgItemModel *model)
0093 {
0094     QString result;
0095     // newer versions of GnuPG emit both VALIDSIG and GOODSIG
0096     // for a good signature. Since VALIDSIG has more information
0097     // we use that.
0098     const QRegularExpression validsig(QStringLiteral("^\\[GNUPG:\\] VALIDSIG([ ]+[^ ]+){10,}.*$"));
0099     const bool useGoodSig = (model == nullptr) || (log.indexOf(validsig) == -1);
0100     QString sigtime;    // timestamp of signature creation
0101 
0102     for (const QString &line : log) {
0103         if (!line.startsWith(QLatin1String("[GNUPG:] ")))
0104             continue;
0105 
0106         const QString msg = line.mid(9);
0107 
0108         if (!useGoodSig && msg.startsWith(QLatin1String("VALIDSIG "))) {
0109             // from GnuPG source, doc/DETAILS:
0110             //   VALIDSIG    <fingerprint in hex> <sig_creation_date> <sig-timestamp>
0111             //                <expire-timestamp> <sig-version> <reserved> <pubkey-algo>
0112             //                <hash-algo> <sig-class> <primary-key-fpr>
0113             const QStringList vsig = msg.mid(9).split(QLatin1Char(' '), Qt::SkipEmptyParts);
0114             Q_ASSERT(vsig.count() >= 10);
0115 
0116             const KGpgKeyNode *node = model->findKeyNode(vsig[9]);
0117 
0118             if (node != nullptr) {
0119                 // ignore for now if this is signed with the primary id (vsig[0] == vsig[9]) or not
0120                 if (node->getEmail().isEmpty())
0121                     result += xi18nc("@info Good signature from: NAME , Key ID: HEXID",
0122                             "<para>Good signature from:<nl/><emphasis strong='true'>%1</emphasis><nl/>Key ID: %2<nl/></para>",
0123                             node->getName(), vsig[9]);
0124                 else
0125                     result += xi18nc("@info Good signature from: NAME <EMAIL>, Key ID: HEXID",
0126                             "<para>Good signature from:<nl/><emphasis strong='true'>%1 <email>%2</email></emphasis><nl/>Key ID: %3</para>",
0127                             node->getName(), node->getEmail(), vsig[9]);
0128 
0129                 result += sigTimeMessage(vsig[2]);
0130             } else {
0131                 // this should normally never happen, but one could delete
0132                 // the key just after the verification. Brute force solution:
0133                 // do the whole report generation again, but this time make
0134                 // sure GOODSIG is used.
0135                 return getReport(log, nullptr);
0136             }
0137         } else if (msg.startsWith(QLatin1String("UNEXPECTED")) ||
0138                 msg.startsWith(QLatin1String("NODATA"))) {
0139             result += xi18nc("@info", "No signature found.") + QLatin1Char('\n');
0140         } else if (useGoodSig && msg.startsWith(QLatin1String("GOODSIG "))) {
0141             int sigpos = msg.indexOf( QLatin1Char(' ') , 8);
0142             const QString keyid = msg.mid(8, sigpos - 8);
0143 
0144             // split the name/email pair to give translators more power to handle this
0145             QString email;
0146             QString name = msg.mid(sigpos + 1);
0147 
0148             int oPos = name.indexOf(QLatin1Char('<'));
0149             int cPos = name.indexOf(QLatin1Char('>'));
0150             if ((oPos >= 0) && (cPos >= 0)) {
0151                 email = name.mid(oPos + 1, cPos - oPos - 1);
0152                 name = name.left(oPos).simplified();
0153             }
0154 
0155             if (email.isEmpty())
0156                 result += xi18nc("@info", "<para>Good signature from:<nl/><emphasis strong='true'>%1</emphasis><nl/>Key ID: %2<nl/></para>",
0157                         name, keyid);
0158             else
0159                 result += xi18nc("@info Good signature from: NAME <EMAIL>, Key ID: HEXID",
0160                         "<para>Good signature from:<nl/><emphasis strong='true'>%1 <email>%2</email></emphasis><nl/>Key ID: %3<nl/></para>",
0161                         name, email, keyid);
0162             if (!sigtime.isEmpty()) {
0163                 result += sigTimeMessage(sigtime);
0164                 sigtime.clear();
0165             }
0166         } else if (msg.startsWith(QLatin1String("SIG_ID "))) {
0167             const QStringList parts = msg.simplified().split(QLatin1Char(' '));
0168             if (parts.count() > 2)
0169                 sigtime = parts[2];
0170         } else if (msg.startsWith(QLatin1String("BADSIG"))) {
0171             int sigpos = msg.indexOf( QLatin1Char(' '), 7);
0172             result += xi18nc("@info", "<para><emphasis strong='true'>BAD signature</emphasis> from:<nl/> %1<nl/>Key ID: %2<nl/><nl/><emphasis strong='true'>The file is corrupted</emphasis></para>",
0173                     msg.mid(sigpos + 1), msg.mid(7, sigpos - 7));
0174         } else  if (msg.startsWith(QLatin1String("TRUST_UNDEFINED"))) {
0175             result += xi18nc("@info", "<para>The signature is valid, but the key is untrusted</para>");
0176         } else if (msg.startsWith(QLatin1String("TRUST_ULTIMATE"))) {
0177             result += xi18nc("@info", "<para>The signature is valid, and the key is ultimately trusted</para>");
0178         }
0179     }
0180 
0181     return result;
0182 }
0183 
0184 QString
0185 KGpgVerify::missingId() const
0186 {
0187     return m_missingId;
0188 }