qca/plugins/qca-gnupg/gpgaction.cpp
2020-02-24 16:24:46 +01:00

955 lines
20 KiB
C++

/*
* Copyright (C) 2003-2005 Justin Karneges <justin@affinix.com>
*
* 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
{
QDateTime dt;
dt.setTime_t(s.toInt());
return dt;
}
}
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;
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();
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"))
{
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
// <https://bugs.g10code.com/gnupg/issue1650>
// 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(' '), QString::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(' '), QString::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