diff --git a/src/gpgolkeyadder/gpgolkeyadder.cpp b/src/gpgolkeyadder/gpgolkeyadder.cpp index 9ae3a30..764cc0f 100644 --- a/src/gpgolkeyadder/gpgolkeyadder.cpp +++ b/src/gpgolkeyadder/gpgolkeyadder.cpp @@ -1,561 +1,565 @@ /* 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; } std::string error() { return mError.asString(); } protected: void run() override { auto ctx = GpgME::Context::create(GpgME::CMS); if (!ctx) { qDebug () << "No ctx"; return; } const auto result = ctx->importKeys(mData); mError = result.error (); if (result.error ()) { qDebug() << "Import failed: " << result.error().asString(); } 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); char **patterns = vector_to_cArray(fingerprints); mError = ctx->startKeyListing((const char**)patterns, false); release_cArray(patterns); if (mError) { qDebug() << "Failed to start keylisting err:" << mError.asString(); return; } GpgME::Error err; while (!err) { const auto key = ctx->nextKey(err); if (err || key.isNull()) { break; } mCerts.push_back(key); } return; } private: GpgME::Error mError; GpgME::Data mData; std::vector mCerts; }; } // Namespace GpgOLKeyAdder::GpgOLKeyAdder(const QCommandLineParser &parser): QDialog(nullptr), 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(" - ") + _("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")); /* FIXME: This should be implemented in GpgOL */ mAlwaysSec->setVisible(false); setupGUI(); // Deletes itself auto reader = new StdinReader; connect (reader, &StdinReader::stdinRead, this, [this] (const QByteArray &data) { if (data.size() > 1) { 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].trimmed()); mCMSEdit->setPlainText(splitData[1].trimmed()); } } void GpgOLKeyAdder::setupGUI() { /* Setup Edits */ auto fixedFont = QFont("Monospace", 10); fixedFont.setStyleHint(QFont::TypeWriter); 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 certificates here. They 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(mAlwaysSec); auto hBox = new QHBoxLayout; hBox->addWidget(new QLabel( _("Use these keys 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 " "for this contact. The keys will be imported and used " "regardless of their trust level.")); const QString smimeMsg = QString::fromUtf8(_("For S/MIME the root certificate has to be trusted.")); const QString multiMsg = QString::fromUtf8(_("Place multiple keys 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, QString::fromUtf8(_("S/MIME (X509 Certificates)"))); layout->addWidget(tab); } else { layout->addWidget(mEdit); } layout->addWidget(buttonBox); } void GpgOLKeyAdder::save() { 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 << 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); } static QStringList buildKeyInfos(const std::vector keys, QWidget *parent, bool *bOk) { QStringList keyInfos; if (!bOk) { qDebug() << "Invalid call"; return keyInfos; } *bOk = false; for (const auto &key: keys) { if (key.isNull() || !key.numSubkeys()) { qDebug() << "Null key?"; continue; } 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 QStringList(); } 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 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, const std::string &errString) { 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.")) + (errString.empty() ? QStringLiteral("") : QStringLiteral("

%1").arg(QString::fromStdString(errString))))); return; } 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("

"); } if (mShowCMS && cmsCerts.size()) { std::vector leaves; std::remove_copy_if(cmsCerts.begin(), cmsCerts.end(), std::back_inserter(leaves), [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 (leaves, 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("

"); } if (!msg.isEmpty()) { msg += "

" + QString::fromUtf8 (_("Continue?")); const auto ret = QMessageBox::question(this, QString::fromUtf8(_("Confirm")), msg, QMessageBox::Yes | QMessageBox::Abort, QMessageBox::Yes); if (ret == QMessageBox::Yes) { save(); return; } } save(); return; } void GpgOLKeyAdder::checkAccept() { const auto openpgpText = mEdit->toPlainText().trimmed().toUtf8(); auto cmsText = mCMSEdit->toPlainText().trimmed().toUtf8(); + if (!openpgpText.size() && !cmsText.size()) { + save(); + return; + } /* Otherwise we get an BER error */ cmsText += '\n'; 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(), std::string()); 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(), workerThread->error()); delete workerThread; }); workerThread->start(); progress->exec(); } #include "gpgolkeyadder.moc"