diff --git a/autotests/CMakeLists.txt b/autotests/CMakeLists.txt index b16ca69..2e022cb 100644 --- a/autotests/CMakeLists.txt +++ b/autotests/CMakeLists.txt @@ -1,30 +1,31 @@ find_package(Qt5Test ${QT_REQUIRED_VERSION} CONFIG REQUIRED) add_definitions(-DSOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}") ecm_add_test(stringutiltest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(datatypestest.cpp LINK_LIBRARIES Qt5::Test Qt5::Qml KPim::Itinerary) ecm_add_test(jsonlddocumenttest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(mergeutiltest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(locationutiltest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(knowledgedbtest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(airportdbtest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(extractorinputtest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(extractorrepositorytest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(bcbpparsertest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(uic9183parsertest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) +ecm_add_test(vdvtickettest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(rct2parsertest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(jsapitest.cpp ../src/jsapi/jsonld.cpp TEST_NAME jsapitest LINK_LIBRARIES Qt5::Test KPim::Itinerary Qt5::Qml) ecm_add_test(bitarraytest.cpp ../src/jsapi/bitarray.cpp TEST_NAME bitarraytest LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(structureddataextractortest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(pdfdocumenttest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary Qt5::Gui) ecm_add_test(htmldocumenttest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(barcodedecodertest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary Qt5::Gui) ecm_add_test(pkpassextractortest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary KPim::PkPass) ecm_add_test(extractorutiltest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(postprocessortest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) if (TARGET KF5::CalendarCore) ecm_add_test(calendarhandlertest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(extractortest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary KPim::PkPass) endif() ecm_add_test(documentutiltest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary) ecm_add_test(filetest.cpp LINK_LIBRARIES Qt5::Test KPim::Itinerary KPim::PkPass) diff --git a/autotests/vdvtickettest.cpp b/autotests/vdvtickettest.cpp new file mode 100644 index 0000000..afbbb8d --- /dev/null +++ b/autotests/vdvtickettest.cpp @@ -0,0 +1,60 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include + +#include +#include +#include + +using namespace KItinerary; + +class VdvTicketTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void testMaybeVdvTicket_data() + { + QTest::addColumn("input"); + QTest::addColumn("isVdv"); + + QTest::newRow("empty") << QByteArray() << false; + QTest::newRow("null") << QByteArray(352, 0x0) << false; + + QByteArray b(352, 0x0); + b[0] = (char)0x9E; + b[1] = (char)0x81; + b[2] = (char)0x80; + b[131] = (char)0x9A; + b[132] = (char)0x05; + b[133] = 'V'; + b[134] = 'D'; + b[135] = 'V'; + QTest::newRow("valid min length") << b << true; + } + + void testMaybeVdvTicket() + { + QFETCH(QByteArray, input); + QFETCH(bool, isVdv); + QCOMPARE(VdvTicketParser::maybeVdvTicket(input), isVdv); + } +}; + +QTEST_APPLESS_MAIN(VdvTicketTest) + +#include "vdvtickettest.moc" diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 86833cd..bbacdd1 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,221 +1,233 @@ set(KDE_INSTALL_INCLUDEDIR_PIM ${KDE_INSTALL_INCLUDEDIR}/KPim) add_subdirectory(cli) if (TARGET Qt5::Network) add_subdirectory(knowledgedb-generator) endif() configure_file(config-kitinerary.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kitinerary.h) set(kitinerary_lib_srcs datatypes/action.cpp datatypes/brand.cpp datatypes/bustrip.cpp datatypes/creativework.cpp datatypes/event.cpp datatypes/flight.cpp datatypes/organization.cpp datatypes/person.cpp datatypes/place.cpp datatypes/reservation.cpp datatypes/taxi.cpp datatypes/ticket.cpp datatypes/traintrip.cpp datatypes/rentalcar.cpp datatypes/visit.cpp generic/genericpdfextractor.cpp generic/genericpkpassextractor.cpp generic/genericuic918extractor.cpp generic/structureddataextractor.cpp jsapi/barcode.cpp jsapi/bitarray.cpp jsapi/context.cpp jsapi/jsonld.cpp knowledgedb/alphaid.cpp knowledgedb/airportdb.cpp knowledgedb/countrydb.cpp knowledgedb/iatacode.cpp knowledgedb/knowledgedb.cpp knowledgedb/timezonedb.cpp knowledgedb/trainstationdb.cpp pdf/pdfdocument.cpp pdf/pdfextractoroutputdevice.cpp pdf/pdfimage.cpp pdf/pdfvectorpicture.cpp pdf/popplerglobalparams.cpp pdf/popplerutils.cpp uic9183/rct2ticket.cpp uic9183/uic9183block.cpp uic9183/uic9183parser.cpp uic9183/uic9183ticketlayout.cpp uic9183/vendor0080block.cpp + vdv/vdvticketparser.cpp + barcodedecoder.cpp calendarhandler.cpp documentutil.cpp extractor.cpp extractorengine.cpp extractorfilter.cpp extractorinput.cpp extractorpostprocessor.cpp extractorrepository.cpp extractorutil.cpp extractorvalidator.cpp file.cpp flightpostprocessor.cpp htmldocument.cpp iatabcbpparser.cpp jsonlddocument.cpp jsonldimportfilter.cpp locationutil.cpp mergeutil.cpp qimagepurebinarizer.cpp sortutil.cpp stringutil.cpp ) qt5_add_resources(kitinerary_lib_srcs extractors/extractors.qrc) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER logging.h IDENTIFIER KItinerary::Log CATEGORY_NAME org.kde.kitinerary) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER compare-logging.h IDENTIFIER KItinerary::CompareLog CATEGORY_NAME org.kde.kitinerary.comparator) ecm_qt_declare_logging_category(kitinerary_lib_srcs HEADER validator-logging.h IDENTIFIER KItinerary::ValidatorLog CATEGORY_NAME org.kde.kitinerary.extractorValidator) add_library(KPimItinerary ${kitinerary_lib_srcs}) add_library(KPim::Itinerary ALIAS KPimItinerary) generate_export_header(KPimItinerary BASE_NAME KItinerary) set_target_properties(KPimItinerary PROPERTIES VERSION ${KITINERARY_VERSION_STRING} SOVERSION ${KITINERARY_SOVERSION} EXPORT_NAME Itinerary ) target_include_directories(KPimItinerary INTERFACE "$") target_include_directories(KPimItinerary PUBLIC "$") target_link_libraries(KPimItinerary PUBLIC Qt5::Core KF5::Mime PRIVATE Qt5::Qml KF5::Archive KF5::I18n KF5::Contacts KPim::PkPass ${ZLIB_LIBRARIES} ) if (HAVE_POPPLER) target_link_libraries(KPimItinerary PRIVATE Poppler::Core) endif() if (HAVE_ZXING) target_link_libraries(KPimItinerary PRIVATE ZXing::Core) endif() if (HAVE_KCAL) target_link_libraries(KPimItinerary PUBLIC KF5::CalendarCore) endif() if (HAVE_LIBXML2) target_compile_definitions(KPimItinerary PRIVATE ${LIBXML2_DEFINITIONS}) target_include_directories(KPimItinerary PRIVATE ${LIBXML2_INCLUDE_DIR}) target_link_libraries(KPimItinerary PRIVATE ${LIBXML2_LIBRARIES}) endif() if (HAVE_PHONENUMBER) target_link_libraries(KPimItinerary PRIVATE PhoneNumber::PhoneNumber) endif() ecm_generate_headers(KItinerary_FORWARDING_HEADERS HEADER_NAMES BarcodeDecoder CalendarHandler DocumentUtil Extractor ExtractorEngine ExtractorFilter ExtractorInput ExtractorPostprocessor ExtractorRepository File HtmlDocument IataBcbpParser JsonLdDocument LocationUtil MergeUtil SortUtil PREFIX KItinerary REQUIRED_HEADERS KItinerary_HEADERS ) ecm_generate_headers(KItinerary_KnowledgeDb_FORWARDING_HEADERS HEADER_NAMES AlphaId CountryDb KnowledgeDb PREFIX KItinerary REQUIRED_HEADERS KItinerary_KnowledgeDb_HEADERS RELATIVE knowledgedb ) ecm_generate_headers(KItinerary_Datatypes_FORWARDING_HEADERS HEADER_NAMES Action Brand BusTrip CreativeWork Datatypes Event Flight Organization Reservation RentalCar Person Place Taxi Ticket TrainTrip Visit PREFIX KItinerary REQUIRED_HEADERS KItinerary_Datatypes_HEADERS RELATIVE datatypes ) ecm_generate_headers(KItinerary_Pdf_FORWARDING_HEADERS HEADER_NAMES PdfDocument PdfImage PREFIX KItinerary REQUIRED_HEADERS KItinerary_Pdf_HEADERS RELATIVE pdf ) ecm_generate_headers(KItinerary_Uic9183_FORWARDING_HEADERS HEADER_NAMES Rct2Ticket Uic9183Block Uic9183Parser Uic9183TicketLayout Vendor0080Block PREFIX KItinerary REQUIRED_HEADERS KItinerary_Uic9183_HEADERS RELATIVE uic9183 ) +ecm_generate_headers(KItinerary_Vdv_FORWARDING_HEADERS + HEADER_NAMES + VdvTicketParser + PREFIX KItinerary + REQUIRED_HEADERS KItinerary_Vdv_HEADERS + RELATIVE vdv +) + install(TARGETS KPimItinerary EXPORT KPimItineraryTargets ${INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${KItinerary_FORWARDING_HEADERS} ${KItinerary_KnowledgeDb_FORWARDING_HEADERS} ${KItinerary_Datatypes_FORWARDING_HEADERS} ${KItinerary_Pdf_FORWARDING_HEADERS} ${KItinerary_Uic9183_FORWARDING_HEADERS} + ${KItinerary_Vdv_FORWARDING_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary ) install(FILES ${KItinerary_HEADERS} ${KItinerary_AirportDb_HEADERS} ${KItinerary_Datatypes_HEADERS} ${KItinerary_KnowledgeDb_HEADERS} ${KItinerary_Pdf_HEADERS} ${KItinerary_Uic9183_HEADERS} + ${KItinerary_Vdv_HEADERS} ${CMAKE_CURRENT_BINARY_DIR}/kitinerary_export.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/kitinerary ) if (NOT ANDROID) install(FILES application-vnd-kde-itinerary.xml DESTINATION ${XDG_MIME_INSTALL_DIR}) update_xdg_mimetypes(${XDG_MIME_INSTALL_DIR}) endif() diff --git a/src/vdv/vdvdata_p.h b/src/vdv/vdvdata_p.h new file mode 100644 index 0000000..447d606 --- /dev/null +++ b/src/vdv/vdvdata_p.h @@ -0,0 +1,153 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef KITINERARY_VDVDATA_P_H +#define KITINERARY_VDVDATA_P_H + +#include +#include + +namespace KItinerary { + +enum : uint8_t { + TagSignature = 0x9E, + TagSignatureRemainder = 0x9A, + TagCaReference = 0x42, +}; + +enum : uint16_t { + TagCvCertificate = 0x7F21, + TagCvCertificateSignature = 0x5F37, + TagCvCertificateContent = 0x5F4E, +}; + +#pragma pack(push) +#pragma pack(1) + +/** Signature container for the signed part of the payload data. */ +struct VdvSignature { + uint8_t tag; + uint8_t stuff; // always 0x81 + uint8_t size; // always 0x80 + uint8_t data[128]; +}; + +/** Signature Remainder header. */ +struct VdvSignatureRemainder { + enum { Offset = 131 }; + + uint8_t tag; + uint8_t contentSize; // >= 5 + // followed by size bytes with the remainder of the signed payload data. */ + + inline bool isValid() const + { + return tag == TagSignatureRemainder && contentSize >= 5; + } + + inline uint8_t size() const + { + return contentSize + sizeof(tag) + sizeof(contentSize); + } +}; + +/** CV certificate. */ +struct VdvCvCertificate { + uint16_t tag; + uint8_t size0; + uint8_t size1; + + inline bool isValid() const + { + return qFromBigEndian(tag) == TagCvCertificate; + } + + inline uint16_t contentSize() const + { + return ((size0 << 8) | size1) - 0x8100; + } + + inline uint16_t size() const + { + return contentSize() + sizeof(tag) + sizeof(size0) + sizeof(size1); + } +}; + +/** Certificate Authority Reference (CAR) */ +struct VdvCAReference { + uint8_t tag; + uint8_t contentSize; + char region[2]; + char name[3]; + uint8_t serviceIndicator: 4; + uint8_t discretionaryData: 4; + uint8_t algorithmReference; + uint8_t year; + + inline bool isValid() const + { + return tag == TagCaReference && contentSize == 8; + } +}; + +/** Certificate Holder Reference (CHR) */ +struct VdvCertificateHolderReference { + uint8_t filler[4]; // always null + char name[5]; + uint8_t extension[3]; +}; + +/** Certificate Holder Authorization (CHA) */ +struct VdvCertificateHolderAuthorization { + char name[6]; + uint8_t stuff; +}; + +/** Certificate key, contained in a certificate object. */ +struct VdvCertificateKey { + uint16_t tag; + uint16_t taggedSize; + uint8_t cpi; + VdvCAReference car; + VdvCertificateHolderReference chr; + VdvCertificateHolderAuthorization cha; + uint8_t date[3]; + uint8_t oid[9]; + uint8_t modulusBegin; + + inline bool isValid() const + { + return qFromBigEndian(tag) == TagCvCertificateContent; + } +}; + +/** Certificate signature. */ +struct VdvCertificateSignature { + uint16_t tag; + uint16_t taggedSize; + + inline bool isValid() const + { + return qFromBigEndian(tag) == TagCvCertificateSignature; + } +}; + +#pragma pack(pop) + +} + +#endif // KITINERARY_VDVDATA_P_H diff --git a/src/vdv/vdvticketparser.cpp b/src/vdv/vdvticketparser.cpp new file mode 100644 index 0000000..737c883 --- /dev/null +++ b/src/vdv/vdvticketparser.cpp @@ -0,0 +1,89 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include "vdvticketparser.h" +#include "vdvdata_p.h" + +#include +#include + +using namespace KItinerary; + +VdvTicketParser::VdvTicketParser() = default; +VdvTicketParser::~VdvTicketParser() = default; + +void VdvTicketParser::parse(const QByteArray &data) +{ + qDebug() << data.size(); + if (!maybeVdvTicket(data)) { + qWarning() << "Input data is not a VDV ticket!"; + return; + } + + // (1) find the certificate authority reference (CAR) to identify the key to decode the CV certificate + const auto sigRemainder = reinterpret_cast(data.constData() + VdvSignatureRemainder::Offset); + if (!sigRemainder->isValid() || VdvSignatureRemainder::Offset + sigRemainder->size() + sizeof(VdvCvCertificate) > (unsigned)data.size()) { + qWarning() << "Invalid VDV signature remainder."; + return; + } + qDebug() << sigRemainder->contentSize; + + const auto cvCertOffset = VdvSignatureRemainder::Offset + sigRemainder->size(); + const auto cvCert = reinterpret_cast(data.constData() + cvCertOffset); + if (!cvCert->isValid() || cvCertOffset + cvCert->size() + sizeof(VdvCAReference) > (unsigned)data.size()) { + qWarning() << "Invalid CV signature."; + return; + } + qDebug() << cvCert->contentSize(); + + const auto carOffset = cvCertOffset + cvCert->size(); + const auto car = reinterpret_cast(data.constData() + carOffset); + if (!car->isValid()) { + qWarning() << "Invalid CA Reference."; + return; + } + qDebug() << QByteArray(car->name, 3) << car->serviceIndicator << car->discretionaryData << car->algorithmReference << car->year; + + // (2) decode the CV certificate + // TODO + + // (3) decode the ticket data using the decoded CV certificate + // TODO + + // (4) profit! + // TODO +} + +bool VdvTicketParser::maybeVdvTicket(const QByteArray& data) +{ + if (data.size() < 352) { + return false; + } + + // signature header + if ((uint8_t)data[0] != TagSignature || (uint8_t)data[1] != 0x81 || (uint8_t)data[2] != 0x80 || (uint8_t)data[VdvSignatureRemainder::Offset] != TagSignatureRemainder) { + return false; + } + + const uint8_t len = data[132]; // length of the 0x9A unsigned data block + if (len + 133 > data.size()) { + return false; + } + + // verify the "VDV" marker is there + return strncmp(data.constData() + 133 + len - 5, "VDV", 3) == 0; +} diff --git a/src/vdv/vdvticketparser.h b/src/vdv/vdvticketparser.h new file mode 100644 index 0000000..038aad9 --- /dev/null +++ b/src/vdv/vdvticketparser.h @@ -0,0 +1,56 @@ +/* + Copyright (C) 2019 Volker Krause + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU Library General Public License as published by + the Free Software Foundation; either version 2 of the License, or (at your + option) any later version. + + This program 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 Library General Public + License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef KITINERARY_VDVTICKETPARSER_H +#define KITINERARY_VDVTICKETPARSER_H + +#include "kitinerary_export.h" + +class QByteArray; + +namespace KItinerary { + +/** Parser for VDV tickets. + * Or more correctly for: "Statische Berechtigungen der VDV-Kernapplikation" + * That is, a standard for 2D barcode tickets for local public transport, commonly found in Germany + * and some neighbouring countries. + * + * This is based on "VDV-Kernapplikation - Spezifikation statischer Berechtigungen für 2D Barcode-Tickets" + * which your favorite search engine should find as a PDF. + * + * The crypto stuff used here is ISO 9796-2, and you'll find some terminology also used in ISO 7816-6/8, + * which isn't entirely surprising given this also exists in a NFC card variant. + * + * Do not use directly, only installed for use in tooling. + */ +class KITINERARY_EXPORT VdvTicketParser +{ +public: + VdvTicketParser(); + ~VdvTicketParser(); + + void parse(const QByteArray &data); + + /** Fast check if @p data might contain a VDV ticket. + * Does not perform full decoding, mainly useful for content auto-detection. + */ + static bool maybeVdvTicket(const QByteArray &data); +}; + +} + +#endif // KITINERARY_VDVTICKETPARSER_H