diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index 84d0b11a2..af242455b 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,51 +1,60 @@ # SPDX-License-Identifier: CC0-1.0 # SPDX-FileCopyrightText: none remove_definitions(-DQT_NO_CAST_FROM_ASCII) include(ECMAddTests) find_package(Qt${QT_MAJOR_VERSION}Test ${REQUIRED_QT_VERSION} CONFIG QUIET) if(NOT TARGET Qt::Test) message(STATUS "Qt${QT_MAJOR_VERSION}Test not found, autotests will not be built.") return() endif() ecm_add_test( flatkeylistmodeltest.cpp abstractkeylistmodeltest.cpp TEST_NAME flatkeylistmodeltest LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo Qt::Test ) ecm_add_test( hierarchicalkeylistmodeltest.cpp abstractkeylistmodeltest.cpp abstractkeylistmodeltest.h TEST_NAME hierarchicalkeylistmodeltest LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo Qt::Test ) ecm_add_test( keyresolvercoretest.cpp keyresolvercoretest.qrc TEST_NAME keyresolvercoretest LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo Qt::Test ) ecm_add_tests( editdirectoryservicedialogtest.cpp LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo KF${KF_MAJOR_VERSION}::WidgetsAddons Qt::Widgets Qt::Test ) ecm_add_tests( keyselectioncombotest.cpp keyserverconfigtest.cpp newkeyapprovaldialogtest.cpp LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo Qt::Widgets Qt::Test ) +ecm_add_test( + expirycheckertest.cpp + expirycheckertest.qrc + TEST_NAME expirycheckertest + LINK_LIBRARIES + KPim${KF_MAJOR_VERSION}::Libkleo + Qt::Test +) + ecm_add_tests( hextest.cpp LINK_LIBRARIES KPim${KF_MAJOR_VERSION}::Libkleo Qt::Test ) diff --git a/autotests/expirycheckertest.cpp b/autotests/expirycheckertest.cpp new file mode 100644 index 000000000..bdf9526ca --- /dev/null +++ b/autotests/expirycheckertest.cpp @@ -0,0 +1,282 @@ +/* + This file is part of libkleopatra's test suite. + SPDX-FileCopyrightText: 2022 Sandro Knauß + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "../src/kleo/expirychecker_p.h" + +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include + +using namespace Kleo; + +static std::vector> getKeys(bool smime = false) +{ + QGpgME::KeyListJob *job = nullptr; + + if (smime) { + const QGpgME::Protocol *const backend = QGpgME::smime(); + Q_ASSERT(backend); + job = backend->keyListJob(false); + } else { + const QGpgME::Protocol *const backend = QGpgME::openpgp(); + Q_ASSERT(backend); + job = backend->keyListJob(false); + } + Q_ASSERT(job); + + std::vector keys; + GpgME::KeyListResult res = job->exec(QStringList(), true, keys); + + Q_ASSERT(!res.error()); + + /* + qDebug() << "got private keys:" << keys.size(); + + for (std::vector< GpgME::Key >::iterator i = keys.begin(); i != keys.end(); ++i) { + qDebug() << "key isnull:" << i->isNull() << "isexpired:" << i->isExpired(); + qDebug() << "key numuserIds:" << i->numUserIDs(); + for (uint k = 0; k < i->numUserIDs(); ++k) { + qDebug() << "userIDs:" << i->userID(k).email(); + } + } + */ + + return keys; +} + +class ExpiryCheckerTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase() + { + qRegisterMetaType(); + + mGnupgHome = QTest::qExtractTestData(QStringLiteral("/fixtures/expirycheckertest")); + qputenv("GNUPGHOME", mGnupgHome->path().toLocal8Bit()); + } + + void cleanupTestCase() + { + (void)QProcess::execute(QStringLiteral("gpgconf"), {"--kill", "all"}); + + mGnupgHome.reset(); + qunsetenv("GNUPGHOME"); + } + + void valid_data() + { + QTest::addColumn("key"); + QTest::addColumn("difftime"); + QTest::newRow("neverExpire") << getKeys()[0] << -1; + + const auto backend = QGpgME::openpgp(); + Q_ASSERT(backend); + const auto job = backend->keyListJob(false); + Q_ASSERT(job); + + std::vector keys; + job->exec(QStringList() << QStringLiteral("EB85BB5FA33A75E15E944E63F231550C4F47E38E"), false, keys); + QTest::newRow("openpgp") << keys[0] << 2 * 24 * 60 * 60; + QTest::newRow("smime") << getKeys(true)[0] << 2 * 24 * 60 * 60; + } + + void valid() + { + QFETCH(GpgME::Key, key); + QFETCH(int, difftime); + + ExpiryChecker checker(1, 1, 1, 1); + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + checker.d->testMode = true; + checker.d->difftime = difftime; + + checker.checkKey(key); + QCOMPARE(spy.count(), 0); + } + + void expired_data() + { + QTest::addColumn("key"); + QTest::addColumn("msg"); + QTest::addColumn("msgOwnKey"); + QTest::addColumn("msgOwnSigningKey"); + + const auto backend = QGpgME::openpgp(); + Q_ASSERT(backend); + const auto job = backend->keyListJob(false); + Q_ASSERT(job); + + std::vector keys; + job->exec(QStringList() << QStringLiteral("EB85BB5FA33A75E15E944E63F231550C4F47E38E"), false, keys); + QTest::newRow("openpgp") + << keys[0] + << QStringLiteral( + "

