File indexing completed on 2023-12-03 09:19:00
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 }