diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ff52036b..0723c8b76 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -284,6 +284,7 @@ jobs: patterns: | -**/*autogen*/** -**/common/qtsingleapplication/** + -**/client/qrcodegen/** -**:cpp/loop-variable-changed -**:cpp/poorly-documented-function input: sarif-results/cpp.sarif diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index dff71cc89..ecd1c0904 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -91,6 +91,8 @@ add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE TokenData.cpp TokenData.h Utils.h + qrcodegen/qrcodegen.cpp qrcodegen/qrcodegen.h + qrcodegen/QrCodeGenerator.cpp qrcodegen/QrCodeGenerator.h ) qt_add_translations(${PROJECT_NAME} TS_FILES translations/en.ts diff --git a/client/CheckConnection.cpp b/client/CheckConnection.cpp index 689521e9a..d57c2cc44 100644 --- a/client/CheckConnection.cpp +++ b/client/CheckConnection.cpp @@ -68,16 +68,17 @@ QNetworkAccessManager* CheckConnection::setupNAM(QNetworkRequest &req, const QBy .arg(Application::applicationName(), Application::applicationVersion(), Common::applicationOs()).toUtf8()); auto *nam = new QNetworkAccessManager(); QObject::connect(nam, &QNetworkAccessManager::sslErrors, nam, [](QNetworkReply *reply, const QList &errors) { - QList ignore; + QSslCertificate peer = reply->sslConfiguration().peerCertificate(); + QList ignore; for(const QSslError &error: errors) { switch(error.error()) { - case QSslError::UnableToVerifyFirstCertificate: - case QSslError::UnableToGetLocalIssuerCertificate: + case QSslError::UnableToVerifyFirstCertificate: case QSslError::CertificateUntrusted: + case QSslError::UnableToGetLocalIssuerCertificate: case QSslError::SelfSignedCertificateInChain: - if(reply->sslConfiguration().caCertificates().contains(reply->sslConfiguration().peerCertificate())) { + if(reply->sslConfiguration().caCertificates().contains(peer)) { ignore.append(error); break; } @@ -111,8 +112,10 @@ QSslConfiguration CheckConnection::sslConfiguration(const QByteArray &add) trusted.reserve(list.size()); for(const auto &cert: list) trusted.append(QSslCertificate(QByteArray::fromBase64(cert.toString().toLatin1()), QSsl::Der)); - if(!add.isEmpty()) - trusted.append(QSslCertificate(QByteArray::fromBase64(add), QSsl::Der)); + if(!add.isEmpty()) { + QSslCertificate cert = QSslCertificate(add, QSsl::Der); + trusted.append(cert); + } ssl.setCaCertificates(trusted); #endif return ssl; diff --git a/client/Crypto.cpp b/client/Crypto.cpp index e41983f1e..b4a89d52d 100644 --- a/client/Crypto.cpp +++ b/client/Crypto.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -331,6 +332,15 @@ QByteArray Crypto::sign_hmac(const QByteArray &key, const QByteArray &data) return sig; } +QByteArray +Crypto::hmacSha256(const QByteArray& key, const QByteArray& data) +{ + uint8_t b[32]; + unsigned int len = 32; + HMAC(EVP_sha256(), key.data(), (int) key.size(), (const uint8_t *) data.data(), (int) data.size(), b, &len); + return QByteArray((const char *) b, len); +} + QByteArray Crypto::toPublicKeyDer(EVP_PKEY *key) { if(!key) diff --git a/client/Crypto.h b/client/Crypto.h index 7ed6d00ca..762a1a9e8 100644 --- a/client/Crypto.h +++ b/client/Crypto.h @@ -65,6 +65,7 @@ class Crypto static QByteArray genKey(const EVP_CIPHER *cipher); static QByteArray hkdf(const QByteArray &key, const QByteArray &salt, const QByteArray &info, int len = 32, int mode = 0); static QByteArray sign_hmac(const QByteArray &key, const QByteArray &data); + static QByteArray hmacSha256(const QByteArray& key, const QByteArray& data); static QByteArray toPublicKeyDer(EVP_PKEY *key); static QByteArray toPublicKeyDer(const QSslKey &key); static QByteArray random(int len = 32); diff --git a/client/MainWindow.cpp b/client/MainWindow.cpp index a1a2b2d13..012b4608f 100644 --- a/client/MainWindow.cpp +++ b/client/MainWindow.cpp @@ -52,6 +52,8 @@ #include #include +#include + using namespace ria::qdigidoc4; using namespace std::chrono; @@ -365,9 +367,12 @@ void MainWindow::onSignAction(int action, const QString &info1, const QString &i break; case SignatureSmartID: sign([this, info1, info2](const QString &city, const QString &state, const QString &zip, const QString &country, const QString &role) { - SmartIDProgress s(this); - return s.init(info1, info2, digiDoc->fileName()) && - digiDoc->sign(city, state, zip, country, role, &s); + SmartIDProgress s(this); + if (!s.init(info1, info2, digiDoc->fileName())) return false; + std::string subj_name = s.cert().subjectName("serialNumber"); + if (subj_name.starts_with("PNOEE-")) subj_name = subj_name.substr(6, subj_name.size() - 6); + return ui->signContainerPage->checkIfAlreadySigned(SignatureMobile, QString::fromStdString(subj_name), {}) && + digiDoc->sign(city, state, zip, country, role, &s); }); break; case ClearSignatureWarning: diff --git a/client/dialogs/MobileProgress.ui b/client/dialogs/MobileProgress.ui index 9b0a2afdc..9abe6930d 100644 --- a/client/dialogs/MobileProgress.ui +++ b/client/dialogs/MobileProgress.ui @@ -3,7 +3,7 @@ MobileProgress - Qt::WindowModal + Qt::WindowModality::WindowModal @@ -64,7 +64,7 @@ background-color: #82A9D3; 30 - QLayout::SetFixedSize + QLayout::SizeConstraint::SetFixedSize 40 @@ -95,20 +95,20 @@ background-color: #82A9D3; Control code: - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter - Qt::TabFocus + Qt::FocusPolicy::TabFocus 1234 - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -118,7 +118,7 @@ background-color: #82A9D3; Make sure control code matches with one in phone screen and enter mobile-ID PIN2-code. - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter true @@ -145,7 +145,7 @@ background-color: #82A9D3; 0 - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter false @@ -159,7 +159,7 @@ background-color: #82A9D3; - + PointingHandCursor diff --git a/client/dialogs/SmartIDProgress.cpp b/client/dialogs/SmartIDProgress.cpp index c0e7dd2e1..63d1369b1 100644 --- a/client/dialogs/SmartIDProgress.cpp +++ b/client/dialogs/SmartIDProgress.cpp @@ -17,13 +17,15 @@ * */ +#define noSIDV3TEST + #include "SmartIDProgress.h" -#include "ui_MobileProgress.h" +#include "ui_SmartIDProgress.h" #include "Application.h" #include "CheckConnection.h" +#include "Crypto.h" #include "Settings.h" -#include "Utils.h" #include "dialogs/WarningDialog.h" #include @@ -35,55 +37,297 @@ #include #include #include +#include #include #include #include #include -#include +#include "../qrcodegen/QrCodeGenerator.h" Q_LOGGING_CATEGORY(SIDLog,"RIA.SmartID") using namespace digidoc; -class SmartIDProgress::Private final: public QDialog, public Ui::MobileProgress +class SmartIDProgress::Private final: public QDialog, public Ui::SmartIDProgress { Q_OBJECT public: using QDialog::QDialog; - void reject() final { l.exit(QDialog::Rejected); } + + void reject() final { l.exit(QDialog::Rejected); } void setVisible(bool visible) final { if(visible && !hider) hider = std::make_unique(); QDialog::setVisible(visible); if(!visible && hider) hider.reset(); } - QTimer *timer{}; - QTimeLine *statusTimer{}; + QTimer *timer {}; + QTimeLine *statusTimer {}; QNetworkAccessManager *manager {}; QNetworkRequest req; - QString documentNumber, sessionID, fileName; + QString documentNumber, sessionID, sessionToken, deviceLinkBase, fileName; + QByteArray sessionSecret; X509Cert cert; std::vector signature; QEventLoop l; bool useCustomUUID = Settings::SID_UUID_CUSTOM; +#ifdef SIDV3TEST + QString UUID = "00000000-0000-4000-8000-000000000000"; + QString NAME = "DEMO"; + QString URL = "https://sid.demo.sk.ee/smart-id-rp/v3"; +#else QString UUID = useCustomUUID ? Settings::SID_UUID : QString(); QString NAME = Settings::SID_NAME; QString URL = !UUID.isNull() && useCustomUUID ? Settings::SID_SK_URL : Settings::SID_PROXY_URL; +#endif + qint64 startTimeStamp; std::unique_ptr hider; + + void httpFinished(QNetworkReply *reply); + void timeSlice(); + void returnError (const QString &err, const QString &details = {}); + + void httpError(QNetworkReply& reply); + void endResultError(QString& result); + + QByteArray getAuthCode(QString unprotectedDeviceLink); + QString getDeviceLink(); + QImage generateQr(); }; +QByteArray +SmartIDProgress::Private::getAuthCode(QString unprotectedDeviceLink) +{ + QString schemeName = "smart-id-demo"; + QString relyingPartyName = "DEMO"; + QString brokeredRpName = ""; + QString authCodePayload = + schemeName + "|" + // schemeName, + "|" + // signatureProtocol + "|" + // digest + relyingPartyName.toUtf8().toBase64() + "|" + // relyingPartyNameBase64 + brokeredRpName.toUtf8().toBase64() + "|" + // brokeredRpNameBase64 + "|" + // interactions + "|" + // initialCallbackUrl + unprotectedDeviceLink; + qCDebug(SIDLog).noquote() << "authCodePayload: " << authCodePayload; + QByteArray authCode = Crypto::hmacSha256(sessionSecret, authCodePayload.toUtf8()); + + return authCode; +} + +QString +SmartIDProgress::Private::getDeviceLink() +{ + QString deviceLinkType = "QR"; + qint64 timestamp = QDateTime::currentSecsSinceEpoch(); + int elapsedSeconds = int(timestamp - startTimeStamp); + QString sessionType = "cert"; + QString version = "1.0"; + QString lang = "eng"; + + QString unprotectedDeviceLink = deviceLinkBase + "?" + + "deviceLinkType=" + deviceLinkType + + "&elapsedSeconds=" + QString::number(elapsedSeconds) + + "&sessionToken=" + sessionToken + + "&sessionType=" + sessionType + + "&version=" + version + + "&lang=" + lang; + qCDebug(SIDLog).noquote() << "unprotectedDeviceLink: " << unprotectedDeviceLink; + QString deviceLink = unprotectedDeviceLink + + "&authCode=" + QString::fromUtf8(getAuthCode(unprotectedDeviceLink).toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals)); + return deviceLink; +} + +void +SmartIDProgress::Private::timeSlice() +{ + qCDebug(SIDLog).noquote() << "timeSlice"; + if (!this->isVisible()) { + qCDebug(SIDLog).noquote() << "showing"; + show(); + } + if (!sessionID.isEmpty()) { + qCDebug(SIDLog).noquote() << "generating"; + QString deviceLink = getDeviceLink(); + qCDebug(SIDLog).noquote() << "deviceLink: " << deviceLink; + QrCodeGenerator gen; + QImage img = gen.generateQr(deviceLink.toUtf8(), 256); + code->setPixmap(QPixmap::fromImage(img)); + } +} + +void +SmartIDProgress::Private::returnError (const QString &err, const QString &details) +{ + qCWarning(SIDLog) << err; + statusTimer->stop(); + delete timer; + timer = nullptr; + hide(); + auto *dlg = WarningDialog::show(parentWidget(), err, details); + QObject::connect(dlg, &WarningDialog::finished, &l, &QEventLoop::exit); +} + +void +SmartIDProgress::Private::httpError(QNetworkReply& reply) +{ + const auto httpStatusCode = reply.attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + qCWarning(SIDLog) << httpStatusCode << "Error :" << reply.error(); + qCDebug(SIDLog).noquote() << reply.readAll(); + + switch(reply.error()) + { + case QNetworkReply::ContentNotFoundError: + return returnError(tr("Failed to sign container. Your Smart-ID transaction has expired or user account not found.")); + case QNetworkReply::ConnectionRefusedError: + return returnError(tr("%1 service has encountered technical errors. Please try again later.").arg(tr("Smart-ID"))); + case QNetworkReply::SslHandshakeFailedError: + return returnError(tr("SSL handshake failed. Check the proxy settings of your computer or software upgrades.")); + case QNetworkReply::TimeoutError: + case QNetworkReply::UnknownNetworkError: + return returnError(tr("Failed to connect with service server. Please check your network settings or try again later.")); + case QNetworkReply::HostNotFoundError: + case QNetworkReply::AuthenticationRequiredError: + return returnError(tr("Failed to send request. Check your %1 service access settings.").arg(tr("Smart-ID"))); + default: + switch (httpStatusCode) + { + case 403: + return returnError(tr("Failed to sign container. Check your %1 service access settings. " + "Additional information").arg(tr("Smart-ID"))); + case 409: + return returnError(tr("Failed to send request. The number of unsuccesful request from this IP address has been exceeded. Please try again later.")); + case 429: + return returnError(tr("The limit for %1 digital signatures per month has been reached. " + "Additional information").arg(tr("Smart-ID"))); + case 471: + return returnError(tr("Your Smart-ID certificate level must be qualified to sign documents in DigiDoc4 Client.")); + case 480: + return returnError(tr("Your signing software needs an upgrade. Please update your ID software, which you can get from " + "www.id.ee. Additional info is available ID-helpline (+372) 666 8888.")); + case 580: + return returnError(tr("Failed to send request. A valid session is associated with this personal code. " + "It is not possible to start a new signing before the current session expires. Please try again later.")); + default: + return returnError(tr("Failed to send request. %1 service has encountered technical errors. Please try again later.").arg(QLatin1String("Smart-ID")), reply.errorString()); + } + } +} + +void +SmartIDProgress::Private::endResultError(QString& endResult) +{ + if(endResult == QLatin1String("USER_REFUSED") || + endResult == QLatin1String("USER_REFUSED_CERT_CHOICE") || + endResult == QLatin1String("USER_REFUSED_DISPLAYTEXTANDPIN") || + endResult == QLatin1String("USER_REFUSED_VC_CHOICE") || + endResult == QLatin1String("USER_REFUSED_CONFIRMATIONMESSAGE") || + endResult == QLatin1String("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE")) + returnError(tr("User denied or cancelled")); + else if(endResult == QLatin1String("TIMEOUT")) + returnError(tr("Failed to sign container. Your Smart-ID transaction has expired or user account not found.")); + else if(endResult == QLatin1String("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP")) + returnError(tr("Failed to sign container. You need to update your Smart-ID application to sign documents in DigiDoc4 Client.")); + else if(endResult == QLatin1String("WRONG_VC")) + returnError(tr("Error: an incorrect control code was chosen")); + else if(endResult == QLatin1String("DOCUMENT_UNUSABLE")) + returnError(tr("Your Smart-ID transaction has failed. Please check your Smart-ID application or contact Smart-ID customer support.")); + else + returnError(tr("Service result: ") + endResult); +} +void +SmartIDProgress::Private::httpFinished(QNetworkReply *reply) { + QScopedPointer scope(reply); + if (reply->error() != QNetworkReply::NoError) { + // http error + return httpError(*reply); + } + if(!reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith(QLatin1String("application/json"))) + return returnError(tr("Invalid content type header ") + reply->header(QNetworkRequest::ContentTypeHeader).toString()); + + // Result 200, get reply body as JSON + QByteArray data = reply->readAll(); + reply->deleteLater(); + qCDebug(SIDLog).noquote() << data; + QJsonObject result = QJsonDocument::fromJson(data, nullptr).object(); + if(result.isEmpty()) { + return returnError(tr("Failed to parse JSON content")); + } + + // Have reply from RP + if(sessionID.isEmpty()) { + // Initial request, save session parameters + qCDebug(SIDLog).noquote() << "New session"; + sessionID = result.value(QLatin1String("sessionID")).toString(); + qCDebug(SIDLog).noquote() << "sessionID: " << sessionID; + sessionToken = result.value(QLatin1String("sessionToken")).toString(); + qCDebug(SIDLog).noquote() << "SessionToken: " << sessionToken; + QString secret64 = result.value(QLatin1String("sessionSecret")).toString(); + sessionSecret = QByteArray::fromBase64(secret64.toUtf8()); + qCDebug(SIDLog).noquote() << "sessionSecret: " << secret64; + deviceLinkBase = result.value(QLatin1String("deviceLinkBase")).toString(); + qCDebug(SIDLog).noquote() << "deviceLinkBase: " << deviceLinkBase; + if (!sessionToken.isEmpty()) { + // Certificate selection session (as opposed to signing) + qCDebug(SIDLog).noquote() << "Starting QR timer"; + startTimeStamp = QDateTime::currentSecsSinceEpoch(); + timeSlice(); + } + } else { + if (result.value(QLatin1String("state")).toString() != QLatin1String("RUNNING")) { + // Disable Qr generation + qCDebug(SIDLog).noquote() << "Disabling Qr timer"; + timer->stop(); + QString endResult = result.value(QLatin1String("result")).toObject().value(QLatin1String("endResult")).toString(); + if (endResult != QLatin1String("OK")) { + return endResultError(endResult); + } + if(documentNumber.isEmpty()) { + documentNumber = result.value(QLatin1String("result")).toObject().value(QLatin1String("documentNumber")).toString(); + } + if(result.contains(QLatin1String("signature"))) { + QJsonObject r_sig = result.value(QLatin1String("signature")).toObject(); + if (r_sig.contains(QLatin1String("value"))) { + QByteArray b64 = QByteArray::fromBase64(r_sig.value(QLatin1String("value")).toString().toUtf8()); + signature.assign(b64.cbegin(), b64.cend()); + hide(); + l.exit(QDialog::Accepted); + return; + } + } + if(result.contains(QLatin1String("cert"))) { + try { + QByteArray b64 = QByteArray::fromBase64(result.value(QLatin1String("cert")).toObject().value(QLatin1String("value")).toString().toUtf8()); + cert = X509Cert((const unsigned char*)b64.constData(), size_t(b64.size()), X509Cert::Der); + hide(); + l.exit(QDialog::Accepted); + } catch(const Exception &e) { + returnError(tr("Failed to parse certificate: ") + QString::fromStdString(e.msg())); + } + } + return; + } + } + req.setUrl(QUrl(QStringLiteral("%1/session/%2?timeoutMs=10000").arg(URL, sessionID))); + qCDebug(SIDLog).noquote() << req.url(); + manager->get(req); +} SmartIDProgress::SmartIDProgress(QWidget *parent) - : d(new Private(parent)) + : d(new Private(parent)) { +#ifdef SIDV3TEST + const_cast(SIDLog()).setEnabled(QtDebugMsg, true); +#else const_cast(SIDLog()).setEnabled(QtDebugMsg, QFile::exists(QStringLiteral("%1/%2.log").arg(QDir::tempPath(), QApplication::applicationName()))); +#endif d->setWindowFlags(Qt::Dialog|Qt::CustomizeWindowHint); d->setupUi(d); d->signProgressBar->setMaximum(100); d->code->setBuddy(d->signProgressBar); - d->code->clear(); #if defined(Q_OS_UNIX) && !defined(Q_OS_MAC) const auto styleSheet = R"(QProgressBar { background-color: #d3d3d3;; @@ -110,118 +354,7 @@ background-color: #007aff; d->req.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); d->manager = CheckConnection::setupNAM(d->req); d->manager->setParent(d); - QNetworkAccessManager::connect(d->manager, &QNetworkAccessManager::finished, d, [&](QNetworkReply *reply){ - QScopedPointer scope(reply); - auto returnError = [=](const QString &err, const QString &details = {}) { - qCWarning(SIDLog) << err; - d->statusTimer->stop(); - delete d->timer; - d->timer = nullptr; - d->hide(); - auto *dlg = WarningDialog::show(d->parentWidget(), err, details); - QObject::connect(dlg, &WarningDialog::finished, &d->l, &QEventLoop::exit); - }; - - switch(reply->error()) - { - case QNetworkReply::NoError: - break; - case QNetworkReply::ContentNotFoundError: - return returnError(tr("Failed to sign container. Your Smart-ID transaction has expired or user account not found.")); - case QNetworkReply::ConnectionRefusedError: - return returnError(tr("%1 service has encountered technical errors. Please try again later.").arg(tr("Smart-ID"))); - case QNetworkReply::SslHandshakeFailedError: - return returnError(tr("SSL handshake failed. Check the proxy settings of your computer or software upgrades.")); - case QNetworkReply::TimeoutError: - case QNetworkReply::UnknownNetworkError: - return returnError(tr("Failed to connect with service server. Please check your network settings or try again later.")); - case QNetworkReply::HostNotFoundError: - case QNetworkReply::AuthenticationRequiredError: - return returnError(tr("Failed to send request. Check your %1 service access settings.").arg(tr("Smart-ID"))); - default: - const auto httpStatusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - qCWarning(SIDLog) << httpStatusCode << "Error :" << reply->error(); - switch (httpStatusCode) - { - case 403: - return returnError(tr("Failed to sign container. Check your %1 service access settings. " - "Additional information").arg(tr("Smart-ID"))); - case 409: - return returnError(tr("Failed to send request. The number of unsuccesful request from this IP address has been exceeded. Please try again later.")); - case 429: - return returnError(tr("The limit for %1 digital signatures per month has been reached. " - "Additional information").arg(tr("Smart-ID"))); - case 471: - return returnError(tr("Your Smart-ID certificate level must be qualified to sign documents in DigiDoc4 Client.")); - case 480: - return returnError(tr("Your signing software needs an upgrade. Please update your ID software, which you can get from " - "www.id.ee. Additional info is available ID-helpline (+372) 666 8888.")); - case 580: - return returnError(tr("Failed to send request. A valid session is associated with this personal code. " - "It is not possible to start a new signing before the current session expires. Please try again later.")); - default: - return returnError(tr("Failed to send request. %1 service has encountered technical errors. Please try again later.").arg(QLatin1String("Smart-ID")), reply->errorString()); - } - } - if(!reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith(QLatin1String("application/json"))) - return returnError(tr("Invalid content type header ") + reply->header(QNetworkRequest::ContentTypeHeader).toString()); - - QByteArray data = reply->readAll(); - reply->deleteLater(); - qCDebug(SIDLog).noquote() << data; - QJsonObject result = QJsonDocument::fromJson(data, nullptr).object(); - if(result.isEmpty()) - return returnError(tr("Failed to parse JSON content")); - if(d->sessionID.isEmpty()) - d->sessionID = result.value(QLatin1String("sessionID")).toString(); - else if(result.value(QLatin1String("state")).toString() != QLatin1String("RUNNING")) - { - QString endResult = result.value(QLatin1String("result")).toObject().value(QLatin1String("endResult")).toString(); - if(endResult == QLatin1String("USER_REFUSED") || - endResult == QLatin1String("USER_REFUSED_CERT_CHOICE") || - endResult == QLatin1String("USER_REFUSED_DISPLAYTEXTANDPIN") || - endResult == QLatin1String("USER_REFUSED_VC_CHOICE") || - endResult == QLatin1String("USER_REFUSED_CONFIRMATIONMESSAGE") || - endResult == QLatin1String("USER_REFUSED_CONFIRMATIONMESSAGE_WITH_VC_CHOICE")) - returnError(tr("User denied or cancelled")); - else if(endResult == QLatin1String("TIMEOUT")) - returnError(tr("Failed to sign container. Your Smart-ID transaction has expired or user account not found.")); - else if(endResult == QLatin1String("REQUIRED_INTERACTION_NOT_SUPPORTED_BY_APP")) - returnError(tr("Failed to sign container. You need to update your Smart-ID application to sign documents in DigiDoc4 Client.")); - else if(endResult == QLatin1String("WRONG_VC")) - returnError(tr("Error: an incorrect control code was chosen")); - else if(endResult == QLatin1String("DOCUMENT_UNUSABLE")) - returnError(tr("Your Smart-ID transaction has failed. Please check your Smart-ID application or contact Smart-ID customer support.")); - else if(endResult != QLatin1String("OK")) - returnError(tr("Service result: ") + endResult); - else if(d->documentNumber.isEmpty()) - d->documentNumber = result.value(QLatin1String("result")).toObject().value(QLatin1String("documentNumber")).toString(); - if(result.contains(QLatin1String("signature"))) - { - QByteArray b64 = QByteArray::fromBase64( - result.value(QLatin1String("signature")).toObject().value(QLatin1String("value")).toString().toUtf8()); - d->signature.assign(b64.cbegin(), b64.cend()); - d->hide(); - d->l.exit(QDialog::Accepted); - } - else if(result.contains(QLatin1String("cert"))) - { - try { - QByteArray b64 = QByteArray::fromBase64( - result.value(QLatin1String("cert")).toObject().value(QLatin1String("value")).toString().toUtf8()); - d->cert = X509Cert((const unsigned char*)b64.constData(), size_t(b64.size()), X509Cert::Der); - d->hide(); - d->l.exit(QDialog::Accepted); - } catch(const Exception &e) { - returnError(tr("Failed to parse certificate: ") + QString::fromStdString(e.msg())); - } - } - return; - } - d->req.setUrl(QUrl(QStringLiteral("%1/session/%2?timeoutMs=10000").arg(d->URL, d->sessionID))); - qCDebug(SIDLog).noquote() << d->req.url(); - d->manager->get(d->req); - }); + QNetworkAccessManager::connect(d->manager, &QNetworkAccessManager::finished, d, &SmartIDProgress::Private::httpFinished); } SmartIDProgress::~SmartIDProgress() @@ -236,8 +369,7 @@ X509Cert SmartIDProgress::cert() const bool SmartIDProgress::init(const QString &country, const QString &idCode, const QString &fileName) { - if(!d->UUID.isEmpty() && QUuid(d->UUID).isNull()) - { + if(!d->UUID.isEmpty() && QUuid(d->UUID).isNull()) { WarningDialog::show(d->parentWidget(), tr("Failed to send request. Check your %1 service access settings.").arg(tr("Smart-ID"))); return false; } @@ -246,7 +378,8 @@ bool SmartIDProgress::init(const QString &country, const QString &idCode, const if(displayFile.size() > 6) displayFile = displayFile.left(3) + QStringLiteral("...") + displayFile.right(3); displayFile += QStringLiteral(".") + info.suffix(); - d->fileName = std::move(displayFile); + d->fileName = displayFile; + qCDebug(SIDLog).noquote() << "fileName: " << d->fileName; d->sessionID.clear(); QByteArray data = QJsonDocument({ {"relyingPartyUUID", d->UUID.isEmpty() ? QStringLiteral("00000000-0000-0000-0000-000000000000") : d->UUID}, @@ -254,60 +387,72 @@ bool SmartIDProgress::init(const QString &country, const QString &idCode, const {"certificateLevel", "QUALIFIED"}, {"nonce", QUuid::createUuid().toString().remove('-').mid(1, 30)} }).toJson(); - d->req.setUrl(QUrl(QStringLiteral("%1/certificatechoice/etsi/PNO%2-%3").arg(d->URL, country, idCode))); + d->req.setUrl(QUrl(QStringLiteral("%1/signature/certificate-choice/device-link/anonymous").arg(d->URL))); qCDebug(SIDLog).noquote() << d->req.url() << data; d->manager->post(d->req, data); - d->info->setText(tr("Open the Smart-ID application on your smart device and confirm device for signing.")); + d->info->setText(tr("Open the Smart-ID application on your smart device and scan the QR code.")); d->code->setAccessibleName(d->info->text()); d->statusTimer->start(); d->adjustSize(); d->timer = new QTimer(d); - d->timer->setSingleShot(true); - QObject::connect(d->timer, &QTimer::timeout, d, &SmartIDProgress::Private::show); - using namespace std::chrono; - d->timer->start(3s); + d->timer->setInterval(1000); + QObject::connect(d->timer, &QTimer::timeout, d, &SmartIDProgress::Private::timeSlice); + d->timer->start(); return d->l.exec() == QDialog::Accepted; } std::vector SmartIDProgress::sign(const std::string &method, const std::vector &digest) const { - d->statusTimer->stop(); - d->sessionID.clear(); - QString digestMethod; - if(method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256") - digestMethod = QStringLiteral("SHA256"); - else if(method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384") - digestMethod = QStringLiteral("SHA384"); - else if(method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512") - digestMethod = QStringLiteral("SHA512"); + if((method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256") || (method == "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1")) + digestMethod = QStringLiteral("SHA-256"); + else if((method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384") || (method == "http://www.w3.org/2007/05/xmldsig-more#sha384-rsa-MGF1")) + digestMethod = QStringLiteral("SHA-384"); + else if((method == "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512") || (method == "http://www.w3.org/2007/05/xmldsig-more#sha512-rsa-MGF1")) + digestMethod = QStringLiteral("SHA-512"); else throw Exception(__FILE__, __LINE__, "Unsupported digest method"); - QByteArray codeDiest = QCryptographicHash::hash(QByteArray::fromRawData((const char*)digest.data(), int(digest.size())), QCryptographicHash::Sha256); - uint code = codeDiest.right(2).toHex().toUInt(nullptr, 16) % 10000; - d->code->setText(QStringLiteral("%1").arg(code, 4, 10, QChar('0'))); - d->info->setText(tr("Make sure control code matches with one in phone screen and enter Smart-ID PIN2-code.")); - d->code->setAccessibleName(QStringLiteral("%1 %2. %3").arg(d->label->text(), d->code->text(), d->info->text())); + QJsonArray interactions { + QJsonObject{ + {"type", "confirmationMessage"}, + {"displayText200", QStringLiteral("%1 %2").arg(tr("Sign document"), d->fileName)} + }, + QJsonObject{ + {"type", "displayTextAndPIN"}, + {"displayText60", QStringLiteral("%1 %2").arg(tr("Sign document"), d->fileName)} + } + }; QJsonObject req{ {"relyingPartyUUID", (d->UUID.isEmpty() ? QStringLiteral("00000000-0000-0000-0000-000000000000") : d->UUID)}, {"relyingPartyName", d->NAME}, {"certificateLevel", "QUALIFIED"}, - {"hash", QString(QByteArray::fromRawData((const char*)digest.data(), int(digest.size())).toBase64())}, - {"hashType", digestMethod}, - {"allowedInteractionsOrder", QJsonArray{QJsonObject{ - {"type", "confirmationMessageAndVerificationCodeChoice"}, - {"displayText200", "%1"} - }}}, + {"signatureProtocol", "RAW_DIGEST_SIGNATURE"}, + {"signatureProtocolParameters", QJsonObject{ + {"digest", QString(QByteArray::fromRawData((const char*)digest.data(), int(digest.size())).toBase64())}, + {"signatureAlgorithm", "rsassa-pss"}, + {"signatureAlgorithmParameters", QJsonObject{ + {"hashAlgorithm", digestMethod} + }} + }}, + {"linkedSessionID", d->sessionID}, + {"nonce", QUuid::createUuid().toString().remove('-').mid(1, 30)}, + {"interactions", QString::fromLatin1(QJsonDocument(interactions).toJson().toBase64())} }; - // Workaround SID proxy issues - QString escape = QStringLiteral("%1 %2").arg(tr("Sign document"), d->fileName); - QByteArray data = QString::fromUtf8(QJsonDocument(req).toJson()).arg(escapeUnicode(escape)).toUtf8(); - d->req.setUrl(QUrl(QStringLiteral("%1/signature/document/%2").arg(d->URL, d->documentNumber))); + QByteArray data = QJsonDocument(req).toJson(); + + d->req.setUrl(QUrl(QStringLiteral("%1/signature/notification/linked/%2").arg(d->URL, d->documentNumber))); qCDebug(SIDLog).noquote() << d->req.url() << data; + + d->statusTimer->stop(); + d->sessionID.clear(); + d->manager->post(d->req, data); d->statusTimer->start(); + d->info->setText(tr("Please confirm signing on your smart device")); + d->label->hide(); + d->code->hide(); d->adjustSize(); d->show(); switch(d->l.exec()) @@ -324,4 +469,10 @@ std::vector SmartIDProgress::sign(const std::string &method, cons } } +std::string +SmartIDProgress::method() const +{ + return "http://www.w3.org/2007/05/xmldsig-more#sha256-rsa-MGF1"; +} + #include "SmartIDProgress.moc" diff --git a/client/dialogs/SmartIDProgress.h b/client/dialogs/SmartIDProgress.h index 18202fd5e..dbf86df6a 100644 --- a/client/dialogs/SmartIDProgress.h +++ b/client/dialogs/SmartIDProgress.h @@ -22,18 +22,20 @@ #include #include +#include class QWidget; class SmartIDProgress final: public digidoc::Signer { - Q_DECLARE_TR_FUNCTIONS(MobileProgress) + Q_DECLARE_TR_FUNCTIONS(SmartIDProgress) public: - explicit SmartIDProgress(QWidget *parent = nullptr); + explicit SmartIDProgress(QWidget *parent = nullptr); ~SmartIDProgress() final; digidoc::X509Cert cert() const final; bool init(const QString &country, const QString &idCode, const QString &fileName); std::vector sign(const std::string &method, const std::vector &digest) const final; + std::string method() const final; private: class Private; diff --git a/client/dialogs/SmartIDProgress.ui b/client/dialogs/SmartIDProgress.ui new file mode 100644 index 000000000..753082173 --- /dev/null +++ b/client/dialogs/SmartIDProgress.ui @@ -0,0 +1,188 @@ + + + SmartIDProgress + + + Qt::WindowModality::WindowModal + + + + 0 + 0 + 432 + 606 + + + + QWidget { +color: #07142A; +font-family: Roboto, Helvetica; +font-size: 14px; +} +#SmartIDProgressProgress { +background-color: #FFFFFF; +border-radius: 4px; +} +#code { +color: #003168; +font-size: 16px; +font-weight: 700; +} +QPushButton { +padding: 12px 12px; +border-radius: 4px; +border: 1px solid #AD2A45; +color: #AD2A45; +font-weight: 700; +} +QPushButton:hover { +background-color: #F5EBED; +} +QPushButton:pressed { +background-color: #E1C1C6; +} +QPushButton:default { +color: #ffffff; +border-color: #2F70B6; +background-color: #2F70B6; +} +QPushButton:default:hover { +border-color: #2B66A6; +background-color: #2B66A6; +} +QPushButton:default:pressed { +border-color: #215081; +background-color: #215081; +} +QPushButton:default:disabled { +border-color: #82A9D3; +background-color: #82A9D3; +} + + + + 30 + + + QLayout::SizeConstraint::SetFixedSize + + + 40 + + + 32 + + + 40 + + + 32 + + + + + 24 + + + + + + 350 + 0 + + + + DeviceLink code + + + Qt::AlignmentFlag::AlignCenter + + + + + + + + 256 + 256 + + + + Qt::FocusPolicy::TabFocus + + + QR Code + + + Qt::AlignmentFlag::AlignCenter + + + true + + + + + + + Please scan the QR code with your SmartID app + + + Qt::AlignmentFlag::AlignCenter + + + true + + + true + + + + + + + + + + 0 + 40 + + + + 75 + + + 0 + + + Qt::AlignmentFlag::AlignCenter + + + false + + + %v sec + + + + + + + + + + + PointingHandCursor + + + Cancel + + + false + + + + + + + + diff --git a/client/qrcodegen/QrCodeGenerator.cpp b/client/qrcodegen/QrCodeGenerator.cpp new file mode 100644 index 000000000..c2804d69f --- /dev/null +++ b/client/qrcodegen/QrCodeGenerator.cpp @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2023 Alex Spataru + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include + +#include "QrCodeGenerator.h" + +/** + * @brief Default constructor for the QrCodeGenerator class. + * @param parent QObject parent + */ +QrCodeGenerator::QrCodeGenerator(QObject *parent) + : QObject(parent) +{ + // No implementation needed for default constructor. +} + +/** + * @brief Generates a QR code image from a given text string. + * @param data The data to encode in the QR code. + * @param size The image size for the generated QR code. + * @param borderSize The size of the border around the QR code. + * @param errorCorrection The level of error correction to apply. + * @return QImage representing the generated QR code. + */ +QImage QrCodeGenerator::generateQr(const QString &data, quint16 size, + quint16 borderSize, + qrcodegen::QrCode::Ecc errorCorrection) +{ + auto b = data.toUtf8(); + const auto qrCode + = qrcodegen::QrCode::encodeText(b.constData(), errorCorrection); + return qrCodeToImage(qrCode, borderSize, size); +} + +/** + * @brief Generates a QR code image from a given text string. + * @param data The data to encode in the QR code. + * @param size The image size for the generated QR code. + * @param borderSize The size of the border around the QR code. + * @param errorCorrection The level of error correction to apply. + * @return QImage representing the generated QR code. + */ +QImage QrCodeGenerator::generateQr(const QByteArray &data, quint16 size, + quint16 borderSize, + qrcodegen::QrCode::Ecc errorCorrection) +{ + std::vector c(data.constData(), data.constData() + data.size()); + const auto qrCode + = qrcodegen::QrCode::encodeBinary(c, errorCorrection); + return qrCodeToImage(qrCode, borderSize, size); +} + +/** + * @brief Generates an SVG string representing a QR code. + * @param data The data to encode in the QR code. + * @param borderSize The size of the border around the QR code. + * @param errorCorrection The level of error correction to apply. + * @return QString containing the SVG representation of the QR code. + */ +QString QrCodeGenerator::generateSvgQr(const QString &data, quint16 borderSize, + qrcodegen::QrCode::Ecc errorCorrection) +{ + auto b = data.toUtf8(); + const auto qrCode + = qrcodegen::QrCode::encodeText(b.constData(), errorCorrection); + return toSvgString(qrCode, borderSize); +} + +/** + * @brief Converts a QR code to its SVG representation as a string. + * @param qr The QR code to convert. + * @param border The border size to use. + * @return QString containing the SVG representation of the QR code. + */ +QString QrCodeGenerator::toSvgString(const qrcodegen::QrCode &qr, + quint16 border) const +{ + QString str; + QTextStream sb(&str); + + sb << R"()" + << R"()" + << R"()"; + return str; +} + +/** + * @brief Converts a QR code to a QImage. + * @param qrCode The QR code to convert. + * @param border The border size to use. + * @param size The image size to generate. + * @return QImage representing the QR code. + */ +// QImage QrCodeGenerator::qrCodeToImage(const qrcodegen::QrCode &qrCode, +// quint16 border, quint16 size) const +// { +// QString svg = toSvgString(qrCode, border); +// QSvgRenderer render(svg.toUtf8()); +// QImage image(size, size, QImage::Format_Mono); +// image.fill(Qt::white); +// QPainter painter(&image); +// painter.setRenderHint(QPainter::Antialiasing); +// render.render(&painter); +// return image; +// } + +// QR code image is being rendered from SVG output, not directly from bitmaps or +// matrices QSvgRenderer has internal limits on path complexity or buffer size — +// hitting those leads to the truncation warning. +// If the generated SVG string is too large or malformed, the QSvgRenderer will +// throw warnings. +// like: W : qt.svg: Invalid path data; path truncated. +QImage QrCodeGenerator::qrCodeToImage(const qrcodegen::QrCode &qrCode, + quint16 border, quint16 size) const +{ + const int qrSize = qrCode.getSize(); + const int totalSize = qrSize + 2 * border; + // Calculate scaling factor to fit requested size + const int pixelSize = size / totalSize; + const int imageSize = pixelSize * totalSize; + // Create the output image + QImage image(imageSize, imageSize, QImage::Format_RGB32); + image.fill(Qt::white); + + QPainter painter(&image); + painter.setBrush(Qt::black); + painter.setPen(Qt::NoPen); + + // Draw each QR module (black square) + for (int y = 0; y < qrSize; ++y) + { + for (int x = 0; x < qrSize; ++x) + { + if (qrCode.getModule(x, y)) + { + int xPos = (x + border) * pixelSize; + int yPos = (y + border) * pixelSize; + painter.drawRect(xPos, yPos, pixelSize, pixelSize); + } + } + } + + return image; +} diff --git a/client/qrcodegen/QrCodeGenerator.h b/client/qrcodegen/QrCodeGenerator.h new file mode 100644 index 000000000..cd2371bbd --- /dev/null +++ b/client/qrcodegen/QrCodeGenerator.h @@ -0,0 +1,111 @@ +/* + * Copyright (c) 2023 Alex Spataru + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#pragma once + +#include +#include +#include + +#include "qrcodegen.h" + +/** + * @class QrCodeGenerator + * @brief The QrCodeGenerator class is a simple C++ class that uses the + * qrcodegen library to generate QR codes from QStrings in Qt applications. + */ +class QrCodeGenerator : public QObject +{ +public: + /** + * @brief Constructs a QrCodeGenerator object. + * @param parent The parent QObject. + */ + explicit QrCodeGenerator(QObject *parent = nullptr); + + /** + * @brief Generates a QR code from the given data and error correction level. + * @param data The QString containing the data to encode in the QR code. + * @param size The desired width/height of the generated image (default: 500). + * @param borderSize The desired border width of the generated image (default: + * 1). + * @param errorCorrection The desired error correction level (default: + * qrcodegen::QrCode::Ecc::MEDIUM). + * + * @return QImage containing the generated QR code. + */ + QImage generateQr(const QString &data, const quint16 size = 1000, + const quint16 borderSize = 1, + qrcodegen::QrCode::Ecc errorCorrection + = qrcodegen::QrCode::Ecc::MEDIUM); + + /** + * @brief Generates a QR code from the given data and error correction level. + * @param data The QByteArray containing the data to encode in the QR code. + * @param size The desired width/height of the generated image (default: 500). + * @param borderSize The desired border width of the generated image (default: + * 1). + * @param errorCorrection The desired error correction level (default: + * qrcodegen::QrCode::Ecc::MEDIUM). + * + * @return QImage containing the generated QR code. + */ + QImage generateQr(const QByteArray &data, const quint16 size = 1000, + const quint16 borderSize = 1, + qrcodegen::QrCode::Ecc errorCorrection + = qrcodegen::QrCode::Ecc::MEDIUM); + + /** + * @brief Generates a QR code from the given data and error correction level. + * @param data The QString containing the data to encode in the QR code. + * @param borderSize The desired border width of the generated image (default: + * 1). + * @param errorCorrection The desired error correction level (default: + * qrcodegen::QrCode::Ecc::MEDIUM). + * + * @return QString string containing the generated QR code in SVG format. + */ + QString generateSvgQr(const QString &data, const quint16 borderSize = 1, + qrcodegen::QrCode::Ecc errorCorrection + = qrcodegen::QrCode::Ecc::MEDIUM); + +private: + /** + * @brief Converts a qrcodegen::QrCode object to a SVG image. + * @param qrCode The qrcodegen::QrCode object to convert. + * @param border The desired border width of the generated image (default: 1). + * + * @return SVG containing the QR code. + */ + QString toSvgString(const qrcodegen::QrCode &qr, quint16 border) const; + + /** + * @brief Converts a qrcodegen::QrCode object to a QImage. + * @param qrCode The qrcodegen::QrCode object to convert. + * @param size The desired width/height of the generated image. + * @param borderSize The desired border width of the generated image. + * + * @return QImage containing the QR code. + */ + QImage qrCodeToImage(const qrcodegen::QrCode &qrCode, quint16 border, + const quint16 size) const; +}; diff --git a/client/qrcodegen/qrcodegen.cpp b/client/qrcodegen/qrcodegen.cpp new file mode 100644 index 000000000..b0492e76c --- /dev/null +++ b/client/qrcodegen/qrcodegen.cpp @@ -0,0 +1,982 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising + * from, out of or in connection with the Software or the use or other dealings + * in the Software. + */ + +#include "qrcodegen.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +using std::int8_t; +using std::size_t; +using std::uint8_t; +using std::vector; + +namespace qrcodegen +{ + +/*---- Class QrSegment ----*/ + +QrSegment::Mode::Mode(int mode, int cc0, int cc1, int cc2) + : modeBits(mode) +{ + numBitsCharCount[0] = cc0; + numBitsCharCount[1] = cc1; + numBitsCharCount[2] = cc2; +} + +int QrSegment::Mode::getModeBits() const +{ + return modeBits; +} + +int QrSegment::Mode::numCharCountBits(int ver) const +{ + return numBitsCharCount[(ver + 7) / 17]; +} + +const QrSegment::Mode QrSegment::Mode::NUMERIC(0x1, 10, 12, 14); +const QrSegment::Mode QrSegment::Mode::ALPHANUMERIC(0x2, 9, 11, 13); +const QrSegment::Mode QrSegment::Mode::BYTE(0x4, 8, 16, 16); +const QrSegment::Mode QrSegment::Mode::KANJI(0x8, 8, 10, 12); +const QrSegment::Mode QrSegment::Mode::ECI(0x7, 0, 0, 0); + +QrSegment QrSegment::makeBytes(const vector &data) +{ + if (data.size() > static_cast(INT_MAX)) + throw std::length_error("Data too long"); + BitBuffer bb; + for (uint8_t b : data) + bb.appendBits(b, 8); + return QrSegment(Mode::BYTE, static_cast(data.size()), std::move(bb)); +} + +QrSegment QrSegment::makeNumeric(const char *digits) +{ + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *digits != '\0'; digits++, charCount++) + { + char c = *digits; + if (c < '0' || c > '9') + throw std::domain_error("String contains non-numeric characters"); + accumData = accumData * 10 + (c - '0'); + accumCount++; + if (accumCount == 3) + { + bb.appendBits(static_cast(accumData), 10); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 or 2 digits remaining + bb.appendBits(static_cast(accumData), accumCount * 3 + 1); + return QrSegment(Mode::NUMERIC, charCount, std::move(bb)); +} + +QrSegment QrSegment::makeAlphanumeric(const char *text) +{ + BitBuffer bb; + int accumData = 0; + int accumCount = 0; + int charCount = 0; + for (; *text != '\0'; text++, charCount++) + { + const char *temp = std::strchr(ALPHANUMERIC_CHARSET, *text); + if (temp == nullptr) + throw std::domain_error( + "String contains unencodable characters in alphanumeric mode"); + accumData = accumData * 45 + static_cast(temp - ALPHANUMERIC_CHARSET); + accumCount++; + if (accumCount == 2) + { + bb.appendBits(static_cast(accumData), 11); + accumData = 0; + accumCount = 0; + } + } + if (accumCount > 0) // 1 character remaining + bb.appendBits(static_cast(accumData), 6); + return QrSegment(Mode::ALPHANUMERIC, charCount, std::move(bb)); +} + +vector QrSegment::makeSegments(const char *text) +{ + // Select the most efficient segment encoding automatically + vector result; + if (*text == '\0') + ; // Leave result empty + else if (isNumeric(text)) + result.push_back(makeNumeric(text)); + else if (isAlphanumeric(text)) + result.push_back(makeAlphanumeric(text)); + else + { + vector bytes; + for (; *text != '\0'; text++) + bytes.push_back(static_cast(*text)); + result.push_back(makeBytes(bytes)); + } + return result; +} + +QrSegment QrSegment::makeEci(long assignVal) +{ + BitBuffer bb; + if (assignVal < 0) + throw std::domain_error("ECI assignment value out of range"); + else if (assignVal < (1 << 7)) + bb.appendBits(static_cast(assignVal), 8); + else if (assignVal < (1 << 14)) + { + bb.appendBits(2, 2); + bb.appendBits(static_cast(assignVal), 14); + } + else if (assignVal < 1000000L) + { + bb.appendBits(6, 3); + bb.appendBits(static_cast(assignVal), 21); + } + else + throw std::domain_error("ECI assignment value out of range"); + return QrSegment(Mode::ECI, 0, std::move(bb)); +} + +QrSegment::QrSegment(const Mode &md, int numCh, const std::vector &dt) + : mode(&md) + , numChars(numCh) + , data(dt) +{ + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + +QrSegment::QrSegment(const Mode &md, int numCh, std::vector &&dt) + : mode(&md) + , numChars(numCh) + , data(std::move(dt)) +{ + if (numCh < 0) + throw std::domain_error("Invalid value"); +} + +int QrSegment::getTotalBits(const vector &segs, int version) +{ + int result = 0; + for (const QrSegment &seg : segs) + { + int ccbits = seg.mode->numCharCountBits(version); + if (seg.numChars >= (1L << ccbits)) + return -1; // The segment's length doesn't fit the field's bit width + if (4 + ccbits > INT_MAX - result) + return -1; // The sum will overflow an int type + result += 4 + ccbits; + if (seg.data.size() > static_cast(INT_MAX - result)) + return -1; // The sum will overflow an int type + result += static_cast(seg.data.size()); + } + return result; +} + +bool QrSegment::isNumeric(const char *text) +{ + for (; *text != '\0'; text++) + { + char c = *text; + if (c < '0' || c > '9') + return false; + } + return true; +} + +bool QrSegment::isAlphanumeric(const char *text) +{ + for (; *text != '\0'; text++) + { + if (std::strchr(ALPHANUMERIC_CHARSET, *text) == nullptr) + return false; + } + return true; +} + +const QrSegment::Mode &QrSegment::getMode() const +{ + return *mode; +} + +int QrSegment::getNumChars() const +{ + return numChars; +} + +const std::vector &QrSegment::getData() const +{ + return data; +} + +const char *QrSegment::ALPHANUMERIC_CHARSET + = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"; + +/*---- Class QrCode ----*/ + +int QrCode::getFormatBits(Ecc ecl) +{ + switch (ecl) + { + case Ecc::LOW: + return 1; + case Ecc::MEDIUM: + return 0; + case Ecc::QUARTILE: + return 3; + case Ecc::HIGH: + return 2; + default: + throw std::logic_error("Unreachable"); + } +} + +QrCode QrCode::encodeText(const char *text, Ecc ecl) +{ + vector segs = QrSegment::makeSegments(text); + return encodeSegments(segs, ecl); +} + +QrCode QrCode::encodeBinary(const vector &data, Ecc ecl) +{ + vector segs{QrSegment::makeBytes(data)}; + return encodeSegments(segs, ecl); +} + +QrCode QrCode::encodeSegments(const vector &segs, Ecc ecl, + int minVersion, int maxVersion, int mask, + bool boostEcl) +{ + if (!(MIN_VERSION <= minVersion && minVersion <= maxVersion + && maxVersion <= MAX_VERSION) + || mask < -1 || mask > 7) + throw std::invalid_argument("Invalid value"); + + // Find the minimal version number to use + int version, dataUsedBits; + for (version = minVersion;; version++) + { + int dataCapacityBits = getNumDataCodewords(version, ecl) + * 8; // Number of data bits available + dataUsedBits = QrSegment::getTotalBits(segs, version); + if (dataUsedBits != -1 && dataUsedBits <= dataCapacityBits) + break; // This version number is found to be suitable + if (version >= maxVersion) + { // All versions in the range could not fit the given data + std::ostringstream sb; + if (dataUsedBits == -1) + sb << "Segment too long"; + else + { + sb << "Data length = " << dataUsedBits << " bits, "; + sb << "Max capacity = " << dataCapacityBits << " bits"; + } + throw data_too_long(sb.str()); + } + } + assert(dataUsedBits != -1); + + // Increase the error correction level while the data still fits in the + // current version number + for (Ecc newEcl : {Ecc::MEDIUM, Ecc::QUARTILE, Ecc::HIGH}) + { // From low to high + if (boostEcl && dataUsedBits <= getNumDataCodewords(version, newEcl) * 8) + ecl = newEcl; + } + + // Concatenate all segments to create the data bit string + BitBuffer bb; + for (const QrSegment &seg : segs) + { + bb.appendBits(static_cast(seg.getMode().getModeBits()), 4); + bb.appendBits(static_cast(seg.getNumChars()), + seg.getMode().numCharCountBits(version)); + bb.insert(bb.end(), seg.getData().begin(), seg.getData().end()); + } + assert(bb.size() == static_cast(dataUsedBits)); + + // Add terminator and pad up to a byte if applicable + size_t dataCapacityBits + = static_cast(getNumDataCodewords(version, ecl)) * 8; + assert(bb.size() <= dataCapacityBits); + bb.appendBits(0, std::min(4, static_cast(dataCapacityBits - bb.size()))); + bb.appendBits(0, (8 - static_cast(bb.size() % 8)) % 8); + assert(bb.size() % 8 == 0); + + // Pad with alternating bytes until data capacity is reached + for (uint8_t padByte = 0xEC; bb.size() < dataCapacityBits; + padByte ^= 0xEC ^ 0x11) + bb.appendBits(padByte, 8); + + // Pack bits into bytes in big endian + vector dataCodewords(bb.size() / 8); + for (size_t i = 0; i < bb.size(); i++) + dataCodewords.at(i >> 3) |= (bb.at(i) ? 1 : 0) << (7 - (i & 7)); + + // Create the QR Code object + return QrCode(version, ecl, dataCodewords, mask); +} + +QrCode::QrCode(int ver, Ecc ecl, const vector &dataCodewords, int msk) + : // Initialize fields and check arguments + version(ver) + , errorCorrectionLevel(ecl) +{ + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version value out of range"); + if (msk < -1 || msk > 7) + throw std::domain_error("Mask value out of range"); + size = ver * 4 + 17; + size_t sz = static_cast(size); + modules = vector>(sz, vector(sz)); // Initially all light + isFunction = vector>(sz, vector(sz)); + + // Compute ECC, draw modules + drawFunctionPatterns(); + const vector allCodewords = addEccAndInterleave(dataCodewords); + drawCodewords(allCodewords); + + // Do masking + if (msk == -1) + { // Automatically choose best mask + long minPenalty = LONG_MAX; + for (int i = 0; i < 8; i++) + { + applyMask(i); + drawFormatBits(i); + long penalty = getPenaltyScore(); + if (penalty < minPenalty) + { + msk = i; + minPenalty = penalty; + } + applyMask(i); // Undoes the mask due to XOR + } + } + assert(0 <= msk && msk <= 7); + mask = msk; + applyMask(msk); // Apply the final choice of mask + drawFormatBits(msk); // Overwrite old format bits + + isFunction.clear(); + isFunction.shrink_to_fit(); +} + +int QrCode::getVersion() const +{ + return version; +} + +int QrCode::getSize() const +{ + return size; +} + +QrCode::Ecc QrCode::getErrorCorrectionLevel() const +{ + return errorCorrectionLevel; +} + +int QrCode::getMask() const +{ + return mask; +} + +bool QrCode::getModule(int x, int y) const +{ + return 0 <= x && x < size && 0 <= y && y < size && module(x, y); +} + +void QrCode::drawFunctionPatterns() +{ + // Draw horizontal and vertical timing patterns + for (int i = 0; i < size; i++) + { + setFunctionModule(6, i, i % 2 == 0); + setFunctionModule(i, 6, i % 2 == 0); + } + + // Draw 3 finder patterns (all corners except bottom right; overwrites some + // timing modules) + drawFinderPattern(3, 3); + drawFinderPattern(size - 4, 3); + drawFinderPattern(3, size - 4); + + // Draw numerous alignment patterns + const vector alignPatPos = getAlignmentPatternPositions(); + size_t numAlign = alignPatPos.size(); + for (size_t i = 0; i < numAlign; i++) + { + for (size_t j = 0; j < numAlign; j++) + { + // Don't draw on the three finder corners + if (!((i == 0 && j == 0) || (i == 0 && j == numAlign - 1) + || (i == numAlign - 1 && j == 0))) + drawAlignmentPattern(alignPatPos.at(i), alignPatPos.at(j)); + } + } + + // Draw configuration data + drawFormatBits(0); // Dummy mask value; overwritten later in the constructor + drawVersion(); +} + +void QrCode::drawFormatBits(int msk) +{ + // Calculate error correction code and pack bits + int data = getFormatBits(errorCorrectionLevel) << 3 + | msk; // errCorrLvl is uint2, msk is uint3 + int rem = data; + for (int i = 0; i < 10; i++) + rem = (rem << 1) ^ ((rem >> 9) * 0x537); + int bits = (data << 10 | rem) ^ 0x5412; // uint15 + assert(bits >> 15 == 0); + + // Draw first copy + for (int i = 0; i <= 5; i++) + setFunctionModule(8, i, getBit(bits, i)); + setFunctionModule(8, 7, getBit(bits, 6)); + setFunctionModule(8, 8, getBit(bits, 7)); + setFunctionModule(7, 8, getBit(bits, 8)); + for (int i = 9; i < 15; i++) + setFunctionModule(14 - i, 8, getBit(bits, i)); + + // Draw second copy + for (int i = 0; i < 8; i++) + setFunctionModule(size - 1 - i, 8, getBit(bits, i)); + for (int i = 8; i < 15; i++) + setFunctionModule(8, size - 15 + i, getBit(bits, i)); + setFunctionModule(8, size - 8, true); // Always dark +} + +void QrCode::drawVersion() +{ + if (version < 7) + return; + + // Calculate error correction code and pack bits + int rem = version; // version is uint6, in the range [7, 40] + for (int i = 0; i < 12; i++) + rem = (rem << 1) ^ ((rem >> 11) * 0x1F25); + long bits = static_cast(version) << 12 | rem; // uint18 + assert(bits >> 18 == 0); + + // Draw two copies + for (int i = 0; i < 18; i++) + { + bool bit = getBit(bits, i); + int a = size - 11 + i % 3; + int b = i / 3; + setFunctionModule(a, b, bit); + setFunctionModule(b, a, bit); + } +} + +void QrCode::drawFinderPattern(int x, int y) +{ + for (int dy = -4; dy <= 4; dy++) + { + for (int dx = -4; dx <= 4; dx++) + { + int dist + = std::max(std::abs(dx), std::abs(dy)); // Chebyshev/infinity norm + int xx = x + dx, yy = y + dy; + if (0 <= xx && xx < size && 0 <= yy && yy < size) + setFunctionModule(xx, yy, dist != 2 && dist != 4); + } + } +} + +void QrCode::drawAlignmentPattern(int x, int y) +{ + for (int dy = -2; dy <= 2; dy++) + { + for (int dx = -2; dx <= 2; dx++) + setFunctionModule(x + dx, y + dy, + std::max(std::abs(dx), std::abs(dy)) != 1); + } +} + +void QrCode::setFunctionModule(int x, int y, bool isDark) +{ + size_t ux = static_cast(x); + size_t uy = static_cast(y); + modules.at(uy).at(ux) = isDark; + isFunction.at(uy).at(ux) = true; +} + +bool QrCode::module(int x, int y) const +{ + return modules.at(static_cast(y)).at(static_cast(x)); +} + +vector QrCode::addEccAndInterleave(const vector &data) const +{ + if (data.size() + != static_cast( + getNumDataCodewords(version, errorCorrectionLevel))) + throw std::invalid_argument("Invalid argument"); + + // Calculate parameter numbers + int numBlocks + = NUM_ERROR_CORRECTION_BLOCKS[static_cast(errorCorrectionLevel)] + [version]; + int blockEccLen + = ECC_CODEWORDS_PER_BLOCK[static_cast(errorCorrectionLevel)] + [version]; + int rawCodewords = getNumRawDataModules(version) / 8; + int numShortBlocks = numBlocks - rawCodewords % numBlocks; + int shortBlockLen = rawCodewords / numBlocks; + + // Split data into blocks and append ECC to each block + vector> blocks; + const vector rsDiv = reedSolomonComputeDivisor(blockEccLen); + for (int i = 0, k = 0; i < numBlocks; i++) + { + vector dat( + data.cbegin() + k, + data.cbegin() + + (k + shortBlockLen - blockEccLen + (i < numShortBlocks ? 0 : 1))); + k += static_cast(dat.size()); + const vector ecc = reedSolomonComputeRemainder(dat, rsDiv); + if (i < numShortBlocks) + dat.push_back(0); + dat.insert(dat.end(), ecc.cbegin(), ecc.cend()); + blocks.push_back(std::move(dat)); + } + + // Interleave (not concatenate) the bytes from every block into a single + // sequence + vector result; + for (size_t i = 0; i < blocks.at(0).size(); i++) + { + for (size_t j = 0; j < blocks.size(); j++) + { + // Skip the padding byte in short blocks + if (i != static_cast(shortBlockLen - blockEccLen) + || j >= static_cast(numShortBlocks)) + result.push_back(blocks.at(j).at(i)); + } + } + assert(result.size() == static_cast(rawCodewords)); + return result; +} + +void QrCode::drawCodewords(const vector &data) +{ + if (data.size() + != static_cast(getNumRawDataModules(version) / 8)) + throw std::invalid_argument("Invalid argument"); + + size_t i = 0; // Bit index into the data + // Do the funny zigzag scan + for (int right = size - 1; right >= 1; right -= 2) + { // Index of right column in each column pair + if (right == 6) + right = 5; + for (int vert = 0; vert < size; vert++) + { // Vertical counter + for (int j = 0; j < 2; j++) + { + size_t x = static_cast(right - j); // Actual x coordinate + bool upward = ((right + 1) & 2) == 0; + size_t y = static_cast(upward ? size - 1 - vert + : vert); // Actual y coordinate + if (!isFunction.at(y).at(x) && i < data.size() * 8) + { + modules.at(y).at(x) + = getBit(data.at(i >> 3), 7 - static_cast(i & 7)); + i++; + } + // If this QR Code has any remainder bits (0 to 7), they were assigned + // as 0/false/light by the constructor and are left unchanged by this + // method + } + } + } + assert(i == data.size() * 8); +} + +void QrCode::applyMask(int msk) +{ + if (msk < 0 || msk > 7) + throw std::domain_error("Mask value out of range"); + size_t sz = static_cast(size); + for (size_t y = 0; y < sz; y++) + { + for (size_t x = 0; x < sz; x++) + { + bool invert; + switch (msk) + { + case 0: + invert = (x + y) % 2 == 0; + break; + case 1: + invert = y % 2 == 0; + break; + case 2: + invert = x % 3 == 0; + break; + case 3: + invert = (x + y) % 3 == 0; + break; + case 4: + invert = (x / 3 + y / 2) % 2 == 0; + break; + case 5: + invert = x * y % 2 + x * y % 3 == 0; + break; + case 6: + invert = (x * y % 2 + x * y % 3) % 2 == 0; + break; + case 7: + invert = ((x + y) % 2 + x * y % 3) % 2 == 0; + break; + default: + throw std::logic_error("Unreachable"); + } + modules.at(y).at(x) + = modules.at(y).at(x) ^ (invert & !isFunction.at(y).at(x)); + } + } +} + +long QrCode::getPenaltyScore() const +{ + long result = 0; + + // Adjacent modules in row having same color, and finder-like patterns + for (int y = 0; y < size; y++) + { + bool runColor = false; + int runX = 0; + std::array runHistory = {}; + for (int x = 0; x < size; x++) + { + if (module(x, y) == runColor) + { + runX++; + if (runX == 5) + result += PENALTY_N1; + else if (runX > 5) + result++; + } + else + { + finderPenaltyAddHistory(runX, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runX = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runX, runHistory) + * PENALTY_N3; + } + // Adjacent modules in column having same color, and finder-like patterns + for (int x = 0; x < size; x++) + { + bool runColor = false; + int runY = 0; + std::array runHistory = {}; + for (int y = 0; y < size; y++) + { + if (module(x, y) == runColor) + { + runY++; + if (runY == 5) + result += PENALTY_N1; + else if (runY > 5) + result++; + } + else + { + finderPenaltyAddHistory(runY, runHistory); + if (!runColor) + result += finderPenaltyCountPatterns(runHistory) * PENALTY_N3; + runColor = module(x, y); + runY = 1; + } + } + result += finderPenaltyTerminateAndCount(runColor, runY, runHistory) + * PENALTY_N3; + } + + // 2*2 blocks of modules having same color + for (int y = 0; y < size - 1; y++) + { + for (int x = 0; x < size - 1; x++) + { + bool color = module(x, y); + if (color == module(x + 1, y) && color == module(x, y + 1) + && color == module(x + 1, y + 1)) + result += PENALTY_N2; + } + } + + // Balance of dark and light modules + int dark = 0; + for (const vector &row : modules) + { + for (bool color : row) + { + if (color) + dark++; + } + } + int total = size * size; // Note that size is odd, so dark/total != 1/2 + // Compute the smallest integer k >= 0 such that (45-5k)% <= dark/total <= + // (55+5k)% + int k = static_cast((std::abs(dark * 20L - total * 10L) + total - 1) + / total) + - 1; + assert(0 <= k && k <= 9); + result += k * PENALTY_N4; + assert(0 <= result + && result <= 2568888L); // Non-tight upper bound based on default + // values of PENALTY_N1, ..., N4 + return result; +} + +vector QrCode::getAlignmentPatternPositions() const +{ + if (version == 1) + return vector(); + else + { + int numAlign = version / 7 + 2; + int step = (version == 32) + ? 26 + : (version * 4 + numAlign * 2 + 1) / (numAlign * 2 - 2) * 2; + vector result; + for (int i = 0, pos = size - 7; i < numAlign - 1; i++, pos -= step) + result.insert(result.begin(), pos); + result.insert(result.begin(), 6); + return result; + } +} + +int QrCode::getNumRawDataModules(int ver) +{ + if (ver < MIN_VERSION || ver > MAX_VERSION) + throw std::domain_error("Version number out of range"); + int result = (16 * ver + 128) * ver + 64; + if (ver >= 2) + { + int numAlign = ver / 7 + 2; + result -= (25 * numAlign - 10) * numAlign - 55; + if (ver >= 7) + result -= 36; + } + assert(208 <= result && result <= 29648); + return result; +} + +int QrCode::getNumDataCodewords(int ver, Ecc ecl) +{ + return getNumRawDataModules(ver) / 8 + - ECC_CODEWORDS_PER_BLOCK[static_cast(ecl)][ver] + * NUM_ERROR_CORRECTION_BLOCKS[static_cast(ecl)][ver]; +} + +vector QrCode::reedSolomonComputeDivisor(int degree) +{ + if (degree < 1 || degree > 255) + throw std::domain_error("Degree out of range"); + // Polynomial coefficients are stored from highest to lowest power, excluding + // the leading term which is always 1. For example the polynomial x^3 + 255x^2 + // + 8x + 93 is stored as the uint8 array {255, 8, 93}. + vector result(static_cast(degree)); + result.at(result.size() - 1) = 1; // Start off with the monomial x^0 + + // Compute the product polynomial (x - r^0) * (x - r^1) * (x - r^2) * ... * (x + // - r^{degree-1}), and drop the highest monomial term which is always + // 1x^degree. Note that r = 0x02, which is a generator element of this field + // GF(2^8/0x11D). + uint8_t root = 1; + for (int i = 0; i < degree; i++) + { + // Multiply the current product by (x - r^i) + for (size_t j = 0; j < result.size(); j++) + { + result.at(j) = reedSolomonMultiply(result.at(j), root); + if (j + 1 < result.size()) + result.at(j) ^= result.at(j + 1); + } + root = reedSolomonMultiply(root, 0x02); + } + return result; +} + +vector +QrCode::reedSolomonComputeRemainder(const vector &data, + const vector &divisor) +{ + vector result(divisor.size()); + for (uint8_t b : data) + { // Polynomial division + uint8_t factor = b ^ result.at(0); + result.erase(result.begin()); + result.push_back(0); + for (size_t i = 0; i < result.size(); i++) + result.at(i) ^= reedSolomonMultiply(divisor.at(i), factor); + } + return result; +} + +uint8_t QrCode::reedSolomonMultiply(uint8_t x, uint8_t y) +{ + // Russian peasant multiplication + int z = 0; + for (int i = 7; i >= 0; i--) + { + z = (z << 1) ^ ((z >> 7) * 0x11D); + z ^= ((y >> i) & 1) * x; + } + assert(z >> 8 == 0); + return static_cast(z); +} + +int QrCode::finderPenaltyCountPatterns( + const std::array &runHistory) const +{ + int n = runHistory.at(1); + assert(n <= size * 3); + bool core = n > 0 && runHistory.at(2) == n && runHistory.at(3) == n * 3 + && runHistory.at(4) == n && runHistory.at(5) == n; + return (core && runHistory.at(0) >= n * 4 && runHistory.at(6) >= n ? 1 : 0) + + (core && runHistory.at(6) >= n * 4 && runHistory.at(0) >= n ? 1 : 0); +} + +int QrCode::finderPenaltyTerminateAndCount(bool currentRunColor, + int currentRunLength, + std::array &runHistory) const +{ + if (currentRunColor) + { // Terminate dark run + finderPenaltyAddHistory(currentRunLength, runHistory); + currentRunLength = 0; + } + currentRunLength += size; // Add light border to final run + finderPenaltyAddHistory(currentRunLength, runHistory); + return finderPenaltyCountPatterns(runHistory); +} + +void QrCode::finderPenaltyAddHistory(int currentRunLength, + std::array &runHistory) const +{ + if (runHistory.at(0) == 0) + currentRunLength += size; // Add light border to initial run + std::copy_backward(runHistory.cbegin(), runHistory.cend() - 1, + runHistory.end()); + runHistory.at(0) = currentRunLength; +} + +bool QrCode::getBit(long x, int i) +{ + return ((x >> i) & 1) != 0; +} + +/*---- Tables of constants ----*/ + +const int QrCode::PENALTY_N1 = 3; +const int QrCode::PENALTY_N2 = 3; +const int QrCode::PENALTY_N3 = 40; +const int QrCode::PENALTY_N4 = 10; + +const int8_t QrCode::ECC_CODEWORDS_PER_BLOCK[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal + // value) + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, + // 18, 19, 20, 21, 22, 23, + // 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error + // correction level + {-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, + 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Low + {-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, + 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, + 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28}, // Medium + {-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, + 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // Quartile + {-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, + 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, + 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30}, // High +}; + +const int8_t QrCode::NUM_ERROR_CORRECTION_BLOCKS[4][41] = { + // Version: (note that index 0 is for padding, and is set to an illegal + // value) + // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, + // 21, 22, 23, 24, 25, + // 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40 Error + // correction level + {-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, + 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, + 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25}, // Low + {-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, + 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, + 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49}, // Medium + {-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, + 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, + 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68}, // Quartile + {-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, + 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, + 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81}, // High +}; + +data_too_long::data_too_long(const std::string &msg) + : std::length_error(msg) +{ +} + +/*---- Class BitBuffer ----*/ + +BitBuffer::BitBuffer() + : std::vector() +{ +} + +void BitBuffer::appendBits(std::uint32_t val, int len) +{ + if (len < 0 || len > 31 || val >> len != 0) + throw std::domain_error("Value out of range"); + for (int i = len - 1; i >= 0; i--) // Append bit by bit + this->push_back(((val >> i) & 1) != 0); +} + +} // namespace qrcodegen diff --git a/client/qrcodegen/qrcodegen.h b/client/qrcodegen/qrcodegen.h new file mode 100644 index 000000000..7bbf80ab4 --- /dev/null +++ b/client/qrcodegen/qrcodegen.h @@ -0,0 +1,612 @@ +/* + * QR Code generator library (C++) + * + * Copyright (c) Project Nayuki. (MIT License) + * https://www.nayuki.io/page/qr-code-generator-library + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * - The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * - The Software is provided "as is", without warranty of any kind, express or + * implied, including but not limited to the warranties of merchantability, + * fitness for a particular purpose and noninfringement. In no event shall the + * authors or copyright holders be liable for any claim, damages or other + * liability, whether in an action of contract, tort or otherwise, arising + * from, out of or in connection with the Software or the use or other dealings + * in the Software. + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace qrcodegen +{ + +/* + * A segment of character/binary/control data in a QR Code symbol. + * Instances of this class are immutable. + * The mid-level way to create a segment is to take the payload data + * and call a static factory function such as QrSegment::makeNumeric(). + * The low-level way to create a segment is to custom-make the bit buffer + * and call the QrSegment() constructor with appropriate values. + * This segment class imposes no length restrictions, but QR Codes have + * restrictions. Even in the most favorable conditions, a QR Code can only hold + * 7089 characters of data. Any segment longer than this is meaningless for the + * purpose of generating QR Codes. + */ +class QrSegment final +{ + + /*---- Public helper enumeration ----*/ + + /* + * Describes how a segment's data bits are interpreted. Immutable. + */ +public: + class Mode final + { + + /*-- Constants --*/ + + public: + static const Mode NUMERIC; + + public: + static const Mode ALPHANUMERIC; + + public: + static const Mode BYTE; + + public: + static const Mode KANJI; + + public: + static const Mode ECI; + + /*-- Fields --*/ + + // The mode indicator bits, which is a uint4 value (range 0 to 15). + private: + int modeBits; + + // Number of character count bits for three different version ranges. + private: + int numBitsCharCount[3]; + + /*-- Constructor --*/ + + private: + Mode(int mode, int cc0, int cc1, int cc2); + + /*-- Methods --*/ + + /* + * (Package-private) Returns the mode indicator bits, which is an unsigned + * 4-bit value (range 0 to 15). + */ + public: + int getModeBits() const; + + /* + * (Package-private) Returns the bit width of the character count field for + * a segment in this mode in a QR Code at the given version number. The + * result is in the range [0, 16]. + */ + public: + int numCharCountBits(int ver) const; + }; + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a segment representing the given binary data encoded in + * byte mode. All input byte vectors are acceptable. Any text string + * can be converted to UTF-8 bytes and encoded as a byte mode segment. + */ +public: + static QrSegment makeBytes(const std::vector &data); + + /* + * Returns a segment representing the given string of decimal digits encoded + * in numeric mode. + */ +public: + static QrSegment makeNumeric(const char *digits); + + /* + * Returns a segment representing the given text string encoded in + * alphanumeric mode. The characters allowed are: 0 to 9, A to Z (uppercase + * only), space, dollar, percent, asterisk, plus, hyphen, period, slash, + * colon. + */ +public: + static QrSegment makeAlphanumeric(const char *text); + + /* + * Returns a list of zero or more segments to represent the given text string. + * The result may use various segment modes and switch modes to optimize the + * length of the bit stream. + */ +public: + static std::vector makeSegments(const char *text); + + /* + * Returns a segment representing an Extended Channel Interpretation + * (ECI) designator with the given assignment value. + */ +public: + static QrSegment makeEci(long assignVal); + + /*---- Public static helper functions ----*/ + + /* + * Tests whether the given string can be encoded as a segment in numeric mode. + * A string is encodable iff each character is in the range 0 to 9. + */ +public: + static bool isNumeric(const char *text); + + /* + * Tests whether the given string can be encoded as a segment in alphanumeric + * mode. A string is encodable iff each character is in the following set: 0 + * to 9, A to Z (uppercase only), space, dollar, percent, asterisk, plus, + * hyphen, period, slash, colon. + */ +public: + static bool isAlphanumeric(const char *text); + + /*---- Instance fields ----*/ + + /* The mode indicator of this segment. Accessed through getMode(). */ +private: + const Mode *mode; + + /* The length of this segment's unencoded data. Measured in characters for + * numeric/alphanumeric/kanji mode, bytes for byte mode, and 0 for ECI mode. + * Always zero or positive. Not the same as the data's bit length. + * Accessed through getNumChars(). */ +private: + int numChars; + + /* The data bits of this segment. Accessed through getData(). */ +private: + std::vector data; + + /*---- Constructors (low level) ----*/ + + /* + * Creates a new QR Code segment with the given attributes and data. + * The character count (numCh) must agree with the mode and the bit buffer + * length, but the constraint isn't checked. The given bit buffer is copied + * and stored. + */ +public: + QrSegment(const Mode &md, int numCh, const std::vector &dt); + + /* + * Creates a new QR Code segment with the given parameters and data. + * The character count (numCh) must agree with the mode and the bit buffer + * length, but the constraint isn't checked. The given bit buffer is moved and + * stored. + */ +public: + QrSegment(const Mode &md, int numCh, std::vector &&dt); + + /*---- Methods ----*/ + + /* + * Returns the mode field of this segment. + */ +public: + const Mode &getMode() const; + + /* + * Returns the character count field of this segment. + */ +public: + int getNumChars() const; + + /* + * Returns the data bits of this segment. + */ +public: + const std::vector &getData() const; + + // (Package-private) Calculates the number of bits needed to encode the given + // segments at the given version. Returns a non-negative number if successful. + // Otherwise returns -1 if a segment has too many characters to fit its length + // field, or the total bits exceeds INT_MAX. +public: + static int getTotalBits(const std::vector &segs, int version); + + /*---- Private constant ----*/ + + /* The set of all legal characters in alphanumeric mode, where + * each character value maps to the index in the string. */ +private: + static const char *ALPHANUMERIC_CHARSET; +}; + +/* + * A QR Code symbol, which is a type of two-dimension barcode. + * Invented by Denso Wave and described in the ISO/IEC 18004 standard. + * Instances of this class represent an immutable square grid of dark and light + * cells. The class provides static factory functions to create a QR Code from + * text or binary data. The class covers the QR Code Model 2 specification, + * supporting all versions (sizes) from 1 to 40, all 4 error correction levels, + * and 4 character encoding modes. + * + * Ways to create a QR Code object: + * - High level: Take the payload data and call QrCode::encodeText() or + * QrCode::encodeBinary(). + * - Mid level: Custom-make the list of segments and call + * QrCode::encodeSegments(). + * - Low level: Custom-make the array of data codeword bytes (including + * segment headers and final padding, excluding error correction codewords), + * supply the appropriate version number, and call the QrCode() constructor. + * (Note that all ways require supplying the desired error correction level.) + */ +class QrCode final +{ + + /*---- Public helper enumeration ----*/ + + /* + * The error correction level in a QR Code symbol. + */ +public: + enum class Ecc + { + LOW = 0, // The QR Code can tolerate about 7% erroneous codewords + MEDIUM, // The QR Code can tolerate about 15% erroneous codewords + QUARTILE, // The QR Code can tolerate about 25% erroneous codewords + HIGH, // The QR Code can tolerate about 30% erroneous codewords + }; + + // Returns a value in the range 0 to 3 (unsigned 2-bit integer). +private: + static int getFormatBits(Ecc ecl); + + /*---- Static factory functions (high level) ----*/ + + /* + * Returns a QR Code representing the given Unicode text string at the given + * error correction level. As a conservative upper bound, this function is + * guaranteed to succeed for strings that have 2953 or fewer UTF-8 code units + * (not Unicode code points) if the low error correction level is used. The + * smallest possible QR Code version is automatically chosen for the output. + * The ECC level of the result may be higher than the ecl argument if it can + * be done without increasing the version. + */ +public: + static QrCode encodeText(const char *text, Ecc ecl); + + /* + * Returns a QR Code representing the given binary data at the given error + * correction level. This function always encodes using the binary segment + * mode, not any text mode. The maximum number of bytes allowed is 2953. The + * smallest possible QR Code version is automatically chosen for the output. + * The ECC level of the result may be higher than the ecl argument if it can + * be done without increasing the version. + */ +public: + static QrCode encodeBinary(const std::vector &data, Ecc ecl); + + /*---- Static factory functions (mid level) ----*/ + + /* + * Returns a QR Code representing the given segments with the given encoding + * parameters. The smallest possible QR Code version within the given range is + * automatically chosen for the output. Iff boostEcl is true, then the ECC + * level of the result may be higher than the ecl argument if it can be done + * without increasing the version. The mask number is either between 0 to 7 + * (inclusive) to force that mask, or -1 to automatically choose an + * appropriate mask (which may be slow). This function allows the user to + * create a custom sequence of segments that switches between modes (such as + * alphanumeric and byte) to encode text in less space. This is a mid-level + * API; the high-level API is encodeText() and encodeBinary(). + */ +public: + static QrCode encodeSegments(const std::vector &segs, Ecc ecl, + int minVersion = 1, int maxVersion = 40, + int mask = -1, + bool boostEcl = true); // All optional parameters + + /*---- Instance fields ----*/ + + // Immutable scalar parameters: + + /* The version number of this QR Code, which is between 1 and 40 (inclusive). + * This determines the size of this barcode. */ +private: + int version; + + /* The width and height of this QR Code, measured in modules, between + * 21 and 177 (inclusive). This is equal to version * 4 + 17. */ +private: + int size; + + /* The error correction level used in this QR Code. */ +private: + Ecc errorCorrectionLevel; + + /* The index of the mask pattern used in this QR Code, which is between 0 and + * 7 (inclusive). Even if a QR Code is created with automatic masking + * requested (mask = -1), the resulting object still has a mask value between + * 0 and 7. */ +private: + int mask; + + // Private grids of modules/pixels, with dimensions of size*size: + + // The modules of this QR Code (false = light, true = dark). + // Immutable after constructor finishes. Accessed through getModule(). +private: + std::vector> modules; + + // Indicates function modules that are not subjected to masking. Discarded + // when constructor finishes. +private: + std::vector> isFunction; + + /*---- Constructor (low level) ----*/ + + /* + * Creates a new QR Code with the given version number, + * error correction level, data codeword bytes, and mask number. + * This is a low-level API that most users should not use directly. + * A mid-level API is the encodeSegments() function. + */ +public: + QrCode(int ver, Ecc ecl, const std::vector &dataCodewords, + int msk); + + /*---- Public instance methods ----*/ + + /* + * Returns this QR Code's version, in the range [1, 40]. + */ +public: + int getVersion() const; + + /* + * Returns this QR Code's size, in the range [21, 177]. + */ +public: + int getSize() const; + + /* + * Returns this QR Code's error correction level. + */ +public: + Ecc getErrorCorrectionLevel() const; + + /* + * Returns this QR Code's mask, in the range [0, 7]. + */ +public: + int getMask() const; + + /* + * Returns the color of the module (pixel) at the given coordinates, which is + * false for light or true for dark. The top left corner has the coordinates + * (x=0, y=0). If the given coordinates are out of bounds, then false (light) + * is returned. + */ +public: + bool getModule(int x, int y) const; + + /*---- Private helper methods for constructor: Drawing function modules ----*/ + + // Reads this object's version field, and draws and marks all function + // modules. +private: + void drawFunctionPatterns(); + + // Draws two copies of the format bits (with its own error correction code) + // based on the given mask and this object's error correction level field. +private: + void drawFormatBits(int msk); + + // Draws two copies of the version bits (with its own error correction code), + // based on this object's version field, iff 7 <= version <= 40. +private: + void drawVersion(); + + // Draws a 9*9 finder pattern including the border separator, + // with the center module at (x, y). Modules can be out of bounds. +private: + void drawFinderPattern(int x, int y); + + // Draws a 5*5 alignment pattern, with the center module + // at (x, y). All modules must be in bounds. +private: + void drawAlignmentPattern(int x, int y); + + // Sets the color of a module and marks it as a function module. + // Only used by the constructor. Coordinates must be in bounds. +private: + void setFunctionModule(int x, int y, bool isDark); + + // Returns the color of the module at the given coordinates, which must be in + // range. +private: + bool module(int x, int y) const; + + /*---- Private helper methods for constructor: Codewords and masking ----*/ + + // Returns a new byte string representing the given data with the appropriate + // error correction codewords appended to it, based on this object's version + // and error correction level. +private: + std::vector + addEccAndInterleave(const std::vector &data) const; + + // Draws the given sequence of 8-bit codewords (data and error correction) + // onto the entire data area of this QR Code. Function modules need to be + // marked off before this is called. +private: + void drawCodewords(const std::vector &data); + + // XORs the codeword modules in this QR Code with the given mask pattern. + // The function modules must be marked and the codeword bits must be drawn + // before masking. Due to the arithmetic of XOR, calling applyMask() with + // the same mask value a second time will undo the mask. A final well-formed + // QR Code needs exactly one (not zero, two, etc.) mask applied. +private: + void applyMask(int msk); + + // Calculates and returns the penalty score based on state of this QR Code's + // current modules. This is used by the automatic mask choice algorithm to + // find the mask pattern that yields the lowest score. +private: + long getPenaltyScore() const; + + /*---- Private helper functions ----*/ + + // Returns an ascending list of positions of alignment patterns for this + // version number. Each position is in the range [0,177), and are used on both + // the x and y axes. This could be implemented as lookup table of 40 + // variable-length lists of unsigned bytes. +private: + std::vector getAlignmentPatternPositions() const; + + // Returns the number of data bits that can be stored in a QR Code of the + // given version number, after all function modules are excluded. This + // includes remainder bits, so it might not be a multiple of 8. The result is + // in the range [208, 29648]. This could be implemented as a 40-entry lookup + // table. +private: + static int getNumRawDataModules(int ver); + + // Returns the number of 8-bit data (i.e. not error correction) codewords + // contained in any QR Code of the given version number and error correction + // level, with remainder bits discarded. This stateless pure function could be + // implemented as a (40*4)-cell lookup table. +private: + static int getNumDataCodewords(int ver, Ecc ecl); + + // Returns a Reed-Solomon ECC generator polynomial for the given degree. This + // could be implemented as a lookup table over all possible parameter values, + // instead of as an algorithm. +private: + static std::vector reedSolomonComputeDivisor(int degree); + + // Returns the Reed-Solomon error correction codeword for the given data and + // divisor polynomials. +private: + static std::vector + reedSolomonComputeRemainder(const std::vector &data, + const std::vector &divisor); + + // Returns the product of the two given field elements modulo GF(2^8/0x11D). + // All inputs are valid. This could be implemented as a 256*256 lookup table. +private: + static std::uint8_t reedSolomonMultiply(std::uint8_t x, std::uint8_t y); + + // Can only be called immediately after a light run is added, and + // returns either 0, 1, or 2. A helper function for getPenaltyScore(). +private: + int finderPenaltyCountPatterns(const std::array &runHistory) const; + + // Must be called at the end of a line (row or column) of modules. A helper + // function for getPenaltyScore(). +private: + int finderPenaltyTerminateAndCount(bool currentRunColor, int currentRunLength, + std::array &runHistory) const; + + // Pushes the given value to the front and drops the last value. A helper + // function for getPenaltyScore(). +private: + void finderPenaltyAddHistory(int currentRunLength, + std::array &runHistory) const; + + // Returns true iff the i'th bit of x is set to 1. +private: + static bool getBit(long x, int i); + + /*---- Constants and tables ----*/ + + // The minimum version number supported in the QR Code Model 2 standard. +public: + static constexpr int MIN_VERSION = 1; + + // The maximum version number supported in the QR Code Model 2 standard. +public: + static constexpr int MAX_VERSION = 40; + + // For use in getPenaltyScore(), when evaluating which mask is best. +private: + static const int PENALTY_N1; + +private: + static const int PENALTY_N2; + +private: + static const int PENALTY_N3; + +private: + static const int PENALTY_N4; + +private: + static const std::int8_t ECC_CODEWORDS_PER_BLOCK[4][41]; + +private: + static const std::int8_t NUM_ERROR_CORRECTION_BLOCKS[4][41]; +}; + +/*---- Public exception class ----*/ + +/* + * Thrown when the supplied data does not fit any QR Code version. Ways to + * handle this exception include: + * - Decrease the error correction level if it was greater than Ecc::LOW. + * - If the encodeSegments() function was called with a maxVersion argument, + * then increase it if it was less than QrCode::MAX_VERSION. (This advice does + * not apply to the other factory functions because they search all versions up + * to QrCode::MAX_VERSION.) + * - Split the text data into better or optimal segments in order to reduce the + * number of bits required. + * - Change the text or binary data to be shorter. + * - Change the text to fit the character set of a particular segment mode (e.g. + * alphanumeric). + * - Propagate the error upward to the caller/user. + */ +class data_too_long : public std::length_error +{ + +public: + explicit data_too_long(const std::string &msg); +}; + +/* + * An appendable sequence of bits (0s and 1s). Mainly used by QrSegment. + */ +class BitBuffer final : public std::vector +{ + + /*---- Constructor ----*/ + + // Creates an empty bit buffer (length 0). +public: + BitBuffer(); + + /*---- Method ----*/ + + // Appends the given number of low-order bits of the given value + // to this buffer. Requires 0 <= len <= 31 and val < 2^len. +public: + void appendBits(std::uint32_t val, int len); +}; + +} // namespace qrcodegen diff --git a/client/widgets/ContainerPage.cpp b/client/widgets/ContainerPage.cpp index c7f221f82..2f7d3e3a7 100644 --- a/client/widgets/ContainerPage.cpp +++ b/client/widgets/ContainerPage.cpp @@ -87,7 +87,7 @@ void ContainerPage::cardChanged(const SslCertificate &cert, bool isBlocked) emit certChanged(cert); } -bool ContainerPage::checkAction(int code, const QString& selectedCard, const QString& selectedMobile) +bool ContainerPage::checkIfAlreadySigned(int code, const QString& selectedCard, const QString& selectedMobile) { switch(code) { @@ -153,7 +153,7 @@ void ContainerPage::forward(int code) { if(MobileDialog dlg(this); dlg.exec() == QDialog::Accepted) { - if(checkAction(SignatureMobile, dlg.idCode(), dlg.phoneNo())) + if(checkIfAlreadySigned(SignatureMobile, dlg.idCode(), dlg.phoneNo())) emit action(SignatureMobile, dlg.idCode(), dlg.phoneNo()); } @@ -166,17 +166,7 @@ void ContainerPage::forward(int code) } case SignatureSmartID: { - if(SmartIDDialog dlg(this); dlg.exec() == QDialog::Accepted) - { - if(checkAction(SignatureMobile, dlg.idCode(), {})) - emit action(SignatureSmartID, dlg.country(), dlg.idCode()); - } - - if(QString newCode = Settings::SMARTID_CODE; newCode != mobileCode) - { - mobileCode = std::move(newCode); - showSigningButton(); - } + emit action(SignatureSmartID); break; } case ContainerCancel: @@ -185,7 +175,7 @@ void ContainerPage::forward(int code) emit action(code); break; default: - if(checkAction(code, cardInReader, mobileCode)) + if(checkIfAlreadySigned(code, cardInReader, mobileCode)) emit action(code); break; } diff --git a/client/widgets/ContainerPage.h b/client/widgets/ContainerPage.h index 47ba67afc..c64320e81 100644 --- a/client/widgets/ContainerPage.h +++ b/client/widgets/ContainerPage.h @@ -51,6 +51,7 @@ class ContainerPage final : public QWidget void togglePrinting(bool enable); void transition(CryptoDoc *container, const QSslCertificate &cert); void transition(DigiDoc* container); + bool checkIfAlreadySigned(int code, const QString& selectedCard, const QString& selectedMobile); signals: void action(int code, const QString &info1 = {}, const QString &info2 = {}); @@ -63,7 +64,6 @@ class ContainerPage final : public QWidget private: void changeEvent(QEvent* event) final; - bool checkAction(int code, const QString& selectedCard, const QString& selectedMobile); void elideFileName(); bool eventFilter(QObject *o, QEvent *e) final; void forward(int code);