From e32f615ecd0f280966cba8a14ff085467e365cc2 Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Wed, 21 Oct 2026 10:52:00 +0300 Subject: [PATCH] Lock grouping IB-8985 Signed-off-by: Raul Metsma --- client/CryptoDoc.cpp | 23 +- client/CryptoDoc.h | 5 +- client/dialogs/AddRecipients.cpp | 167 +++++++----- client/dialogs/AddRecipients.h | 6 +- client/dialogs/DecryptionDialog.cpp | 58 +++- client/dialogs/DecryptionDialog.h | 9 +- client/translations/en.ts | 24 +- client/translations/et.ts | 24 +- client/widgets/AddressItem.cpp | 407 +++++++++++++++++++--------- client/widgets/AddressItem.h | 15 +- client/widgets/AddressItem.ui | 94 +++---- client/widgets/ContainerPage.cpp | 56 ++-- client/widgets/ContainerPage.h | 1 + 13 files changed, 574 insertions(+), 315 deletions(-) diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index bb67c7fb6..e26857a75 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -60,7 +60,7 @@ CKey CKey::share(const QString &personalCode) { libcdoc::Lock lock(libcdoc::Lock::SHARE_SERVER); - // The form CDoc2 stores it in, so a new recipient reads the same as an encrypted one + // The form CDoc2 stores it in, so personalCode() reads both the same way lock.setString(libcdoc::Lock::RECIPIENT_ID, "etsi/PNOEE-" + personalCode.toStdString()); return CKey(lock); } @@ -76,6 +76,23 @@ CKey::canDecrypt(const QSslCertificate &cert) const QByteArray::fromRawData((const char*)key.data(), qsizetype(key.size())); } +QString +CKey::personalCode() const +{ + if(!rcpt_cert.isNull()) + return SslCertificate(rcpt_cert).personalCode(); + if(lock.isCDoc1()) { + const std::vector &der = lock.getBytes(libcdoc::Lock::CERT); + return SslCertificate(QByteArray::fromRawData((const char*)der.data(), qsizetype(der.size())), QSsl::Der).personalCode(); + } + std::string id; + if(lock.isPKI()) + id = libcdoc::Lock::parseLabel(lock.label)[std::string(libcdoc::CDoc2::Label::SERIAL_NUMBER)]; + else if(std::string rcpt = lock.getString(libcdoc::Lock::RECIPIENT_ID); rcpt.starts_with("etsi/")) + id = rcpt.substr(5); // Mobile-ID / Smart-ID key share: "etsi/PNOEE-48010010101" + return SslCertificate::personalCode(QString::fromStdString(id)); +} + bool CKey::isSupported() const { @@ -618,10 +635,10 @@ bool CryptoDoc::open(const QString &file) return true; } -void CryptoDoc::removeKey(unsigned int id) +void CryptoDoc::removeKeys(const std::vector &keys) { if(!d->isEncryptedWarning(tr("Failed to remove key"))) - d->keys.erase(d->keys.begin() + id); + std::erase_if(d->keys, [&keys](const CKey &key) { return std::ranges::find(keys, key) != keys.cend(); }); } void CryptoDoc::clearKeys() diff --git a/client/CryptoDoc.h b/client/CryptoDoc.h index 1b52db620..7429f7074 100644 --- a/client/CryptoDoc.h +++ b/client/CryptoDoc.h @@ -54,6 +54,9 @@ struct CKey { // holding its private key is the one that can open the container. bool canDecrypt(const QSslCertificate &cert) const; bool isSupported() const; + // Personal code of the person the key is addressed to, empty when it is + // not addressed to a person (password, symmetric or unsupported lock) + QString personalCode() const; bool operator== (const CKey& rhs) const; }; @@ -80,7 +83,7 @@ class CryptoDoc final: public QObject const std::vector& keys() const; bool move(const QString &to); bool open(const QString &file); - void removeKey(unsigned int id); + void removeKeys(const std::vector &keys); void clearKeys(); bool saveCopy(const QString &filename); ria::qdigidoc4::ContainerState state() const; diff --git a/client/dialogs/AddRecipients.cpp b/client/dialogs/AddRecipients.cpp index 0cbc8d592..c756a5f9c 100644 --- a/client/dialogs/AddRecipients.cpp +++ b/client/dialogs/AddRecipients.cpp @@ -141,8 +141,10 @@ AddRecipients::AddRecipients(ItemList* itemList, bool keyShares, QWidget *parent connect(this, &AddRecipients::finished, this, &AddRecipients::close); connect(ui->leftPane, &ItemList::add, this, [this] { - for(Item *item: ui->leftPane->items) - addRecipientToRightPane(item); + for(Item *item: ui->leftPane->items) { + if(auto *address = qobject_cast(item)) + addRecipientToRightPane(address->selectedKeys()); + } }); connect(ui->rightPane, &ItemList::removed, ui->rightPane, &ItemList::removeItem ); @@ -163,8 +165,10 @@ AddRecipients::AddRecipients(ItemList* itemList, bool keyShares, QWidget *parent connect(ui->fromFile, &QPushButton::clicked, this, &AddRecipients::addRecipientFromFile); connect(ui->fromHistory, &QPushButton::clicked, this, &AddRecipients::addRecipientFromHistory); - for(Item *item: itemList->items) - addRecipientToRightPane(item, false); + for(Item *item: itemList->items) { + if(auto *address = qobject_cast(item)) + addRecipientToRightPane(address->keys(), false); + } } AddRecipients::~AddRecipients() @@ -246,76 +250,106 @@ void AddRecipients::addRecipient(const CKey &key, bool select) { leftItem = new AddressItem(key, AddressItem::Add, ui->leftPane); ui->leftPane->addWidget(leftItem); - leftItem->setDisabled(rightList.contains(key)); - connect(leftItem, &AddressItem::add, this, [this](Item *item) { addRecipientToRightPane(item); }); + connect(leftItem, &AddressItem::add, this, [this](Item *item) { + addRecipientToRightPane(qobject_cast(item)->selectedKeys()); + }); if(auto *add = ui->leftPane->findChild(QStringLiteral("add"))) add->setVisible(true); } + else if(std::ranges::find(leftItem->keys(), key) == leftItem->keys().cend()) + // The person's other certificate, e.g. Digi-ID next to the ID-card + leftItem->addKey(key); + setAdded(leftItem); if(select) - addRecipientToRightPane(leftItem); + addRecipientToRightPane({key}); } -void AddRecipients::addRecipientToRightPane(Item *item, bool update) +bool AddRecipients::acceptKey(const CKey &key) { - auto *address = qobject_cast(item); - if(!address) return; - const CKey& key = address->getKey(); - if (rightList.contains(key)) return; - - // A Mobile-ID / Smart-ID recipient has no certificate to check - if(update && !key.rcpt_cert.isNull()) + if(key.rcpt_cert.isNull()) + return true; + if(auto expiryDate = key.rcpt_cert.expiryDate(); expiryDate <= QDateTime::currentDateTime()) { - if(auto expiryDate = key.rcpt_cert.expiryDate(); expiryDate <= QDateTime::currentDateTime()) + if(Settings::CDOC2_DEFAULT && Settings::CDOC2_USE_KEYSERVER) { - if(Settings::CDOC2_DEFAULT && Settings::CDOC2_USE_KEYSERVER) - { - WarningDialog::create(this) - ->withTitle(tr("Failed to add certificate")) - ->withText(tr("An expired certificate cannot be used for encryption.")) - ->open(); - return; - } - auto *dlg = WarningDialog::create(this)->withRichText(tr( - "Are you sure that you want use certificate for encrypting, which expired on %1?
" - "When decrypter has updated certificates then decrypting is impossible.") - .arg(expiryDate.toString(QStringLiteral("dd.MM.yyyy hh:mm:ss")))) - ->setCancelText(WarningDialog::NO) - ->addButton(WarningDialog::YES, QMessageBox::Yes); - if(dlg->exec() != QMessageBox::Yes) - return; + WarningDialog::create(this) + ->withTitle(tr("Failed to add certificate")) + ->withText(tr("An expired certificate cannot be used for encryption.")) + ->open(); + return false; } - QSslConfiguration backup = QSslConfiguration::defaultConfiguration(); - QSslConfiguration::setDefaultConfiguration(CheckConnection::sslConfiguration()); - QList errors = QSslCertificate::verify({key.rcpt_cert}); - QSslConfiguration::setDefaultConfiguration(backup); - errors.removeAll(QSslError(QSslError::CertificateExpired, key.rcpt_cert)); - if(!errors.isEmpty()) + auto *dlg = WarningDialog::create(this)->withRichText(tr( + "Are you sure that you want use certificate for encrypting, which expired on %1?
" + "When decrypter has updated certificates then decrypting is impossible.") + .arg(expiryDate.toString(QStringLiteral("dd.MM.yyyy hh:mm:ss")))) + ->setCancelText(WarningDialog::NO) + ->addButton(WarningDialog::YES, QMessageBox::Yes); + if(dlg->exec() != QMessageBox::Yes) + return false; + } + QSslConfiguration backup = QSslConfiguration::defaultConfiguration(); + QSslConfiguration::setDefaultConfiguration(CheckConnection::sslConfiguration()); + QList errors = QSslCertificate::verify({key.rcpt_cert}); + QSslConfiguration::setDefaultConfiguration(backup); + errors.removeAll(QSslError(QSslError::CertificateExpired, key.rcpt_cert)); + if(!errors.isEmpty()) + { + auto *dlg = WarningDialog::create(this) + ->withText(tr("Recipient’s certification chain contains certificates that are not trusted. Continue with encryption?")) + ->setCancelText(WarningDialog::NO) + ->addButton(WarningDialog::YES, QMessageBox::Yes); + if(dlg->exec() != QMessageBox::Yes) + return false; + } + return true; +} + +void AddRecipients::addRecipientToRightPane(const std::vector &keys, bool update) +{ + for(const CKey &key: keys) + { + if(rightList.contains(key) || (update && !acceptKey(key))) + continue; + rightList.append(key); + + // One card per person; their further certificates join it + if(AddressItem *rightItem = itemListValue(ui->rightPane, key)) + rightItem->addKey(key); + else { - auto *dlg = WarningDialog::create(this) - ->withText(tr("Recipient’s certification chain contains certificates that are not trusted. Continue with encryption?")) - ->setCancelText(WarningDialog::NO) - ->addButton(WarningDialog::YES, QMessageBox::Yes); - if(dlg->exec() != QMessageBox::Yes) - return; + rightItem = new AddressItem(key, AddressItem::Remove, ui->rightPane); + connect(rightItem, &AddressItem::remove, this, [this](Item *item) { + auto *rightItem = qobject_cast(item); + for(const CKey &key: rightItem->keys()) + rightList.removeAll(key); + if(auto *leftItem = itemListValue(ui->leftPane, rightItem->getKey())) + setAdded(leftItem); + ui->confirm->setDisabled(rightList.isEmpty()); + }); + ui->rightPane->addWidget(rightItem); } + ui->confirm->setEnabled(true); + ui->confirm->setDefault(true); + if(!key.rcpt_cert.isNull()) + historyCertData.addAndSave(key.rcpt_cert); + if(auto *leftItem = itemListValue(ui->leftPane, key)) + setAdded(leftItem); } +} - rightList.append(key); - - auto *rightItem = new AddressItem(key, AddressItem::Remove, ui->rightPane); - connect(rightItem, &AddressItem::remove, this, [this](Item *item) { - auto *rightItem = qobject_cast(item); - if(auto *leftItem = itemListValue(ui->leftPane, rightItem->getKey())) - leftItem->setDisabled(false); - rightList.removeAll(rightItem->getKey()); - ui->confirm->setDisabled(rightList.isEmpty()); - }); - ui->rightPane->addWidget(rightItem); - ui->confirm->setEnabled(true); - ui->confirm->setDefault(true); - historyCertData.addAndSave(key.rcpt_cert); - if(auto *leftItem = itemListValue(ui->leftPane, key)) - leftItem->setDisabled(true); +void AddRecipients::setAdded(AddressItem *leftItem) +{ + // Stays open while some of the person's certificates are still to be added. + // An expired one cannot be ticked, so it does not hold the card open. + bool added = false, open = false; + for(const CKey &key: leftItem->keys()) + { + if(rightList.contains(key)) + added = true; + else if(key.rcpt_cert.isNull() || key.rcpt_cert.expiryDate() > QDateTime::currentDateTime()) + open = true; + } + leftItem->setDisabled(added && !open); } bool AddRecipients::isUpdated() const @@ -325,9 +359,15 @@ bool AddRecipients::isUpdated() const AddressItem* AddRecipients::itemListValue(ItemList *list, const CKey &key) { + // Keys without a personal code get a card of their own + const QString code = key.personalCode(); for(auto *item: list->items) { - if(auto *address = qobject_cast(item); address && (key == address->getKey())) + auto *address = qobject_cast(item); + if(!address) + continue; + if(code.isEmpty() ? std::ranges::find(address->keys(), key) != address->keys().cend() : + address->getKey().personalCode() == code) return address; } return nullptr; @@ -339,7 +379,7 @@ QList AddRecipients::keys() const for(auto *item: ui->rightPane->items) { if(auto *address = qobject_cast(item)) - recipients.append(address->getKey()); + recipients.append(QList(address->keys().cbegin(), address->keys().cend())); } return recipients; } @@ -373,7 +413,8 @@ void AddRecipients::search(const QString &term, bool select, const QString &type ldap_corp->search(QStringLiteral("(serialNumber=%1)" ).arg(cleanTerm), userData); else if(IKValidator::isValid(cleanTerm)) { - // Mobile-ID / Smart-ID certificates are not in LDAP; any person can be offered it + // Mobile-ID / Smart-ID certificates are not in LDAP; any person can be + // offered it, and their certificates join the same card as they arrive if(keyShares) addRecipient(CKey::share(cleanTerm), false); userData[QStringLiteral("personSearch")] = true; diff --git a/client/dialogs/AddRecipients.h b/client/dialogs/AddRecipients.h index 6bd487390..1705ab312 100644 --- a/client/dialogs/AddRecipients.h +++ b/client/dialogs/AddRecipients.h @@ -29,7 +29,6 @@ class AddRecipients; } class AddressItem; -class Item; class ItemList; class LdapSearch; class QSslCertificate; @@ -52,8 +51,11 @@ class AddRecipients final : public QDialog void addRecipientFromHistory(); void addRecipient(const QSslCertificate& cert, bool select = true); void addRecipient(const CKey &key, bool select); - void addRecipientToRightPane(Item *item, bool update = true); + void addRecipientToRightPane(const std::vector &keys, bool update = true); + bool acceptKey(const CKey &key); + void setAdded(AddressItem *leftItem); + // The card of the person the key belongs to static AddressItem* itemListValue(ItemList *list, const CKey &key); void search(const QString &term, bool select = false, const QString &type = {}); void showError(const QString &title, const QString &details); diff --git a/client/dialogs/DecryptionDialog.cpp b/client/dialogs/DecryptionDialog.cpp index 742d3d3aa..e94c47d4e 100644 --- a/client/dialogs/DecryptionDialog.cpp +++ b/client/dialogs/DecryptionDialog.cpp @@ -20,11 +20,11 @@ #include -DecryptionDialog::DecryptionDialog(CryptoDoc *doc, const CKey &key, const QString &recipient, QWidget *parent) +DecryptionDialog::DecryptionDialog(CryptoDoc *doc, std::vector keys, const QString &recipient, QWidget *parent) : QDialog(parent) , cryptoDoc(doc) , m_overlay(std::make_unique(this)) - , m_key(key) + , m_keys(std::move(keys)) , ui(new Ui::DecryptionDialog) { ui->setupUi(this); @@ -42,18 +42,27 @@ DecryptionDialog::DecryptionDialog(CryptoDoc *doc, const CKey &key, const QStrin ui->tabs->addTab({}); updateTabs(); // Tab bar: a card's lock opens only with the card, a key share only with - // Mobile-ID or Smart-ID, so the methods that cannot open this lock are hidden - const bool share = m_key.lock.type == libcdoc::Lock::SHARE_SERVER; + // Mobile-ID or Smart-ID, so the methods none of the recipient's locks open are hidden + const bool share = shareKey(); + const bool card = std::ranges::any_of(m_keys, [](const CKey &key) { return key.lock.isPKI(); }); // Checked on each opening, so an updated central configuration is picked up - m_sharesKnown = !share || DDConfiguration::shareServersKnown(m_key.lock.getString(libcdoc::Lock::SHARE_URLS)); - ui->errorShares->setVisible(!m_sharesKnown); - ui->tabs->setTabVisible(IDCard, !share); + m_sharesKnown = !share || DDConfiguration::shareServersKnown(shareKey()->lock.getString(libcdoc::Lock::SHARE_URLS)); + ui->tabs->setTabVisible(IDCard, card); ui->tabs->setTabVisible(MobileID, share); ui->tabs->setTabVisible(SmartID, share); ui->tabs->setVisible(share); connect(ui->tabs, &QTabBar::currentChanged, ui->tabStack, &QStackedWidget::setCurrentIndex); - connect(ui->tabs, &QTabBar::currentChanged, this, &DecryptionDialog::updateShareButton); - ui->tabs->setCurrentIndex(share ? MobileID : IDCard); + connect(ui->tabs, &QTabBar::currentChanged, this, [this](int index) { + // A person with both a card and a key share sees all tabs; the card list is kept + // current by cacheChanged, so only the Decrypt button and the share warning follow + ui->errorShares->setVisible(!m_sharesKnown && index != IDCard); + if(index == IDCard) + updateSelection(); + else + updateShareButton(); + }); + ui->tabs->setCurrentIndex(card ? IDCard : MobileID); + ui->errorShares->setVisible(!m_sharesKnown && ui->tabs->currentIndex() != IDCard); ui->tabStack->setCurrentIndex(ui->tabs->currentIndex()); // ID-card tab @@ -98,6 +107,11 @@ std::unique_ptr DecryptionDialog::takeBackend() return std::move(m_backend); } +const CKey* DecryptionDialog::selectedKey() const +{ + return keyFor(m_selectedToken); +} + DecryptionDialog::Method DecryptionDialog::currentMethod() const { return static_cast(ui->tabs->currentIndex()); @@ -154,13 +168,33 @@ void DecryptionDialog::updateNoCard() ui->noCard->setAccessibleDescription(ui->noCardStatus->text()); } +const CKey* DecryptionDialog::keyFor(const TokenData &token) const +{ + // A card can be addressed both directly and through the key server; + // the direct lock needs no network and cannot expire on the server. + const CKey *found = nullptr; + for(const CKey &key: m_keys) { + if(key.canDecrypt(token.cert()) && (!found || found->lock.type == libcdoc::Lock::SERVER)) + found = &key; + } + return found; +} + +const CKey* DecryptionDialog::shareKey() const +{ + auto it = std::ranges::find(m_keys, libcdoc::Lock::SHARE_SERVER, [](const CKey &key) { return key.lock.type; }); + return it != m_keys.cend() ? &*it : nullptr; +} + void DecryptionDialog::updateCards() { qDeleteAll(ui->cardListContainer->findChildren()); + // Only a card one of the locks is addressed to can open it; the others are listed + // but not selectable, so it stays visible which card is missing or blocked. auto usableToken = [this](const TokenData &token) { SslCertificate c(token.cert()); - return c.isValid() && !token.data(QStringLiteral("blocked")).toBool() && m_key.canDecrypt(token.cert()); + return c.isValid() && !token.data(QStringLiteral("blocked")).toBool() && keyFor(token); }; const QList tokens = qApp->cryptoManager()->cache(); @@ -188,7 +222,7 @@ void DecryptionDialog::updateCards() if(!SslCertificate(token.cert()).isEncryptionCert()) continue; auto *w = new CardListItem(ui->cardListContainer); - w->update(token, token == m_selectedToken, m_key.canDecrypt(token.cert()) ? + w->update(token, token == m_selectedToken, keyFor(token) ? nullptr : QT_TRANSLATE_NOOP("CardListItem", "This card cannot decrypt the container")); connect(w, &CardListItem::selected, this, &DecryptionDialog::updateSelection); ui->cardListContainer->layout()->addWidget(w); @@ -285,7 +319,7 @@ bool DecryptionDialog::performShareDecrypt() self->ui->progressBar->setValue(0); timer->start(); }; - bool result = cryptoDoc->decrypt(m_key.lock, + bool result = cryptoDoc->decrypt(shareKey()->lock, currentMethod() == MobileID ? ui->phoneNo->text() : QString(), feedback); if(!self) return result; diff --git a/client/dialogs/DecryptionDialog.h b/client/dialogs/DecryptionDialog.h index 6870aee83..6288c9743 100644 --- a/client/dialogs/DecryptionDialog.h +++ b/client/dialogs/DecryptionDialog.h @@ -16,10 +16,13 @@ class DecryptionDialog final : public QDialog public: enum Method { IDCard, MobileID, SmartID }; - explicit DecryptionDialog(CryptoDoc *doc, const CKey &key, const QString &recipient, + // keys: the recipient's locks, e.g. one per ID-card and Digi-ID of the same person + explicit DecryptionDialog(CryptoDoc *doc, std::vector keys, const QString &recipient, QWidget *parent = nullptr); ~DecryptionDialog() final; std::unique_ptr takeBackend(); + // The lock a card's login (takeBackend()) is for, the one of the recipient's that its card opens + const CKey* selectedKey() const; private: void changeEvent(QEvent *event) final; @@ -34,11 +37,13 @@ class DecryptionDialog final : public QDialog bool performLogin(); bool performShareDecrypt(); Method currentMethod() const; + const CKey* keyFor(const TokenData &token) const; + const CKey* shareKey() const; CryptoDoc *cryptoDoc; std::unique_ptr m_backend; std::unique_ptr m_overlay; - CKey m_key; + std::vector m_keys; Ui::DecryptionDialog *ui; TokenData m_selectedToken; bool m_confirmDecrypt = false; diff --git a/client/translations/en.ts b/client/translations/en.ts index 710bee4dd..48fb0b438 100644 --- a/client/translations/en.ts +++ b/client/translations/en.ts @@ -90,10 +90,6 @@ mobile-ID mobile-ID - - Expires on - Expires on - Added Added @@ -119,12 +115,16 @@ e-Seal - Authentication certificate - Authentication certificate + %1 (decryption expired %2) + %1 (decryption expired %2) + + + %1 (decryption until %2) + %1 (decryption until %2) - Expired on - Expired on + Authentication certificate + Authentication certificate Unsupported cryptographic algorithm or recipient type @@ -138,14 +138,6 @@ pw Password - - Decryption is possible until: - Decryption is possible until: - - - Decryption has expired - Decryption has expired - ID-CARD ID-CARD diff --git a/client/translations/et.ts b/client/translations/et.ts index b2fe91af2..68027ce7a 100644 --- a/client/translations/et.ts +++ b/client/translations/et.ts @@ -90,10 +90,6 @@ mobile-ID mobiil-ID - - Expires on - Aegub - Added Lisatud @@ -119,12 +115,16 @@ e-Tempel - Authentication certificate - Isikutuvastamise sertifikaat + %1 (decryption expired %2) + %1 (dekrüpteerimine aegus %2) + + + %1 (decryption until %2) + %1 (dekrüpteerimine kuni %2) - Expired on - Aegus + Authentication certificate + Isikutuvastamise sertifikaat Unsupported cryptographic algorithm or recipient type @@ -138,14 +138,6 @@ pw Parool - - Decryption is possible until: - Dekrüpteerimine on võimalik kuni: - - - Decryption has expired - Dekrüpteerimine on aegunud - ID-CARD ID-KAART diff --git a/client/widgets/AddressItem.cpp b/client/widgets/AddressItem.cpp index 47a66bf1e..0d8b50eeb 100644 --- a/client/widgets/AddressItem.cpp +++ b/client/widgets/AddressItem.cpp @@ -21,66 +21,56 @@ #include "ui_AddressItem.h" #include "CryptoDoc.h" +#include "Settings.h" #include "SslCertificate.h" #include "dialogs/KeyDialog.h" +#include +#include +#include +#include + using namespace ria::qdigidoc4; struct AddressItem::Private: public Ui::AddressItem { - CKey key; + Private(Type type) : type(type) {} + + const Type type; QString code; + // Keys of one person, sharing the card; the first one names it + std::vector keys; + // One line per key: a checkbox tile on an Add card, a badge on the others + struct Tile { + QWidget *tile; + QCheckBox *check; + // QCheckBox cannot wrap "Mobile-ID / Smart-ID" in a narrow tile; a label can + QLabel *text; + }; + std::vector tiles; + std::vector badges; + QGridLayout *checkLayout {}; QString label; - QDateTime expireDate; - ContainerState state = Uninitialized; + ContainerState state = UnencryptedContainer; }; AddressItem::AddressItem(const CKey &key, Type type, QWidget *parent) : Item(parent) - , ui(new Private{.key = key}) + , ui(new Private(type)) { ui->setupUi(this); - if(type == Icon) - ui->icon->load(QStringLiteral(":/images/icon_Krypto_small.svg")); - ui->icon->setVisible(type == Icon); + setProperty("card", type != Icon); + ui->icon->load(QStringLiteral(":/images/icon_Minu_eID_hover.svg")); ui->name->setAttribute(Qt::WA_TransparentForMouseEvents, true); - ui->expire->setAttribute(Qt::WA_TransparentForMouseEvents, true); - ui->idType->setAttribute(Qt::WA_TransparentForMouseEvents, true); + if(type == Add) { + ui->checkLayout = new QGridLayout; + ui->checkLayout->setSpacing(8); + ui->AddressItemLayout->addLayout(ui->checkLayout, 1, 0, 1, -1); + } - bool unsupported = false; - if (!ui->key.rcpt_cert.isNull()) { - // Recipient certificate - ui->code = SslCertificate(ui->key.rcpt_cert).personalCode(); - ui->label = !ui->key.rcpt_cert.subjectInfo("GN").isEmpty() && - !ui->key.rcpt_cert.subjectInfo("SN").isEmpty() - ? ui->key.rcpt_cert.subjectInfo("GN").join(' ') + ' ' + - ui->key.rcpt_cert.subjectInfo("SN").join(' ') - : ui->key.rcpt_cert.subjectInfo("CN").join(' '); + bool unsupported = !setRecipient(key); + if(!key.rcpt_cert.isNull()) ui->decrypt->hide(); - } else if (ui->key.lock.type == libcdoc::Lock::SHARE_SERVER) { - // Mobile-ID / Smart-ID key share, addressed by personal code and without a label - ui->code = SslCertificate::personalCode(QString::fromStdString( - ui->key.lock.getString(libcdoc::Lock::RECIPIENT_ID)).remove(QLatin1String("etsi/"))); - } else if (ui->key.lock.isValid()) { - // Known lock type - ui->code.clear(); - auto map = libcdoc::Lock::parseLabel(ui->key.lock.label); - if (map.contains("cn")) { - ui->label = QString::fromStdString(map["cn"]); - } else if (map.contains("label")) { - ui->label = QString::fromStdString(map["label"]); - } else { - ui->label = QString::fromStdString(ui->key.lock.label); - } - if (map.contains("server_exp")) { - ui->expireDate = QDateTime::fromSecsSinceEpoch(QString::fromStdString(map["server_exp"]).toLongLong()); - } - } else { - // No rcpt, lock is invalid = unsupported lock - unsupported = true; - ui->code.clear(); - ui->label = tr("Unsupported cryptographic algorithm or recipient type"); - } if(!unsupported) setCursor(Qt::PointingHandCursor); @@ -90,10 +80,10 @@ AddressItem::AddressItem(const CKey &key, Type type, QWidget *parent) connect(ui->decrypt, &QToolButton::clicked, this, &AddressItem::decrypt); setName(); - setIdType(); + addKey(key); + setName(); ui->add->setVisible(type == Add); ui->remove->setVisible(type != Add); - setDecryptVisible(); } AddressItem::~AddressItem() @@ -107,25 +97,175 @@ void AddressItem::changeEvent(QEvent* event) { ui->retranslateUi(this); setName(); - setIdType(); + for(size_t i = 0; i < ui->keys.size(); ++i) + setKeyText(i); } - if (event->type() == QEvent::EnabledChange) + // retranslateUi() resets the text to the .ui's "Add" + if (event->type() == QEvent::LanguageChange || event->type() == QEvent::EnabledChange) ui->add->setText(isEnabled() ? tr("Add") : tr("Added")); QWidget::changeEvent(event); } +bool AddressItem::eventFilter(QObject *watched, QEvent *event) +{ + // A click anywhere on a tile toggles it, and does not open the key details + if(auto *tile = qobject_cast(watched); tile && tile->objectName() == QLatin1String("tile")) { + if(event->type() == QEvent::MouseButtonPress) + return true; + if(event->type() == QEvent::MouseButtonRelease) { + if(auto *check = tile->findChild(); check && check->isEnabled()) + check->toggle(); + return true; + } + } + return Item::eventFilter(watched, event); +} + +void AddressItem::setAddEnabled() +{ + ui->add->setDisabled(std::ranges::none_of(ui->tiles, [](const auto &t) { return t.check->isChecked(); })); +} + +bool AddressItem::setRecipient(const CKey &key) +{ + if (!key.rcpt_cert.isNull()) { + // Recipient certificate + ui->code = SslCertificate(key.rcpt_cert).personalCode(); + ui->label = !key.rcpt_cert.subjectInfo("GN").isEmpty() && + !key.rcpt_cert.subjectInfo("SN").isEmpty() + ? key.rcpt_cert.subjectInfo("GN").join(' ') + ' ' + + key.rcpt_cert.subjectInfo("SN").join(' ') + : key.rcpt_cert.subjectInfo("CN").join(' '); + } else if (key.lock.type == libcdoc::Lock::SHARE_SERVER) { + // Mobile-ID / Smart-ID: only the personal code is known, and libcdoc + // writes no label for it, so the lock does not pass isValid() + ui->code = key.personalCode(); + ui->label.clear(); + } else if (key.lock.isValid()) { + // Known lock type + ui->code.clear(); + auto map = libcdoc::Lock::parseLabel(key.lock.label); + if (map.contains("cn")) { + ui->label = QString::fromStdString(map["cn"]); + } else if (map.contains("label")) { + ui->label = QString::fromStdString(map["label"]); + } else { + ui->label = QString::fromStdString(key.lock.label); + } + } else { + // No rcpt, lock is invalid = unsupported lock + ui->code.clear(); + ui->label = tr("Unsupported cryptographic algorithm or recipient type"); + return false; + } + return true; +} + +void AddressItem::addKey(const CKey &key) +{ + // A card started from a key share has no name until a certificate brings it + if(ui->label.isEmpty() && !key.rcpt_cert.isNull()) { + setRecipient(key); + setName(); + } + // Mobile-ID / Smart-ID stays last, after the person's certificates + auto isShare = [](const CKey &k) { return k.lock.type == libcdoc::Lock::SHARE_SERVER; }; + size_t pos = isShare(key) ? ui->keys.size() : + size_t(std::ranges::find_if(ui->keys, isShare) - ui->keys.cbegin()); + ui->keys.insert(ui->keys.begin() + qsizetype(pos), key); + if(ui->type == Add) { + auto *tile = new QWidget(this); + tile->setObjectName(QStringLiteral("tile")); + tile->setAttribute(Qt::WA_StyledBackground); + tile->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred); + tile->setCursor(Qt::PointingHandCursor); + tile->installEventFilter(this); + auto *check = new QCheckBox(tile); + // The macOS style otherwise lays it out by a visual rect that the label overlaps + check->setAttribute(Qt::WA_LayoutUsesWidgetRect); + auto *text = new QLabel(tile); + text->setWordWrap(true); + text->setAttribute(Qt::WA_TransparentForMouseEvents, true); + auto *tileLayout = new QHBoxLayout(tile); + tileLayout->setContentsMargins(8, 8, 8, 8); + tileLayout->setSpacing(8); + tileLayout->addWidget(check); + tileLayout->addWidget(text, 1); + ui->tiles.insert(ui->tiles.begin() + qsizetype(pos), {tile, check, text}); + // Equal tiles, at most three to a row as in the design + constexpr int columns = 3; + for(const auto &t: ui->tiles) + ui->checkLayout->removeWidget(t.tile); + for(int i = 0; i < columns; ++i) + ui->checkLayout->setColumnStretch(i, i < int(ui->tiles.size()) ? 1 : 0); + for(size_t i = 0; i < ui->tiles.size(); ++i) + ui->checkLayout->addWidget(ui->tiles[i].tile, int(i) / columns, int(i) % columns); + connect(check, &QCheckBox::toggled, this, [this, tile](bool checked) { + tile->setProperty("checked", checked); + tile->style()->unpolish(tile); + tile->style()->polish(tile); + setAddEnabled(); + }); + check->setChecked(true); + setKeyText(pos); + setAddEnabled(); + return; + } + auto *badge = new QLabel(this); + badge->setFocusPolicy(Qt::TabFocus); + badge->setAttribute(Qt::WA_TransparentForMouseEvents, true); + ui->badges.push_back(badge); + // Each key gets its own line under the name; the side widgets span them all + int rows = int(ui->badges.size()) + 1; + ui->AddressItemLayout->addWidget(badge, rows - 1, 1, Qt::AlignLeft); + for(QWidget *w: {(QWidget*)ui->icon, (QWidget*)ui->remove, (QWidget*)ui->add, (QWidget*)ui->decrypt}) { + int row{}, column{}, rowSpan{}, columnSpan{}; + ui->AddressItemLayout->getItemPosition(ui->AddressItemLayout->indexOf(w), &row, &column, &rowSpan, &columnSpan); + ui->AddressItemLayout->removeWidget(w); + ui->AddressItemLayout->addWidget(w, row, column, rows, columnSpan); + } + // Badges hold no state, so a key inserted ahead of others just shifts the texts + for(size_t i = pos; i < ui->keys.size(); ++i) + setKeyText(i); + setDecryptVisible(); +} + const CKey& AddressItem::getKey() const { - return ui->key; + return ui->keys.front(); +} + +const std::vector& AddressItem::keys() const +{ + return ui->keys; +} + +std::vector AddressItem::selectedKeys() const +{ + if(ui->type != Add) + return ui->keys; + std::vector result; + for(size_t i = 0; i < ui->keys.size(); ++i) { + if(ui->tiles[i].check->isChecked()) + result.push_back(ui->keys[i]); + } + return result; } void AddressItem::initTabOrder(QWidget *item) { - setTabOrder(item, ui->name); - setTabOrder(ui->name, ui->idType); - setTabOrder(ui->idType, ui->expire); - setTabOrder(ui->expire, ui->remove); - setTabOrder(ui->remove, lastTabWidget()); + QWidget *prev = item; + auto chain = [&prev](QWidget *next) { + setTabOrder(prev, next); + prev = next; + }; + chain(ui->name); + for(const auto &t: ui->tiles) + chain(t.check); + for(QWidget *badge: ui->badges) + chain(badge); + chain(ui->remove); + chain(lastTabWidget()); } QWidget* AddressItem::lastTabWidget() @@ -134,24 +274,30 @@ QWidget* AddressItem::lastTabWidget() } void AddressItem::mouseReleaseEvent(QMouseEvent * /*event*/) { - (new KeyDialog(ui->key, this))->open(); + (new KeyDialog(ui->keys.front(), this))->open(); } -QString AddressItem::name() const +void AddressItem::setDecryptVisible() { - return QStringLiteral("%1 %2").arg(ui->label, ui->code).trimmed(); + ui->decrypt->setVisible(ui->state == EncryptedContainer && + std::ranges::any_of(ui->keys, [](const CKey &key) { + return key.lock.isPKI() || key.lock.type == libcdoc::Lock::PASSWORD || + key.lock.type == libcdoc::Lock::SHARE_SERVER; + })); } -void AddressItem::setDecryptVisible() +QString AddressItem::name() const { - ui->decrypt->setVisible(ui->state == EncryptedContainer && (ui->key.lock.isPKI() || - ui->key.lock.type == libcdoc::Lock::PASSWORD || ui->key.lock.type == libcdoc::Lock::SHARE_SERVER)); + if(ui->code.isEmpty() || ui->label.isEmpty()) + return ui->code.isEmpty() ? ui->label : ui->code; + return QStringLiteral("%1, %2").arg(ui->label, ui->code); } void AddressItem::setName() { - ui->name->setText(QStringLiteral("%1 %2") - .arg(ui->label.toHtmlEscaped(), ui->code.toHtmlEscaped())); + ui->name->setText(ui->code.isEmpty() || ui->label.isEmpty() ? name().toHtmlEscaped() : + QStringLiteral("%1, %2") + .arg(ui->label.toHtmlEscaped(), ui->code.toHtmlEscaped())); if(ui->name->text().isEmpty()) ui->name->hide(); } @@ -159,87 +305,82 @@ void AddressItem::setName() void AddressItem::stateChange(ContainerState state) { ui->state = state; + if(ui->type == Icon) + ui->icon->load(state == EncryptedContainer ? + QStringLiteral(":/images/icon_Krypto_small.svg") : QStringLiteral(":/images/icon_Minu_eID_hover.svg")); if(ui->add->isHidden()) ui->remove->setVisible(state == UnencryptedContainer); setDecryptVisible(); } -void AddressItem::setIdType(const SslCertificate &cert) { - SslCertificate::CertType type = cert.type(); - if(type & SslCertificate::DigiIDType) - ui->idType->setText(tr("digi-ID")); - else if(type & SslCertificate::EstEidType) - ui->idType->setText(tr("ID-card")); - else if(type & SslCertificate::MobileIDType) - ui->idType->setText(tr("mobile-ID")); - else if(type & SslCertificate::TempelType) - { - if(cert.keyUsage().contains(SslCertificate::NonRepudiation)) - ui->idType->setText(tr("e-Seal")); - else if(cert.enhancedKeyUsage().contains(SslCertificate::ClientAuth)) - ui->idType->setText(tr("Authentication certificate")); - else - ui->idType->setText(tr("Certificate for Encryption")); +void AddressItem::setKeyText(size_t i) +{ + const CKey &key = ui->keys[i]; + QString type; + // Until when the key can open the container: the certificate's own expiry, or + // the capsule's on the key server. The latter is known only once encrypted. + QDateTime until; + SslCertificate cert(key.rcpt_cert); + if(cert.isNull() && key.lock.isCDoc1()) { + const auto &bytes = key.lock.getBytes(libcdoc::Lock::CERT); + cert = SslCertificate(QByteArray::fromRawData((const char *)bytes.data(), qsizetype(bytes.size())), QSsl::Der); } - ui->expire->setProperty("label", QStringLiteral("default")); - ui->expire->setText(QStringLiteral("%1 %2").arg( - cert.isValid() ? tr("Expires on") : tr("Expired on"), - cert.expiryDate().toLocalTime().toString( - QStringLiteral("dd.MM.yyyy")))); -} - -void AddressItem::setIdType() { - ui->expire->clear(); - - if (!ui->key.rcpt_cert.isNull()) { - // Recipient certificate - setIdType(SslCertificate(ui->key.rcpt_cert)); - } else if (ui->key.lock.type == libcdoc::Lock::SHARE_SERVER) { - ui->idType->setText(tr("Mobile-ID / Smart-ID")); - } else if (ui->key.lock.isValid()) { - // Known lock type + if(!cert.isNull()) { + type = typeName(cert); + until = cert.expiryDate(); + if(!key.rcpt_cert.isNull() && cert.isValid() && Settings::CDOC2_DEFAULT && Settings::CDOC2_USE_KEYSERVER) + until = {}; + } else if(key.lock.type == libcdoc::Lock::SHARE_SERVER) { + type = tr("Mobile-ID / Smart-ID"); + } else if(key.lock.isValid()) { // Needed to include translation for "ID-CARD" void(QT_TR_NOOP("ID-CARD")); void(QT_TR_NOOP("pw")); - auto items = libcdoc::Lock::parseLabel(ui->key.lock.label); - if (ui->key.lock.isCDoc1()) { - const auto &bytes = ui->key.lock.getBytes(libcdoc::Lock::CERT); - setIdType(SslCertificate(QByteArray::fromRawData((const char *)bytes.data(), bytes.size()), QSsl::Der)); - } else { - ui->idType->setText(tr(items["type"].data())); - } - if (ui->key.lock.type == libcdoc::Lock::SERVER) { - std::string server_exp = items["server_exp"]; - if (!server_exp.empty()) { - uint64_t seconds = std::stoull(server_exp); - auto date = QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC); - bool canDecrypt = QDateTime::currentDateTimeUtc() < date; - ui->expire->setProperty("label", canDecrypt - ? QStringLiteral("good") - : QStringLiteral("error")); - ui->expire->setText( - canDecrypt ? QStringLiteral("%1 %2").arg( - tr("Decryption is possible until:"), - date.toLocalTime().toString( - QStringLiteral("dd.MM.yyyy"))) - : tr("Decryption has expired")); - } - } else if(!ui->expireDate.isNull()) { - bool canDecrypt = ui->expireDate > QDateTime::currentDateTime(); - ui->expire->setProperty("label", canDecrypt - ? QStringLiteral("good") - : QStringLiteral("warn")); - ui->expire->setText( - canDecrypt ? QStringLiteral("%1 %2").arg( - tr("Decryption is possible until:"), - ui->expireDate.toLocalTime().toString(QStringLiteral("dd.MM.yyyy"))) - : tr("Decryption has expired")); - } - } else { - // No rcpt, lock is invalid = unsupported lock - ui->idType->setText("Unsupported"); - ui->expire->setHidden(true); + auto items = libcdoc::Lock::parseLabel(key.lock.label); + type = tr(items["type"].data()); + if(const std::string &exp = items["server_exp"]; !exp.empty()) + until = QDateTime::fromSecsSinceEpoch(QString::fromStdString(exp).toLongLong()); + } + + bool expired = until.isValid() && until <= QDateTime::currentDateTimeUtc(); + QString text = type; + if(until.isValid()) + text = (expired ? tr("%1 (decryption expired %2)") : tr("%1 (decryption until %2)")) + .arg(type, until.toLocalTime().toString(QStringLiteral("dd.MM.yyyy"))); + + if(ui->type == Add) { + // An expired key cannot be ticked; its date is too long for the tile + const auto &t = ui->tiles[i]; + t.text->setText(type); + t.tile->setToolTip(expired ? text : QString()); + if(expired) + t.check->setChecked(false); + t.tile->setDisabled(expired); + return; + } + QLabel *badge = ui->badges[i]; + badge->setText(text); + badge->setProperty("label", expired ? QStringLiteral("error") : + key.lock.type == libcdoc::Lock::PASSWORD ? QStringLiteral("default") : QStringLiteral("good")); + badge->setHidden(text.isEmpty()); +} + +QString AddressItem::typeName(const SslCertificate &cert) const +{ + SslCertificate::CertType type = cert.type(); + if(type & SslCertificate::DigiIDType) + return tr("digi-ID"); + if(type & SslCertificate::EstEidType) + return tr("ID-card"); + if(type & SslCertificate::MobileIDType) + return tr("mobile-ID"); + if(type & SslCertificate::TempelType) + { + if(cert.keyUsage().contains(SslCertificate::NonRepudiation)) + return tr("e-Seal"); + if(cert.enhancedKeyUsage().contains(SslCertificate::ClientAuth)) + return tr("Authentication certificate"); } - ui->idType->setHidden(ui->idType->text().isEmpty()); - ui->expire->setHidden(ui->expire->text().isEmpty()); + // An unrecognised certificate would otherwise leave its tile unlabelled + return tr("Certificate for Encryption"); } diff --git a/client/widgets/AddressItem.h b/client/widgets/AddressItem.h index 7b08f5b85..99a81fd22 100644 --- a/client/widgets/AddressItem.h +++ b/client/widgets/AddressItem.h @@ -21,6 +21,8 @@ #include "widgets/Item.h" +#include + struct CKey; class AddressItem final : public Item @@ -38,7 +40,13 @@ class AddressItem final : public Item explicit AddressItem(const CKey &k, Type type, QWidget *parent = {}); ~AddressItem() final; + // Adds another key of the same person to this card + void addKey(const CKey &key); const CKey& getKey() const; + const std::vector& keys() const; + // Keys ticked on an Add card, all keys on the others + std::vector selectedKeys() const; + // Recipient as shown on the row, as plain text QString name() const; void initTabOrder(QWidget *item) final; QWidget* lastTabWidget() final; @@ -49,11 +57,14 @@ class AddressItem final : public Item private: void changeEvent(QEvent *event) final; + bool eventFilter(QObject *watched, QEvent *event) final; + void setAddEnabled(); void mouseReleaseEvent(QMouseEvent *event) final; void setDecryptVisible(); void setName(); - void setIdType(); - void setIdType(const SslCertificate& cert); + bool setRecipient(const CKey &key); + void setKeyText(size_t i); + QString typeName(const SslCertificate &cert) const; struct Private; Private *ui; diff --git a/client/widgets/AddressItem.ui b/client/widgets/AddressItem.ui index 4635c1379..2bd0a1ac5 100644 --- a/client/widgets/AddressItem.ui +++ b/client/widgets/AddressItem.ui @@ -15,6 +15,10 @@ border-bottom: 1px solid #E7EAEF; background-color: #FFFFFF; } +#AddressItem[card="true"] { +border: 1px solid #E7EAEF; +border-radius: 4px; +} #AddressItem:disabled { background-color: #F0F0F2; } @@ -26,10 +30,6 @@ color: #07142A; font-size: 14px; font-weight: 700; } -#idType { -color: #07142A; -font-size: 12px; -} QLabel[label] { font-size: 12px; padding: 2px 8px; @@ -47,6 +47,42 @@ QLabel[label="good"] { color: #1A641B; background: #EAF8EA; } +#tile { +background-color: #F3F5F7; +border-radius: 4px; +} +#tile[checked="true"] { +background-color: #EAF1F8; +} +#tile QLabel { +color: #07142A; +font-size: 14px; +} +#tile QLabel:disabled { +color: #C4CBD8; +} +QCheckBox::indicator { +width: 16px; +height: 16px; +} +QCheckBox::indicator:unchecked { +image: url(:/images/icon_checkbox.svg); +} +QCheckBox::indicator:unchecked:hover, QCheckBox::indicator:unchecked:focus { +image: url(:/images/icon_checkbox_active.svg); +} +QCheckBox::indicator:unchecked:disabled { +image: url(:/images/icon_checkbox_disabled.svg); +} +QCheckBox::indicator:checked { +image: url(:/images/icon_checkbox_check.svg); +} +QCheckBox::indicator:checked:hover, QCheckBox::indicator:checked:focus { +image: url(:/images/icon_checkbox_check_active.svg); +} +QCheckBox::indicator:checked:disabled { +image: url(:/images/icon_checkbox_check_disabled.svg); +} #add { padding: 9px 8px; font-size: 12px; @@ -86,7 +122,7 @@ icon: url(:/images/icon_remove_hover.svg); icon: url(:/images/icon_remove_clicked.svg); } - + 8 @@ -103,9 +139,9 @@ icon: url(:/images/icon_remove_clicked.svg); 8 - 2 + 4 - + @@ -121,7 +157,7 @@ icon: url(:/images/icon_remove_clicked.svg); - + Qt::TabFocus @@ -134,43 +170,7 @@ icon: url(:/images/icon_remove_clicked.svg); - - - - Qt::TabFocus - - - ID-card - - - - - - - Qt::TabFocus - - - Expire - - - default - - - - - - - Qt::Horizontal - - - - 40 - 0 - - - - - + @@ -199,7 +199,7 @@ icon: url(:/images/icon_remove_clicked.svg); - + PointingHandCursor @@ -209,7 +209,7 @@ icon: url(:/images/icon_remove_clicked.svg); - + PointingHandCursor diff --git a/client/widgets/ContainerPage.cpp b/client/widgets/ContainerPage.cpp index d43d0b007..67ce39e13 100644 --- a/client/widgets/ContainerPage.cpp +++ b/client/widgets/ContainerPage.cpp @@ -229,17 +229,13 @@ void ContainerPage::transition(CryptoDoc *container) if(!dlg.exec() || !dlg.isUpdated()) return; container->clearKeys(); - ui->rightPane->clear(); for(auto &key: dlg.keys()) - { container->addEncryptionKey(key); - ui->rightPane->addWidget(new AddressItem(key, AddressItem::Icon, ui->rightPane)); - } + showRecipients(container); mainAction->setEnabled(isEncryptEnabled(container)); }); disconnect(ui->rightPane, &ItemList::removed, container, nullptr); connect(ui->rightPane, &ItemList::removed, container, [this, container](int index) { - container->removeKey(index); ui->rightPane->removeItem(index); mainAction->setEnabled(isEncryptEnabled(container)); }); @@ -273,15 +269,40 @@ void ContainerPage::transition(CryptoDoc *container) setHeader(container->fileName()); ui->leftPane->init(fileName, QT_TRANSLATE_NOOP("ItemList", "Encrypted files")); ui->rightPane->init(ItemList::ItemAddress, QT_TRANSLATE_NOOP("ItemList", "Recipients")); - bool hasUnsupported = false; - for (const auto &key : container->keys()) { + showRecipients(container); + if(std::ranges::any_of(container->keys(), [](const CKey &key) { // libcdoc writes no label for a key share, which isValid() requires - hasUnsupported = hasUnsupported || (key.rcpt_cert.isNull() && !key.lock.isValid() && - key.lock.type != libcdoc::Lock::SHARE_SERVER); + return key.rcpt_cert.isNull() && !key.lock.isValid() && key.lock.type != libcdoc::Lock::SHARE_SERVER; + })) + emit warning({WarningText::UnsupportedCDocWarning}); + ui->leftPane->setModel(container->documentModel()); + updatePanes(container->state(), container); +} + +void ContainerPage::showRecipients(CryptoDoc *container) +{ + ui->rightPane->clear(); + // One row per person, so e.g. their ID-card and Digi-ID share one Decrypt + // button, or are removed together before encrypting. + QHash people; + for (const auto &key : container->keys()) { + QString code = key.personalCode(); + if(AddressItem *person = people.value(code)) { + person->addKey(key); + continue; + } auto *addr = new AddressItem(key, AddressItem::Icon, ui->rightPane); + // A row is a person and takes all their keys with it. Connected ahead of + // the pane, whose removed() handler then sees the keys already gone. + connect(addr, &AddressItem::remove, container, [container, addr] { + container->removeKeys(addr->keys()); + }); ui->rightPane->addWidget(addr); + if(!code.isEmpty()) + people.insert(code, addr); connect(addr, &AddressItem::decrypt, container, [container, key, addr, this] { - if(!key.isSupported()) { + // A person's row can hold several locks; the dialog offers only those it can open + if(std::ranges::none_of(addr->keys(), &CKey::isSupported)) { WarningDialog::create(this) ->withTitle(CryptoDoc::tr("Failed to decrypt document")) ->withText(QCoreApplication::translate("AddressItem", "Unsupported cryptographic algorithm or recipient type")) @@ -298,16 +319,19 @@ void ContainerPage::transition(CryptoDoc *container) } else { std::unique_ptr backend; + libcdoc::Lock lock; { - DecryptionDialog dialog(container, key, addr->name(), this); + DecryptionDialog dialog(container, addr->keys(), addr->name(), this); if(!dialog.exec()) return; backend = dialog.takeBackend(); + if(const CKey *selected = dialog.selectedKey()) + lock = selected->lock; } - // A card's login is handed over; Mobile-ID / Smart-ID has already decrypted in the - // dialog, which shows its control codes meanwhile + // A card's login is handed over with the lock its card opens; Mobile-ID / + // Smart-ID has already decrypted in the dialog, which shows its control codes if(backend) - decrypt(container, key.lock, {}, std::move(backend)); + decrypt(container, lock, {}, std::move(backend)); else { transition(container); emit action(DecryptContainerSuccess); @@ -315,10 +339,6 @@ void ContainerPage::transition(CryptoDoc *container) } }); } - if(hasUnsupported) - emit warning({WarningText::UnsupportedCDocWarning}); - ui->leftPane->setModel(container->documentModel()); - updatePanes(container->state(), container); } void ContainerPage::transition(DigiDoc* container) diff --git a/client/widgets/ContainerPage.h b/client/widgets/ContainerPage.h index a65389ff3..33e0ebd7c 100644 --- a/client/widgets/ContainerPage.h +++ b/client/widgets/ContainerPage.h @@ -60,6 +60,7 @@ class ContainerPage final : public QWidget template bool deleteConfirm(C *c, int index); void elideFileName(); + void showRecipients(CryptoDoc *container); void encrypt(CryptoDoc *container); bool eventFilter(QObject *o, QEvent *e) final; bool isPasswordEncryption() const;