diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 196660798..5aea818aa 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -18,15 +18,21 @@ */ #include +#include +#include #include #include +#include #include #include +#include +#include #include #include #include #include #include +#include #include #include "Application.h" @@ -200,10 +206,63 @@ checkConnection() }); } +static QString +trimUrl(QString url) +{ + while (url.endsWith(QLatin1Char('/'))) + url.chop(1); + return url; +} + +// Mobile-ID / Smart-ID authentication (AUTH) or relying party (RP) server, unless +// overridden in the user settings from the CDOC2-CONF entry whose SHARES include +// all of the lock's share servers. Another entry's servers would not authorize these +// share servers, and would learn the user's identity only to fail after the login. +static std::string +shareServer(QLatin1String key, QLatin1String field, const std::string &shareUrls) +{ + if (QString url = QSettings().value(key).toString(); !url.isEmpty()) + return url.toStdString(); + QStringList lockUrls; + for (const QString &share : QString::fromStdString(shareUrls).split(QLatin1Char(';'), Qt::SkipEmptyParts)) + lockUrls.append(trimUrl(share.section(QLatin1Char(','), 0, 0))); + QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); + for (const QJsonValue &value : list) { + QJsonObject data = value.toObject(); + QStringList shares; + for (const QJsonValue &url : data.value(QLatin1String("SHARES")).toArray()) + shares.append(trimUrl(url.toString())); + if (!lockUrls.isEmpty() && std::all_of(lockUrls.cbegin(), lockUrls.cend(), + [&shares](const QString &url) { return shares.contains(url); })) + return data.value(field).toString().toStdString(); + } + return {}; +} + +bool +DDConfiguration::shareServersKnown(const std::string &shareUrls) +{ + return !shareServer(QLatin1String("CDOC2-SHARES-AUTH"), QLatin1String("AUTH"), shareUrls).empty() && + !shareServer(QLatin1String("CDOC2-SHARES-RP"), QLatin1String("RP"), shareUrls).empty(); +} + std::string DDConfiguration::getValue(std::string_view domain, std::string_view param) const { - if (param == libcdoc::Configuration::KEYSERVER_SEND_URL) { + if (param == libcdoc::Configuration::AUTH_SERVER) { + return shareServer(QLatin1String("CDOC2-SHARES-AUTH"), QLatin1String("AUTH"), shareUrls); + } else if (param == libcdoc::Configuration::RP_SERVER) { + return shareServer(QLatin1String("CDOC2-SHARES-RP"), QLatin1String("RP"), shareUrls); + } else if (param == libcdoc::Configuration::SHARE_SIGNER) { + return shareSigner; + } else if (param == libcdoc::Configuration::PHONE_NUMBER) { + return phone; + } else if (param == libcdoc::Configuration::SHARE_SERVER_URLS) { + QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); + QJsonObject data = list.value(QLatin1String(domain.data(), domain.size())).toObject(); + if (QJsonArray shares = data.value(QLatin1String("SHARES")).toArray(); !shares.isEmpty()) + return QJsonDocument(shares).toJson(QJsonDocument::Compact).toStdString(); + } else if (param == libcdoc::Configuration::KEYSERVER_SEND_URL) { #ifdef CONFIG_URL QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); QJsonObject data = list.value(QLatin1String(domain.data(), domain.size())).toObject(); @@ -258,6 +317,19 @@ DDNetworkBackend::getLastErrorStr(libcdoc::result_t code) const return libcdoc::NetworkBackend::getLastErrorStr(code); } +static std::string +replyError(QNetworkReply *reply) +{ + QString error = reply->errorString(); + QJsonObject problem = QJsonDocument::fromJson(reply->readAll()).object(); + QString detail = problem.value(QLatin1String("errorMessage")).toString(); + if (detail.isEmpty()) + detail = problem.value(QLatin1String("errorCode")).toString(); + if (!detail.isEmpty()) + error += QLatin1String(": ") + detail; + return error.toStdString(); +} + libcdoc::result_t DDNetworkBackend::sendKey( libcdoc::NetworkBackend::CapsuleInfo &dst, const std::string &url, const std::vector &rcpt_key, @@ -284,12 +356,12 @@ libcdoc::result_t DDNetworkBackend::sendKey( e.exec(); QNetworkReply::NetworkError n_err = reply->error(); if (n_err != QNetworkReply::NoError) { - last_error = reply->errorString().toStdString(); + last_error = replyError(reply); return BACKEND_ERROR; } int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (status != 201) { - last_error = reply->errorString().toStdString(); + last_error = replyError(reply); return BACKEND_ERROR; } QString tr_id; @@ -335,7 +407,7 @@ DDNetworkBackend::fetchKey(std::vector &result, const std::string &url, e.exec(); if(reply->error() != QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 201) { - last_error = reply->errorString().toStdString(); + last_error = replyError(reply); return BACKEND_ERROR; } QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); @@ -345,6 +417,186 @@ DDNetworkBackend::fetchKey(std::vector &result, const std::string &url, return libcdoc::OK; } +libcdoc::result_t +DDNetworkBackend::get(const std::string &url, std::vector &body, + std::map &headers, bool client_cert) +{ + return request(false, url, body, headers, client_cert); +} + +libcdoc::result_t +DDNetworkBackend::post(const std::string &url, std::vector &body, + std::map &headers, bool client_cert) +{ + return request(true, url, body, headers, client_cert); +} + +libcdoc::result_t +DDNetworkBackend::request(bool post, const std::string &url, std::vector &body, + std::map &headers, bool client_cert) +{ + if (client_cert) { + // Only the key server authenticates with the card, and fetchKey does that itself + last_error = "Client certificate is not supported"; + return libcdoc::NOT_IMPLEMENTED; + } + if (!checkConnection()) { + last_error = "No connection"; + return BACKEND_ERROR; + } + return send(post, QUrl(QString::fromStdString(url)), body, headers); +} + +libcdoc::result_t +DDNetworkBackend::send(bool post, const QUrl &url, std::vector &body, + std::map &headers, int timeout) +{ + if (canceled) + return DDCryptoBackend::PIN_CANCELED; + QNetworkRequest req(url); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + for (const auto &[name, value] : headers) + req.setRawHeader(QByteArray::fromStdString(name), QByteArray::fromStdString(value)); + QScopedPointer nam(CheckConnection::setupNAM(req)); + QNetworkReply *reply = post ? nam->post(req, toByteArray(body)) : nam->get(req); + QEventLoop e; + connect(reply, &QNetworkReply::finished, &e, &QEventLoop::quit); + QDeadlineTimer deadline(timeout); + QTimer timer; + connect(&timer, &QTimer::timeout, reply, [&] { + if(canceled || deadline.hasExpired()) + reply->abort(); + }); + timer.start(100); + if(!reply->isFinished()) + e.exec(); + if(canceled) + return DDCryptoBackend::PIN_CANCELED; + if(deadline.hasExpired()) { + last_error = "Timeout waiting for server response"; + return BACKEND_ERROR; + } + int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (reply->error() != QNetworkReply::NoError || status < 200 || status >= 300) { + last_error = replyError(reply); + return BACKEND_ERROR; + } + // Callers look headers up in lower case, as libcdoc's own client stores them + headers.clear(); + for (const auto &[name, value] : reply->rawHeaderPairs()) + headers.emplace(name.toLower().toStdString(), value.toStdString()); + QByteArray data = reply->readAll(); + body.assign(data.cbegin(), data.cend()); + return libcdoc::OK; +} + +// The Mobile-ID / Smart-ID round-trips below follow libcdoc's own, which use its +// built-in HTTP client and so would need the servers' certificates pinned + +libcdoc::result_t +DDNetworkBackend::getAuthResponse(const std::string &url, std::vector &body, + std::map &headers) +{ + if (!checkConnection()) { + last_error = "No connection"; + return BACKEND_ERROR; + } + QUrl start(QString::fromStdString(url)); + QString path = start.path(); + if (path.endsWith(QLatin1Char('/'))) + path.chop(1); + start.setPath(path + QLatin1String("/auth/start")); + if (auto result = send(true, start, body, headers); result != libcdoc::OK) + return result; + // The status is polled on the server the session was started on + QString location = QString::fromStdString(headers["location"]); + if (!location.startsWith(path + QLatin1String("/auth/status/"))) { + last_error = "Unexpected Location header value"; + return BACKEND_ERROR; + } + bool ok = false; + SIDMIDFeedback feedback { .code = QJsonDocument::fromJson(toByteArray(body)).object() + .value(QLatin1String("vc")).toString().toInt(&ok) }; + if (!ok || feedback.code < 0 || feedback.code > 9999) { + last_error = "Invalid verification code in response"; + return BACKEND_ERROR; + } + if (auto result = showFeedback(feedback); result != libcdoc::OK) + return result; + QUrl status = start; + status.setPath(location); + headers.clear(); + return poll(status, QLatin1String("status"), body, headers); +} + +libcdoc::result_t +DDNetworkBackend::getSignResponse(const std::string &url, const std::string &post_path, + std::vector &body, std::map &headers, + const std::string &poll_path_prefix) +{ + if (!checkConnection()) { + last_error = "No connection"; + return BACKEND_ERROR; + } + QUrl sign(QString::fromStdString(url)); + QString path = sign.path(); + if (path.endsWith(QLatin1Char('/'))) + path.chop(1); + sign.setPath(path + QString::fromStdString(post_path)); + // The session token headers go with every poll as well + const std::map request_headers = headers; + if (auto result = send(true, sign, body, headers); result != libcdoc::OK) + return result; + QString session = QJsonDocument::fromJson(toByteArray(body)).object() + .value(QLatin1String("sessionID")).toString(); + if (session.isEmpty()) { + last_error = "No session ID in response"; + return BACKEND_ERROR; + } + QUrl status = sign; + status.setPath(path + QString::fromStdString(poll_path_prefix) + session); + headers = request_headers; + return poll(status, QLatin1String("state"), body, headers); +} + +libcdoc::result_t +DDNetworkBackend::poll(const QUrl &url, QLatin1String field, std::vector &body, + std::map &headers) +{ + const std::map request_headers = headers; + for (QDeadlineTimer deadline(std::chrono::minutes(1)); !deadline.hasExpired();) { + headers = request_headers; + if (auto result = send(false, url, body, headers, + int(std::min(30'000, deadline.remainingTime()))); result != libcdoc::OK) + return result; + QJsonObject json = QJsonDocument::fromJson(toByteArray(body)).object(); + QString state = json.value(field).toString(); + if (state == QLatin1String("COMPLETE")) + return libcdoc::OK; + if (state != QLatin1String("RUNNING") && state != QLatin1String("STARTED")) { + // A session that ended without success says why in endResult (e.g. NOT_MID_CLIENT), + // which the caller turns into its Mobile-ID / Smart-ID error + if (json.contains(QLatin1String("endResult"))) + return libcdoc::OK; + last_error = "Invalid Mobile-ID / Smart-ID state: " + state.toStdString(); + return BACKEND_ERROR; + } + QThread::msleep(500); + } + last_error = "Timeout waiting for Mobile-ID / Smart-ID"; + return BACKEND_ERROR; +} + +libcdoc::result_t +DDNetworkBackend::showFeedback(SIDMIDFeedback &feedback) +{ + if (canceled) + return DDCryptoBackend::PIN_CANCELED; + if (onFeedback) + QMetaObject::invokeMethod(qApp, [callback = onFeedback, code = feedback.code] { callback(code); }); + return libcdoc::OK; +} + static constexpr QLatin1String level2Str(libcdoc::LogLevel level) noexcept { diff --git a/client/CDocSupport.h b/client/CDocSupport.h index d0d0f2b72..80daf7c90 100644 --- a/client/CDocSupport.h +++ b/client/CDocSupport.h @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -32,6 +33,9 @@ #include #include +#include +#include + // // libcdoc handler implementations // @@ -39,12 +43,22 @@ // // Configuration // -// Provides KEYSERVER_SEND_URL and KEYSERVER_FETCH_URL +// Provides KEYSERVER_SEND_URL and KEYSERVER_FETCH_URL, and the Mobile-ID / Smart-ID +// AUTH_SERVER, RP_SERVER and SHARE_SERVER_URLS from the CDOC2-CONF entries // struct DDConfiguration : public libcdoc::Configuration { std::string getValue(std::string_view domain, std::string_view param) const final; + // Whether the authentication and relying party servers for a lock's share + // servers (Lock::SHARE_URLS) are configured, i.e. Mobile-ID / Smart-ID can open it + static bool shareServersKnown(const std::string &shareUrls); + // Mobile-ID / Smart-ID decryption: Configuration::SHARE_SIGNER_MID or + // SHARE_SIGNER_SID, the Mobile-ID phone number, and the lock's share servers + // (Lock::SHARE_URLS) that select the CDOC2-CONF entry + std::string shareSigner; + std::string phone; + std::string shareUrls; explicit DDConfiguration() = default; }; @@ -108,6 +122,17 @@ struct DDNetworkBackend final : public libcdoc::NetworkBackend, private QObject libcdoc::result_t fetchKey(std::vector &result, const std::string &keyserver_id, const std::string &transaction_id) final; + // Key share and Mobile-ID / Smart-ID requests, over Qt for the proxy and TLS setup + libcdoc::result_t get(const std::string &url, std::vector &body, + std::map &headers, bool client_cert) final; + libcdoc::result_t post(const std::string &url, std::vector &body, + std::map &headers, bool client_cert) final; + libcdoc::result_t getAuthResponse(const std::string &url, std::vector &body, + std::map &headers) final; + libcdoc::result_t getSignResponse(const std::string &url, const std::string &post_path, + std::vector &body, std::map &headers, + const std::string &poll_path_prefix) final; + libcdoc::result_t showFeedback(SIDMIDFeedback &feedback) final; libcdoc::result_t getClientTLSCertificate(std::vector &dst) final { @@ -122,6 +147,18 @@ struct DDNetworkBackend final : public libcdoc::NetworkBackend, private QObject DDCryptoBackend &crypto; std::string last_error; + // Gets each Mobile-ID / Smart-ID verification code, on the main thread + std::function onFeedback; + // Set from the main thread to end a Mobile-ID / Smart-ID session + std::atomic_bool canceled = false; + +private: + libcdoc::result_t request(bool post, const std::string &url, std::vector &body, + std::map &headers, bool client_cert); + libcdoc::result_t send(bool post, const QUrl &url, std::vector &body, + std::map &headers, int timeout = 30'000); + libcdoc::result_t poll(const QUrl &url, QLatin1String field, std::vector &body, + std::map &headers); }; // diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index ccfb16f67..7e3eafa6e 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -129,6 +129,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE BUILD_DATE="${BUILD_DATE}" VERSION_NUMBER=${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH},${BUILD_NUMBER} VERSION_STR="${VERSION}" + HAS_KEYSHARES ) if( APPLE ) diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index 2f3989a4c..a5ddaf99c 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -70,6 +70,9 @@ CKey::canDecrypt(const QSslCertificate &cert) const bool CKey::isSupported() const { + // libcdoc writes no label for a Mobile-ID / Smart-ID key share, which isValid() requires + if(lock.type == libcdoc::Lock::SHARE_SERVER) + return true; if(!lock.isCDoc1()) return lock.isValid(); if(lock.encrypted_fmk.empty()) @@ -292,6 +295,47 @@ ContainerState CryptoDoc::state() const return d->isEncrypted() ? EncryptedContainer : UnencryptedContainer; } +// Mobile-ID / Smart-ID results, worded and translated as when signing +struct MIDSIDResult +{ + Q_DECLARE_TR_FUNCTIONS(MobileProgress) +public: + static QString text(libcdoc::result_t result, bool mid) + { + switch (result) { + case libcdoc::NetworkBackend::MIDSID_NOT_MID_CLIENT: + return tr("User is not a mobile-ID client"); + case libcdoc::NetworkBackend::MIDSID_USER_CANCELLED: + case libcdoc::NetworkBackend::MIDSID_USER_REFUSED: + case libcdoc::NetworkBackend::MIDSID_USER_REFUSED_CERT_CHOICE: + case libcdoc::NetworkBackend::MIDSID_USER_REFUSED_INTERACTION: + return tr("User denied or cancelled"); + case libcdoc::NetworkBackend::MIDSID_TIMEOUT: + return mid ? tr("Your mobile-ID transaction has expired. Please try again.") : + tr("Your Smart-ID transaction has expired or user account not found."); + case libcdoc::NetworkBackend::MIDSID_PHONE_ABSENT: + return tr("Phone is not in coverage area"); + case libcdoc::NetworkBackend::MIDSID_DELIVERY_ERROR: + return tr("Request sending error"); + case libcdoc::NetworkBackend::MIDSID_SIM_ERROR: + return tr("SIM error"); + case libcdoc::NetworkBackend::MIDSID_SIGNATURE_HASH_MISMATCH: + return tr("Your mobile-ID transaction has failed. Please contact your mobile network operator."); + case libcdoc::NetworkBackend::MIDSID_WRONG_VC: + return tr("Error: an incorrect control code was chosen"); + case libcdoc::NetworkBackend::MIDSID_DOCUMENT_UNUSABLE: + case libcdoc::NetworkBackend::MIDSID_REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP: + case libcdoc::NetworkBackend::MIDSID_PROTOCOL_FAILURE: + case libcdoc::NetworkBackend::MIDSID_EXPECTED_LINKED_SESSION: + case libcdoc::NetworkBackend::MIDSID_SERVER_ERROR: + case libcdoc::NetworkBackend::ACCOUNT_UNUSABLE: + return tr("Your Smart-ID transaction has failed. Please check your Smart-ID application or contact Smart-ID customer support."); + default: + return {}; + } + } +}; + bool CryptoDoc::decrypt(const libcdoc::Lock &lock, const QByteArray& secret, std::unique_ptr &&backend) { if(!d->reader) @@ -315,6 +359,7 @@ bool CryptoDoc::decrypt(const libcdoc::Lock &lock, const QByteArray& secret, std } auto lock_idx = unsigned(std::distance(locks.cbegin(), it)); + d->network.canceled = false; d->crypto.setBackend(std::move(backend)); auto resetBackend = qScopeGuard([this] { d->crypto.setBackend({}); @@ -365,7 +410,9 @@ bool CryptoDoc::decrypt(const libcdoc::Lock &lock, const QByteArray& secret, std str = QCryptoBackend::errorString(QCryptoBackend::Status::InProgress); break; default: - str = tr("Please check your internet connection and network settings."); + str = MIDSIDResult::text(result, d->conf.shareSigner == libcdoc::Configuration::SHARE_SIGNER_MID); + if (str.isEmpty()) + str = tr("Please check your internet connection and network settings."); break; } WarningDialog::create() @@ -389,6 +436,26 @@ bool CryptoDoc::decrypt(const libcdoc::Lock &lock, const QByteArray& secret, std return !d->isEncrypted(); } +bool CryptoDoc::decrypt(const libcdoc::Lock &lock, const QString &phone, std::function onFeedback) +{ + d->conf.shareSigner = phone.isEmpty() ? libcdoc::Configuration::SHARE_SIGNER_SID : libcdoc::Configuration::SHARE_SIGNER_MID; + d->conf.phone = phone.isEmpty() ? std::string() : '+' + phone.toStdString(); + d->conf.shareUrls = lock.getString(libcdoc::Lock::SHARE_URLS); + d->network.onFeedback = std::move(onFeedback); + auto reset = qScopeGuard([this] { + d->conf.shareSigner.clear(); + d->conf.phone.clear(); + d->conf.shareUrls.clear(); + d->network.onFeedback = {}; + }); + return decrypt(lock, QByteArray(), std::unique_ptr()); +} + +void CryptoDoc::cancel() +{ + d->network.canceled = true; +} + DocumentModel *CryptoDoc::documentModel() const { return d->documents; } bool CryptoDoc::encrypt(const QString &filename, const QString& label, const QByteArray& secret) diff --git a/client/CryptoDoc.h b/client/CryptoDoc.h index 2cb251d63..20c24f71e 100644 --- a/client/CryptoDoc.h +++ b/client/CryptoDoc.h @@ -25,6 +25,7 @@ #include #include +#include #include #include @@ -47,6 +48,8 @@ struct CKey { libcdoc::Lock lock; QSslCertificate rcpt_cert; + // True when cert is the one this lock is addressed to, i.e. the card + // holding its private key is the one that can open the container. bool canDecrypt(const QSslCertificate &cert) const; bool isSupported() const; bool operator== (const CKey& rhs) const; @@ -63,6 +66,11 @@ class CryptoDoc final: public QObject bool addEncryptionKey(const CKey& key); void clear(const QString &file = {}, int version = -1); bool decrypt(const libcdoc::Lock &lock, const QByteArray& secret, std::unique_ptr &&backend); + // Mobile-ID with phone, Smart-ID without, for a key share lock; onFeedback gets + // each verification code to show while the user confirms on their device + bool decrypt(const libcdoc::Lock &lock, const QString &phone, std::function onFeedback); + // Ends a Mobile-ID / Smart-ID decryption in progress + void cancel(); bool encrypt(const QString &filename = {}, const QString& label = {}, const QByteArray& secret = {}); DocumentModel* documentModel() const; QString fileName() const; diff --git a/client/SslCertificate.cpp b/client/SslCertificate.cpp index 8472cd63d..3e437af59 100644 --- a/client/SslCertificate.cpp +++ b/client/SslCertificate.cpp @@ -216,15 +216,19 @@ QHash SslCertificate::keyUsage() const } QString SslCertificate::personalCode() const +{ + if(QString code = personalCode(subjectInfo(QSslCertificate::SerialNumber)); !code.isEmpty()) + return code; + return QString(serialNumber()).remove(':'); +} + +QString SslCertificate::personalCode(const QString &data) { // https://www.etsi.org/deliver/etsi_en/319400_319499/31941201/01.01.01_60/en_31941201v010101p.pdf static const QStringList types {"PAS", "IDC", "PNO", "TAX", "TIN"}; - QString data = subjectInfo(QSslCertificate::SerialNumber); if(data.size() > 6 && (types.contains(data.left(3)) || data[2] == ':') && data[5] == '-') return data.mid(6); - if(!data.isEmpty()) - return data; - return QString(serialNumber()).remove(':'); + return data; } QStringList SslCertificate::policies() const diff --git a/client/SslCertificate.h b/client/SslCertificate.h index 6b144b36b..fae483cb5 100644 --- a/client/SslCertificate.h +++ b/client/SslCertificate.h @@ -101,6 +101,7 @@ class SslCertificate: public QSslCertificate QString keyName() const; QHash keyUsage() const; QString personalCode() const; + static QString personalCode(const QString &serialNumber); QStringList policies() const; bool showCN() const; QString signatureAlgorithm() const; diff --git a/client/dialogs/DecryptionDialog.cpp b/client/dialogs/DecryptionDialog.cpp index 088365560..742d3d3aa 100644 --- a/client/dialogs/DecryptionDialog.cpp +++ b/client/dialogs/DecryptionDialog.cpp @@ -5,7 +5,9 @@ #include "ui_DecryptionDialog.h" #include "Application.h" +#include "CDocSupport.h" #include "CryptoDoc.h" +#include "IKValidator.h" #include "QCryptoBackend.h" #include "SslCertificate.h" #include "dialogs/PinLogin.h" @@ -14,11 +16,13 @@ #include "widgets/CardListItem.h" #include +#include #include -DecryptionDialog::DecryptionDialog(const CKey &key, const QString &recipient, QWidget *parent) +DecryptionDialog::DecryptionDialog(CryptoDoc *doc, const CKey &key, const QString &recipient, QWidget *parent) : QDialog(parent) + , cryptoDoc(doc) , m_overlay(std::make_unique(this)) , m_key(key) , ui(new Ui::DecryptionDialog) @@ -33,6 +37,26 @@ DecryptionDialog::DecryptionDialog(const CKey &key, const QString &recipient, QW ui->recipientIcon->load(QStringLiteral(":/images/icon_Minu_eID_hover.svg")); ui->recipientName->setText(recipient); + ui->tabs->addTab({}); + ui->tabs->addTab({}); + 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; + // 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); + 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); + ui->tabStack->setCurrentIndex(ui->tabs->currentIndex()); + + // ID-card tab ui->noCardIcon->load(QStringLiteral(":/images/icon_IDkaart_red.svg")); updateCards(); connect(qApp->cryptoManager(), &QCryptoManager::cacheChanged, this, &DecryptionDialog::updateCards); @@ -42,11 +66,25 @@ DecryptionDialog::DecryptionDialog(const CKey &key, const QString &recipient, QW }); connect(ui->pin, &QLineEdit::returnPressed, ui->decrypt, &QPushButton::click); + // Mobile-ID and Smart-ID tabs: the lock names the recipient, shown in the header, + // so only the Mobile-ID phone number is asked for + ui->phoneNo->setValidator(new NumberValidator(ui->phoneNo)); + ui->phoneNo->setAttribute(Qt::WA_MacShowFocusRect, false); + ui->errorPhone->hide(); + connect(ui->phoneNo, &QLineEdit::textEdited, this, &DecryptionDialog::updateShareButton); + connect(ui->phoneNo, &QLineEdit::returnPressed, ui->decrypt, &QPushButton::click); + connect(ui->decrypt, &QPushButton::clicked, this, [this] { - if(performLogin()) + if(currentMethod() == IDCard ? performLogin() : performShareDecrypt()) accept(); }); + connect(ui->progressCancel, &QPushButton::clicked, this, [this] { + ui->progressCancel->setEnabled(false); + cryptoDoc->cancel(); + }); connect(ui->cancel, &QPushButton::clicked, this, &QDialog::reject); + if(share) + updateShareButton(); connect(this, &QDialog::finished, this, &QDialog::close); } @@ -60,21 +98,54 @@ std::unique_ptr DecryptionDialog::takeBackend() return std::move(m_backend); } +DecryptionDialog::Method DecryptionDialog::currentMethod() const +{ + return static_cast(ui->tabs->currentIndex()); +} + void DecryptionDialog::changeEvent(QEvent *event) { if(event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); + updateTabs(); updateNoCard(); updatePinControls(); + if(currentMethod() != IDCard) + updateProgressInfo(); } QDialog::changeEvent(event); } void DecryptionDialog::reject() { - if(!ui->progressPage->isVisible()) - QDialog::reject(); + // A PIN pad entry is shown without Cancel and runs on the reader until it ends there + if(ui->progressPage->isVisible()) + { + if(ui->progressCancel->isVisible() && ui->progressCancel->isEnabled()) + ui->progressCancel->click(); + return; + } + QDialog::reject(); +} + +void DecryptionDialog::updateTabs() +{ + ui->tabs->setTabText(IDCard, tr("ID-card")); + ui->tabs->setTabText(MobileID, tr("Mobile-ID")); + ui->tabs->setTabText(SmartID, tr("Smart-ID")); +} + +void DecryptionDialog::updateProgressInfo() +{ + if(currentMethod() == MobileID) + ui->progressInfo->setText(m_confirmDecrypt ? + tr("Confirm decryption: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1.") : + tr("Log in: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1.")); + else + ui->progressInfo->setText(m_confirmDecrypt ? + tr("Confirm decryption: make sure the control code matches the one in your Smart-ID app and enter your PIN1.") : + tr("Log in: make sure the control code matches the one in your Smart-ID app and enter your PIN1.")); } void DecryptionDialog::updateNoCard() @@ -142,13 +213,15 @@ void DecryptionDialog::updateCards() void DecryptionDialog::updateSelection() { - PinLogin::updateSelection(ui, m_selectedToken); + PinLogin::updateSelection(ui, m_selectedToken, currentMethod() == IDCard); updatePinControls(); } void DecryptionDialog::updatePinControls() { - ui->decrypt->setEnabled(PinLogin::update(ui, m_selectedToken)); + const bool ready = PinLogin::update(ui, m_selectedToken); + if(currentMethod() == IDCard) + ui->decrypt->setEnabled(ready); } bool DecryptionDialog::performLogin() @@ -172,3 +245,52 @@ bool DecryptionDialog::performLogin() m_backend.reset(val.value()); return true; } + +void DecryptionDialog::updateShareButton() +{ + static const QStringList countryCodes {QStringLiteral("372"), QStringLiteral("370")}; + const QString phone = ui->phoneNo->text(); + ui->decrypt->setEnabled(m_sharesKnown && (currentMethod() == SmartID || + (phone.size() >= 8 && countryCodes.contains(phone.left(3))))); +} + +bool DecryptionDialog::performShareDecrypt() +{ + auto showPage = [this](QWidget *page) { + ui->formPage->setVisible(page == ui->formPage); + ui->progressPage->setVisible(page == ui->progressPage); + }; + ui->progressCode->clear(); + m_confirmDecrypt = false; + updateProgressInfo(); + ui->progressBar->setValue(0); + ui->progressCancel->setEnabled(true); + showPage(ui->progressPage); + + // Each confirmation on the device gets its own code and its own time to answer + QPointer timer = new QTimer(this); + timer->setInterval(1000); + connect(timer, &QTimer::timeout, this, [this] { + ui->progressBar->setValue(std::min(ui->progressBar->value() + 1, ui->progressBar->maximum())); + }); + QPointer self(this); + auto feedback = [self, timer](int code) { + if(!self || !timer) + return; + if(!self->ui->progressCode->text().isEmpty()) { + self->m_confirmDecrypt = true; + self->updateProgressInfo(); + } + self->ui->progressCode->setText(QStringLiteral("%1").arg(code, 4, 10, QLatin1Char('0'))); + self->ui->progressBar->setValue(0); + timer->start(); + }; + bool result = cryptoDoc->decrypt(m_key.lock, + currentMethod() == MobileID ? ui->phoneNo->text() : QString(), feedback); + if(!self) + return result; + timer->deleteLater(); + if(!result) + showPage(ui->formPage); + return result; +} diff --git a/client/dialogs/DecryptionDialog.h b/client/dialogs/DecryptionDialog.h index 961d10d3f..6870aee83 100644 --- a/client/dialogs/DecryptionDialog.h +++ b/client/dialogs/DecryptionDialog.h @@ -14,7 +14,9 @@ class DecryptionDialog final : public QDialog { Q_OBJECT public: - explicit DecryptionDialog(const CKey &key, const QString &recipient, + enum Method { IDCard, MobileID, SmartID }; + + explicit DecryptionDialog(CryptoDoc *doc, const CKey &key, const QString &recipient, QWidget *parent = nullptr); ~DecryptionDialog() final; std::unique_ptr takeBackend(); @@ -26,11 +28,20 @@ class DecryptionDialog final : public QDialog void updateSelection(); void updatePinControls(); void updateNoCard(); + void updateProgressInfo(); + void updateTabs(); + void updateShareButton(); bool performLogin(); + bool performShareDecrypt(); + Method currentMethod() const; + CryptoDoc *cryptoDoc; std::unique_ptr m_backend; std::unique_ptr m_overlay; CKey m_key; Ui::DecryptionDialog *ui; TokenData m_selectedToken; + bool m_confirmDecrypt = false; + // The lock's share servers have known authentication and relying party servers + bool m_sharesKnown = true; }; diff --git a/client/dialogs/DecryptionDialog.ui b/client/dialogs/DecryptionDialog.ui index 532a74522..eb869a5a9 100644 --- a/client/dialogs/DecryptionDialog.ui +++ b/client/dialogs/DecryptionDialog.ui @@ -56,6 +56,9 @@ QProgressBar::chunk { border-radius: 3px; background-color: #2F70B6; } +#errorPhone, #errorShares { +color: #AD2A45; +} QLineEdit { padding: 10px 14px; border: 1px solid #C4CBD8; @@ -64,7 +67,7 @@ background-color: #FFFFFF; placeholder-text-color: #607496; font-size: 16px; } -QLineEdit[label="error"] { +QLineEdit[label="error"] { border-color: #BE7884; } QPushButton { @@ -97,7 +100,31 @@ QPushButton:default:disabled { border-color: #82A9D3; background-color: #82A9D3; } - +QTabBar::tab { +padding: 11px 19px; +color: #003168; +border: none; +font-weight: 700; +background-color: #FFFFFF; +border: 1px solid #003168; +border-width: 1px 0px 1px 1px; +} +QTabBar::tab:first { +border-top-left-radius: 4px; +border-bottom-left-radius: 4px; +} +QTabBar::tab:last { +border-top-right-radius: 4px; +border-bottom-right-radius: 4px; +border-right-width: 1px; +} +QTabBar::tab:selected { +background-color: #003168; +color: #FFFFFF; +} +QTabBar::tab:hover:!selected { +background-color: #EDF1F6; +} @@ -193,49 +220,61 @@ background-color: #82A9D3; - - - - 12 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - 0 - 20 - - - - Select the certificate to decrypt with - - - true - - - - - - - - 0 - 0 - - - - #cardList, #cardList > QWidget, #cardListContainer { + + + 16 + + + + + + + + 0 + + + + + 12 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 20 + + + + Select the certificate to decrypt with + + + true + + + + + + + + 0 + 0 + + + + #cardList, #cardList > QWidget, #cardListContainer { background-color: transparent; } QScrollBar { @@ -264,143 +303,240 @@ QScrollBar::add-page, QScrollBar::sub-page { background: transparent; } - - - QFrame::NoFrame - - - Qt::ScrollBarAlwaysOff - - - true - - - - - 8 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - - QLabel { + + + QFrame::NoFrame + + + Qt::ScrollBarAlwaysOff + + + true + + + + + 8 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + QLabel { color: #07142A; font-family: Roboto, Helvetica; font-size: 14px; font-weight: 400; } - - + + + + 8 + + + 8 + + + 0 + + + 8 + + + 0 + + + + + + 24 + 24 + + + + + 24 + 24 + + + + + + + + Qt::TabFocus + + + No card in reader + + + + + + + + + + Qt::TabFocus + + + Qt::PlainText + + + true + + + + + + + 6 + + + + + + 0 + 20 + + + + pin + + + + + + + + + + + 0 + 20 + + + + Qt::TabFocus + + + + + + + + + - 8 + 24 - 8 + 0 0 - 8 + 0 0 - - - - 24 - 24 - - - - - 24 - 24 - - - - - - - - Qt::TabFocus - - - No card in reader + + + 6 - + + + + + 0 + 20 + + + + Country code and phone number + + + phoneNo + + + + + + + 37254321 + + + + + + + + 0 + 20 + + + + Qt::TabFocus + + + + - - - - - Qt::TabFocus - - - Qt::PlainText - - - true - + + + + 24 + + + 0 + + + 0 + + + 0 + + + 0 + + - - - - - 6 - - - - - - 0 - 20 - - - - pin - - - - - - - - - - - 0 - 20 - - - - Qt::TabFocus - - - - - - - + + + + + + Qt::TabFocus + + + The key share servers of this container are not configured + + + true + + + + @@ -478,7 +614,7 @@ font-weight: 400; - + Control code: Qt::AlignCenter @@ -517,7 +653,7 @@ font-weight: 400; - 30 + 75 0 @@ -552,6 +688,12 @@ font-weight: 400;
widgets/CardList.h
1 + + ShrinkingStack + QStackedWidget +
widgets/ShrinkingStack.h
+ 1 +
LineEdit QLineEdit @@ -568,7 +710,12 @@ font-weight: 400;
QSvgWidget
1
+ + QTabBar + QWidget +
qtabbar.h
+
- - + + diff --git a/client/dialogs/KeyDialog.cpp b/client/dialogs/KeyDialog.cpp index ba5b7c45a..0b2d687e1 100644 --- a/client/dialogs/KeyDialog.cpp +++ b/client/dialogs/KeyDialog.cpp @@ -48,7 +48,9 @@ KeyDialog::KeyDialog(const CKey &k, QWidget *parent ) view->addTopLevelItem(i); }; - addItem(tr("Recipient"), QString::fromStdString(k.lock.label)); + // A key share recipient has no label before it is encrypted for + addItem(tr("Recipient"), QString::fromStdString(k.lock.label.empty() ? + k.lock.getString(libcdoc::Lock::RECIPIENT_ID) : k.lock.label)); addItem(tr("Lock type"), [type = k.lock.type] { switch(type) { using enum libcdoc::Lock::Type; @@ -57,9 +59,7 @@ KeyDialog::KeyDialog(const CKey &k, QWidget *parent ) case PUBLIC_KEY: return QStringLiteral("PUBLIC_KEY"); case CDOC1: return QStringLiteral("CDOC1"); case SERVER: return QStringLiteral("SERVER"); -#if 0 case SHARE_SERVER: return QStringLiteral("SHARE_SERVER"); -#endif case UNKNOWN: default: return QStringLiteral("UNKNOWN"); } diff --git a/client/libcdoc b/client/libcdoc index 4953672fa..fa18414e7 160000 --- a/client/libcdoc +++ b/client/libcdoc @@ -1 +1 @@ -Subproject commit 4953672fa3bd63a54ef16323d6ecbd2b351e6373 +Subproject commit fa18414e730bcb662892daed6a1cbdf459a3dfcc diff --git a/client/translations/en.ts b/client/translations/en.ts index 5cf5c83f5..b30caa76c 100644 --- a/client/translations/en.ts +++ b/client/translations/en.ts @@ -130,6 +130,10 @@ Unsupported cryptographic algorithm or recipient type Unsupported cryptographic algorithm or recipient type + + Mobile-ID / Smart-ID + Mobile-ID / Smart-ID + pw Password @@ -528,6 +532,10 @@ It is removed along with the container. It is removed along with the container. + + Failed to decrypt document + Failed to decrypt document + In order to view Validity Confirmation Sheet there has to be at least one printer installed! In order to view Validity Confirmation Sheet there has to be at least one printer installed! @@ -678,6 +686,18 @@ Select the certificate to decrypt with Select the certificate to decrypt with + + No card in reader + No card in reader + + + Country code and phone number + Country code and phone number + + + The key share servers of this container are not configured + The key share servers of this container are not configured + Cancel Cancel @@ -686,6 +706,38 @@ Decrypt Decrypt + + Control code: + Control code: + + + ID-card + ID-card + + + Mobile-ID + Mobile-ID + + + Smart-ID + Smart-ID + + + Confirm decryption: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + Confirm decryption: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + + + Log in: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + Log in: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + + + Confirm decryption: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + Confirm decryption: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + + + Log in: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + Log in: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + Failed to decrypt document Failed to decrypt document @@ -1859,6 +1911,21 @@ LDAP server is unavailable. %1 has been entered incorrectly at least once + + PinPopup + + You need to use a personal identification certificate for the selected activity. + You need to use a personal identification certificate for the selected activity. + + + %1 will be locked next failed attempt + %1 will be locked next failed attempt + + + %1 has been entered incorrectly at least once + %1 has been entered incorrectly at least once + + PinUnblock @@ -1998,6 +2065,17 @@ LDAP server is unavailable. Change + + PinpadLogin + + After you continue, enter %1 code on the card reader keypad + After you continue, enter %1 code on the card reader keypad + + + Enter %1 code on the card reader keypad + Enter %1 code on the card reader keypad + + PinpadPopup @@ -2764,6 +2842,10 @@ Additional licenses and components Choose signing method Choose signing method + + No card in reader + + Country code and phone number Country code and phone number diff --git a/client/translations/et.ts b/client/translations/et.ts index 2cba366a8..64aef3669 100644 --- a/client/translations/et.ts +++ b/client/translations/et.ts @@ -130,6 +130,10 @@ Unsupported cryptographic algorithm or recipient type Mittetoetatud krüptograafiline algoritm või adressaadi tüüp + + Mobile-ID / Smart-ID + Mobiil-ID / Smart-ID + pw Parool @@ -528,6 +532,10 @@ It is removed along with the container. Koos sellega eemaldatakse ka ümbrik. + + Failed to decrypt document + Dokumendi dekrüpteerimine ebaõnnestus + In order to view Validity Confirmation Sheet there has to be at least one printer installed! Digitaalallkirjade kinnituslehe kuvamiseks peab olema arvutis vähemalt üks printer seadistatud! @@ -678,6 +686,18 @@ Select the certificate to decrypt with Vali sertifikaat, millega dekrüpteerida + + No card in reader + Lugejas ei ole kaarti; kontrolli, kas ID-kaart on õiget pidi lugejas + + + Country code and phone number + Riigikood ja telefoninumber + + + The key share servers of this container are not configured + Selle ümbriku võtmeosade serverid ei ole seadistatud + Cancel Katkesta @@ -686,6 +706,38 @@ Decrypt Dekrüpteeri + + Control code: + Kontrollkood: + + + ID-card + ID-kaart + + + Mobile-ID + Mobiil-ID + + + Smart-ID + Smart-ID + + + Confirm decryption: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + Kinnita dekrüpteerimine: veendu kontrollkoodi õigsuses ja sisesta telefonil mobiil-ID PIN1-kood. + + + Log in: make sure the control code matches the one on your phone screen and enter your Mobile-ID PIN1. + Logi sisse: veendu kontrollkoodi õigsuses ja sisesta telefonil mobiil-ID PIN1-kood. + + + Confirm decryption: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + Kinnita dekrüpteerimine: veendu kontrollkoodi õigsuses ja sisesta nutiseadmes Smart-ID PIN1-kood. + + + Log in: make sure the control code matches the one in your Smart-ID app and enter your PIN1. + Logi sisse: veendu kontrollkoodi õigsuses ja sisesta nutiseadmes Smart-ID PIN1-kood. + Failed to decrypt document Dokumendi dekrüpteerimine ebaõnnestus @@ -1859,6 +1911,21 @@ LDAP serveriga ei saa ühendust. %1 on vähemalt ühe korra valesti sisestatud + + PinPopup + + You need to use a personal identification certificate for the selected activity. + Valitud tegevuse jaoks on vaja kasutada isikutuvastuse sertifikaati. + + + %1 will be locked next failed attempt + Vale koodi sisestamisel %1 lukustub + + + %1 has been entered incorrectly at least once + %1 on vähemalt ühe korra valesti sisestatud + + PinUnblock @@ -1998,6 +2065,17 @@ LDAP serveriga ei saa ühendust. Muuda + + PinpadLogin + + After you continue, enter %1 code on the card reader keypad + Pärast jätkamist sisesta %1-kood kaardilugeja sõrmistikult + + + Enter %1 code on the card reader keypad + Sisesta %1-kood kaardilugeja sõrmistikult + + PinpadPopup @@ -2764,6 +2842,10 @@ Täiendavad litsentsid ja komponendid Choose signing method Vali allkirjastamise meetod + + No card in reader + Lugejas ei ole kaarti; kontrolli, kas ID-kaart on õiget pidi lugejas + Country code and phone number Riigikood ja telefoninumber diff --git a/client/widgets/AddressItem.cpp b/client/widgets/AddressItem.cpp index e26635575..47a66bf1e 100644 --- a/client/widgets/AddressItem.cpp +++ b/client/widgets/AddressItem.cpp @@ -57,6 +57,10 @@ AddressItem::AddressItem(const CKey &key, Type type, QWidget *parent) ui->key.rcpt_cert.subjectInfo("SN").join(' ') : ui->key.rcpt_cert.subjectInfo("CN").join(' '); 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(); @@ -141,7 +145,7 @@ QString AddressItem::name() const void AddressItem::setDecryptVisible() { ui->decrypt->setVisible(ui->state == EncryptedContainer && (ui->key.lock.isPKI() || - ui->key.lock.type == libcdoc::Lock::PASSWORD)); + ui->key.lock.type == libcdoc::Lock::PASSWORD || ui->key.lock.type == libcdoc::Lock::SHARE_SERVER)); } void AddressItem::setName() @@ -190,6 +194,8 @@ void AddressItem::setIdType() { 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 // Needed to include translation for "ID-CARD" diff --git a/client/widgets/ContainerPage.cpp b/client/widgets/ContainerPage.cpp index f183712df..9c0901df7 100644 --- a/client/widgets/ContainerPage.cpp +++ b/client/widgets/ContainerPage.cpp @@ -275,7 +275,9 @@ void ContainerPage::transition(CryptoDoc *container) ui->rightPane->init(ItemList::ItemAddress, QT_TRANSLATE_NOOP("ItemList", "Recipients")); bool hasUnsupported = false; for (const auto &key : container->keys()) { - hasUnsupported = hasUnsupported || (key.rcpt_cert.isNull() && !key.lock.isValid()); + // 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); auto *addr = new AddressItem(key, AddressItem::Icon, ui->rightPane); ui->rightPane->addWidget(addr); connect(addr, &AddressItem::decrypt, container, [container, key, addr, this] { @@ -297,12 +299,19 @@ void ContainerPage::transition(CryptoDoc *container) else { std::unique_ptr backend; { - DecryptionDialog dialog(key, addr->name(), this); + DecryptionDialog dialog(container, key, addr->name(), this); if(!dialog.exec()) return; backend = dialog.takeBackend(); } - decrypt(container, key.lock, {}, std::move(backend)); + // A card's login is handed over; Mobile-ID / Smart-ID has already decrypted in the + // dialog, which shows its control codes meanwhile + if(backend) + decrypt(container, key.lock, {}, std::move(backend)); + else { + transition(container); + emit action(DecryptContainerSuccess); + } } }); } diff --git a/client/widgets/ShrinkingStack.h b/client/widgets/ShrinkingStack.h index e4e471f1b..ccf820398 100644 --- a/client/widgets/ShrinkingStack.h +++ b/client/widgets/ShrinkingStack.h @@ -2,16 +2,30 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include #include class ShrinkingStack final : public QStackedWidget { public: - using QStackedWidget::QStackedWidget; + explicit ShrinkingStack(QWidget *parent = nullptr) + : QStackedWidget(parent) + { + // QStackedLayout would set the minimum size to that of the largest page; + // the pages not shown must not hold the size + layout()->setSizeConstraint(QLayout::SetNoConstraint); + connect(this, &QStackedWidget::currentChanged, this, &QWidget::updateGeometry); + } QSize sizeHint() const final { return currentWidget() ? currentWidget()->sizeHint() : QStackedWidget::sizeHint(); } QSize minimumSizeHint() const final { return currentWidget() ? currentWidget()->minimumSizeHint() : QStackedWidget::minimumSizeHint(); } + // A page with wrapped labels would make the parent layout ask for height-for-width, + // which QWidgetItem takes from QStackedLayout: the tallest of all pages, hidden ones + // included. Without it the layout falls back to sizeHint(), the current page's. + bool hasHeightForWidth() const final { + return false; + } };