diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d690d1c84..65a05f2b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | brew install flatbuffers - curl -O -L -s https://installer.id.ee/media/github/opensc_0.26.1.pkg + curl -O -L -s https://installer.id.ee/media/github/opensc_0.27.1-3.pkg sudo installer -verboseR -pkg libdigidocpp-pkg/build/macos/libdigidocpp*.pkg -target / sudo installer -verboseR -pkg opensc_*.pkg -target / rm -rf libdigidocpp-pkg @@ -41,12 +41,12 @@ jobs: key: vcpkg-macOS-${{ hashFiles('prepare_osx_build_environment.sh') }} - name: Build libraries run: | - mkdir -p ${LIBS_PATH}/Qt-6.10.3-OpenSSL + mkdir -p ${LIBS_PATH}/Qt-6.11.2-OpenSSL ./prepare_osx_build_environment.sh -p ${LIBS_PATH} - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: 6.10.3 + version: 6.11.2 arch: clang_64 cache: true - name: Build @@ -169,22 +169,27 @@ jobs: Rename-Item "libdigidocpp*.msi" libdigidocpp.msi $r = Start-Process msiexec -ArgumentList '/a', 'libdigidocpp.msi', '/qn', "TARGETDIR=$env:GITHUB_WORKSPACE\libs", '/l*v', 'install.log' -Wait -PassThru if ($r.ExitCode -ne 0) { Get-Content install.log; exit $r.ExitCode } - - name: Cache vcpkg + - name: Cache vcpkg and WiX uses: actions/cache@v5 with: - path: ${{ github.workspace }}/vcpkg_cache - key: vcpkg-${{ matrix.vcver }}-${{ matrix.platform }}-${{ hashFiles('client/libcdoc/vcpkg.json') }} + path: | + ${{ github.workspace }}/vcpkg_cache + ~/.dotnet/tools + ~/.wix + key: vcpkg-${{ matrix.vcver }}-${{ matrix.platform }}-${{ hashFiles('client/libcdoc/vcpkg.json') }}-wix-7.0.0 - name: Install Qt uses: jurplel/install-qt-action@v4 with: - version: 6.11.1 + version: 6.11.2 arch: ${{ matrix.platform == 'arm64' && 'win64_msvc2022_arm64_cross_compiled' || 'win64_msvc2022_64' }} cache: true aqtsource: git+https://github.com/miurahr/aqtinstall.git - name: Install WiX run: | - dotnet tool install -g wix --version 6.0.2 - wix extension -g add WixToolset.UI.wixext/6.0.2 + dotnet tool install -g wix --version 7.0.0 + wix eula accept wix7 + wix extension -g add WixToolset.UI.wixext/7.0.0 + wix extension -g add WixToolset.Util.wixext/7.0.0 - name: Build shell: cmd env: diff --git a/CMakeLists.txt b/CMakeLists.txt index d5da89210..8e3bfdf4c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.22) if(NOT EXISTS ${CMAKE_SOURCE_DIR}/common/CMakeLists.txt) message(FATAL_ERROR "common submodule directory empty, did you 'git clone --recursive'?") endif() -project(qdigidoc4 VERSION 4.11.1 +project(qdigidoc4 VERSION 4.12.0 DESCRIPTION "DigiDoc4 application for digital signing and encryption" HOMEPAGE_URL https://github.com/open-eid/DigiDoc4-Client ) @@ -46,7 +46,7 @@ set(CPACK_RPM_PACKAGE_RELEASE_DIST ON) include(CPack) include(GNUInstallDirs) -find_package(libdigidocpp 4.5.0 REQUIRED HINTS /Library) +find_package(libdigidocpp 4.6.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) diff --git a/README.md b/README.md index f7fd729de..ca030141d 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ 3. Configure cmake -B build -S . \ - -DCMAKE_PREFIX_PATH=~/cmake_builds/Qt-6.11.1-OpenSSL \ + -DCMAKE_PREFIX_PATH=~/cmake_builds/Qt-6.11.2-OpenSSL \ -DOPENSSL_ROOT_DIR=~/cmake_build/OpenSSL \ -DLDAP_ROOT=~/cmake_build/OpenLDAP \ -DCMAKE_OSX_ARCHITECTURES="x86_64;arm64" @@ -95,7 +95,7 @@ 3. Configure cmake -G"NMAKE Makefiles" -B build -S . ` - -DCMAKE_PREFIX_PATH=C:\Qt\6.11.1\msvc2022_64 ` + -DCMAKE_PREFIX_PATH=C:\Qt\6.11.2\msvc2022_64 ` -DLibDigiDocpp_ROOT="C:\Program Files (x86)\libdigidocpp" ` -DVCPKG_MANIFEST_DIR=client/libcdoc diff --git a/client/CryptoDoc.cpp b/client/CryptoDoc.cpp index a83b10d91..2ec479718 100644 --- a/client/CryptoDoc.cpp +++ b/client/CryptoDoc.cpp @@ -171,11 +171,6 @@ quint64 CDocumentModel::fileSize(int row) const return d->files.at(row).size; } -QString CDocumentModel::mime(int row) const -{ - return FileDialog::normalized(QString::fromStdString(d->files.at(row).mime)); -} - void CDocumentModel::open(int row) { if(!d->isEncrypted()) @@ -414,6 +409,7 @@ bool CryptoDoc::encrypt(const QString &filename, const QString& label, const QBy return false; } QString writer_last_error; + auto keepAlive = FileDialog::keepAccessAlive(d->fileName); libcdoc::result_t result = waitFor([&] -> libcdoc::result_t { qCDebug(CRYPTO) << "Encrypt" << d->fileName; auto writer = std::unique_ptr(libcdoc::CDocWriter::createWriter(d->version, d->fileName.toStdString(), &d->conf, &d->crypto, &d->network)); @@ -524,6 +520,7 @@ bool CryptoDoc::saveCopy(const QString &filename) QFileInfo dst(filename); if(src == dst) return true; + auto keepAlive = FileDialog::keepAccessAlive(filename); std::error_code ec; std::filesystem::copy_file(src.filesystemFilePath(), dst.filesystemFilePath(), std::filesystem::copy_options::overwrite_existing, ec); diff --git a/client/CryptoDoc.h b/client/CryptoDoc.h index 3a0c2da71..be9ca121a 100644 --- a/client/CryptoDoc.h +++ b/client/CryptoDoc.h @@ -89,7 +89,6 @@ class CDocumentModel final: public DocumentModel bool addFile(const QString &file, const QString &mime = QStringLiteral("application/octet-stream")) final; QString data(int row) const final; quint64 fileSize(int row) const final; - QString mime(int row) const final; void open(int row) final; bool removeRow(int row) final; int rowCount() const final; diff --git a/client/DigiDoc.cpp b/client/DigiDoc.cpp index 8409b55b4..79eb2e727 100644 --- a/client/DigiDoc.cpp +++ b/client/DigiDoc.cpp @@ -333,14 +333,6 @@ quint64 SDocumentModel::fileSize(int row) const return doc->b->dataFiles().at(size_t(row))->fileSize(); } -QString SDocumentModel::mime(int row) const -{ - if(row >= rowCount()) - return {}; - - return from(doc->b->dataFiles().at(size_t(row))->mediaType()); -} - bool SDocumentModel::removeRow(int row) { if(!doc->b) @@ -368,6 +360,7 @@ QString SDocumentModel::save(int row, const QString &path) const return {}; if(QFileInfo::exists(path)) return path; + auto keepAlive = FileDialog::keepAccessAlive(path); doc->b->dataFiles().at(size_t(row))->saveAs(path.toStdString()); if(!QFileInfo::exists(path)) return {}; @@ -431,15 +424,16 @@ bool DigiDoc::extend() Common::applicationOs(), Common::drivers().join(',')).toUtf8().constData()); qApp->waitForTSL(fileName()); - ServiceConfirmation cb(parent); QString current = m_fileName; size_t extendCount = 0; - bool wrapped = false; + const auto signatures = b->signatures(); + const bool timestampedContainer = std::any_of(signatures.cbegin(), signatures.cend(), [](const Signature *signature) { + return signature->profile() == "TimeStampToken"; + }); if(std::unique_ptr extended = waitFor([&] { return Container::extendContainerValidity(*b, signer, extendCount); })) { - wrapped = true; const QString asics = QCoreApplication::translate("MainWindow", "Documents (%1)").arg(QLatin1String("*.asics *.scs")); QFileInfo f(current); QString name = f.absolutePath() + '/' + f.completeBaseName() + QStringLiteral(".asics"); @@ -447,14 +441,12 @@ bool DigiDoc::extend() if(filename.isEmpty()) return false; auto original = std::exchange(b, std::move(extended)); - auto originalParent = std::exchange(parentContainer, {}); if(!saveAs(filename)) { b = std::move(original); - parentContainer = std::move(originalParent); return false; } - load(std::move(b), cb); + load(std::move(b)); m_fileName = filename; Application::addRecent(m_fileName); modified = false; @@ -466,8 +458,8 @@ bool DigiDoc::extend() } if(!save()) return false; - load(std::move(b), cb); - if(extendCount < b->signatures().size()) + load(std::move(b)); + if(!timestampedContainer && extendCount < b->signatures().size()) { WarningDialog::create(parent) ->withTitle(tr("Container validity extended")) @@ -546,35 +538,19 @@ bool DigiDoc::isSupported() const return b && b->mediaType() == "application/vnd.etsi.asic-e+zip" && !isCades(); } -void DigiDoc::load(std::unique_ptr &&doc, ServiceConfirmation &cb) +void DigiDoc::load(std::unique_ptr &&doc) { - parentContainer.reset(); m_signatures.clear(); m_timestamps.clear(); documentModel()->clearTempFolder(); b = std::move(doc); - if(b && b->mediaType() == "application/vnd.etsi.asic-s+zip" && - b->dataFiles().size() == 1 && - b->signatures().size() == 1) - { - if(from(b->dataFiles().at(0)->fileName()).endsWith(QStringLiteral(".ddoc"), Qt::CaseInsensitive)) - { - if(QString tmppath = documentModel()->saveTemp(0); !tmppath.isEmpty()) - { - try { - parentContainer = std::exchange(b, Container::openPtr(to(tmppath), &cb)); - } catch(const Exception &) {} - } - } - } - bool isTimeStamped = parentContainer && parentContainer->signatures().at(0)->trustedSigningTime().compare("2018-07-01T00:00:00Z") < 0; - for(const Signature *signature: b->signatures()) - m_signatures.emplace_back(signature, this, isTimeStamped); - if(parentContainer) - { - for(const Signature *signature: parentContainer->signatures()) - m_timestamps.emplace_back(signature, this); - } + const auto containerSignatures = b->signatures(); + const bool isTimeStamped = std::any_of(containerSignatures.cbegin(), containerSignatures.cend(), [](const Signature *signature) { + return signature->profile() == "TimeStampToken" && + signature->trustedSigningTime().compare("2018-07-01T00:00:00Z") < 0; + }); + for(const Signature *signature: containerSignatures) + m_signatures.emplace_back(signature, this, signature->profile() != "TimeStampToken" && isTimeStamped); setState(signatures().isEmpty() ? ContainerState::UnsignedSavedContainer : ContainerState::SignedContainer); } @@ -615,7 +591,7 @@ std::unique_ptr DigiDoc::open(const QString &file, QWidget *parent) std::unique_ptr doc(new DigiDoc(parent)); WaitDialogHolder waitDialog(parent, tr("Opening"), false); if(waitFor([&] { - doc->load(Container::openPtr(to(file), &cb), cb); + doc->load(Container::openPtr(to(file), &cb)); Application::addRecent(file); doc->m_fileName = file; return true; @@ -710,8 +686,9 @@ bool DigiDoc::saveAs(const QString &filename) { try { + auto keepAlive = FileDialog::keepAccessAlive(filename); return waitFor([&] { - parentContainer ? parentContainer->save(to(filename)) : b->save(to(filename)); + b->save(to(filename)); return true; }); } diff --git a/client/DigiDoc.h b/client/DigiDoc.h index 739deede2..fc6affc7f 100644 --- a/client/DigiDoc.h +++ b/client/DigiDoc.h @@ -95,7 +95,6 @@ class SDocumentModel final: public DocumentModel bool addFile(const QString &file, const QString &mime = QStringLiteral("application/octet-stream")) final; QString data(int row) const final; quint64 fileSize(int row) const final; - QString mime(int row) const final; bool removeRow(int row) final; int rowCount() const final; QString save(int row, const QString &path) const final; @@ -158,12 +157,11 @@ class DigiDoc: public QObject QWidget *parentWidget() const; explicit DigiDoc(QWidget *parent = {}); bool isError(bool failure, const QString &title, const QString &text) const; - void load(std::unique_ptr &&doc, ServiceConfirmation &cb); + void load(std::unique_ptr &&doc); static void setLastError(const QString &title, const digidoc::Exception &e); void setState(ContainerState s); std::unique_ptr b; - std::unique_ptr parentContainer; std::unique_ptr m_documentModel; ContainerState containerState = ria::qdigidoc4::UnsignedContainer; diff --git a/client/DocumentModel.h b/client/DocumentModel.h index 3dc2ae2ce..f17fef7c4 100644 --- a/client/DocumentModel.h +++ b/client/DocumentModel.h @@ -35,7 +35,6 @@ class DocumentModel: public QObject virtual void copyModel(DocumentModel *model); virtual QString data(int row) const = 0; virtual quint64 fileSize(int row) const = 0; - virtual QString mime(int row) const = 0; virtual void open(int row); virtual bool removeRow(int row) = 0; virtual int rowCount() const = 0; diff --git a/client/dialogs/FileDialog.cpp b/client/dialogs/FileDialog.cpp index f47151c36..5f4d11a3c 100644 --- a/client/dialogs/FileDialog.cpp +++ b/client/dialogs/FileDialog.cpp @@ -104,6 +104,25 @@ bool FileDialog::fileIsWritable( const QString &filename ) return result; } +std::unique_ptr FileDialog::keepAccessAlive(const QString &path) +{ +#ifdef Q_OS_MACOS + // Qt 6.11's macOS sandbox file engine only keeps a user-selected path's + // security-scoped access active while some QFile instance for that exact + // path exists (not merely open). Code that writes to disk without going + // through QFile (libdigidocpp, libcdoc, std::filesystem) bypasses that + // entirely, so callers doing raw I/O to a save-panel-selected path must + // keep the object this returns alive for the duration of that write. + auto file = std::make_unique(path); + bool opened = file->open(QFile::WriteOnly|QFile::Append); + Q_UNUSED(opened) + return file; +#else + Q_UNUSED(path) + return {}; +#endif +} + bool FileDialog::isSignedPDF(const QString &path) { if(!path.endsWith(QLatin1String("pdf"), Qt::CaseInsensitive)) diff --git a/client/dialogs/FileDialog.h b/client/dialogs/FileDialog.h index cb87355ff..28e0dcf25 100644 --- a/client/dialogs/FileDialog.h +++ b/client/dialogs/FileDialog.h @@ -19,8 +19,11 @@ #pragma once +#include #include +#include + class FileDialog : public QFileDialog { Q_OBJECT @@ -36,6 +39,7 @@ class FileDialog : public QFileDialog static QString createNewFileName(const QString &file, bool signature, QWidget *parent); static FileType detect(const QString &filename); static bool fileIsWritable( const QString &filename ); + static std::unique_ptr keepAccessAlive(const QString &path); static bool isSignedPDF(const QString &path); static void setFileZone(const QString &target, const QString &source); static void setReadOnly(const QString &path, bool readonly = true); diff --git a/client/libcdoc b/client/libcdoc index 83408f9fc..4f28aa8d9 160000 --- a/client/libcdoc +++ b/client/libcdoc @@ -1 +1 @@ -Subproject commit 83408f9fcf4af25e81d891c34f9cc437d23f021a +Subproject commit 4f28aa8d9f20a3dcbf7c86093496840fd34f5a33 diff --git a/prepare_osx_build_environment.sh b/prepare_osx_build_environment.sh index 26c014e3e..b3d2f15cd 100755 --- a/prepare_osx_build_environment.sh +++ b/prepare_osx_build_environment.sh @@ -4,9 +4,9 @@ set -e ######### Versions of libraries/frameworks to be compiled -QT_VER="6.10.3" +QT_VER="6.11.2" OPENSSL_VER="3.5.7" -OPENLDAP_VER="2.6.13" +OPENLDAP_VER="2.6.14" REBUILD=false BUILD_PATH=~/cmake_builds : ${MACOSX_DEPLOYMENT_TARGET:="14.0"} diff --git a/qdigidoc4.wxs b/qdigidoc4.wxs index abedfe22f..4e7249379 100644 --- a/qdigidoc4.wxs +++ b/qdigidoc4.wxs @@ -17,8 +17,7 @@ + xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">