The OpenPGP key for

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired less than a day ago.

") + << QStringLiteral( + "

Your OpenPGP encryption key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired less than a day " + "ago.

") + << QStringLiteral( + "

Your OpenPGP signing key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expired less than a day " + "ago.

"); + QTest::newRow("smime") << getKeys(true)[0] + << QStringLiteral( + "

The S/MIME certificate for

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US (serial " + "number 00D345203A186385C9)

expired less than a day ago.

") + << QStringLiteral( + "

Your S/MIME encryption certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " + "(serial number 00D345203A186385C9)

expired less than a day ago.

") + << QStringLiteral( + "

Your S/MIME signing certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " + "(serial number 00D345203A186385C9)

expired less than a day ago.

"); + } + + void expired() + { + QFETCH(GpgME::Key, key); + QFETCH(QString, msg); + QFETCH(QString, msgOwnKey); + QFETCH(QString, msgOwnSigningKey); + + ExpiryChecker checker(1, 1, 1, 1); + checker.d->testMode = true; + checker.d->difftime = -1; + { + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + checker.checkKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msg); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OtherKeyExpired); + } + checker.d->alreadyWarnedFingerprints.clear(); + { + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + checker.checkOwnKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msgOwnKey); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyExpired); + } + checker.d->alreadyWarnedFingerprints.clear(); + { + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + checker.checkOwnSigningKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msgOwnSigningKey); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyExpired); + } + } + + void nearexpiry_data() + { + QTest::addColumn("key"); + QTest::addColumn("msg"); + QTest::addColumn("msgOwnKey"); + QTest::addColumn("msgOwnSigningKey"); + + const auto backend = QGpgME::openpgp(); + Q_ASSERT(backend); + const auto job = backend->keyListJob(false); + Q_ASSERT(job); + + std::vector keys; + job->exec(QStringList() << QStringLiteral("EB85BB5FA33A75E15E944E63F231550C4F47E38E"), false, keys); + QTest::newRow("openpgp") + << keys[0] + << QStringLiteral( + "

The OpenPGP key for

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in less than 6 days.

") + << QStringLiteral( + "

Your OpenPGP encryption key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in less than 6 " + "days.

") + << QStringLiteral( + "

Your OpenPGP signing key

alice@autocrypt.example (KeyID 0xF231550C4F47E38E)

expires in less than 6 " + "days.

"); + QTest::newRow("smime") << getKeys(true)[0] + << QStringLiteral( + "

The S/MIME certificate for

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US (serial " + "number 00D345203A186385C9);

expires in less than 6 days.

") + << QStringLiteral( + "

Your S/MIME encryption certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " + "(serial number 00D345203A186385C9);

expires in less than 6 days.

") + << QStringLiteral( + "

Your S/MIME signing certificate

CN=unittest cert,EMAIL=test@example.com,O=KDAB,C=US " + "(serial number 00D345203A186385C9);

expires in less than 6 days.

