diff --git a/src/gpgolkeyadder/gpgolkeyadder-options.h b/src/gpgolkeyadder/gpgolkeyadder-options.h index 038e411..f4de06d 100644 --- a/src/gpgolkeyadder/gpgolkeyadder-options.h +++ b/src/gpgolkeyadder/gpgolkeyadder-options.h @@ -1,37 +1,43 @@ #ifndef GPGOLKEYADDER_OPTIONS #define GPGOLKEYADDER_OPTIONS /* Copyright (C) 2018 by Intevation GmbH * * This file is Free Software under the GNU GPL (v>=2) * and comes with ABSOLUTELY NO WARRANTY! * See LICENSE.txt for details. */ #include #include /** @file Commandline options*/ static void options(QCommandLineParser &parser) { QList options; options << QCommandLineOption(QStringList() << QStringLiteral("debug"), QStringLiteral("Print debug output.")) + << QCommandLineOption(QStringList() << QStringLiteral("cms"), + QStringLiteral("Add S/MIME settings")) << QCommandLineOption(QStringLiteral("hwnd"), QStringLiteral("Parent Window"), QStringLiteral("windows window handle")) + << QCommandLineOption(QStringList() << QStringLiteral("sign"), + QStringLiteral("Always sign")) + << QCommandLineOption(QStringList() << QStringLiteral("encrypt"), + QStringLiteral("Always enccrypt")) << QCommandLineOption(QStringLiteral("username"), QStringLiteral("Name"), QStringLiteral("username")) << QCommandLineOption(QStringLiteral("lang"), QStringLiteral("Language"), QStringLiteral("Language to be used e.g. de_DE")); for (const auto &opt: options) { parser.addOption(opt); } parser.addVersionOption(); parser.addHelpOption(); } #endif diff --git a/src/gpgolkeyadder/gpgolkeyadder.cpp b/src/gpgolkeyadder/gpgolkeyadder.cpp index 6efc183..4182a2f 100644 --- a/src/gpgolkeyadder/gpgolkeyadder.cpp +++ b/src/gpgolkeyadder/gpgolkeyadder.cpp @@ -1,249 +1,536 @@ /* Copyright (C) 2018 by Intevation GmbH * * This file is Free Software under the GNU GPL (v>=2) * and comes with ABSOLUTELY NO WARRANTY! * See LICENSE.txt for details. */ #include "gpgolkeyadder.h" #include "w32-gettext.h" #include "w32-util.h" #include "w32-qt-util.h" #include "stdinreader.h" #include #include #include #include #include #include +#include #include #include #include #include #include #include #include - +#include +#include +#include +#include + +#include +#include +#include #include #include #include +namespace +{ + +static char **vector_to_cArray(const std::vector &vec) +{ + char ** ret = (char**) malloc (sizeof (char*) * (vec.size() + 1)); + for (size_t i = 0; i < vec.size(); i++) + { + ret[i] = strdup (vec[i].c_str()); + } + ret[vec.size()] = NULL; + return ret; +} + +static void release_cArray (char **carray) +{ + if (carray) + { + for (int idx = 0; carray[idx]; idx++) + { + free (carray[idx]); + } + free (carray); + } +} + +class CMSImportThread: public QThread +{ + Q_OBJECT + + public: + explicit CMSImportThread(const GpgME::Data &data): + mData(data) + { + } + + std::vector certs() + { + return mCerts; + } + + protected: + void run() override + { + auto ctx = GpgME::Context::create(GpgME::CMS); + + if (!ctx) { + qDebug () << "No ctx"; + return; + } + + const auto result = ctx->importKeys(mData); + std::vector fingerprints; + + for (const auto import: result.imports()) { + if (import.error()){ + qDebug() << "Error importing:" << import.error().asString(); + continue; + } + const char *fpr = import.fingerprint (); + if (!fpr) + { + qDebug() << "Import with no fpr."; + continue; + } + fingerprints.push_back (fpr); + qDebug () << "imported: " << fpr; + } + if (!fingerprints.size()) { + qDebug () << "Nothing imported"; + return; + } + ctx = GpgME::Context::create(GpgME::CMS); + ctx->setKeyListMode (GpgME::KeyListMode::Local | + GpgME::KeyListMode::Validate); + + GpgME::Error err; + char **patterns = vector_to_cArray(fingerprints); + err = ctx->startKeyListing((const char**)patterns, false); + release_cArray(patterns); + if (err) { + qDebug() << "Failed to start keylisting err:" << err.asString(); + return; + } + + while (!err) { + const auto key = ctx->nextKey(err); + if (err || key.isNull()) { + break; + } + mCerts.push_back(key); + } + return; + } + + private: + GpgME::Data mData; + std::vector mCerts; +}; +} // Namespace + + GpgOLKeyAdder::GpgOLKeyAdder(const QCommandLineParser &parser): QDialog(nullptr), - mEdit(new QTextEdit) + mEdit(new QTextEdit), + mCMSEdit(new QTextEdit), + mAlwaysSec(new QCheckBox) { setWindowFlags(windowFlags() & (~Qt::WindowContextHelpButtonHint)); mName = parser.value(QStringLiteral("username")); + mShowCMS = parser.isSet(QStringLiteral("cms")); - setWindowTitle(_("GpgOL") + QStringLiteral(" - ") + _("Configure key for:") + + setWindowTitle(_("GpgOL") + QStringLiteral(" - ") + _("Settings for:") + QStringLiteral(" ") + mName); setWindowIcon(QIcon(":/gpgol-icon.svg")); const auto hwnd = parser.value(QStringLiteral("hwnd")); if (!hwnd.isEmpty()) { bool ok; WId id = (WId) hwnd.toInt(&ok); if (!ok) { qDebug() << "invalid hwnd value"; } else { W32::setupForeignParent(id, this, true); setModal(true); } } + mAlwaysSec->setChecked(parser.isSet("encrypt") && parser.isSet("sign")); + setupGUI(); // Deletes itself auto reader = new StdinReader; connect (reader, &StdinReader::stdinRead, this, [this] (const QByteArray &data) { if (data.size() > 1) { - mEdit->setPlainText(QString::fromUtf8(data)); + handleInput(data); } }); reader->start(); } +void GpgOLKeyAdder::handleInput(const QByteArray &data) { + const auto stringData = QString::fromUtf8(data); + const auto splitData = stringData.split("BEGIN CMS DATA\n"); + if (splitData.size() != 2) { + qDebug() << "Failed to split data. Old GpgOL Version?"; + mEdit->setPlainText(stringData); + } else { + mEdit->setPlainText(splitData[0]); + mCMSEdit->setPlainText(splitData[1]); + } +} + void GpgOLKeyAdder::setupGUI() { - /* Setup Edit */ + /* Setup Edits */ auto fixedFont = QFont("Monospace", 10); fixedFont.setStyleHint(QFont::TypeWriter); - mEdit->setFont(fixedFont); resize(QFontMetrics(fixedFont).averageCharWidth() * 80, QFontMetrics(fixedFont).height() * 30); + + mEdit->setFont(fixedFont); mEdit->setPlaceholderText(QString::fromUtf8(_("Paste a public key export here. It should look like:")) + QStringLiteral("\n\n-----BEGIN PGP PUBLIC KEY BLOCK-----\n\n" "mQENBEzJavsBCADG/guWL6AxGgngUxp/DcmoitJjaJMqcJkBtD3uKrW81Pbnm3LI\n" "...\n" "dCl8hHggB9x2\n" "=oShe\n" "-----END PGP PUBLIC KEY BLOCK-----")); mEdit->setUndoRedoEnabled(true); + + mCMSEdit->setFont(fixedFont); + mCMSEdit->setPlaceholderText(QString::fromUtf8(_("Paste the certificate here. It should look like:")) + + QStringLiteral("\n\n-----BEGIN CERTIFICATE-----\n\n" + "MIICeDCCAeGgAwIBAgIJANNFIDoYY4XJMA0GCSqGSIb3DQEBBQUAMFUxCzAJBgNV\n" + "...\n" + "dCl8hHggB9x2\n" + "-----END CERTIFICATE-----")); + mCMSEdit->setUndoRedoEnabled(true); + /* Setup buttons */ QDialogButtonBox *buttonBox = new QDialogButtonBox(); buttonBox->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Ok); connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, [this] () { checkAccept(); }); connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, [this] () { qApp->quit(); }); auto okBtn = buttonBox->button(QDialogButtonBox::Ok); okBtn->setEnabled(false); connect(mEdit, &QTextEdit::textChanged, this, [this, okBtn] () { const auto text = mEdit->toPlainText().trimmed().toUtf8(); if (!text.size()) { okBtn->setEnabled(true); return; } GpgME::Data data(text.constData(), text.size(), false); okBtn->setEnabled(data.type() == GpgME::Data::PGPKey); }); + connect(mCMSEdit, &QTextEdit::textChanged, this, [this, okBtn] () { + const auto text = mCMSEdit->toPlainText().trimmed().toUtf8(); + if (!text.size()) { + okBtn->setEnabled(true); + return; + } + GpgME::Data data(text.constData(), text.size(), false); + + okBtn->setEnabled(data.type() == GpgME::Data::X509Cert); + }); + + /* The always secure box */ + mAlwaysSec->setText(QString::fromUtf8(_("Always secure mails"))); /* Setup layout */ auto layout = new QVBoxLayout; setLayout(layout); - layout->addWidget(mEdit); + + layout->addWidget(mAlwaysSec); + + auto hBox = new QHBoxLayout; + + hBox->addWidget(new QLabel( + _("Use specific keys / certs for this contact:"))); + + auto infoBtn = new QPushButton; + infoBtn->setIcon(QIcon::fromTheme("help-contextual")); + infoBtn->setFlat(true); + hBox->addWidget(infoBtn); + hBox->addStretch(1); + + connect(infoBtn, &QPushButton::clicked, this, [this, infoBtn] () { + const QString generalMsg = QString::fromUtf8(_("You can use this to override the keys / certificates used " + "for this contact. The keys will be imported and used for " + "this contact, regardless of OpenPGP trust.")); + const QString smimeMsg = QString::fromUtf8(_("For S/MIME the root certificate has to be trusted.")); + const QString multiMsg = QString::fromUtf8(_("Place multiple keys / certificates in here to encrypt to all of them.")); + QToolTip::showText(infoBtn->mapToGlobal(QPoint()), + QStringLiteral("%1

%2").arg(generalMsg).arg( + (mShowCMS ? smimeMsg + QStringLiteral("

") + multiMsg : + multiMsg)), infoBtn, QRect(), 30000); + }); + + + layout->addLayout(hBox); + + if (mShowCMS) { + auto tab = new QTabWidget; + tab->addTab(mEdit, QStringLiteral("OpenPGP")); + tab->addTab(mCMSEdit, QStringLiteral("S/MIME (X509)")); + layout->addWidget(tab); + } else { + layout->addWidget(mEdit); + } layout->addWidget(buttonBox); } -static void save(const QByteArray &data) +void GpgOLKeyAdder::save() { - if (!data.size()) { + std::cout << "BEGIN KEYADDER PGP DATA" << std::endl; + const QByteArray &pgpData = mEdit->toPlainText().trimmed().toUtf8(); + if (!pgpData.size()) { + /* Empty is a special case which can mean that an + * existing key should be removed. */ + std::cout << "empty" << std::endl; + } else { + std::cout << pgpData.constData() << std::endl; + } + std::cout << "END KEYADDER PGP DATA" << std::endl; + + std::cout << "BEGIN KEYADDER CMS DATA" << std::endl; + const QByteArray &cmsData = mCMSEdit->toPlainText().trimmed().toUtf8(); + if (!cmsData.size()) { /* Empty is a special case which can mean that an * existing key should be removed. */ std::cout << "empty" << std::endl; } else { - std::cout << data.constData() << std::endl; + std::cout << cmsData.constData() << std::endl; } + std::cout << "END KEYADDER CMS DATA" << std::endl; + std::cout << "BEGIN KEYADDER OPTIONS" << std::endl; + std::cout << "secure=" << (mAlwaysSec->isChecked() ? "3" : "0") << std::endl; + std::cout << "END KEYADDER OPTIONS" << std::endl; + qApp->quit(); } static QString prettyNameAndEMail(const QString &id, const QString &name, const QString &email, const QString &comment) { if (name.isEmpty()) { if (email.isEmpty()) { return QString(); } else if (comment.isEmpty()) { return QStringLiteral("<%1>").arg(email); } else { return QStringLiteral("(%2) <%1>").arg(email, comment); } } if (email.isEmpty()) { if (comment.isEmpty()) { return name; } else { return QStringLiteral("%1 (%2)").arg(name, comment); } } if (comment.isEmpty()) { return QStringLiteral("%1 <%2>").arg(name, email); } else { return QStringLiteral("%1 (%3) <%2>").arg(name, email, comment); } return QString(); } static QString prettyNameAndEMail(const char *id, const char *name_, const std::string &email_, const char *comment_) { return prettyNameAndEMail(QString::fromUtf8(id), QString::fromUtf8(name_), QString::fromStdString(email_), QString::fromUtf8(comment_)); } static QString prettyNameAndEMail(const GpgME::UserID &uid) { return prettyNameAndEMail(uid.id(), uid.name(), uid.addrSpec(), uid.comment()); } static QString time_t2string(time_t t) { QDateTime dt; dt.setTime_t(t); return QLocale().toString(dt, QLocale::ShortFormat); } -void GpgOLKeyAdder::checkAccept() +static QStringList +buildKeyInfos(const std::vector keys, QWidget *parent, + bool *bOk) { - const auto text = mEdit->toPlainText().trimmed().toUtf8(); - - if (text.isEmpty()) { - save(text); - // Save exits - return; - } - - GpgME::Data data(text.constData(), text.size(), false); - - const auto keys = data.toKeys(); + QStringList keyInfos; - if (!keys.size()) { - QMessageBox::warning(this, QString::fromUtf8 (_("Error")), - QStringLiteral("%1

").arg(QString::fromUtf8(_("Failed to parse any public key.")))); - return; + if (!bOk) { + qDebug() << "Invalid call"; + return keyInfos; } - - QStringList keyInfos; + *bOk = false; for (const auto &key: keys) { if (key.isNull() || !key.numSubkeys()) { qDebug() << "Null key?"; continue; } - - if (key.hasSecret()) { - QMessageBox::warning(this, QString::fromUtf8 (_("Error")), + if (key.hasSecret() && key.protocol() == GpgME::OpenPGP) { + QMessageBox::warning(parent, QString::fromUtf8 (_("Error")), QStringLiteral("%1

").arg(QString::fromUtf8(_("Secret key detected."))) + QString::fromUtf8(_("You can only configure public keys in Outlook." " Import secret keys with Kleopatra."))); - return; + return QStringList(); } - if (key.isRevoked() || key.isExpired() || key.isInvalid() || key.isDisabled() || !key.canEncrypt()) { - QMessageBox::warning(this, QString::fromUtf8 (_("Error")), + if (key.isBad() || !key.canEncrypt()) { + QMessageBox::warning(parent, QString::fromUtf8 (_("Error")), QStringLiteral("%1

%2

").arg(QString::fromUtf8(_("Invalid key detected."))).arg( key.primaryFingerprint()) + QString::fromUtf8(_("The key is unusable for Outlook." " Please check Kleopatra for more information."))); - return; + return QStringList(); } const auto subkey = key.subkey(0); QString info = QString::fromLatin1(key.primaryFingerprint()) + "
" + QString::fromUtf8(_("Created:")) + " " + time_t2string(subkey.creationTime()) + "
" + QString::fromUtf8(_("User Ids:")) + "
"; for (const auto &uid: key.userIDs()) { if (uid.isNull() || uid.isRevoked() || uid.isInvalid()) { continue; } info += "  " + prettyNameAndEMail(uid).toHtmlEscaped() + "
"; } keyInfos << info; } + *bOk = true; + return keyInfos; +} + +void GpgOLKeyAdder::checkAcceptBottom(const std::vector &pgpKeys, const std::vector &cmsCerts) +{ + bool ok; + QString msg; + + if (!pgpKeys.size() && (mShowCMS && !pgpKeys.size() && !cmsCerts.size())) { + QMessageBox::warning(this, QString::fromUtf8 (_("Error")), + QStringLiteral("%1

