From 16dea8fd708216aa6b6eb89f232d947e3f754e55 Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Fri, 11 Oct 2024 14:14:25 +0300 Subject: [PATCH] Move QPCSC to digidoc and increase version number IB-7927 Signed-off-by: Raul Metsma --- .github/workflows/build.yml | 22 +- CMakeLists.txt | 5 +- client/Application.cpp | 67 ++++- client/Application.h | 18 +- client/CMakeLists.txt | 7 +- client/CheckConnection.cpp | 6 +- client/CheckConnection.h | 1 - client/Diagnostics.cpp | 3 +- client/Diagnostics_unix.cpp | 1 + client/Diagnostics_win.cpp | 3 +- client/QCNG.cpp | 1 + client/QPCSC.cpp | 439 ++++++++++++++++++++++++++++++ client/QPCSC.h | 124 +++++++++ client/QPCSC_p.h | 201 ++++++++++++++ client/QPKCS11.cpp | 3 +- client/QSmartCard_p.h | 3 +- client/SslCertificate.cpp | 2 +- client/dialogs/SettingsDialog.cpp | 3 +- cmake | 2 +- common | 2 +- qdigidoc4.wxs | 2 +- 21 files changed, 866 insertions(+), 49 deletions(-) create mode 100644 client/QPCSC.cpp create mode 100644 client/QPCSC.h create mode 100644 client/QPCSC_p.h diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f77420712..acfaf57b6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -56,7 +56,7 @@ jobs: container: ubuntu:${{ matrix.container }} strategy: matrix: - container: ['20.04', '22.04', '24.04'] + container: ['20.04', '22.04', '24.04', '24.10'] env: DEBIAN_FRONTEND: noninteractive DEBFULLNAME: github-actions @@ -103,18 +103,12 @@ jobs: container: fedora:${{ matrix.container }} strategy: matrix: - container: [39, 40] + container: [40, 41] steps: - name: Install Deps run: | dnf install -y --setopt=install_weak_deps=False \ git gcc-c++ cmake rpm-build gettext openssl-devel openldap-devel pcsc-lite-devel qt6-qtsvg-devel qt6-qttools-devel flatbuffers-devel flatbuffers-compiler zlib-devel - - name: Install CMake - if: matrix.container == 39 - run: | - dnf install -y --setopt=install_weak_deps=False wget - wget -q https://github.com/Kitware/CMake/releases/download/v3.28.1/cmake-3.28.1-linux-x86_64.sh - sh cmake-3.28.1-linux-x86_64.sh --skip-license --prefix=/usr/local - name: Checkout uses: actions/checkout@v4 with: @@ -143,10 +137,8 @@ jobs: runs-on: ${{ matrix.image }} strategy: matrix: - vcver: [142, 143] + vcver: [143] include: - - vcver: 142 - image: windows-2019 - vcver: 143 image: windows-2022 env: @@ -208,7 +200,7 @@ jobs: coverity: name: Run Coverity tests if: github.repository == 'open-eid/DigiDoc4-Client' && contains(github.ref, 'coverity_scan') - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 env: TOKEN: ${{ secrets.COVERITY_SCAN_TOKEN }} PROJECTNAME: open-eid/DigiDoc4-Client @@ -222,7 +214,7 @@ jobs: with: workflow: build.yml branch: master - name: ubuntu_22.04 + name: ubuntu_24.04 path: libdigidocpp-pkg repo: open-eid/libdigidocpp - name: Install dependencies @@ -251,7 +243,7 @@ jobs: codeql: name: Run CodeQL tests if: github.repository == 'open-eid/DigiDoc4-Client' - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 permissions: security-events: write steps: @@ -264,7 +256,7 @@ jobs: with: workflow: build.yml branch: master - name: ubuntu_22.04 + name: ubuntu_24.04 path: libdigidocpp-pkg repo: open-eid/libdigidocpp - name: Install dependencies diff --git a/CMakeLists.txt b/CMakeLists.txt index b4ff38e42..1880f952f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16) if(NOT EXISTS ${CMAKE_SOURCE_DIR}/cmake/modules/VersionInfo.cmake) message(FATAL_ERROR "cmake submodule directory empty, did you 'git clone --recursive'?") endif() -project(qdigidoc4 VERSION 4.6.1) +project(qdigidoc4 VERSION 4.7.0) set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) set(CMAKE_INCLUDE_CURRENT_DIR ON) @@ -11,7 +11,6 @@ set(CMAKE_AUTOMOC ON) include( GNUInstallDirs ) include( VersionInfo ) -find_package( PKCS11 ) find_package(LibDigiDocpp 4.0.0 REQUIRED) find_package( LDAP REQUIRED ) find_package(QT NAMES Qt6 Qt5 COMPONENTS Core REQUIRED) @@ -25,6 +24,8 @@ set_env(CDOC2_GET_URL "https://cdoc2-keyserver-get" CACHE STRING "CDoc 2.0 Key S set_env(CDOC2_POST_URL "https://cdoc2-keyserver-post" CACHE STRING "CDoc 2.0 Key Server post URL") set_env( MOBILEID_URL "https://dd-mid.ria.ee/mid-api" CACHE STRING "URL for Mobile-ID" ) set_env( SMARTID_URL "https://dd-sid.ria.ee/v1" CACHE STRING "URL for Smart-ID" ) +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED YES) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION YES) set(CMAKE_INTERPROCEDURAL_OPTIMIZATION_DEBUG NO) if(CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") diff --git a/client/Application.cpp b/client/Application.cpp index d06a75099..94132d38b 100644 --- a/client/Application.cpp +++ b/client/Application.cpp @@ -21,6 +21,8 @@ #include "Application.h" +#include "Common.h" +#include "Configuration.h" #include "MainWindow.h" #include "QSigner.h" #include "QSmartCard.h" @@ -40,7 +42,6 @@ class MacMenuBar {}; #include "dialogs/WarningDialog.h" #include "effects/Overlay.h" -#include #include #include @@ -308,13 +309,35 @@ class Application::Private }; Application::Application( int &argc, char **argv ) -#ifdef Q_OS_MAC - : Common(argc, argv, QStringLiteral("qdigidoc4"), QStringLiteral(":/images/Icon.svg")) -#else - : Common(argc, argv, QStringLiteral("qdigidoc4"), QStringLiteral(":/images/digidoc_128.png")) -#endif + : BaseApplication(argc, argv) , d(new Private) { + setApplicationName(QStringLiteral("qdigidoc4")); + setApplicationVersion(QStringLiteral("%1.%2.%3.%4") + .arg( MAJOR_VER ).arg( MINOR_VER ).arg( RELEASE_VER ).arg( BUILD_VER ) ); + setOrganizationDomain(QStringLiteral("ria.ee")); + setOrganizationName(QStringLiteral("RIA")); + setWindowIcon(QIcon(QStringLiteral(":/images/Icon.svg"))); + if(QFile::exists(QStringLiteral("%1/%2.log").arg(QDir::tempPath(), applicationName()))) + qInstallMessageHandler(msgHandler); + + Q_INIT_RESOURCE(common_tr); +#if defined(Q_OS_WIN) + AllowSetForegroundWindow( ASFW_ANY ); +#ifdef NDEBUG + setLibraryPaths({ applicationDirPath() }); +#endif +#elif defined(Q_OS_MAC) + qputenv("OPENSSL_CONF", applicationDirPath().toUtf8() + "../Resources/openssl.cnf"); +#ifdef NDEBUG + setLibraryPaths({ applicationDirPath() + "/../PlugIns" }); +#endif +#endif + setStyleSheet(QStringLiteral( + "QDialogButtonBox { dialogbuttonbox-buttons-have-icons: 0; }\n")); + + QNetworkProxyFactory::setUseSystemConfiguration(true); + QStringList args = arguments(); args.removeFirst(); #ifndef Q_OS_MAC @@ -432,7 +455,7 @@ Application::Application( int &argc, char **argv ) updateTSLCache(QDateTime::currentDateTimeUtc().addDays(-7)); digidoc::initialize(applicationName().toUtf8().constData(), QStringLiteral("%1/%2 (%3)") - .arg(applicationName(), applicationVersion(), applicationOs()).toUtf8().constData(), + .arg(applicationName(), applicationVersion(), Common::applicationOs()).toUtf8().constData(), [](const digidoc::Exception *ex) { qDebug() << "TSL loading finished"; Q_EMIT qApp->TSLLoadingFinished(); @@ -627,9 +650,9 @@ bool Application::event(QEvent *event) // Load here because cocoa NSApplication overides events case QEvent::ApplicationActivate: initMacEvents(); - return Common::event(event); + return BaseApplication::event(event); #endif - default: return Common::event(event); + default: return BaseApplication::event(event); } } @@ -756,6 +779,32 @@ QWidget* Application::mainWindow() return nullptr; } +void Application::msgHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg) +{ + QFile f(QStringLiteral("%1/%2.log").arg(QDir::tempPath(), applicationName())); + if(!f.open( QFile::Append )) + return; + f.write(QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd hh:mm:ss ")).toUtf8()); + switch(type) + { + case QtDebugMsg: f.write("D"); break; + case QtWarningMsg: f.write("W"); break; + case QtCriticalMsg: f.write("C"); break; + case QtFatalMsg: f.write("F"); break; + default: f.write("I"); break; + } + f.write(QStringLiteral(" %1 ").arg(QLatin1String(ctx.category)).toUtf8()); + if(ctx.line > 0) + { + f.write(QStringLiteral("%1:%2 \"%3\" ") + .arg(QFileInfo(QString::fromLatin1(ctx.file)).fileName()) + .arg(ctx.line) + .arg(QLatin1String(ctx.function)).toUtf8()); + } + f.write(msg.toUtf8()); + f.write("\n"); +} + bool Application::notify(QObject *object, QEvent *event) { try diff --git a/client/Application.h b/client/Application.h index e24cbe5a9..b0c028698 100644 --- a/client/Application.h +++ b/client/Application.h @@ -19,10 +19,15 @@ #pragma once -#include +#include -#include -#include +#ifdef Q_OS_MAC +#include +using BaseApplication = QApplication; +#else +#include "qtsingleapplication/src/QtSingleApplication" +using BaseApplication = QtSingleApplication; +#endif #if defined(qApp) #undef qApp @@ -33,12 +38,12 @@ namespace digidoc { class Exception; } class Configuration; class QAction; class QSigner; -class Application final: public Common +class Application final: public BaseApplication { Q_OBJECT public: - enum ConfParameter + enum ConfParameter : quint8 { SiVaUrl, ProxyHost, @@ -86,6 +91,7 @@ private Q_SLOTS: private: bool event(QEvent *event) final; static void closeWindow(); + static void msgHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg); static void parseArgs(const QString &msg = {}); static void parseArgs(QStringList args); static void showWarning(const QString &msg, const digidoc::Exception &e); @@ -102,7 +108,7 @@ private Q_SLOTS: class REOpenEvent: public QEvent { public: - enum { Type = QEvent::User + 1 }; + enum : quint16 { Type = QEvent::User + 1 }; REOpenEvent(): QEvent( QEvent::Type(Type) ) {} }; diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 44377dffd..b2251bcb4 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -73,6 +73,9 @@ add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE QCardLock.h QCryptoBackend.cpp QCryptoBackend.h + QPCSC.cpp + QPCSC_p.h + QPCSC.h QPKCS11.cpp QPKCS11.h QSigner.cpp @@ -118,7 +121,7 @@ set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME ${PROJECT_NAME} MACOSX_BUNDLE_GUI_IDENTIFIER "ee.ria.${PROJECT_NAME}" ) -target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR} ${LIBDIGIDOCPP_INCLUDE_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE ${LIBDIGIDOCPP_INCLUDE_DIR}) target_compile_definitions(${PROJECT_NAME} PRIVATE CDOC2_GET_URL="${CDOC2_GET_URL}" CDOC2_POST_URL="${CDOC2_POST_URL}" @@ -160,6 +163,7 @@ if( APPLE ) set_source_files_properties( Application_mac.mm dialogs/CertificateDetails_mac.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc" ) set_source_files_properties( LdapSearch.cpp PROPERTIES COMPILE_FLAGS "-Wno-deprecated-declarations" ) target_link_libraries(${PROJECT_NAME} "-framework Quartz" "-fobjc-arc") + find_library(PKCS11_MODULE NAMES opensc-pkcs11.so HINTS /Library/OpenSC/lib) add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND cp -a ${PKCS11_MODULE} $ COMMAND mkdir -p $/Library/QuickLook @@ -229,7 +233,6 @@ elseif(WIN32) -ext WixToolset.UI.wixext -bv WixUIDialogBmp=${CMAKE_SOURCE_DIR}/cmake/modules/dlgbmp.bmp -bv WixUIBannerBmp=${CMAKE_SOURCE_DIR}/cmake/modules/banner.bmp - -d MSI_VERSION=${VERSION} -d ico_path=${CMAKE_CURRENT_SOURCE_DIR}/images/digidoc.ico -d libs_path=${LIBS_PATH} -d client_path=$ diff --git a/client/CheckConnection.cpp b/client/CheckConnection.cpp index 426666f2c..689521e9a 100644 --- a/client/CheckConnection.cpp +++ b/client/CheckConnection.cpp @@ -20,6 +20,7 @@ #include "CheckConnection.h" #include "Application.h" +#include "Common.h" #include #include @@ -42,7 +43,6 @@ bool CheckConnection::check(const QUrl &url) return m_error == QNetworkReply::NoError; } -QNetworkReply::NetworkError CheckConnection::error() const { return m_error; } QString CheckConnection::errorDetails() const { return qtmessage; } QString CheckConnection::errorString() const { @@ -106,8 +106,10 @@ QSslConfiguration CheckConnection::sslConfiguration(const QByteArray &add) { QSslConfiguration ssl = QSslConfiguration::defaultConfiguration(); #ifdef CONFIG_URL + const auto list = Application::confValue(QLatin1String("CERT-BUNDLE")).toArray(); QList trusted; - for(const auto &cert: Application::confValue(QLatin1String("CERT-BUNDLE")).toArray()) + 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)); diff --git a/client/CheckConnection.h b/client/CheckConnection.h index d68ffda6c..65f4b63bd 100644 --- a/client/CheckConnection.h +++ b/client/CheckConnection.h @@ -25,7 +25,6 @@ class CheckConnection { public: bool check(const QUrl &url = QStringLiteral("https://id.eesti.ee/config.json")); - QNetworkReply::NetworkError error() const; QString errorString() const; QString errorDetails() const; diff --git a/client/Diagnostics.cpp b/client/Diagnostics.cpp index 9f380b8f8..72dd8608d 100644 --- a/client/Diagnostics.cpp +++ b/client/Diagnostics.cpp @@ -19,6 +19,7 @@ #include "Diagnostics.h" #include "Application.h" +#include "Common.h" #include "QPCSC.h" #include "Settings.h" @@ -148,6 +149,6 @@ void Diagnostics::generalInfo(QTextStream &s) } #ifdef Q_OS_WIN - s << "" << tr("Smart Card reader drivers") << ":
" << QPCSC::instance().drivers().join(QLatin1String("
")); + s << "" << tr("Smart Card reader drivers") << ":
" << Common::drivers().join(QLatin1String("
")); #endif } diff --git a/client/Diagnostics_unix.cpp b/client/Diagnostics_unix.cpp index 88b401594..0cb612322 100644 --- a/client/Diagnostics_unix.cpp +++ b/client/Diagnostics_unix.cpp @@ -21,6 +21,7 @@ #include "Common.h" +#include #include #include #include diff --git a/client/Diagnostics_win.cpp b/client/Diagnostics_win.cpp index 2f634b5e0..c7d5c662c 100644 --- a/client/Diagnostics_win.cpp +++ b/client/Diagnostics_win.cpp @@ -21,6 +21,7 @@ #include "Common.h" +#include #include #include #include @@ -145,7 +146,7 @@ void Diagnostics::run() + ";C:\\Program Files\\Open-EID" + ";C:\\Program Files\\EstIDMinidriver Minidriver" + ";C:\\Program Files (x86)\\EstIDMinidriver Minidriver"); - SetDllDirectory(LPCWSTR(qApp->applicationDirPath().utf16())); + SetDllDirectory(LPCWSTR(QCoreApplication::applicationDirPath().utf16())); static const QStringList dlls{ "digidocpp", "qdigidoc4.exe", "EsteidShellExtension", "id-updater.exe", "EstIDMinidriver", "EstIDMinidriver64", "web-eid.exe", diff --git a/client/QCNG.cpp b/client/QCNG.cpp index ac91ac13a..8dd55f976 100644 --- a/client/QCNG.cpp +++ b/client/QCNG.cpp @@ -19,6 +19,7 @@ #include "QCNG.h" +#include "QPCSC.h" #include "SslCertificate.h" #include "TokenData.h" diff --git a/client/QPCSC.cpp b/client/QPCSC.cpp new file mode 100644 index 000000000..3f56cdbf1 --- /dev/null +++ b/client/QPCSC.cpp @@ -0,0 +1,439 @@ +/* + * QEstEidCommon + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include "QPCSC_p.h" + +#include +#include +#include +#include + +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) +namespace Qt { + using ::hex; +} +#endif + +Q_LOGGING_CATEGORY(APDU,"QPCSC.APDU") +Q_LOGGING_CATEGORY(SCard,"QPCSC.SCard") + +static quint16 toUInt16(const QByteArray &data, int size) +{ + return size >= 2 ? quint16((quint16(data[size - 2]) << 8) | quint16(data[size - 1])) : 0; +} + +static QStringList stateToString(DWORD state) +{ + QStringList result; + #define STATE(X) if(state & SCARD_STATE_##X) result.append(QStringLiteral(#X)) + STATE(IGNORE); + STATE(CHANGED); + STATE(UNKNOWN); + STATE(UNAVAILABLE); + STATE(EMPTY); + STATE(PRESENT); + STATE(ATRMATCH); + STATE(EXCLUSIVE); + STATE(INUSE); + STATE(MUTE); + return result; +} + +template +static auto SCCall(const char *file, int line, const char *function, Func func, Args... args) +{ + auto err = func(args...); + if(SCard().isDebugEnabled()) + QMessageLogger(file, line, function, SCard().categoryName()).debug() + << function << Qt::hex << (unsigned long)err; + return err; +} +#define SC(API, ...) SCCall(__FILE__, __LINE__, "SCard"#API, SCard##API, __VA_ARGS__) + +QHash QPCSCReader::Private::features() +{ + if(!featuresList.isEmpty()) + return featuresList; + DWORD size = 0; + std::array feature{}; + if(SC(Control, card, DWORD(CM_IOCTL_GET_FEATURE_REQUEST), nullptr, 0U, feature.data(), DWORD(feature.size()), &size)) + return featuresList; + for(auto p = feature.cbegin(); std::distance(feature.cbegin(), p) < size; ) + { + unsigned int tag = *p++, len = *p++, value = 0; + for(unsigned int i = 0; i < len; ++i) + value |= *p++ << 8 * i; + featuresList[DRIVER_FEATURES(tag)] = qFromBigEndian(value); + } + return featuresList; +} + + + +QPCSC::QPCSC() + : d(new Private) +{ + const_cast(SCard()).setEnabled(QtDebugMsg, qEnvironmentVariableIsSet("PCSC_DEBUG")); + const_cast(APDU()).setEnabled(QtDebugMsg, qEnvironmentVariableIsSet("APDU_DEBUG")); + Q_UNUSED(serviceRunning()) +} + +QPCSC::~QPCSC() +{ + requestInterruption(); + wait(); + if( d->context ) + SC(ReleaseContext, d->context); + qDeleteAll(d->lock); + delete d; +} + +QPCSC& QPCSC::instance() +{ + static QPCSC pcsc; + return pcsc; +} + +QByteArray QPCSC::rawReaders() const +{ + if( !serviceRunning() ) + return {}; + + DWORD size = 0; + LONG err = SC(ListReaders, d->context, nullptr, nullptr, &size); + if(err != SCARD_S_SUCCESS || !size) + return {}; + + QByteArray data(int(size), 0); + err = SC(ListReaders, d->context, nullptr, data.data(), &size); + if(err != SCARD_S_SUCCESS) + data.clear(); + return data; +} + +QStringList QPCSC::readers() const +{ + QByteArray tmp = rawReaders(); + QStringList readers = QString::fromLocal8Bit(tmp.data(), tmp.size()).split(QChar(0)); + readers.removeAll({}); + return readers; +} + +void QPCSC::run() +{ + QPCSC pcsc; + std::vector list; + while(!isInterruptionRequested()) + { + if(!pcsc.serviceRunning()) + { + sleep(5); + continue; + } + // "\\?PnP?\Notification" does not work on macOS + QByteArray data = pcsc.rawReaders(); + if(data.isEmpty()) + { + sleep(5); + continue; + } + for(const char *name = data.constData(); *name; name += strlen(name) + 1) + { + if(std::none_of(list.cbegin(), list.cend(), [&name](const SCARD_READERSTATE &state) { return strcmp(state.szReader, name) == 0; })) + list.push_back({ strdup(name), nullptr, 0, 0, 0, {} }); + } + if(SC(GetStatusChange, pcsc.d->context, 5*1000U, list.data(), DWORD(list.size())) != SCARD_S_SUCCESS) + continue; + for(auto i = list.begin(); i != list.end(); ) + { + if((i->dwEventState & SCARD_STATE_CHANGED) == 0) + { + ++i; + continue; + } + i->dwCurrentState = i->dwEventState; + qCDebug(SCard) << "New state: " << QString::fromLocal8Bit(i->szReader) << stateToString(i->dwCurrentState); + Q_EMIT statusChanged(QString::fromLocal8Bit(i->szReader), stateToString(i->dwCurrentState)); + if((i->dwCurrentState & (SCARD_STATE_UNKNOWN|SCARD_STATE_IGNORE)) > 0) + { + free((void*)i->szReader); + i = list.erase(i); + } + else + ++i; + } + } +} + +bool QPCSC::serviceRunning() const +{ + if(d->context && SC(IsValidContext, d->context) == SCARD_S_SUCCESS) + return true; + SC(EstablishContext, DWORD(SCARD_SCOPE_USER), nullptr, nullptr, &d->context); + return d->context; +} + + + +QPCSCReader::QPCSCReader( const QString &reader, QPCSC *parent ) + : d(new Private) +{ + if(!parent->d->lock.contains(reader)) + parent->d->lock[reader] = new QMutex(); + parent->d->lock[reader]->lock(); + d->d = parent->d; + d->reader = reader.toUtf8(); + d->state.szReader = d->reader.constData(); + updateState(); +} + +QPCSCReader::~QPCSCReader() +{ + disconnect(); + d->d->lock[d->reader]->unlock(); + delete d; +} + +QByteArray QPCSCReader::atr() const +{ + return QByteArray::fromRawData((const char*)d->state.rgbAtr, int(d->state.cbAtr)).toHex().toUpper(); +} + +bool QPCSCReader::beginTransaction() +{ + return d->isTransacted = SC(BeginTransaction, d->card) == SCARD_S_SUCCESS; +} + +bool QPCSCReader::connect(Connect connect, Mode mode) +{ + return connectEx(connect, mode) == SCARD_S_SUCCESS; +} + +quint32 QPCSCReader::connectEx(Connect connect, Mode mode) +{ + LONG err = SC(Connect, d->d->context, d->state.szReader, connect, mode, &d->card, &d->io.dwProtocol); + updateState(); + return quint32(err); +} + +void QPCSCReader::disconnect( Reset reset ) +{ + if(d->isTransacted) + endTransaction(); + if( d->card ) + SC(Disconnect, d->card, reset); + d->io.dwProtocol = SCARD_PROTOCOL_UNDEFINED; + d->card = {}; + d->featuresList.clear(); + updateState(); +} + +bool QPCSCReader::endTransaction( Reset reset ) +{ + bool result = SC(EndTransaction, d->card, reset) == SCARD_S_SUCCESS; + if(result) + d->isTransacted = false; + return result; +} + +bool QPCSCReader::isPinPad() const +{ + if(d->reader.contains("HID Global OMNIKEY 3x21 Smart Card Reader") || + d->reader.contains("HID Global OMNIKEY 6121 Smart Card Reader")) + return false; + if(qEnvironmentVariableIsSet("SMARTCARDPP_NOPINPAD")) + return false; + QHash features = d->features(); + return features.contains(FEATURE_VERIFY_PIN_DIRECT) || features.contains(FEATURE_VERIFY_PIN_START); +} + +bool QPCSCReader::isPresent() const +{ + return d->state.dwEventState & SCARD_STATE_PRESENT; +} + +QString QPCSCReader::name() const +{ + return QString::fromLocal8Bit( d->reader ); +} + +QHash QPCSCReader::properties() const +{ + QHash properties; + if( DWORD ioctl = d->features().value(FEATURE_GET_TLV_PROPERTIES) ) + { + DWORD size = 0; + std::array recv{}; + if(SC(Control, d->card, ioctl, nullptr, 0U, recv.data(), DWORD(recv.size()), &size)) + return properties; + for(auto p = recv.cbegin(); std::distance(recv.cbegin(), p) < size; ) + { + int tag = *p++, len = *p++, value = 0; + for(int i = 0; i < len; ++i) + value |= *p++ << 8 * i; + properties[Properties(tag)] = value; + } + } + return properties; +} + +bool QPCSCReader::reconnect( Reset reset, Mode mode ) +{ + if( !d->card ) + return false; + LONG err = SC(Reconnect, d->card, DWORD(SCARD_SHARE_SHARED), mode, reset, &d->io.dwProtocol); + updateState(); + return err == SCARD_S_SUCCESS; +} + +QStringList QPCSCReader::state() const +{ + return stateToString(d->state.dwEventState); +} + +QPCSCReader::Result QPCSCReader::transfer( const QByteArray &apdu ) const +{ + QByteArray data( 1024, 0 ); + auto size = DWORD(data.size()); + + qCDebug(APDU).nospace().noquote() << 'T' << d->io.dwProtocol - 1 << "> " << apdu.toHex(); + LONG ret = SC(Transmit, d->card, &d->io, + LPCBYTE(apdu.constData()), DWORD(apdu.size()), nullptr, LPBYTE(data.data()), &size); + if( ret != SCARD_S_SUCCESS ) + return { {}, {}, quint32(ret) }; + + Result result { data.left(int(size - 2)), toUInt16(data, size), quint32(ret) }; + qCDebug(APDU).nospace() << 'T' << d->io.dwProtocol - 1 << "< " << Qt::hex << result.SW; + if(!result.data.isEmpty()) qCDebug(APDU).nospace().noquote() << result.data.toHex(); + + switch(result.SW & 0xFF00) + { + case 0x6100: // Read more + { + QByteArray cmd( "\x00\xC0\x00\x00\x00", 5 ); + cmd[4] = data.at(int(size - 1)); + Result result2 = transfer( cmd ); + result2.data.prepend(result.data); + return result2; + } + case 0x6C00: // Excpected lenght + { + QByteArray cmd = apdu; + cmd[4] = char(result.SW); + return transfer(cmd); + } + default: return result; + } +} + +QPCSCReader::Result QPCSCReader::transferCTL(const QByteArray &apdu, bool verify, + quint16 lang, quint8 minlen, quint8 newPINOffset, bool requestCurrentPIN) const +{ + bool display = false; + QHash features = d->features(); + if( DWORD ioctl = features.value(FEATURE_IFD_PIN_PROPERTIES) ) + { + PIN_PROPERTIES_STRUCTURE caps{}; + DWORD size = sizeof(caps); + if(!SC(Control, d->card, ioctl, nullptr, 0U, &caps, size, &size)) + display = caps.wLcdLayout > 0; + } + + quint8 PINFrameOffset = 0, PINLengthOffset = 0; + auto toByteArray = [&](auto &data) { + data.bTimerOut = 30; + data.bTimerOut2 = 30; + data.bmFormatString = FormatASCII|AlignLeft|quint8(PINFrameOffset << 4)|PINFrameOffsetUnitBits; + data.bmPINBlockString = PINLengthNone << 5|PINFrameSizeAuto; + data.bmPINLengthFormat = PINLengthOffsetUnitBits|PINLengthOffset; + data.wPINMaxExtraDigit = quint16(minlen << 8) | 12; + data.bEntryValidationCondition = ValidOnKeyPressed; + data.wLangId = lang; + data.ulDataLength = quint32(apdu.size()); + return QByteArray((const char*)&data, sizeof(data) - 1) + apdu; + }; + + QByteArray cmd; + if( verify ) + { + PIN_VERIFY_STRUCTURE data{}; + data.bNumberMessage = display ? CCIDDefaultInvitationMessage : NoInvitationMessage; + data.bMsgIndex = NoInvitationMessage; + cmd = toByteArray(data); + } + else + { + PIN_MODIFY_STRUCTURE data{}; + data.bNumberMessage = display ? ThreeInvitationMessage : NoInvitationMessage; + data.bInsertionOffsetOld = 0x00; + data.bInsertionOffsetNew = newPINOffset; + data.bConfirmPIN = ConfirmNewPin; + if(requestCurrentPIN) + { + data.bConfirmPIN |= RequestCurrentPin; + data.bMsgIndex1 = NoInvitationMessage; + data.bMsgIndex2 = OneInvitationMessage; + data.bMsgIndex3 = TwoInvitationMessage; + } + else + { + data.bMsgIndex1 = OneInvitationMessage; + data.bMsgIndex2 = TwoInvitationMessage; + data.bMsgIndex3 = ThreeInvitationMessage; + } + cmd = toByteArray(data); + } + + DWORD ioctl = features.value( verify ? FEATURE_VERIFY_PIN_START : FEATURE_MODIFY_PIN_START ); + if( !ioctl ) + ioctl = features.value( verify ? FEATURE_VERIFY_PIN_DIRECT : FEATURE_MODIFY_PIN_DIRECT ); + + qCDebug(APDU).nospace().noquote() << 'T' << d->io.dwProtocol - 1 << "> " << apdu.toHex(); + qCDebug(APDU).nospace().noquote() << "CTL" << "> " << cmd.toHex(); + QByteArray data( 255 + 3, 0 ); + auto size = DWORD(data.size()); + LONG err = SC(Control, d->card, ioctl, cmd.constData(), DWORD(cmd.size()), LPVOID(data.data()), DWORD(data.size()), &size); + + if( DWORD finish = features.value( verify ? FEATURE_VERIFY_PIN_FINISH : FEATURE_MODIFY_PIN_FINISH ) ) + { + size = DWORD(data.size()); + err = SC(Control, d->card, finish, nullptr, 0U, LPVOID(data.data()), DWORD(data.size()), &size); + } + + Result result { data.left(int(size - 2)), toUInt16(data, size), quint32(err) }; + qCDebug(APDU).nospace() << 'T' << d->io.dwProtocol - 1 << "< " << Qt::hex << result.SW; + if(!result.data.isEmpty()) qCDebug(APDU).nospace().noquote() << result.data.toHex(); + return result; +} + +bool QPCSCReader::updateState( quint32 msec ) +{ + if(!d->d->context) + return false; + d->state.dwCurrentState = d->state.dwEventState; + switch(SC(GetStatusChange, d->d->context, msec, &d->state, 1U)) + { + case LONG(SCARD_S_SUCCESS): return true; + case LONG(SCARD_E_TIMEOUT): return msec == 0; + default: return false; + } +} diff --git a/client/QPCSC.h b/client/QPCSC.h new file mode 100644 index 000000000..20a6fc63a --- /dev/null +++ b/client/QPCSC.h @@ -0,0 +1,124 @@ +/* + * QEstEidCommon + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include + +template class QHash; + +class QPCSCReader; +class QPCSC final: public QThread +{ + Q_OBJECT +public: + ~QPCSC() final; + + static QPCSC& instance(); + QStringList readers() const; + bool serviceRunning() const; + +Q_SIGNALS: + void statusChanged(const QString &reader, const QStringList &state); + +private: + QPCSC(); + Q_DISABLE_COPY(QPCSC) + + QByteArray rawReaders() const; + void run() final; + + class Private; + Private *d; + + friend class QPCSCReader; +}; + +class QPCSCReader final: public QObject +{ + Q_OBJECT +public: + struct Result { + QByteArray data; + quint16 SW; + quint32 err = 0; + constexpr operator bool() const { return SW == 0x9000; } + }; + + enum Properties { + wLcdLayout = 0x01, + bEntryValidationCondition = 0x02, + bTimeOut2 = 0x03, + wLcdMaxCharacters = 0x04, + wLcdMaxLines = 0x05, + bMinPINSize = 0x06, + bMaxPINSize = 0x07, + sFirmwareID = 0x08, + bPPDUSupport = 0x09, + dwMaxAPDUDataSize = 0x0A, + wIdVendor = 0x0B, + wIdProduct = 0x0C + }; + + enum Connect { + Exclusive = 1, + Shared = 2, + Direct = 3 + }; + + enum Reset + { + LeaveCard = 0, + ResetCard = 1, + UnpowerCard = 2, + EjectCard = 3 + }; + + enum Mode { + Undefined = 0, + T0 = 1, + T1 = 2 + }; + + explicit QPCSCReader( const QString &reader, QPCSC *parent ); + ~QPCSCReader() final; + + QByteArray atr() const; + bool isPinPad() const; + bool isPresent() const; + QString name() const; + QHash properties() const; + QStringList state() const; + bool updateState( quint32 msec = 0 ); + + bool connect( Connect connect = Shared, Mode mode = Mode(T0|T1) ); + quint32 connectEx( Connect connect = Shared, Mode mode = Mode(T0|T1) ); + void disconnect( Reset reset = LeaveCard ); + bool reconnect( Reset reset = LeaveCard, Mode mode = Mode(T0|T1) ); + bool beginTransaction(); + bool endTransaction( Reset reset = LeaveCard ); + Result transfer( const QByteArray &apdu ) const; + Result transferCTL(const QByteArray &apdu, bool verify, quint16 lang = 0, + quint8 minlen = 4, quint8 newPINOffset = 0, bool requestCurrentPIN = true) const; + +private: + Q_DISABLE_COPY(QPCSCReader) + class Private; + Private *d; +}; diff --git a/client/QPCSC_p.h b/client/QPCSC_p.h new file mode 100644 index 000000000..4ffa83b5a --- /dev/null +++ b/client/QPCSC_p.h @@ -0,0 +1,201 @@ +/* + * QEstEidCommon + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include "QPCSC.h" + +#include +#include + +#ifdef Q_OS_WIN +#undef UNICODE +#define NOMINMAX +#include +#include +#elif defined(Q_OS_MAC) +#include +#include +#include +#else +#include +#include +#include +#endif + +#ifndef SCARD_CTL_CODE +#define SCARD_CTL_CODE(code) (0x42000000 + (code)) +#endif + +// http://pcscworkgroup.com/Download/Specifications/pcsc10_v2.02.09.pdf +// http://ludovic.rousseau.free.fr/softwares/pcsc-lite/SecurePIN%20discussion%20v5.pdf +#define CM_IOCTL_GET_FEATURE_REQUEST SCARD_CTL_CODE(3400) + +enum DRIVER_FEATURES : quint8 { +FEATURE_VERIFY_PIN_START = 0x01, +FEATURE_VERIFY_PIN_FINISH = 0x02, +FEATURE_MODIFY_PIN_START = 0x03, +FEATURE_MODIFY_PIN_FINISH = 0x04, +FEATURE_GET_KEY_PRESSED = 0x05, +FEATURE_VERIFY_PIN_DIRECT = 0x06, +FEATURE_MODIFY_PIN_DIRECT = 0x07, +FEATURE_MCT_READER_DIRECT = 0x08, +FEATURE_MCT_UNIVERSAL = 0x09, +FEATURE_IFD_PIN_PROPERTIES = 0x0A, +FEATURE_ABORT = 0x0B, +FEATURE_SET_SPE_MESSAGE = 0x0C, +FEATURE_VERIFY_PIN_DIRECT_APP_ID = 0x0D, +FEATURE_MODIFY_PIN_DIRECT_APP_ID = 0x0E, +FEATURE_WRITE_DISPLAY = 0x0F, +FEATURE_GET_KEY = 0x10, +FEATURE_IFD_DISPLAY_PROPERTIES = 0x11, +FEATURE_GET_TLV_PROPERTIES = 0x12, +FEATURE_CCID_ESC_COMMAND = 0x13 +}; + +#pragma pack(push, 1) + +struct PCSC_TLV_STRUCTURE +{ + quint8 tag; + quint8 length; + quint32 value; +}; + +enum bmFormatString : quint8 +{ + FormatBinary = 0, // (1234 => 01h 02h 03h 04h) + FormatBCD = 1 << 0, // (1234 => 12h 34h) + FormatASCII = 1 << 1, // (1234 => 31h 32h 33h 34h) + AlignLeft = 0, + AlignRight = 1 << 2, + PINFrameOffsetUnitBits = 0, + PINFrameOffsetUnitBytes = 1 << 7, +}; + +enum bmPINBlockString : quint8 +{ + PINLengthNone = 0, + PINFrameSizeAuto = 0, +}; + +enum bmPINLengthFormat : quint8 +{ + PINLengthOffsetUnitBits = 0, + PINLengthOffsetUnitBytes = 1 << 5, +}; + +enum bEntryValidationCondition : quint8 +{ + ValidOnMaxSizeReached = 1 << 0, + ValidOnKeyPressed = 1 << 1, + ValidOnTimeout = 1 << 2, +}; + +enum bNumberMessage : quint8 +{ + NoInvitationMessage = 0, + OneInvitationMessage = 1, + TwoInvitationMessage = 2, // MODIFY + ThreeInvitationMessage = 3, // MODIFY + CCIDDefaultInvitationMessage = 0xFF, +}; + +enum bConfirmPIN : quint8 +{ + ConfirmNewPin = 1 << 0, + RequestCurrentPin = 1 << 1, + AdvancedModify = 1 << 2, +}; + +struct PIN_VERIFY_STRUCTURE +{ + quint8 bTimerOut; // timeout in seconds (00 means use default timeout) + quint8 bTimerOut2; // timeout in seconds after first key stroke + quint8 bmFormatString; // formatting options + quint8 bmPINBlockString; // PIN block definition + quint8 bmPINLengthFormat; // PIN length definition + quint16 wPINMaxExtraDigit; // 0xXXYY where XX is minimum PIN size in digits, and YY is maximum PIN size in digits + quint8 bEntryValidationCondition; // Conditions under which PIN entry should be considered complete + quint8 bNumberMessage; // Number of messages to display for PIN verification + quint16 wLangId; // Language for messages (http://www.usb.org/developers/docs/USB_LANGIDs.pdf) + quint8 bMsgIndex; // Message index (should be 00) + quint8 bTeoPrologue[3]; // T=1 I-block prologue field to use (fill with 00) + quint32 ulDataLength; // length of Data to be sent to the ICC + quint8 abData[1]; // Data to send to the ICC +}; + +struct PIN_MODIFY_STRUCTURE +{ + quint8 bTimerOut; // timeout in seconds (00 means use default timeout) + quint8 bTimerOut2; // timeout in seconds after first key stroke + quint8 bmFormatString; // formatting options + quint8 bmPINBlockString; // PIN block definition + quint8 bmPINLengthFormat; // PIN length definition + quint8 bInsertionOffsetOld; // Insertion position offset in bytes for the current PIN + quint8 bInsertionOffsetNew; // Insertion position offset in bytes for the new PIN + quint16 wPINMaxExtraDigit; // 0xXXYY where XX is minimum PIN size in digits, and YY is maximum PIN size in digits + quint8 bConfirmPIN; // Flags governing need for confirmation of new PIN + quint8 bEntryValidationCondition; // Conditions under which PIN entry should be considered complete + quint8 bNumberMessage; // Number of messages to display for PIN verification + quint16 wLangId; // Language for messages (http://www.usb.org/developers/docs/USB_LANGIDs.pdf) + quint8 bMsgIndex1; // Index of the invitation message + quint8 bMsgIndex2; // Index of the invitation message + quint8 bMsgIndex3; // Index of the invitation message + quint8 bTeoPrologue[3]; // T=1 I-block prologue field to use (fill with 00) + quint32 ulDataLength; // length of Data to be sent to the ICC + quint8 abData[1]; // Data to send to the ICC +}; + +struct PIN_PROPERTIES_STRUCTURE +{ + quint16 wLcdLayout; + quint8 bEntryValidationCondition; + quint8 bTimeOut2; +}; + +struct DISPLAY_PROPERTIES_STRUCTURE +{ + quint16 wLcdMaxCharacters; + quint16 wLcdMaxLines; +}; + +#pragma pack(pop) + +class QPCSC::Private +{ +public: + SCARDCONTEXT context {}; + QHash lock; +}; + +class QPCSCReader::Private +{ +public: + QHash features(); + + QPCSC::Private *d {}; + SCARDHANDLE card {}; + SCARD_IO_REQUEST io {SCARD_PROTOCOL_UNDEFINED, sizeof(SCARD_IO_REQUEST)}; + SCARD_READERSTATE state {}; + QByteArray reader; + bool isTransacted {}; + + QHash featuresList; +}; diff --git a/client/QPKCS11.cpp b/client/QPKCS11.cpp index 09cc11b84..709c13eba 100644 --- a/client/QPKCS11.cpp +++ b/client/QPKCS11.cpp @@ -21,12 +21,11 @@ #include "Application.h" #include "Crypto.h" +#include "QPCSC.h" #include "SslCertificate.h" #include "TokenData.h" #include "dialogs/PinPopup.h" -#include - #include #include diff --git a/client/QSmartCard_p.h b/client/QSmartCard_p.h index f9f61a776..458f564b9 100644 --- a/client/QSmartCard_p.h +++ b/client/QSmartCard_p.h @@ -19,11 +19,10 @@ #include "QSmartCard.h" +#include "QPCSC.h" #include "SslCertificate.h" #include "TokenData.h" -#include - #include #include diff --git a/client/SslCertificate.cpp b/client/SslCertificate.cpp index fc89619ee..e7d709fb7 100644 --- a/client/SslCertificate.cpp +++ b/client/SslCertificate.cpp @@ -349,7 +349,7 @@ SslCertificate::Validity SslCertificate::validateOnline() const // Get issuer QNetworkRequest r(urls.values(SslCertificate::ad_CAIssuers).first()); r.setRawHeader("User-Agent", QStringLiteral("%1/%2 (%3)") - .arg(QApplication::applicationName(), QApplication::applicationVersion(), Common::applicationOs()).toUtf8()); + .arg(QCoreApplication::applicationName(), QCoreApplication::applicationVersion(), Common::applicationOs()).toUtf8()); QNetworkReply *repl = m.get(r); e.exec(); QSslCertificate issuer(repl->readAll(), QSsl::Der); diff --git a/client/dialogs/SettingsDialog.cpp b/client/dialogs/SettingsDialog.cpp index 10cd02995..5d2125452 100644 --- a/client/dialogs/SettingsDialog.cpp +++ b/client/dialogs/SettingsDialog.cpp @@ -25,6 +25,7 @@ #include "CertStore.h" #endif #include "CheckConnection.h" +#include "Configuration.h" #include "Diagnostics.h" #include "FileDialog.h" #include "QSigner.h" @@ -39,8 +40,6 @@ #include "effects/Overlay.h" #include "effects/FadeInNotification.h" -#include "common/Configuration.h" - #include #include diff --git a/cmake b/cmake index 8ce75e605..4caaffcae 160000 --- a/cmake +++ b/cmake @@ -1 +1 @@ -Subproject commit 8ce75e6057067ca51d7c619d34d88422bad201e5 +Subproject commit 4caaffcae278ffe357f9489a4d0b23180af556bb diff --git a/common b/common index 27319130f..610b08b41 160000 --- a/common +++ b/common @@ -1 +1 @@ -Subproject commit 27319130f517cc085531d59beda5779b5b223b4e +Subproject commit 610b08b41d19356b68ca1a00ce29184bef5b58fc diff --git a/qdigidoc4.wxs b/qdigidoc4.wxs index d669a008b..182070771 100644 --- a/qdigidoc4.wxs +++ b/qdigidoc4.wxs @@ -19,7 +19,7 @@ + Language="1033" Version="!(bind.FileVersion.qdigidoc4.exe)" Manufacturer="RIA" Scope="perMachine">