diff --git a/src/commands/changeexpirycommand.cpp b/src/commands/changeexpirycommand.cpp index 2ebffc2b8..0c972a979 100644 --- a/src/commands/changeexpirycommand.cpp +++ b/src/commands/changeexpirycommand.cpp @@ -1,316 +1,318 @@ /* -*- mode: c++; c-basic-offset:4 -*- commands/changeexpirycommand.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "changeexpirycommand.h" #include "command_p.h" #include "dialogs/expirydialog.h" +#include "utils/keys.h" #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::Commands; using namespace Kleo::Dialogs; using namespace GpgME; using namespace QGpgME; namespace { #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY bool subkeyHasSameExpirationAsPrimaryKey(const Subkey &subkey) { // we allow for a difference in expiration of up to 10 seconds static const auto maxExpirationDifference = 10; Q_ASSERT(!subkey.isNull()); const auto key = subkey.parent(); const auto primaryKey = key.subkey(0); const auto primaryExpiration = quint32(primaryKey.expirationTime()); const auto subkeyExpiration = quint32(subkey.expirationTime()); if (primaryExpiration != 0 && subkeyExpiration != 0) { return (primaryExpiration == subkeyExpiration) // || ((primaryExpiration > subkeyExpiration) && (primaryExpiration - subkeyExpiration <= maxExpirationDifference)) // || ((primaryExpiration < subkeyExpiration) && (subkeyExpiration - primaryExpiration <= maxExpirationDifference)); } return primaryKey.neverExpires() && subkey.neverExpires(); } bool allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(const Key &key) { Q_ASSERT(!key.isNull() && key.numSubkeys() > 0); const auto subkeys = key.subkeys(); return std::all_of(std::begin(subkeys), std::end(subkeys), [](const auto &subkey) { // revoked subkeys are ignored by gpg --quick-set-expire when updating the expiration of all subkeys; // check if expiration of subkey is (more or less) the same as the expiration of the primary key return subkey.isRevoked() || subkeyHasSameExpirationAsPrimaryKey(subkey); }); } #endif } class ChangeExpiryCommand::Private : public Command::Private { friend class ::Kleo::Commands::ChangeExpiryCommand; ChangeExpiryCommand *q_func() const { return static_cast(q); } public: explicit Private(ChangeExpiryCommand *qq, KeyListController *c); ~Private() override; private: void slotDialogAccepted(); void slotDialogRejected(); void slotResult(const Error &err); private: void ensureDialogCreated(ExpiryDialog::Mode mode); void createJob(); void showErrorDialog(const Error &error); void showSuccessDialog(); private: GpgME::Key key; GpgME::Subkey subkey; QPointer dialog; QPointer job; }; ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() { return static_cast(d.get()); } const ChangeExpiryCommand::Private *ChangeExpiryCommand::d_func() const { return static_cast(d.get()); } #define d d_func() #define q q_func() ChangeExpiryCommand::Private::Private(ChangeExpiryCommand *qq, KeyListController *c) : Command::Private{qq, c} { } ChangeExpiryCommand::Private::~Private() = default; void ChangeExpiryCommand::Private::slotDialogAccepted() { Q_ASSERT(dialog); static const QTime END_OF_DAY{23, 59, 00}; const QDateTime expiry{dialog->dateOfExpiry(), END_OF_DAY}; qCDebug(KLEOPATRA_LOG) << "expiry" << expiry; createJob(); Q_ASSERT(job); std::vector subkeysToUpdate; if (!subkey.isNull()) { // change expiration of a single subkey if (subkey.keyID() != key.keyID()) { // ignore the primary subkey subkeysToUpdate.push_back(subkey); } } else { #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY // change expiration of the (primary) key and, optionally, of some subkeys job->setOptions(ChangeExpiryJob::UpdatePrimaryKey); if (dialog->updateExpirationOfAllSubkeys() && key.numSubkeys() > 1) { // explicitly list the subkeys for which the expiration should be changed // together with the expiration of the (primary) key, so that already expired // subkeys are also updated const auto subkeys = key.subkeys(); std::copy_if(std::next(subkeys.begin()), subkeys.end(), std::back_inserter(subkeysToUpdate), [](const auto &subkey) { // skip revoked subkeys which would anyway be ignored by gpg; // also skip subkeys without explicit expiration because they inherit the primary key's expiration; // include all subkeys that are not yet expired or that expired around the same time as the primary key return !subkey.isRevoked() // && !subkey.neverExpires() // && (!subkey.isExpired() || subkeyHasSameExpirationAsPrimaryKey(subkey)); }); } #else // nothing to do #endif } if (const Error err = job->start(key, expiry, subkeysToUpdate)) { showErrorDialog(err); finished(); } } void ChangeExpiryCommand::Private::slotDialogRejected() { Q_EMIT q->canceled(); finished(); } void ChangeExpiryCommand::Private::slotResult(const Error &err) { if (err.isCanceled()) ; else if (err) { showErrorDialog(err); } else { showSuccessDialog(); } finished(); } void ChangeExpiryCommand::Private::ensureDialogCreated(ExpiryDialog::Mode mode) { if (dialog) { return; } dialog = new ExpiryDialog{mode}; applyWindowID(dialog); dialog->setAttribute(Qt::WA_DeleteOnClose); connect(dialog, &QDialog::accepted, q, [this]() { slotDialogAccepted(); }); connect(dialog, &QDialog::rejected, q, [this]() { slotDialogRejected(); }); } void ChangeExpiryCommand::Private::createJob() { Q_ASSERT(!job); const auto backend = (key.protocol() == GpgME::OpenPGP) ? QGpgME::openpgp() : QGpgME::smime(); if (!backend) { return; } ChangeExpiryJob *const j = backend->changeExpiryJob(); if (!j) { return; } #if QGPGME_JOB_HAS_NEW_PROGRESS_SIGNALS connect(j, &QGpgME::Job::jobProgress, q, &Command::progress); #else connect(j, &QGpgME::Job::progress, q, [this](const QString &, int current, int total) { Q_EMIT q->progress(current, total); }); #endif connect(j, &ChangeExpiryJob::result, q, [this] (const auto &err) { slotResult(err); }); job = j; } void ChangeExpiryCommand::Private::showErrorDialog(const Error &err) { error(i18n("

An error occurred while trying to change " "the end of the validity period for %1:

%2

", Formatting::formatForComboBox(key), Formatting::errorAsString(err))); } void ChangeExpiryCommand::Private::showSuccessDialog() { success(i18n("End of validity period changed successfully.")); } ChangeExpiryCommand::ChangeExpiryCommand(KeyListController *c) : Command{new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(QAbstractItemView *v, KeyListController *c) : Command{v, new Private{this, c}} { } ChangeExpiryCommand::ChangeExpiryCommand(const GpgME::Key &key) : Command{key, new Private{this, nullptr}} { } ChangeExpiryCommand::~ChangeExpiryCommand() = default; void ChangeExpiryCommand::setSubkey(const GpgME::Subkey &subkey) { d->subkey = subkey; } void ChangeExpiryCommand::doStart() { const std::vector keys = d->keys(); if (keys.size() != 1 || keys.front().protocol() != GpgME::OpenPGP || !keys.front().hasSecret() || keys.front().subkey(0).isNull()) { d->finished(); return; } d->key = keys.front(); if (!d->subkey.isNull() && d->subkey.parent().primaryFingerprint() != d->key.primaryFingerprint()) { qDebug() << "Invalid subkey" << d->subkey.fingerprint() << ": Not a subkey of key" << d->key.primaryFingerprint(); d->finished(); return; } ExpiryDialog::Mode mode; if (!d->subkey.isNull()) { mode = ExpiryDialog::Mode::UpdateIndividualSubkey; } else if (d->key.numSubkeys() == 1) { mode = ExpiryDialog::Mode::UpdateCertificateWithoutSubkeys; } else { mode = ExpiryDialog::Mode::UpdateCertificateWithSubkeys; } d->ensureDialogCreated(mode); Q_ASSERT(d->dialog); const Subkey subkey = !d->subkey.isNull() ? d->subkey : d->key.subkey(0); - d->dialog->setDateOfExpiry(subkey.neverExpires() ? QDate() : - QDateTime::fromSecsSinceEpoch(quint32(subkey.expirationTime())).date()); + d->dialog->setDateOfExpiry((subkey.neverExpires() // + ? QDate{} // + : defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration))); #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY if (mode == ExpiryDialog::Mode::UpdateCertificateWithSubkeys) { d->dialog->setUpdateExpirationOfAllSubkeys(allNotRevokedSubkeysHaveSameExpirationAsPrimaryKey(d->key)); } #endif d->dialog->show(); } void ChangeExpiryCommand::doCancel() { if (d->job) { d->job->slotCancel(); } } #undef d #undef q #include "moc_changeexpirycommand.cpp" diff --git a/src/dialogs/expirydialog.cpp b/src/dialogs/expirydialog.cpp index 78f3acdeb..f186d70c8 100644 --- a/src/dialogs/expirydialog.cpp +++ b/src/dialogs/expirydialog.cpp @@ -1,354 +1,358 @@ /* -*- mode: c++; c-basic-offset:4 -*- dialogs/expirydialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2021 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "expirydialog.h" #include "utils/gui-helper.h" #include "utils/qt-cxx20-compat.h" +#include "utils/keys.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kleo; using namespace Kleo::Dialogs; namespace { enum Period { Days, Weeks, Months, Years, NumPeriods }; static QDate date_by_amount_and_unit(int inAmount, int inUnit) { const QDate current = QDate::currentDate(); switch (inUnit) { case Days: return current.addDays(inAmount); case Weeks: return current.addDays(7 * inAmount); case Months: return current.addMonths(inAmount); case Years: return current.addYears(inAmount); default: Q_ASSERT(!"Should not reach here"); } return QDate(); } static QString accessibleValidityDuration(int amount, Period unit) { switch (unit) { case Days: return i18np("Valid for %1 day", "Valid for %1 days", amount); case Weeks: return i18np("Valid for %1 week", "Valid for %1 weeks", amount); case Months: return i18np("Valid for %1 month", "Valid for %1 months", amount); case Years: return i18np("Valid for %1 year", "Valid for %1 years", amount); default: Q_ASSERT(!"invalid unit"); } return {}; } // these calculations should be precise enough for the forseeable future... static const double DAYS_IN_GREGORIAN_YEAR = 365.2425; static int monthsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR * 12); } static int yearsBetween(const QDate &d1, const QDate &d2) { const int days = d1.daysTo(d2); return qRound(days / DAYS_IN_GREGORIAN_YEAR); } } class ExpiryDialog::Private { friend class ::Kleo::Dialogs::ExpiryDialog; ExpiryDialog *const q; public: explicit Private(Mode mode, ExpiryDialog *qq) : q{qq} , mode{mode} , inUnit{Days} , ui{mode, q} { #if QT_DEPRECATED_SINCE(5, 14) connect(ui.inSB, qOverload(&QSpinBox::valueChanged), q, [this] () { slotInAmountChanged(); }); #else connect(ui.inSB, &QSpinBox::valueChanged, q, [this] () { slotInAmountChanged(); }); #endif connect(ui.inCB, qOverload(&QComboBox::currentIndexChanged), q, [this] () { slotInUnitChanged(); }); connect(ui.onCB, &KDateComboBox::dateChanged, q, [this] () { slotOnDateChanged(); }); Q_ASSERT(ui.inCB->currentIndex() == inUnit); } private: void slotInAmountChanged(); void slotInUnitChanged(); void slotOnDateChanged(); private: QDate inDate() const; int inAmountByDate(const QDate &date) const; void setInitialFocus(); private: ExpiryDialog::Mode mode; int inUnit; bool initialFocusWasSet = false; struct UI { QRadioButton *neverRB; QRadioButton *inRB; QSpinBox *inSB; QComboBox *inCB; QRadioButton *onRB; KDateComboBox *onCB; QCheckBox *updateSubkeysCheckBox; explicit UI(Mode mode, Dialogs::ExpiryDialog *qq) { auto mainLayout = new QVBoxLayout{qq}; auto mainWidget = new QWidget{qq}; auto vboxLayout = new QVBoxLayout{mainWidget}; vboxLayout->setContentsMargins(0, 0, 0, 0); { auto label = new QLabel{qq}; label->setText(mode == Mode::UpdateIndividualSubkey ? i18n("Please select until when the subkey should be valid:") : i18n("Please select until when the certificate should be valid:")); vboxLayout->addWidget(label); } neverRB = new QRadioButton(i18n("Unlimited validity"), mainWidget); neverRB->setChecked(false); vboxLayout->addWidget(neverRB); { auto hboxLayout = new QHBoxLayout; inRB = new QRadioButton{i18n("Valid for:"), mainWidget}; inRB->setChecked(false); hboxLayout->addWidget(inRB); inSB = new QSpinBox{mainWidget}; inSB->setEnabled(false); inSB->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); inSB->setMinimum(1); hboxLayout->addWidget(inSB); inCB = new QComboBox{mainWidget}; inCB->addItem(i18n("Days")); inCB->addItem(i18n("Weeks")); inCB->addItem(i18n("Months")); inCB->addItem(i18n("Years")); Q_ASSERT(inCB->count() == NumPeriods); inCB->setEnabled(false); hboxLayout->addWidget(inCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { auto hboxLayout = new QHBoxLayout; onRB = new QRadioButton{i18n("Valid until:"), mainWidget}; onRB->setChecked(true); hboxLayout->addWidget(onRB); onCB = new KDateComboBox{mainWidget}; onCB->setMinimumDate(QDate::currentDate().addDays(1)); hboxLayout->addWidget(onCB); hboxLayout->addStretch(1); vboxLayout->addLayout(hboxLayout); } { updateSubkeysCheckBox = new QCheckBox{i18n("Also update the validity period of the subkeys"), qq}; #if QGPGME_SUPPORTS_CHANGING_EXPIRATION_OF_COMPLETE_KEY updateSubkeysCheckBox->setVisible(mode == Mode::UpdateCertificateWithSubkeys); #else updateSubkeysCheckBox->setVisible(false); #endif vboxLayout->addWidget(updateSubkeysCheckBox); } vboxLayout->addStretch(1); mainLayout->addWidget(mainWidget); auto buttonBox = new QDialogButtonBox{QDialogButtonBox::Ok | QDialogButtonBox::Cancel, qq}; auto okButton = buttonBox->button(QDialogButtonBox::Ok); KGuiItem::assign(okButton, KStandardGuiItem::ok()); okButton->setDefault(true); okButton->setShortcut(Qt::CTRL | Qt::Key_Return); KGuiItem::assign(buttonBox->button(QDialogButtonBox::Cancel), KStandardGuiItem::cancel()); qq->connect(buttonBox, &QDialogButtonBox::accepted, qq, &QDialog::accept); qq->connect(buttonBox, &QDialogButtonBox::rejected, qq, &QDialog::reject); mainLayout->addWidget(buttonBox); connect(onRB, &QRadioButton::toggled, onCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inCB, &QWidget::setEnabled); connect(inRB, &QRadioButton::toggled, inSB, &QWidget::setEnabled); } } ui; }; void ExpiryDialog::Private::slotInUnitChanged() { const int oldInAmount = ui.inSB->value(); const QDate targetDate = date_by_amount_and_unit(oldInAmount, inUnit); inUnit = ui.inCB->currentIndex(); if (targetDate.isValid()) { ui.inSB->setValue(inAmountByDate(targetDate)); } else { slotInAmountChanged(); } } void ExpiryDialog::Private::slotInAmountChanged() { if (ui.inRB->isChecked()) { ui.onCB->setDate(inDate()); } ui.inRB->setAccessibleName(accessibleValidityDuration(ui.inSB->value(), static_cast(ui.inCB->currentIndex()))); } void ExpiryDialog::Private::slotOnDateChanged() { - if (ui.onRB->isChecked()) { + if (!ui.inRB->isChecked()) { ui.inSB->setValue(inAmountByDate(ui.onCB->date())); } ui.onRB->setAccessibleName(i18nc("Valid until DATE", "Valid until %1", Formatting::accessibleDate(ui.onCB->date()))); } QDate ExpiryDialog::Private::inDate() const { return date_by_amount_and_unit(ui.inSB->value(), ui.inCB->currentIndex()); } int ExpiryDialog::Private::inAmountByDate(const QDate &selected) const { const QDate current = QDate::currentDate(); switch (ui.inCB->currentIndex()) { case Days: return current.daysTo(selected); case Weeks: return qRound(current.daysTo(selected) / 7.0); case Months: return monthsBetween(current, selected); case Years: return yearsBetween(current, selected); }; Q_ASSERT(!"Should not reach here"); return -1; } void ExpiryDialog::Private::setInitialFocus() { if (initialFocusWasSet) { return; } // give focus to the checked radio button (void) focusFirstCheckedButton({ui.neverRB, ui.inRB, ui.onRB}); initialFocusWasSet = true; } ExpiryDialog::ExpiryDialog(Mode mode, QWidget *p) : QDialog{p} , d{new Private{mode, this}} { setWindowTitle(i18nc("@title:window", "Change Validity Period")); } ExpiryDialog::~ExpiryDialog() = default; void ExpiryDialog::setDateOfExpiry(const QDate &date) { const QDate current = QDate::currentDate(); if (date.isValid()) { d->ui.onRB->setChecked(true); - d->ui.onCB->setDate(qMax(date, current)); + if (date <= current) { + d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); + } else { + d->ui.onCB->setDate(date); + } } else { d->ui.neverRB->setChecked(true); - d->ui.onCB->setDate(current); - d->ui.inSB->setValue(0); + d->ui.onCB->setDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } } QDate ExpiryDialog::dateOfExpiry() const { return d->ui.inRB->isChecked() ? d->inDate() : d->ui.onRB->isChecked() ? d->ui.onCB->date() : QDate{}; } void ExpiryDialog::setUpdateExpirationOfAllSubkeys(bool update) { d->ui.updateSubkeysCheckBox->setChecked(update); } bool ExpiryDialog::updateExpirationOfAllSubkeys() const { return d->ui.updateSubkeysCheckBox->isChecked(); } void ExpiryDialog::showEvent(QShowEvent *event) { d->setInitialFocus(); QDialog::showEvent(event); } #include "moc_expirydialog.cpp" diff --git a/src/newcertificatewizard/advancedsettingsdialog.cpp b/src/newcertificatewizard/advancedsettingsdialog.cpp index 84788327f..838c30ce6 100644 --- a/src/newcertificatewizard/advancedsettingsdialog.cpp +++ b/src/newcertificatewizard/advancedsettingsdialog.cpp @@ -1,1088 +1,1069 @@ /* -*- mode: c++; c-basic-offset:4 -*- newcertificatewizard/advancedsettingsdialog.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2008 Klarälvdalens Datakonsult AB SPDX-FileCopyrightText: 2016, 2017 Bundesamt für Sicherheit in der Informationstechnik SPDX-FileContributor: Intevation GmbH SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include #include "advancedsettingsdialog_p.h" #include "keyalgo_p.h" #include "listwidget.h" #include "utils/gui-helper.h" +#include "utils/keys.h" #include "utils/scrollarea.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "kleopatra_debug.h" using namespace Kleo; using namespace Kleo::NewCertificateUi; using namespace GpgME; static const char RSA_KEYSIZES_ENTRY[] = "RSAKeySizes"; static const char DSA_KEYSIZES_ENTRY[] = "DSAKeySizes"; static const char ELG_KEYSIZES_ENTRY[] = "ELGKeySizes"; static const char RSA_KEYSIZE_LABELS_ENTRY[] = "RSAKeySizeLabels"; static const char DSA_KEYSIZE_LABELS_ENTRY[] = "DSAKeySizeLabels"; static const char ELG_KEYSIZE_LABELS_ENTRY[] = "ELGKeySizeLabels"; static const char PGP_KEY_TYPE_ENTRY[] = "PGPKeyType"; static const char CMS_KEY_TYPE_ENTRY[] = "CMSKeyType"; // This should come from gpgme in the future // For now we only support the basic 2.1 curves and check // for GnuPG 2.1. The whole subkey / usage generation needs // new api and a reworked dialog. (ah 10.3.16) // EDDSA should be supported, too. static const QStringList curveNames { { QStringLiteral("brainpoolP256r1") }, { QStringLiteral("brainpoolP384r1") }, { QStringLiteral("brainpoolP512r1") }, { QStringLiteral("NIST P-256") }, { QStringLiteral("NIST P-384") }, { QStringLiteral("NIST P-521") }, }; namespace { static void set_keysize(QComboBox *cb, unsigned int strength) { if (!cb) { return; } const int idx = cb->findData(static_cast(strength)); if (idx >= 0) { cb->setCurrentIndex(idx); } } static unsigned int get_keysize(const QComboBox *cb) { if (!cb) { return 0; } const int idx = cb->currentIndex(); if (idx < 0) { return 0; } return cb->itemData(idx).toInt(); } static void set_curve(QComboBox *cb, const QString &curve) { if (!cb) { return; } const int idx = cb->findText(curve, Qt::MatchFixedString); if (idx >= 0) { cb->setCurrentIndex(idx); } } static QString get_curve(const QComboBox *cb) { if (!cb) { return QString(); } return cb->currentText(); } // Extract the algo information from default_pubkey_algo format // // and put it into the return values size, algo and curve. // // Values look like: // RSA-2048 // rsa2048/cert,sign+rsa2048/enc // brainpoolP256r1+brainpoolP256r1 static void parseAlgoString(const QString &algoString, int *size, Subkey::PubkeyAlgo *algo, QString &curve) { const auto split = algoString.split(QLatin1Char('/')); bool isEncrypt = split.size() == 2 && split[1].contains(QLatin1String("enc")); // Normalize const auto lowered = split[0].toLower().remove(QLatin1Char('-')); if (!algo || !size) { return; } *algo = Subkey::AlgoUnknown; if (lowered.startsWith(QLatin1String("rsa"))) { *algo = Subkey::AlgoRSA; } else if (lowered.startsWith(QLatin1String("dsa"))) { *algo = Subkey::AlgoDSA; } else if (lowered.startsWith(QLatin1String("elg"))) { *algo = Subkey::AlgoELG; } if (*algo != Subkey::AlgoUnknown) { bool ok; #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) *size = lowered.rightRef(lowered.size() - 3).toInt(&ok); #else *size = QStringView(lowered).right(lowered.size() - 3).toInt(&ok); #endif if (!ok) { qCWarning(KLEOPATRA_LOG) << "Could not extract size from: " << lowered; *size = 3072; } return; } // Now the ECC Algorithms if (lowered.startsWith(QLatin1String("ed25519"))) { // Special handling for this as technically // this is a cv25519 curve used for EDDSA if (isEncrypt) { curve = QLatin1String("cv25519"); *algo = Subkey::AlgoECDH; } else { curve = split[0]; *algo = Subkey::AlgoEDDSA; } return; } if (lowered.startsWith(QLatin1String("cv25519")) || lowered.startsWith(QLatin1String("nist")) || lowered.startsWith(QLatin1String("brainpool")) || lowered.startsWith(QLatin1String("secp"))) { curve = split[0]; *algo = isEncrypt ? Subkey::AlgoECDH : Subkey::AlgoECDSA; return; } qCWarning(KLEOPATRA_LOG) << "Failed to parse default_pubkey_algo:" << algoString; } -enum class OnUnlimitedValidity { - ReturnInvalidDate, - ReturnInternalDefault -}; - -QDate defaultExpirationDate(OnUnlimitedValidity onUnlimitedValidity) -{ - QDate expirationDate{}; - - const auto settings = Kleo::Settings{}; - const auto defaultExpirationInDays = settings.validityPeriodInDays(); - if (defaultExpirationInDays > 0) { - expirationDate = QDate::currentDate().addDays(defaultExpirationInDays); - } else if (defaultExpirationInDays < 0 || onUnlimitedValidity == OnUnlimitedValidity::ReturnInternalDefault) { - expirationDate = QDate::currentDate().addYears(2); - } - - return expirationDate; -} - } struct AdvancedSettingsDialog::UI { QTabWidget *tabWidget = nullptr; QRadioButton *rsaRB = nullptr; QComboBox *rsaKeyStrengthCB = nullptr; QCheckBox *rsaSubCB = nullptr; QComboBox *rsaKeyStrengthSubCB = nullptr; QRadioButton *dsaRB = nullptr; QComboBox *dsaKeyStrengthCB = nullptr; QCheckBox *elgCB = nullptr; QComboBox *elgKeyStrengthCB = nullptr; QRadioButton *ecdsaRB = nullptr; QComboBox *ecdsaKeyCurvesCB = nullptr; QCheckBox *ecdhCB = nullptr; QComboBox *ecdhKeyCurvesCB = nullptr; QCheckBox *certificationCB = nullptr; QCheckBox *signingCB = nullptr; QCheckBox *encryptionCB = nullptr; QCheckBox *authenticationCB = nullptr; QCheckBox *expiryCB = nullptr; KDateComboBox *expiryDE = nullptr; ScrollArea *personalTab = nullptr; QGroupBox *uidGB = nullptr; Kleo::NewCertificateUi::ListWidget *uidLW = nullptr; QGroupBox *emailGB = nullptr; Kleo::NewCertificateUi::ListWidget *emailLW = nullptr; QGroupBox *dnsGB = nullptr; Kleo::NewCertificateUi::ListWidget *dnsLW = nullptr; QGroupBox *uriGB = nullptr; Kleo::NewCertificateUi::ListWidget *uriLW = nullptr; QDialogButtonBox *buttonBox = nullptr; UI(QDialog *parent) { parent->setWindowTitle(i18nc("@title:window", "Advanced Settings")); auto mainLayout = new QVBoxLayout{parent}; tabWidget = new QTabWidget{parent}; { auto technicalTab = new ScrollArea{tabWidget}; technicalTab->setFocusPolicy(Qt::NoFocus); technicalTab->setFrameStyle(QFrame::NoFrame); technicalTab->setBackgroundRole(parent->backgroundRole()); technicalTab->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); technicalTab->setSizeAdjustPolicy(QScrollArea::AdjustToContents); auto tabLayout = qobject_cast(technicalTab->widget()->layout()); { auto groupBox = new QGroupBox{i18nc("@title:group", "Key Material"), technicalTab}; auto groupBoxGrid = new QGridLayout{groupBox}; int row = 0; rsaRB = new QRadioButton{i18nc("@option:radio", "RSA"), groupBox}; rsaRB->setChecked(false); groupBoxGrid->addWidget(rsaRB, row, 0, 1, 2); rsaKeyStrengthCB = new QComboBox{groupBox}; rsaKeyStrengthCB->setEnabled(false); groupBoxGrid->addWidget(rsaKeyStrengthCB, row, 2, 1, 1); row++; auto subKeyIndentation = new QSpacerItem(13, 13, QSizePolicy::Fixed, QSizePolicy::Minimum); groupBoxGrid->addItem(subKeyIndentation, row, 0, 1, 1); rsaSubCB = new QCheckBox{i18nc("@option:check", "+ RSA"), groupBox}; rsaSubCB->setEnabled(true); groupBoxGrid->addWidget(rsaSubCB, row, 1, 1, 1); rsaKeyStrengthSubCB = new QComboBox{groupBox}; rsaKeyStrengthSubCB->setEnabled(false); groupBoxGrid->addWidget(rsaKeyStrengthSubCB, row, 2, 1, 1); row++; dsaRB = new QRadioButton{i18nc("@option:radio", "DSA"), groupBox}; groupBoxGrid->addWidget(dsaRB, row, 0, 1, 2); dsaKeyStrengthCB = new QComboBox{groupBox}; dsaKeyStrengthCB->setEnabled(false); groupBoxGrid->addWidget(dsaKeyStrengthCB, row, 2, 1, 1); row++; elgCB = new QCheckBox{i18nc("@option:check", "+ Elgamal"), groupBox}; elgCB->setToolTip(i18nc("@info:tooltip", "This subkey is required for encryption.")); elgCB->setEnabled(true); groupBoxGrid->addWidget(elgCB, row, 1, 1, 1); elgKeyStrengthCB = new QComboBox{groupBox}; elgKeyStrengthCB->setEnabled(false); groupBoxGrid->addWidget(elgKeyStrengthCB, row, 2, 1, 1); row++; ecdsaRB = new QRadioButton{i18nc("@option:radio", "ECDSA"), groupBox}; groupBoxGrid->addWidget(ecdsaRB, row, 0, 1, 2); ecdsaKeyCurvesCB = new QComboBox{groupBox}; ecdsaKeyCurvesCB->setEnabled(false); groupBoxGrid->addWidget(ecdsaKeyCurvesCB, row, 2, 1, 1); row++; ecdhCB = new QCheckBox{i18nc("@option:check", "+ ECDH"), groupBox}; ecdhCB->setToolTip(i18nc("@info:tooltip", "This subkey is required for encryption.")); ecdhCB->setEnabled(true); groupBoxGrid->addWidget(ecdhCB, row, 1, 1, 1); ecdhKeyCurvesCB = new QComboBox{groupBox}; ecdhKeyCurvesCB->setEnabled(false); groupBoxGrid->addWidget(ecdhKeyCurvesCB, row, 2, 1, 1); groupBoxGrid->setColumnStretch(3, 1); tabLayout->addWidget(groupBox); } { auto groupBox = new QGroupBox{i18nc("@title:group", "Certificate Usage"), technicalTab}; auto groupBoxGrid = new QGridLayout{groupBox}; int row = 0; signingCB = new QCheckBox{i18nc("@option:check", "Signing"), groupBox}; signingCB->setChecked(true); groupBoxGrid->addWidget(signingCB, row, 0, 1, 1); certificationCB = new QCheckBox{i18nc("@option:check", "Certification"), groupBox}; groupBoxGrid->addWidget(certificationCB, row, 1, 1, 1); row++; encryptionCB = new QCheckBox{i18nc("@option:check", "Encryption"), groupBox}; encryptionCB->setChecked(true); groupBoxGrid->addWidget(encryptionCB, row, 0, 1, 1); authenticationCB = new QCheckBox{i18nc("@option:check", "Authentication"), groupBox}; groupBoxGrid->addWidget(authenticationCB, row, 1, 1, 1); row++; { auto hbox = new QHBoxLayout; expiryCB = new QCheckBox{i18nc("@option:check", "Valid until:"), groupBox}; hbox->addWidget(expiryCB); expiryDE = new KDateComboBox(groupBox); hbox->addWidget(expiryDE, 1); groupBoxGrid->addLayout(hbox, row, 0, 1, 2); } tabLayout->addWidget(groupBox); } tabLayout->addStretch(1); tabWidget->addTab(technicalTab, i18nc("@title:tab", "Technical Details")); } { personalTab = new ScrollArea{tabWidget}; personalTab->setFocusPolicy(Qt::NoFocus); personalTab->setFrameStyle(QFrame::NoFrame); personalTab->setBackgroundRole(parent->backgroundRole()); personalTab->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); personalTab->setSizeAdjustPolicy(QScrollArea::AdjustToContents); auto scrollAreaLayout = qobject_cast(personalTab->widget()->layout()); auto tabGrid = new QGridLayout; uidGB = new QGroupBox{i18nc("@title:group", "Additional User IDs"), personalTab}; { auto layout = new QVBoxLayout{uidGB}; uidLW = new Kleo::NewCertificateUi::ListWidget{uidGB}; layout->addWidget(uidLW); } tabGrid->addWidget(uidGB, 0, 0, 1, 2); emailGB = new QGroupBox{i18nc("@title:group", "EMail Addresses"), personalTab}; { auto layout = new QVBoxLayout{emailGB}; emailLW = new Kleo::NewCertificateUi::ListWidget{emailGB}; layout->addWidget(emailLW); } tabGrid->addWidget(emailGB, 2, 0, 2, 1); dnsGB = new QGroupBox{i18nc("@title:group", "DNS Names"), personalTab}; { auto layout = new QVBoxLayout{dnsGB}; dnsLW = new Kleo::NewCertificateUi::ListWidget{dnsGB}; layout->addWidget(dnsLW); } tabGrid->addWidget(dnsGB, 2, 1, 1, 1); uriGB = new QGroupBox{i18nc("@title:group", "URIs"), personalTab}; { auto layout = new QVBoxLayout{uriGB}; uriLW = new Kleo::NewCertificateUi::ListWidget{uriGB}; layout->addWidget(uriLW); } tabGrid->addWidget(uriGB, 3, 1, 1, 1); scrollAreaLayout->addLayout(tabGrid); tabWidget->addTab(personalTab, i18nc("@title:tab", "Personal Details")); } mainLayout->addWidget(tabWidget); buttonBox = new QDialogButtonBox{parent}; buttonBox->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Ok); mainLayout->addWidget(buttonBox); } }; AdvancedSettingsDialog::AdvancedSettingsDialog(QWidget *parent) : QDialog{parent} , ui{new UI{this}} , mECCSupported{engineIsVersion(2, 1, 0)} , mEdDSASupported{engineIsVersion(2, 1, 15)} { qRegisterMetaType("Subkey::PubkeyAlgo"); const auto settings = Kleo::Settings{}; { const auto minimumExpiry = std::max(0, settings.validityPeriodInDaysMin()); ui->expiryDE->setMinimumDate(QDate::currentDate().addDays(minimumExpiry)); } { const auto maximumExpiry = settings.validityPeriodInDaysMax(); if (maximumExpiry >= 0) { ui->expiryDE->setMaximumDate(std::max(ui->expiryDE->minimumDate(), QDate::currentDate().addDays(maximumExpiry))); } } if (unlimitedValidityIsAllowed()) { ui->expiryDE->setEnabled(ui->expiryCB->isChecked()); } else { ui->expiryCB->setEnabled(false); ui->expiryCB->setChecked(true); if (ui->expiryDE->maximumDate() == ui->expiryDE->minimumDate()) { // validity period is a fixed number of days ui->expiryDE->setEnabled(false); } } ui->expiryDE->setToolTip(validityPeriodHint(ui->expiryDE->minimumDate(), ui->expiryDE->maximumDate())); ui->emailLW->setDefaultValue(i18n("new email")); ui->dnsLW->setDefaultValue(i18n("new dns name")); ui->uriLW->setDefaultValue(i18n("new uri")); fillKeySizeComboBoxen(); connect(ui->rsaRB, &QAbstractButton::toggled, ui->rsaKeyStrengthCB, &QWidget::setEnabled); connect(ui->rsaRB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->rsaSubCB, &QAbstractButton::toggled, ui->rsaKeyStrengthSubCB, &QWidget::setEnabled); connect(ui->rsaSubCB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->dsaRB, &QAbstractButton::toggled, ui->dsaKeyStrengthCB, &QWidget::setEnabled); connect(ui->dsaRB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->elgCB, &QAbstractButton::toggled, ui->elgKeyStrengthCB, &QWidget::setEnabled); connect(ui->elgCB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->ecdsaRB, &QAbstractButton::toggled, ui->ecdsaKeyCurvesCB, &QWidget::setEnabled); connect(ui->ecdsaRB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->ecdhCB, &QAbstractButton::toggled, ui->ecdhKeyCurvesCB, &QWidget::setEnabled); connect(ui->ecdhCB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotKeyMaterialSelectionChanged); connect(ui->signingCB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotSigningAllowedToggled); connect(ui->encryptionCB, &QAbstractButton::toggled, this, &AdvancedSettingsDialog::slotEncryptionAllowedToggled); connect(ui->expiryCB, &QAbstractButton::toggled, this, [this](bool checked) { ui->expiryDE->setEnabled(checked); if (checked && !ui->expiryDE->isValid()) { - setExpiryDate(defaultExpirationDate(OnUnlimitedValidity::ReturnInternalDefault)); + setExpiryDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } }); connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } AdvancedSettingsDialog::~AdvancedSettingsDialog() = default; QString AdvancedSettingsDialog::dateToString(const QDate &date) const { // workaround for QLocale using "yy" way too often for years // stolen from KDateComboBox const auto dateFormat = (locale().dateFormat(QLocale::ShortFormat) // .replace(QLatin1String{"yy"}, QLatin1String{"yyyy"}) .replace(QLatin1String{"yyyyyyyy"}, QLatin1String{"yyyy"})); return locale().toString(date, dateFormat); } QString AdvancedSettingsDialog::validityPeriodHint(const QDate &minDate, const QDate &maxDate) const { // Note: minDate is always valid const auto today = QDate::currentDate(); if (maxDate.isValid()) { if (maxDate == minDate) { return i18n("The validity period cannot be changed."); } else if (minDate == today) { return i18nc("... between today and .", "The validity period must end between today and %1.", dateToString(maxDate)); } else { return i18nc("... between and .", "The validity period must end between %1 and %2.", dateToString(minDate), dateToString(maxDate)); } } else { if (minDate == today) { return i18n("The validity period must end after today."); } else { return i18nc("... after .", "The validity period must end after %1.", dateToString(minDate)); } } } bool AdvancedSettingsDialog::unlimitedValidityIsAllowed() const { return !ui->expiryDE->maximumDate().isValid(); } void AdvancedSettingsDialog::setProtocol(GpgME::Protocol proto) { if (protocol == proto) { return; } protocol = proto; loadDefaults(); } void AdvancedSettingsDialog::setAdditionalUserIDs(const QStringList &items) { ui->uidLW->setItems(items); } QStringList AdvancedSettingsDialog::additionalUserIDs() const { return ui->uidLW->items(); } void AdvancedSettingsDialog::setAdditionalEMailAddresses(const QStringList &items) { ui->emailLW->setItems(items); } QStringList AdvancedSettingsDialog::additionalEMailAddresses() const { return ui->emailLW->items(); } void AdvancedSettingsDialog::setDnsNames(const QStringList &items) { ui->dnsLW->setItems(items); } QStringList AdvancedSettingsDialog::dnsNames() const { return ui->dnsLW->items(); } void AdvancedSettingsDialog::setUris(const QStringList &items) { ui->uriLW->setItems(items); } QStringList AdvancedSettingsDialog::uris() const { return ui->uriLW->items(); } void AdvancedSettingsDialog::setKeyStrength(unsigned int strength) { set_keysize(ui->rsaKeyStrengthCB, strength); set_keysize(ui->dsaKeyStrengthCB, strength); } unsigned int AdvancedSettingsDialog::keyStrength() const { return ui->dsaRB->isChecked() ? get_keysize(ui->dsaKeyStrengthCB) : ui->rsaRB->isChecked() ? get_keysize(ui->rsaKeyStrengthCB) : 0; } void AdvancedSettingsDialog::setKeyType(Subkey::PubkeyAlgo algo) { QRadioButton *const rb = is_rsa(algo) ? ui->rsaRB : is_dsa(algo) ? ui->dsaRB : is_ecdsa(algo) || is_eddsa(algo) ? ui->ecdsaRB : nullptr; if (rb) { rb->setChecked(true); } } Subkey::PubkeyAlgo AdvancedSettingsDialog::keyType() const { return ui->dsaRB->isChecked() ? Subkey::AlgoDSA : ui->rsaRB->isChecked() ? Subkey::AlgoRSA : ui->ecdsaRB->isChecked() ? ui->ecdsaKeyCurvesCB->currentText() == QLatin1String("ed25519") ? Subkey::AlgoEDDSA : Subkey::AlgoECDSA : Subkey::AlgoUnknown; } void AdvancedSettingsDialog::setKeyCurve(const QString &curve) { set_curve(ui->ecdsaKeyCurvesCB, curve); } QString AdvancedSettingsDialog::keyCurve() const { return get_curve(ui->ecdsaKeyCurvesCB); } void AdvancedSettingsDialog::setSubkeyType(Subkey::PubkeyAlgo algo) { ui->elgCB->setChecked(is_elg(algo)); ui->rsaSubCB->setChecked(is_rsa(algo)); ui->ecdhCB->setChecked(is_ecdh(algo)); } Subkey::PubkeyAlgo AdvancedSettingsDialog::subkeyType() const { if (ui->elgCB->isChecked()) { return Subkey::AlgoELG_E; } else if (ui->rsaSubCB->isChecked()) { return Subkey::AlgoRSA; } else if (ui->ecdhCB->isChecked()) { return Subkey::AlgoECDH; } return Subkey::AlgoUnknown; } void AdvancedSettingsDialog::setSubkeyCurve(const QString &curve) { set_curve(ui->ecdhKeyCurvesCB, curve); } QString AdvancedSettingsDialog::subkeyCurve() const { return get_curve(ui->ecdhKeyCurvesCB); } void AdvancedSettingsDialog::setSubkeyStrength(unsigned int strength) { if (subkeyType() == Subkey::AlgoRSA) { set_keysize(ui->rsaKeyStrengthSubCB, strength); } else { set_keysize(ui->elgKeyStrengthCB, strength); } } unsigned int AdvancedSettingsDialog::subkeyStrength() const { if (subkeyType() == Subkey::AlgoRSA) { return get_keysize(ui->rsaKeyStrengthSubCB); } return get_keysize(ui->elgKeyStrengthCB); } void AdvancedSettingsDialog::setSigningAllowed(bool on) { ui->signingCB->setChecked(on); } bool AdvancedSettingsDialog::signingAllowed() const { return ui->signingCB->isChecked(); } void AdvancedSettingsDialog::setEncryptionAllowed(bool on) { ui->encryptionCB->setChecked(on); } bool AdvancedSettingsDialog::encryptionAllowed() const { return ui->encryptionCB->isChecked(); } void AdvancedSettingsDialog::setCertificationAllowed(bool on) { ui->certificationCB->setChecked(on); } bool AdvancedSettingsDialog::certificationAllowed() const { return ui->certificationCB->isChecked(); } void AdvancedSettingsDialog::setAuthenticationAllowed(bool on) { ui->authenticationCB->setChecked(on); } bool AdvancedSettingsDialog::authenticationAllowed() const { return ui->authenticationCB->isChecked(); } QDate AdvancedSettingsDialog::forceDateIntoAllowedRange(QDate date) const { const auto minDate = ui->expiryDE->minimumDate(); if (minDate.isValid() && date < minDate) { date = minDate; } const auto maxDate = ui->expiryDE->maximumDate(); if (maxDate.isValid() && date > maxDate) { date = maxDate; } return date; } void AdvancedSettingsDialog::setExpiryDate(QDate date) { if (date.isValid()) { ui->expiryDE->setDate(forceDateIntoAllowedRange(date)); } else { // check if unlimited validity is allowed if (unlimitedValidityIsAllowed()) { ui->expiryDE->setDate(date); } } if (ui->expiryCB->isEnabled()) { ui->expiryCB->setChecked(ui->expiryDE->isValid()); } } QDate AdvancedSettingsDialog::expiryDate() const { return ui->expiryCB->isChecked() ? forceDateIntoAllowedRange(ui->expiryDE->date()) : QDate(); } void AdvancedSettingsDialog::slotKeyMaterialSelectionChanged() { const unsigned int algo = keyType(); const unsigned int sk_algo = subkeyType(); if (protocol == OpenPGP) { // first update the enabled state, but only if key type is not forced if (!keyTypeImmutable) { ui->elgCB->setEnabled(is_dsa(algo)); ui->rsaSubCB->setEnabled(is_rsa(algo)); ui->ecdhCB->setEnabled(is_ecdsa(algo) || is_eddsa(algo)); if (is_rsa(algo)) { ui->encryptionCB->setEnabled(true); ui->signingCB->setEnabled(true); ui->authenticationCB->setEnabled(true); if (is_rsa(sk_algo)) { ui->encryptionCB->setEnabled(false); } else { ui->encryptionCB->setEnabled(true); } } else if (is_dsa(algo)) { ui->encryptionCB->setEnabled(false); } else if (is_ecdsa(algo) || is_eddsa(algo)) { ui->signingCB->setEnabled(true); ui->authenticationCB->setEnabled(true); ui->encryptionCB->setEnabled(false); } } // then update the checked state if (sender() == ui->dsaRB || sender() == ui->rsaRB || sender() == ui->ecdsaRB) { ui->elgCB->setChecked(is_dsa(algo)); ui->ecdhCB->setChecked(is_ecdsa(algo) || is_eddsa(algo)); ui->rsaSubCB->setChecked(is_rsa(algo)); } if (is_rsa(algo)) { ui->encryptionCB->setChecked(true); ui->signingCB->setChecked(true); if (is_rsa(sk_algo)) { ui->encryptionCB->setChecked(true); } } else if (is_dsa(algo)) { if (is_elg(sk_algo)) { ui->encryptionCB->setChecked(true); } else { ui->encryptionCB->setChecked(false); } } else if (is_ecdsa(algo) || is_eddsa(algo)) { ui->signingCB->setChecked(true); ui->encryptionCB->setChecked(is_ecdh(sk_algo)); } } else { //assert( is_rsa( keyType() ) ); // it can happen through misconfiguration by the admin that no key type is selectable at all } } void AdvancedSettingsDialog::slotSigningAllowedToggled(bool on) { if (!on && protocol == CMS && !encryptionAllowed()) { setEncryptionAllowed(true); } } void AdvancedSettingsDialog::slotEncryptionAllowedToggled(bool on) { if (!on && protocol == CMS && !signingAllowed()) { setSigningAllowed(true); } } static void fill_combobox(QComboBox &cb, const QList &sizes, const QStringList &labels) { cb.clear(); for (int i = 0, end = sizes.size(); i != end; ++i) { const int size = std::abs(sizes[i]); /* As we respect the defaults configurable in GnuPG, and we also have configurable * defaults in Kleopatra its difficult to print out "default" here. To avoid confusion * about that its better not to show any default indication. */ cb.addItem(i < labels.size() && !labels[i].trimmed().isEmpty() ? i18ncp("%2: some admin-supplied text, %1: key size in bits", "%2 (1 bit)", "%2 (%1 bits)", size, labels[i].trimmed()) : i18ncp("%1: key size in bits", "1 bit", "%1 bits", size), size); if (sizes[i] < 0) { cb.setCurrentIndex(cb.count() - 1); } } } void AdvancedSettingsDialog::fillKeySizeComboBoxen() { const KConfigGroup config(KSharedConfig::openConfig(), "CertificateCreationWizard"); QList rsaKeySizes = config.readEntry(RSA_KEYSIZES_ENTRY, QList() << 2048 << -3072 << 4096); if (DeVSCompliance::isActive()) { rsaKeySizes = config.readEntry(RSA_KEYSIZES_ENTRY, QList() << -3072 << 4096); } const QList dsaKeySizes = config.readEntry(DSA_KEYSIZES_ENTRY, QList() << -2048); const QList elgKeySizes = config.readEntry(ELG_KEYSIZES_ENTRY, QList() << -2048 << 3072 << 4096); const QStringList rsaKeySizeLabels = config.readEntry(RSA_KEYSIZE_LABELS_ENTRY, QStringList()); const QStringList dsaKeySizeLabels = config.readEntry(DSA_KEYSIZE_LABELS_ENTRY, QStringList()); const QStringList elgKeySizeLabels = config.readEntry(ELG_KEYSIZE_LABELS_ENTRY, QStringList()); fill_combobox(*ui->rsaKeyStrengthCB, rsaKeySizes, rsaKeySizeLabels); fill_combobox(*ui->rsaKeyStrengthSubCB, rsaKeySizes, rsaKeySizeLabels); fill_combobox(*ui->dsaKeyStrengthCB, dsaKeySizes, dsaKeySizeLabels); fill_combobox(*ui->elgKeyStrengthCB, elgKeySizes, elgKeySizeLabels); if (mEdDSASupported) { // If supported we recommend cv25519 ui->ecdsaKeyCurvesCB->addItem(QStringLiteral("ed25519")); ui->ecdhKeyCurvesCB->addItem(QStringLiteral("cv25519")); } ui->ecdhKeyCurvesCB->addItems(curveNames); ui->ecdsaKeyCurvesCB->addItems(curveNames); } // Try to load the default key type from GnuPG void AdvancedSettingsDialog::loadDefaultGnuPGKeyType() { const auto conf = QGpgME::cryptoConfig(); if (!conf) { qCWarning(KLEOPATRA_LOG) << "Failed to obtain cryptoConfig."; return; } const auto entry = getCryptoConfigEntry(conf, protocol == CMS ? "gpgsm" : "gpg", "default_pubkey_algo"); if (!entry) { qCDebug(KLEOPATRA_LOG) << "GnuPG does not have default key type. Fallback to RSA"; setKeyType(Subkey::AlgoRSA); setSubkeyType(Subkey::AlgoRSA); return; } qCDebug(KLEOPATRA_LOG) << "Have default key type: " << entry->stringValue(); // Format is [/usage]+[/usage] const auto split = entry->stringValue().split(QLatin1Char('+')); int size = 0; Subkey::PubkeyAlgo algo = Subkey::AlgoUnknown; QString curve; parseAlgoString(split[0], &size, &algo, curve); if (algo == Subkey::AlgoUnknown) { setSubkeyType(Subkey::AlgoRSA); return; } setKeyType(algo); if (is_rsa(algo) || is_elg(algo) || is_dsa(algo)) { setKeyStrength(size); } else { setKeyCurve(curve); } { auto algoString = (split.size() == 2) ? split[1] : split[0]; // If it has no usage we assume encrypt subkey if (!algoString.contains(QLatin1Char('/'))) { algoString += QStringLiteral("/enc"); } parseAlgoString(algoString, &size, &algo, curve); if (algo == Subkey::AlgoUnknown) { setSubkeyType(Subkey::AlgoRSA); return; } setSubkeyType(algo); if (is_rsa(algo) || is_elg(algo)) { setSubkeyStrength(size); } else { setSubkeyCurve(curve); } } } void AdvancedSettingsDialog::loadDefaultKeyType() { if (protocol != CMS && protocol != OpenPGP) { return; } const KConfigGroup config(KSharedConfig::openConfig(), "CertificateCreationWizard"); const QString entry = protocol == CMS ? QLatin1String(CMS_KEY_TYPE_ENTRY) : QLatin1String(PGP_KEY_TYPE_ENTRY); const QString keyType = config.readEntry(entry).trimmed().toUpper(); if (protocol == OpenPGP && keyType == QLatin1String("DSA")) { setKeyType(Subkey::AlgoDSA); setSubkeyType(Subkey::AlgoUnknown); } else if (protocol == OpenPGP && keyType == QLatin1String("DSA+ELG")) { setKeyType(Subkey::AlgoDSA); setSubkeyType(Subkey::AlgoELG_E); } else if (keyType.isEmpty() && engineIsVersion(2, 1, 17)) { loadDefaultGnuPGKeyType(); } else { if (!keyType.isEmpty() && keyType != QLatin1String("RSA")) qCWarning(KLEOPATRA_LOG) << "invalid value \"" << qPrintable(keyType) << "\" for entry \"[CertificateCreationWizard]" << qPrintable(entry) << "\""; setKeyType(Subkey::AlgoRSA); setSubkeyType(Subkey::AlgoRSA); } keyTypeImmutable = config.isEntryImmutable(entry); } void AdvancedSettingsDialog::loadDefaultExpiration() { if (protocol != OpenPGP) { return; } if (unlimitedValidityIsAllowed()) { - setExpiryDate(defaultExpirationDate(OnUnlimitedValidity::ReturnInvalidDate)); + setExpiryDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::NoExpiration)); } else { - setExpiryDate(defaultExpirationDate(OnUnlimitedValidity::ReturnInternalDefault)); + setExpiryDate(defaultExpirationDate(ExpirationOnUnlimitedValidity::InternalDefaultExpiration)); } } void AdvancedSettingsDialog::loadDefaults() { loadDefaultKeyType(); loadDefaultExpiration(); updateWidgetVisibility(); } void AdvancedSettingsDialog::updateWidgetVisibility() { // Personal Details Page if (protocol == OpenPGP) { // ### hide until multi-uid is implemented if (ui->tabWidget->indexOf(ui->personalTab) != -1) { ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->personalTab)); } } else { if (ui->tabWidget->indexOf(ui->personalTab) == -1) { ui->tabWidget->addTab(ui->personalTab, i18nc("@title:tab", "Personal Details")); } } ui->uidGB->setVisible(protocol == OpenPGP); ui->uidGB->setEnabled(false); ui->uidGB->setToolTip(i18nc("@info:tooltip", "Adding more than one user ID is not yet implemented.")); ui->emailGB->setVisible(protocol == CMS); ui->dnsGB->setVisible(protocol == CMS); ui->uriGB->setVisible(protocol == CMS); // Technical Details Page ui->ecdhCB->setVisible(mECCSupported); ui->ecdhKeyCurvesCB->setVisible(mECCSupported); ui->ecdsaKeyCurvesCB->setVisible(mECCSupported); ui->ecdsaRB->setVisible(mECCSupported); if (mEdDSASupported) { // We use the same radio button for EdDSA as we use for // ECDSA GnuPG does the same and this is really super technical // land. ui->ecdsaRB->setText(QStringLiteral("ECDSA/EdDSA")); } const bool deVsHack = DeVSCompliance::isActive(); if (deVsHack) { // GnuPG Provides no API to query which keys are compliant for // a mode. If we request a different one it will error out so // we have to remove the options. // // Does anyone want to use NIST anyway? int i; while ((i = ui->ecdsaKeyCurvesCB->findText(QStringLiteral("NIST"), Qt::MatchStartsWith)) != -1 || (i = ui->ecdsaKeyCurvesCB->findText(QStringLiteral("25519"), Qt::MatchEndsWith)) != -1) { ui->ecdsaKeyCurvesCB->removeItem(i); } while ((i = ui->ecdhKeyCurvesCB->findText(QStringLiteral("NIST"), Qt::MatchStartsWith)) != -1 || (i = ui->ecdhKeyCurvesCB->findText(QStringLiteral("25519"), Qt::MatchEndsWith)) != -1) { ui->ecdhKeyCurvesCB->removeItem(i); } } ui->certificationCB->setVisible(protocol == OpenPGP); // gpgsm limitation? ui->authenticationCB->setVisible(protocol == OpenPGP); if (keyTypeImmutable) { ui->rsaRB->setEnabled(false); ui->rsaSubCB->setEnabled(false); ui->dsaRB->setEnabled(false); ui->elgCB->setEnabled(false); ui->ecdsaRB->setEnabled(false); ui->ecdhCB->setEnabled(false); // force usage if key type is forced ui->certificationCB->setEnabled(false); ui->signingCB->setEnabled(false); ui->encryptionCB->setEnabled(false); ui->authenticationCB->setEnabled(false); } else { ui->rsaRB->setEnabled(true); ui->rsaSubCB->setEnabled(protocol == OpenPGP); ui->dsaRB->setEnabled(protocol == OpenPGP && !deVsHack); ui->elgCB->setEnabled(protocol == OpenPGP && !deVsHack); ui->ecdsaRB->setEnabled(protocol == OpenPGP); ui->ecdhCB->setEnabled(protocol == OpenPGP); if (protocol == OpenPGP) { // OpenPGP keys must have certify capability ui->certificationCB->setEnabled(false); } if (protocol == CMS) { ui->encryptionCB->setEnabled(true); ui->rsaKeyStrengthSubCB->setEnabled(false); } } if (protocol == OpenPGP) { // OpenPGP keys must have certify capability ui->certificationCB->setChecked(true); } if (protocol == CMS) { ui->rsaSubCB->setChecked(false); } ui->expiryDE->setVisible(protocol == OpenPGP); ui->expiryCB->setVisible(protocol == OpenPGP); slotKeyMaterialSelectionChanged(); } void AdvancedSettingsDialog::setInitialFocus() { // first try the key type radio buttons if (focusFirstCheckedButton({ui->rsaRB, ui->dsaRB, ui->ecdsaRB})) { return; } // then try the usage check boxes and the expiration check box if (focusFirstEnabledButton({ui->signingCB, ui->certificationCB, ui->encryptionCB, ui->authenticationCB, ui->expiryCB})) { return; } // finally, focus the OK button ui->buttonBox->button(QDialogButtonBox::Ok)->setFocus(); } void AdvancedSettingsDialog::showEvent(QShowEvent *event) { if (isFirstShowEvent) { setInitialFocus(); isFirstShowEvent = false; } QDialog::showEvent(event); } diff --git a/src/utils/keys.cpp b/src/utils/keys.cpp index 559d1b2f0..5f44f1af1 100644 --- a/src/utils/keys.cpp +++ b/src/utils/keys.cpp @@ -1,162 +1,179 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/keys.cpp This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #include "keys.h" #include +#include #include #include +#include // needed for GPGME_VERSION_NUMBER #include #include #include namespace { bool isLastValidUserID(const GpgME::UserID &userId) { if (Kleo::isRevokedOrExpired(userId)) { return false; } const auto userIds = userId.parent().userIDs(); const int numberOfValidUserIds = std::count_if(std::begin(userIds), std::end(userIds), [](const auto &u) { return !Kleo::isRevokedOrExpired(u); }); return numberOfValidUserIds == 1; } bool hasValidUserID(const GpgME::Key &key) { return Kleo::any_of(key.userIDs(), [](const auto &u) { return !Kleo::isRevokedOrExpired(u); }); } } bool Kleo::isSelfSignature(const GpgME::UserID::Signature &signature) { return !qstrcmp(signature.parent().parent().keyID(), signature.signerKeyID()); } bool Kleo::isRevokedOrExpired(const GpgME::UserID &userId) { if (userId.isRevoked() || userId.parent().isExpired()) { return true; } const auto sigs = userId.signatures(); std::vector selfSigs; std::copy_if(std::begin(sigs), std::end(sigs), std::back_inserter(selfSigs), &Kleo::isSelfSignature); std::sort(std::begin(selfSigs), std::end(selfSigs)); // check the most recent signature const auto sig = !selfSigs.empty() ? selfSigs.back() : GpgME::UserID::Signature{}; return !sig.isNull() && (sig.isRevokation() || sig.isExpired()); } bool Kleo::canCreateCertifications(const GpgME::Key &key) { return key.canCertify() && canBeUsedForSecretKeyOperations(key); } bool Kleo::canBeCertified(const GpgME::Key &key) { return key.protocol() == GpgME::OpenPGP // && !key.isBad() // && hasValidUserID(key); } bool Kleo::canBeUsedForSecretKeyOperations(const GpgME::Key &key) { #if GPGME_VERSION_NUMBER >= 0x011102 // 1.17.2 // we need to check the primary subkey because Key::hasSecret() is also true if just the secret key stub of an offline key is available return key.subkey(0).isSecret(); #else // older versions of GpgME did not always set the secret flag for card keys return key.subkey(0).isSecret() || key.subkey(0).isCardKey(); #endif } bool Kleo::canRevokeUserID(const GpgME::UserID &userId) { return (!userId.isNull() // && userId.parent().protocol() == GpgME::OpenPGP && !isLastValidUserID(userId)); } bool Kleo::isSecretKeyStoredInKeyRing(const GpgME::Key &key) { return key.subkey(0).isSecret() && !key.subkey(0).isCardKey(); } bool Kleo::userHasCertificationKey() { const auto secretKeys = KeyCache::instance()->secretKeys(); return Kleo::any_of(secretKeys, [](const auto &k) { return (k.protocol() == GpgME::OpenPGP) && canCreateCertifications(k); }); } Kleo::CertificationRevocationFeasibility Kleo::userCanRevokeCertification(const GpgME::UserID::Signature &certification) { const auto certificationKey = KeyCache::instance()->findByKeyIDOrFingerprint(certification.signerKeyID()); const bool isSelfSignature = qstrcmp(certification.parent().parent().keyID(), certification.signerKeyID()) == 0; if (!certificationKey.hasSecret()) { return CertificationNotMadeWithOwnKey; } else if (isSelfSignature) { return CertificationIsSelfSignature; } else if (certification.isRevokation()) { return CertificationIsRevocation; } else if (certification.isExpired()) { return CertificationIsExpired; } else if (certification.isInvalid()) { return CertificationIsInvalid; } else if (!canCreateCertifications(certificationKey)) { return CertificationKeyNotAvailable; } return CertificationCanBeRevoked; } bool Kleo::userCanRevokeCertifications(const GpgME::UserID &userId) { if (userId.numSignatures() == 0) { qCWarning(KLEOPATRA_LOG) << __func__ << "- Error: Signatures of user ID" << QString::fromUtf8(userId.id()) << "not available"; } return Kleo::any_of(userId.signatures(), [](const auto &certification) { return userCanRevokeCertification(certification) == CertificationCanBeRevoked; }); } bool Kleo::userIDBelongsToKey(const GpgME::UserID &userID, const GpgME::Key &key) { return !qstricmp(userID.parent().primaryFingerprint(), key.primaryFingerprint()); } static time_t creationDate(const GpgME::UserID &uid) { // returns the date of the first self-signature for (unsigned int i = 0, numSignatures = uid.numSignatures(); i < numSignatures; ++i) { const auto sig = uid.signature(i); if (Kleo::isSelfSignature(sig)) { return sig.creationTime(); } } return 0; } bool Kleo::userIDsAreEqual(const GpgME::UserID &lhs, const GpgME::UserID &rhs) { return (qstrcmp(lhs.parent().primaryFingerprint(), rhs.parent().primaryFingerprint()) == 0 && qstrcmp(lhs.id(), rhs.id()) == 0 && creationDate(lhs) == creationDate(rhs)); } + +QDate Kleo::defaultExpirationDate(Kleo::ExpirationOnUnlimitedValidity onUnlimitedValidity) +{ + QDate expirationDate; + + const auto settings = Kleo::Settings{}; + const auto defaultExpirationInDays = settings.validityPeriodInDays(); + if (defaultExpirationInDays > 0) { + expirationDate = QDate::currentDate().addDays(defaultExpirationInDays); + } else if (defaultExpirationInDays < 0 || onUnlimitedValidity == ExpirationOnUnlimitedValidity::InternalDefaultExpiration) { + expirationDate = QDate::currentDate().addYears(2); + } + + return expirationDate; +} diff --git a/src/utils/keys.h b/src/utils/keys.h index 3d74664e2..9d2102bbe 100644 --- a/src/utils/keys.h +++ b/src/utils/keys.h @@ -1,122 +1,136 @@ /* -*- mode: c++; c-basic-offset:4 -*- utils/keys.h This file is part of Kleopatra, the KDE keymanager SPDX-FileCopyrightText: 2022 g10 Code GmbH SPDX-FileContributor: Ingo Klöcker SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once #include +class QDate; + namespace Kleo { struct CertificatePair { GpgME::Key openpgp; GpgME::Key cms; }; /** Returns true if \p signature is a self-signature. */ bool isSelfSignature(const GpgME::UserID::Signature &signature); /** * Returns true if the most recent self-signature of \p userId is a revocation * signature or if it has expired. */ bool isRevokedOrExpired(const GpgME::UserID &userId); /** * Returns true if \p key can be used to certify user IDs, i.e. if the key * has the required capability and if the secret key of the (primary) * certification subkey is available in the keyring or on a smart card. */ bool canCreateCertifications(const GpgME::Key &key); /** * Returns true if the key \p key can be certified, i.e. it is an OpenPGP key * which is neither revoked nor expired and which has at least one user ID * that is neither revoked nor expired. */ bool canBeCertified(const GpgME::Key &key); /** * Returns true if \p key can be used for operations requiring the secret key, * i.e. if the secret key of the primary key pair is available in the keyring * or on a smart card. * * \note Key::hasSecret() also returns true if a secret key stub, e.g. of an * offline key, is available in the keyring. */ bool canBeUsedForSecretKeyOperations(const GpgME::Key &key); /** * Returns true if \p userId can be revoked, i.e. if it isn't the last valid * user ID of an OpenPGP key. */ bool canRevokeUserID(const GpgME::UserID &userId); /** * Returns true if the secret key of the primary key pair of \p key is stored * in the keyring. */ bool isSecretKeyStoredInKeyRing(const GpgME::Key &key); /** * Returns true if any keys suitable for certifying user IDs are available in * the keyring or on a smart card. * * \sa canCreateCertifications */ bool userHasCertificationKey(); enum CertificationRevocationFeasibility { CertificationCanBeRevoked = 0, CertificationNotMadeWithOwnKey, CertificationIsSelfSignature, CertificationIsRevocation, CertificationIsExpired, CertificationIsInvalid, CertificationKeyNotAvailable, }; /** * Checks if the user can revoke the given \p certification. */ CertificationRevocationFeasibility userCanRevokeCertification(const GpgME::UserID::Signature &certification); /** * Returns true if the user can revoke any of the certifications of the \p userId. * * \sa userCanRevokeCertification */ bool userCanRevokeCertifications(const GpgME::UserID &userId); /** * Returns true, if the user ID \p userID belongs to the key \p key. */ bool userIDBelongsToKey(const GpgME::UserID &userID, const GpgME::Key &key); /** * Returns a unary predicate to check if a user ID belongs to the key \p key. */ inline auto userIDBelongsToKey(const GpgME::Key &key) { return [key](const GpgME::UserID &userID) { return userIDBelongsToKey(userID, key); }; } /** * Returns true, if the two user IDs \p lhs and \p rhs are equal. * * Equality means that both user IDs belong to the same key, contain identical * text, and have the same creation date (i.e. the creation date of the first * self-signature is the same). */ bool userIDsAreEqual(const GpgME::UserID &lhs, const GpgME::UserID &rhs); +enum class ExpirationOnUnlimitedValidity { + NoExpiration, + InternalDefaultExpiration, +}; + +/** + * Returns a useful value for the default expiration date based on the current + * date and the configured default validity. If the configured validity is + * unlimited, then the return value depends on \p onUnlimitedValidity. + */ +QDate defaultExpirationDate(ExpirationOnUnlimitedValidity onUnlimitedValidity); + }