Commit 666f7317 authored by Volker Krause's avatar Volker Krause
Browse files

Initial work on decoding VDV tickets

Those are common among German local transport providers, and visually look
like UIC 918.3 codes. Unfortunately the description of what they are valid
for is entangled with their cryptographic signatures, which makes it
necessary to deal with all that here as well to get to the information we
are interested in. I only have partial documentation for this, so this
might take a while...
parent 3655f7ef
......@@ -12,6 +12,7 @@ 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)
......
/*
Copyright (C) 2019 Volker Krause <vkrause@kde.org>
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 <https://www.gnu.org/licenses/>.
*/
#include <vdv/vdvticketparser.h>
#include <QDebug>
#include <QObject>
#include <QTest>
using namespace KItinerary;
class VdvTicketTest : public QObject
{
Q_OBJECT
private Q_SLOTS:
void testMaybeVdvTicket_data()
{
QTest::addColumn<QByteArray>("input");
QTest::addColumn<bool>("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"
......@@ -54,6 +54,8 @@ set(kitinerary_lib_srcs
uic9183/uic9183ticketlayout.cpp
uic9183/vendor0080block.cpp
vdv/vdvticketparser.cpp
barcodedecoder.cpp
calendarhandler.cpp
documentutil.cpp
......@@ -196,6 +198,14 @@ ecm_generate_headers(KItinerary_Uic9183_FORWARDING_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}
......@@ -203,6 +213,7 @@ install(FILES
${KItinerary_Datatypes_FORWARDING_HEADERS}
${KItinerary_Pdf_FORWARDING_HEADERS}
${KItinerary_Uic9183_FORWARDING_HEADERS}
${KItinerary_Vdv_FORWARDING_HEADERS}
DESTINATION ${KDE_INSTALL_INCLUDEDIR_PIM}/KItinerary
)
install(FILES
......@@ -212,6 +223,7 @@ install(FILES
${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
)
......
/*
Copyright (C) 2019 Volker Krause <vkrause@kde.org>
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 <https://www.gnu.org/licenses/>.
*/
#ifndef KITINERARY_VDVDATA_P_H
#define KITINERARY_VDVDATA_P_H
#include <QtEndian>
#include <cstdint>
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
/*
Copyright (C) 2019 Volker Krause <vkrause@kde.org>
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 <https://www.gnu.org/licenses/>.
*/
#include "vdvticketparser.h"
#include "vdvdata_p.h"
#include <QByteArray>
#include <QDebug>
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<const VdvSignatureRemainder*>(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<const VdvCvCertificate*>(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<const VdvCAReference*>(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;
}
/*
Copyright (C) 2019 Volker Krause <vkrause@kde.org>
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 <https://www.gnu.org/licenses/>.
*/
#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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment