File indexing completed on 2024-06-23 05:14:06

0001 /*
0002     dialogs/subkeyswidget.cpp
0003 
0004     This file is part of Kleopatra, the KDE keymanager
0005     SPDX-FileCopyrightText: 2016 Klarälvdalens Datakonsult AB
0006     SPDX-FileCopyrightText: 2017 Bundesamt für Sicherheit in der Informationstechnik
0007     SPDX-FileContributor: Intevation GmbH
0008     SPDX-FileCopyrightText: 2022 g10 Code GmbH
0009     SPDX-FileContributor: Ingo Klöcker <dev@ingo-kloecker.de>
0010 
0011     SPDX-License-Identifier: GPL-2.0-or-later
0012 */
0013 
0014 #include <config-kleopatra.h>
0015 
0016 #include "subkeyswidget.h"
0017 
0018 #include "commands/changeexpirycommand.h"
0019 #include "commands/exportsecretsubkeycommand.h"
0020 #include "commands/importpaperkeycommand.h"
0021 #include "commands/keytocardcommand.h"
0022 #include "exportdialog.h"
0023 
0024 #include <kleopatra_debug.h>
0025 
0026 #include <Libkleo/Formatting>
0027 #include <Libkleo/KeyCache>
0028 #include <Libkleo/KeyHelpers>
0029 #include <Libkleo/TreeWidget>
0030 
0031 #include <KConfigGroup>
0032 #include <KLocalizedString>
0033 #include <KSharedConfig>
0034 
0035 #include <QDialogButtonBox>
0036 #include <QHeaderView>
0037 #include <QLabel>
0038 #include <QMenu>
0039 #include <QPushButton>
0040 #include <QTreeWidgetItem>
0041 #include <QVBoxLayout>
0042 
0043 #include <gpgme++/context.h>
0044 #include <gpgme++/key.h>
0045 
0046 Q_DECLARE_METATYPE(GpgME::Subkey)
0047 
0048 using namespace Kleo;
0049 using namespace Kleo::Commands;
0050 
0051 class SubKeysWidget::Private
0052 {
0053     SubKeysWidget *const q;
0054 
0055 public:
0056     Private(SubKeysWidget *qq)
0057         : q{qq}
0058         , ui{qq}
0059     {
0060         ui.subkeysTree->setContextMenuPolicy(Qt::CustomContextMenu);
0061         connect(ui.subkeysTree, &QAbstractItemView::customContextMenuRequested, q, [this](const QPoint &p) {
0062             tableContextMenuRequested(p);
0063         });
0064         connect(Kleo::KeyCache::instance().get(), &Kleo::KeyCache::keysMayHaveChanged, q, [this]() {
0065             keysMayHaveChanged();
0066         });
0067     }
0068 
0069     void changeValidity(const GpgME::Subkey &subkey);
0070     void exportSSH(const GpgME::Subkey &subkey);
0071     void keyToCard(const GpgME::Subkey &subkey);
0072     void exportSecret(const GpgME::Subkey &subkey);
0073     void importPaperKey();
0074 
0075 private:
0076     void tableContextMenuRequested(const QPoint &p);
0077     void keysMayHaveChanged();
0078 
0079 public:
0080     GpgME::Key key;
0081 
0082 public:
0083     struct UI {
0084         QVBoxLayout *mainLayout;
0085         TreeWidget *subkeysTree;
0086 
0087         QPushButton *changeValidityBtn = nullptr;
0088         QPushButton *exportOpenSSHBtn = nullptr;
0089         QPushButton *restoreBtn = nullptr;
0090         QPushButton *transferToSmartcardBtn = nullptr;
0091         QPushButton *exportSecretBtn = nullptr;
0092 
0093         UI(QWidget *widget)
0094         {
0095             mainLayout = new QVBoxLayout{widget};
0096             mainLayout->setContentsMargins(0, 0, 0, 0);
0097 
0098             auto subkeysTreeLabel = new QLabel{i18nc("@label", "Subkeys:"), widget};
0099             mainLayout->addWidget(subkeysTreeLabel);
0100 
0101             subkeysTree = new TreeWidget{widget};
0102             subkeysTreeLabel->setBuddy(subkeysTree);
0103             subkeysTree->setAccessibleName(i18nc("@label", "Subkeys"));
0104             subkeysTree->setRootIsDecorated(false);
0105             subkeysTree->setHeaderLabels({
0106                 i18nc("@title:column", "ID"),
0107                 i18nc("@title:column", "Type"),
0108                 i18nc("@title:column", "Valid From"),
0109                 i18nc("@title:column", "Valid Until"),
0110                 i18nc("@title:column", "Status"),
0111                 i18nc("@title:column", "Strength"),
0112                 i18nc("@title:column", "Usage"),
0113                 i18nc("@title:column", "Primary"),
0114                 i18nc("@title:column", "Storage"),
0115             });
0116             mainLayout->addWidget(subkeysTree);
0117 
0118             {
0119                 auto buttonRow = new QHBoxLayout;
0120 
0121                 changeValidityBtn = new QPushButton(i18nc("@action:button", "Change validity"), widget);
0122                 buttonRow->addWidget(changeValidityBtn);
0123 
0124                 exportOpenSSHBtn = new QPushButton{i18nc("@action:button", "Export OpenSSH key"), widget};
0125                 buttonRow->addWidget(exportOpenSSHBtn);
0126 
0127                 restoreBtn = new QPushButton(i18nc("@action:button", "Restore printed backup"), widget);
0128                 buttonRow->addWidget(restoreBtn);
0129 
0130                 transferToSmartcardBtn = new QPushButton(i18nc("@action:button", "Transfer to smartcard"), widget);
0131                 buttonRow->addWidget(transferToSmartcardBtn);
0132 
0133                 exportSecretBtn = new QPushButton(i18nc("@action:button", "Export secret subkey"), widget);
0134                 buttonRow->addWidget(exportSecretBtn);
0135 
0136                 buttonRow->addStretch(1);
0137 
0138                 mainLayout->addLayout(buttonRow);
0139             }
0140         }
0141     } ui;
0142 };
0143 
0144 void SubKeysWidget::Private::changeValidity(const GpgME::Subkey &subkey)
0145 {
0146     auto cmd = new ChangeExpiryCommand(subkey.parent());
0147     cmd->setSubkey(subkey);
0148     ui.subkeysTree->setEnabled(false);
0149     connect(cmd, &ChangeExpiryCommand::finished, q, [this]() {
0150         ui.subkeysTree->setEnabled(true);
0151         key.update();
0152         q->setKey(key);
0153     });
0154     cmd->setParentWidget(q);
0155     cmd->start();
0156 }
0157 
0158 void SubKeysWidget::Private::exportSSH(const GpgME::Subkey &subkey)
0159 {
0160     QScopedPointer<ExportDialog> dlg(new ExportDialog(q));
0161     dlg->setKey(subkey, static_cast<unsigned int>(GpgME::Context::ExportSSH));
0162     dlg->exec();
0163 }
0164 
0165 void SubKeysWidget::Private::importPaperKey()
0166 {
0167     auto cmd = new ImportPaperKeyCommand(key);
0168     ui.subkeysTree->setEnabled(false);
0169     connect(cmd, &ImportPaperKeyCommand::finished, q, [this]() {
0170         ui.subkeysTree->setEnabled(true);
0171     });
0172     cmd->setParentWidget(q);
0173     cmd->start();
0174 }
0175 
0176 void SubKeysWidget::Private::keyToCard(const GpgME::Subkey &subkey)
0177 {
0178     auto cmd = new KeyToCardCommand(subkey);
0179     ui.subkeysTree->setEnabled(false);
0180     connect(cmd, &KeyToCardCommand::finished, q, [this]() {
0181         ui.subkeysTree->setEnabled(true);
0182     });
0183     cmd->setParentWidget(q);
0184     cmd->start();
0185 }
0186 
0187 void SubKeysWidget::Private::exportSecret(const GpgME::Subkey &subkey)
0188 {
0189     auto cmd = new ExportSecretSubkeyCommand{{subkey}};
0190     ui.subkeysTree->setEnabled(false);
0191     connect(cmd, &ExportSecretSubkeyCommand::finished, q, [this]() {
0192         ui.subkeysTree->setEnabled(true);
0193     });
0194     cmd->setParentWidget(q);
0195     cmd->start();
0196 }
0197 
0198 void SubKeysWidget::Private::tableContextMenuRequested(const QPoint &p)
0199 {
0200     auto item = ui.subkeysTree->itemAt(p);
0201     if (!item) {
0202         return;
0203     }
0204     const auto subkey = item->data(0, Qt::UserRole).value<GpgME::Subkey>();
0205     const bool isOwnKey = subkey.parent().hasSecret();
0206     const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey();
0207 
0208     auto menu = new QMenu(q);
0209     connect(menu, &QMenu::aboutToHide, menu, &QObject::deleteLater);
0210 
0211     if (isOwnKey) {
0212         auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("change-date-symbolic")), i18n("Change validity"), q, [this, subkey]() {
0213             changeValidity(subkey);
0214         });
0215         action->setEnabled(canBeUsedForSecretKeyOperations(subkey.parent()));
0216     }
0217 
0218     if (subkey.canAuthenticate()) {
0219         menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export OpenSSH key"), q, [this, subkey]() {
0220             exportSSH(subkey);
0221         });
0222     }
0223 
0224     auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-import")), i18n("Restore printed backup"), q, [this, subkey]() {
0225         importPaperKey();
0226     });
0227 
0228     action->setEnabled(!secretSubkeyStoredInKeyRing);
0229 
0230     if (isOwnKey) {
0231         auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("send-to-symbolic")), i18n("Transfer to smartcard"), q, [this, subkey]() {
0232             keyToCard(subkey);
0233         });
0234         action->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty());
0235     }
0236 
0237     const bool isPrimarySubkey = subkey.keyID() == key.keyID();
0238     if (isOwnKey && !isPrimarySubkey) {
0239         auto action = menu->addAction(QIcon::fromTheme(QStringLiteral("view-certificate-export")), i18n("Export secret subkey"), q, [this, subkey]() {
0240             exportSecret(subkey);
0241         });
0242         action->setEnabled(secretSubkeyStoredInKeyRing);
0243     }
0244 
0245     menu->popup(ui.subkeysTree->viewport()->mapToGlobal(p));
0246 }
0247 
0248 void SubKeysWidget::Private::keysMayHaveChanged()
0249 {
0250     qCDebug(KLEOPATRA_LOG) << q << __func__;
0251     const auto updatedKey = Kleo::KeyCache::instance()->findByFingerprint(key.primaryFingerprint());
0252     if (!updatedKey.isNull()) {
0253         q->setKey(updatedKey);
0254     }
0255 }
0256 
0257 SubKeysWidget::SubKeysWidget(QWidget *parent)
0258     : QWidget(parent)
0259     , d(new Private(this))
0260 {
0261     connect(d->ui.subkeysTree, &TreeWidget::currentItemChanged, this, [this] {
0262         const auto currentIndex = d->ui.subkeysTree->currentIndex().row();
0263         const auto &subkey = d->key.subkey(currentIndex);
0264         const bool secretSubkeyStoredInKeyRing = subkey.isSecret() && !subkey.isCardKey();
0265         d->ui.exportOpenSSHBtn->setEnabled(subkey.canAuthenticate());
0266         d->ui.changeValidityBtn->setEnabled(d->key.hasSecret() && canBeUsedForSecretKeyOperations(subkey.parent()));
0267         d->ui.exportSecretBtn->setEnabled(d->key.hasSecret() && subkey.fingerprint() != d->key.primaryFingerprint() && secretSubkeyStoredInKeyRing);
0268         d->ui.restoreBtn->setEnabled(!secretSubkeyStoredInKeyRing);
0269         d->ui.transferToSmartcardBtn->setEnabled(secretSubkeyStoredInKeyRing && !KeyToCardCommand::getSuitableCards(subkey).empty());
0270     });
0271     connect(d->ui.changeValidityBtn, &QPushButton::clicked, this, [this] {
0272         d->changeValidity(d->key.subkey(d->ui.subkeysTree->currentIndex().row()));
0273     });
0274     connect(d->ui.exportOpenSSHBtn, &QPushButton::clicked, this, [this] {
0275         d->exportSSH(d->key.subkey(d->ui.subkeysTree->currentIndex().row()));
0276     });
0277     connect(d->ui.restoreBtn, &QPushButton::clicked, this, [this] {
0278         d->importPaperKey();
0279     });
0280     connect(d->ui.transferToSmartcardBtn, &QPushButton::clicked, this, [this] {
0281         d->keyToCard(d->key.subkey(d->ui.subkeysTree->currentIndex().row()));
0282     });
0283     connect(d->ui.exportSecretBtn, &QPushButton::clicked, this, [this] {
0284         d->exportSecret(d->key.subkey(d->ui.subkeysTree->currentIndex().row()));
0285     });
0286 }
0287 
0288 SubKeysWidget::~SubKeysWidget() = default;
0289 
0290 void SubKeysWidget::setKey(const GpgME::Key &key)
0291 {
0292     if (key.protocol() != GpgME::OpenPGP) {
0293         return;
0294     }
0295     d->key = key;
0296 
0297     const auto currentItem = d->ui.subkeysTree->currentItem();
0298     const QByteArray selectedKeyFingerprint = currentItem ? QByteArray(currentItem->data(0, Qt::UserRole).value<GpgME::Subkey>().fingerprint()) : QByteArray();
0299     d->ui.subkeysTree->clear();
0300 
0301     const auto subkeys = key.subkeys();
0302     for (const auto &subkey : subkeys) {
0303         auto item = new QTreeWidgetItem;
0304         item->setData(0, Qt::DisplayRole, Formatting::prettyID(subkey.keyID()));
0305         item->setData(0, Qt::AccessibleTextRole, Formatting::accessibleHexID(subkey.keyID()));
0306         item->setData(0, Qt::UserRole, QVariant::fromValue(subkey));
0307         item->setData(1, Qt::DisplayRole, Kleo::Formatting::type(subkey));
0308         item->setData(2, Qt::DisplayRole, Kleo::Formatting::creationDateString(subkey));
0309         item->setData(2, Qt::AccessibleTextRole, Formatting::accessibleCreationDate(subkey));
0310         item->setData(3,
0311                       Qt::DisplayRole,
0312                       subkey.neverExpires() ? Kleo::Formatting::expirationDateString(subkey.parent()) : Kleo::Formatting::expirationDateString(subkey));
0313         item->setData(3,
0314                       Qt::AccessibleTextRole,
0315                       subkey.neverExpires() ? Kleo::Formatting::accessibleExpirationDate(subkey.parent()) : Kleo::Formatting::accessibleExpirationDate(subkey));
0316         item->setData(4, Qt::DisplayRole, Kleo::Formatting::validityShort(subkey));
0317         switch (subkey.publicKeyAlgorithm()) {
0318         case GpgME::Subkey::AlgoECDSA:
0319         case GpgME::Subkey::AlgoEDDSA:
0320         case GpgME::Subkey::AlgoECDH:
0321             item->setData(5, Qt::DisplayRole, QString::fromStdString(subkey.algoName()));
0322             break;
0323         default:
0324             item->setData(5, Qt::DisplayRole, QString::number(subkey.length()));
0325         }
0326         item->setData(6, Qt::DisplayRole, Kleo::Formatting::usageString(subkey));
0327         const auto isPrimary = subkey.keyID() == key.keyID();
0328         item->setData(7, Qt::DisplayRole, isPrimary ? QStringLiteral("✓") : QString());
0329         item->setData(7, Qt::AccessibleTextRole, isPrimary ? i18nc("yes, is primary key", "yes") : i18nc("no, is not primary key", "no"));
0330         if (subkey.isCardKey()) {
0331             if (const char *serialNo = subkey.cardSerialNumber()) {
0332                 item->setData(8, Qt::DisplayRole, i18nc("smart card <serial number>", "smart card %1", QString::fromUtf8(serialNo)));
0333             } else {
0334                 item->setData(8, Qt::DisplayRole, i18n("smart card"));
0335             }
0336         } else if (isPrimary && key.hasSecret() && !subkey.isSecret()) {
0337             item->setData(8, Qt::DisplayRole, i18nc("key is 'offline key', i.e. secret key is not stored on this computer", "offline"));
0338         } else if (subkey.isSecret()) {
0339             item->setData(8, Qt::DisplayRole, i18n("on this computer"));
0340         } else {
0341             item->setData(8, Qt::DisplayRole, i18nc("unknown storage location", "unknown"));
0342         }
0343         d->ui.subkeysTree->addTopLevelItem(item);
0344         if (subkey.fingerprint() == selectedKeyFingerprint) {
0345             d->ui.subkeysTree->setCurrentItem(item);
0346         }
0347     }
0348     d->ui.subkeysTree->header()->resizeSections(QHeaderView::ResizeToContents);
0349 
0350     d->ui.changeValidityBtn->setVisible(key.hasSecret());
0351     d->ui.exportSecretBtn->setVisible(key.hasSecret());
0352     d->ui.transferToSmartcardBtn->setVisible(key.hasSecret());
0353 
0354     d->ui.subkeysTree->restoreColumnLayout(QStringLiteral("SubkeysWidget"));
0355     if (!key.hasSecret()) {
0356         // hide information about storage location for keys of other people
0357         d->ui.subkeysTree->hideColumn(8);
0358     }
0359 }
0360 
0361 GpgME::Key SubKeysWidget::key() const
0362 {
0363     return d->key;
0364 }
0365 
0366 SubKeysDialog::SubKeysDialog(QWidget *parent)
0367     : QDialog(parent)
0368 {
0369     setWindowTitle(i18nc("@title:window", "Subkeys Details"));
0370     auto l = new QVBoxLayout(this);
0371     l->addWidget(new SubKeysWidget(this));
0372 
0373     auto bbox = new QDialogButtonBox(this);
0374     auto btn = bbox->addButton(QDialogButtonBox::Close);
0375     connect(btn, &QPushButton::clicked, this, &QDialog::accept);
0376     l->addWidget(bbox);
0377     readConfig();
0378 }
0379 
0380 SubKeysDialog::~SubKeysDialog()
0381 {
0382     writeConfig();
0383 }
0384 
0385 void SubKeysDialog::readConfig()
0386 {
0387     KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("SubKeysDialog"));
0388     const QSize size = dialog.readEntry("Size", QSize(820, 280));
0389     if (size.isValid()) {
0390         resize(size);
0391     }
0392 }
0393 
0394 void SubKeysDialog::writeConfig()
0395 {
0396     KConfigGroup dialog(KSharedConfig::openStateConfig(), QStringLiteral("SubKeysDialog"));
0397     dialog.writeEntry("Size", size());
0398     dialog.sync();
0399 }
0400 
0401 void SubKeysDialog::setKey(const GpgME::Key &key)
0402 {
0403     auto w = findChild<SubKeysWidget *>();
0404     Q_ASSERT(w);
0405     w->setKey(key);
0406 }
0407 
0408 GpgME::Key SubKeysDialog::key() const
0409 {
0410     auto w = findChild<SubKeysWidget *>();
0411     Q_ASSERT(w);
0412     return w->key();
0413 }
0414 
0415 #include "moc_subkeyswidget.cpp"