").arg(QString::fromUtf8(_("Failed to parse any public key.")))); + return; + } - QString msg = QString::fromUtf8(_("You are about to configure the following %1 for:")).arg( - (keyInfos.size() > 1 ? QString::fromUtf8(_("keys")) : QString::fromUtf8(_("key")))) + + if (pgpKeys.size()) + { + const auto keyInfos = buildKeyInfos (pgpKeys, this, &ok); + if (!ok) { + return; + } + msg += QString::fromUtf8(_("You are about to configure the following OpenPGP %1 for:")).arg( + (keyInfos.size() > 1 ? QString::fromUtf8(_("keys")) : QString::fromUtf8(_("key")))) + QStringLiteral("
\t\"%1\"

").arg(mName.toHtmlEscaped()) + - keyInfos.join("

"); + keyInfos.join("

"); + + } + if (mShowCMS && cmsCerts.size()) + { + std::vector leafs; + std::remove_copy_if(cmsCerts.begin(), cmsCerts.end(), + std::back_inserter(leafs), + [cmsCerts] (const auto &k) { + /* Check if a key has this fingerprint in the + * chain ID. Meaning that there is any child of + * this certificate. In that case remove it. */ + for (const auto &c: cmsCerts) { + if (!c.chainID()) { + continue; + } + if (!k.primaryFingerprint() || !c.primaryFingerprint()) { + /* WTF? */ + continue; + } + if (!strcmp (c.chainID(), k.primaryFingerprint())) { + qDebug() << "Filtering out non leaf cert" << k.primaryFingerprint(); + return true; + } + } + return false; + }); + + const auto certInfos = buildKeyInfos (leafs, this, &ok); + if (!ok) { + return; + } + msg += QString::fromUtf8(_("You are about to configure the following S/MIME %1 for:")).arg( + (certInfos.size() > 1 ? QString::fromUtf8(_("certificates")) : QString::fromUtf8(_("certificate")))) + + QStringLiteral("
\t\"%1\"