"); + } + + void nearexpiry() + { + QFETCH(GpgME::Key, key); + QFETCH(QString, msg); + QFETCH(QString, msgOwnKey); + QFETCH(QString, msgOwnSigningKey); + + { + ExpiryChecker checker(1, 10, 1, 1); + checker.d->testMode = true; + checker.d->difftime = 5 * 24 * 3600; // 5 days + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + // Test if the correct treshold is taken + checker.checkKey(key); + checker.checkOwnKey(key); + checker.checkOwnSigningKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msg); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OtherKeyNearExpiry); + } + { + ExpiryChecker checker(10, 1, 1, 1); + checker.d->testMode = true; + checker.d->difftime = 5 * 24 * 3600; // 5 days + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + // Test if the correct treshold is taken + checker.checkKey(key); + checker.checkOwnKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msgOwnKey); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyNearExpiry); + } + { + ExpiryChecker checker(10, 1, 1, 1); + checker.d->testMode = true; + checker.d->difftime = 5 * 24 * 3600; // 5 days + QSignalSpy spy(&checker, &ExpiryChecker::expiryMessage); + // Test if the correct treshold is taken + checker.checkKey(key); + checker.checkOwnSigningKey(key); + QCOMPARE(spy.count(), 1); + QList arguments = spy.takeFirst(); + QCOMPARE(arguments.at(0).value().keyID(), key.keyID()); + QCOMPARE(arguments.at(1).toString(), msgOwnSigningKey); + QCOMPARE(arguments.at(2).value(), ExpiryChecker::OwnKeyNearExpiry); + } + } + +private: + QSharedPointer mGnupgHome; +}; + +QTEST_MAIN(ExpiryCheckerTest) +#include "expirycheckertest.moc" diff --git a/autotests/expirycheckertest.qrc b/autotests/expirycheckertest.qrc new file mode 100644 index 000000000..7c2d37f77 --- /dev/null +++ b/autotests/expirycheckertest.qrc @@ -0,0 +1,13 @@ + + + + + fixtures/expirycheckertest/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key + fixtures/expirycheckertest/pubring.kbx + + diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key new file mode 100644 index 000000000..39ac307b4 Binary files /dev/null and b/autotests/fixtures/expirycheckertest/private-keys-v1.d/1AA8BA52430E51AE249AF0DA97D59F869E4101A8.key differ diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key new file mode 100644 index 000000000..af872d912 Binary files /dev/null and b/autotests/fixtures/expirycheckertest/private-keys-v1.d/53F70182AE3A9CFDDA3DA5B3A1742B875F43524B.key differ diff --git a/autotests/fixtures/expirycheckertest/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key b/autotests/fixtures/expirycheckertest/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key new file mode 100644 index 000000000..e2822e175 Binary files /dev/null and b/autotests/fixtures/expirycheckertest/private-keys-v1.d/EC06D8C339EF73304D5B2CCF5363B437E0C915F2.key differ diff --git a/autotests/fixtures/expirycheckertest/pubring.kbx b/autotests/fixtures/expirycheckertest/pubring.kbx new file mode 100644 index 000000000..7395acdb9 Binary files /dev/null and b/autotests/fixtures/expirycheckertest/pubring.kbx differ diff --git a/autotests/fixtures/expirycheckertest/trustdb.gpg b/autotests/fixtures/expirycheckertest/trustdb.gpg new file mode 100644 index 000000000..855a11f1e Binary files /dev/null and b/autotests/fixtures/expirycheckertest/trustdb.gpg differ diff --git a/autotests/fixtures/expirycheckertest/trustlist.txt b/autotests/fixtures/expirycheckertest/trustlist.txt new file mode 100644 index 000000000..16b797a64 --- /dev/null +++ b/autotests/fixtures/expirycheckertest/trustlist.txt @@ -0,0 +1,11 @@ +5E:7C:B2:F4:9F:70:05:43:42:32:5D:75:74:70:00:09:B9:D8:08:61 S + + + +# CN=unittest cert +# O=KDAB +# C=US +# EMail=test@example.com +# 24:D2:FC:A2:2E:B3:B8:0A:1E:37:71:D1:4C:C6:58:E3:21:2B:49:DC S + + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1d922d58b..62b6ea14c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,389 +1,393 @@ # SPDX-License-Identifier: CC0-1.0 # SPDX-FileCopyrightText: none # target_include_directories does not handle empty include paths include_directories( ${Boost_INCLUDE_DIRS} ${GPGME_INCLUDES} ) add_definitions(-DTRANSLATION_DOMAIN=\"libkleopatra\") #add_definitions( -DQT_NO_CAST_FROM_ASCII ) #add_definitions( -DQT_NO_CAST_TO_ASCII ) kde_enable_exceptions() add_definitions( -DGPGMEPP_ERR_SOURCE_DEFAULT=13 ) # 13 is GPG_ERR_SOURCE_KLEO, even if gpg-error's too old to know about add_subdirectory( pics ) add_library(KPim${KF_MAJOR_VERSION}Libkleo) add_library(KPim${KF_MAJOR_VERSION}::Libkleo ALIAS KPim${KF_MAJOR_VERSION}Libkleo) ########### next target ############### target_sources(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE kleo/auditlogentry.cpp kleo/auditlogentry.h kleo/checksumdefinition.cpp kleo/checksumdefinition.h kleo/debug.cpp kleo/debug.h kleo/defaultkeyfilter.cpp kleo/defaultkeyfilter.h kleo/defaultkeygenerationjob.cpp kleo/defaultkeygenerationjob.h kleo/docaction.cpp kleo/docaction.h kleo/dn.cpp kleo/dn.h kleo/enum.cpp kleo/enum.h + kleo/expirychecker.cpp + kleo/expirychecker.h + kleo/expirychecker_p.h kleo/kconfigbasedkeyfilter.cpp kleo/kconfigbasedkeyfilter.h kleo/keyfilter.h kleo/keyfiltermanager.cpp kleo/keyfiltermanager.h kleo/keygroup.cpp kleo/keygroup.h kleo/keygroupconfig.cpp kleo/keygroupconfig.h kleo/keygroupimportexport.cpp kleo/keygroupimportexport.h kleo/keyresolver.cpp kleo/keyresolver.h kleo/keyresolvercore.cpp kleo/keyresolvercore.h kleo/keyserverconfig.cpp kleo/keyserverconfig.h kleo/kleoexception.cpp kleo/kleoexception.h kleo/oidmap.cpp kleo/oidmap.h kleo/predicates.h kleo/stl_util.h models/keycache.cpp models/keycache.h models/keycache_p.h models/keylist.h models/keylistmodel.cpp models/keylistmodel.h models/keylistmodelinterface.cpp models/keylistmodelinterface.h models/keylistsortfilterproxymodel.cpp models/keylistsortfilterproxymodel.h models/keyrearrangecolumnsproxymodel.cpp models/keyrearrangecolumnsproxymodel.h models/subkeylistmodel.cpp models/subkeylistmodel.h models/useridlistmodel.cpp models/useridlistmodel.h utils/algorithm.h utils/assuan.cpp utils/assuan.h utils/classify.cpp utils/classify.h utils/compat.cpp utils/compat.h utils/compliance.cpp utils/compliance.h utils/cryptoconfig.cpp utils/cryptoconfig.h utils/cryptoconfig_p.h utils/filesystemwatcher.cpp utils/filesystemwatcher.h utils/formatting.cpp utils/formatting.h utils/gnupg-registry.c utils/gnupg-registry.h utils/gnupg.cpp utils/gnupg.h utils/hex.cpp utils/hex.h utils/keyhelpers.cpp utils/keyhelpers.h utils/qtstlhelpers.cpp utils/qtstlhelpers.h utils/scdaemon.cpp utils/scdaemon.h utils/stringutils.cpp utils/stringutils.h utils/systeminfo.cpp utils/systeminfo.h utils/test.cpp utils/test.h utils/uniquelock.cpp utils/uniquelock.h ) ecm_qt_declare_logging_category(KPim${KF_MAJOR_VERSION}Libkleo HEADER libkleo_debug.h IDENTIFIER LIBKLEO_LOG CATEGORY_NAME org.kde.pim.libkleo DESCRIPTION "libkleo (kleo_core)" EXPORT LIBKLEO ) target_sources(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE ui/auditlogviewer.cpp ui/auditlogviewer.h ui/cryptoconfigentryreaderport.cpp ui/cryptoconfigentryreaderport_p.h ui/cryptoconfigmodule.cpp ui/cryptoconfigmodule.h ui/cryptoconfigmodule_p.h ui/directoryserviceswidget.cpp ui/directoryserviceswidget.h ui/dnattributeorderconfigwidget.cpp ui/dnattributeorderconfigwidget.h ui/editdirectoryservicedialog.cpp ui/editdirectoryservicedialog.h ui/filenamerequester.cpp ui/filenamerequester.h ui/messagebox.cpp ui/messagebox.h ui/navigatabletreeview.cpp ui/navigatabletreeview.h ui/navigatabletreewidget.cpp ui/navigatabletreewidget.h ui/progressbar.cpp ui/progressbar.h ui/progressdialog.cpp ui/progressdialog.h ui/readerportselection.cpp ui/readerportselection.h ) ecm_qt_declare_logging_category(KPim${KF_MAJOR_VERSION}Libkleo HEADER kleo_ui_debug.h IDENTIFIER KLEO_UI_LOG CATEGORY_NAME org.kde.pim.kleo_ui DESCRIPTION "libkleo (kleo_ui)" OLD_CATEGORY_NAMES log_kleo_ui EXPORT LIBKLEO ) target_sources(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE # make this a separate lib. ui/keyapprovaldialog.cpp ui/keyapprovaldialog.h ui/keylistview.cpp ui/keylistview.h ui/keyrequester.cpp ui/keyrequester.h ui/keyselectioncombo.cpp ui/keyselectioncombo.h ui/keyselectiondialog.cpp ui/keyselectiondialog.h ui/newkeyapprovaldialog.cpp ui/newkeyapprovaldialog.h ) if(MINGW) # we do not care about different signedness of passed pointer arguments set_source_files_properties(utils/gnupg-registry.c PROPERTIES COMPILE_OPTIONS "-Wno-pointer-sign") endif() target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo PUBLIC Gpgmepp PRIVATE Qt::Widgets KF${KF_MAJOR_VERSION}::I18n KF${KF_MAJOR_VERSION}::Completion KF${KF_MAJOR_VERSION}::ConfigCore KF${KF_MAJOR_VERSION}::ConfigWidgets KF${KF_MAJOR_VERSION}::CoreAddons KF${KF_MAJOR_VERSION}::WidgetsAddons KF${KF_MAJOR_VERSION}::ItemModels KF${KF_MAJOR_VERSION}::Codecs) if (QT_MAJOR_VERSION STREQUAL "6") target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE Qt6::Core5Compat PUBLIC QGpgmeQt6) else() target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo PUBLIC QGpgme) endif() # Boost::headers may not be available for old versions of Boost if (TARGET Boost::headers) target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE Boost::headers) endif() if (KPim${KF_MAJOR_VERSION}TextEdit_FOUND) add_definitions(-DHAVE_PIMTEXTEDIT) target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo PRIVATE KPim${KF_MAJOR_VERSION}::PimTextEdit) endif() if (COMPILE_WITH_UNITY_CMAKE_SUPPORT) set_target_properties(KPim${KF_MAJOR_VERSION}Libkleo PROPERTIES UNITY_BUILD ON) endif() ecm_generate_export_header(KPim${KF_MAJOR_VERSION}Libkleo BASE_NAME kleo VERSION ${PIM_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 5.23 ) if(WIN32) target_link_libraries(KPim${KF_MAJOR_VERSION}Libkleo ${GPGME_VANILLA_LIBRARIES} ) endif() set_target_properties(KPim${KF_MAJOR_VERSION}Libkleo PROPERTIES VERSION ${LIBKLEO_VERSION} SOVERSION ${LIBKLEO_SOVERSION} EXPORT_NAME Libkleo ) install(TARGETS KPim${KF_MAJOR_VERSION}Libkleo EXPORT KPim${KF_MAJOR_VERSION}LibkleoTargets ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} ) target_include_directories(KPim${KF_MAJOR_VERSION}Libkleo INTERFACE "$") target_include_directories(KPim${KF_MAJOR_VERSION}Libkleo PUBLIC "$") ecm_generate_headers(libkleo_CamelCase_HEADERS HEADER_NAMES AuditLogEntry ChecksumDefinition Debug DefaultKeyFilter DefaultKeyGenerationJob DocAction Dn Enum + ExpiryChecker KConfigBasedKeyFilter KeyFilter KeyFilterManager KeyGroup KeyGroupConfig KeyGroupImportExport KeyResolver KeyResolverCore KeyserverConfig KleoException OidMap Predicates Stl_Util REQUIRED_HEADERS libkleo_HEADERS PREFIX Libkleo RELATIVE kleo ) ecm_generate_headers(libkleo_CamelCase_models_HEADERS HEADER_NAMES KeyCache KeyList KeyListModel KeyListModelInterface KeyListSortFilterProxyModel KeyRearrangeColumnsProxyModel SubkeyListModel UserIDListModel REQUIRED_HEADERS libkleo_models_HEADERS PREFIX Libkleo RELATIVE models ) ecm_generate_headers(libkleo_CamelCase_utils_HEADERS HEADER_NAMES Algorithm Assuan Classify Compat Compliance CryptoConfig FileSystemWatcher Formatting GnuPG Hex KeyHelpers QtStlHelpers SCDaemon StringUtils SystemInfo Test UniqueLock REQUIRED_HEADERS libkleo_utils_HEADERS PREFIX Libkleo RELATIVE utils ) ecm_generate_headers(libkleo_CamelCase_ui_HEADERS HEADER_NAMES AuditLogViewer CryptoConfigModule DNAttributeOrderConfigWidget DirectoryServicesWidget EditDirectoryServiceDialog FileNameRequester KeyApprovalDialog KeyListView KeyRequester KeySelectionCombo KeySelectionDialog MessageBox NavigatableTreeView NavigatableTreeWidget NewKeyApprovalDialog ProgressDialog ReaderPortSelection REQUIRED_HEADERS libkleo_ui_HEADERS PREFIX Libkleo RELATIVE ui ) if (QT_MAJOR_VERSION STREQUAL "6") ecm_generate_pri_file(BASE_NAME Libkleo LIB_NAME KPim${KF_MAJOR_VERSION}Libkleo DEPS "QGpgme" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR}/KPim${KF_MAJOR_VERSION}/Libkleo ) else() ecm_generate_pri_file(BASE_NAME Libkleo LIB_NAME KPim${KF_MAJOR_VERSION}Libkleo DEPS "QGpgmeQt6" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR}/KPim${KF_MAJOR_VERSION}/Libkleo ) endif() install(FILES ${libkleo_CamelCase_HEADERS} ${libkleo_CamelCase_models_HEADERS} ${libkleo_CamelCase_ui_HEADERS} ${libkleo_CamelCase_utils_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim${KF_MAJOR_VERSION}/Libkleo/Libkleo COMPONENT Devel ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kleo_export.h ${libkleo_HEADERS} ${libkleo_models_HEADERS} ${libkleo_ui_HEADERS} ${libkleo_utils_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR}/KPim${KF_MAJOR_VERSION}/Libkleo/libkleo COMPONENT Devel ) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) if ( WIN32 ) install ( FILES libkleopatrarc-win32.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) else () install ( FILES libkleopatrarc.desktop DESTINATION ${KDE_INSTALL_CONFDIR} RENAME libkleopatrarc ) endif () if (BUILD_QCH) ecm_add_qch( KPim${KF_MAJOR_VERSION}Libkleo_QCH NAME KPim${KF_MAJOR_VERSION}Libkleo BASE_NAME KPim${KF_MAJOR_VERSION}Libkleo VERSION ${PIM_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${libkleo_HEADERS} ${libkleo_models_HEADERS} ${libkleo_ui_HEADERS} ${libkleo_utils_HEADERS} #MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" #IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt${QT_MAJOR_VERSION}Core_QCH Qt${QT_MAJOR_VERSION}Gui_QCH Qt${QT_MAJOR_VERSION}Widgets_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} BLANK_MACROS KLEO_EXPORT TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() diff --git a/src/kleo/expirychecker.cpp b/src/kleo/expirychecker.cpp new file mode 100644 index 000000000..49358bc79 --- /dev/null +++ b/src/kleo/expirychecker.cpp @@ -0,0 +1,361 @@ +/* + This file is part of libkleopatra, the KDE keymanagement library + SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB + SPDX-FileCopyrightText: 2021 Sandro Knauß + SPDX-FileCopyrightText: 2023 g10 Code GmbH + SPDX-FileContributor: Ingo Klöcker + + Based on kpgp.h + Copyright (C) 2001,2002 the KPGP authors + See file libkdenetwork/AUTHORS.kpgp for details + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "expirychecker.h" +#include "expirychecker_p.h" + +#include "dn.h" + +#include + +#include + +#include +#include + +#include + +using namespace Kleo; + +ExpiryChecker::ExpiryChecker(int encrOwnKeyNearExpiryThresholdDays, + int encrKeyNearExpiryThresholdDays, + int encrRootCertNearExpiryThresholdDays, + int encrChainCertNearExpiryThresholdDays) + : d(new ExpiryCheckerPrivate) +{ + d->encryptOwnKeyNearExpiryWarningThreshold = encrOwnKeyNearExpiryThresholdDays; + d->encryptKeyNearExpiryWarningThreshold = encrKeyNearExpiryThresholdDays; + d->encryptRootCertNearExpiryWarningThreshold = encrRootCertNearExpiryThresholdDays; + d->encryptChainCertNearExpiryWarningThreshold = encrChainCertNearExpiryThresholdDays; +} + +ExpiryChecker::~ExpiryChecker() = default; + +int ExpiryChecker::encryptOwnKeyNearExpiryWarningThresholdInDays() const +{ + return d->encryptOwnKeyNearExpiryWarningThreshold; +} + +int ExpiryChecker::encryptKeyNearExpiryWarningThresholdInDays() const +{ + return d->encryptKeyNearExpiryWarningThreshold; +} + +int ExpiryChecker::encryptRootCertNearExpiryWarningThresholdInDays() const +{ + return d->encryptRootCertNearExpiryWarningThreshold; +} + +int ExpiryChecker::encryptChainCertNearExpiryWarningThresholdInDays() const +{ + return d->encryptChainCertNearExpiryWarningThreshold; +} + +QString formatOpenPGPMessage(const GpgME::Key &key, int secsTillExpiry, bool isOwnKey, bool isSigningKey) +{ + KLocalizedString msg; + static const double secsPerDay = 24 * 60 * 60; + const int daysTillExpiry = 1 + int(abs(secsTillExpiry) / secsPerDay); + if (secsTillExpiry <= 0) { + qCDebug(LIBKLEO_LOG) << "Key 0x" << key.keyID() << " expired " << daysTillExpiry << " days ago"; + if (isSigningKey) { + msg = ki18np( + "

