diff --git a/autotests/vdvtickettest.cpp b/autotests/vdvtickettest.cpp index afbbb8d..4b09aaa 100644 --- a/autotests/vdvtickettest.cpp +++ b/autotests/vdvtickettest.cpp @@ -1,60 +1,70 @@ /* 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); } + + void testTicket() + { + const auto data = QByteArray::fromHex("00f569d018f111d017d43b7d68003b8268008532da110000030000000000000992000000000000db0c000000000000000000000000dc0f1117d40000000004b000000000000018f110007a18f13b7974cfd400000017d48a00000105d601000105d602474200000000005644561400"); + + VdvTicket ticket(data); + QCOMPARE(ticket.issuerId(), 6385); + QCOMPARE(ticket.beginDateTime(), QDateTime({2019, 11, 29}, {13, 0})); + QCOMPARE(ticket.endDateTime(), QDateTime({2019, 12, 2}, {13, 0})); + } }; QTEST_APPLESS_MAIN(VdvTicketTest) #include "vdvtickettest.moc" diff --git a/src/vdv/vdvdata_p.h b/src/vdv/vdvdata_p.h index 9dcf4b5..c8e491b 100644 --- a/src/vdv/vdvdata_p.h +++ b/src/vdv/vdvdata_p.h @@ -1,272 +1,310 @@ /* 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, TagOneByteSize = 0x81, TagTwoByteSize = 0x82, + TagTicketProductData = 0x85, + TagTicketProductTransactionData = 0x8A, }; enum : uint16_t { TagCertificate = 0x7F21, TagCertificateSignature = 0x5F37, TagCertificateSignatureRemainder = 0x5F38, TagCertificateContent = 0x5F4E, }; +enum { + MinimumTicketDataSize = 111, +}; + #pragma pack(push) #pragma pack(1) /** Generic structure for the header of data blocks in VDV binary data. * This consits of: * - a one or two byte tag (@tparam TagType) with a fixed value (@tparam TagValue) * - a one byte field indicating the size of the size field (optional) * - one or two bytes for the size * - followed by size bytes of content */ template struct VdvAbstractDataBlock { TagType tag; inline bool isValid() const { return qFromBigEndian(tag) == TagValue; } }; template struct VdvSimpleDataBlock : public VdvAbstractDataBlock { uint8_t size0; inline uint16_t contentSize() const { return size0; } inline uint16_t contentOffset() const { return sizeof(VdvSimpleDataBlock); } inline const uint8_t* contentData() const { return reinterpret_cast(this) + contentOffset(); } inline uint16_t size() const { return contentSize() + contentOffset(); } template inline const T* contentAt(int offset) const { return reinterpret_cast(contentData() + offset); } }; template struct VdvTaggedSizeDataBlock : public VdvAbstractDataBlock { uint8_t sizeTag; uint8_t size0; uint8_t size1; inline bool isValid() const { return VdvAbstractDataBlock::isValid() && (sizeTag == TagOneByteSize || sizeTag == TagTwoByteSize); } inline uint16_t contentSize() const { return sizeTag == TagOneByteSize ? size0 : ((size0 << 8) + size1); } inline uint16_t contentOffset() const { return sizeof(VdvTaggedSizeDataBlock) - ((sizeTag == TagOneByteSize) ? 1 : 0); } inline const uint8_t* contentData() const { return reinterpret_cast(this) + contentOffset(); } inline uint16_t size() const { return contentSize() + contentOffset(); } template inline const T* contentAt(int offset) const { return reinterpret_cast(contentData() + offset); } }; /** Two-digit BCD encoded number. */ struct VdvBcdNumber { uint8_t data; uint8_t value() const { return ((data & 0xF0) >> 4) * 10 + (data & 0x0F); } }; /** Date encoded as 8 BCD digits. */ struct VdvBcdDate { VdvBcdNumber bcdYear[2]; VdvBcdNumber bcdMonth; VdvBcdNumber bcdDay; inline uint16_t year() const { return bcdYear[0].value() * 100 + bcdYear[1].value(); } inline uint8_t month() const { return bcdMonth.value(); } inline uint8_t day() const { return bcdDay.value(); } }; /** Signature container for the signed part of the payload data. */ struct VdvSignature : public VdvTaggedSizeDataBlock {}; /** Signature Remainder header. */ struct VdvSignatureRemainder : public VdvSimpleDataBlock {}; /** CV certificate. */ struct VdvCertificateHeader : public VdvTaggedSizeDataBlock {}; /** Certificate Authority Reference (CAR) content. */ struct VdvCaReference { char region[2]; char name[3]; uint8_t serviceIndicator: 4; uint8_t discretionaryData: 4; uint8_t algorithmReference; uint8_t year; }; struct VdvCaReferenceBlock : public VdvSimpleDataBlock {}; /** Certificate Holder Reference (CHR) */ struct VdvCertificateHolderReference { uint8_t filler[4]; // always null char name[5]; uint8_t serviceIndicator: 4; uint8_t discretionaryData: 4; uint8_t algorithmReference; uint8_t year; }; /** Certificate Holder Authorization (CHA) */ struct VdvCertificateHolderAuthorization { char name[6]; uint8_t stuff; }; /** Certificate key, contained in a certificate object. */ struct VdvCertificateKey { uint8_t certificateProfileIdentifier; VdvCaReference car; VdvCertificateHolderReference chr; VdvCertificateHolderAuthorization cha; VdvBcdDate date; uint8_t oidBegin; inline uint8_t oidSize() const { return oidBegin == 0x2a ? 9 : 7; // ugly, but works for now } inline uint8_t headerSize() const { return sizeof(VdvCertificateKey) + oidSize() - 1; } }; struct VdvCertificateKeyBlock : public VdvTaggedSizeDataBlock {}; /** Certificate signature. */ struct VdvCertificateSignature : public VdvTaggedSizeDataBlock {}; /** Certificate signature remainder. */ struct VdvCertificateSignatureRemainder : public VdvSimpleDataBlock {}; /** Date/time representation encoded in 4 byte. */ struct VdvDateTimeCompact { uint32_t data; inline int year() const { return ((qFromBigEndian(data) & 0b1111'1110'0000'0000'0000'0000'0000'0000) >> 25) + 1990; } inline int month() const { return (qFromBigEndian(data) & 0b0000'0001'1110'0000'0000'0000'0000'0000) >> 21; } inline int day() const { return (qFromBigEndian(data) & 0b0000'0000'0001'1111'0000'0000'0000'0000) >> 16; } inline int hour() const { return (qFromBigEndian(data) & 0b0000'0000'0000'0000'1111'1000'0000'0000) >> 11; } inline int minute() const { return (qFromBigEndian(data) & 0b0000'0000'0000'0000'0000'0111'1110'0000) >> 5; } inline int second() const { return (qFromBigEndian(data) & 0b0000'0000'0000'0000'0000'0000'0001'1111) * 2; } }; /** Ticket data header. */ struct VdvTicketHeader { uint32_t ticketId; uint16_t kvpOrgId; uint16_t productId; uint16_t pvOrgId; VdvDateTimeCompact beginDt; VdvDateTimeCompact endDt; }; +/** Product-specific ticket data block. + * Contains a set of TLV elements. + */ +struct VdvTicketProductData : public VdvSimpleDataBlock {}; + +/** Ticket transaction data block. */ +struct VdvTicketTransactionData +{ + uint16_t kvpOrgId; + uint8_t terminalId[5]; + VdvDateTimeCompact dt; + uint8_t locationId[6]; +}; +/** Product-specific transaction data block (variable length). */ +struct VdvTicketProductTransactionData : public VdvSimpleDataBlock {}; + +/** Ticket issuer data block. */ +struct VdvTicketIssueData +{ + uint32_t samSeq1; + uint8_t version; + uint32_t samSeq2; + uint8_t samId[3]; +}; + +/** Ticket trailer, after padding. */ +struct VdvTicketTrailer +{ + const char identifier[3]; + uint16_t version; +}; + #pragma pack(pop) } #endif // KITINERARY_VDVDATA_P_H diff --git a/src/vdv/vdvticket.cpp b/src/vdv/vdvticket.cpp index e45e5d8..3151328 100644 --- a/src/vdv/vdvticket.cpp +++ b/src/vdv/vdvticket.cpp @@ -1,89 +1,135 @@ /* 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 "vdvticket.h" #include "vdvdata_p.h" +#include "logging.h" #include using namespace KItinerary; namespace KItinerary { class VdvTicketPrivate : public QSharedData { public: QByteArray m_data; }; } VdvTicket::VdvTicket() : d(new VdvTicketPrivate) { } VdvTicket::VdvTicket(const QByteArray &data) : d(new VdvTicketPrivate) { - qDebug() << data.toHex() << data.size(); - if ((unsigned)data.size() < sizeof(VdvTicketHeader)) { - qWarning() << "Ticket data too small"; + if (data.size() < MinimumTicketDataSize) { + qCWarning(Log) << "Ticket data too small" << data.size(); return; } + static_assert(sizeof(VdvTicketHeader) < MinimumTicketDataSize, ""); + const auto hdr = reinterpret_cast(data.constData()); + int offset = sizeof(VdvTicketHeader); + + const auto productBlock = reinterpret_cast(data.constData() + offset); + if (!productBlock->isValid() || productBlock->size() + offset > data.size()) { + qCWarning(Log) << "Invalid product block" << productBlock->isValid() << productBlock->size() << offset << data.size(); + return; + } + offset += productBlock->size(); + + const auto transactionBlock = reinterpret_cast(data.constData() + offset); + qDebug() << "transaction block:" << qFromBigEndian(transactionBlock->kvpOrgId); + offset += sizeof(VdvTicketTransactionData); + + const auto prodTransactionBlock = reinterpret_cast(data.constData() + offset); + if (!prodTransactionBlock->isValid() || prodTransactionBlock->size() + offset > data.size()) { + qCWarning(Log) << "Invalid product transaction block" << prodTransactionBlock->isValid() << prodTransactionBlock->size() << offset << data.size(); + return; + } + offset += prodTransactionBlock->size(); + + const auto issueData = reinterpret_cast(data.constData() + offset); + qDebug() << issueData->version << QByteArray((const char*)&issueData->samId, 3).toHex(); + offset += sizeof(VdvTicketIssueData); + + qDebug() << "padding:" << std::max(111 - offset, 0); + offset += std::max(111 - offset - (int)sizeof(VdvTicketTrailer), 0); + + const auto trailer = reinterpret_cast(data.constData() + offset); + qDebug() << QByteArray(trailer->identifier, 3) << qFromBigEndian(trailer->version); + if (memcmp(trailer->identifier, "VDV", 3) != 0) { + qCWarning(Log) << "Invalid ticket trailer identifier."; + return; + } d->m_data = data; - const auto hdr = reinterpret_cast(d->m_data.constData()); - qDebug() << qFromBigEndian(hdr->ticketId) << qFromBigEndian(hdr->kvpOrgId) << qFromBigEndian(hdr->productId) << qFromBigEndian(hdr->pvOrgId); + + // TODO temporary + qDebug() << qFromBigEndian(hdr->ticketId) << issuerId() << qFromBigEndian(hdr->productId) << qFromBigEndian(hdr->pvOrgId); qDebug() << "begin:" << beginDateTime(); qDebug() << "end:" << endDateTime(); - - // iterate over TLV blocks - int offset = sizeof(VdvTicketHeader); - while (offset < d->m_data.size() - 1) { - qDebug() << "tag:" << (uint8_t)d->m_data[offset] << "size:" << (uint8_t)d->m_data[offset + 1] << "remaining:" << (d->m_data.size() - offset - (uint8_t)d->m_data[offset + 1]); - offset += (uint8_t)d->m_data[offset + 1] + 2; + // iterate over TLV content + int tlvOff = 0; + while (tlvOff < productBlock->contentSize()) { + const auto tlvSize = (uint8_t)productBlock->contentData()[tlvOff + 1]; + qDebug() << "tag:" << (uint8_t)productBlock->contentData()[tlvOff] << "size:" << tlvSize << "content:" << QByteArray((const char*)productBlock->contentData() + tlvOff + 2, tlvSize).toHex(); + tlvOff += tlvSize + 2; } } VdvTicket::VdvTicket(const VdvTicket&) = default; VdvTicket::~VdvTicket() = default; VdvTicket& VdvTicket::operator=(const VdvTicket&) = default; static QDateTime dtCompactToQdt(const VdvDateTimeCompact &dtc) { return QDateTime({dtc.year(), dtc.month(), dtc.day()}, {dtc.hour(), dtc.minute(), dtc.second()}); } QDateTime VdvTicket::beginDateTime() const { if (d->m_data.isEmpty()) { return {}; } const auto hdr = reinterpret_cast(d->m_data.constData()); return dtCompactToQdt(hdr->beginDt); } QDateTime KItinerary::VdvTicket::endDateTime() const { if (d->m_data.isEmpty()) { return {}; } const auto hdr = reinterpret_cast(d->m_data.constData()); return dtCompactToQdt(hdr->endDt); } + +int VdvTicket::issuerId() const +{ + if (d->m_data.isEmpty()) { + return 0; + } + + const auto hdr = reinterpret_cast(d->m_data.constData()); + return qFromBigEndian(hdr->kvpOrgId); +} diff --git a/src/vdv/vdvticket.h b/src/vdv/vdvticket.h index d31ad85..e75f624 100644 --- a/src/vdv/vdvticket.h +++ b/src/vdv/vdvticket.h @@ -1,60 +1,64 @@ /* 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_VDVTICKET_H #define KITINERARY_VDVTICKET_H #include "kitinerary_export.h" #include #include #include namespace KItinerary { class VdvTicketPrivate; /** Ticket information from a VDV barcode. * For use by tooling or custom extractor scripts. */ class KITINERARY_EXPORT VdvTicket { Q_GADGET /** Begin of the validitiy of this ticket. */ Q_PROPERTY(QDateTime beginDateTime READ beginDateTime) /** End of the validity of this ticket. */ Q_PROPERTY(QDateTime endDateTime READ endDateTime) + /** VDV organization identifier of the ticket issuer. */ + Q_PROPERTY(int issuerId READ issuerId) + public: VdvTicket(); VdvTicket(const QByteArray &data); VdvTicket(const VdvTicket&); ~VdvTicket(); VdvTicket& operator=(const VdvTicket&); QDateTime beginDateTime() const; QDateTime endDateTime() const; + int issuerId() const; private: QExplicitlySharedDataPointer d; }; } Q_DECLARE_METATYPE(KItinerary::VdvTicket) #endif // KITINERARY_VDVTICKET_H diff --git a/src/vdv/vdvticketparser.h b/src/vdv/vdvticketparser.h index 781b9e7..088a93e 100644 --- a/src/vdv/vdvticketparser.h +++ b/src/vdv/vdvticketparser.h @@ -1,64 +1,66 @@ /* 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" #include "vdvticket.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. + * + * @see https://de.wikipedia.org/wiki/VDV-Kernapplikation */ class KITINERARY_EXPORT VdvTicketParser { public: VdvTicketParser(); ~VdvTicketParser(); /** Tries to parse the ticket in @p data. */ bool parse(const QByteArray &data); /** Returns the parsed ticket data. */ VdvTicket ticket() const; /** 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); private: VdvTicket m_ticket; }; } #endif // KITINERARY_VDVTICKETPARSER_H