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"