From 4ba0798806f6f42f32a212f3b714b0ed04aca0a1 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 6 Aug 2025 13:17:19 +0300 Subject: [PATCH 01/13] Latest DigiDoc/CDoc --- .github/workflows/build.yml | 9 +- .gitmodules | 3 + CMakeLists.txt | 6 - client/Application.cpp | 2 + client/CDoc1.cpp | 627 ------------------------ client/CDoc1.h | 66 --- client/CDoc2.cpp | 778 ------------------------------ client/CDoc2.h | 45 -- client/CDocSupport.cpp | 394 +++++++++++++++ client/CDocSupport.h | 173 +++++++ client/CMakeLists.txt | 36 +- client/CheckConnection.cpp | 2 +- client/Crypto.cpp | 23 +- client/Crypto.h | 4 +- client/CryptoDoc.cpp | 507 +++++++++++-------- client/CryptoDoc.h | 80 +-- client/MainWindow.cpp | 87 +++- client/MainWindow.h | 9 +- client/Utils.h | 29 ++ client/common_enums.h | 1 + client/dialogs/AddRecipients.cpp | 48 +- client/dialogs/AddRecipients.h | 6 +- client/dialogs/KeyDialog.cpp | 43 +- client/dialogs/KeyDialog.h | 5 +- client/dialogs/PasswordDialog.cpp | 136 ++++++ client/dialogs/PasswordDialog.h | 47 ++ client/dialogs/PasswordDialog.ui | 175 +++++++ client/widgets/AddressItem.cpp | 155 ++++-- client/widgets/AddressItem.h | 13 +- client/widgets/AddressItem.ui | 8 + client/widgets/ContainerPage.cpp | 37 +- client/widgets/ContainerPage.h | 6 +- client/widgets/MainAction.cpp | 1 + schema/header.fbs | 59 --- schema/recipients.fbs | 82 ---- 35 files changed, 1617 insertions(+), 2085 deletions(-) delete mode 100644 client/CDoc1.cpp delete mode 100644 client/CDoc1.h delete mode 100644 client/CDoc2.cpp delete mode 100644 client/CDoc2.h create mode 100644 client/CDocSupport.cpp create mode 100644 client/CDocSupport.h create mode 100644 client/dialogs/PasswordDialog.cpp create mode 100644 client/dialogs/PasswordDialog.h create mode 100644 client/dialogs/PasswordDialog.ui delete mode 100644 schema/header.fbs delete mode 100644 schema/recipients.fbs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ffd2e060..415aca4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ permissions: env: BUILD_NUMBER: ${{ github.run_number }} CMAKE_BUILD_PARALLEL_LEVEL: 4 - UBUNTU_DEPS: ./libdigidocpp-pkg/*.deb cmake libldap2-dev gettext libpcsclite-dev libssl-dev libgl-dev libqt6svg6-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools libflatbuffers-dev zlib1g-dev + UBUNTU_DEPS: ./libdigidocpp-pkg/*.deb cmake libldap2-dev gettext libpcsclite-dev libssl-dev libgl-dev libqt6svg6-dev qt6-tools-dev qt6-tools-dev-tools qt6-l10n-tools libflatbuffers-dev libxml2-dev zlib1g-dev jobs: macos: name: Build on macOS @@ -154,7 +154,7 @@ jobs: Rename-Item "libdigidocpp*.msi" libdigidocpp.msi msiexec /qn /i libdigidocpp.msi - name: Prepare vcpkg - uses: lukka/run-vcpkg@v7 + uses: lukka/run-vcpkg@v11 with: vcpkgArguments: openssl zlib flatbuffers vcpkgGitCommitId: 4008642a50a01a7115c2406b04d5273898e7fe1c @@ -173,9 +173,10 @@ jobs: dotnet tool install -g wix --version 5.0.2 wix extension -g add WixToolset.UI.wixext/5.0.2 - name: Build + env: + VCPKG_MANIFEST_DIR: ${{ github.workspace }}/client/libcdoc run: | - cmake "-GNinja" -B build -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo ` - -DCMAKE_TOOLCHAIN_FILE=${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake + cmake "-GNinja" -B build -S . -DCMAKE_BUILD_TYPE=RelWithDebInfo "-DCMAKE_TOOLCHAIN_FILE=${{ env.VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake" -DVCPKG_MANIFEST_DIR=${{ github.workspace }}/client/libcdoc cmake --build build --target msi cmake --build build --target msishellext cmake --build build --target appx diff --git a/.gitmodules b/.gitmodules index eed6069b..a73e2acd 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "common"] path = common url = ../qt-common +[submodule "client/libcdoc"] + path = client/libcdoc + url = git@github.com:open-eid/libcdoc.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b030881..063b514a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,10 +30,6 @@ 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") - set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-object_path_lto,lto.o") - set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,-object_path_lto,lto.o") -endif() set(CPACK_PACKAGE_VERSION ${VERSION}) set(CPACK_GENERATOR RPM) set(CPACK_PACKAGE_CONTACT "RIA ") @@ -48,8 +44,6 @@ find_package(libdigidocpp 4.2.0 REQUIRED HINTS /Library) message("-- Found libdigidocpp: ${libdigidocpp_DIR} (found version \"${libdigidocpp_VERSION}\")") find_package(LDAP REQUIRED) find_package(Qt6 6.2.0 REQUIRED COMPONENTS Core Widgets Network PrintSupport SvgWidgets LinguistTools) -find_package(FlatBuffers CONFIG REQUIRED NAMES FlatBuffers Flatbuffers) -find_package(ZLIB REQUIRED) if(APPLE) add_subdirectory(extensions/DigiDocQL) diff --git a/client/Application.cpp b/client/Application.cpp index c9790204..44e9ea3a 100644 --- a/client/Application.cpp +++ b/client/Application.cpp @@ -23,6 +23,7 @@ #include "Common.h" #include "Configuration.h" +#include "CDocSupport.h" #include "MainWindow.h" #include "QSigner.h" #include "QSmartCard.h" @@ -462,6 +463,7 @@ Application::Application( int &argc, char **argv ) setQuitOnLastWindowClosed( true ); return; } + DDCDocLogger::setUpLogger(); QMetaObject::invokeMethod(this, [this] { #ifdef Q_OS_MAC diff --git a/client/CDoc1.cpp b/client/CDoc1.cpp deleted file mode 100644 index 33879715..00000000 --- a/client/CDoc1.cpp +++ /dev/null @@ -1,627 +0,0 @@ -/* - * QDigiDocClient - * - * 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 "CDoc1.h" - -#include "Application.h" -#include "Crypto.h" -#include "QCryptoBackend.h" -#include "QSigner.h" -#include "Utils.h" -#include "dialogs/FileDialog.h" - -#include -#include -#include -#include -#include - -#include - -const QString CDoc1::AES128CBC_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#aes128-cbc"); -const QString CDoc1::AES192CBC_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#aes192-cbc"); -const QString CDoc1::AES256CBC_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#aes256-cbc"); -const QString CDoc1::AES128GCM_MTH = QStringLiteral("http://www.w3.org/2009/xmlenc11#aes128-gcm"); -const QString CDoc1::AES192GCM_MTH = QStringLiteral("http://www.w3.org/2009/xmlenc11#aes192-gcm"); -const QString CDoc1::AES256GCM_MTH = QStringLiteral("http://www.w3.org/2009/xmlenc11#aes256-gcm"); -const QString CDoc1::RSA_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#rsa-1_5"); -const QString CDoc1::KWAES256_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#kw-aes256"); -const QString CDoc1::CONCATKDF_MTH = QStringLiteral("http://www.w3.org/2009/xmlenc11#ConcatKDF"); -const QString CDoc1::AGREEMENT_MTH = QStringLiteral("http://www.w3.org/2009/xmlenc11#ECDH-ES"); -const QString CDoc1::SHA256_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha256"); -const QString CDoc1::SHA384_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha384"); -const QString CDoc1::SHA512_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha512"); - -const QString CDoc1::DS = QStringLiteral("http://www.w3.org/2000/09/xmldsig#"); -const QString CDoc1::DENC = QStringLiteral("http://www.w3.org/2001/04/xmlenc#"); -const QString CDoc1::DSIG11 = QStringLiteral("http://www.w3.org/2009/xmldsig11#"); -const QString CDoc1::XENC11 = QStringLiteral("http://www.w3.org/2009/xmlenc11#"); - -const QString CDoc1::MIME_ZLIB = QStringLiteral("http://www.isi.edu/in-noes/iana/assignments/media-types/application/zip"); -const QString CDoc1::MIME_DDOC = QStringLiteral("http://www.sk.ee/DigiDoc/v1.3.0/digidoc.xsd"); -const QString CDoc1::MIME_DDOC_OLD = QStringLiteral("http://www.sk.ee/DigiDoc/1.3.0/digidoc.xsd"); - -const QHash CDoc1::ENC_MTH{ - {AES128CBC_MTH, EVP_aes_128_cbc()}, {AES192CBC_MTH, EVP_aes_192_cbc()}, {AES256CBC_MTH, EVP_aes_256_cbc()}, - {AES128GCM_MTH, EVP_aes_128_gcm()}, {AES192GCM_MTH, EVP_aes_192_gcm()}, {AES256GCM_MTH, EVP_aes_256_gcm()}, -}; -const QHash CDoc1::SHA_MTH{ - {SHA256_MTH, QCryptographicHash::Sha256}, {SHA384_MTH, QCryptographicHash::Sha384}, {SHA512_MTH, QCryptographicHash::Sha512} -}; - -CDoc1::CDoc1(const QString &path) - : QFile(path) -{ - setLastError(CryptoDoc::tr("An error occurred while opening the document.")); - if(!open(QFile::ReadOnly)) - return; - readXML(this, [this](QXmlStreamReader &xml) { - // EncryptedData - if(xml.name() == QLatin1String("EncryptedData")) - mime = xml.attributes().value(QLatin1String("MimeType")).toString(); - // EncryptedData/EncryptionProperties/EncryptionProperty - else if(xml.name() == QLatin1String("EncryptionProperty")) - { - for(const QXmlStreamAttribute &attr: xml.attributes()) - { - if(attr.name() != QLatin1String("Name")) - continue; - if(attr.value() == QLatin1String("orig_file")) - { - QStringList fileparts = xml.readElementText().split('|'); - files.push_back({ - fileparts.value(0), - fileparts.value(3), - fileparts.value(2), - fileparts.value(1).toUInt(), - {} - }); - } - else - properties[attr.value().toString()] = xml.readElementText(); - } - } - // EncryptedData/EncryptionMethod - else if(xml.name() == QLatin1String("EncryptionMethod")) - method = xml.attributes().value(QLatin1String("Algorithm")).toString(); - // EncryptedData/KeyInfo/EncryptedKey - if(xml.name() != QLatin1String("EncryptedKey")) - return; - - CKey key; - key.recipient = xml.attributes().value(QLatin1String("Recipient")).toString(); - while(!xml.atEnd()) - { - xml.readNext(); - if(xml.name() == QLatin1String("EncryptedKey") && xml.isEndElement()) - break; - if(!xml.isStartElement()) - continue; - if(xml.name() == QLatin1String("EncryptionMethod")) - { - auto method = xml.attributes().value(QLatin1String("Algorithm")); - key.unsupported = std::max(key.unsupported, method != KWAES256_MTH && method != RSA_MTH); - } - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod - else if(xml.name() == QLatin1String("AgreementMethod")) - key.unsupported = std::max(key.unsupported, xml.attributes().value(QLatin1String("Algorithm")) != AGREEMENT_MTH); - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod - else if(xml.name() == QLatin1String("KeyDerivationMethod")) - key.unsupported = std::max(key.unsupported, xml.attributes().value(QLatin1String("Algorithm")) != CONCATKDF_MTH); - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod/ConcatKDFParams - else if(xml.name() == QLatin1String("ConcatKDFParams")) - { - key.AlgorithmID = QByteArray::fromHex(xml.attributes().value(QLatin1String("AlgorithmID")).toUtf8()); - if(key.AlgorithmID.front() == char(0x00)) key.AlgorithmID.remove(0, 1); - key.PartyUInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyUInfo")).toUtf8()); - if(key.PartyUInfo.front() == char(0x00)) key.PartyUInfo.remove(0, 1); - key.PartyVInfo = QByteArray::fromHex(xml.attributes().value(QLatin1String("PartyVInfo")).toUtf8()); - if(key.PartyVInfo.front() == char(0x00)) key.PartyVInfo.remove(0, 1); - } - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/KeyDerivationMethod/ConcatKDFParams/DigestMethod - else if(xml.name() == QLatin1String("DigestMethod")) - key.concatDigest = xml.attributes().value(QLatin1String("Algorithm")).toString(); - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/AgreementMethod/OriginatorKeyInfo/KeyValue/ECKeyValue/PublicKey - else if(xml.name() == QLatin1String("PublicKey")) - { - xml.readNext(); - key.publicKey = fromBase64(xml.text()); - } - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/X509Data/X509Certificate - else if(xml.name() == QLatin1String("X509Certificate")) - { - xml.readNext(); - key.setCert(QSslCertificate(fromBase64(xml.text()), QSsl::Der)); - } - // EncryptedData/KeyInfo/EncryptedKey/KeyInfo/CipherData/CipherValue - else if(xml.name() == QLatin1String("CipherValue")) - { - xml.readNext(); - key.cipher = fromBase64(xml.text()); - } - } - keys.append(std::move(key)); - }); - if(!keys.isEmpty()) - setLastError({}); - - if(files.empty() && properties.contains(QStringLiteral("Filename"))) - { - files.push_back({ - properties.value(QStringLiteral("Filename")), - {}, - mime == MIME_ZLIB ? properties.value(QStringLiteral("OriginalMimeType")) : mime, - properties.value(QStringLiteral("OriginalSize")).toUInt(), - {} - }); - } -} - -bool CDoc1::decryptPayload(const QByteArray &key) -{ - if(!isOpen()) - return false; - setLastError({}); - QByteArray data; - seek(0); - readXML(this, [&data](QXmlStreamReader &xml) { - // EncryptedData/KeyInfo - if(xml.name() == QLatin1String("KeyInfo")) - xml.skipCurrentElement(); - // EncryptedData/CipherData/CipherValue - else if(xml.name() == QLatin1String("CipherValue")) - { - xml.readNext(); - data = fromBase64(xml.text()); - } - }); - if(data.isEmpty()) - return setLastError(CryptoDoc::tr("Error parsing document")); - data = Crypto::cipher(ENC_MTH[method], key, data, false); - if(data.isEmpty()) - return setLastError(CryptoDoc::tr("Failed to decrypt document")); - - // remove ANSIX923 padding - if(data.size() > 0 && method == AES128CBC_MTH) - { - QByteArray ansix923(data[data.size()-1], 0); - ansix923[ansix923.size()-1] = char(ansix923.size()); - if(data.right(ansix923.size()) == ansix923) - { - qCDebug(CRYPTO) << "Removing ANSIX923 padding size:" << ansix923.size(); - data.resize(data.size() - ansix923.size()); - } - } - - if(mime == MIME_ZLIB) - { - // Add size header for qUncompress compatibilty - unsigned origsize = std::max(properties.value(QStringLiteral("OriginalSize")).toUInt(), 1); - qCDebug(CRYPTO) << "Decompressing zlib content size" << origsize; - QByteArray size(4, 0); - size[0] = char((origsize & 0xff000000) >> 24); - size[1] = char((origsize & 0x00ff0000) >> 16); - size[2] = char((origsize & 0x0000ff00) >> 8); - size[3] = char((origsize & 0x000000ff)); - data = qUncompress(size + data); - mime = properties[QStringLiteral("OriginalMimeType")]; - } - - if(mime == MIME_DDOC || mime == MIME_DDOC_OLD) - { - qCDebug(CRYPTO) << "Contains DDoc content" << mime; - QTemporaryFile ddoc(QDir::tempPath() + "/XXXXXX"); - if(!ddoc.open()) - return setLastError(CryptoDoc::tr("Failed to create temporary files
%1").arg(ddoc.errorString())); - ddoc.write(data); - ddoc.flush(); - ddoc.reset(); - files = readDDoc(&ddoc); - return !files.empty(); - } - - auto buffer = std::make_unique(); - buffer->setData(data); - if(!buffer->open(QBuffer::ReadWrite)) - return false; - qCDebug(CRYPTO) << "Contains raw file" << mime; - if(!files.empty()) - { - files[0].size = data.size(); - files[0].data = std::move(buffer); - } - else if(properties.contains(QStringLiteral("Filename"))) - { - files.push_back({ - properties.value(QStringLiteral("Filename")), - {}, - mime, - data.size(), - std::move(buffer), - }); - } - else - return setLastError(CryptoDoc::tr("Error parsing document")); - - return !files.empty(); -} - -CKey CDoc1::canDecrypt(const QSslCertificate &cert) const -{ - for(const CKey &k: qAsConst(keys)) - { - if(!ENC_MTH.contains(method) || - k.cert != cert || - k.cipher.isEmpty() || - k.unsupported) - continue; - if(cert.publicKey().algorithm() == QSsl::Rsa) - return k; - if(cert.publicKey().algorithm() == QSsl::Ec && - !k.publicKey.isEmpty()) - return k; - } - return {}; -} - -QByteArray CDoc1::fromBase64(QStringView data) -{ - unsigned int buf = 0; - int nbits = 0; - QByteArray result((data.size() * 3) / 4, Qt::Uninitialized); - - int offset = 0; - for(const QChar &i: data) - { - int ch = int(i.toLatin1()); - int d {}; - - if(ch >= 'A' && ch <= 'Z') - d = ch - 'A'; - else if(ch >= 'a' && ch <= 'z') - d = ch - 'a' + 26; - else if(ch >= '0' && ch <= '9') - d = ch - '0' + 52; - else if(ch == '+') - d = 62; - else if(ch == '/') - d = 63; - else - continue; - - buf = (buf << 6) | uint(d); - nbits += 6; - if(nbits >= 8) - { - nbits -= 8; - result[offset++] = char(buf >> nbits); - buf &= (1 << nbits) - 1; - } - } - - result.truncate(offset); - return result; -} - -std::vector CDoc1::readDDoc(QIODevice *ddoc) -{ - qCDebug(CRYPTO) << "Parsing DDOC container"; - std::vector files; - readXML(ddoc, [&files] (QXmlStreamReader &x) { - if(x.name() == QLatin1String("DataFile")) - { - File file; - file.name = x.attributes().value(QLatin1String("Filename")).toString().normalized(QString::NormalizationForm_C); - file.id = x.attributes().value(QLatin1String("Id")).toString().normalized(QString::NormalizationForm_C); - file.mime = x.attributes().value(QLatin1String("MimeType")).toString().normalized(QString::NormalizationForm_C); - x.readNext(); - auto buffer = std::make_unique(); - buffer->setData(fromBase64(x.text())); - buffer->open(QBuffer::ReadWrite); - file.size = buffer->data().size(); - file.data = std::move(buffer); - files.push_back(std::move(file)); - } - }); - return files; -} - -void CDoc1::readXML(QIODevice *io, const std::function &f) -{ - QXmlStreamReader r(io); - while(!r.atEnd()) - { - switch(r.readNext()) - { - case QXmlStreamReader::DTD: - qCWarning(CRYPTO) << "XML DTD Declarations are not supported"; - return; - case QXmlStreamReader::EntityReference: - qCWarning(CRYPTO) << "XML ENTITY References are not supported"; - return; - case QXmlStreamReader::StartElement: - f(r); - break; - default: - break; - } - } -} - -bool CDoc1::save(const QString &path) -{ - setLastError({}); - QFile cdoc(path); - if(!cdoc.open(QFile::WriteOnly)) - return setLastError(cdoc.errorString()); - - QBuffer data; - if(!data.open(QBuffer::WriteOnly)) - return false; - - QString mime, name; - if(files.size() > 1) - { - qCDebug(CRYPTO) << "Creating DDoc container"; - writeDDoc(&data); - mime = MIME_DDOC; - name = QStringLiteral("payload.ddoc"); - } - else - { - qCDebug(CRYPTO) << "Adding raw file"; - files[0].data->seek(0); - copyIODevice(files[0].data.get(), &data); - mime = files[0].mime; - name = files[0].name; - } - - QString method = AES256GCM_MTH; - QByteArray transportKey = Crypto::genKey(ENC_MTH[method]); - if(transportKey.isEmpty()) - return setLastError(QStringLiteral("Failed to generate transport key")); -#ifndef NDEBUG - qDebug() << "ENC Transport Key" << transportKey.toHex(); -#endif - - qCDebug(CRYPTO) << "Writing CDOC file ver 1.1 mime" << mime; - QMultiHash props { - { QStringLiteral("DocumentFormat"), QStringLiteral("ENCDOC-XML|1.1") }, - { QStringLiteral("LibraryVersion"), Application::applicationName() + "|" + Application::applicationVersion() }, - { QStringLiteral("Filename"), name }, - }; - for(const File &f: qAsConst(files)) - props.insert(QStringLiteral("orig_file"), QStringLiteral("%1|%2|%3|%4").arg(f.name).arg(f.size).arg(f.mime).arg(f.id)); - - QXmlStreamWriter w(&cdoc); - w.setAutoFormatting(true); - w.writeStartDocument(); - w.writeNamespace(DENC, QStringLiteral("denc")); - writeElement(w, DENC, QStringLiteral("EncryptedData"), [&]{ - if(!mime.isEmpty()) - w.writeAttribute(QStringLiteral("MimeType"), mime); - writeElement(w, DENC, QStringLiteral("EncryptionMethod"), { - {QStringLiteral("Algorithm"), method}, - }); - w.writeNamespace(DS, QStringLiteral("ds")); - writeElement(w, DS, QStringLiteral("KeyInfo"), [&]{ - for(const CKey &k: qAsConst(keys)) - { - writeElement(w, DENC, QStringLiteral("EncryptedKey"), [&]{ - if(!k.recipient.isEmpty()) - w.writeAttribute(QStringLiteral("Recipient"), k.recipient); - QByteArray cipher; - if(k.isRSA) - { - cipher = Crypto::encrypt(X509_get0_pubkey((const X509*)k.cert.handle()), RSA_PKCS1_PADDING, transportKey); - if(cipher.isEmpty()) - return; - writeElement(w, DENC, QStringLiteral("EncryptionMethod"), { - {QStringLiteral("Algorithm"), RSA_MTH}, - }); - writeElement(w, DS, QStringLiteral("KeyInfo"), [&]{ - writeElement(w, DS, QStringLiteral("X509Data"), [&]{ - writeBase64Element(w, DS, QStringLiteral("X509Certificate"), k.cert.toDer()); - }); - }); - } - else - { - EVP_PKEY *peerPKey = X509_get0_pubkey((const X509*)k.cert.handle()); - auto priv = Crypto::genECKey(peerPKey); - QByteArray sharedSecret = Crypto::derive(priv.get(), peerPKey); - if(sharedSecret.isEmpty()) - return; - - QByteArray oid = Crypto::curve_oid(peerPKey); - QByteArray SsDer = Crypto::toPublicKeyDer(priv.get()); - - QString concatDigest = SHA384_MTH; - switch((SsDer.size() - 1) / 2) { - case 32: concatDigest = SHA256_MTH; break; - case 48: concatDigest = SHA384_MTH; break; - default: concatDigest = SHA512_MTH; break; - } - QByteArray encryptionKey = Crypto::concatKDF(SHA_MTH[concatDigest], - sharedSecret, props.value(QStringLiteral("DocumentFormat")).toUtf8() + SsDer + k.cert.toDer()); -#ifndef NDEBUG - qDebug() << "ENC Ss" << SsDer.toHex(); - qDebug() << "ENC Ksr" << sharedSecret.toHex(); - qDebug() << "ENC ConcatKDF" << encryptionKey.toHex(); -#endif - - cipher = Crypto::aes_wrap(encryptionKey, transportKey, true); - if(cipher.isEmpty()) - return; - - writeElement(w, DENC, QStringLiteral("EncryptionMethod"), { - {QStringLiteral("Algorithm"), KWAES256_MTH}, - }); - writeElement(w, DS, QStringLiteral("KeyInfo"), [&]{ - writeElement(w, DENC, QStringLiteral("AgreementMethod"), { - {QStringLiteral("Algorithm"), AGREEMENT_MTH}, - }, [&]{ - w.writeNamespace(XENC11, QStringLiteral("xenc11")); - writeElement(w, XENC11, QStringLiteral("KeyDerivationMethod"), { - {QStringLiteral("Algorithm"), CONCATKDF_MTH}, - }, [&]{ - writeElement(w, XENC11, QStringLiteral("ConcatKDFParams"), { - {QStringLiteral("AlgorithmID"), QStringLiteral("00") + props.value(QStringLiteral("DocumentFormat")).toUtf8().toHex()}, - {QStringLiteral("PartyUInfo"), QStringLiteral("00") + SsDer.toHex()}, - {QStringLiteral("PartyVInfo"), QStringLiteral("00") + k.cert.toDer().toHex()}, - }, [&]{ - writeElement(w, DS, QStringLiteral("DigestMethod"), { - {QStringLiteral("Algorithm"), concatDigest}, - }); - }); - }); - writeElement(w, DENC, QStringLiteral("OriginatorKeyInfo"), [&]{ - writeElement(w, DS, QStringLiteral("KeyValue"), [&]{ - w.writeNamespace(DSIG11, QStringLiteral("dsig11")); - writeElement(w, DSIG11, QStringLiteral("ECKeyValue"), [&]{ - writeElement(w, DSIG11, QStringLiteral("NamedCurve"), { - {QStringLiteral("URI"), QStringLiteral("urn:oid:") + oid}, - }); - writeBase64Element(w, DSIG11, QStringLiteral("PublicKey"), SsDer); - }); - }); - }); - writeElement(w, DENC, QStringLiteral("RecipientKeyInfo"), [&]{ - writeElement(w, DS, QStringLiteral("X509Data"), [&]{ - writeBase64Element(w, DS, QStringLiteral("X509Certificate"), k.cert.toDer()); - }); - }); - }); - }); - } - writeElement(w, DENC, QStringLiteral("CipherData"), [&]{ - writeBase64Element(w, DENC, QStringLiteral("CipherValue"), cipher); - }); - }); - }}); - writeElement(w,DENC, QStringLiteral("CipherData"), [&]{ - writeBase64Element(w, DENC, QStringLiteral("CipherValue"), - Crypto::cipher(ENC_MTH[method], transportKey, data.buffer(), true) - ); - }); - writeElement(w, DENC, QStringLiteral("EncryptionProperties"), [&]{ - for(QMultiHash::const_iterator i = props.constBegin(); i != props.constEnd(); ++i) - writeElement(w, DENC, QStringLiteral("EncryptionProperty"), { - {QStringLiteral("Name"), i.key()}, - }, [&]{ - w.writeCharacters(i.value()); - }); - }); - }); - w.writeEndDocument(); - return true; -} - -QByteArray CDoc1::transportKey(const CKey &key) -{ - setLastError({}); - QByteArray decryptedKey = qApp->signer()->decrypt([&key](QCryptoBackend *backend) { - if(key.isRSA) - return backend->decrypt(key.cipher, false); - return backend->deriveConcatKDF(key.publicKey, SHA_MTH[key.concatDigest], - key.AlgorithmID, key.PartyUInfo, key.PartyVInfo); - }); - if(decryptedKey.isEmpty()) - { - setLastError(QStringLiteral("Failed to decrypt/derive key")); - return {}; - } - if(key.isRSA) - return decryptedKey; -#ifndef NDEBUG - qDebug() << "DEC Ss" << key.publicKey.toHex(); - qDebug() << "DEC ConcatKDF" << decryptedKey.toHex(); -#endif - return Crypto::aes_wrap(decryptedKey, key.cipher, false); -} - -int CDoc1::version() -{ - return 1; -} - -void CDoc1::writeAttributes(QXmlStreamWriter &x, const QMap &attrs) -{ - for(QMap::const_iterator i = attrs.cbegin(), end = attrs.cend(); i != end; ++i) - x.writeAttribute(i.key(), i.value()); -} - -void CDoc1::writeBase64Element(QXmlStreamWriter &x, const QString &ns, const QString &name, const QByteArray &data) -{ - x.writeStartElement(ns, name); - for(int i = 0; i < data.size(); i+=48) - x.writeCharacters(data.mid(i, 48).toBase64() + '\n'); - x.writeEndElement(); -} - -void CDoc1::writeDDoc(QIODevice *ddoc) -{ - qCDebug(CRYPTO) << "Creating DDOC container"; - QXmlStreamWriter x(ddoc); - x.setAutoFormatting(true); - x.writeStartDocument(); - x.writeDefaultNamespace(QStringLiteral("http://www.sk.ee/DigiDoc/v1.3.0#")); - x.writeStartElement(QStringLiteral("SignedDoc")); - writeAttributes(x, { - {QStringLiteral("format"), QStringLiteral("DIGIDOC-XML")}, - {QStringLiteral("version"), QStringLiteral("1.3")}, - }); - - for(const File &file: qAsConst(files)) - { - x.writeStartElement(QStringLiteral("DataFile")); - writeAttributes(x, { - {QStringLiteral("ContentType"), QStringLiteral("EMBEDDED_BASE64")}, - {QStringLiteral("Filename"), file.name}, - {QStringLiteral("Id"), file.id}, - {QStringLiteral("MimeType"), file.mime}, - {QStringLiteral("Size"), QString::number(file.size)}, - }); - std::array buf{}; - file.data->seek(0); - for(auto size = file.data->read(buf.data(), buf.size()); size > 0; size = file.data->read(buf.data(), buf.size())) - x.writeCharacters(QByteArray::fromRawData(buf.data(), size).toBase64() + '\n'); - x.writeEndElement(); //DataFile - } - - x.writeEndElement(); //SignedDoc - x.writeEndDocument(); -} - -void CDoc1::writeElement(QXmlStreamWriter &x, const QString &ns, const QString &name, std::function &&f) -{ - x.writeStartElement(ns, name); - if(f) - f(); - x.writeEndElement(); -} - -void CDoc1::writeElement(QXmlStreamWriter &x, const QString &ns, const QString &name, const QMap &attrs, std::function &&f) -{ - x.writeStartElement(ns, name); - writeAttributes(x, attrs); - if(f) - f(); - x.writeEndElement(); -} diff --git a/client/CDoc1.h b/client/CDoc1.h deleted file mode 100644 index 5cf1f46a..00000000 --- a/client/CDoc1.h +++ /dev/null @@ -1,66 +0,0 @@ -/* - * QDigiDocClient - * - * 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 "CryptoDoc.h" - -#include -#include - -class QXmlStreamReader; -class QXmlStreamWriter; - -using EVP_CIPHER = struct evp_cipher_st; - -class CDoc1 final: public CDoc, private QFile -{ -public: - CDoc1() = default; - CDoc1(const QString &path); - CKey canDecrypt(const QSslCertificate &cert) const final; - bool decryptPayload(const QByteArray &key) final; - bool save(const QString &path) final; - QByteArray transportKey(const CKey &key) final; - int version() final; - -private: - void writeDDoc(QIODevice *ddoc); - - static QByteArray fromBase64(QStringView data); - static std::vector readDDoc(QIODevice *ddoc); - static void readXML(QIODevice *io, const std::function &f); - static void writeAttributes(QXmlStreamWriter &x, const QMap &attrs); - static void writeBase64Element(QXmlStreamWriter &x, const QString &ns, const QString &name, const QByteArray &data); - static void writeElement(QXmlStreamWriter &x, const QString &ns, const QString &name, std::function &&f = {}); - static void writeElement(QXmlStreamWriter &x, const QString &ns, const QString &name, const QMap &attrs, std::function &&f = {}); - - QString method, mime; - QHash properties; - - static const QString - AES128CBC_MTH, AES192CBC_MTH, AES256CBC_MTH, - AES128GCM_MTH, AES192GCM_MTH, AES256GCM_MTH, - SHA256_MTH, SHA384_MTH, SHA512_MTH, - RSA_MTH, CONCATKDF_MTH, AGREEMENT_MTH, KWAES256_MTH; - static const QString DS, DENC, DSIG11, XENC11; - static const QString MIME_ZLIB, MIME_DDOC, MIME_DDOC_OLD; - static const QHash ENC_MTH; - static const QHash SHA_MTH; -}; diff --git a/client/CDoc2.cpp b/client/CDoc2.cpp deleted file mode 100644 index 12a9b637..00000000 --- a/client/CDoc2.cpp +++ /dev/null @@ -1,778 +0,0 @@ -/* - * QDigiDocClient - * - * 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 "CDoc2.h" - -#include "Application.h" -#include "CheckConnection.h" -#include "Crypto.h" -#include "QCryptoBackend.h" -#include "QSigner.h" -#include "Settings.h" -#include "TokenData.h" -#include "Utils.h" -#include "header_generated.h" -#include "effects/FadeInNotification.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include - -#include - -const QByteArray CDoc2::LABEL = "CDOC\x02"; -const QByteArray CDoc2::CEK = "CDOC20cek"; -const QByteArray CDoc2::HMAC = "CDOC20hmac"; -const QByteArray CDoc2::KEK = "CDOC20kek"; -const QByteArray CDoc2::KEKPREMASTER = "CDOC20kekpremaster"; -const QByteArray CDoc2::PAYLOAD = "CDOC20payload"; -const QByteArray CDoc2::SALT = "CDOC20salt"; - -namespace cdoc20 { - bool checkConnection() { - if(CheckConnection().check()) - return true; - return dispatchToMain([] { - FadeInNotification::error(Application::mainWindow()->findChild(QStringLiteral("topBar")), - QCoreApplication::translate("MainWindow", "Check internet connection")); - return false; - }); - } - - QNetworkRequest req(const QString &keyserver_id, const QString &transaction_id = {}) { -#ifdef CONFIG_URL - QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); - QJsonObject data = list.value(keyserver_id).toObject(); - QString url = transaction_id.isEmpty() ? - data.value(QLatin1String("POST")).toString(Settings::CDOC2_POST) : - data.value(QLatin1String("FETCH")).toString(Settings::CDOC2_GET); -#else - QString url = transaction_id.isEmpty() ? Settings::CDOC2_POST : Settings::CDOC2_GET; -#endif - if(url.isEmpty()) - return QNetworkRequest{}; - QNetworkRequest req(QStringLiteral("%1/key-capsules%2").arg(url, - transaction_id.isEmpty() ? QString(): QStringLiteral("/%1").arg(transaction_id))); - req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); - return req; - } - - struct stream final: public QIODevice - { - static constexpr qint64 CHUNK = 16LL * 1024LL; - QIODevice *io {}; - Crypto::Cipher *cipher {}; - z_stream s {}; - QByteArray buf; - int flush = Z_NO_FLUSH; - - stream(QIODevice *_io, Crypto::Cipher *_cipher) - : io(_io) - , cipher(_cipher) - { - if(io->isReadable()) - { - if(inflateInit2(&s, MAX_WBITS) == Z_OK) - open(QIODevice::ReadOnly); - } - if(io->isWritable()) - { - if(deflateInit(&s, Z_DEFAULT_COMPRESSION) == Z_OK) - open(QIODevice::WriteOnly); - } - } - - ~stream() final - { - if(io->isReadable()) - inflateEnd(&s); - if(io->isWritable()) - { - flush = Z_FINISH; - writeData(nullptr, 0); - deflateEnd(&s); - } - } - - bool isSequential() const final - { - return true; - } - - qint64 bytesAvailable() const final - { - return (io->bytesAvailable() - Crypto::Cipher::tagLen()) + buf.size() + QIODevice::bytesAvailable(); - } - - qint64 readData(char *data, qint64 maxlen) final - { - s.next_out = (Bytef*)data; - s.avail_out = uInt(maxlen); - std::array in{}; - for(int res = Z_OK; s.avail_out > 0 && res == Z_OK;) - { - if(auto insize = io->bytesAvailable() - Crypto::Cipher::tagLen(), - size = io->read(in.data(), qMin(insize, in.size())); size > 0) - { - if(!cipher->update(in.data(), int(size))) - return -1; - buf.append(in.data(), size); - } - s.next_in = (z_const Bytef*)buf.data(); - s.avail_in = uInt(buf.size()); - switch(res = inflate(&s, flush)) - { - case Z_OK: - buf = buf.right(s.avail_in); - break; - case Z_STREAM_END: - buf.clear(); - break; - default: return -1; - } - } - return qint64(maxlen - s.avail_out); - } - - qint64 writeData(const char *data, qint64 len) final - { - s.next_in = (z_const Bytef *)data; - s.avail_in = uInt(len); - std::array out{}; - while(true) { - s.next_out = (Bytef *)out.data(); - s.avail_out = out.size(); - int res = deflate(&s, flush); - if(res == Z_STREAM_ERROR) - return -1; - if(auto size = out.size() - s.avail_out; size > 0) - { - if(!cipher->update(out.data(), int(size)) || - io->write(out.data(), size) != size) - return -1; - } - if(res == Z_STREAM_END) - break; - if(flush == Z_FINISH) - continue; - if(s.avail_in == 0) - break; - } - return len; - } - }; - - struct TAR { - std::unique_ptr io; - explicit TAR(std::unique_ptr &&in) - : io(std::move(in)) - {} - - bool save(const std::vector &files) - { - auto writeHeader = [this](Header &h, qint64 size) { - h.chksum.fill(' '); - toOctal(h.size, size); - toOctal(h.chksum, h.checksum().first); - return io->write((const char*)&h, Header::Size) == Header::Size; - }; - auto writePadding = [this](qint64 size) { - QByteArray pad(padding(size), 0); - return io->write(pad) == pad.size(); - }; - auto toPaxRecord = [](const QByteArray &keyword, const QByteArray &value) { - QByteArray record = ' ' + keyword + '=' + value + '\n'; - QByteArray result; - for(auto len = record.size(); result.size() != len; ++len) - result = QByteArray::number(len + 1) + record; - return result; - }; - for(const CDoc::File &file: files) - { - Header h {}; - QByteArray filename = file.name.toUtf8(); - QByteArray filenameTruncated = filename.left(h.name.size()); - std::copy(filenameTruncated.cbegin(), filenameTruncated.cend(), h.name.begin()); - - if(filename.size() > 100 || - file.size > 07777777) - { - h.typeflag = 'x'; - QByteArray paxData; - if(filename.size() > 100) - paxData += toPaxRecord("path", filename); - if(file.size > 07777777) - paxData += toPaxRecord("size", QByteArray::number(file.size)); - if(!writeHeader(h, paxData.size()) || - io->write(paxData) != paxData.size() || - !writePadding(paxData.size())) - return false; - } - - h.typeflag = '0'; - if(!writeHeader(h, file.size)) - return false; - file.data->seek(0); - if(auto size = copyIODevice(file.data.get(), io.get()); size < 0 || !writePadding(size)) - return false; - } - return io->write((const char*)&Header::Empty, Header::Size) == Header::Size && - io->write((const char*)&Header::Empty, Header::Size) == Header::Size; - } - - std::vector files(bool &warning) const - { - std::vector result; - Header h {}; - auto readHeader = [&h, this] { return io->read((char*)&h, Header::Size) == Header::Size; }; - while(io->bytesAvailable() > 0) - { - if(!readHeader()) - return {}; - if(h.isNull()) - { - if(!readHeader() && !h.isNull()) - return {}; - warning = io->bytesAvailable() > 0; - return result; - } - if(!h.verify()) - return {}; - - CDoc::File f; - f.name = QString::fromUtf8(h.name.data(), std::min(h.name.size(), int(strlen(h.name.data())))); - f.size = fromOctal(h.size); - if(h.typeflag == 'x') - { - QByteArray paxData = io->read(f.size); - if(paxData.size() != f.size) - return {}; - io->skip(padding(f.size)); - if(!readHeader() || h.isNull() || !h.verify()) - return {}; - if(f.name.startsWith(QLatin1String("./PaxHeaders.X"))) - f.name = QString::fromUtf8(h.name.data(), std::min(h.name.size(), int(strlen(h.name.data())))); - f.size = fromOctal(h.size); - for(const QByteArray &data: paxData.split('\n')) - { - if(data.isEmpty()) - break; - const auto &headerValue = data.split('='); - const auto &lenKeyword = headerValue[0].split(' '); - if(data.size() + 1 != lenKeyword[0].toUInt()) - return {}; - if(lenKeyword[1] == "path") - f.name = QString::fromUtf8(headerValue[1]); - if(lenKeyword[1] == "size") - f.size = headerValue[1].toUInt(); - } - } - - if(h.typeflag == '0' || h.typeflag == 0) - { - if(f.size > 500L * 1024L * 1024L) - f.data = std::make_unique(); - else - f.data = std::make_unique(); - f.data->open(QIODevice::ReadWrite); - if(f.size != copyIODevice(io.get(), f.data.get(), f.size)) - return {}; - io->skip(padding(f.size)); - result.push_back(std::move(f)); - } - else - io->skip(f.size + padding(f.size)); - } - return result; - } - - private: - struct Header { - std::array name; - std::array mode; - std::array uid; - std::array gid; - std::array size; - std::array mtime; - std::array chksum; - char typeflag; - std::array linkname; - std::array magic; - std::array version; - std::array uname; - std::array gname; - std::array devmajor; - std::array devminor; - std::array prefix; - std::array padding; - - std::pair checksum() const - { - int64_t unsignedSum = 0; - int64_t signedSum = 0; - for (size_t i = 0, size = sizeof(Header); i < size; i++) { - unsignedSum += ((unsigned char*) this)[i]; - signedSum += ((signed char*) this)[i]; - } - return {unsignedSum, signedSum}; - } - - bool isNull() { - return memcmp(this, &Empty, sizeof(Header)) == 0; - } - - bool verify() { - auto copy = chksum; - chksum.fill(' '); - auto checkSum = checksum(); - chksum.swap(copy); - int64_t referenceChecksum = fromOctal(chksum); - return referenceChecksum == checkSum.first || - referenceChecksum == checkSum.second; - } - - static const Header Empty; - static const int Size; - }; - - template - static int64_t fromOctal(const std::array &data) - { - int64_t i = 0; - for(const char c: data) - { - if(c < '0' || c > '7') - continue; - i <<= 3; - i += c - '0'; - } - return i; - } - - template - static void toOctal(std::array &data, int64_t value) - { - data.fill(' '); - for(auto it = data.rbegin() + 1; it != data.rend(); ++it) - { - *it = char(value & 7) + '0'; - value >>= 3; - } - } - - static constexpr int padding(int64_t size) - { - return Header::Size - size % Header::Size; - } - }; - - const TAR::Header TAR::Header::Empty {}; - const int TAR::Header::Size = int(sizeof(TAR::Header)); -} - -CDoc2::CDoc2(const QString &path) - : QFile(path) -{ - using namespace cdoc20::Recipients; - using namespace cdoc20::Header; - setLastError(QStringLiteral("Invalid CDoc 2.0 header")); - uint32_t header_len = 0; - if(!open(QFile::ReadOnly) || - read(LABEL.length()) != LABEL || - read((char*)&header_len, int(sizeof(header_len))) != int(sizeof(header_len))) - return; - header_data = read(qFromBigEndian(header_len)); - if(header_data.size() != qFromBigEndian(header_len)) - return; - headerHMAC = read(KEY_LEN); - if(headerHMAC.size() != KEY_LEN) - return; - noncePos = pos(); - flatbuffers::Verifier verifier(reinterpret_cast(header_data.data()), header_data.size()); - if(!VerifyHeaderBuffer(verifier)) - return; - const auto *header = GetHeader(header_data.constData()); - if(!header) - return; - if(header->payload_encryption_method() != PayloadEncryptionMethod::CHACHA20POLY1305) - return; - const auto *recipients = header->recipients(); - if(!recipients) - return; - setLastError({}); - - auto toByteArray = [](const flatbuffers::Vector *data) -> QByteArray { - return data ? QByteArray((const char*)data->Data(), int(data->size())) : QByteArray(); - }; - auto toString = [](const flatbuffers::String *data) -> QString { - return data ? QString::fromUtf8(data->c_str(), data->size()) : QString(); - }; - for(const auto *recipient: *recipients){ - if(recipient->fmk_encryption_method() != FMKEncryptionMethod::XOR) - { - keys.append(CKey::Unsupported); - qWarning() << "Unsupported FMK encryption method: skipping"; - continue; - } - auto fillRecipient = [&] (auto key, bool isRSA, bool unsupported = false) { - CKey k(toByteArray(key->recipient_public_key()), isRSA); - k.recipient = toString(recipient->key_label()); - k.cipher = toByteArray(recipient->encrypted_fmk()); - k.unsupported = unsupported; - return k; - }; - switch(recipient->capsule_type()) - { - case Capsule::ECCPublicKeyCapsule: - if(const auto *key = recipient->capsule_as_ECCPublicKeyCapsule()) - { - CKey k = fillRecipient(key, false, key->curve() != EllipticCurve::secp384r1); - k.publicKey = toByteArray(key->sender_public_key()); - keys.append(std::move(k)); - } - break; - case Capsule::RSAPublicKeyCapsule: - if(const auto *key = recipient->capsule_as_RSAPublicKeyCapsule()) - { - CKey k = fillRecipient(key, true); - k.encrypted_kek = toByteArray(key->encrypted_kek()); - keys.append(std::move(k)); - } - break; - case Capsule::KeyServerCapsule: - if(const auto *server = recipient->capsule_as_KeyServerCapsule()) - { - auto fillKeyServer = [&] (auto key, bool isRSA, bool unsupported = false) { - CKey k = fillRecipient(key, isRSA, unsupported); - k.keyserver_id = toString(server->keyserver_id()); - k.transaction_id = toString(server->transaction_id()); - return k; - }; - switch(server->recipient_key_details_type()) - { - case ServerDetailsUnion::ServerEccDetails: - if(const auto *eccDetails = server->recipient_key_details_as_ServerEccDetails()) - keys.append(fillKeyServer(eccDetails, false, eccDetails->curve() != EllipticCurve::secp384r1)); - break; - case ServerDetailsUnion::ServerRsaDetails: - if(const auto *rsaDetails = server->recipient_key_details_as_ServerRsaDetails()) - keys.append(fillKeyServer(rsaDetails, true)); - break; - default: - keys.append(CKey::Unsupported); - qWarning() << "Unsupported Key Server Details: skipping"; - } - } - break; - default: - keys.append(CKey::Unsupported); - qWarning() << "Unsupported Key Details: skipping"; - } - } -} - -CKey CDoc2::canDecrypt(const QSslCertificate &cert) const -{ - auto key = keys.value(keys.indexOf(CKey(cert))); - if(key.unsupported || (!key.transaction_id.isEmpty() && cert.expiryDate() <= QDateTime::currentDateTimeUtc())) - return {}; - return key; -} - -bool CDoc2::decryptPayload(const QByteArray &fmk) -{ - if(!isOpen() || noncePos == -1) - return false; - setLastError({}); - seek(noncePos); - QByteArray cek = Crypto::expand(fmk, CEK); - QByteArray nonce = read(NONCE_LEN); -#ifndef NDEBUG - qDebug() << "cek" << cek.toHex(); - qDebug() << "nonce" << nonce.toHex(); -#endif - Crypto::Cipher dec(EVP_chacha20_poly1305(), cek, nonce, false); - if(!dec.updateAAD(PAYLOAD + header_data + headerHMAC)) - return false; - bool warning = false; - files = cdoc20::TAR(std::unique_ptr(new cdoc20::stream(this, &dec))).files(warning); - if(warning) - setLastError(tr("CDoc contains additional payload data that is not part of content")); - QByteArray tag = read(16); -#ifndef NDEBUG - qDebug() << "tag" << tag.toHex(); -#endif - dec.setTag(tag); - if(!dec.result()) - files.clear(); - return !files.empty(); -} - -bool CDoc2::save(const QString &path) -{ - setLastError({}); - QByteArray fmk = Crypto::extract(Crypto::random(KEY_LEN), SALT); - QByteArray cek = Crypto::expand(fmk, CEK); - QByteArray hhk = Crypto::expand(fmk, HMAC); -#ifndef NDEBUG - qDebug() << "fmk" << fmk.toHex(); - qDebug() << "cek" << cek.toHex(); - qDebug() << "hhk" << hhk.toHex(); -#endif - - flatbuffers::FlatBufferBuilder builder; - std::vector> recipients; - auto toVector = [&builder](const QByteArray &data) { - return builder.CreateVector((const uint8_t*)data.data(), size_t(data.length())); - }; - auto toString = [&builder](const QString &data) { - QByteArray utf8 = data.toUtf8(); - return builder.CreateString(utf8.data(), size_t(utf8.length())); - }; - auto sendToServer = [this](CKey &key, const QByteArray &recipient_id, const QByteArray &key_material, QLatin1String type) { - key.keyserver_id = Settings::CDOC2_DEFAULT_KEYSERVER; - if(key.keyserver_id.isEmpty()) - return setLastError(QStringLiteral("keyserver_id cannot be empty")); - QNetworkRequest req = cdoc20::req(key.keyserver_id); - if(req.url().isEmpty()) - return setLastError(QStringLiteral("No valid config found for keyserver_id: %1").arg(key.keyserver_id)); - if(!cdoc20::checkConnection()) - return false; - QScopedPointer nam(CheckConnection::setupNAM(req, Settings::CDOC2_POST_CERT)); - req.setRawHeader("x-expiry-time", QDateTime::currentDateTimeUtc().addMonths(6).toString(Qt::ISODate).toLatin1()); - QEventLoop e; - QNetworkReply *reply = nam->post(req, QJsonDocument({ - {QLatin1String("recipient_id"), QLatin1String(recipient_id.toBase64())}, - {QLatin1String("ephemeral_key_material"), QLatin1String(key_material.toBase64())}, - {QLatin1String("capsule_type"), type}, - }).toJson()); - connect(reply, &QNetworkReply::finished, &e, &QEventLoop::quit); - e.exec(); - if(reply->error() == QNetworkReply::NoError && - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 201) - key.transaction_id = QString::fromLatin1(reply->rawHeader("Location")).remove(QLatin1String("/key-capsules/")); - else - return setLastError(reply->errorString()); - if(key.transaction_id.isEmpty()) - return setLastError(QStringLiteral("Failed to post key capsule")); - return true; - }; - - for(CKey &key: keys) - { - if(key.isRSA) - { - QByteArray kek = Crypto::random(fmk.size()); - QByteArray xor_key = Crypto::xor_data(fmk, kek); - auto publicKey = Crypto::fromRSAPublicKeyDer(key.key); - if(!publicKey) - return false; - QByteArray encrytpedKek = Crypto::encrypt(publicKey.get(), RSA_PKCS1_OAEP_PADDING, kek); -#ifndef NDEBUG - qDebug() << "publicKeyDer" << key.key.toHex(); - qDebug() << "kek" << kek.toHex(); - qDebug() << "xor" << xor_key.toHex(); - qDebug() << "encrytpedKek" << encrytpedKek.toHex(); -#endif - if(!Settings::CDOC2_USE_KEYSERVER) - { - auto rsaPublicKey = cdoc20::Recipients::CreateRSAPublicKeyCapsule(builder, - toVector(key.key), toVector(encrytpedKek)); - recipients.push_back(cdoc20::Header::CreateRecipientRecord(builder, - cdoc20::Recipients::Capsule::RSAPublicKeyCapsule, rsaPublicKey.Union(), - toString(key.toKeyLabel()), toVector(xor_key), cdoc20::Header::FMKEncryptionMethod::XOR)); - continue; - } - - if(!sendToServer(key, key.key, encrytpedKek, QLatin1String("rsa"))) - return false; - auto rsaKeyServer = cdoc20::Recipients::CreateServerRsaDetails(builder, toVector(key.key)); - auto keyServer = cdoc20::Recipients::CreateKeyServerCapsule(builder, - cdoc20::Recipients::ServerDetailsUnion::ServerRsaDetails, - rsaKeyServer.Union(), toString(key.keyserver_id), toString(key.transaction_id)); - recipients.push_back(cdoc20::Header::CreateRecipientRecord(builder, - cdoc20::Recipients::Capsule::KeyServerCapsule, keyServer.Union(), - toString(key.toKeyLabel()), toVector(xor_key), cdoc20::Header::FMKEncryptionMethod::XOR)); - continue; - } - - auto publicKey = Crypto::fromECPublicKeyDer(key.key, NID_secp384r1); - if(!publicKey) - return false; - auto ephKey = Crypto::genECKey(publicKey.get()); - QByteArray sharedSecret = Crypto::derive(ephKey.get(), publicKey.get()); - QByteArray ephPublicKeyDer = Crypto::toPublicKeyDer(ephKey.get()); - QByteArray kekPm = Crypto::extract(sharedSecret, KEKPREMASTER); - QByteArray info = KEK + cdoc20::Header::EnumNameFMKEncryptionMethod(cdoc20::Header::FMKEncryptionMethod::XOR) + key.key + ephPublicKeyDer; - QByteArray kek = Crypto::expand(kekPm, info, fmk.size()); - QByteArray xor_key = Crypto::xor_data(fmk, kek); -#ifndef NDEBUG - qDebug() << "publicKeyDer" << key.key.toHex(); - qDebug() << "ephPublicKeyDer" << ephPublicKeyDer.toHex(); - qDebug() << "sharedSecret" << sharedSecret.toHex(); - qDebug() << "kekPm" << kekPm.toHex(); - qDebug() << "kek" << kek.toHex(); - qDebug() << "xor" << xor_key.toHex(); -#endif - if(!Settings::CDOC2_USE_KEYSERVER) - { - auto eccPublicKey = cdoc20::Recipients::CreateECCPublicKeyCapsule(builder, - cdoc20::Recipients::EllipticCurve::secp384r1, toVector(key.key), toVector(ephPublicKeyDer)); - recipients.push_back(cdoc20::Header::CreateRecipientRecord(builder, - cdoc20::Recipients::Capsule::ECCPublicKeyCapsule, eccPublicKey.Union(), - toString(key.toKeyLabel()), toVector(xor_key), cdoc20::Header::FMKEncryptionMethod::XOR)); - continue; - } - - if(!sendToServer(key, key.key, ephPublicKeyDer, QLatin1String("ecc_secp384r1"))) - return false; - auto eccKeyServer = cdoc20::Recipients::CreateServerEccDetails(builder, - cdoc20::Recipients::EllipticCurve::secp384r1, toVector(key.key)); - auto keyServer = cdoc20::Recipients::CreateKeyServerCapsule(builder, - cdoc20::Recipients::ServerDetailsUnion::ServerEccDetails, - eccKeyServer.Union(), toString(key.keyserver_id), toString(key.transaction_id)); - recipients.push_back(cdoc20::Header::CreateRecipientRecord(builder, - cdoc20::Recipients::Capsule::KeyServerCapsule, keyServer.Union(), - toString(key.toKeyLabel()), toVector(xor_key), cdoc20::Header::FMKEncryptionMethod::XOR)); - } - - auto offset = cdoc20::Header::CreateHeader(builder, builder.CreateVector(recipients), - cdoc20::Header::PayloadEncryptionMethod::CHACHA20POLY1305); - builder.Finish(offset); - - QByteArray header = QByteArray::fromRawData((const char*)builder.GetBufferPointer(), int(builder.GetSize())); - QByteArray headerHMAC = Crypto::sign_hmac(hhk, header); - QByteArray nonce = Crypto::random(NONCE_LEN); -#ifndef NDEBUG - qDebug() << "hmac" << headerHMAC.toHex(); - qDebug() << "nonce" << nonce.toHex(); -#endif - Crypto::Cipher enc(EVP_chacha20_poly1305(), cek, nonce, true); - enc.updateAAD(PAYLOAD + header + headerHMAC); - auto header_len = qToBigEndian(uint32_t(header.size())); - remove(); - QFile file(path); - if(!file.open(QFile::WriteOnly)) - return setLastError(file.errorString()); - file.write(LABEL); - file.write((const char*)&header_len, int(sizeof(header_len))); - file.write(header); - file.write(headerHMAC); - file.write(nonce); - if(!cdoc20::TAR(std::unique_ptr(new cdoc20::stream(&file, &enc))).save(files)) - { - file.remove(); - return false; - } - if(!enc.result()) - { - file.remove(); - return false; - } - QByteArray tag = enc.tag(); -#ifndef NDEBUG - qDebug() << "tag" << tag.toHex(); -#endif - file.write(tag); - return true; -} - -QByteArray CDoc2::transportKey(const CKey &_key) -{ - setLastError({}); - CKey key = _key; - if(!key.transaction_id.isEmpty()) - { - QNetworkRequest req = cdoc20::req(key.keyserver_id, key.transaction_id); - if(req.url().isEmpty()) - { - setLastError(QStringLiteral("No valid config found for keyserver_id: %1").arg(key.keyserver_id)); - return {}; - } - if(!cdoc20::checkConnection()) - return {}; - auto authKey = dispatchToMain(&QSigner::key, qApp->signer()); - QScopedPointer nam( - CheckConnection::setupNAM(req, qApp->signer()->tokenauth().cert(), authKey, Settings::CDOC2_GET_CERT)); - QEventLoop e; - QNetworkReply *reply = nam->get(req); - connect(reply, &QNetworkReply::finished, &e, &QEventLoop::quit); - e.exec(); - if(authKey.handle()) - qApp->signer()->logout(); - if(reply->error() != QNetworkReply::NoError && - reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 201) - { - setLastError(reply->errorString()); - return {}; - } - QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); - QByteArray key_material = QByteArray::fromBase64( - json.value(QLatin1String("ephemeral_key_material")).toString().toLatin1()); - if(json.value(QLatin1String("capsule_type")) == QLatin1String("rsa")) - key.encrypted_kek = std::move(key_material); - else - key.publicKey = std::move(key_material); - } -#ifndef NDEBUG - qDebug() << "publicKeyDer" << key.key.toHex(); - qDebug() << "ephPublicKeyDer" << key.publicKey.toHex(); -#endif - QByteArray kek = qApp->signer()->decrypt([&key](QCryptoBackend *backend) { - if(key.isRSA) - return backend->decrypt(key.encrypted_kek, true); - QByteArray kekPm = backend->deriveHMACExtract(key.publicKey, KEKPREMASTER, KEY_LEN); -#ifndef NDEBUG - qDebug() << "kekPm" << kekPm.toHex(); -#endif - QByteArray info = KEK + cdoc20::Header::EnumNameFMKEncryptionMethod(cdoc20::Header::FMKEncryptionMethod::XOR) + key.key + key.publicKey; - return Crypto::expand(kekPm, info, KEY_LEN); - }); - if(kek.isEmpty()) - { - setLastError(QStringLiteral("Failed to derive key")); - return {}; - } - QByteArray fmk = Crypto::xor_data(key.cipher, kek); - QByteArray hhk = Crypto::expand(fmk, HMAC); -#ifndef NDEBUG - qDebug() << "kek" << kek.toHex(); - qDebug() << "xor" << key.cipher.toHex(); - qDebug() << "fmk" << fmk.toHex(); - qDebug() << "hhk" << hhk.toHex(); - qDebug() << "hmac" << headerHMAC.toHex(); -#endif - if(Crypto::sign_hmac(hhk, header_data) == headerHMAC) - return fmk; - setLastError(QStringLiteral("CDoc 2.0 hash mismatch")); - return {}; -} - -int CDoc2::version() -{ - return 2; -} diff --git a/client/CDoc2.h b/client/CDoc2.h deleted file mode 100644 index 54bcf58d..00000000 --- a/client/CDoc2.h +++ /dev/null @@ -1,45 +0,0 @@ -/* - * QDigiDocClient - * - * 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 "CryptoDoc.h" - -#include - -class CDoc2 final: public CDoc, private QFile { -public: - explicit CDoc2() = default; - explicit CDoc2(const QString &path); - - CKey canDecrypt(const QSslCertificate &cert) const final; - bool decryptPayload(const QByteArray &fmk) final; - QByteArray deriveFMK(const QByteArray &priv, const CKey &key); - bool isSupported(); - bool save(const QString &path) final; - QByteArray transportKey(const CKey &key) final; - int version() final; - -private: - QByteArray header_data, headerHMAC; - qint64 noncePos = -1; - - static const QByteArray LABEL, CEK, HMAC, KEK, KEKPREMASTER, PAYLOAD, SALT; - static constexpr int KEY_LEN = 32, NONCE_LEN = 12; -}; diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp new file mode 100644 index 00000000..391edf07 --- /dev/null +++ b/client/CDocSupport.cpp @@ -0,0 +1,394 @@ +#define __CDOCSUPPORT_CPP__ + +/* + * QDigiDocCrypto + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Application.h" +#include "CheckConnection.h" +#include "QCryptoBackend.h" +#include "QSigner.h" +#include "Settings.h" +#include "TokenData.h" +#include "Utils.h" +#include "effects/FadeInNotification.h" +#include + +#include "CDocSupport.h" + +std::vector +CDocSupport::getCDocFileList(QString filename) +{ + std::vector files; + int version = libcdoc::CDocReader::getCDocFileVersion(filename.toStdString()); + if (version != 1) return files; + QFile ifs(filename); + ifs.open(QIODevice::ReadOnly); + QXmlStreamReader xml(&ifs); + while (xml.readNextStartElement()) { + if (xml.name() == QStringLiteral("EncryptedData")) { + while (xml.readNextStartElement()) { + if (xml.name() == QStringLiteral("EncryptionProperties")) { + while (xml.readNextStartElement()) { + if (xml.name() == QStringLiteral("EncryptionProperty")) { + if (xml.attributes().value(QStringLiteral("Name")) == QStringLiteral("orig_file")) { + QString content = xml.readElementText(); + auto list = content.split("|"); + files.push_back({list.at(0).toStdString(), list.at(1).toInt()}); + } else { + xml.skipCurrentElement(); + } + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + } else { + xml.skipCurrentElement(); + } + } + ifs.close(); + + return files; +} + +libcdoc::result_t +DDCryptoBackend::decryptRSA(std::vector& result, const std::vector &data, bool oaep, unsigned int idx) +{ + QByteArray qdata(reinterpret_cast(data.data()), data.size()); + QByteArray qkek = qApp->signer()->decrypt([&qdata, &oaep](QCryptoBackend *backend) { + return backend->decrypt(qdata, oaep); + }); + result.assign(qkek.cbegin(), qkek.cend()); + return (result.empty()) ? OPENSSL_ERROR : libcdoc::OK; +} + +const QString SHA256_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha256"); +const QString SHA384_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha384"); +const QString SHA512_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha512"); +const QHash SHA_MTH{ + {SHA256_MTH, QCryptographicHash::Sha256}, {SHA384_MTH, QCryptographicHash::Sha384}, {SHA512_MTH, QCryptographicHash::Sha512} +}; + +libcdoc::result_t +DDCryptoBackend::deriveConcatKDF(std::vector& dst, const std::vector &publicKey, const std::string &digest, + const std::vector &algorithmID, const std::vector &partyUInfo, const std::vector &partyVInfo, unsigned int idx) +{ + QByteArray decryptedKey = qApp->signer()->decrypt([&publicKey, &digest, &algorithmID, &partyUInfo, &partyVInfo](QCryptoBackend *backend) { + QByteArray ba(reinterpret_cast(publicKey.data()), publicKey.size()); + return backend->deriveConcatKDF(ba, SHA_MTH[QString::fromStdString(digest)], + QByteArray(reinterpret_cast(algorithmID.data()), algorithmID.size()), + QByteArray(reinterpret_cast(partyUInfo.data()), partyUInfo.size()), + QByteArray(reinterpret_cast(partyVInfo.data()), partyVInfo.size())); + }); + dst.assign(decryptedKey.cbegin(), decryptedKey.cend()); + return (dst.empty()) ? OPENSSL_ERROR : libcdoc::OK; +} + +libcdoc::result_t +DDCryptoBackend::deriveHMACExtract(std::vector& dst, const std::vector &key_material, const std::vector &salt, unsigned int idx) +{ + QByteArray qkey_material(reinterpret_cast(key_material.data()), key_material.size()); + QByteArray qsalt(reinterpret_cast(salt.data()), salt.size()); + QByteArray qkekpm = qApp->signer()->decrypt([&qkey_material, &qsalt](QCryptoBackend *backend) { + return backend->deriveHMACExtract(qkey_material, qsalt, ECC_KEY_LEN); + }); + dst = std::vector(qkekpm.cbegin(), qkekpm.cend()); + return (dst.empty()) ? OPENSSL_ERROR : libcdoc::OK; +} + +libcdoc::result_t +DDCryptoBackend::getSecret(std::vector& _secret, unsigned int idx) +{ + _secret = secret; + return libcdoc::OK; +} + +bool +checkConnection() +{ + if(CheckConnection().check()) { + return true; + } + return dispatchToMain([] { + FadeInNotification::error(Application::mainWindow()->findChild(QStringLiteral("topBar")), + QCoreApplication::translate("MainWindow", "Check internet connection")); + return false; + }); +} + +std::string +DDConfiguration::getValue(std::string_view domain, std::string_view param) const +{ + std::string def = Settings::CDOC2_DEFAULT_KEYSERVER; + if (domain == def) { + if (param == libcdoc::Configuration::KEYSERVER_SEND_URL) { +#ifdef CONFIG_URL + QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); + QJsonObject data = list.value(QString::fromUtf8(domain.data(), domain.size())).toObject(); + QString url = data.value(QLatin1String("POST")).toString(Settings::CDOC2_POST); + return url.toStdString(); +#else + QString url = Settings::CDOC2_POST; + return url.toStdString(); +#endif + } else if (param == libcdoc::Configuration::KEYSERVER_FETCH_URL) { +#ifdef CONFIG_URL + QJsonObject list = Application::confValue(QLatin1String("CDOC2-CONF")).toObject(); + QJsonObject data = list.value(QString::fromUtf8(domain.data(), domain.size())).toObject(); + QString url = data.value(QLatin1String("FETCH")).toString(Settings::CDOC2_GET); + return url.toStdString(); +#else + QString url = Settings::CDOC2_GET; + return url.toStdString(); +#endif + } + } + return {}; +} + +std::string +DDNetworkBackend::getLastErrorStr(libcdoc::result_t code) const +{ + if (code == BACKEND_ERROR) return last_error; + return libcdoc::NetworkBackend::getLastErrorStr(code); +} + +libcdoc::result_t DDNetworkBackend::sendKey( + libcdoc::NetworkBackend::CapsuleInfo &dst, const std::string &url, + const std::vector &rcpt_key, + const std::vector &key_material, const std::string &type) { + QNetworkRequest req(QString::fromStdString(url + "/key-capsules")); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + if (!checkConnection()) { + last_error = "No connection"; + return BACKEND_ERROR; + } + QScopedPointer nam(CheckConnection::setupNAM(req, Settings::CDOC2_POST_CERT)); + QEventLoop e; + QNetworkReply *reply = nam->post(req, + QJsonDocument({ + {QLatin1String("recipient_id"), + QLatin1String( + QByteArray(reinterpret_cast(rcpt_key.data()), + rcpt_key.size()) + .toBase64())}, + {QLatin1String("ephemeral_key_material"), + QLatin1String(QByteArray(reinterpret_cast( + key_material.data()), + key_material.size()) + .toBase64())}, + {QLatin1String("capsule_type"), QLatin1String(type.c_str())}, + }).toJson()); + connect(reply, &QNetworkReply::finished, &e, &QEventLoop::quit); + e.exec(); + QString tr_id; + if (reply->error() == QNetworkReply::NoError && + reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == + 201) { + tr_id = QString::fromLatin1(reply->rawHeader("Location")) + .remove(QLatin1String("/key-capsules/")); + } else { + last_error = reply->errorString().toStdString(); + return BACKEND_ERROR; + } + if (tr_id.isEmpty()) { + last_error = "Failed to post key capsule"; + return BACKEND_ERROR; + } + dst.transaction_id = tr_id.toStdString(); + QDateTime dt = QDateTime::currentDateTimeUtc(); + dt = dt.addMonths(6); + dst.expiry_time = dt.toSecsSinceEpoch(); + return libcdoc::OK; +}; + +libcdoc::result_t +DDNetworkBackend::fetchKey(std::vector &result, + const std::string &url, + const std::string &transaction_id) { + QNetworkRequest req(QStringLiteral("%1/key-capsules/%2").arg(QString::fromStdString(url), QString::fromStdString(transaction_id))); + req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + if(!checkConnection()) { + last_error = "No connection"; + return BACKEND_ERROR; + } + auto authKey = dispatchToMain(&QSigner::key, qApp->signer()); + QScopedPointer nam( + CheckConnection::setupNAM(req, qApp->signer()->tokenauth().cert(), authKey, Settings::CDOC2_GET_CERT)); + QEventLoop e; + QNetworkReply *reply = nam->get(req); + connect(reply, &QNetworkReply::finished, &e, &QEventLoop::quit); + e.exec(); + if(authKey.handle()) { + qApp->signer()->logout(); + } + + if(reply->error() != QNetworkReply::NoError && reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() != 201) { + last_error = reply->errorString().toStdString(); + return BACKEND_ERROR; + } + QJsonObject json = QJsonDocument::fromJson(reply->readAll()).object(); + QByteArray key_material = QByteArray::fromBase64(json.value(QLatin1String("ephemeral_key_material")).toString().toLatin1()); + result.assign(key_material.cbegin(), key_material.cend()); + return libcdoc::OK; +} + +Q_DECLARE_LOGGING_CATEGORY(LOG_CDOC) +Q_LOGGING_CATEGORY(LOG_CDOC, "libcdoc") + +void DDCDocLogger::LogMessage(libcdoc::ILogger::LogLevel level, std::string_view file, int line, std::string_view message) { + switch (level) { + case libcdoc::ILogger::LogLevel::LEVEL_FATAL: + qCFatal(LOG_CDOC) << message; + break; + case libcdoc::ILogger::LogLevel::LEVEL_ERROR: + qCCritical(LOG_CDOC) << message; + break; + case libcdoc::ILogger::LogLevel::LEVEL_WARNING: + qCWarning(LOG_CDOC) << message; + break; + case libcdoc::ILogger::LogLevel::LEVEL_INFO: + qCInfo(LOG_CDOC) << message; + break; + case libcdoc::ILogger::LogLevel::LEVEL_DEBUG: + qCDebug(LOG_CDOC) << message; + break; + default: + // Trace, if present goes to debug categrory + qCDebug(LOG_CDOC) << message; + break; + } +} + +void DDCDocLogger::setUpLogger() { + static DDCDocLogger *logger = nullptr; + if (logger) { + logger = new DDCDocLogger(); + logger->SetMinLogLevel(libcdoc::ILogger::LogLevel::LEVEL_TRACE); + libcdoc::ILogger::addLogger(logger); + } +} + +TempListConsumer::~TempListConsumer() +{ + if (!files.empty()) { + IOEntry& file = files.back(); + file.data->close(); + } +} + +libcdoc::result_t TempListConsumer::write(const uint8_t *src, size_t size) { + if (files.empty()) + return libcdoc::OUTPUT_ERROR; + IOEntry &file = files.back(); + if (!file.data->isWritable()) + return libcdoc::OUTPUT_ERROR; + if (file.data->write((const char *)src, size) != size) + return libcdoc::OUTPUT_STREAM_ERROR; + file.size += size; + return size; +} + +libcdoc::result_t TempListConsumer::close() { + if (files.empty()) + return libcdoc::OUTPUT_ERROR; + IOEntry &file = files.back(); + if (!file.data->isWritable()) + return libcdoc::OUTPUT_ERROR; + return libcdoc::OK; +} + +bool +TempListConsumer::isError() +{ + if (files.empty()) return false; + IOEntry& file = files.back(); + return !file.data->isWritable(); +} + +libcdoc::result_t +TempListConsumer::open(const std::string& name, int64_t size) +{ + IOEntry io({name, "application/octet-stream", 0, {}}); + if ((size < 0) || (size > MAX_VEC_SIZE)) { + io.data = std::make_unique(); + } else { + io.data = std::make_unique(); + } + io.data->open(QIODevice::ReadWrite); + files.push_back(std::move(io)); + return libcdoc::OK; +} + +StreamListSource::StreamListSource(const std::vector& files) : _files(files), _current(-1) +{ +} + +libcdoc::result_t +StreamListSource::read(uint8_t *dst, size_t size) +{ + if ((_current < 0) || (_current >= _files.size())) return 0; + return _files[_current].data->read((char *) dst, size); +} + +bool +StreamListSource::isError() +{ + if ((_current < 0) || (_current >= _files.size())) return 0; + return _files[_current].data->isReadable(); +} + +bool +StreamListSource::isEof() +{ + if (_current < 0) return false; + if (_current >= _files.size()) return true; + return _files[_current].data->atEnd(); +} + +libcdoc::result_t +StreamListSource::getNumComponents() +{ + return _files.size(); +} + +libcdoc::result_t +StreamListSource::next(std::string& name, int64_t& size) +{ + ++_current; + if (_current >= _files.size()) return libcdoc::END_OF_STREAM; + _files[_current].data->seek(0); + name = _files[_current].name; + size = _files[_current].size; + return libcdoc::OK; +} diff --git a/client/CDocSupport.h b/client/CDocSupport.h new file mode 100644 index 00000000..5b719f61 --- /dev/null +++ b/client/CDocSupport.h @@ -0,0 +1,173 @@ +#ifndef __CDOCSUPPORT_H__ +#define __CDOCSUPPORT_H__ + +/* + * QDigiDocCrypto + * + * 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 + +#include +#include +#include +#include +#include + +// +// libcdoc handler implementations +// + +// +// Configuration +// +// Provides KEYSERVER_SEND_URL and KEYSERVER_FETCH_URL +// + +struct DDConfiguration : public libcdoc::Configuration { + std::string getValue(std::string_view domain, + std::string_view param) const final; + + explicit DDConfiguration() = default; +}; + +// +// CryptoBackend +// +// Bridges to qApp->signer() +// + +struct DDCryptoBackend : public libcdoc::CryptoBackend { + libcdoc::result_t decryptRSA(std::vector &result, + const std::vector &data, bool oaep, + unsigned int idx) override final; + libcdoc::result_t deriveConcatKDF(std::vector &dst, + const std::vector &publicKey, + const std::string &digest, + const std::vector &algorithmID, + const std::vector &partyUInfo, + const std::vector &partyVInfo, + unsigned int idx) override final; + libcdoc::result_t deriveHMACExtract(std::vector &dst, + const std::vector &publicKey, + const std::vector &salt, + unsigned int idx) override final; + libcdoc::result_t getSecret(std::vector &secret, + unsigned int idx) override final; + + std::vector secret; + + explicit DDCryptoBackend() = default; +}; + +// +// NetworkBackend +// +// Bridges to QNetworkAccessManager +// + +struct DDNetworkBackend : public libcdoc::NetworkBackend, private QObject { + static constexpr int BACKEND_ERROR = -303; + + std::string getLastErrorStr(libcdoc::result_t code) const final; + libcdoc::result_t sendKey(libcdoc::NetworkBackend::CapsuleInfo &dst, + const std::string &url, + const std::vector &rcpt_key, + const std::vector &key_material, + const std::string &type) override final; + libcdoc::result_t + fetchKey(std::vector &result, const std::string &keyserver_id, + const std::string &transaction_id) override final; + + libcdoc::result_t + getClientTLSCertificate(std::vector &dst) override final { + return libcdoc::NOT_IMPLEMENTED; + } + libcdoc::result_t getPeerTLSCertificates( + std::vector> &dst) override final { + return libcdoc::NOT_IMPLEMENTED; + } + + explicit DDNetworkBackend() = default; + + std::string last_error; +}; + +// +// ILogger +// +// Bridges to Qt logging system +// + +class DDCDocLogger : private libcdoc::ILogger { + public: + static void setUpLogger(); + + private: + DDCDocLogger() = default; + ~DDCDocLogger() = default; + void LogMessage(libcdoc::ILogger::LogLevel level, std::string_view file, int line, + std::string_view message) override final; +}; + +class CDocSupport { +public: + static std::vector getCDocFileList(QString filename); +}; + +// +// DataSource and consumer that can share temporary files/buffwers +// + +struct IOEntry +{ + std::string name, mime; + int64_t size; + std::unique_ptr data; +}; + +struct TempListConsumer : public libcdoc::MultiDataConsumer { + static constexpr int64_t MAX_VEC_SIZE = 500L * 1024L * 1024L; + + explicit TempListConsumer(size_t max_memory_size = 500L * 1024L * 1024L) + : _max_memory_size(max_memory_size) {} + ~TempListConsumer(); + + libcdoc::result_t write(const uint8_t *src, size_t size) override final; + libcdoc::result_t close() override final; + bool isError() override final; + libcdoc::result_t open(const std::string &name, + int64_t size) override final; + + size_t _max_memory_size; + std::vector files; +}; + +struct StreamListSource : public libcdoc::MultiDataSource { + StreamListSource(const std::vector &files); + + int64_t read(uint8_t *dst, size_t size) override final; + bool isError() override final; + bool isEof() override final; + libcdoc::result_t getNumComponents() override final; + libcdoc::result_t next(std::string &name, int64_t &size) override final; + + const std::vector &_files; + int64_t _current; +}; + +#endif // __CDOCSUPPORT_H__ diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 5f138107..3118451f 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,3 +1,5 @@ +set(CMAKE_CXX_STANDARD 20) + get_target_property(qtCore_install_prefix Qt6::qmake IMPORTED_LOCATION) get_filename_component(qtCore_install_prefix ${qtCore_install_prefix} DIRECTORY) get_filename_component(TSL_FILENAME ${TSL_URL} NAME_WLE) @@ -20,6 +22,9 @@ else() ) endif() +set(BUILD_SHARED_LIBS NO) +add_subdirectory(libcdoc EXCLUDE_FROM_ALL) + file(GLOB WIDGETS CONFIGURE_DEPENDS "dialogs/*.cpp" "dialogs/*.h" "dialogs/*.ui" "effects/*.cpp" "effects/*.h" "effects/*.ui" @@ -38,10 +43,6 @@ add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE main.cpp Application.cpp Application.h - CDoc1.cpp - CDoc1.h - CDoc2.cpp - CDoc2.h CheckConnection.cpp CheckConnection.h Crypto.cpp @@ -89,6 +90,8 @@ add_executable(${PROJECT_NAME} WIN32 MACOSX_BUNDLE TokenData.cpp TokenData.h Utils.h + dialogs/PasswordDialog.h dialogs/PasswordDialog.cpp dialogs/PasswordDialog.ui + CDocSupport.cpp CDocSupport.h ) qt_add_translations(${PROJECT_NAME} TS_FILES translations/en.ts @@ -109,9 +112,7 @@ target_link_libraries(${PROJECT_NAME} Qt6::SvgWidgets digidocpp::digidocpp ${LDAP_LIBRARIES} - $ - $ - ZLIB::ZLIB + cdoc ) if(NOT BUILD_DATE) @@ -129,6 +130,8 @@ set_target_properties(${PROJECT_NAME} PROPERTIES MACOSX_BUNDLE_ICON_FILE Icon.icns MACOSX_BUNDLE_GUI_IDENTIFIER "ee.ria.${PROJECT_NAME}" ) +target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_SOURCE_DIR}) + target_compile_definitions(${PROJECT_NAME} PRIVATE CDOC2_GET_URL="${CDOC2_GET_URL}" CDOC2_POST_URL="${CDOC2_POST_URL}" @@ -139,25 +142,6 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE VERSION_STR="${VERSION}" ) -foreach(SCHEMA ${SCHEMAS}) - get_filename_component(stem ${SCHEMA} NAME_WE) - get_filename_component(name ${SCHEMA} NAME) - set(GENERATED_INCLUDE ${CMAKE_CURRENT_BINARY_DIR}/${stem}_generated.h) - add_custom_command( - OUTPUT ${GENERATED_INCLUDE} - COMMENT "Compiling flatbuffer for ${name}" - COMMAND flatbuffers::flatc - --cpp - --scoped-enums - -o ${CMAKE_CURRENT_BINARY_DIR} - -I ${CMAKE_CURRENT_SOURCE_DIR} - ${SCHEMA} - DEPENDS flatbuffers::flatc ${SCHEMA} - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - ) - target_sources(${PROJECT_NAME} PRIVATE ${SCHEMA} ${GENERATED_INCLUDE}) -endforeach() - if( APPLE ) file(GLOB_RECURSE RESOURCE_FILES ${CMAKE_CURRENT_SOURCE_DIR}/mac/Resources/*.icns diff --git a/client/CheckConnection.cpp b/client/CheckConnection.cpp index 689521e9..45f26105 100644 --- a/client/CheckConnection.cpp +++ b/client/CheckConnection.cpp @@ -86,7 +86,7 @@ QNetworkAccessManager* CheckConnection::setupNAM(QNetworkRequest &req, const QBy qDebug() << "SSL Error:" << error.error() << error.certificate().subjectInfo(QSslCertificate::CommonName); } } - reply->ignoreSslErrors(ignore); + reply->ignoreSslErrors(errors); }); return nam; } diff --git a/client/Crypto.cpp b/client/Crypto.cpp index e41983f1..17a9dfe4 100644 --- a/client/Crypto.cpp +++ b/client/Crypto.cpp @@ -91,9 +91,17 @@ bool Crypto::Cipher::setTag(const QByteArray &data) const return !isError(EVP_CIPHER_CTX_ctrl(ctx.get(), EVP_CTRL_AEAD_SET_TAG, int(data.size()), const_cast(data.data()))); } -QByteArray Crypto::aes_wrap(const QByteArray &key, const QByteArray &data, bool encrypt) +QByteArray Crypto::aes_wrap(const QByteArray &key, const QByteArray &data) { - Cipher c(key.size() == 32 ? EVP_aes_256_wrap() : EVP_aes_128_wrap(), key, {}, encrypt); + Cipher c(key.size() == 32 ? EVP_aes_256_wrap() : EVP_aes_128_wrap(), key, {}, true); + if(QByteArray result = c.update(data); c.result()) + return result; + return {}; +} + +QByteArray Crypto::aes_unwrap(const QByteArray &key, const QByteArray &data) +{ + Cipher c(key.size() == 32 ? EVP_aes_256_wrap() : EVP_aes_128_wrap(), key, {}, false); if(QByteArray result = c.update(data); c.result()) return result; return {}; @@ -366,3 +374,14 @@ QByteArray Crypto::xor_data(const QByteArray &a, const QByteArray &b) result[i] = char(a[i] ^ b[i]); return result; } + +QByteArray +Crypto::pbkdf2_sha256(const QByteArray& pw, const QByteArray& salt, uint32_t iter) +{ + QByteArray key(32, 0); + PKCS5_PBKDF2_HMAC(pw.data(), pw.length(), + (const unsigned char *) salt.data(), int(salt.length()), + iter, EVP_sha256(), int(key.size()), (unsigned char *)key.data()); + return key; +} + diff --git a/client/Crypto.h b/client/Crypto.h index 7ed6d00c..5ea04f80 100644 --- a/client/Crypto.h +++ b/client/Crypto.h @@ -50,7 +50,8 @@ class Crypto bool setTag(const QByteArray &data) const; }; - static QByteArray aes_wrap(const QByteArray &key, const QByteArray &data, bool encrypt); + static QByteArray aes_wrap(const QByteArray &key, const QByteArray &data); + static QByteArray aes_unwrap(const QByteArray &key, const QByteArray &data); static QByteArray cipher(const EVP_CIPHER *cipher, const QByteArray &key, QByteArray &data, bool encrypt); static QByteArray curve_oid(EVP_PKEY *key); static QByteArray concatKDF(QCryptographicHash::Algorithm digestMethod, const QByteArray &z, const QByteArray &otherInfo); @@ -70,6 +71,7 @@ class Crypto static QByteArray random(int len = 32); static QByteArray xor_data(const QByteArray &a, const QByteArray &b); + static QByteArray pbkdf2_sha256(const QByteArray& pw, const QByteArray& salt, uint32_t iter); private: static bool isError(int err); }; diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index 3e59d9ab..4bd6e27c 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -20,9 +20,8 @@ #include "CryptoDoc.h" #include "Application.h" -#include "CDoc1.h" -#include "CDoc2.h" #include "Crypto.h" +#include "CDocSupport.h" #include "TokenData.h" #include "QCryptoBackend.h" #include "QSigner.h" @@ -43,13 +42,45 @@ #include #include +#include +#include +#include +#include + using namespace ria::qdigidoc4; +auto toHex = [](const std::vector& data) -> QString { + QByteArray ba(reinterpret_cast(data.data()), data.size()); + return ba.toHex(); +}; + +std::string +CryptoDoc::labelFromCertificate(const std::vector& cert) +{ + QSslCertificate kcert(QByteArray(reinterpret_cast(cert.data()), cert.size()), QSsl::Der); + return [](const SslCertificate &c) { + QString cn = c.subjectInfo(QSslCertificate::CommonName); + QString gn = c.subjectInfo("GN"); + QString sn = c.subjectInfo("SN"); + if(!gn.isEmpty() || !sn.isEmpty()) + cn = QStringLiteral("%1 %2 %3").arg(gn, sn, c.personalCode()); + + int certType = c.type(); + if(certType & SslCertificate::EResidentSubType) + return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("Digi-ID E-RESIDENT")).toStdString(); + if(certType & SslCertificate::DigiIDType) + return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("Digi-ID")).toStdString(); + if(certType & SslCertificate::EstEidType) + return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("ID-CARD")).toStdString(); + return cn.toStdString(); + }(kcert); +} + class CryptoDoc::Private final: public QThread { Q_OBJECT public: - bool isEncryptedWarning() const; + bool warnIfNotWritable() const; void run() final; inline void waitForFinished() { @@ -59,78 +90,168 @@ class CryptoDoc::Private final: public QThread e.exec(); } - std::unique_ptr cdoc; + std::unique_ptr reader; + std::string writer_last_error; + QString fileName; - QByteArray key; - bool isEncrypted = false; + bool isEncrypted() const { return reader != nullptr; } CDocumentModel *documents = new CDocumentModel(this); QStringList tempFiles; + // Decryption data + QByteArray fmk; + // Encryption data + QString label; + uint32_t kdf_iter; + + // libcdoc handlers + DDConfiguration conf; + DDCryptoBackend crypto; + DDNetworkBackend network; + + std::vector files; + std::vector keys; + + const std::vector &getFiles() { + return files; + } + std::unique_ptr createCDocReader(const std::string& filename) { + libcdoc::CDocReader *r = libcdoc::CDocReader::createReader(filename, &conf, &crypto, &network); + if (!r) { + WarningDialog::show(tr("Failed to open document"), tr("Unsupported file format")); + return nullptr; + } + keys.clear(); + for (auto& key : r->getLocks()) { + keys.push_back({key, QSslCertificate()}); + } + return std::unique_ptr(r); + } +private: + bool encrypt(); }; -bool CryptoDoc::Private::isEncryptedWarning() const +bool CryptoDoc::Private::warnIfNotWritable() const { - if( fileName.isEmpty() ) + if(fileName.isEmpty()) { WarningDialog::show(CryptoDoc::tr("Container is not open")); - if(isEncrypted) + } else if(reader) { WarningDialog::show(CryptoDoc::tr("Container is encrypted")); - return fileName.isEmpty() || isEncrypted; + } else { + return false; + } + return true; } void CryptoDoc::Private::run() { - if(isEncrypted) - { + if(reader) { qCDebug(CRYPTO) << "Decrypt" << fileName; - isEncrypted = !cdoc->decryptPayload(key); + std::vector pfmk(fmk.cbegin(), fmk.cend()); + + TempListConsumer cons; + if (reader->decrypt(pfmk, &cons) == libcdoc::OK) { + files = std::move(cons.files); + // Success, immediately create writer from reader + keys.clear(); + writer_last_error.clear(); + reader.reset(); + } + } else { + if (encrypt()) { + // Encryption successful, open new reader + reader = createCDocReader(fileName.toStdString()); + if (!reader) return; + } } - else - { - qCDebug(CRYPTO) << "Encrypt" << fileName; - isEncrypted = cdoc->save(fileName); +} + +bool CryptoDoc::Private::encrypt() { + qCDebug(CRYPTO) << "Encrypt" << fileName; + + libcdoc::OStreamConsumer ofs(fileName.toStdString()); + if (ofs.isError()) + return false; + + StreamListSource slsrc(files); + std::vector enc_keys; + + std::string keyserver_id; + if (Settings::CDOC2_DEFAULT && Settings::CDOC2_USE_KEYSERVER) { + keyserver_id = Settings::CDOC2_DEFAULT_KEYSERVER; } + for (auto &key : keys) { + QByteArray ba = key.rcpt_cert.toDer(); + std::vector cert_der = + std::vector(ba.cbegin(), ba.cend()); + QSslKey qkey = key.rcpt_cert.publicKey(); + ba = Crypto::toPublicKeyDer(qkey); + std::vector key_der(ba.cbegin(), ba.cend()); + libcdoc::Recipient::PKType pk_type = + (qkey.algorithm() == QSsl::KeyAlgorithm::Rsa) + ? libcdoc::Recipient::PKType::RSA + : libcdoc::Recipient::PKType::ECC; + if (!keyserver_id.empty()) { + std::string label = CryptoDoc::labelFromCertificate(cert_der); + enc_keys.push_back(libcdoc::Recipient::makeServer( + label, key_der, pk_type, keyserver_id)); + } else { + std::string label = CryptoDoc::labelFromCertificate(cert_der); + enc_keys.push_back( + libcdoc::Recipient::makeCertificate(label, cert_der)); + } + } + if (!crypto.secret.empty()) { + auto key = + libcdoc::Recipient::makeSymmetric(label.toStdString(), kdf_iter); + enc_keys.push_back(key); + } + + libcdoc::CDocWriter *writer = libcdoc::CDocWriter::createWriter( + Settings::CDOC2_DEFAULT ? 2 : 1, &ofs, false, &conf, &crypto, &network); + int result = writer->encrypt(slsrc, enc_keys); + if (result != libcdoc::OK) { + writer_last_error = writer->getLastErrorStr(); + std::filesystem::remove(std::filesystem::path(fileName.toStdString())); + } + delete writer; + ofs.close(); + return (result == libcdoc::OK); } -CDocumentModel::CDocumentModel(CryptoDoc::Private *doc) -: d( doc ) -{} +CDocumentModel::CDocumentModel(CryptoDoc::Private *doc) : d(doc) {} bool CDocumentModel::addFile(const QString &file, const QString &mime) { - if( d->isEncryptedWarning() ) - return false; + if(d->warnIfNotWritable()) return false; QFileInfo info(file); - if(info.size() == 0) - { + if(info.size() == 0) { WarningDialog::show(DocumentModel::tr("Cannot add empty file to the container.")); return false; } - if(d->cdoc->version() == 1 && info.size() > 120*1024*1024) - { + if(!Settings::CDOC2_DEFAULT && info.size() > 120*1024*1024) { WarningDialog::show(tr("Added file(s) exceeds the maximum size limit of the container (∼120MB). " "Read more about it")); return false; } - - QString fileName(info.fileName()); - if(std::any_of(d->cdoc->files.cbegin(), d->cdoc->files.cend(), + std::string fileName(info.fileName().toStdString()); + if(std::any_of(d->files.cbegin(), d->files.cend(), [&fileName](const auto &containerFile) { return containerFile.name == fileName; })) { WarningDialog::show(DocumentModel::tr("Cannot add the file to the envelope. File '%1' is already in container.") - .arg(FileDialog::normalized(fileName))); + .arg(FileDialog::normalized(info.fileName()))); return false; } auto data = std::make_unique(file); data->open(QFile::ReadOnly); - d->cdoc->files.push_back({ - QFileInfo(file).fileName(), - QStringLiteral("D%1").arg(d->cdoc->files.size()), - mime, + d->files.push_back({ + QFileInfo(file).fileName().toStdString(), + mime.toStdString(), data->size(), std::move(data), }); - emit added(FileDialog::normalized(d->cdoc->files.back().name)); + emit added(FileDialog::normalized(info.fileName())); return true; } @@ -141,7 +262,7 @@ void CDocumentModel::addTempReference(const QString &file) QString CDocumentModel::copy(int row, const QString &dst) const { - const CDoc::File &file = d->cdoc->files.at(row); + auto &file = d->files.at(row); if( QFile::exists( dst ) ) QFile::remove( dst ); file.data->seek(0); @@ -153,22 +274,22 @@ QString CDocumentModel::copy(int row, const QString &dst) const QString CDocumentModel::data(int row) const { - return FileDialog::normalized(d->cdoc->files.at(row).name); + return FileDialog::normalized(QString::fromStdString(d->getFiles().at(row).name)); } quint64 CDocumentModel::fileSize(int row) const { - return d->cdoc->files.at(row).size; + return d->getFiles().at(row).size; } QString CDocumentModel::mime(int row) const { - return FileDialog::normalized(d->cdoc->files.at(row).mime); + return FileDialog::normalized(QString::fromStdString(d->getFiles().at(row).mime)); } void CDocumentModel::open(int row) { - if(d->isEncrypted) + if(d->isEncrypted()) return; QString path = FileDialog::tempPath(FileDialog::safeName(data(row))); if(!verifyFile(path)) @@ -185,28 +306,27 @@ void CDocumentModel::open(int row) bool CDocumentModel::removeRow(int row) { - if(d->isEncryptedWarning()) + if(d->warnIfNotWritable()) return false; - if(d->cdoc->files.empty() || row >= d->cdoc->files.size()) - { + if(row >= d->files.size()) { WarningDialog::show(DocumentModel::tr("Internal error")); return false; } - d->cdoc->files.erase(d->cdoc->files.cbegin() + row); + d->files.erase(d->files.begin() + row); emit removed(row); return true; } int CDocumentModel::rowCount() const { - return int(d->cdoc->files.size()); + return int(d->getFiles().size()); } QString CDocumentModel::save(int row, const QString &path) const { - if(d->isEncrypted) + if(d->isEncrypted()) return {}; int zone = FileDialog::fileZone(d->fileName); @@ -218,95 +338,6 @@ QString CDocumentModel::save(int row, const QString &path) const return fileName; } -CKey::CKey(Tag) - : unsupported(true) -{} - -CKey::CKey(const QSslCertificate &c) -{ - setCert(c); - recipient = [](const SslCertificate &c) { - QString cn = c.subjectInfo(QSslCertificate::CommonName); - QString gn = c.subjectInfo("GN"); - QString sn = c.subjectInfo("SN"); - if(!gn.isEmpty() || !sn.isEmpty()) - cn = QStringLiteral("%1 %2 %3").arg(gn, sn, c.personalCode()); - - int certType = c.type(); - if(certType & SslCertificate::EResidentSubType) - return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("Digi-ID E-RESIDENT")); - if(certType & SslCertificate::DigiIDType) - return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("Digi-ID")); - if(certType & SslCertificate::EstEidType) - return QStringLiteral("%1 %2").arg(cn, CryptoDoc::tr("ID-CARD")); - return cn; - }(c); -} - -void CKey::setCert(const QSslCertificate &c) -{ - QSslKey k = c.publicKey(); - cert = c; - key = Crypto::toPublicKeyDer(k); - isRSA = k.algorithm() == QSsl::Rsa; -} - -QHash CKey::fromKeyLabel() const -{ - QHash result; - if(!recipient.startsWith(QLatin1String("data:"), Qt::CaseInsensitive)) - return result; - QString payload = recipient.mid(5); - QString mimeType; - QString encoding; - if(auto pos = payload.indexOf(','); pos != -1) - { - mimeType = payload.left(pos); - payload = payload.mid(pos + 1); - if(auto header = mimeType.split(';'); header.size() == 2) - { - mimeType = header.value(0); - encoding = header.value(1); - } - } - if(!mimeType.isEmpty() && mimeType != QLatin1String("application/x-www-form-urlencoded")) - return result; - if(encoding == QLatin1String("base64")) - payload = QByteArray::fromBase64(payload.toLatin1()); - ; - for(const auto &[key,value]: QUrlQuery(payload).queryItems(QUrl::FullyDecoded)) - result[key.toLower()] = value; - if(!result.contains(QStringLiteral("type")) || !result.contains(QStringLiteral("v"))) - result.clear(); - return result; -} - -QString CKey::toKeyLabel() const -{ - if(cert.isNull()) - return recipient; - QDateTime exp = cert.expiryDate(); - if(Settings::CDOC2_USE_KEYSERVER) - exp = std::min(exp, QDateTime::currentDateTimeUtc().addMonths(6)); - auto escape = [](QString data) { return data.replace(',', QLatin1String("%2C")); }; - QString type = QStringLiteral("ID-card"); - if(auto t = SslCertificate(cert).type(); t & SslCertificate::EResidentSubType) - type = QStringLiteral("Digi-ID E-RESIDENT"); - else if(t & SslCertificate::DigiIDType) - type = QStringLiteral("Digi-ID"); - QUrlQuery q; - q.setQueryItems({ - {QStringLiteral("v"), QString::number(1)}, - {QStringLiteral("type"), type}, - {QStringLiteral("serial_number"), escape(cert.subjectInfo("serialNumber").join(','))}, - {QStringLiteral("cn"), escape(cert.subjectInfo("CN").join(','))}, - {QStringLiteral("server_exp"), QString::number(exp.toSecsSinceEpoch())}, - }); - return "data:" + q.query(QUrl::FullyEncoded); -} - - - CryptoDoc::CryptoDoc( QObject *parent ) : QObject(parent) , d(new Private) @@ -317,22 +348,34 @@ CryptoDoc::CryptoDoc( QObject *parent ) CryptoDoc::~CryptoDoc() { clear(); delete d; } -bool CryptoDoc::addKey( const CKey &key ) +bool +CryptoDoc::supportsSymmetricKeys() const { - if(d->isEncryptedWarning()) - return false; - if(d->cdoc->keys.contains(key)) - { - WarningDialog::show(tr("Key already exists")); + return !d->reader && Settings::CDOC2_DEFAULT; +} + +bool CryptoDoc::addEncryptionKey(const QSslCertificate &cert) { + if (d->warnIfNotWritable()) { return false; } - d->cdoc->keys.append(key); + for (auto &k : d->keys) { + if (k.rcpt_cert == cert) { + WarningDialog::show( + tr("Recipient with the same key already exists")); + return false; + } + } + d->keys.push_back({{}, cert}); return true; } -bool CryptoDoc::canDecrypt(const QSslCertificate &cert) -{ - return !d->cdoc->canDecrypt(cert).key.isEmpty(); +bool CryptoDoc::canDecrypt(const QSslCertificate &cert) { + if (!d->reader) + return false; + QByteArray der = cert.toDer(); + libcdoc::Lock lock; + return d->reader->getLockForCert( + std::vector(der.cbegin(), der.cend())) >= 0; } void CryptoDoc::clear( const QString &file ) @@ -344,41 +387,63 @@ void CryptoDoc::clear( const QString &file ) QFile::remove(f); } d->tempFiles.clear(); - d->isEncrypted = false; + d->reader.reset(); d->fileName = file; - if(Settings::CDOC2_DEFAULT) - d->cdoc = std::make_unique(); - else - d->cdoc = std::make_unique(); + d->writer_last_error.clear(); + d->files.clear(); } ContainerState CryptoDoc::state() const { - return d->isEncrypted ? EncryptedContainer : UnencryptedContainer; + return d->isEncrypted() ? EncryptedContainer : UnencryptedContainer; } -bool CryptoDoc::decrypt() -{ - if( d->fileName.isEmpty() ) - { +bool CryptoDoc::decrypt(const libcdoc::Lock *lock, const QByteArray &secret) { + if (d->fileName.isEmpty()) { WarningDialog::show(tr("Container is not open")); return false; } - if(!d->isEncrypted) + if (!d->reader) return true; - CKey key = d->cdoc->canDecrypt(qApp->signer()->tokenauth().cert()); - if(key.key.isEmpty()) - { - WarningDialog::show(tr("You do not have the key to decrypt this document")); + int lock_idx = -1; + const std::vector locks = d->reader->getLocks(); + if (lock == nullptr) { + QByteArray der = qApp->signer()->tokenauth().cert().toDer(); + lock_idx = d->reader->getLockForCert( + std::vector(der.cbegin(), der.cend())); + if (lock_idx < 0) { + WarningDialog::show( + tr("You do not have the key to decrypt this document")); + return false; + } + lock = &locks.at(lock_idx); + } else { + for (lock_idx = 0; lock_idx < locks.size(); lock_idx++) { + if (lock->label == locks[lock_idx].label) { + lock = &locks.at(lock_idx); + break; + } + } + if (lock_idx >= locks.size()) + lock_idx = -1; + } + if (!lock || (lock->isSymmetric() && secret.isEmpty())) { + WarningDialog::show( + tr("You do not have the key to decrypt this document")); return false; } - if(d->cdoc->version() == 2 && !key.transaction_id.isEmpty() && !Settings::CDOC2_NOTIFICATION.isSet()) - { - auto *dlg = new WarningDialog(tr("You must enter your PIN code twice in order to decrypt the CDOC2 container. " - "The first PIN entry is required for authentication to the key server referenced in the CDOC2 container. " - "Second PIN entry is required to decrypt the CDOC2 container."), Application::mainWindow()); + if (d->reader->version == 2 && + (lock->type == libcdoc::Lock::Type::SERVER) && + !Settings::CDOC2_NOTIFICATION.isSet()) { + auto *dlg = new WarningDialog( + tr("You must enter your PIN code twice in order to decrypt the " + "CDOC2 container. " + "The first PIN entry is required for authentication to the key " + "server referenced in the CDOC2 container. " + "Second PIN entry is required to decrypt the CDOC2 container."), + Application::mainWindow()); dlg->setCancelText(WarningDialog::Cancel); dlg->addButton(WarningDialog::OK, QMessageBox::Ok); dlg->addButton(tr("Don't show again"), QMessageBox::Ignore); @@ -388,66 +453,82 @@ bool CryptoDoc::decrypt() case QMessageBox::Ignore: Settings::CDOC2_NOTIFICATION = true; break; - default: return false; + default: + return false; } } - d->key = d->cdoc->transportKey(key); -#ifndef NDEBUG - qDebug() << "Transport key" << d->key.toHex(); -#endif - if(d->key.isEmpty()) - { - WarningDialog::show(tr("Failed to decrypt document. Please check your internet connection and network settings."), d->cdoc->lastError); + d->crypto.secret.assign(secret.cbegin(), secret.cend()); + std::vector fmk; + if (d->reader->getFMK(fmk, lock_idx) != libcdoc::OK) + return false; + d->fmk = QByteArray(reinterpret_cast(fmk.data()), fmk.size()); + if (d->fmk.isEmpty()) { + const std::string &msg = d->reader->getLastErrorStr(); + WarningDialog::show(tr("Failed to decrypt document. Please check your " + "internet connection and network settings."), + QString::fromStdString(msg)); return false; } d->waitForFinished(); - if(d->isEncrypted) - WarningDialog::show(tr("Error parsing document")); - if(!d->cdoc->lastError.isEmpty()) - WarningDialog::show(d->cdoc->lastError); - - return !d->isEncrypted; + if (d->reader) { + const std::string &msg = d->reader->getLastErrorStr(); + if (msg.empty()) { + WarningDialog::show(tr("Error parsing document")); + } else { + WarningDialog::show(QString::fromStdString(msg)); + } + } + return !d->isEncrypted(); } -DocumentModel* CryptoDoc::documentModel() const { return d->documents; } +DocumentModel *CryptoDoc::documentModel() const { return d->documents; } -bool CryptoDoc::encrypt( const QString &filename ) +bool CryptoDoc::encrypt( const QString &filename, const QString& label, const QByteArray& secret, uint32_t kdf_iter) { - if( !filename.isEmpty() ) - d->fileName = filename; - if( d->fileName.isEmpty() ) - { + if(!filename.isEmpty()) d->fileName = filename; + if(d->fileName.isEmpty()) { WarningDialog::show(tr("Container is not open")); return false; } - if(d->isEncrypted) - return true; - if(d->cdoc->keys.isEmpty()) - { - WarningDialog::show(tr("No keys specified")); - return false; + // I think the correct semantics is to fail if container is already encrypted + if(d->reader) return false; + if (secret.isEmpty()) { + // Encrypt for address list + if(d->keys.empty()) + { + WarningDialog::show(tr("No keys specified")); + return false; + } + } else { + // Encrypt with symmetric key + d->label = label; + d->crypto.secret.assign(secret.cbegin(), secret.cend()); + d->kdf_iter = kdf_iter; } - d->waitForFinished(); - if(d->isEncrypted) + d->label.clear(); + d->crypto.secret.clear(); + if(d->isEncrypted()) { open(d->fileName); - else - WarningDialog::show(tr("Failed to encrypt document. Please check your internet connection and network settings."), d->cdoc->lastError); - return d->isEncrypted; + } else { + WarningDialog::show(tr("Failed to encrypt document. Please check your internet connection and network settings."), QString::fromStdString(d->writer_last_error)); + } + return d->isEncrypted(); } QString CryptoDoc::fileName() const { return d->fileName; } -QList CryptoDoc::keys() const +const std::vector& +CryptoDoc::keys() const { - return d->cdoc->keys; + return d->keys; } bool CryptoDoc::move(const QString &to) { - if(!d->isEncrypted) + if(!d->isEncrypted()) { d->fileName = to; return true; @@ -456,26 +537,32 @@ bool CryptoDoc::move(const QString &to) return false; } -bool CryptoDoc::open( const QString &file ) +bool CryptoDoc::open(const QString &file) { + d->writer_last_error.clear(); clear(file); - d->cdoc = std::make_unique(d->fileName); - if(d->cdoc->keys.isEmpty()) - d->cdoc = std::make_unique(d->fileName); - d->isEncrypted = bool(d->cdoc); - if(!d->isEncrypted || d->cdoc->keys.isEmpty()) - { - WarningDialog::show(tr("Failed to open document"), d->cdoc->lastError); + d->reader = d->createCDocReader(file.toStdString()); + if (!d->reader) { return false; } + std::vector files = CDocSupport::getCDocFileList(file); + for (auto& f : files) { + d->files.push_back({f.name, {}, f.size, {}}); + } Application::addRecent( file ); return true; } -void CryptoDoc::removeKey( int id ) +void CryptoDoc::removeKey(unsigned int id) +{ + if(!d->warnIfNotWritable()) + d->keys.erase(d->keys.begin() + id); +} + +void CryptoDoc::clearKeys() { - if(!d->isEncryptedWarning()) - d->cdoc->keys.removeAt(id); + if(!d->warnIfNotWritable()) + d->keys.clear(); } bool CryptoDoc::saveCopy(const QString &filename) diff --git a/client/CryptoDoc.h b/client/CryptoDoc.h index cdf1683c..87ed8d97 100644 --- a/client/CryptoDoc.h +++ b/client/CryptoDoc.h @@ -25,62 +25,23 @@ #include #include -#include +#include +#include +#include +#include class QSslKey; -class CKey -{ -public: - enum Tag - { - Unsupported, - }; - CKey() = default; - CKey(Tag); - CKey(QByteArray _key, bool _isRSA): key(std::move(_key)), isRSA(_isRSA) {} - CKey(const QSslCertificate &cert); - bool operator==(const CKey &other) const { return other.key == key; } - - void setCert(const QSslCertificate &c); - QHash fromKeyLabel() const; - QString toKeyLabel() const; - - QByteArray key, cipher, publicKey; - QSslCertificate cert; - bool isRSA = false, unsupported = false; - QString recipient; - // CDoc1 - QString concatDigest; - QByteArray AlgorithmID, PartyUInfo, PartyVInfo; - // CDoc2 - QByteArray encrypted_kek; - QString keyserver_id, transaction_id; -}; - +// +// A wrapper structure for UI that contains either: +// - lock information for decryption +// - recipient certificate for encryption +// - -class CDoc -{ -public: - struct File - { - QString name, id, mime; - qint64 size; - std::unique_ptr data; - }; - - virtual ~CDoc() = default; - virtual CKey canDecrypt(const QSslCertificate &cert) const = 0; - virtual bool decryptPayload(const QByteArray &key) = 0; - virtual bool save(const QString &path) = 0; - bool setLastError(const QString &msg) { return (lastError = msg).isEmpty(); } - virtual QByteArray transportKey(const CKey &key) = 0; - virtual int version() = 0; - - QList keys; - std::vector files; - QString lastError; +struct CDKey { + libcdoc::Lock lock; + QSslCertificate rcpt_cert; + bool operator== (const CDKey& rhs) const = default; }; class CryptoDoc final: public QObject @@ -90,20 +51,23 @@ class CryptoDoc final: public QObject CryptoDoc(QObject *parent = nullptr); ~CryptoDoc() final; - bool addKey( const CKey &key ); + bool supportsSymmetricKeys() const; + bool addEncryptionKey(const QSslCertificate& cert); bool canDecrypt(const QSslCertificate &cert); void clear(const QString &file = {}); - bool decrypt(); + bool decrypt(const libcdoc::Lock *lock, const QByteArray& secret); + bool encrypt(const QString &filename = {}, const QString& label = {}, const QByteArray& secret = {}, uint32_t kdf_iter = 0); DocumentModel* documentModel() const; - bool encrypt(const QString &filename = {}); QString fileName() const; - QList keys() const; + const std::vector& keys() const; bool move(const QString &to); - bool open( const QString &file ); - void removeKey( int id ); + bool open(const QString &file); + void removeKey(unsigned int id); + void clearKeys(); bool saveCopy(const QString &filename); ria::qdigidoc4::ContainerState state() const; + static std::string labelFromCertificate(const std::vector& cert); private: class Private; Private *d; diff --git a/client/MainWindow.cpp b/client/MainWindow.cpp index 472c0cb3..1991d7fc 100644 --- a/client/MainWindow.cpp +++ b/client/MainWindow.cpp @@ -35,6 +35,7 @@ #include "effects/Overlay.h" #include "dialogs/FileDialog.h" #include "dialogs/MobileProgress.h" +#include "dialogs/PasswordDialog.h" #include "dialogs/RoleAddressDialog.h" #include "dialogs/SettingsDialog.h" #include "dialogs/SmartIDProgress.h" @@ -149,6 +150,8 @@ MainWindow::MainWindow( QWidget *parent ) ui->crypto->warningIcon(true); }); + connect(ui->cryptoContainerPage, &ContainerPage::decryptReq, this, &MainWindow::decryptClicked); + connect(ui->accordion, &Accordion::changePin1Clicked, this, &MainWindow::changePin1Clicked); connect(ui->accordion, &Accordion::changePin2Clicked, this, &MainWindow::changePin2Clicked); connect(ui->accordion, &Accordion::changePukClicked, this, &MainWindow::changePukClicked); @@ -228,14 +231,38 @@ ContainerState MainWindow::currentState() return ContainerState::Uninitialized; } -bool MainWindow::decrypt() -{ - if(!cryptoDoc) +bool MainWindow::decrypt(const libcdoc::Lock *lock) { + if (!cryptoDoc) return false; + QByteArray secret; + if (lock && (lock->type == libcdoc::Lock::Type::SYMMETRIC_KEY || + lock->type == libcdoc::Lock::Type::PASSWORD)) { + PasswordDialog p; + p.setLabel(QString::fromStdString(lock->label)); + if (lock->type == libcdoc::Lock::Type::PASSWORD) { + p.setMode(PasswordDialog::Mode::DECRYPT, + PasswordDialog::Type::PASSWORD); + if (!p.exec()) + return false; + secret = p.secret(); + } else { + p.setMode(PasswordDialog::Mode::DECRYPT, PasswordDialog::Type::KEY); + if (!p.exec()) + return false; + secret = p.secret(); + } + } + WaitDialogHolder waitDialog(this, tr("Decrypting")); - return cryptoDoc->decrypt(); + if (cryptoDoc->decrypt(lock, secret)) { + ui->cryptoContainerPage->transition(cryptoDoc.get(), + qApp->signer()->tokenauth().cert()); + FadeInNotification::success(ui->topBar, tr("Decryption succeeded!")); + return true; + } + return false; } void MainWindow::dragEnterEvent(QDragEnterEvent *event) @@ -283,7 +310,7 @@ QStringList MainWindow::dropEventFiles(QDropEvent *event) return files; } -bool MainWindow::encrypt() +bool MainWindow::encrypt(bool askForKey) { if(!cryptoDoc) return false; @@ -294,14 +321,28 @@ bool MainWindow::encrypt() dlg->addButton(WarningDialog::YES, QMessageBox::Yes); if(dlg->exec() == QMessageBox::Yes) { moveCryptoContainer(); - return encrypt(); + return encrypt(askForKey); } return false; } - WaitDialogHolder waitDialog(this, tr("Encrypting")); - - return cryptoDoc->encrypt(); + if (askForKey) { + PasswordDialog p; + p.setMode(PasswordDialog::Mode::ENCRYPT, PasswordDialog::Type::PASSWORD); + if(!p.exec()) return false; + QString label = p.label(); + QByteArray secret = p.secret(); + if (p.type == PasswordDialog::Type::PASSWORD) { + WaitDialogHolder waitDialog(this, tr("Encrypting")); + return cryptoDoc->encrypt(cryptoDoc->fileName(), label, secret, 65536); + } else { + WaitDialogHolder waitDialog(this, tr("Encrypting")); + return cryptoDoc->encrypt(cryptoDoc->fileName(), label, secret, 0); + } + } else { + WaitDialogHolder waitDialog(this, tr("Encrypting")); + return cryptoDoc->encrypt(); + } } void MainWindow::mouseReleaseEvent(QMouseEvent *event) @@ -455,15 +496,17 @@ void MainWindow::convertToCDoc() auto cryptoContainer = std::make_unique(this); cryptoContainer->clear(filename); - // If signed, add whole signed document to cryptocontainer; otherwise content only - if(digiDoc->state() == SignedContainer) + // If signed, add whole signed document to cryptocontainer; otherwise + // content only + if (digiDoc->state() == SignedContainer) cryptoContainer->documentModel()->addFile(digiDoc->fileName()); else cryptoContainer->documentModel()->addTempFiles(digiDoc->documentModel()->tempFiles()); auto cardData = qApp->signer()->tokenauth(); - if(!cardData.cert().isNull()) - cryptoContainer->addKey(CKey(cardData.cert())); + if (!cardData.cert().isNull()) { + cryptoContainer->addEncryptionKey(cardData.cert()); + } resetCryptoDoc(std::move(cryptoContainer)); resetDigiDoc(nullptr, false); @@ -501,7 +544,7 @@ void MainWindow::onCryptoAction(int action, const QString &/*id*/, const QString break; case DecryptContainer: case DecryptToken: - if(decrypt()) + if(decrypt(nullptr)) { ui->cryptoContainerPage->transition(cryptoDoc.get(), qApp->signer()->tokenauth().cert()); FadeInNotification::success(ui->topBar, tr("Decryption succeeded!")); @@ -514,7 +557,13 @@ void MainWindow::onCryptoAction(int action, const QString &/*id*/, const QString FadeInNotification::success(ui->topBar, tr("Encryption succeeded!")); } break; - case ContainerSaveAs: + case EncryptLT: + if(encrypt(true)) { + ui->cryptoContainerPage->transition(cryptoDoc.get(), qApp->signer()->tokenauth().cert()); + FadeInNotification::success(ui->topBar, tr("Encryption succeeded!")); + } + break; + case ContainerSaveAs: { if(!cryptoDoc) break; @@ -1069,3 +1118,11 @@ void MainWindow::containerSummary() dialog->exec(); dialog->deleteLater(); } + +void +MainWindow::decryptClicked(const libcdoc::Lock *lock) +{ + qDebug() << "Decrypt clicked:"; + decrypt(lock); +} + diff --git a/client/MainWindow.h b/client/MainWindow.h index b63e9fe9..a3390ea9 100644 --- a/client/MainWindow.h +++ b/client/MainWindow.h @@ -23,12 +23,13 @@ #include "common_enums.h" #include "QSmartCard.h" +#include "cdoc/Lock.h" namespace Ui { class MainWindow; } -class CKey; +struct CDKey; class CryptoDoc; class DigiDoc; class DocumentModel; @@ -71,8 +72,8 @@ private Q_SLOTS: void convertToBDoc(); void convertToCDoc(); ria::qdigidoc4::ContainerState currentState(); - bool decrypt(); - bool encrypt(); + bool decrypt(const libcdoc::Lock *lock); + bool encrypt(bool askForKey = false); void loadPicture(); void moveCryptoContainer(); void moveSignatureContainer(); @@ -114,4 +115,6 @@ private Q_SLOTS: std::unique_ptr cryptoDoc; DigiDoc* digiDoc = nullptr; Ui::MainWindow *ui; + + void decryptClicked(const libcdoc::Lock *lock); }; diff --git a/client/Utils.h b/client/Utils.h index 4038e5f5..072d09e6 100644 --- a/client/Utils.h +++ b/client/Utils.h @@ -24,6 +24,9 @@ #include #include #include +#include + +#include "Application.h" namespace { template @@ -81,6 +84,32 @@ namespace { return escaped; } + inline qint64 copyIODevice(std::basic_istream *from, QIODevice *to, qint64 max = std::numeric_limits::max()) + { + std::array buf{}; + size_t total_read = 0; + while ((total_read < max) && !from->eof() && !from->bad()) { + size_t to_read = std::min(max, buf.size()); + from->read(buf.data(), to_read); + size_t n_read = from->gcount(); + if(to->write(buf.data(), n_read) != n_read) return -1; + total_read += n_read; + } + return total_read; + } + + inline qint64 copyIODevice(QIODevice *from, std::streambuf *to, qint64 max = std::numeric_limits::max()) + { + std::array buf{}; + qint64 size = 0, i = 0; + for(; (i = from->read(buf.data(), std::min(max, buf.size()))) > 0; size += i, max -= i) + { + if(to->sputn(buf.data(), i) != i) + return -1; + } + return i < 0 ? i : size; + } + inline qint64 copyIODevice(QIODevice *from, QIODevice *to, qint64 max = std::numeric_limits::max()) { std::array buf{}; diff --git a/client/common_enums.h b/client/common_enums.h index c9529e7c..52aa2cfa 100644 --- a/client/common_enums.h +++ b/client/common_enums.h @@ -56,6 +56,7 @@ enum Actions { SignatureToken, ClearSignatureWarning, ClearCryptoWarning, + EncryptLT }; enum ItemType { diff --git a/client/dialogs/AddRecipients.cpp b/client/dialogs/AddRecipients.cpp index c79311ff..71341aaf 100644 --- a/client/dialogs/AddRecipients.cpp +++ b/client/dialogs/AddRecipients.cpp @@ -19,6 +19,7 @@ #include "AddRecipients.h" +#include "dialogs/PasswordDialog.h" #include "ui_AddRecipients.h" #include "Application.h" @@ -32,12 +33,14 @@ #include "TokenData.h" #include "dialogs/WarningDialog.h" #include "effects/Overlay.h" +#include "Crypto.h" #include #include #include #include #include +#include #include AddRecipients::AddRecipients(ItemList* itemList, QWidget *parent) @@ -96,7 +99,6 @@ AddRecipients::~AddRecipients() delete ui; } - void AddRecipients::addRecipientFromFile() { QString file = FileDialog::getOpenFileName(this, windowTitle(), {}, @@ -148,12 +150,23 @@ void AddRecipients::addRecipientFromHistory() void AddRecipients::addRecipient(const QSslCertificate& cert, bool select) { - AddressItem *leftItem = itemListValue(ui->leftPane, cert); + CDKey key = { {}, cert }; + AddressItem *leftItem = itemListValue(ui->leftPane, key); if(!leftItem) { - leftItem = new AddressItem(cert, AddressItem::Add, ui->leftPane); + QByteArray qder = cert.toDer(); + std::vector sder = std::vector(qder.cbegin(), qder.cend()); + + leftItem = new AddressItem(key, AddressItem::Add, ui->leftPane); ui->leftPane->addWidget(leftItem); - bool contains = rightList.contains(cert); + + bool contains = false; + for (auto rhs: rightList) { + if (rhs.rcpt_cert == cert) { + contains = true; + break; + } + } leftItem->setDisabled(contains); connect(leftItem, &AddressItem::add, this, [this](Item *item) { addRecipientToRightPane(item); }); if(auto *add = ui->leftPane->findChild(QStringLiteral("add"))) @@ -167,13 +180,16 @@ void AddRecipients::addRecipient(const QSslCertificate& cert, bool select) void AddRecipients::addRecipientToRightPane(Item *item, bool update) { auto *address = qobject_cast(item); - if(!address || rightList.contains(address->getKey())) - return; + const CDKey& key = address->getKey(); + if(!address) return; + for (auto &rhs : rightList) { + if (key.rcpt_cert == rhs.rcpt_cert) + return; + } - const auto &key = address->getKey(); if(update) { - if(auto expiryDate = key.cert.expiryDate(); expiryDate <= QDateTime::currentDateTime()) + if(auto expiryDate = key.rcpt_cert.expiryDate(); expiryDate <= QDateTime::currentDateTime()) { if(Settings::CDOC2_DEFAULT && Settings::CDOC2_USE_KEYSERVER) { @@ -190,9 +206,9 @@ void AddRecipients::addRecipientToRightPane(Item *item, bool update) } QSslConfiguration backup = QSslConfiguration::defaultConfiguration(); QSslConfiguration::setDefaultConfiguration(CheckConnection::sslConfiguration()); - QList errors = QSslCertificate::verify({ key.cert }); + QList errors = QSslCertificate::verify({key.rcpt_cert}); QSslConfiguration::setDefaultConfiguration(backup); - errors.removeAll(QSslError(QSslError::CertificateExpired, key.cert)); + errors.removeAll(QSslError(QSslError::CertificateExpired, key.rcpt_cert)); if(!errors.isEmpty()) { auto *dlg = new WarningDialog(tr("Recipient’s certification chain contains certificates that are not trusted. Continue with encryption?"), this); @@ -209,7 +225,7 @@ void AddRecipients::addRecipientToRightPane(Item *item, bool update) auto *rightItem = new AddressItem(key, AddressItem::Remove, ui->rightPane); connect(rightItem, &AddressItem::remove, this, [this](Item *item) { auto *rightItem = qobject_cast(item); - if(auto *leftItem = itemListValue(ui->leftPane, rightItem->getKey().cert)) + if(auto *leftItem = itemListValue(ui->leftPane, rightItem->getKey())) leftItem->setDisabled(false); rightList.removeAll(rightItem->getKey()); updated = true; @@ -217,7 +233,7 @@ void AddRecipients::addRecipientToRightPane(Item *item, bool update) }); ui->rightPane->addWidget(rightItem); ui->confirm->setEnabled(true); - historyCertData.addAndSave(key.cert); + historyCertData.addAndSave(key.rcpt_cert); if(auto *leftItem = itemListValue(ui->leftPane, key)) leftItem->setDisabled(true); } @@ -232,19 +248,19 @@ bool AddRecipients::isUpdated() const return updated; } -AddressItem* AddRecipients::itemListValue(ItemList *list, const CKey &cert) +AddressItem* AddRecipients::itemListValue(ItemList *list, const CDKey &key) { for(auto *item: list->items) { - if(auto *address = qobject_cast(item); address && address->getKey() == cert) + if(auto *address = qobject_cast(item); address && address->getKey() == key) return address; } return nullptr; } -QList AddRecipients::keys() +QList AddRecipients::keys() { - QList recipients; + QList recipients; for(auto *item: ui->rightPane->items) { if(auto *address = qobject_cast(item)) diff --git a/client/dialogs/AddRecipients.h b/client/dialogs/AddRecipients.h index 0c9fb001..5f55e906 100644 --- a/client/dialogs/AddRecipients.h +++ b/client/dialogs/AddRecipients.h @@ -41,7 +41,7 @@ class AddRecipients final : public QDialog explicit AddRecipients(ItemList* itemList, QWidget *parent = nullptr); ~AddRecipients() final; - QList keys(); + QList keys(); bool isUpdated() const; private: @@ -50,7 +50,7 @@ class AddRecipients final : public QDialog void addRecipient(const QSslCertificate& cert, bool select = true); void addRecipientToRightPane(Item *item, bool update = true); - AddressItem* itemListValue(ItemList *list, const CKey &cert); + AddressItem* itemListValue(ItemList *list, const CDKey &key); void search(const QString &term, bool select = false, const QString &type = {}); void showError(const QString &msg, const QString &details = {}); void showResult(const QList &result, int resultCount, const QVariantMap &userData); @@ -58,7 +58,7 @@ class AddRecipients final : public QDialog static QString defaultUrl(QLatin1String key, const QString &defaultValue); Ui::AddRecipients *ui; - QList rightList; + QList rightList; LdapSearch *ldap_person, *ldap_corp; bool updated = false; diff --git a/client/dialogs/KeyDialog.cpp b/client/dialogs/KeyDialog.cpp index 69f43f9c..a6e5edb3 100644 --- a/client/dialogs/KeyDialog.cpp +++ b/client/dialogs/KeyDialog.cpp @@ -24,7 +24,7 @@ #include "effects/Overlay.h" #include "dialogs/CertificateDetails.h" -KeyDialog::KeyDialog( const CKey &k, QWidget *parent ) +KeyDialog::KeyDialog(const CDKey &k, QWidget *parent ) : QDialog( parent ) { Ui::KeyDialog d; @@ -37,10 +37,21 @@ KeyDialog::KeyDialog( const CKey &k, QWidget *parent ) new Overlay(this); connect(d.close, &QPushButton::clicked, this, &KeyDialog::accept); - connect(d.showCert, &QPushButton::clicked, this, [this, cert=k.cert] { - CertificateDetails::showCertificate(cert, this); - }); - d.showCert->setHidden(k.cert.isNull()); + if (!k.rcpt_cert.isNull()) { + connect(d.showCert, &QPushButton::clicked, this, [this, cert = k.rcpt_cert] { + CertificateDetails::showCertificate(cert, this); + }); + d.showCert->setHidden(false); + } else if (k.lock.isCertificate()) { + std::vector cert = k.lock.getBytes(libcdoc::Lock::Params::CERT); + QSslCertificate kcert(QByteArray(reinterpret_cast(cert.data()), cert.size()), QSsl::Der); + connect(d.showCert, &QPushButton::clicked, this, [this, c = kcert] { + CertificateDetails::showCertificate(c, this); + }); + d.showCert->setHidden(kcert.isNull()); + } else { + d.showCert->setHidden(true); + } auto addItem = [view = d.view](const QString ¶meter, const QString &value) { if(value.isEmpty()) @@ -51,12 +62,22 @@ KeyDialog::KeyDialog( const CKey &k, QWidget *parent ) view->addTopLevelItem(i); }; - addItem(tr("Recipient"), k.recipient); - addItem(tr("ConcatKDF digest method"), k.concatDigest); - addItem(tr("Key server ID"), k.keyserver_id); - addItem(tr("Transaction ID"), k.transaction_id); - addItem(tr("Expiry date"), k.cert.expiryDate().toLocalTime().toString(QStringLiteral("dd.MM.yyyy hh:mm:ss"))); - addItem(tr("Issuer"), k.cert.issuerInfo(QSslCertificate::CommonName).join(' ')); + addItem(tr("Recipient"), QString::fromStdString(k.lock.label)); + if (k.lock.isCertificate()) { + std::vector cert = k.lock.getBytes(libcdoc::Lock::Params::CERT); + QSslCertificate kcert(QByteArray(reinterpret_cast(cert.data()), cert.size()), QSsl::Der); + if (k.lock.isCDoc1()) { + std::string cdigest = k.lock.getString(libcdoc::Lock::Params::CONCAT_DIGEST); + addItem(tr("ConcatKDF digest method"), QString::fromStdString(cdigest)); + } + addItem(tr("Expiry date"), kcert.expiryDate().toLocalTime().toString(QStringLiteral("dd.MM.yyyy hh:mm:ss"))); + auto iss = kcert.issuerInfo(QSslCertificate::CommonName); + addItem(tr("Issuer"), iss.join(" ")); + d.view->resizeColumnToContents(0); + } else if (k.lock.type == libcdoc::Lock::SERVER) { + addItem(tr("Key server ID"), QString::fromUtf8(k.lock.getString(libcdoc::Lock::Params::KEYSERVER_ID))); + addItem(tr("Transaction ID"), QString::fromUtf8(k.lock.getString(libcdoc::Lock::Params::TRANSACTION_ID))); + } d.view->resizeColumnToContents( 0 ); adjustSize(); } diff --git a/client/dialogs/KeyDialog.h b/client/dialogs/KeyDialog.h index f65afe7c..44efdd37 100644 --- a/client/dialogs/KeyDialog.h +++ b/client/dialogs/KeyDialog.h @@ -21,11 +21,12 @@ #include -class CKey; +struct CDKey; + class KeyDialog final: public QDialog { Q_OBJECT public: - KeyDialog(const CKey &key, QWidget *parent = nullptr); + KeyDialog(const CDKey &key, QWidget *parent = nullptr); }; diff --git a/client/dialogs/PasswordDialog.cpp b/client/dialogs/PasswordDialog.cpp new file mode 100644 index 00000000..c34dc007 --- /dev/null +++ b/client/dialogs/PasswordDialog.cpp @@ -0,0 +1,136 @@ +#include "PasswordDialog.h" +#include "Crypto.h" +#include "ui_PasswordDialog.h" + +PasswordDialog::PasswordDialog(QWidget *parent) + : QDialog(parent), mode(Mode::DECRYPT), type(Type::PASSWORD) + , ui(new Ui::PasswordDialog) +{ + ui->setupUi(this); + connect(ui->generateKey, &QPushButton::clicked, this, &PasswordDialog::genKeyClicked); + connect(ui->typeSelector, &QTabWidget::currentChanged, this, &PasswordDialog::typeChanged); + connect(ui->passwordLine, &QLineEdit::textChanged, this, &PasswordDialog::lineChanged); + connect(ui->password2Line, &QLineEdit::textChanged, this, &PasswordDialog::lineChanged); + connect(ui->keyEdit, &QPlainTextEdit::textChanged, this, &PasswordDialog::editChanged); +} + +PasswordDialog::~PasswordDialog() +{ + delete ui; +} + +void +PasswordDialog::setMode(Mode _mode, Type _type) +{ + mode = _mode; + type = _type; + updateUI(); +} + +void +PasswordDialog::setLabel(const QString& label) +{ + ui->labelLine->setText(label); +} + +QString +PasswordDialog::label() +{ + return ui->labelLine->text(); +} + +QByteArray +PasswordDialog::secret() const +{ + if (type == Type::PASSWORD) { + return ui->passwordLine->text().toUtf8(); + } else { + QString hex = ui->keyEdit->toPlainText(); + return QByteArray::fromHex(hex.toUtf8()); + } +} + +void +PasswordDialog::typeChanged(int index) +{ + Type new_type = static_cast(index); + if (new_type != type) setMode(mode, new_type); +} + +void +PasswordDialog::lineChanged(const QString& text) +{ + updateOK(); +} + +void +PasswordDialog::editChanged() +{ + updateOK(); +} + +void +PasswordDialog::genKeyClicked() +{ + QByteArray key = Crypto::random(); + ui->keyEdit->clear(); + ui->keyEdit->appendPlainText(key.toHex()); +} + +void +PasswordDialog::updateUI() +{ + ui->typeSelector->setCurrentIndex(type); + if (mode == Mode::DECRYPT) { + ui->labelLine->setReadOnly(true); + if (type == Type::PASSWORD) { + ui->typeSelector->setTabEnabled(Type::PASSWORD, true); + ui->typeSelector->setTabEnabled(Type::KEY, false); + ui->passwordLabel->setText("Enter password to decrypt the document"); + ui->passwordLine->setEchoMode(QLineEdit::EchoMode::Password); + ui->password2Label->hide(); + ui->password2Line->hide(); + } else { + ui->typeSelector->setTabEnabled(Type::PASSWORD, false); + ui->typeSelector->setTabEnabled(Type::KEY, true); + ui->keyLabel->setText("Enter key to decrypt the document"); + ui->generateKey->hide(); + } + } else { + ui->labelLine->setReadOnly(false); + if (type == Type::PASSWORD) { + ui->typeSelector->setTabEnabled(Type::PASSWORD, true); + ui->typeSelector->setTabEnabled(Type::KEY, true); + ui->passwordLabel->setText("Enter a password to encrypt the document"); + ui->passwordLine->setEchoMode(QLineEdit::EchoMode::Password); + ui->password2Label->show(); + ui->password2Line->show(); + } else { + ui->typeSelector->setTabEnabled(Type::PASSWORD, true); + ui->typeSelector->setTabEnabled(Type::KEY, true); + ui->keyLabel->setText("Enter a key to encrypt the document"); + ui->generateKey->show(); + } + } + updateOK(); +} + +void +PasswordDialog::updateOK() +{ + bool active = false; + if (mode == Mode::DECRYPT) { + if (type == Type::PASSWORD) { + active = !ui->passwordLine->text().isEmpty(); + } else { + active = !ui->keyEdit->toPlainText().isEmpty(); + } + } else { + if (type == Type::PASSWORD) { + active = !ui->passwordLine->text().isEmpty() && ui->passwordLine->text() == ui->password2Line->text(); + } else { + active = !ui->keyEdit->toPlainText().isEmpty(); + } + } + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(active); +} diff --git a/client/dialogs/PasswordDialog.h b/client/dialogs/PasswordDialog.h new file mode 100644 index 00000000..451144c2 --- /dev/null +++ b/client/dialogs/PasswordDialog.h @@ -0,0 +1,47 @@ +#ifndef PASSWORDDIALOG_H +#define PASSWORDDIALOG_H + +#include + +namespace Ui { +class PasswordDialog; +} + +class PasswordDialog : public QDialog +{ + Q_OBJECT + +public: + enum Mode { + ENCRYPT, + DECRYPT + }; + + enum Type { + PASSWORD, + KEY + }; + + Mode mode; + Type type; + + explicit PasswordDialog(QWidget *parent = nullptr); + ~PasswordDialog(); + + void setMode(Mode mode, Type type); + + void setLabel(const QString& label); + QString label(); + QByteArray secret() const; +private: + Ui::PasswordDialog *ui; + + void typeChanged(int index); + void lineChanged(const QString& text); + void editChanged(); + void genKeyClicked(); + void updateUI(); + void updateOK(); +}; + +#endif // PASSWORDDIALOG_H diff --git a/client/dialogs/PasswordDialog.ui b/client/dialogs/PasswordDialog.ui new file mode 100644 index 00000000..dd8d2049 --- /dev/null +++ b/client/dialogs/PasswordDialog.ui @@ -0,0 +1,175 @@ + + + PasswordDialog + + + + 0 + 0 + 400 + 322 + + + + Dialog + + + + + + Key label (recipient name or id) + + + Qt::AlignCenter + + + + + + + + + + 0 + + + + Password + + + + + + Password + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + + + + + QLineEdit::Password + + + + + + + Repeat password + + + + + + + QLineEdit::Password + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + Symmetric key + + + + + + Symmetric key + + + + + + + + + + Generate Key + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + false + + + + + + + + + buttonBox + accepted() + PasswordDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + PasswordDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/client/widgets/AddressItem.cpp b/client/widgets/AddressItem.cpp index 2bdeb019..82c15aad 100644 --- a/client/widgets/AddressItem.cpp +++ b/client/widgets/AddressItem.cpp @@ -17,6 +17,10 @@ * */ +#include +#include +#include + #include "AddressItem.h" #include "ui_AddressItem.h" @@ -30,16 +34,16 @@ class AddressItem::Private: public Ui::AddressItem { public: QString code; - CKey key; + CDKey key; QString label; bool yourself = false; }; -AddressItem::AddressItem(CKey k, Type type, QWidget *parent) +AddressItem::AddressItem(const CDKey &key, Type type, QWidget *parent) : Item(parent) , ui(new Private) { - ui->key = std::move(k); + ui->key = key; ui->setupUi(this); if(type == Icon) ui->icon->load(QStringLiteral(":/images/icon_Krypto_small.svg")); @@ -47,18 +51,47 @@ AddressItem::AddressItem(CKey k, Type type, QWidget *parent) ui->name->setAttribute(Qt::WA_TransparentForMouseEvents, true); ui->expire->setAttribute(Qt::WA_TransparentForMouseEvents, true); ui->idType->setAttribute(Qt::WA_TransparentForMouseEvents, true); - if(!ui->key.unsupported) + + bool unsupported = false; + if (!ui->key.rcpt_cert.isNull()) { + // Recipient certificate + ui->code = SslCertificate(ui->key.rcpt_cert).personalCode(); + ui->label = !ui->key.rcpt_cert.subjectInfo("GN").isEmpty() && + !ui->key.rcpt_cert.subjectInfo("SN").isEmpty() + ? ui->key.rcpt_cert.subjectInfo("GN").join(' ') + ' ' + + ui->key.rcpt_cert.subjectInfo("SN").join(' ') + : ui->key.rcpt_cert.subjectInfo("CN").join(' '); + ui->decrypt->hide(); + } else if (ui->key.lock.isValid()) { + // Known lock type + ui->code.clear(); + auto map = libcdoc::Recipient::parseLabel(ui->key.lock.label); + if (map.contains("cn")) { + ui->label = QString::fromStdString(map["cn"]); + } else { + ui->label = QString::fromStdString(ui->key.lock.label); + } + if (ui->key.lock.isSymmetric()) { + ui->decrypt->show(); + connect(ui->decrypt, &QToolButton::clicked, this, + [this] { emit decrypt(&ui->key.lock); }); + } else { + ui->decrypt->hide(); + } + } else { + // No rcpt, lock is invalid = unsupported lock + unsupported = true; + ui->code.clear(); + ui->label = tr("Unsupported cryptographic algorithm or recipient type"); + ui->decrypt->hide(); + } + + if(!unsupported) setCursor(Qt::PointingHandCursor); connect(ui->add, &QToolButton::clicked, this, [this]{ emit add(this);}); connect(ui->remove, &QToolButton::clicked, this, [this]{ emit remove(this);}); - ui->code = SslCertificate(ui->key.cert).personalCode(); - ui->label = !ui->key.cert.subjectInfo("GN").isEmpty() && !ui->key.cert.subjectInfo("SN").isEmpty() ? - ui->key.cert.subjectInfo("GN").join(' ') + ' ' + ui->key.cert.subjectInfo("SN").join(' ') : - ui->key.cert.subjectInfo("CN").join(' '); - if(ui->label.isEmpty()) - ui->label = ui->key.fromKeyLabel().value(QStringLiteral("cn"), ui->key.recipient); setIdType(); ui->add->setVisible(type == Add); ui->remove->setVisible(type != Add); @@ -82,15 +115,21 @@ void AddressItem::changeEvent(QEvent* event) QWidget::changeEvent(event); } -const CKey& AddressItem::getKey() const +const CDKey& AddressItem::getKey() const { return ui->key; } -void AddressItem::idChanged(const SslCertificate &cert) -{ - CKey key(cert); - ui->yourself = !key.key.isNull() && ui->key == key; +void AddressItem::idChanged(const SslCertificate &cert) { + QByteArray qder = cert.toDer(); + std::vector sder = std::vector(qder.cbegin(), qder.cend()); + + if (ui->key.lock.isValid()) { + QSslKey pkey = cert.publicKey(); + QByteArray der = pkey.toDer(); + ui->yourself = ui->key.lock.hasTheSameKey( + std::vector(der.cbegin(), der.cend())); + } setName(); } @@ -108,10 +147,9 @@ QWidget* AddressItem::lastTabWidget() return ui->add; } -void AddressItem::mouseReleaseEvent(QMouseEvent * /*event*/) -{ - if(!ui->key.unsupported) - (new KeyDialog(ui->key, this))->open(); +void AddressItem::mouseReleaseEvent(QMouseEvent * /*event*/) { + if (ui->key.rcpt_cert.isNull() && !ui->key.lock.isValid()) + (new KeyDialog(ui->key))->open(); } void AddressItem::setName() @@ -128,17 +166,9 @@ void AddressItem::stateChange(ContainerState state) ui->remove->setVisible(state == UnencryptedContainer); } -void AddressItem::setIdType() -{ - ui->expire->clear(); - SslCertificate cert(ui->key.cert); +void AddressItem::setIdType(const SslCertificate &cert) { SslCertificate::CertType type = cert.type(); - if(ui->key.unsupported) - { - ui->label = tr("Unsupported cryptographic algorithm or recipient type"); - ui->idType->clear(); - } - else if(type & SslCertificate::DigiIDType) + if(type & SslCertificate::DigiIDType) ui->idType->setText(tr("digi-ID")); else if(type & SslCertificate::EstEidType) ui->idType->setText(tr("ID-card")); @@ -153,30 +183,55 @@ void AddressItem::setIdType() else ui->idType->setText(tr("Certificate for Encryption")); } - else - { - auto items = ui->key.fromKeyLabel(); + ui->expire->setProperty("label", QStringLiteral("default")); + ui->expire->setText(QStringLiteral("%1 %2").arg( + cert.isValid() ? tr("Expires on") : tr("Expired on"), + cert.expiryDate().toLocalTime().toString( + QStringLiteral("dd.MM.yyyy")))); +} + +void AddressItem::setIdType() { + ui->expire->clear(); + + if (!ui->key.rcpt_cert.isNull()) { + // Recipient certificate + SslCertificate cert(ui->key.rcpt_cert); + setIdType(cert); + } else if (ui->key.lock.isValid()) { + // Known lock type + // Needed to include translation for "ID-CARD" void(QT_TR_NOOP("ID-CARD")); - ui->idType->setText(tr(items[QStringLiteral("type")].toUtf8().data())); - if(QString server_exp = items[QStringLiteral("server_exp")]; !server_exp.isEmpty()) - { - auto date = QDateTime::fromSecsSinceEpoch(server_exp.toLongLong(), Qt::UTC); - bool canDecrypt = QDateTime::currentDateTimeUtc() < date; - ui->expire->setProperty("label", canDecrypt ? QStringLiteral("good") : QStringLiteral("error")); - ui->expire->setText(canDecrypt ? QStringLiteral("%1 %2").arg( - tr("Decryption is possible until:"), date.toLocalTime().toString(QStringLiteral("dd.MM.yyyy"))) : - tr("Decryption has expired")); + auto items = libcdoc::Recipient::parseLabel(ui->key.lock.label); + if (ui->key.lock.isCertificate()) { + auto bytes = ui->key.lock.getBytes(libcdoc::Lock::CERT); + QByteArray qbytes((const char *)bytes.data(), bytes.size()); + SslCertificate cert(qbytes, QSsl::Der); + setIdType(cert); + } else { + ui->idType->setText(tr(items["type"].data())); } + if (ui->key.lock.type == libcdoc::Lock::SERVER) { + std::string server_exp = items["server_exp"]; + if (!server_exp.empty()) { + uint64_t seconds = std::stoull(server_exp); + auto date = QDateTime::fromSecsSinceEpoch(seconds, Qt::UTC); + bool canDecrypt = QDateTime::currentDateTimeUtc() < date; + ui->expire->setProperty("label", canDecrypt + ? QStringLiteral("good") + : QStringLiteral("error")); + ui->expire->setText( + canDecrypt ? QStringLiteral("%1 %2").arg( + tr("Decryption is possible until:"), + date.toLocalTime().toString( + QStringLiteral("dd.MM.yyyy"))) + : tr("Decryption has expired")); + } + } + } else { + // No rcpt, lock is invalid = unsupported lock + ui->idType->setText("Unsupported"); + ui->expire->setHidden(true); } - - if(!cert.isNull()) - { - ui->expire->setProperty("label", QStringLiteral("default")); - ui->expire->setText(QStringLiteral("%1 %2").arg( - cert.isValid() ? tr("Expires on") : tr("Expired on"), - cert.expiryDate().toLocalTime().toString(QStringLiteral("dd.MM.yyyy")))); - } - ui->idType->setHidden(ui->idType->text().isEmpty()); ui->expire->setHidden(ui->expire->text().isEmpty()); } diff --git a/client/widgets/AddressItem.h b/client/widgets/AddressItem.h index 507c6449..fe0d949e 100644 --- a/client/widgets/AddressItem.h +++ b/client/widgets/AddressItem.h @@ -19,9 +19,12 @@ #pragma once +#include + #include "widgets/Item.h" +#include "cdoc/Lock.h" -class CKey; +struct CDKey; class AddressItem final : public Item { @@ -35,20 +38,24 @@ class AddressItem final : public Item Icon, }; - explicit AddressItem(CKey k, Type type, QWidget *parent = {}); + explicit AddressItem(const CDKey &k, Type type, QWidget *parent = {}); ~AddressItem() final; - const CKey& getKey() const; + const CDKey& getKey() const; void idChanged(const SslCertificate &cert) final; void initTabOrder(QWidget *item) final; QWidget* lastTabWidget() final; void stateChange(ria::qdigidoc4::ContainerState state) final; +signals: + void decrypt(const libcdoc::Lock *lock); + private: void changeEvent(QEvent *event) final; void mouseReleaseEvent(QMouseEvent *event) final; void setName(); void setIdType(); + void setIdType(const SslCertificate& cert); class Private; Private *ui; diff --git a/client/widgets/AddressItem.ui b/client/widgets/AddressItem.ui index af93507c..64c35503 100644 --- a/client/widgets/AddressItem.ui +++ b/client/widgets/AddressItem.ui @@ -188,6 +188,14 @@ color: #727679; ++ ++ + +- Add ++ DECRYPT + + + diff --git a/client/widgets/ContainerPage.cpp b/client/widgets/ContainerPage.cpp index 1fdab787..7fb2dc2c 100644 --- a/client/widgets/ContainerPage.cpp +++ b/client/widgets/ContainerPage.cpp @@ -221,9 +221,10 @@ void ContainerPage::showMainAction(const QList &actions) bool isSignCard = actions.contains(SignatureAdd) || actions.contains(SignatureToken); bool isSignMobile = !isSignCard && (actions.contains(SignatureMobile) || actions.contains(SignatureSmartID)); bool isEncrypt = actions.contains(EncryptContainer) && !ui->rightPane->findChildren().isEmpty(); + bool isEncryptLT = actions.contains(EncryptLT); bool isDecrypt = !isBlocked && (actions.contains(DecryptContainer) || actions.contains(DecryptToken)); mainAction->setButtonEnabled(isSupported && !hasEmptyFile && - (isEncrypt || isDecrypt || isSignMobile || (isSignCard && !isBlocked && !isExpired))); + (isEncrypt || isEncryptLT || isDecrypt || isSignMobile || (isSignCard && !isBlocked && !isExpired))); ui->mainActionSpacer->changeSize(198, 20, QSizePolicy::Fixed); ui->navigationArea->layout()->invalidate(); } @@ -252,12 +253,11 @@ void ContainerPage::transition(CryptoDoc *container, const QSslCertificate &cert AddRecipients dlg(ui->rightPane, this); if(!dlg.exec() || !dlg.isUpdated()) return; - for(auto i = container->keys().size() - 1; i >= 0; i--) - container->removeKey(i); + container->clearKeys(); ui->rightPane->clear(); for(const auto &key: dlg.keys()) { - container->addKey(key); + container->addEncryptionKey(key.rcpt_cert); ui->rightPane->addWidget(new AddressItem(key, AddressItem::Icon, ui->rightPane)); } showMainAction({ EncryptContainer }); @@ -279,17 +279,19 @@ void ContainerPage::transition(CryptoDoc *container, const QSslCertificate &cert clear(); emit action(ClearCryptoWarning); - isSupported = container->state() & UnencryptedContainer || container->canDecrypt(cert); + isSupported = container->state() & UnencryptedContainer || + container->canDecrypt(cert); setHeader(container->fileName()); bool hasUnsupported = false; - for(const CKey &key: container->keys()) - { - hasUnsupported = std::max(hasUnsupported, key.unsupported); - ui->rightPane->addWidget(new AddressItem(key, AddressItem::Icon, ui->rightPane)); + for (auto &key : container->keys()) { + hasUnsupported = hasUnsupported || (key.rcpt_cert.isNull() && !key.lock.isValid()); + AddressItem *addr = new AddressItem(key, AddressItem::Icon, ui->rightPane); + ui->rightPane->addWidget(addr); + connect(addr, &AddressItem::decrypt, this, [this, key] { emit decryptReq(&key.lock); }); } - if(hasUnsupported) + if (hasUnsupported) emit warning({UnsupportedCDocWarning}); - updatePanes(container->state()); + updatePanes(container->state(), container); ui->leftPane->setModel(container->documentModel()); } @@ -349,7 +351,7 @@ void ContainerPage::transition(DigiDoc* container) showSigningButton(); ui->leftPane->setModel(container->documentModel()); - updatePanes(container->state()); + updatePanes(container->state(), nullptr); } void ContainerPage::updateDecryptionButton() @@ -357,7 +359,7 @@ void ContainerPage::updateDecryptionButton() showMainAction({ isSeal ? DecryptToken : DecryptContainer }); } -void ContainerPage::updatePanes(ContainerState state) +void ContainerPage::updatePanes(ria::qdigidoc4::ContainerState state, CryptoDoc *crypto_container) { ui->leftPane->stateChange(state); ui->rightPane->stateChange(state); @@ -367,7 +369,7 @@ void ContainerPage::updatePanes(ContainerState state) for(QWidget *button: buttons) button->setVisible(visible); }; - switch( state ) + switch(state) { case UnsignedContainer: cancelText = QT_TR_NOOP("Cancel"); @@ -403,7 +405,11 @@ void ContainerPage::updatePanes(ContainerState state) ui->changeLocation->show(); ui->leftPane->init(fileName, QT_TRANSLATE_NOOP("ItemList", "Encrypted files")); ui->rightPane->init(ItemAddress, QT_TRANSLATE_NOOP("ItemList", "Recipients")); - showMainAction({ EncryptContainer }); + if (crypto_container && crypto_container->supportsSymmetricKeys()) { + showMainAction({ EncryptContainer, EncryptLT }); + } else { + showMainAction({ EncryptContainer }); + } setButtonsVisible({ ui->saveAs, ui->email }, false); break; case EncryptedContainer: @@ -434,3 +440,4 @@ void ContainerPage::translateLabels() ui->cancel->setText(tr(cancelText)); ui->convert->setText(tr(convertText)); } + diff --git a/client/widgets/ContainerPage.h b/client/widgets/ContainerPage.h index 47ba67af..ca128be7 100644 --- a/client/widgets/ContainerPage.h +++ b/client/widgets/ContainerPage.h @@ -21,6 +21,7 @@ #include "common_enums.h" #include "widgets/MainAction.h" +#include "CryptoDoc.h" #include @@ -28,7 +29,6 @@ namespace Ui { class ContainerPage; } -class CKey; class CryptoDoc; class DigiDoc; class QSslCertificate; @@ -61,6 +61,8 @@ class ContainerPage final : public QWidget void removed(int row); void warning(const WarningText &warningText); + void decryptReq(const libcdoc::Lock *key); + private: void changeEvent(QEvent* event) final; bool checkAction(int code, const QString& selectedCard, const QString& selectedMobile); @@ -70,7 +72,7 @@ class ContainerPage final : public QWidget void showMainAction(const QList &actions); void showSigningButton(); void updateDecryptionButton(); - void updatePanes(ria::qdigidoc4::ContainerState state); + void updatePanes(ria::qdigidoc4::ContainerState state, CryptoDoc *crypto_container); void translateLabels(); Ui::ContainerPage *ui; diff --git a/client/widgets/MainAction.cpp b/client/widgets/MainAction.cpp index 0305411c..65223aa1 100644 --- a/client/widgets/MainAction.cpp +++ b/client/widgets/MainAction.cpp @@ -135,6 +135,7 @@ QString MainAction::label(Actions action) case SignatureSmartID: return tr("Sign with\nSmart-ID"); case SignatureToken: return tr("Sign with\nE-Seal"); case EncryptContainer: return tr("Encrypt"); + case EncryptLT: return tr("Encrypt long-term"); case DecryptContainer: return tr("Decrypt with\nID-Card"); case DecryptToken: return tr("Decrypt"); default: return tr("Sign with\nID-Card"); diff --git a/schema/header.fbs b/schema/header.fbs deleted file mode 100644 index 6a95a668..00000000 --- a/schema/header.fbs +++ /dev/null @@ -1,59 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Open Electronic Identity - * - * 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 "recipients.fbs"; - -namespace cdoc20.Header; - -// FMK encryption method enum. -enum FMKEncryptionMethod:byte { - UNKNOWN, - XOR -} - -// Payload encryption method enum. -enum PayloadEncryptionMethod:byte { - UNKNOWN, - CHACHA20POLY1305 -} - -// Intermediate record, some languages act very poorly when it comes -// to an array of unions. -// Thus it is better to have an an array of tables that -// contains the union as a field. -table RecipientRecord { - capsule: cdoc20.Recipients.Capsule; - key_label: string (required); - encrypted_fmk: [ubyte] (required); - fmk_encryption_method: FMKEncryptionMethod = UNKNOWN; -} - -// Header structure. -table Header { - recipients: [RecipientRecord]; - payload_encryption_method: PayloadEncryptionMethod = UNKNOWN; -} - -root_type Header; diff --git a/schema/recipients.fbs b/schema/recipients.fbs deleted file mode 100644 index 8325864c..00000000 --- a/schema/recipients.fbs +++ /dev/null @@ -1,82 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2023 Open Electronic Identity - * - * 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. - * - */ - -namespace cdoc20.Recipients; - -// Union for communicating the recipient type -union Capsule { - ECCPublicKeyCapsule, - RSAPublicKeyCapsule, - KeyServerCapsule, - SymmetricKeyCapsule -} - -//for future proofing and data type -union ServerDetailsUnion { - ServerEccDetails, - ServerRsaDetails -} - -// Elliptic curve type enum for ECCPublicKey recipient -enum EllipticCurve:byte { - UNKNOWN, - secp384r1 -} - -table ServerRsaDetails { - //RSA pub key in DER - recipient_public_key: [ubyte] (required); -} - -table ServerEccDetails { - // Elliptic curve type enum - curve: EllipticCurve = UNKNOWN; - //EC pub key in TLS format - //for secp384r1 curve: 0x04 + X 48 coord bytes + Y coord 48 bytes) - recipient_public_key: [ubyte] (required); -} - -// ECC public key recipient -table ECCPublicKeyCapsule { - curve: EllipticCurve = UNKNOWN; - recipient_public_key: [ubyte] (required); - sender_public_key: [ubyte] (required); -} - -table RSAPublicKeyCapsule { - recipient_public_key: [ubyte] (required); - encrypted_kek: [ubyte] (required); -} - -table KeyServerCapsule { - recipient_key_details: ServerDetailsUnion; - keyserver_id: string (required); - transaction_id: string (required); -} - -// symmetric long term crypto -table SymmetricKeyCapsule { - salt: [ubyte] (required); -} From 5ad7abf536cfa15e76e6ef9cd04bdd2f1d182aa1 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 6 Aug 2025 13:38:22 +0300 Subject: [PATCH 02/13] Attached libcdoc submodule --- client/libcdoc | 1 + 1 file changed, 1 insertion(+) create mode 160000 client/libcdoc diff --git a/client/libcdoc b/client/libcdoc new file mode 160000 index 00000000..594c6eb3 --- /dev/null +++ b/client/libcdoc @@ -0,0 +1 @@ +Subproject commit 594c6eb325fd5aa2b73da8c71f4df11c26413852 From cb5d796c5afe8a16697cdd99f74515313b398580 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 6 Aug 2025 14:03:54 +0300 Subject: [PATCH 03/13] Fixed logging for Qt < 6.5 --- client/CDocSupport.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 391edf07..67c397fb 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -42,6 +42,12 @@ #include "CDocSupport.h" +#if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) +#define SV2S(m) QUtf8StringView(m) +#else +#define SV2S(m) (m) +#endif + std::vector CDocSupport::getCDocFileList(QString filename) { @@ -269,23 +275,23 @@ Q_LOGGING_CATEGORY(LOG_CDOC, "libcdoc") void DDCDocLogger::LogMessage(libcdoc::ILogger::LogLevel level, std::string_view file, int line, std::string_view message) { switch (level) { case libcdoc::ILogger::LogLevel::LEVEL_FATAL: - qCFatal(LOG_CDOC) << message; + qFatal(LOG_CDOC, "%s", std::string(message).c_str()); break; case libcdoc::ILogger::LogLevel::LEVEL_ERROR: - qCCritical(LOG_CDOC) << message; + qCCritical(LOG_CDOC) << SV2S(message); break; case libcdoc::ILogger::LogLevel::LEVEL_WARNING: - qCWarning(LOG_CDOC) << message; + qCWarning(LOG_CDOC) << SV2S(message); break; case libcdoc::ILogger::LogLevel::LEVEL_INFO: - qCInfo(LOG_CDOC) << message; + qCInfo(LOG_CDOC) << SV2S(message); break; case libcdoc::ILogger::LogLevel::LEVEL_DEBUG: - qCDebug(LOG_CDOC) << message; + qCDebug(LOG_CDOC) << SV2S(message); break; default: // Trace, if present goes to debug categrory - qCDebug(LOG_CDOC) << message; + qCDebug(LOG_CDOC) << SV2S(message); break; } } From 670c1d5325047b6f54d946dfcecc629b2e5259d9 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 6 Aug 2025 14:14:23 +0300 Subject: [PATCH 04/13] qFatal fix for Qt < 6.5 --- client/CDocSupport.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 67c397fb..0e9ee4e0 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -44,8 +44,10 @@ #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) #define SV2S(m) QUtf8StringView(m) +#define Q_FATAL(m) QFatal("%s", std::string(m).c_str()) #else #define SV2S(m) (m) +#define Q_FATAL(m) qCFatal(LOG_CDOC) << (m) #endif std::vector @@ -275,7 +277,7 @@ Q_LOGGING_CATEGORY(LOG_CDOC, "libcdoc") void DDCDocLogger::LogMessage(libcdoc::ILogger::LogLevel level, std::string_view file, int line, std::string_view message) { switch (level) { case libcdoc::ILogger::LogLevel::LEVEL_FATAL: - qFatal(LOG_CDOC, "%s", std::string(message).c_str()); + Q_FATAL(message); break; case libcdoc::ILogger::LogLevel::LEVEL_ERROR: qCCritical(LOG_CDOC) << SV2S(message); From 2026f4e7a428dd553bd5494b4af333faf166e183 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Wed, 6 Aug 2025 14:19:28 +0300 Subject: [PATCH 05/13] QFatal -> qFatal --- client/CDocSupport.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 0e9ee4e0..5755e1b0 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -44,7 +44,7 @@ #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) #define SV2S(m) QUtf8StringView(m) -#define Q_FATAL(m) QFatal("%s", std::string(m).c_str()) +#define Q_FATAL(m) qFatal("%s", std::string(m).c_str()) #else #define SV2S(m) (m) #define Q_FATAL(m) qCFatal(LOG_CDOC) << (m) From 345175d3b03940badd66fa228e06dd934436187e Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 6 Nov 2025 15:36:12 +0200 Subject: [PATCH 06/13] Merge fixes Signed-off-by: Lauris Kaplinski --- client/dialogs/AddRecipients.cpp | 5 +++-- client/dialogs/AddRecipients.h | 4 +--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/client/dialogs/AddRecipients.cpp b/client/dialogs/AddRecipients.cpp index edd6df2b..fe554c84 100644 --- a/client/dialogs/AddRecipients.cpp +++ b/client/dialogs/AddRecipients.cpp @@ -33,6 +33,7 @@ #include "dialogs/WarningDialog.h" #include "effects/Overlay.h" #include "Crypto.h" +#include "widgets/AddressItem.h" #include #include @@ -228,7 +229,7 @@ void AddRecipients::addRecipientToRightPane(Item *item, bool update) rightList.append(key); - auto *rightItem = new AddressItem(CKey(key), AddressItem::Remove, ui->rightPane); + auto *rightItem = new AddressItem(key, AddressItem::Remove, ui->rightPane); connect(rightItem, &AddressItem::remove, this, [this](Item *item) { auto *rightItem = qobject_cast(item); if(auto *leftItem = itemListValue(ui->leftPane, rightItem->getKey())) @@ -258,7 +259,7 @@ AddressItem* AddRecipients::itemListValue(ItemList *list, const CDKey &key) return nullptr; } -QList AddRecipients::keys() const +QList AddRecipients::keys() const { QList recipients; for(auto *item: ui->rightPane->items) diff --git a/client/dialogs/AddRecipients.h b/client/dialogs/AddRecipients.h index 30a8b78e..4a74fe88 100644 --- a/client/dialogs/AddRecipients.h +++ b/client/dialogs/AddRecipients.h @@ -41,7 +41,7 @@ class AddRecipients final : public QDialog explicit AddRecipients(ItemList* itemList, QWidget *parent = nullptr); ~AddRecipients() final; - QList keys() const; + QList keys() const; bool isUpdated() const; private: @@ -55,8 +55,6 @@ class AddRecipients final : public QDialog void showError(const QString &msg, const QString &details = {}); void showResult(const QList &result, int resultCount, const QVariantMap &userData); - static AddressItem* itemListValue(ItemList *list, const CKey &cert); - Ui::AddRecipients *ui; QList rightList; QList ldap_person; From 8aad0345060c8528f1fbe4ec341451b0bb7e90f5 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Nov 2025 09:43:33 +0200 Subject: [PATCH 07/13] Fixed build.yml Signed-off-by: Lauris Kaplinski --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d2c1c397..88ffbb19 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -162,8 +162,8 @@ jobs: run: | Rename-Item "libdigidocpp*.msi" libdigidocpp.msi msiexec /qn /i libdigidocpp.msi - - name: Prepare vcpkg - uses: lukka/run-vcpkg@v11 + - name: Cache vcpkg + uses: actions/cache@v4 with: path: ${{ github.workspace }}/vcpkg_cache key: vcpkg-${{ matrix.vcver }}-${{ hashFiles('vcpkg.json') }} From 423ee029d1c3742a54146d5434d1e36570794e67 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Nov 2025 10:22:41 +0200 Subject: [PATCH 08/13] Updated libcdoc Signed-off-by: Lauris Kaplinski --- client/libcdoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/libcdoc b/client/libcdoc index 594c6eb3..3b0d1272 160000 --- a/client/libcdoc +++ b/client/libcdoc @@ -1 +1 @@ -Subproject commit 594c6eb325fd5aa2b73da8c71f4df11c26413852 +Subproject commit 3b0d12726fde786a9e1c5b9a0baeeda5edeec2ce From 5063b064e8590130bdb782f4b736de4b84b9a3a4 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Nov 2025 10:39:21 +0200 Subject: [PATCH 09/13] Updated sendKey and timestamp calculation Signed-off-by: Lauris Kaplinski --- client/CDocSupport.cpp | 9 ++++++--- client/CDocSupport.h | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 5755e1b0..6714338d 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -194,9 +194,12 @@ DDNetworkBackend::getLastErrorStr(libcdoc::result_t code) const libcdoc::result_t DDNetworkBackend::sendKey( libcdoc::NetworkBackend::CapsuleInfo &dst, const std::string &url, const std::vector &rcpt_key, - const std::vector &key_material, const std::string &type) { + const std::vector &key_material, const std::string &type, uint64_t expiry_ts) { QNetworkRequest req(QString::fromStdString(url + "/key-capsules")); req.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json")); + if (expiry_ts) { + req.setRawHeader("x-expiry-time", QDateTime::fromSecsSinceEpoch(expiry_ts).toString(Qt::ISODate).toLatin1()); + } if (!checkConnection()) { last_error = "No connection"; return BACKEND_ERROR; @@ -234,8 +237,8 @@ libcdoc::result_t DDNetworkBackend::sendKey( return BACKEND_ERROR; } dst.transaction_id = tr_id.toStdString(); - QDateTime dt = QDateTime::currentDateTimeUtc(); - dt = dt.addMonths(6); + + QDateTime dt = QDateTime::fromString(QString::fromLatin1(reply->rawHeader("x-expiry-time"))); dst.expiry_time = dt.toSecsSinceEpoch(); return libcdoc::OK; }; diff --git a/client/CDocSupport.h b/client/CDocSupport.h index 5b719f61..b382a5bc 100644 --- a/client/CDocSupport.h +++ b/client/CDocSupport.h @@ -88,7 +88,8 @@ struct DDNetworkBackend : public libcdoc::NetworkBackend, private QObject { const std::string &url, const std::vector &rcpt_key, const std::vector &key_material, - const std::string &type) override final; + const std::string &type, + uint64_t expiry_ts) override final; libcdoc::result_t fetchKey(std::vector &result, const std::string &keyserver_id, const std::string &transaction_id) override final; From d72f4fbf8d230f578bd558d63481e282458fb4ac Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Fri, 7 Nov 2025 11:10:05 +0200 Subject: [PATCH 10/13] Update MacOS deployment target Signed-off-by: Lauris Kaplinski --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88ffbb19..63d80de8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: name: Build on macOS runs-on: macos-latest env: - MACOSX_DEPLOYMENT_TARGET: 13.0 + MACOSX_DEPLOYMENT_TARGET: 13.3 LIBS_PATH: ${{ github.workspace }}/cache steps: - name: Checkout From e8d1c31652124c1c3f5d4e16028f3b265a7b78be Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Tue, 19 Aug 2025 12:50:40 +0300 Subject: [PATCH 11/13] Improve --- client/CDocSupport.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index 6714338d..efa8e8f2 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -26,7 +26,6 @@ #include #include #include -#include #include #include @@ -43,10 +42,11 @@ #include "CDocSupport.h" #if QT_VERSION < QT_VERSION_CHECK(6, 5, 0) -#define SV2S(m) QUtf8StringView(m) +QDebug operator<<(QDebug d, std::string_view str) { + return d << QUtf8StringView(str); +} #define Q_FATAL(m) qFatal("%s", std::string(m).c_str()) #else -#define SV2S(m) (m) #define Q_FATAL(m) qCFatal(LOG_CDOC) << (m) #endif @@ -283,27 +283,27 @@ void DDCDocLogger::LogMessage(libcdoc::ILogger::LogLevel level, std::string_view Q_FATAL(message); break; case libcdoc::ILogger::LogLevel::LEVEL_ERROR: - qCCritical(LOG_CDOC) << SV2S(message); + qCCritical(LOG_CDOC) << message; break; case libcdoc::ILogger::LogLevel::LEVEL_WARNING: - qCWarning(LOG_CDOC) << SV2S(message); + qCWarning(LOG_CDOC) << message; break; case libcdoc::ILogger::LogLevel::LEVEL_INFO: - qCInfo(LOG_CDOC) << SV2S(message); + qCInfo(LOG_CDOC) << message; break; case libcdoc::ILogger::LogLevel::LEVEL_DEBUG: - qCDebug(LOG_CDOC) << SV2S(message); + qCDebug(LOG_CDOC) << message; break; default: // Trace, if present goes to debug categrory - qCDebug(LOG_CDOC) << SV2S(message); + qCDebug(LOG_CDOC) << message; break; } } void DDCDocLogger::setUpLogger() { static DDCDocLogger *logger = nullptr; - if (logger) { + if (!logger) { logger = new DDCDocLogger(); logger->SetMinLogLevel(libcdoc::ILogger::LogLevel::LEVEL_TRACE); libcdoc::ILogger::addLogger(logger); From 340f66b1be0cd5a2af2ce0da0955dc51c0900a30 Mon Sep 17 00:00:00 2001 From: Raul Metsma Date: Fri, 7 Nov 2025 11:16:03 +0200 Subject: [PATCH 12/13] Use new Expire date Signed-off-by: Raul Metsma --- client/CryptoDoc.cpp | 2 +- client/widgets/AddressItem.cpp | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index 4bd6e27c..a70ed201 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -195,7 +195,7 @@ bool CryptoDoc::Private::encrypt() { enc_keys.push_back(libcdoc::Recipient::makeServer( label, key_der, pk_type, keyserver_id)); } else { - std::string label = CryptoDoc::labelFromCertificate(cert_der); + std::string label;// = CryptoDoc::labelFromCertificate(cert_der); enc_keys.push_back( libcdoc::Recipient::makeCertificate(label, cert_der)); } diff --git a/client/widgets/AddressItem.cpp b/client/widgets/AddressItem.cpp index 82c15aad..f2ff2ff7 100644 --- a/client/widgets/AddressItem.cpp +++ b/client/widgets/AddressItem.cpp @@ -36,6 +36,7 @@ class AddressItem::Private: public Ui::AddressItem QString code; CDKey key; QString label; + QDateTime expireDate; bool yourself = false; }; @@ -71,6 +72,9 @@ AddressItem::AddressItem(const CDKey &key, Type type, QWidget *parent) } else { ui->label = QString::fromStdString(ui->key.lock.label); } + if (map.contains("x-expiry-time")) { + ui->expireDate = QDateTime::fromSecsSinceEpoch(QString::fromStdString(map["x-expiry-time"]).toLongLong()); + } if (ui->key.lock.isSymmetric()) { ui->decrypt->show(); connect(ui->decrypt, &QToolButton::clicked, this, @@ -226,6 +230,16 @@ void AddressItem::setIdType() { QStringLiteral("dd.MM.yyyy"))) : tr("Decryption has expired")); } + } else if(!ui->expireDate.isNull()) { + bool canDecrypt = ui->expireDate > QDateTime::currentDateTime(); + ui->expire->setProperty("label", canDecrypt + ? QStringLiteral("good") + : QStringLiteral("warn")); + ui->expire->setText( + canDecrypt ? QStringLiteral("%1 %2").arg( + tr("Decryption is possible until:"), + ui->expireDate.toLocalTime().toString(QStringLiteral("dd.MM.yyyy"))) + : tr("Decryption has expired")); } } else { // No rcpt, lock is invalid = unsupported lock From 36f1b9f9666f7b81f0fc805cbb62f1c7a4b25739 Mon Sep 17 00:00:00 2001 From: Lauris Kaplinski Date: Thu, 18 Dec 2025 23:35:41 +0200 Subject: [PATCH 13/13] Clean up error handling and background processing --- client/CDocSupport.cpp | 15 ++++-- client/CDocSupport.h | 2 + client/CryptoDoc.cpp | 103 +++++++++++++++++++++-------------------- client/QPKCS11.cpp | 36 +++++++------- client/QSigner.cpp | 7 +++ client/QSigner.h | 1 + 6 files changed, 95 insertions(+), 69 deletions(-) diff --git a/client/CDocSupport.cpp b/client/CDocSupport.cpp index efa8e8f2..a0d73611 100644 --- a/client/CDocSupport.cpp +++ b/client/CDocSupport.cpp @@ -97,7 +97,7 @@ DDCryptoBackend::decryptRSA(std::vector& result, const std::vectordecrypt(qdata, oaep); }); result.assign(qkek.cbegin(), qkek.cend()); - return (result.empty()) ? OPENSSL_ERROR : libcdoc::OK; + return (result.empty()) ? BACKEND_ERROR : libcdoc::OK; } const QString SHA256_MTH = QStringLiteral("http://www.w3.org/2001/04/xmlenc#sha256"); @@ -119,7 +119,7 @@ DDCryptoBackend::deriveConcatKDF(std::vector& dst, const std::vector(partyVInfo.data()), partyVInfo.size())); }); dst.assign(decryptedKey.cbegin(), decryptedKey.cend()); - return (dst.empty()) ? OPENSSL_ERROR : libcdoc::OK; + return (dst.empty()) ? BACKEND_ERROR : libcdoc::OK; } libcdoc::result_t @@ -131,7 +131,7 @@ DDCryptoBackend::deriveHMACExtract(std::vector& dst, const std::vector< return backend->deriveHMACExtract(qkey_material, qsalt, ECC_KEY_LEN); }); dst = std::vector(qkekpm.cbegin(), qkekpm.cend()); - return (dst.empty()) ? OPENSSL_ERROR : libcdoc::OK; + return (dst.empty()) ? BACKEND_ERROR : libcdoc::OK; } libcdoc::result_t @@ -141,6 +141,15 @@ DDCryptoBackend::getSecret(std::vector& _secret, unsigned int idx) return libcdoc::OK; } +std::string +DDCryptoBackend::getLastErrorStr(libcdoc::result_t code) const +{ + if (code == BACKEND_ERROR) { + return qApp->signer()->getLastErrorStr().toStdString(); + } + return libcdoc::CryptoBackend::getLastErrorStr(code); +} + bool checkConnection() { diff --git a/client/CDocSupport.h b/client/CDocSupport.h index b382a5bc..4019c8d5 100644 --- a/client/CDocSupport.h +++ b/client/CDocSupport.h @@ -52,6 +52,7 @@ struct DDConfiguration : public libcdoc::Configuration { // struct DDCryptoBackend : public libcdoc::CryptoBackend { + static constexpr int BACKEND_ERROR = -303; libcdoc::result_t decryptRSA(std::vector &result, const std::vector &data, bool oaep, unsigned int idx) override final; @@ -68,6 +69,7 @@ struct DDCryptoBackend : public libcdoc::CryptoBackend { unsigned int idx) override final; libcdoc::result_t getSecret(std::vector &secret, unsigned int idx) override final; + std::string getLastErrorStr(libcdoc::result_t code) const final; std::vector secret; diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index a70ed201..dec83557 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -89,6 +89,40 @@ class CryptoDoc::Private final: public QThread start(); e.exec(); } + inline libcdoc::result_t decrypt(unsigned int lock_idx) { + TempListConsumer cons; + libcdoc::result_t result = waitFor([&]{ + std::vector fmk; + libcdoc::result_t result = reader->getFMK(fmk, lock_idx); + qDebug() << "getFMK result: " << result << " " << reader->getLastErrorStr(); + if (result != libcdoc::OK) return result; + result = reader->decrypt(fmk, &cons); + std::fill(fmk.begin(), fmk.end(), 0); + qDebug() << "Decryption result: " << result << " " << reader->getLastErrorStr(); + return result; + }); + if (result == libcdoc::OK) { + files = std::move(cons.files); + // Success, immediately create writer from reader + keys.clear(); + writer_last_error.clear(); + reader.reset(); + } + return result; + } + + inline libcdoc::result_t encrypt(unsigned int lock_idx) { + libcdoc::result_t result = waitFor([&]{ + libcdoc::result_t result = encrypt(); + qDebug() << "Encryption result: " << result << " " << reader->getLastErrorStr(); + if (result == libcdoc::OK) { + // Encryption successful, open new reader + reader = createCDocReader(fileName.toStdString()); + } + return result; + }); + return result; + } std::unique_ptr reader; std::string writer_last_error; @@ -97,8 +131,6 @@ class CryptoDoc::Private final: public QThread bool isEncrypted() const { return reader != nullptr; } CDocumentModel *documents = new CDocumentModel(this); QStringList tempFiles; - // Decryption data - QByteArray fmk; // Encryption data QString label; uint32_t kdf_iter; @@ -126,8 +158,7 @@ class CryptoDoc::Private final: public QThread } return std::unique_ptr(r); } -private: - bool encrypt(); + libcdoc::result_t encrypt(); }; bool CryptoDoc::Private::warnIfNotWritable() const @@ -144,33 +175,20 @@ bool CryptoDoc::Private::warnIfNotWritable() const void CryptoDoc::Private::run() { - if(reader) { - qCDebug(CRYPTO) << "Decrypt" << fileName; - std::vector pfmk(fmk.cbegin(), fmk.cend()); - - TempListConsumer cons; - if (reader->decrypt(pfmk, &cons) == libcdoc::OK) { - files = std::move(cons.files); - // Success, immediately create writer from reader - keys.clear(); - writer_last_error.clear(); - reader.reset(); - } - } else { - if (encrypt()) { - // Encryption successful, open new reader - reader = createCDocReader(fileName.toStdString()); - if (!reader) return; - } + if (encrypt() == libcdoc::OK) { + // Encryption successful, open new reader + reader = createCDocReader(fileName.toStdString()); + if (!reader) return; } } -bool CryptoDoc::Private::encrypt() { +libcdoc::result_t +CryptoDoc::Private::encrypt() { qCDebug(CRYPTO) << "Encrypt" << fileName; libcdoc::OStreamConsumer ofs(fileName.toStdString()); if (ofs.isError()) - return false; + return libcdoc::OUTPUT_ERROR; StreamListSource slsrc(files); std::vector enc_keys; @@ -195,7 +213,7 @@ bool CryptoDoc::Private::encrypt() { enc_keys.push_back(libcdoc::Recipient::makeServer( label, key_der, pk_type, keyserver_id)); } else { - std::string label;// = CryptoDoc::labelFromCertificate(cert_der); + std::string label; enc_keys.push_back( libcdoc::Recipient::makeCertificate(label, cert_der)); } @@ -215,7 +233,7 @@ bool CryptoDoc::Private::encrypt() { } delete writer; ofs.close(); - return (result == libcdoc::OK); + return result; } CDocumentModel::CDocumentModel(CryptoDoc::Private *doc) : d(doc) {} @@ -459,26 +477,12 @@ bool CryptoDoc::decrypt(const libcdoc::Lock *lock, const QByteArray &secret) { } d->crypto.secret.assign(secret.cbegin(), secret.cend()); - std::vector fmk; - if (d->reader->getFMK(fmk, lock_idx) != libcdoc::OK) - return false; - d->fmk = QByteArray(reinterpret_cast(fmk.data()), fmk.size()); - if (d->fmk.isEmpty()) { - const std::string &msg = d->reader->getLastErrorStr(); - WarningDialog::show(tr("Failed to decrypt document. Please check your " - "internet connection and network settings."), - QString::fromStdString(msg)); - return false; - } - d->waitForFinished(); - if (d->reader) { + libcdoc::result_t result = d->decrypt(lock_idx); + if (result != libcdoc::OK) { const std::string &msg = d->reader->getLastErrorStr(); - if (msg.empty()) { - WarningDialog::show(tr("Error parsing document")); - } else { - WarningDialog::show(QString::fromStdString(msg)); - } + WarningDialog::show(tr("Failed to decrypt document"), + QString::fromStdString(d->reader->getLastErrorStr())); } return !d->isEncrypted(); } @@ -507,14 +511,15 @@ bool CryptoDoc::encrypt( const QString &filename, const QString& label, const QB d->crypto.secret.assign(secret.cbegin(), secret.cend()); d->kdf_iter = kdf_iter; } - d->waitForFinished(); + libcdoc::result_t result = d->encrypt(); d->label.clear(); d->crypto.secret.clear(); - if(d->isEncrypted()) { - open(d->fileName); - } else { - WarningDialog::show(tr("Failed to encrypt document. Please check your internet connection and network settings."), QString::fromStdString(d->writer_last_error)); + if (result != libcdoc::OK) { + const std::string &msg = d->reader->getLastErrorStr(); + WarningDialog::show(tr("Failed to encrypt document"), + QString::fromStdString(d->writer_last_error)); } + return d->isEncrypted(); } diff --git a/client/QPKCS11.cpp b/client/QPKCS11.cpp index 7592db62..2cab3d75 100644 --- a/client/QPKCS11.cpp +++ b/client/QPKCS11.cpp @@ -24,6 +24,7 @@ #include "QPCSC.h" #include "SslCertificate.h" #include "TokenData.h" +#include "Utils.h" #include "dialogs/PinPopup.h" #include @@ -222,23 +223,24 @@ QPKCS11::PinStatus QPKCS11::login(const TokenData &t) if(token.flags & CKF_USER_PIN_LOCKED) return PinLocked; if(token.flags & CKF_USER_PIN_COUNT_LOW) f = PinPopup::PinCountLow; if(token.flags & CKF_USER_PIN_FINAL_TRY) f = PinPopup::PinFinalTry; - if(token.flags & CKF_PROTECTED_AUTHENTICATION_PATH) - { - PinPopup p(isSign ? PinPopup::Pin2PinpadType : PinPopup::Pin1PinpadType, cert, f, Application::mainWindow()); - connect(d, &Private::started, &p, &PinPopup::startTimer); - connect(d, &Private::finished, &p, &PinPopup::accept); - d->start(); - p.exec(); - err = d->result; - } - else - { - PinPopup p(isSign ? PinPopup::Pin2Type : PinPopup::Pin1Type, cert, f, Application::mainWindow()); - p.setPinLen(token.ulMinPinLen, token.ulMaxPinLen < 12 ? 12 : token.ulMaxPinLen); - if( !p.exec() ) - return PinCanceled; - QByteArray pin = p.pin().toUtf8(); - err = d->f->C_Login(d->session, CKU_USER, CK_UTF8CHAR_PTR(pin.constData()), CK_ULONG(pin.size())); + if(token.flags & CKF_PROTECTED_AUTHENTICATION_PATH) { + err = dispatchToMain([&]{ + PinPopup p(isSign ? PinPopup::Pin2PinpadType : PinPopup::Pin1PinpadType, cert, f, Application::mainWindow()); + connect(d, &Private::started, &p, &PinPopup::startTimer); + connect(d, &Private::finished, &p, &PinPopup::accept); + d->start(); + p.exec(); + return d->result; + }); + } else { + err = dispatchToMain([&]{ + PinPopup p(isSign ? PinPopup::Pin2Type : PinPopup::Pin1Type, cert, f, Application::mainWindow()); + p.setPinLen(token.ulMinPinLen, token.ulMaxPinLen < 12 ? 12 : token.ulMaxPinLen); + if(!p.exec()) + return CKR_FUNCTION_CANCELED; + QByteArray pin = p.pin().toUtf8(); + return d->f->C_Login(d->session, CKU_USER, CK_UTF8CHAR_PTR(pin.constData()), CK_ULONG(pin.size())); + }); } d->f->C_GetTokenInfo(currentSlot, &token); diff --git a/client/QSigner.cpp b/client/QSigner.cpp index 83e721c8..b8d12ccf 100644 --- a/client/QSigner.cpp +++ b/client/QSigner.cpp @@ -438,3 +438,10 @@ std::vector QSigner::sign(const std::string &method, const std::v QSmartCard * QSigner::smartcard() const { return d->smartcard; } TokenData QSigner::tokenauth() const { return d->auth; } TokenData QSigner::tokensign() const { return d->sign; } + +QString +QSigner::getLastErrorStr() const +{ + QCryptoBackend::PinStatus status = d->backend->lastError(); + return d->backend->errorString(status); +} diff --git a/client/QSigner.h b/client/QSigner.h index e15d2e01..12d1aa88 100644 --- a/client/QSigner.h +++ b/client/QSigner.h @@ -50,6 +50,7 @@ class QSigner final: public QThread, public digidoc::Signer QSmartCard * smartcard() const; TokenData tokenauth() const; TokenData tokensign() const; + QString getLastErrorStr() const; Q_SIGNALS: void cacheChanged();