/**
* This file is part of GPGFrontend.
*
* GPGFrontend is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Foobar 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Foobar. If not, see .
*
* The initial version of the source code is inherited from gpg4usb-team.
* Their source code version also complies with GNU General Public License.
*
* The source code version of this software was modified and released
* by Saturneric starting on May 12, 2021.
*
*/
#include "gpg/GpgContext.h"
#include "ui/WaitingDialog.h"
#include
#include /* contains read/write */
#ifdef _WIN32
#include
#endif
#define INT2VOIDP(i) (void*)(uintptr_t)(i)
namespace GpgME {
/** Constructor
* Set up gpgme-context, set paths to app-run path
*/
GpgContext::GpgContext() {
/** get application path */
QString appPath = qApp->applicationDirPath();
/** The function `gpgme_check_version' must be called before any other
* function in the library, because it initializes the thread support
* subsystem in GPGME. (from the info page) */
gpgme_check_version(nullptr);
// the locale set here is used for the other setlocale calls which have nullptr
// -> nullptr means use default, which is configured here
setlocale(LC_ALL, settings.value("int/lang").toLocale().name().toUtf8().constData());
/** set locale, because tests do also */
gpgme_set_locale(nullptr, LC_CTYPE, setlocale(LC_CTYPE, nullptr));
//qDebug() << "Locale set to" << LC_CTYPE << " - " << setlocale(LC_CTYPE, nullptr);
#ifndef _WIN32
gpgme_set_locale(nullptr, LC_MESSAGES, setlocale(LC_MESSAGES, nullptr));
#endif
err = gpgme_new(&mCtx);
checkErr(err);
gpgme_engine_info_t engineInfo;
engineInfo = gpgme_ctx_get_engine_info(mCtx);
// Check ENV before running
bool check_pass = false, find_openpgp = false, find_gpgconf = false, find_assuan = false, find_cms = false;
while (engineInfo != nullptr) {
qDebug() << gpgme_get_protocol_name(engineInfo->protocol) << engineInfo->file_name << engineInfo->protocol
<< engineInfo->home_dir << engineInfo->version;
if (engineInfo->protocol == GPGME_PROTOCOL_GPGCONF && strcmp(engineInfo->version, "1.0.0") != 0)
find_gpgconf = true;
if (engineInfo->protocol == GPGME_PROTOCOL_OpenPGP && strcmp(engineInfo->version, "1.0.0") != 0)
find_openpgp = true, info.appPath = engineInfo->file_name;
if (engineInfo->protocol == GPGME_PROTOCOL_CMS && strcmp(engineInfo->version, "1.0.0") != 0)
find_cms = true;
if (engineInfo->protocol == GPGME_PROTOCOL_ASSUAN)
find_assuan = true;
engineInfo = engineInfo->next;
}
if (find_gpgconf && find_openpgp && find_cms && find_assuan)
check_pass = true;
if (!check_pass) {
good = false;
return;
} else good = true;
/** Setting the output type must be done at the beginning */
/** think this means ascii-armor --> ? */
gpgme_set_armor(mCtx, 1);
/** passphrase-callback */
gpgme_set_passphrase_cb(mCtx, passphraseCb, this);
/** check if app is called with -d from command line */
if (qApp->arguments().contains("-d")) {
qDebug() << "gpgme_data_t debug on";
debug = true;
} else {
debug = false;
}
connect(this, SIGNAL(signalKeyDBChanged()),
this, SLOT(slotRefreshKeyList()), Qt::DirectConnection);
connect(this, SIGNAL(signalKeyUpdated(QString)),
this, SLOT(slotUpdateKeyList(QString)), Qt::DirectConnection);
slotRefreshKeyList();
}
/** Destructor
* Release gpgme-context
*/
GpgContext::~GpgContext() {
if (mCtx) gpgme_release(mCtx);
mCtx = nullptr;
}
bool GpgContext::isGood() const {
return good;
}
/** Read gpgme-Data to QByteArray
* mainly from http://basket.kde.org/ (kgpgme.cpp)
*/
#define BUF_SIZE (32 * 1024)
gpgme_error_t GpgContext::readToBuffer(gpgme_data_t dataIn, QByteArray *outBuffer) {
gpgme_off_t ret;
gpgme_error_t gpgErrNoError = GPG_ERR_NO_ERROR;
ret = gpgme_data_seek(dataIn, 0, SEEK_SET);
if (ret) {
gpgErrNoError = gpgme_err_code_from_errno(errno);
checkErr(gpgErrNoError, "failed dataseek dataIn readToBuffer");
} else {
char buf[BUF_SIZE + 2];
while ((ret = gpgme_data_read(dataIn, buf, BUF_SIZE)) > 0) {
const size_t size = outBuffer->size();
outBuffer->resize(static_cast(size + ret));
memcpy(outBuffer->data() + size, buf, ret);
}
if (ret < 0) {
gpgErrNoError = gpgme_err_code_from_errno(errno);
checkErr(gpgErrNoError, "failed data_read dataIn readToBuffer");
}
}
return gpgErrNoError;
}
/**
* The Passphrase window, if not provided by env-Var GPG_AGENT_INFO
* originally copied from http://basket.kde.org/ (kgpgme.cpp), but modified
*/
gpgme_error_t GpgContext::passphraseCb(void *hook, const char *uid_hint,
const char *passphrase_info,
int last_was_bad, int fd) {
auto *gpg = static_cast(hook);
return gpg->passphrase(uid_hint, passphrase_info, last_was_bad, fd);
}
gpgme_error_t GpgContext::passphrase(const char *uid_hint,
const char * /*passphrase_info*/,
int last_was_bad, int fd) {
gpgme_error_t returnValue = GPG_ERR_CANCELED;
QString passwordDialogMessage;
QString gpgHint = QString::fromUtf8(uid_hint);
bool result;
#ifdef _WIN32
DWORD written;
auto hd = INT2VOIDP(fd);
#endif
if (last_was_bad) {
passwordDialogMessage += "" + tr("Wrong password") + ".
\n\n";
clearPasswordCache();
}
/** if uid provided */
if (!gpgHint.isEmpty()) {
// remove UID, leave only username & email
gpgHint.remove(0, gpgHint.indexOf(" "));
passwordDialogMessage += "" + tr("Enter Password for") + "
" + gpgHint + "
";
}
if (mPasswordCache.isEmpty()) {
QString password = QInputDialog::getText(QApplication::activeWindow(), tr("Enter Password"),
passwordDialogMessage, QLineEdit::Password,
"", &result);
if (result) mPasswordCache = password.toUtf8();
} else result = true;
if (result) {
#ifndef _WIN32
if (write(fd, mPasswordCache.data(), mPasswordCache.length()) == -1) qDebug() << "something is terribly broken";
#else
WriteFile(hd, mPasswordCache.data(), mPasswordCache.length(), &written, 0);
#endif
returnValue = GPG_ERR_NO_ERROR;
}
#ifndef _WIN32
if (write(fd, "\n", 1) == -1) qDebug() << "something is terribly broken";
#else
WriteFile(hd, "\n", 1, &written, 0);
/* program will hang on cancel if hd not closed */
if (!result) CloseHandle(hd);
#endif
return returnValue;
}
/** also from kgpgme.cpp, seems to clear password from mem */
void GpgContext::clearPasswordCache() {
if (mPasswordCache.size() > 0) {
mPasswordCache.fill('\0');
mPasswordCache.truncate(0);
}
}
// error-handling
void GpgContext::checkErr(gpgme_error_t gpgmeError, const QString &comment) {
//if (gpgmeError != GPG_ERR_NO_ERROR && gpgmeError != GPG_ERR_CANCELED) {
if (gpgmeError != GPG_ERR_NO_ERROR) {
qDebug() << "[Error " << gpg_err_code(gpgmeError)
<< "] Source: " << gpgme_strsource(gpgmeError) << " Description: " << gpgErrString(gpgmeError);
}
}
void GpgContext::checkErr(gpgme_error_t gpgmeError) {
//if (gpgmeError != GPG_ERR_NO_ERROR && gpgmeError != GPG_ERR_CANCELED) {
if (gpg_err_code(gpgmeError) != GPG_ERR_NO_ERROR) {
qDebug() << "[Error " << gpg_err_code(gpgmeError)
<< "] Source: " << gpgme_strsource(gpgmeError) << " Description: " << gpgErrString(gpgmeError);
}
}
QString GpgContext::gpgErrString(gpgme_error_t err) {
return QString::fromUtf8(gpgme_strerror(err));
}
/** return type should be gpgme_error_t*/
void
GpgContext::executeGpgCommand(const QStringList &arguments, const std::function &interactFunc) {
QEventLoop looper;
auto dialog = new WaitingDialog(tr("Processing"), nullptr);
dialog->show();
auto *gpgProcess = new QProcess(&looper);
gpgProcess->setProcessChannelMode(QProcess::MergedChannels);
connect(gpgProcess, qOverload(&QProcess::finished), &looper, &QEventLoop::quit);
connect(gpgProcess, qOverload(&QProcess::finished), dialog, &WaitingDialog::deleteLater);
connect(gpgProcess, &QProcess::errorOccurred, []() -> void { qDebug("Error in Process"); });
connect(gpgProcess, &QProcess::errorOccurred, &looper, &QEventLoop::quit);
connect(gpgProcess, &QProcess::started, []() -> void { qDebug() << "Gpg Process Started Success"; });
connect(gpgProcess, &QProcess::readyReadStandardOutput, [interactFunc, gpgProcess]() {
qDebug() << "Function Called";
interactFunc(gpgProcess);
});
gpgProcess->setProgram(info.appPath);
gpgProcess->setArguments(arguments);
gpgProcess->start();
looper.exec();
dialog->close();
}
/*
* if there is no '\n' before the PGP-Begin-Block, but for example a whitespace,
* GPGME doesn't recognise the Message as encrypted. This function adds '\n'
* before the PGP-Begin-Block, if missing.
*/
void GpgContext::preventNoDataErr(QByteArray *in) {
int block_start = in->indexOf(GpgConstants::PGP_CRYPT_BEGIN);
if (block_start > 0 && in->at(block_start - 1) != '\n') {
in->insert(block_start, '\n');
}
block_start = in->indexOf(GpgConstants::PGP_SIGNED_BEGIN);
if (block_start > 0 && in->at(block_start - 1) != '\n') {
in->insert(block_start, '\n');
}
}
/*
* isSigned returns:
* - 0, if text isn't signed at all
* - 1, if text is partially signed
* - 2, if text is completly signed
*/
int GpgContext::textIsSigned(const QByteArray &text) {
if (text.trimmed().startsWith(GpgConstants::PGP_SIGNED_BEGIN) &&
text.trimmed().endsWith(GpgConstants::PGP_SIGNED_END))
return 2;
else if (text.contains(GpgConstants::PGP_SIGNED_BEGIN) && text.contains(GpgConstants::PGP_SIGNED_END))
return 1;
else return 0;
}
QString GpgContext::beautifyFingerprint(QString fingerprint) {
uint len = fingerprint.length();
if ((len > 0) && (len % 4 == 0))
for (uint n = 0; 4 * (n + 1) < len; ++n) fingerprint.insert(static_cast(5u * n + 4u), ' ');
return fingerprint;
}
void GpgContext::slotRefreshKeyList() {
qDebug() << "Refreshing Keys";
this->fetch_keys();
emit signalKeyInfoChanged();
}
QString GpgContext::getGpgmeVersion() {
return {gpgme_check_version(nullptr)};
}
const GpgKeyList &GpgContext::getKeys() const {
return mKeyList;
}
void GpgContext::getSigners(QVector &signer) {
auto count = gpgme_signers_count(mCtx);
signer.clear();
for (auto i = 0; i < count; i++) {
auto key = gpgme_signers_enum(mCtx, i);
auto it = mKeyMap.find(key->subkeys->keyid);
if (it == mKeyMap.end()) {
qDebug() << "Inconsistent state";
signer.push_back(GpgKey(key));
} else {
signer.push_back(*it.value());
}
}
}
void GpgContext::setSigners(const QVector &keys) {
gpgme_signers_clear(mCtx);
for (const auto &key : keys) {
if (checkIfKeyCanSign(key)) {
auto gpgmeError = gpgme_signers_add(mCtx, key.key_refer);
checkErr(gpgmeError);
}
}
if (keys.length() != gpgme_signers_count(mCtx)) {
qDebug() << "No All Keys Added";
}
}
void GpgContext::slotUpdateKeyList(const QString &key_id) {
auto it = mKeyMap.find(key_id);
if (it != mKeyMap.end()) {
gpgme_key_t new_key_refer;
auto gpgmeErr = gpgme_get_key(mCtx, key_id.toUtf8().constData(), &new_key_refer, 0);
if (gpgme_err_code(gpgmeErr) == GPG_ERR_EOF) {
gpgmeErr = gpgme_get_key(mCtx, key_id.toUtf8().constData(), &new_key_refer, 1);
if (gpgme_err_code(gpgmeErr) == GPG_ERR_EOF) {
throw std::runtime_error("key_id not found in key database");
}
}
if (new_key_refer != nullptr) {
it.value()->swapKeyRefer(new_key_refer);
emit signalKeyInfoChanged();
}
}
}
bool GpgContext::revSign(const GpgKey &key, const GpgKeySignature &signature) {
auto signing_key = getKeyById(signature.keyid);
auto gpgmeError = gpgme_op_revsig(mCtx, key.key_refer,
signing_key.key_refer,
signature.uid.toUtf8().constData(), 0);
if (gpg_err_code(gpgmeError) == GPG_ERR_NO_ERROR) {
emit signalKeyUpdated(key.id);
return true;
} else {
checkErr(gpgmeError);
return false;
}
}
}