File indexing completed on 2024-04-28 15:28:59

0001 /*
0002     This file is part of KNewStuff2.
0003     SPDX-FileCopyrightText: 2004, 2005 Andras Mantia <amantia@kde.org>
0004     SPDX-FileCopyrightText: 2007 Josef Spillner <spillner@kde.org>
0005 
0006     SPDX-License-Identifier: LGPL-2.1-or-later
0007 */
0008 
0009 // app includes
0010 #include "security.h"
0011 #include "question.h"
0012 
0013 // qt includes
0014 #include <QFile>
0015 #include <QFileInfo>
0016 #include <QStringList>
0017 #include <QTextStream>
0018 #include <QTimer>
0019 #include <qstandardpaths.h>
0020 
0021 #include <QCryptographicHash>
0022 
0023 // kde includes
0024 #include <KLocalizedString>
0025 
0026 using namespace KNSCore;
0027 
0028 #if KNEWSTUFFCORE_BUILD_DEPRECATED_SINCE(5, 31)
0029 static QString gpgExecutable()
0030 {
0031     QString gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg"));
0032     if (gpgExe.isEmpty()) {
0033         gpgExe = QStandardPaths::findExecutable(QStringLiteral("gpg2"));
0034     }
0035     if (gpgExe.isEmpty()) {
0036         return QStringLiteral("gpg");
0037     }
0038     return gpgExe;
0039 }
0040 
0041 Security::Security()
0042 {
0043     m_keysRead = false;
0044     m_gpgRunning = false;
0045     readKeys();
0046     readSecretKeys();
0047 }
0048 
0049 Security::~Security()
0050 {
0051 }
0052 
0053 void Security::readKeys()
0054 {
0055     if (m_gpgRunning) {
0056         QTimer::singleShot(5, this, &Security::readKeys);
0057         return;
0058     }
0059     m_runMode = List;
0060     m_keys.clear();
0061     m_process = new QProcess();
0062     QStringList arguments;
0063     arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--no-tty") << QStringLiteral("--with-colon") << QStringLiteral("--list-keys");
0064     connect(m_process, &QProcess::finished, this, &Security::slotFinished);
0065     connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
0066     m_process->start(gpgExecutable(), arguments);
0067     if (!m_process->waitForStarted()) {
0068         Q_EMIT signalError(
0069             i18n("<qt>Cannot start <i>gpg</i> and retrieve the available keys. Make sure that <i>gpg</i> is installed, otherwise verification of downloaded "
0070                  "resources will not be possible.</qt>"));
0071         delete m_process;
0072         m_process = nullptr;
0073     } else {
0074         m_gpgRunning = true;
0075     }
0076 }
0077 
0078 void Security::readSecretKeys()
0079 {
0080     if (m_gpgRunning) {
0081         QTimer::singleShot(5, this, &Security::readSecretKeys);
0082         return;
0083     }
0084     m_runMode = ListSecret;
0085     m_process = new QProcess();
0086     QStringList arguments;
0087     arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--no-tty") << QStringLiteral("--with-colon") << QStringLiteral("--list-secret-keys");
0088     connect(m_process, &QProcess::finished, this, &Security::slotFinished);
0089     connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
0090     m_process->start(gpgExecutable(), arguments);
0091     if (!m_process->waitForStarted()) {
0092         delete m_process;
0093         m_process = nullptr;
0094     } else {
0095         m_gpgRunning = true;
0096     }
0097 }
0098 
0099 void Security::slotFinished(int exitCode, QProcess::ExitStatus exitStatus)
0100 {
0101     if (exitStatus != QProcess::NormalExit) {
0102         m_gpgRunning = false;
0103         delete m_process;
0104         m_process = nullptr;
0105         return;
0106     }
0107     switch (m_runMode) {
0108     case ListSecret:
0109         m_keysRead = true;
0110         break;
0111     case Verify:
0112         Q_EMIT validityResult(m_result);
0113         break;
0114     case Sign:
0115         Q_EMIT fileSigned(m_result);
0116         break;
0117     }
0118     m_gpgRunning = false;
0119     delete m_process;
0120     m_process = nullptr;
0121 
0122     Q_UNUSED(exitCode)
0123 }
0124 
0125 void Security::slotReadyReadStandardOutput()
0126 {
0127     QString data;
0128     while (m_process->canReadLine()) {
0129         data = QString::fromLocal8Bit(m_process->readLine());
0130         switch (m_runMode) {
0131         case List:
0132         case ListSecret:
0133             if (data.startsWith(QLatin1String("pub")) || data.startsWith(QLatin1String("sec"))) {
0134                 KeyStruct key;
0135                 if (data.startsWith(QLatin1String("pub"))) {
0136                     key.secret = false;
0137                 } else {
0138                     key.secret = true;
0139                 }
0140                 QStringList line = data.split(QLatin1Char(':'), Qt::KeepEmptyParts);
0141                 key.id = line[4];
0142                 QString shortId = key.id.right(8);
0143                 QString trustStr = line[1];
0144                 key.trusted = false;
0145                 if (trustStr == QLatin1Char('u') || trustStr == QLatin1Char('f')) {
0146                     key.trusted = true;
0147                 }
0148                 data = line[9];
0149                 key.mail = data.section(QLatin1Char('<'), -1, -1);
0150                 key.mail.chop(1);
0151                 key.name = data.section(QLatin1Char('<'), 0, 0);
0152                 if (key.name.contains(QLatin1Char('('))) {
0153                     key.name = key.name.section(QLatin1Char('('), 0, 0);
0154                 }
0155                 m_keys[shortId] = key;
0156             }
0157             break;
0158         case Verify:
0159             data = data.section(QLatin1Char(']'), 1, -1).trimmed();
0160             if (data.startsWith(QLatin1String("GOODSIG"))) {
0161                 m_result &= SIGNED_BAD_CLEAR;
0162                 m_result |= SIGNED_OK;
0163                 QString id = data.section(QLatin1Char(' '), 1, 1).right(8);
0164                 if (!m_keys.contains(id)) {
0165                     m_result |= UNKNOWN;
0166                 } else {
0167                     m_signatureKey = m_keys[id];
0168                 }
0169             } else if (data.startsWith(QLatin1String("NO_PUBKEY"))) {
0170                 m_result &= SIGNED_BAD_CLEAR;
0171                 m_result |= UNKNOWN;
0172             } else if (data.startsWith(QLatin1String("BADSIG"))) {
0173                 m_result |= SIGNED_BAD;
0174                 QString id = data.section(QLatin1Char(' '), 1, 1).right(8);
0175                 if (!m_keys.contains(id)) {
0176                     m_result |= UNKNOWN;
0177                 } else {
0178                     m_signatureKey = m_keys[id];
0179                 }
0180             } else if (data.startsWith(QLatin1String("TRUST_ULTIMATE"))) {
0181                 m_result &= SIGNED_BAD_CLEAR;
0182                 m_result |= TRUSTED;
0183             }
0184             break;
0185 
0186         case Sign:
0187             if (data.contains(QLatin1String("passphrase.enter"))) {
0188                 KeyStruct key = m_keys[m_secretKey];
0189                 Question question(Question::PasswordQuestion);
0190                 question.setQuestion(
0191                     i18n("<qt>Enter passphrase for key <b>0x%1</b>, belonging to<br /><i>%2&lt;%3&gt;</i><br />:</qt>", m_secretKey, key.name, key.mail));
0192                 if (question.ask() == Question::ContinueResponse) {
0193                     m_process->write(question.response().toLocal8Bit() + '\n');
0194                 } else {
0195                     m_result |= BAD_PASSPHRASE;
0196                     m_process->kill();
0197                     return;
0198                 }
0199             } else if (data.contains(QLatin1String("BAD_PASSPHRASE"))) {
0200                 m_result |= BAD_PASSPHRASE;
0201             }
0202             break;
0203         }
0204     }
0205 }
0206 
0207 void Security::checkValidity(const QString &filename)
0208 {
0209     m_fileName = filename;
0210     slotCheckValidity();
0211 }
0212 
0213 void Security::slotCheckValidity()
0214 {
0215     if (!m_keysRead || m_gpgRunning) {
0216         QTimer::singleShot(5, this, &Security::slotCheckValidity);
0217         return;
0218     }
0219     if (m_keys.isEmpty()) {
0220         Q_EMIT validityResult(-1);
0221         return;
0222     }
0223 
0224     m_result = 0;
0225     m_runMode = Verify;
0226     QFileInfo f(m_fileName);
0227     // check the MD5 sum
0228     QString md5sum;
0229     QCryptographicHash context(QCryptographicHash::Md5);
0230     QFile file(m_fileName);
0231     if (!m_fileName.isEmpty() && file.open(QIODevice::ReadOnly)) {
0232         context.reset();
0233         context.addData(&file);
0234         md5sum = QString::fromLatin1(context.result().toHex());
0235         file.close();
0236     }
0237     file.setFileName(f.path() + QStringLiteral("/md5sum"));
0238     if (file.open(QIODevice::ReadOnly)) {
0239         QByteArray md5sum_file;
0240         file.readLine(md5sum_file.data(), 50);
0241         if (!md5sum_file.isEmpty() && QString::fromLatin1(md5sum_file).startsWith(md5sum)) {
0242             m_result |= MD5_OK;
0243         }
0244         file.close();
0245     }
0246     m_result |= SIGNED_BAD;
0247     m_signatureKey.id = QLatin1String("");
0248     m_signatureKey.name = QLatin1String("");
0249     m_signatureKey.mail = QLatin1String("");
0250     m_signatureKey.trusted = false;
0251 
0252     // verify the signature
0253     m_process = new QProcess();
0254     QStringList arguments;
0255     arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--status-fd=2") << QStringLiteral("--command-fd=0") << QStringLiteral("--verify")
0256               << f.path() + QStringLiteral("/signature") << m_fileName;
0257     connect(m_process, &QProcess::finished, this, &Security::slotFinished);
0258     connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
0259     m_process->start(gpgExecutable(), arguments);
0260     if (m_process->waitForStarted()) {
0261         m_gpgRunning = true;
0262     } else {
0263         Q_EMIT signalError(
0264             i18n("<qt>Cannot start <i>gpg</i> and check the validity of the file. Make sure that <i>gpg</i> is installed, otherwise verification of downloaded "
0265                  "resources will not be possible.</qt>"));
0266         Q_EMIT validityResult(0);
0267         delete m_process;
0268         m_process = nullptr;
0269     }
0270 }
0271 
0272 void Security::signFile(const QString &fileName)
0273 {
0274     m_fileName = fileName;
0275     slotSignFile();
0276 }
0277 
0278 void Security::slotSignFile()
0279 {
0280     if (!m_keysRead || m_gpgRunning) {
0281         QTimer::singleShot(5, this, &Security::slotSignFile);
0282         return;
0283     }
0284 
0285     QStringList secretKeys;
0286     for (QMap<QString, KeyStruct>::Iterator it = m_keys.begin(); it != m_keys.end(); ++it) {
0287         if (it.value().secret) {
0288             secretKeys.append(it.key());
0289         }
0290     }
0291 
0292     if (secretKeys.isEmpty()) {
0293         Q_EMIT fileSigned(-1);
0294         return;
0295     }
0296 
0297     m_result = 0;
0298     QFileInfo f(m_fileName);
0299 
0300     // create the MD5 sum
0301     QString md5sum;
0302     QCryptographicHash context(QCryptographicHash::Md5);
0303     QFile file(m_fileName);
0304     if (file.open(QIODevice::ReadOnly)) {
0305         context.reset();
0306         context.addData(&file);
0307         md5sum = QString::fromLatin1(context.result().toHex());
0308         file.close();
0309     }
0310     file.setFileName(f.path() + QStringLiteral("/md5sum"));
0311     if (file.open(QIODevice::WriteOnly)) {
0312         QTextStream stream(&file);
0313         stream << md5sum;
0314         m_result |= MD5_OK;
0315         file.close();
0316     }
0317 
0318     if (secretKeys.count() > 1) {
0319         Question question(Question::SelectFromListQuestion);
0320         question.setQuestion(i18n("Key used for signing:"));
0321         question.setTitle(i18n("Select Signing Key"));
0322         question.setList(secretKeys);
0323         if (question.ask() == Question::OKResponse) {
0324             m_secretKey = question.response();
0325         } else {
0326             // emit an error to be forwarded to the user for selecting a signing key...
0327             Q_EMIT fileSigned(0);
0328             return;
0329         }
0330     } else {
0331         m_secretKey = secretKeys[0];
0332     }
0333 
0334     // verify the signature
0335     m_process = new QProcess();
0336     QStringList arguments;
0337     arguments << QStringLiteral("--no-secmem-warning") << QStringLiteral("--status-fd=2") << QStringLiteral("--command-fd=0") << QStringLiteral("--no-tty")
0338               << QStringLiteral("--detach-sign") << QStringLiteral("-u") << m_secretKey << QStringLiteral("-o") << f.path() + QStringLiteral("/signature")
0339               << m_fileName;
0340     connect(m_process, &QProcess::finished, this, &Security::slotFinished);
0341     connect(m_process, &QProcess::readyReadStandardOutput, this, &Security::slotReadyReadStandardOutput);
0342     m_runMode = Sign;
0343     m_process->start(gpgExecutable(), arguments);
0344     if (m_process->waitForStarted()) {
0345         m_gpgRunning = true;
0346     } else {
0347         Q_EMIT signalError(
0348             i18n("<qt>Cannot start <i>gpg</i> and sign the file. Make sure that <i>gpg</i> is installed, otherwise signing of the resources will not be "
0349                  "possible.</qt>"));
0350         Q_EMIT fileSigned(0);
0351         delete m_process;
0352         m_process = nullptr;
0353     }
0354 }
0355 
0356 #include "moc_security.cpp"
0357 
0358 #endif