File indexing completed on 2025-02-02 05:08:36

0001 /*
0002     SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #include "cryptopage.h"
0008 #include "accountwizard_debug.h"
0009 #include "dialog.h"
0010 #include "identity.h"
0011 #include "transport.h"
0012 
0013 #include <Libkleo/Classify>
0014 #include <Libkleo/DefaultKeyFilter>
0015 #include <Libkleo/DefaultKeyGenerationJob>
0016 #include <Libkleo/ProgressDialog>
0017 #include <QGpgME/ImportJob>
0018 #include <QGpgME/Job>
0019 #include <QGpgME/KeyListJob>
0020 #include <QGpgME/Protocol>
0021 #include <QGpgME/WKSPublishJob>
0022 
0023 #include <gpgme++/context.h>
0024 #include <gpgme++/importresult.h>
0025 #include <gpgme++/keygenerationresult.h>
0026 #include <gpgme++/keylistresult.h>
0027 
0028 #include <KMessageBox>
0029 #include <KNewPasswordDialog>
0030 #include <KNotifications/KNotification>
0031 
0032 #include <KIdentityManagementCore/Identity>
0033 #include <KIdentityManagementCore/IdentityManager>
0034 
0035 #include <QEventLoopLocker>
0036 #include <QFileDialog>
0037 
0038 class KeyGenerationJob : public QObject
0039 {
0040     Q_OBJECT
0041 
0042 public:
0043     KeyGenerationJob(SetupManager *setupManager, const QString &passphrase, Key::PublishingMethod publishingMethod)
0044         : mSetupManager(setupManager)
0045         , mName(setupManager->name())
0046         , mEmail(setupManager->email())
0047         , mPassphrase(passphrase)
0048         , mTransportId(0)
0049         , mPublishingMethod(publishingMethod)
0050     {
0051         // Listen for when setup of Transport is finished so we can snatch
0052         // the transport ID
0053         connect(mSetupManager.data(), &SetupManager::setupFinished, this, &KeyGenerationJob::onObjectSetupFinished);
0054 
0055         qCDebug(ACCOUNTWIZARD_LOG) << "Starting key generation";
0056         auto job = new Kleo::DefaultKeyGenerationJob(this);
0057         job->setPassphrase(mPassphrase);
0058         connect(job, &Kleo::DefaultKeyGenerationJob::result, this, &KeyGenerationJob::keyGenerated);
0059         job->start(mEmail, mName);
0060     }
0061 
0062     ~KeyGenerationJob() override = default;
0063 
0064 Q_SIGNALS:
0065     void result(const QString &fingerprint);
0066 
0067 private Q_SLOTS:
0068     void onObjectSetupFinished(SetupObject *obj)
0069     {
0070         if (auto *transport = qobject_cast<Transport *>(obj)) {
0071             mTransportId = transport->transportId();
0072         }
0073     }
0074 
0075     void keyGenerated(const GpgME::KeyGenerationResult &result)
0076     {
0077         if (result.error()) {
0078             qCWarning(ACCOUNTWIZARD_LOG) << "Key generation finished with error:" << result.error().asString();
0079             KNotification::event(KNotification::Error,
0080                                  i18n("Account Wizard"),
0081                                  i18n("Error while generating new key pair for your account %1: %2", mEmail, QString::fromUtf8(result.error().asString())),
0082                                  QStringLiteral("akonadi"));
0083             deleteLater();
0084             return;
0085         }
0086 
0087         const QString fpr = QString::fromLatin1(result.fingerprint());
0088         qCDebug(ACCOUNTWIZARD_LOG) << "Finished generating key" << fpr;
0089         Q_EMIT this->result(fpr);
0090 
0091         auto listJob = QGpgME::openpgp()->keyListJob(false, true, true);
0092         connect(listJob, &QGpgME::KeyListJob::result, this, &KeyGenerationJob::keyRetrieved);
0093         listJob->start({fpr}, true);
0094     }
0095 
0096     void keyRetrieved(const GpgME::KeyListResult &result, const std::vector<GpgME::Key> &keys)
0097     {
0098         if (result.error() || keys.size() != 1) {
0099             qCWarning(ACCOUNTWIZARD_LOG) << "Key listing finished with error;" << result.error().asString();
0100             KNotification::event(KNotification::Error,
0101                                  i18n("Account Wizard"),
0102                                  i18n("Error while generating new key pair for your account %1: %2", mEmail, QString::fromUtf8(result.error().asString())),
0103                                  QStringLiteral("akonadi"));
0104             deleteLater();
0105             return;
0106         }
0107 
0108         qCDebug(ACCOUNTWIZARD_LOG) << "Post-generation key listing finished";
0109 
0110         const auto key = keys[0];
0111 
0112         // SetupManager no longer exists -> all is set up, so we need to modify
0113         // the Identity ourselves
0114         if (!mSetupManager) {
0115             qDebug(ACCOUNTWIZARD_LOG) << "SetupManager no longer exists!";
0116             updateIdentity(mEmail, key.primaryFingerprint());
0117             publishKeyIfNeeded(key);
0118             return;
0119         }
0120 
0121         {
0122             // SetupManager still lives, see if Identity has already been set up.
0123             // If not then pass it the new key and let it set up everything
0124             const auto objectsToSetup = mSetupManager->objectsToSetup();
0125             const auto identIt = std::find_if(objectsToSetup.cbegin(), objectsToSetup.cend(), [](SetupObject *obj) -> bool {
0126                 return qobject_cast<Identity *>(obj);
0127             });
0128             const auto keyIt = std::find_if(objectsToSetup.cbegin(), objectsToSetup.cend(), [](SetupObject *obj) -> bool {
0129                 return qobject_cast<Key *>(obj);
0130             });
0131             if (identIt != objectsToSetup.cend()) {
0132                 qCDebug(ACCOUNTWIZARD_LOG) << "Identity not set up yet, less work for us";
0133                 qobject_cast<Identity *>(*identIt)->setKey(GpgME::OpenPGP, key.primaryFingerprint());
0134                 // The Key depends on Identity, so if Identity wasn't set up
0135                 // yet, neither was the Key.
0136                 if (mPublishingMethod != Key::NoPublishing && keyIt != objectsToSetup.cend()) {
0137                     auto keyObj = qobject_cast<Key *>(*keyIt);
0138                     keyObj->setKey(key);
0139                     keyObj->setPublishingMethod(mPublishingMethod);
0140                     keyObj->setMailBox(mEmail);
0141                 }
0142                 return;
0143             }
0144         }
0145 
0146         {
0147             // SetupManager still lives, but Identity has already been set up,
0148             // so update it.
0149             const auto setupObjects = mSetupManager->setupObjects();
0150             const auto it = std::find_if(setupObjects.cbegin(), setupObjects.cend(), [](SetupObject *obj) -> bool {
0151                 return qobject_cast<Identity *>(obj);
0152             });
0153             if (it != setupObjects.cend()) {
0154                 qCDebug(ACCOUNTWIZARD_LOG) << "Identity already set up, will modify existing Identity";
0155                 updateIdentity(mEmail, key.primaryFingerprint());
0156                 // Too late, we must publish the key ourselves now.
0157                 publishKeyIfNeeded(key);
0158                 return;
0159             }
0160         }
0161 
0162         // SetupManager still lives, but Identity is not pending nor finished,
0163         // which means it was not even prepared yet in SetupManager. This will
0164         // also handle publishing for us if needed.
0165         qCDebug(ACCOUNTWIZARD_LOG) << "Identity not ready yet, passing the key to SetupManager";
0166         mSetupManager->setKey(key);
0167         mSetupManager->setKeyPublishingMethod(mPublishingMethod);
0168         deleteLater();
0169     }
0170 
0171     void updateIdentity(const QString &email, const QByteArray &fingerprint)
0172     {
0173         auto manager = KIdentityManagementCore::IdentityManager::self();
0174         for (auto it = manager->modifyBegin(), end = manager->modifyEnd(); it != end; ++it) {
0175             if (it->primaryEmailAddress() == email) {
0176                 qCDebug(ACCOUNTWIZARD_LOG) << "Found matching identity for" << email << ":" << it->uoid();
0177                 it->setPGPSigningKey(fingerprint);
0178                 it->setPGPEncryptionKey(fingerprint);
0179                 manager->commit();
0180                 return;
0181             }
0182         }
0183         manager->rollback();
0184 
0185         qCWarning(ACCOUNTWIZARD_LOG) << "What? Could not find a matching identity for" << email << "!";
0186     }
0187 
0188     void publishKeyIfNeeded(const GpgME::Key &key)
0189     {
0190         if (mPublishingMethod == Key::NoPublishing) {
0191             qCDebug(ACCOUNTWIZARD_LOG) << "Key publishing not requested, we are done";
0192             deleteLater();
0193             return;
0194         }
0195 
0196         auto keyObj = new Key(mSetupManager); // mSetupManager can be null, but that's OK
0197         keyObj->setKey(key);
0198         keyObj->setPublishingMethod(mPublishingMethod);
0199         keyObj->setMailBox(mEmail);
0200         keyObj->setTransportId(mTransportId);
0201         connect(keyObj, &Key::error, this, [this](const QString &msg) {
0202             KNotification::event(KNotification::Error, i18n("Account Wizard"), msg, QStringLiteral("akonadi"));
0203             deleteLater();
0204         });
0205         connect(keyObj, &Key::finished, this, &KeyGenerationJob::deleteLater);
0206         keyObj->create();
0207     }
0208 
0209 private:
0210     // This job may outlive the application, make sure QApplication won't
0211     // quit before we are done
0212     QEventLoopLocker mLocker;
0213     QPointer<SetupManager> mSetupManager;
0214     QString mName;
0215     QString mEmail;
0216     QString mPassphrase;
0217     int mTransportId;
0218     Key::PublishingMethod mPublishingMethod;
0219 };
0220 
0221 class KeyImportJob : public QGpgME::Job
0222 {
0223     Q_OBJECT
0224 public:
0225     KeyImportJob(const QString &file, Kleo::KeySelectionCombo *parent)
0226         : QGpgME::Job(parent)
0227         , mFile(file)
0228         , mJob(nullptr)
0229     {
0230     }
0231 
0232     ~KeyImportJob() override = default;
0233 
0234     void slotCancel() override
0235     {
0236         if (mJob) {
0237             mJob->slotCancel();
0238         }
0239     }
0240 
0241     void start()
0242     {
0243         QGpgME::ImportJob *job = nullptr;
0244         switch (Kleo::findProtocol(mFile)) {
0245         case GpgME::OpenPGP:
0246             job = QGpgME::openpgp()->importJob();
0247             break;
0248         case GpgME::CMS:
0249             job = QGpgME::smime()->importJob();
0250             break;
0251         default:
0252             job = nullptr;
0253             break;
0254         }
0255 
0256         if (!job) {
0257             KMessageBox::error(qobject_cast<QWidget *>(parent()), i18n("Could not detect valid key type"), i18n("Import error"));
0258             Q_EMIT done();
0259             return;
0260         }
0261 
0262         QFile keyFile(mFile);
0263         if (!keyFile.open(QIODevice::ReadOnly)) {
0264             KMessageBox::error(qobject_cast<QWidget *>(parent()),
0265                                i18n("Cannot read data from the certificate file: %1", keyFile.errorString()),
0266                                i18n("Import error"));
0267             Q_EMIT done();
0268             return;
0269         }
0270 
0271         connect(job, &QGpgME::ImportJob::result, this, &KeyImportJob::keyImported);
0272         job->start(keyFile.readAll());
0273         mJob = job;
0274     }
0275 
0276     void keyImported(const GpgME::ImportResult &result)
0277     {
0278         mJob = nullptr;
0279         if (result.error()) {
0280             KMessageBox::error(qobject_cast<QWidget *>(parent()),
0281                                i18n("Failed to import key: %1", QString::fromUtf8(result.error().asString())),
0282                                i18n("Import error"));
0283             Q_EMIT done();
0284             return;
0285         }
0286 
0287         const auto imports = result.imports();
0288         if (imports.size() == 0) {
0289             KMessageBox::error(qobject_cast<QWidget *>(parent()), i18n("Failed to import key."), i18n("Import error"));
0290             Q_EMIT done();
0291             return;
0292         }
0293 
0294         auto combo = qobject_cast<Kleo::KeySelectionCombo *>(parent());
0295         combo->setDefaultKey(QLatin1StringView(result.import(0).fingerprint()));
0296         connect(combo, &Kleo::KeySelectionCombo::keyListingFinished, this, &KeyImportJob::done);
0297         combo->refreshKeys();
0298     }
0299 
0300 private:
0301     QString mFile;
0302     QGpgME::Job *mJob = nullptr;
0303 };
0304 
0305 CryptoPage::CryptoPage(Dialog *parent)
0306     : Page(parent)
0307     , mSetupManager(parent->setupManager())
0308 {
0309     ui.setupUi(this);
0310 
0311     std::shared_ptr<Kleo::DefaultKeyFilter> filter(new Kleo::DefaultKeyFilter);
0312     filter->setCanSign(Kleo::DefaultKeyFilter::Set);
0313     filter->setCanEncrypt(Kleo::DefaultKeyFilter::Set);
0314     filter->setHasSecret(Kleo::DefaultKeyFilter::Set);
0315     ui.keyCombo->setKeyFilter(filter);
0316     ui.keyCombo->prependCustomItem(QIcon(), i18n("No key"), NoKey);
0317     ui.keyCombo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Generate a new key pair"), GenerateKey);
0318     ui.keyCombo->appendCustomItem(QIcon::fromTheme(QStringLiteral("document-import")), i18n("Import a key"), ImportKey);
0319 
0320     connect(ui.keyCombo, &Kleo::KeySelectionCombo::customItemSelected, this, &CryptoPage::customItemSelected);
0321     connect(ui.keyCombo, &Kleo::KeySelectionCombo::currentKeyChanged, this, &CryptoPage::keySelected);
0322 }
0323 
0324 void CryptoPage::enterPageNext()
0325 {
0326     ui.keyCombo->setIdFilter(mSetupManager->email());
0327     if (ui.keyCombo->count() == 3) {
0328         // No key + Generate key + Import key options, no actual keys, so
0329         // pre-select the "Generate key" option
0330         const int idx = ui.keyCombo->findData(GenerateKey, Qt::UserRole);
0331         ui.keyCombo->setCurrentIndex(idx);
0332     } else {
0333         // We have at least one key, pre-select it
0334         // Index 0 is "No key" option, so index 1 will be a key
0335         ui.keyCombo->setCurrentIndex(1);
0336     }
0337 
0338     ui.stackedWidget->setCurrentIndex(CheckingkWKSPage);
0339     auto job = QGpgME::openpgp()->wksPublishJob();
0340     connect(job, &QGpgME::WKSPublishJob::result, this, [this](const GpgME::Error &error) {
0341         if (error) {
0342             ui.stackedWidget->setCurrentIndex(PKSPage);
0343         } else {
0344             ui.stackedWidget->setCurrentIndex(WKSPage);
0345         }
0346     });
0347     job->startCheck(mSetupManager->email());
0348 }
0349 
0350 void CryptoPage::leavePageNext()
0351 {
0352     const auto key = ui.keyCombo->currentKey();
0353     if (!key.isNull()) {
0354         mSetupManager->setKey(key);
0355         mSetupManager->setKeyPublishingMethod(currentPublishingMethod());
0356     } else if (ui.keyCombo->currentData(Qt::UserRole).toInt() == GenerateKey) {
0357         if (!mKeyGenerationJob) {
0358             mKeyGenerationJob = new KeyGenerationJob(mSetupManager, ui.passwordWidget->password(), currentPublishingMethod());
0359             ui.keyCombo->setEnabled(false); // disable until key is generated
0360             ui.passwordWidget->setEnabled(false);
0361             connect(mKeyGenerationJob.data(), &KeyGenerationJob::result, this, [this](const QString &fpr) {
0362                 ui.keyCombo->setEnabled(true);
0363                 ui.passwordWidget->setEnabled(true);
0364                 ui.keyCombo->setDefaultKey(fpr);
0365                 ui.keyCombo->refreshKeys();
0366             });
0367         }
0368     }
0369     mSetupManager->setPgpAutoEncrypt(ui.enableCryptoCheckBox->isChecked());
0370     mSetupManager->setPgpAutoSign(ui.enableCryptoCheckBox->isChecked());
0371 }
0372 
0373 Key::PublishingMethod CryptoPage::currentPublishingMethod() const
0374 {
0375     if (ui.stackedWidget->currentIndex() == PKSPage && ui.pksCheckBox->isChecked()) {
0376         return Key::PKS;
0377     } else if (ui.stackedWidget->currentIndex() == WKSPage && ui.wksCheckBox->isChecked()) {
0378         return Key::WKS;
0379     } else {
0380         return Key::NoPublishing;
0381     }
0382 }
0383 
0384 void CryptoPage::customItemSelected(const QVariant &data)
0385 {
0386     switch (data.toInt()) {
0387     case NoKey:
0388         setValid(true);
0389         setPublishingEnabled(false);
0390         ui.passwordWidget->setVisible(false);
0391         return;
0392     case GenerateKey:
0393         setValid(true);
0394         setPublishingEnabled(true);
0395         ui.passwordWidget->setVisible(true);
0396         break;
0397     case ImportKey:
0398         setValid(false);
0399         setPublishingEnabled(true);
0400         ui.passwordWidget->setVisible(false);
0401         importKey();
0402         break;
0403     }
0404 }
0405 
0406 void CryptoPage::keySelected(const GpgME::Key &key)
0407 {
0408     ui.passwordWidget->setVisible(false);
0409 
0410     // We don't support publishing for S/MIME
0411     setPublishingEnabled(key.protocol() == GpgME::OpenPGP);
0412     setValid(!key.isNull());
0413 }
0414 
0415 void CryptoPage::setPublishingEnabled(bool enabled)
0416 {
0417     ui.wksCheckBox->setEnabled(enabled);
0418     ui.wksCheckBox->setChecked(enabled);
0419     ui.pksCheckBox->setEnabled(enabled);
0420     ui.pksCheckBox->setChecked(enabled);
0421 }
0422 
0423 void CryptoPage::importKey()
0424 {
0425     const QString certificateFilter = i18n("Certificates") + QLatin1StringView(" (*.asc *.cer *.cert *.crt *.der *.pem *.gpg *.p7c *.p12 *.pfx *.pgp)");
0426     const QString anyFilesFilter = i18n("Any files") + QLatin1StringView(" (*)");
0427 
0428     const QString file =
0429         QFileDialog::getOpenFileName(this, i18n("Select Certificate File"), QString(), certificateFilter + QLatin1StringView(";;") + anyFilesFilter);
0430     if (file.isEmpty()) {
0431         return;
0432     }
0433 
0434     auto job = new KeyImportJob(file, ui.keyCombo);
0435     new Kleo::ProgressDialog(job, i18n("Importing key..."), ui.keyCombo->parentWidget());
0436     ui.keyCombo->setEnabled(false);
0437     QObject::connect(job, &KeyImportJob::done, ui.keyCombo, [this]() {
0438         ui.keyCombo->setEnabled(true);
0439     });
0440     job->start();
0441 }
0442 
0443 #include "cryptopage.moc"
0444 
0445 #include "moc_cryptopage.cpp"