").arg(mName.toHtmlEscaped()) + + certInfos.join("

"); + } msg += "

" + QString::fromUtf8 (_("Continue?")); - const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm keys")), msg, + const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm")), msg, QMessageBox::Yes | QMessageBox::Abort, QMessageBox::Yes); if (ret == QMessageBox::Yes) { - save(text); + save(); return; } } + +void GpgOLKeyAdder::checkAccept() +{ + const auto openpgpText = mEdit->toPlainText().trimmed().toUtf8(); + const auto cmsText = mCMSEdit->toPlainText().trimmed().toUtf8(); + + GpgME::Data pgp(openpgpText.constData(), openpgpText.size(), false); + GpgME::Data cms(cmsText.constData(), cmsText.size(), false); + + const auto keys = pgp.toKeys(GpgME::OpenPGP); + + if (!mShowCMS || !cmsText.size()) { + checkAcceptBottom (keys, std::vector()); + return; + } + + auto progress = new QProgressDialog(this, Qt::CustomizeWindowHint | Qt::WindowTitleHint | Qt::Dialog); + progress->setAutoClose(true); + progress->setMinimumDuration(0); + progress->setMaximum(0); + progress->setMinimum(0); + progress->setModal(true); + progress->setCancelButton(nullptr); + progress->setWindowTitle(_("Validating S/MIME certificates")); + progress->setLabel(new QLabel(_("This may take several minutes..."))); + auto workerThread = new CMSImportThread(cms); + connect(workerThread, &QThread::finished, this, [this, workerThread, progress, keys] { + progress->accept(); + progress->deleteLater(); + checkAcceptBottom(keys, workerThread->certs()); + delete workerThread; + }); + workerThread->start(); + progress->exec(); +} + +#include "gpgolkeyadder.moc" diff --git a/src/gpgolkeyadder/gpgolkeyadder.h b/src/gpgolkeyadder/gpgolkeyadder.h index 22c9d80..7813653 100644 --- a/src/gpgolkeyadder/gpgolkeyadder.h +++ b/src/gpgolkeyadder/gpgolkeyadder.h @@ -1,33 +1,44 @@ #ifndef GPGOLKEYADDER_H #define GPGOLKEYADDER_H /* Copyright (C) 2018 by Intevation GmbH * * This file is Free Software under the GNU GPL (v>=2) * and comes with ABSOLUTELY NO WARRANTY! * See LICENSE.txt for details. */ #include #include +#include +#include + class QCommandLineParser; class QTextEdit; +class QCheckBox; + class GpgOLKeyAdder: public QDialog { Q_OBJECT public: GpgOLKeyAdder(const QCommandLineParser &parser); protected: /** @brief UI setup */ void setupGUI(); private: void checkAccept(); + void checkAcceptBottom(const std::vector &pgpKeys, const std::vector &cmsKeys); + void save(); + void handleInput(const QByteArray &data); QTextEdit *mEdit; + QTextEdit *mCMSEdit; QString mName; + QCheckBox *mAlwaysSec; + bool mShowCMS; }; #endif // GPGOLKEYADDER_H