File indexing completed on 2024-04-14 05:43:32

0001 /*
0002     SPDX-FileCopyrightText: 2007-2022 Rolf Eike Beer <kde@opensource.sf-tec.de>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "gpgproc.h"
0007 
0008 #include "kgpgsettings.h"
0009 #include "kgpg_general_debug.h"
0010 
0011 #include <KProcess>
0012 
0013 
0014 #include <QDir>
0015 #include <QFileInfo>
0016 #include <QTextCodec>
0017 
0018 #ifndef Q_OS_WIN
0019   #include <sys/stat.h>
0020 #endif
0021 
0022 class GnupgBinary {
0023 public:
0024     GnupgBinary();
0025 
0026     const QString &binary() const;
0027     void setBinary(const QString &executable);
0028     const QStringList &standardArguments() const;
0029     unsigned int version() const;
0030     bool supportsDebugLevel() const;
0031 
0032 private:
0033     QString m_binary;
0034     QStringList m_standardArguments;
0035     unsigned int m_version;
0036     bool m_useDebugLevel;
0037 };
0038 
0039 GnupgBinary::GnupgBinary()
0040     : m_version(0),
0041     m_useDebugLevel(false)
0042 {
0043 }
0044 
0045 const QString &GnupgBinary::binary() const
0046 {
0047     return m_binary;
0048 }
0049 
0050 /**
0051  * @brief check if GnuPG returns an error for this arguments
0052  * @param executable the GnuPG executable to call
0053  * @param arguments the arguments to pass to executable
0054  *
0055  * The arguments will be used together with "--version", so they should not
0056  * be any commands.
0057  */
0058 static bool checkGnupgArguments(const QString &executable, const QStringList &arguments)
0059 {
0060     KProcess gpg;
0061 
0062     // We ignore the output anyway, just make sure it doesn't clutter the output of
0063     // the parent process. Simplify the handling by putting all trash in one can.
0064     gpg.setOutputChannelMode(KProcess::MergedChannels);
0065 
0066     QStringList allArguments = arguments;
0067     allArguments << QLatin1String("--version");
0068     gpg.setProgram(executable, allArguments);
0069 
0070     return (gpg.execute() == 0);
0071 }
0072 
0073 static QString
0074 getGpgStatusLine(const QString &binary, const QString &key)
0075 {
0076     GPGProc process(nullptr, binary);
0077     process << QLatin1String( "--version" );
0078 
0079     QProcessEnvironment env = process.processEnvironment();
0080     env.insert(QStringLiteral("LANG"), QStringLiteral("C"));
0081     process.setProcessEnvironment(env);
0082 
0083     process.start();
0084     process.waitForFinished(-1);
0085 
0086     if (process.exitCode() == 255) {
0087         return QString();
0088     }
0089 
0090     QString line;
0091     while (process.readln(line) != -1) {
0092         if (line.startsWith(key)) {
0093             line.remove(0, key.length());
0094             return line.trimmed();
0095         }
0096     }
0097 
0098     return QString();
0099 }
0100 
0101 static QString
0102 getGpgProcessHome(const QString &binary)
0103 {
0104     return getGpgStatusLine(binary, QLatin1String("Home: "));
0105 }
0106 
0107 void GnupgBinary::setBinary(const QString &executable)
0108 {
0109     qCDebug(KGPG_LOG_GENERAL) << "checking version of GnuPG executable" << executable;
0110     // must be set first as gpgVersionString() uses GPGProc to parse the output
0111     m_binary = executable;
0112     const QString verstr = GPGProc::gpgVersionString(executable);
0113     m_version = GPGProc::gpgVersion(verstr);
0114     qCDebug(KGPG_LOG_GENERAL) << "version is" << verstr << m_version;
0115 
0116     m_useDebugLevel = (m_version > 0x20000);
0117 
0118     m_standardArguments.clear();
0119     m_standardArguments << QLatin1String( "--no-secmem-warning" )
0120             << QLatin1String( "--no-tty" )
0121             << QLatin1String("--no-greeting");
0122 
0123     m_standardArguments << GPGProc::getGpgHomeArguments(executable);
0124 
0125     QStringList debugLevelArguments(QLatin1String("--debug-level"));
0126     debugLevelArguments << QLatin1String("none");
0127     if (checkGnupgArguments(executable, debugLevelArguments))
0128         m_standardArguments << debugLevelArguments;
0129 }
0130 
0131 const QStringList& GnupgBinary::standardArguments() const
0132 {
0133     return m_standardArguments;
0134 }
0135 
0136 unsigned int GnupgBinary::version() const
0137 {
0138     return m_version;
0139 }
0140 
0141 bool GnupgBinary::supportsDebugLevel() const
0142 {
0143     return m_useDebugLevel;
0144 }
0145 
0146 Q_GLOBAL_STATIC(GnupgBinary, lastBinary)
0147 
0148 GPGProc::GPGProc(QObject *parent, const QString &binary)
0149        : KLineBufferedProcess(parent)
0150 {
0151     resetProcess(binary);
0152 }
0153 
0154 void
0155 GPGProc::resetProcess(const QString &binary)
0156 {
0157     GnupgBinary *bin = lastBinary;
0158     QString executable;
0159     if (binary.isEmpty())
0160         executable = KGpgSettings::gpgBinaryPath();
0161     else
0162         executable = binary;
0163 
0164     if (bin->binary() != executable)
0165         bin->setBinary(executable);
0166 
0167     setProgram(executable, bin->standardArguments());
0168 
0169     setOutputChannelMode(OnlyStdoutChannel);
0170 
0171     disconnect(this, QOverload<int, QProcess::ExitStatus>::of(&GPGProc::finished), this, &GPGProc::processExited);
0172     disconnect(this, &GPGProc::lineReadyStandardOutput, this, &GPGProc::readReady);
0173 }
0174 
0175 void GPGProc::start()
0176 {
0177     // make sure there is exactly one connection from us to that signal
0178     connect(this, QOverload<int, QProcess::ExitStatus>::of(&GPGProc::finished), this, &GPGProc::processExited, Qt::UniqueConnection);
0179     connect(this, &GPGProc::lineReadyStandardOutput, this, &GPGProc::readReady, Qt::UniqueConnection);
0180     KProcess::start();
0181 }
0182 
0183 int GPGProc::readln(QString &line, const bool colons)
0184 {
0185     QByteArray a;
0186     if (!readLineStandardOutput(&a))
0187         return -1;
0188 
0189     line = recode(a, colons, m_codec);
0190 
0191     return line.length();
0192 }
0193 
0194 int GPGProc::readln(QStringList &l)
0195 {
0196     QString s;
0197 
0198     int len = readln(s);
0199     if (len < 0)
0200         return len;
0201 
0202     l = s.split(QLatin1Char( ':' ));
0203 
0204     for (int i = 0; i < l.count(); ++i)
0205     {
0206         int j = 0;
0207         while ((j = l[i].indexOf(QLatin1String( "\\x3a" ), j, Qt::CaseInsensitive)) >= 0)
0208         {
0209             l[i].replace(j, 4, QLatin1Char( ':' ));
0210             j++;
0211         }
0212     }
0213 
0214     return l.count();
0215 }
0216 
0217 QString
0218 GPGProc::recode(QByteArray a, const bool colons, const QByteArray &codec)
0219 {
0220     const char *textcodec = codec.isEmpty() ? "utf8" : codec.constData();
0221     int pos = 0;
0222 
0223     while ((pos = a.indexOf("\\x", pos)) >= 0) {
0224         if (pos > a.length() - 4)
0225             break;
0226 
0227         const QByteArray pattern(a.mid(pos, 4));
0228         const QByteArray hexnum(pattern.right(2));
0229         bool ok;
0230         char n[2];
0231         n[0] = hexnum.toUShort(&ok, 16);
0232         n[1] = '\0';    // to use n as a 0-terminated string
0233         if (!ok) {
0234             // skip this occurrence
0235             pos += 2;
0236             continue;
0237         }
0238 
0239         // QLatin1Char( ':' ) must be skipped, it is used as column delimiter
0240         // since it is pure ascii it can be replaced in QString.
0241         if (!colons && (n[0] == ':' )) {
0242             pos += 3;
0243             continue;
0244         }
0245 
0246         // it is likely to find the same byte sequence more than once
0247         int npos = pos;
0248         do {
0249             a.replace(npos, 4, n);
0250         } while ((npos = a.indexOf(pattern, npos)) >= 0);
0251     }
0252 
0253     return QTextCodec::codecForName(textcodec)->toUnicode(a);
0254 }
0255 
0256 bool
0257 GPGProc::setCodec(const QByteArray &codec)
0258 {
0259     const QList<QByteArray> codecs = QTextCodec::availableCodecs();
0260     if (!codecs.contains(codec))
0261         return false;
0262 
0263     m_codec = codec;
0264 
0265     return true;
0266 }
0267 
0268 int GPGProc::gpgVersion(const QString &vstr)
0269 {
0270     if (vstr.isEmpty())
0271         return -1;
0272 
0273     QStringList values(vstr.split(QLatin1Char( '.' )));
0274     if (values.count() < 3)
0275         return -2;
0276 
0277     return (0x10000 * values[0].toInt() + 0x100 * values[1].toInt() + values[2].toInt());
0278 }
0279 
0280 QString GPGProc::gpgVersionString(const QString &binary)
0281 {
0282     const QStringList vlist = getGgpParsedConfig(binary, "version");
0283 
0284     if (vlist.empty())
0285         return QString();
0286 
0287     return vlist.first().split(QLatin1Char(':')).first();
0288 }
0289 
0290 QStringList
0291 GPGProc::getGpgPubkeyAlgorithms(const QString &binary)
0292 {
0293     QStringList ret = getGgpParsedConfig(binary, "pubkeyname");
0294 
0295     if (ret.isEmpty())
0296         return ret;
0297 
0298     return ret.first().split(QLatin1Char(':')).first().split(QLatin1Char(';'));
0299 }
0300 
0301 QString GPGProc::getGpgStartupError(const QString &binary)
0302 {
0303     GPGProc process(nullptr, binary);
0304     process << QLatin1String( "--version" );
0305     process.start();
0306     process.waitForFinished(-1);
0307 
0308     QString result;
0309 
0310     while (process.hasLineStandardError()) {
0311         QByteArray tmp;
0312         process.readLineStandardError(&tmp);
0313         tmp += '\n';
0314         result += QString::fromUtf8(tmp);
0315     }
0316 
0317     return result;
0318 }
0319 
0320 QStringList GPGProc::getGgpParsedConfig(const QString &binary, const QByteArray &key)
0321 {
0322     GPGProc process(nullptr, binary);
0323     process << QLatin1String("--list-config") << QLatin1String("--with-colons");
0324     process.start();
0325     process.waitForFinished(-1);
0326 
0327     QStringList result;
0328     QByteArray filter = "cfg:";
0329     if (!key.isEmpty())
0330         filter += key + ':';
0331 
0332     while (process.hasLineStandardOutput()) {
0333         QByteArray tmp;
0334         process.readLineStandardOutput(&tmp);
0335 
0336         if (tmp.startsWith(filter))
0337             result << QString::fromUtf8(tmp.mid(filter.length()));
0338     }
0339 
0340     return result;
0341 }
0342 
0343 QString GPGProc::getGpgHome(const QString &binary)
0344 {
0345     // First try: if environment is set GnuPG will use that directory
0346     // We can use this directly without starting a new process
0347     QByteArray env(qgetenv("GNUPGHOME"));
0348     QString gpgHome;
0349     if (!env.isEmpty()) {
0350         gpgHome = QLatin1String( env );
0351     } else if (!binary.isEmpty()) {
0352         // Second try: start GnuPG and ask what it is
0353         gpgHome = getGpgProcessHome(binary);
0354     }
0355 
0356     // Third try: guess what it is.
0357     if (gpgHome.isEmpty()) {
0358 #ifdef Q_OS_WIN     //krazy:exclude=cpp
0359         gpgHome = qgetenv("APPDATA") + QLatin1String( "/gnupg/" );
0360         gpgHome.replace(QLatin1Char( '\\' ), QLatin1Char( '/' ));
0361 #else
0362         gpgHome = QDir::homePath() + QLatin1String( "/.gnupg/" );
0363 #endif
0364     }
0365 
0366     gpgHome.replace(QLatin1String( "//" ), QLatin1String( "/" ));
0367 
0368     if (!gpgHome.endsWith(QLatin1Char( '/' )))
0369         gpgHome.append(QLatin1Char( '/' ));
0370 
0371     if (gpgHome.startsWith(QLatin1String("~/")))
0372         gpgHome.replace(0, 1, QDir::homePath());
0373 
0374 #ifdef Q_OS_WIN
0375     QDir().mkpath(gpgHome);
0376 #else
0377     uint mask = umask(077);
0378     QDir().mkpath(gpgHome);
0379     umask(mask);
0380 #endif
0381     return gpgHome;
0382 }
0383 
0384 QStringList
0385 GPGProc::getGpgHomeArguments(const QString &binary)
0386 {
0387     const QString gpgConfigFile = KGpgSettings::gpgConfigPath();
0388 
0389     if (gpgConfigFile.isEmpty())
0390         return {};
0391 
0392     QStringList options{ QLatin1String("--options"), gpgConfigFile };
0393 
0394     // Check if the config file is in the default home directory
0395     // of the binary. If it isn't add --homedir to command line also.
0396     QString gpgdir = GPGProc::getGpgHome(binary);
0397     gpgdir.chop(1); // remove trailing '/' as QFileInfo returns string without it
0398     QFileInfo confFile(gpgConfigFile);
0399     if (confFile.absolutePath() != gpgdir)
0400         options << QLatin1String("--homedir") << confFile.absolutePath();
0401 
0402     return options;
0403 }