Your OpenPGP signing key

%2 (KeyID 0x%3)

" + "

expired less than a day ago.

", + "

Your OpenPGP signing key

%2 (KeyID 0x%3)

" + "

expired %1 days ago.

"); + } else if (isOwnKey) { + msg = ki18np( + "

Your OpenPGP encryption key

%2 (KeyID 0x%3)

" + "

expired less than a day ago.

", + "

Your OpenPGP encryption key

%2 (KeyID 0x%3)

" + "

expired %1 days ago.

"); + } else { + msg = ki18np( + "

The OpenPGP key for

%2 (KeyID 0x%3)

" + "

expired less than a day ago.

", + "

The OpenPGP key for

%2 (KeyID 0x%3)

" + "

expired %1 days ago.

"); + } + } else { + qCDebug(LIBKLEO_LOG) << "Key 0x" << key.keyID() << " expires in less than " << daysTillExpiry << " days"; + if (isSigningKey) { + msg = ki18np( + "

Your OpenPGP signing key

%2 (KeyID 0x%3)

" + "

expires in less than a day.

", + "

Your OpenPGP signing key

%2 (KeyID 0x%3)

" + "

expires in less than %1 days.

"); + } else if (isOwnKey) { + msg = ki18np( + "

Your OpenPGP encryption key

