File indexing completed on 2024-06-23 05:13:37

0001 /* -*- mode: c++; c-basic-offset:4 -*-
0002     commands/changeexpirycommand.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2021 g10 Code GmbH
0007     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0008 
0009     SPDX-License-Identifier: GPL-2.0-or-later
0010 */
0011 
0012 #include <config-kleopatra.h>
0013 
0014 #include "changeexpirycommand.h"
0015 #include "command_p.h"
0016 
0017 #include "dialogs/expirydialog.h"
0018 #include "utils/expiration.h"
0019 
0020 #include <Libkleo/Formatting>
0021 
0022 #include <KLocalizedString>
0023 
0024 #include <QGpgME/ChangeExpiryJob>
0025 #include <QGpgME/Protocol>
0026 
0027 #include <QDateTime>
0028 
0029 #include <gpgme++/key.h>
0030 
0031 #include "kleopatra_debug.h"
0032 
0033 using namespace Kleo;
0034 using namespace Kleo::Commands;
0035 using namespace Kleo::Dialogs;
0036 using namespace GpgME;
0037 using namespace QGpgME;
0038 
0039 namespace
0040 {
0041 bool subkeyHasSameExpirationAsPrimaryKey(const Subkey &subkey)
0042 {
0043     // we allow for a difference in expiration of up to 10 seconds
0044     static const auto maxExpirationDifference = 10;
0045 
0046     Q_ASSERT(!subkey.isNull());
0047     const auto key = subkey.parent();
0048     const auto primaryKey = key.subkey(0);
0049     const auto primaryExpiration = quint32(primaryKey.expirationTime());
0050     const auto subkeyExpiration = quint32(subkey.expirationTime());
0051     if (primaryExpiration != 0 && subkeyExpiration != 0) {
0052         return (primaryExpiration == subkeyExpiration) //
0053             || ((primaryExpiration > subkeyExpiration) && (primaryExpiration - subkeyExpiration <= maxExpirationDifference)) //
0054             || ((primaryExpiration < subkeyExpiration) && (subkeyExpiration - primaryExpiration <= maxExpirationDifference));
0055     }
0056     return primaryKey.neverExpires() && subkey.neverExpires();
0057 }
0058 
0059 bool allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(const Key &key)
0060 {
0061     Q_ASSERT(!key.isNull() && key.numSubkeys() > 0);
0062     const auto subkeys = key.subkeys();
0063     return std::all_of(std::begin(subkeys), std::end(subkeys), [](const auto &subkey) {
0064         // revoked subkeys are ignored by gpg --quick-set-expire when updating the expiration of all subkeys;
0065         // check if expiration of subkey is (more or less) the same as the expiration of the primary key
0066         return subkey.isRevoked() || subkeyHasSameExpirationAsPrimaryKey(subkey);
0067     });
0068 }
0069 }
0070 
0071 class ChangeExpiryCommand::Private : public Command::Private
0072 {
0073     friend class ::Kleo::Commands::ChangeExpiryCommand;
0074     ChangeExpiryCommand *q_func() const
0075     {
0076         return static_cast<ChangeExpiryCommand *>(q);
0077     }
0078 
0079 public:
0080     explicit Private(ChangeExpiryCommand *qq, KeyListController *c);
0081     ~Private() override;
0082 
0083 private:
0084     void slotDialogAccepted();
0085     void slotDialogRejected();
0086     void slotResult(const Error &err);
0087 
0088 private:
0089     void ensureDialogCreated(ExpiryDialog::Mode mode);
0090     void createJob();
0091     void showErrorDialog(const Error &error);
0092     void showSuccessDialog();
0093 
0094 private:
0095     GpgME::Key key;
0096     GpgME::Subkey subkey;
0097     QPointer<ExpiryDialog> dialog;
0098     QPointer<ChangeExpiryJob> job;
0099 };
0100 
0101 ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func()
0102 {
0103     return static_cast<Private *>(d.get());
0104 }
0105 const ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() const
0106 {
0107     return static_cast<const Private *>(d.get());
0108 }
0109 
0110 #define d d_func()
0111 #define q q_func()
0112 
0113 ChangeExpiryCommand::Private::Private(ChangeExpiryCommand *qq, KeyListController *c)
0114     : Command::Private{qq, c}
0115 {
0116 }
0117 
0118 ChangeExpiryCommand::Private::~Private() = default;
0119 
0120 void ChangeExpiryCommand::Private::slotDialogAccepted()
0121 {
0122     Q_ASSERT(dialog);
0123 
0124     static const QTime END_OF_DAY{23, 59, 00};
0125 
0126     const QDateTime expiry{dialog->dateOfExpiry(), END_OF_DAY};
0127 
0128     qCDebug(KLEOPATRA_LOG) << "expiry" << expiry;
0129 
0130     createJob();
0131     Q_ASSERT(job);
0132 
0133     std::vector<Subkey> subkeysToUpdate;
0134     if (!subkey.isNull()) {
0135         // change expiration of a single subkey
0136         if (subkey.keyID() != key.keyID()) { // ignore the primary subkey
0137             subkeysToUpdate.push_back(subkey);
0138         }
0139     } else {
0140         // change expiration of the (primary) key and, optionally, of some subkeys
0141         job->setOptions(ChangeExpiryJob::UpdatePrimaryKey);
0142         if (dialog->updateExpirationOfAllSubkeys() && key.numSubkeys() > 1) {
0143             // explicitly list the subkeys for which the expiration should be changed
0144             // together with the expiration of the (primary) key, so that already expired
0145             // subkeys are also updated
0146             const auto subkeys = key.subkeys();
0147             std::copy_if(std::next(subkeys.begin()), subkeys.end(), std::back_inserter(subkeysToUpdate), [](const auto &subkey) {
0148                 // skip revoked subkeys which would anyway be ignored by gpg;
0149                 // also skip subkeys without explicit expiration because they inherit the primary key's expiration;
0150                 // include all subkeys that are not yet expired or that expired around the same time as the primary key
0151                 return !subkey.isRevoked() //
0152                     && !subkey.neverExpires() //
0153                     && (!subkey.isExpired() || subkeyHasSameExpirationAsPrimaryKey(subkey));
0154             });
0155         }
0156     }
0157 
0158     if (const Error err = job->start(key, expiry, subkeysToUpdate)) {
0159         showErrorDialog(err);
0160         finished();
0161     }
0162 }
0163 
0164 void ChangeExpiryCommand::Private::slotDialogRejected()
0165 {
0166     Q_EMIT q->canceled();
0167     finished();
0168 }
0169 
0170 void ChangeExpiryCommand::Private::slotResult(const Error &err)
0171 {
0172     if (err.isCanceled())
0173         ;
0174     else if (err) {
0175         showErrorDialog(err);
0176     } else {
0177         showSuccessDialog();
0178     }
0179     finished();
0180 }
0181 
0182 void ChangeExpiryCommand::Private::ensureDialogCreated(ExpiryDialog::Mode mode)
0183 {
0184     if (dialog) {
0185         return;
0186     }
0187 
0188     dialog = new ExpiryDialog{mode};
0189     applyWindowID(dialog);
0190     dialog->setAttribute(Qt::WA_DeleteOnClose);
0191 
0192     connect(dialog, &QDialog::accepted, q, [this]() {
0193         slotDialogAccepted();
0194     });
0195     connect(dialog, &QDialog::rejected, q, [this]() {
0196         slotDialogRejected();
0197     });
0198 }
0199 
0200 void ChangeExpiryCommand::Private::createJob()
0201 {
0202     Q_ASSERT(!job);
0203 
0204     const auto backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime();
0205     if (!backend) {
0206         return;
0207     }
0208 
0209     ChangeExpiryJob *const j = backend->changeExpiryJob();
0210     if (!j) {
0211         return;
0212     }
0213 
0214     connect(j, &QGpgME::Job::jobProgress, q, &Command::progress);
0215     connect(j, &ChangeExpiryJob::result, q, [this](const auto &err) {
0216         slotResult(err);
0217     });
0218 
0219     job = j;
0220 }
0221 
0222 void ChangeExpiryCommand::Private::showErrorDialog(const Error &err)
0223 {
0224     error(
0225         i18n("<p>An error occurred while trying to change "
0226              "the end of the validity period for <b>%1</b>:</p><p>%2</p>",
0227              Formatting::formatForComboBox(key),
0228              Formatting::errorAsString(err)));
0229 }
0230 
0231 void ChangeExpiryCommand::Private::showSuccessDialog()
0232 {
0233     success(i18n("End of validity period changed successfully."));
0234 }
0235 
0236 ChangeExpiryCommand::ChangeExpiryCommand(KeyListController *c)
0237     : Command{new Private{this, c}}
0238 {
0239 }
0240 
0241 ChangeExpiryCommand::ChangeExpiryCommand(QAbstractItemView *v, KeyListController *c)
0242     : Command{v, new Private{this, c}}
0243 {
0244 }
0245 
0246 ChangeExpiryCommand::ChangeExpiryCommand(const GpgME::Key &key)
0247     : Command{key, new Private{this, nullptr}}
0248 {
0249 }
0250 
0251 ChangeExpiryCommand::~ChangeExpiryCommand() = default;
0252 
0253 void ChangeExpiryCommand::setSubkey(const GpgME::Subkey &subkey)
0254 {
0255     d->subkey = subkey;
0256 }
0257 
0258 void ChangeExpiryCommand::doStart()
0259 {
0260     const std::vector<Key> keys = d->keys();
0261     if (keys.size() != 1 //
0262         || keys.front().protocol() != GpgME::OpenPGP //
0263         || !keys.front().hasSecret() //
0264         || keys.front().subkey(0).isNull()) {
0265         d->finished();
0266         return;
0267     }
0268 
0269     d->key = keys.front();
0270 
0271     if (!d->subkey.isNull() && d->subkey.parent().primaryFingerprint() != d->key.primaryFingerprint()) {
0272         qDebug() << "Invalid subkey" << d->subkey.fingerprint() << ": Not a subkey of key" << d->key.primaryFingerprint();
0273         d->finished();
0274         return;
0275     }
0276 
0277     ExpiryDialog::Mode mode;
0278     if (!d->subkey.isNull()) {
0279         mode = ExpiryDialog::Mode::UpdateIndividualSubkey;
0280     } else if (d->key.numSubkeys() == 1) {
0281         mode = ExpiryDialog::Mode::UpdateCertificateWithoutSubkeys;
0282     } else {
0283         mode = ExpiryDialog::Mode::UpdateCertificateWithSubkeys;
0284     }
0285     d->ensureDialogCreated(mode);
0286     Q_ASSERT(d->dialog);
0287     const Subkey subkey = !d->subkey.isNull() ? d->subkey : d->key.subkey(0);
0288     d->dialog->setDateOfExpiry((subkey.neverExpires() //
0289                                     ? QDate{} //
0290                                     : defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)));
0291     if (mode == ExpiryDialog::Mode::UpdateIndividualSubkey && subkey.keyID() != subkey.parent().keyID()) {
0292         d->dialog->setPrimaryKey(subkey.parent());
0293     } else if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) {
0294         d->dialog->setUpdateExpirationOfAllSubkeys(allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(d->key));
0295     }
0296 
0297     d->dialog->show();
0298 }
0299 
0300 void ChangeExpiryCommand::doCancel()
0301 {
0302     if (d->job) {
0303         d->job->slotCancel();
0304     }
0305 }
0306 
0307 #undef d
0308 #undef q
0309 
0310 #include "moc_changeexpirycommand.cpp"