/* * Copyright (C) 2003-2005 Justin Karneges * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA * */ // #define GPGOP_DEBUG #include "gpgaction.h" #ifdef GPGOP_DEBUG #include "stdio.h" #endif namespace gpgQCAPlugin { static QDateTime getTimestamp(const QString &s) { if (s.isEmpty()) return QDateTime(); if (s.contains(QLatin1Char('T'))) { return QDateTime::fromString(s, Qt::ISODate); } else { return QDateTime::fromSecsSinceEpoch(s.toInt()); } } static QByteArray getCString(const QByteArray &a) { QByteArray out; // convert the "backslash" C-string syntax for (int n = 0; n < a.size(); ++n) { if (a[n] == '\\' && n + 1 < a.size()) { ++n; unsigned char c = (unsigned char)a[n]; if (c == '\\') { out += '\\'; } else if (c == 'x' && n + 2 < a.size()) { ++n; const QByteArray hex = a.mid(n, 2); ++n; // only skip one, loop will skip the next bool ok; uint val = hex.toInt(&ok, 16); if (ok) { out += (unsigned char)val; } else { out += "\\x"; out += hex; } } } else { out += a[n]; } } return out; } static bool stringToKeyList(const QString &outstr, GpgOp::KeyList *_keylist, QString *_keyring) { GpgOp::KeyList keyList; const QStringList lines = outstr.split(QLatin1Char('\n')); if (lines.count() < 1) return false; QStringList::ConstIterator it = lines.constBegin(); // first line is keyring file QString keyring = *(it++); // if the second line isn't a divider, we are dealing // with a new version of gnupg that doesn't give us // the keyring file on gpg --list-keys --with-colons if (it == lines.constEnd() || (*it).isEmpty() || (*it).at(0) != QLatin1Char('-')) { // first line wasn't the keyring name... keyring.clear(); // ...so read the first line again it--; } else { // this was the divider line - skip it it++; } for (; it != lines.constEnd(); ++it) { const QStringList f = (*it).split(QLatin1Char(':')); if (f.count() < 1) continue; const QString &type = f[0]; bool key = false; // key or not bool primary = false; // primary key or sub key // bool sec = false; // private key or not if (type == QLatin1String("pub")) { key = true; primary = true; } else if (type == QLatin1String("sec")) { key = true; primary = true; // sec = true; } else if (type == QLatin1String("sub")) { key = true; } else if (type == QLatin1String("ssb")) { key = true; // sec = true; } if (key) { if (primary) { keyList += GpgOp::Key(); const QString &trust = f[1]; if (trust == QLatin1String("f") || trust == QLatin1String("u")) keyList.last().isTrusted = true; } const int key_type = f[3].toInt(); const QString &caps = f[11]; GpgOp::KeyItem item; item.bits = f[2].toInt(); if (key_type == 1) item.type = GpgOp::KeyItem::RSA; else if (key_type == 16) item.type = GpgOp::KeyItem::ElGamal; else if (key_type == 17) item.type = GpgOp::KeyItem::DSA; else item.type = GpgOp::KeyItem::Unknown; item.id = f[4]; item.creationDate = getTimestamp(f[5]); item.expirationDate = getTimestamp(f[6]); if (caps.contains(QLatin1Char('e'))) item.caps |= GpgOp::KeyItem::Encrypt; if (caps.contains(QLatin1Char('s'))) item.caps |= GpgOp::KeyItem::Sign; if (caps.contains(QLatin1Char('c'))) item.caps |= GpgOp::KeyItem::Certify; if (caps.contains(QLatin1Char('a'))) item.caps |= GpgOp::KeyItem::Auth; keyList.last().keyItems += item; } else if (type == QLatin1String("uid")) { const QByteArray uid = getCString(f[9].toUtf8()); keyList.last().userIds.append(QString::fromUtf8(uid)); } else if (type == QLatin1String("fpr")) { const QString &s = f[9]; keyList.last().keyItems.last().fingerprint = s; } } if (_keylist) *_keylist = keyList; if (_keyring) *_keyring = keyring; return true; } static bool findKeyringFilename(const QString &outstr, QString *_keyring) { const QStringList lines = outstr.split(QLatin1Char('\n')); if (lines.count() < 1) return false; *_keyring = lines[0]; return true; } GpgAction::GpgAction(QObject *parent) : QObject(parent) , proc(this) , dtextTimer(this) , utf8Output(false) { dtextTimer.setSingleShot(true); connect(&proc, &GPGProc::error, this, &GpgAction::proc_error); connect(&proc, &GPGProc::finished, this, &GpgAction::proc_finished); connect(&proc, &GPGProc::readyReadStdout, this, &GpgAction::proc_readyReadStdout); connect(&proc, &GPGProc::readyReadStderr, this, &GpgAction::proc_readyReadStderr); connect(&proc, &GPGProc::readyReadStatusLines, this, &GpgAction::proc_readyReadStatusLines); connect(&proc, &GPGProc::bytesWrittenStdin, this, &GpgAction::proc_bytesWrittenStdin); connect(&proc, &GPGProc::bytesWrittenAux, this, &GpgAction::proc_bytesWrittenAux); connect(&proc, &GPGProc::bytesWrittenCommand, this, &GpgAction::proc_bytesWrittenCommand); connect(&proc, &GPGProc::debug, this, &GpgAction::proc_debug); connect(&dtextTimer, &QCA::SafeTimer::timeout, this, &GpgAction::t_dtext); reset(); } GpgAction::~GpgAction() { reset(); } void GpgAction::reset() { collectOutput = true; allowInput = false; readConv.setup(LineConverter::Read); writeConv.setup(LineConverter::Write); readText = false; writeText = false; useAux = false; passphraseKeyId = QString(); signing = false; decryptGood = false; signGood = false; curError = GpgOp::ErrorUnknown; badPassphrase = false; need_submitPassphrase = false; need_cardOkay = false; diagnosticText = QString(); dtextTimer.stop(); output = Output(); proc.reset(); } void GpgAction::start() { reset(); QStringList args; bool extra = false; if (input.opt_ascii) args += QStringLiteral("--armor"); if (input.opt_noagent) args += QStringLiteral("--no-use-agent"); if (input.opt_alwaystrust) args += QStringLiteral("--always-trust"); if (!input.opt_pubfile.isEmpty() && !input.opt_secfile.isEmpty()) { args += QStringLiteral("--no-default-keyring"); args += QStringLiteral("--keyring"); args += input.opt_pubfile; args += QStringLiteral("--secret-keyring"); args += input.opt_secfile; } switch (input.op) { case GpgOp::Check: { args += QStringLiteral("--version"); readText = true; break; } case GpgOp::SecretKeyringFile: { #ifndef Q_OS_WIN args += QStringLiteral("--display-charset=utf-8"); #endif args += QStringLiteral("--list-secret-keys"); readText = true; break; } case GpgOp::PublicKeyringFile: { #ifndef Q_OS_WIN args += QStringLiteral("--display-charset=utf-8"); #endif args += QStringLiteral("--list-public-keys"); readText = true; break; } case GpgOp::SecretKeys: { args += QStringLiteral("--fixed-list-mode"); args += QStringLiteral("--with-colons"); args += QStringLiteral("--with-fingerprint"); args += QStringLiteral("--with-fingerprint"); args += QStringLiteral("--list-secret-keys"); utf8Output = true; readText = true; break; } case GpgOp::PublicKeys: { args += QStringLiteral("--fixed-list-mode"); args += QStringLiteral("--with-colons"); args += QStringLiteral("--with-fingerprint"); args += QStringLiteral("--with-fingerprint"); args += QStringLiteral("--list-public-keys"); utf8Output = true; readText = true; break; } case GpgOp::Encrypt: { args += QStringLiteral("--encrypt"); // recipients for (QStringList::ConstIterator it = input.recip_ids.constBegin(); it != input.recip_ids.constEnd(); ++it) { args += QStringLiteral("--recipient"); args += QStringLiteral("0x") + *it; } extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) readText = true; break; } case GpgOp::Decrypt: { args += QStringLiteral("--decrypt"); extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) writeText = true; break; } case GpgOp::Sign: { args += QStringLiteral("--default-key"); args += QStringLiteral("0x") + input.signer_id; args += QStringLiteral("--sign"); extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) readText = true; signing = true; break; } case GpgOp::SignAndEncrypt: { args += QStringLiteral("--default-key"); args += QStringLiteral("0x") + input.signer_id; args += QStringLiteral("--sign"); args += QStringLiteral("--encrypt"); // recipients for (QStringList::ConstIterator it = input.recip_ids.constBegin(); it != input.recip_ids.constEnd(); ++it) { args += QStringLiteral("--recipient"); args += QStringLiteral("0x") + *it; } extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) readText = true; signing = true; break; } case GpgOp::SignClearsign: { args += QStringLiteral("--default-key"); args += QStringLiteral("0x") + input.signer_id; args += QStringLiteral("--clearsign"); extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) readText = true; signing = true; break; } case GpgOp::SignDetached: { args += QStringLiteral("--default-key"); args += QStringLiteral("0x") + input.signer_id; args += QStringLiteral("--detach-sign"); extra = true; collectOutput = false; allowInput = true; if (input.opt_ascii) readText = true; signing = true; break; } case GpgOp::Verify: { args += QStringLiteral("--verify"); args += QStringLiteral("-"); // krazy:exclude=doublequote_chars extra = true; allowInput = true; if (input.opt_ascii) writeText = true; break; } case GpgOp::VerifyDetached: { args += QStringLiteral("--verify"); args += QStringLiteral("-"); // krazy:exclude=doublequote_chars args += QStringLiteral("-&?"); extra = true; allowInput = true; useAux = true; break; } case GpgOp::Import: { args += QStringLiteral("--import"); readText = true; if (input.opt_ascii) writeText = true; break; } case GpgOp::Export: { args += QStringLiteral("--export"); args += QStringLiteral("0x") + input.export_key_id; collectOutput = false; if (input.opt_ascii) readText = true; break; } case GpgOp::DeleteKey: { args += QStringLiteral("--batch"); args += QStringLiteral("--delete-key"); args += QStringLiteral("0x") + input.delete_key_fingerprint; break; } } #ifdef GPG_PROFILE timer.start(); printf("<< launch >>\n"); #endif proc.start(input.bin, args, extra ? GPGProc::ExtendedMode : GPGProc::NormalMode); // detached sig if (input.op == GpgOp::VerifyDetached) { QByteArray a = input.sig; if (input.opt_ascii) { LineConverter conv; conv.setup(LineConverter::Write); a = conv.process(a); } proc.writeStdin(a); proc.closeStdin(); } // import if (input.op == GpgOp::Import) { QByteArray a = input.inkey; if (writeText) { LineConverter conv; conv.setup(LineConverter::Write); a = conv.process(a); } proc.writeStdin(a); proc.closeStdin(); } } #ifdef QPIPE_SECURE void GpgAction::submitPassphrase(const QCA::SecureArray &a) #else void GpgAction::submitPassphrase(const QByteArray &a) #endif { if (!need_submitPassphrase) return; need_submitPassphrase = false; #ifdef QPIPE_SECURE QCA::SecureArray b; #else QByteArray b; #endif // filter out newlines, since that's the delimiter used // to indicate a submitted passphrase b.resize(a.size()); int at = 0; for (int n = 0; n < a.size(); ++n) { if (a[n] != '\n') b[at++] = a[n]; } b.resize(at); // append newline b.resize(b.size() + 1); b[b.size() - 1] = '\n'; proc.writeCommand(b); } QByteArray GpgAction::read() { if (collectOutput) return QByteArray(); QByteArray a = proc.readStdout(); if (readText) a = readConv.update(a); if (!proc.isActive()) a += readConv.final(); return a; } void GpgAction::write(const QByteArray &in) { if (!allowInput) return; QByteArray a = in; if (writeText) a = writeConv.update(in); if (useAux) proc.writeAux(a); else proc.writeStdin(a); } void GpgAction::endWrite() { if (!allowInput) return; if (useAux) proc.closeAux(); else proc.closeStdin(); } void GpgAction::cardOkay() { if (need_cardOkay) { need_cardOkay = false; submitCommand("\n"); } } QString GpgAction::readDiagnosticText() { QString s = diagnosticText; diagnosticText = QString(); return s; } void GpgAction::submitCommand(const QByteArray &a) { proc.writeCommand(a); } // since str is taken as a value, it is ok to use the same variable for 'rest' QString GpgAction::nextArg(QString str, QString *rest) { int n = str.indexOf(QLatin1Char(' ')); if (n == -1) { if (rest) *rest = QString(); return str; } else { if (rest) *rest = str.mid(n + 1); return str.mid(0, n); } } void GpgAction::processStatusLine(const QString &line) { appendDiagnosticText(QStringLiteral("{") + line + QStringLiteral("}")); ensureDTextEmit(); if (!proc.isActive()) return; QString s, rest; s = nextArg(line, &rest); if (s == QLatin1String("NODATA")) { // only set this if it'll make it better if (curError == GpgOp::ErrorUnknown) curError = GpgOp::ErrorFormat; } else if (s == QLatin1String("UNEXPECTED")) { if (curError == GpgOp::ErrorUnknown) curError = GpgOp::ErrorFormat; } else if (s == QLatin1String("EXPKEYSIG")) { curError = GpgOp::ErrorSignerExpired; } else if (s == QLatin1String("REVKEYSIG")) { curError = GpgOp::ErrorSignerRevoked; } else if (s == QLatin1String("EXPSIG")) { curError = GpgOp::ErrorSignatureExpired; } else if (s == QLatin1String("INV_RECP")) { const int r = nextArg(rest).toInt(); if (curError == GpgOp::ErrorUnknown) { if (r == 10) curError = GpgOp::ErrorEncryptUntrusted; else if (r == 4) curError = GpgOp::ErrorEncryptRevoked; else if (r == 5) curError = GpgOp::ErrorEncryptExpired; else // due to GnuPG bug #1650 // // encrypting to expired and revoked keys will // not specify any reason for failing, // defaulting to this curError = GpgOp::ErrorEncryptInvalid; } } else if (s == QLatin1String("NO_SECKEY")) { output.encryptedToId = nextArg(rest); if (curError == GpgOp::ErrorUnknown) curError = GpgOp::ErrorDecryptNoKey; } else if (s == QLatin1String("DECRYPTION_OKAY")) { decryptGood = true; // message could be encrypted with several keys if (curError == GpgOp::ErrorDecryptNoKey) curError = GpgOp::ErrorUnknown; } else if (s == QLatin1String("SIG_CREATED")) { signGood = true; } else if (s == QLatin1String("USERID_HINT")) { passphraseKeyId = nextArg(rest); } else if (s == QLatin1String("GET_HIDDEN")) { QString arg = nextArg(rest); if (arg == QLatin1String("passphrase.enter") || arg == QLatin1String("passphrase.pin.ask")) { need_submitPassphrase = true; // for signal-safety, emit later QMetaObject::invokeMethod(this, "needPassphrase", Qt::QueuedConnection, Q_ARG(QString, passphraseKeyId)); } } else if (s == QLatin1String("GET_LINE")) { QString arg = nextArg(rest); if (arg == QLatin1String("cardctrl.insert_card.okay")) { need_cardOkay = true; QMetaObject::invokeMethod(this, "needCard", Qt::QueuedConnection); } } else if (s == QLatin1String("GET_BOOL")) { QString arg = nextArg(rest); if (arg == QLatin1String("untrusted_key.override")) submitCommand("no\n"); } else if (s == QLatin1String("GOOD_PASSPHRASE")) { badPassphrase = false; } else if (s == QLatin1String("BAD_PASSPHRASE")) { badPassphrase = true; } else if (s == QLatin1String("GOODSIG")) { output.wasSigned = true; output.signerId = nextArg(rest); output.verifyResult = GpgOp::VerifyGood; } else if (s == QLatin1String("BADSIG")) { output.wasSigned = true; output.signerId = nextArg(rest); output.verifyResult = GpgOp::VerifyBad; } else if (s == QLatin1String("ERRSIG")) { output.wasSigned = true; const QStringList list = rest.split(QLatin1Char(' '), Qt::SkipEmptyParts); output.signerId = list[0]; output.timestamp = getTimestamp(list[4]); output.verifyResult = GpgOp::VerifyNoKey; } else if (s == QLatin1String("VALIDSIG")) { const QStringList list = rest.split(QLatin1Char(' '), Qt::SkipEmptyParts); output.timestamp = getTimestamp(list[2]); } } void GpgAction::processResult(int code) { #ifdef GPG_PROFILE printf("<< launch: %d >>\n", timer.elapsed()); #endif // put stdout and stderr into QStrings QString outstr; QString errstr; #ifdef Q_OS_WIN if (!utf8Output) { outstr = QString::fromLocal8Bit(buf_stdout); errstr = QString::fromLocal8Bit(buf_stderr); } else { #endif outstr = QString::fromUtf8(buf_stdout); errstr = QString::fromUtf8(buf_stderr); #ifdef Q_OS_WIN } #endif if (collectOutput) appendDiagnosticText(QStringLiteral("stdout: [%1]").arg(outstr)); appendDiagnosticText(QStringLiteral("stderr: [%1]").arg(errstr)); ensureDTextEmit(); if (badPassphrase) { output.errorCode = GpgOp::ErrorPassphrase; } else if (curError != GpgOp::ErrorUnknown) { output.errorCode = curError; } else if (code == 0) { if (input.op == GpgOp::Check) { const QStringList strList = outstr.split(QStringLiteral("\n")); foreach (const QString &str, strList) { if (!str.startsWith(QLatin1String("Home: "))) continue; output.homeDir = str.section(QLatin1Char(' '), 1); break; } output.success = true; } else if (input.op == GpgOp::SecretKeyringFile || input.op == GpgOp::PublicKeyringFile) { if (findKeyringFilename(outstr, &output.keyringFile)) output.success = true; } else if (input.op == GpgOp::SecretKeys || input.op == GpgOp::PublicKeys) { if (stringToKeyList(outstr, &output.keys, &output.keyringFile)) output.success = true; } else output.success = true; } else { // decrypt and sign success based on status only. // this is mainly because gpg uses fatal return // values if there is trouble with gpg-agent, even // though the operation otherwise works. if (input.op == GpgOp::Decrypt && decryptGood) output.success = true; if (signing && signGood) output.success = true; // gpg will indicate failure for bad sigs, but we don't // consider this to be operation failure. bool signedMakesItGood = false; if (input.op == GpgOp::Verify || input.op == GpgOp::VerifyDetached) signedMakesItGood = true; if (signedMakesItGood && output.wasSigned) output.success = true; } emit finished(); } void GpgAction::ensureDTextEmit() { if (!dtextTimer.isActive()) dtextTimer.start(); } void GpgAction::t_dtext() { emit readyReadDiagnosticText(); } void GpgAction::proc_error(gpgQCAPlugin::GPGProc::Error e) { QString str; if (e == GPGProc::FailedToStart) str = QStringLiteral("FailedToStart"); else if (e == GPGProc::UnexpectedExit) str = QStringLiteral("UnexpectedExit"); else if (e == GPGProc::ErrorWrite) str = QStringLiteral("ErrorWrite"); appendDiagnosticText(QStringLiteral("GPG Process Error: %1").arg(str)); ensureDTextEmit(); output.errorCode = GpgOp::ErrorProcess; emit finished(); } void GpgAction::proc_finished(int exitCode) { appendDiagnosticText(QStringLiteral("GPG Process Finished: exitStatus=%1").arg(exitCode)); ensureDTextEmit(); processResult(exitCode); } void GpgAction::proc_readyReadStdout() { if (collectOutput) { QByteArray a = proc.readStdout(); if (readText) a = readConv.update(a); buf_stdout.append(a); } else emit readyRead(); } void GpgAction::proc_readyReadStderr() { buf_stderr.append(proc.readStderr()); } void GpgAction::proc_readyReadStatusLines() { const QStringList lines = proc.readStatusLines(); for (int n = 0; n < lines.count(); ++n) processStatusLine(lines[n]); } void GpgAction::proc_bytesWrittenStdin(int bytes) { if (!useAux) { int actual = writeConv.writtenToActual(bytes); emit bytesWritten(actual); } } void GpgAction::proc_bytesWrittenAux(int bytes) { if (useAux) { int actual = writeConv.writtenToActual(bytes); emit bytesWritten(actual); } } void GpgAction::proc_bytesWrittenCommand(int) { // don't care about this } void GpgAction::proc_debug(const QString &str) { appendDiagnosticText(QStringLiteral("GPGProc: ") + str); ensureDTextEmit(); } void GpgAction::appendDiagnosticText(const QString &line) { #ifdef GPGOP_DEBUG printf("%s\n", qPrintable(line)); #endif diagnosticText += line; } } // end namespace gpgQCAPlugin