%2 (KeyID 0x%3)

" + "

expires in less than a day.

", + "

Your OpenPGP encryption key

%2 (KeyID 0x%3)

" + "

expires in less than %1 days.

"); + } else { + msg = ki18np( + "

The OpenPGP key for

%2 (KeyID 0x%3)

" + "

expires in less than a day.

", + "

The OpenPGP key for

%2 (KeyID 0x%3)

" + "

expires in less than %1 days.

"); + } + } + return msg.subs(daysTillExpiry).subs(QString::fromUtf8(key.userID(0).id())).subs(QString::fromLatin1(key.keyID())).toString(); +} + +QString formatSMIMEMessage(const GpgME::Key &key, const GpgME::Key &orig_key, int secsTillExpiry, bool isOwnKey, bool isSigningKey, bool ca) +{ + KLocalizedString msg; + static const double secsPerDay = 24 * 60 * 60; + const int daysTillExpiry = 1 + int(abs(secsTillExpiry) / secsPerDay); + if (secsTillExpiry <= 0) { + qCDebug(LIBKLEO_LOG) << "Key 0x" << key.keyID() << " expired " << daysTillExpiry << " days ago"; + if (ca) { + if (key.isRoot()) { + if (isSigningKey) { + msg = ki18np( + "

The root certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The root certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else if (isOwnKey) { + msg = ki18np( + "

The root certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The root certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else { + msg = ki18np( + "

The root certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The root certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } + } else { + if (isSigningKey) { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The intermediate CA certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else if (isOwnKey) { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The intermediate CA certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The intermediate CA certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } + } + return msg.subs(daysTillExpiry) + .subs(Kleo::DN(orig_key.userID(0).id()).prettyDN()) + .subs(QString::fromLatin1(orig_key.issuerSerial())) + .subs(Kleo::DN(key.userID(0).id()).prettyDN()) + .toString(); + } else { + if (isSigningKey) { + msg = ki18np( + "

Your S/MIME signing certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

Your S/MIME signing certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else if (isOwnKey) { + msg = ki18np( + "

Your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

Your S/MIME encryption certificate

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } else { + msg = ki18np( + "

The S/MIME certificate for

%2 (serial number %3)

" + "

expired less than a day ago.

", + "

The S/MIME certificate for

%2 (serial number %3)

" + "

expired %1 days ago.

"); + } + return msg.subs(daysTillExpiry).subs(Kleo::DN(key.userID(0).id()).prettyDN()).subs(QString::fromLatin1(key.issuerSerial())).toString(); + } + } else { + qCDebug(LIBKLEO_LOG) << "Key 0x" << key.keyID() << " expires in less than " << daysTillExpiry << " days"; + if (ca) { + if (key.isRoot()) { + if (isSigningKey) { + msg = ki18np( + "

The root certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The root certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else if (isOwnKey) { + msg = ki18np( + "

The root certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The root certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else { + msg = ki18np( + "

The root certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The root certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } + } else { + if (isSigningKey) { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The intermediate CA certificate

%4

" + "

for your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else if (isOwnKey) { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The intermediate CA certificate

%4

" + "

for your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else { + msg = ki18np( + "

The intermediate CA certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The intermediate CA certificate

%4

" + "

for S/MIME certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } + } + return msg.subs(daysTillExpiry) + .subs(Kleo::DN(orig_key.userID(0).id()).prettyDN()) + .subs(QString::fromLatin1(orig_key.issuerSerial())) + .subs(Kleo::DN(key.userID(0).id()).prettyDN()) + .toString(); + } else { + if (isSigningKey) { + msg = ki18np( + "

Your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

Your S/MIME signing certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else if (isOwnKey) { + msg = ki18np( + "

Your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than a day.

", + "

Your S/MIME encryption certificate

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } else { + msg = ki18np( + "

The S/MIME certificate for

%2 (serial number %3);

" + "

expires in less than a day.

", + "

The S/MIME certificate for

%2 (serial number %3);

" + "

expires in less than %1 days.

"); + } + return msg.subs(daysTillExpiry).subs(Kleo::DN(key.userID(0).id()).prettyDN()).subs(QString::fromLatin1(key.issuerSerial())).toString(); + } + } +} + +double ExpiryChecker::calculateSecsTillExpiriy(const GpgME::Subkey &key) const +{ + if (d->testMode) { + return d->difftime; + } + + return ::difftime(key.expirationTime(), time(nullptr)); +} + +void ExpiryChecker::checkKeyNearExpiry(const GpgME::Key &key, bool isOwnKey, bool isSigningKey, bool ca, int recur_limit, const GpgME::Key &orig_key) const +{ + if (recur_limit <= 0) { + qCDebug(LIBKLEO_LOG) << "Key chain too long (>100 certs)"; + return; + } + const GpgME::Subkey subkey = key.subkey(0); + + const bool newMessage = !d->alreadyWarnedFingerprints.count(subkey.fingerprint()); + + if (subkey.neverExpires()) { + return; + } + static const double secsPerDay = 24 * 60 * 60; + const double secsTillExpiry = calculateSecsTillExpiriy(subkey); + if (secsTillExpiry <= 0) { + const QString msg = key.protocol() == GpgME::OpenPGP ? formatOpenPGPMessage(key, secsTillExpiry, isOwnKey, isSigningKey) + : formatSMIMEMessage(key, orig_key, secsTillExpiry, isOwnKey, isSigningKey, ca); + d->alreadyWarnedFingerprints.insert(subkey.fingerprint()); + Q_EMIT expiryMessage(key, msg, isOwnKey ? OwnKeyExpired : OtherKeyExpired, newMessage); + } else { + const int daysTillExpiry = 1 + int(secsTillExpiry / secsPerDay); + const int threshold = ca ? (key.isRoot() ? encryptRootCertNearExpiryWarningThresholdInDays() : encryptChainCertNearExpiryWarningThresholdInDays()) + : (isOwnKey ? encryptOwnKeyNearExpiryWarningThresholdInDays() : encryptKeyNearExpiryWarningThresholdInDays()); + if (threshold > -1 && daysTillExpiry <= threshold) { + const QString msg = key.protocol() == GpgME::OpenPGP ? formatOpenPGPMessage(key, secsTillExpiry, isOwnKey, isSigningKey) + : formatSMIMEMessage(key, orig_key, secsTillExpiry, isOwnKey, isSigningKey, ca); + d->alreadyWarnedFingerprints.insert(subkey.fingerprint()); + Q_EMIT expiryMessage(key, msg, isOwnKey ? OwnKeyNearExpiry : OtherKeyNearExpiry, newMessage); + } + } + if (key.isRoot()) { + return; + } else if (key.protocol() != GpgME::CMS) { // Key chaining is only possible in SMIME + return; + } else if (const char *chain_id = key.chainID()) { + QGpgME::Protocol *p = QGpgME::smime(); + Q_ASSERT(p); + std::unique_ptr job(p->keyListJob(false, false, true)); + if (job.get()) { + std::vector keys; + job->exec(QStringList(QLatin1String(chain_id)), false, keys); + if (!keys.empty()) { + return checkKeyNearExpiry(keys.front(), isOwnKey, isSigningKey, true, recur_limit - 1, ca ? orig_key : key); + } + } + } +} + +void ExpiryChecker::checkOwnSigningKey(const GpgME::Key &key) const +{ + checkKeyNearExpiry(key, /*isOwnKey*/ true, /*isSigningKey*/ true); +} + +void ExpiryChecker::checkOwnKey(const GpgME::Key &key) const +{ + checkKeyNearExpiry(key, /*isOwnKey*/ true, /*isSigningKey*/ false); +} + +void ExpiryChecker::checkKey(const GpgME::Key &key) const +{ + checkKeyNearExpiry(key, false, false); +} diff --git a/src/kleo/expirychecker.h b/src/kleo/expirychecker.h new file mode 100644 index 000000000..f27679c5c --- /dev/null +++ b/src/kleo/expirychecker.h @@ -0,0 +1,79 @@ +/* + This file is part of libkleopatra, the KDE keymanagement library + SPDX-FileCopyrightText: 2004 Klarälvdalens Datakonsult AB + SPDX-FileCopyrightText: 2021 Sandro Knauß + SPDX-FileCopyrightText: 2023 g10 Code GmbH + SPDX-FileContributor: Ingo Klöcker + + Based on kpgp.h + Copyright (C) 2001,2002 the KPGP authors + See file libkdenetwork/AUTHORS.kpgp for details + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include "kleo_export.h" + +#include + +#include + +#include + +class ExpiryCheckerTest; + +namespace Kleo +{ + +class ExpiryCheckerPrivate; + +class KLEO_EXPORT ExpiryChecker : public QObject +{ + Q_OBJECT +public: + using Ptr = QSharedPointer; + explicit ExpiryChecker(int encrOwnKeyNearExpiryThresholdDays, + int encrKeyNearExpiryThresholdDays, + int encrRootCertNearExpiryThresholdDays, + int encrChainCertNearExpiryThresholdDays); + + ~ExpiryChecker() override; + + Q_REQUIRED_RESULT int encryptOwnKeyNearExpiryWarningThresholdInDays() const; + Q_REQUIRED_RESULT int encryptKeyNearExpiryWarningThresholdInDays() const; + Q_REQUIRED_RESULT int encryptRootCertNearExpiryWarningThresholdInDays() const; + Q_REQUIRED_RESULT int encryptChainCertNearExpiryWarningThresholdInDays() const; + + enum ExpiryInformation { + OwnKeyExpired, + OwnKeyNearExpiry, + OtherKeyExpired, + OtherKeyNearExpiry, + }; + Q_ENUM(ExpiryInformation) + + void checkOwnSigningKey(const GpgME::Key &key) const; + void checkOwnKey(const GpgME::Key &key) const; + void checkKey(const GpgME::Key &key) const; + +Q_SIGNALS: + void expiryMessage(const GpgME::Key &key, QString msg, Kleo::ExpiryChecker::ExpiryInformation info, bool isNewMessage) const; + +private: + friend class ::ExpiryCheckerTest; + + std::unique_ptr const d; + + Q_REQUIRED_RESULT double calculateSecsTillExpiriy(const GpgME::Subkey &key) const; + + void checkKeyNearExpiry(const GpgME::Key &key, + bool isOwnKey, + bool isSigningKey, + bool ca = false, + int recur_limit = 100, + const GpgME::Key &orig_key = GpgME::Key::null) const; +}; +} +Q_DECLARE_METATYPE(GpgME::Key) diff --git a/src/kleo/expirychecker_p.h b/src/kleo/expirychecker_p.h new file mode 100644 index 000000000..7699f2e3e --- /dev/null +++ b/src/kleo/expirychecker_p.h @@ -0,0 +1,29 @@ +/* + SPDX-FileCopyrightText: 2022 Sandro Knauß + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#pragma once + +#include + +#include + +namespace Kleo +{ + +class ExpiryCheckerPrivate +{ +public: + int encryptOwnKeyNearExpiryWarningThreshold; + int encryptKeyNearExpiryWarningThreshold; + int encryptRootCertNearExpiryWarningThreshold; + int encryptChainCertNearExpiryWarningThreshold; + + std::set alreadyWarnedFingerprints; + bool testMode = false; + double difftime = 0; +}; + +}