Jump to content

KDE PIM/KItinerary/SBB Barcode: Difference between revisions

From KDE Community Wiki
Vkrause (talk | contribs)
No edit summary
Vkrause (talk | contribs)
No edit summary
 
(2 intermediate revisions by 2 users not shown)
Line 3: Line 3:
* The QR code content is encoded using protobuf.
* The QR code content is encoded using protobuf.
* There is a trailing signature, unclear whether that is part of the protobuf structure or just concatenated.
* There is a trailing signature, unclear whether that is part of the protobuf structure or just concatenated.
You can find Org IDs and some Article Numbers at https://www.allianceswisspass.ch/de/tarife-vorschriften/uebersicht under "Anwendungsbereich" and "Sortiment automatisches Ticketing".


Protobuf structure based on running existing samples through the generic protobuf decoder (names are therefore all guesses or placeholders):
Protobuf structure based on running existing samples through the generic protobuf decoder (names are therefore all guesses or placeholders):


<pre>
<pre>
syntax = "proto3";
message Time {
message Time {
     uint64 msecsSinceEpoch = 1;
     uint64 msecsSinceEpoch = 1;
Line 12: Line 16:


message Block1_2_1 {
message Block1_2_1 {
     varint _1 = 1; // "4" in all samples from SBB, "2" in "Geneva Transport Card", "1" for "Veloplatzreservierung"
     uint64 _1 = 1; // "4" in all samples from SBB, "2" in "Geneva Transport Card", "1" for "Veloplatzreservierung"
     string tariffName = 2; // "Point-to-point Ticket", "Supersaver Ticket"  
     string tariffName = 2; // "Point-to-point Ticket", "Supersaver Ticket"  
}
}


message Block1_2_13 {
message ZoneInformation {
     varint _2 = 2; // numeric zone id?
     uint64 zone = 2;
     varint _3 = 3;
     uint64 zoneOrg = 3;
}
}


Line 25: Line 29:
     optional string departureStation = 2; // missing for zoned tickets
     optional string departureStation = 2; // missing for zoned tickets
     optional string arrivalStation = 3;
     optional string arrivalStation = 3;
     varint classOfTransport = 4;
     uint64 classOfTransport = 4;
     varint _5 = 5;
     optional uint64 _5 = 5;
     string via = 6; // or covered zone in case of zoned tickets
     optional string via = 6; // or covered zone in case of zoned tickets
     varint _7 = 7:
     uint64 _7 = 7;
     Time departureTime = 8; // or valid from for unbound tickets
     Time departureTime = 8; // or valid from for unbound tickets
     Time arrivalTime = 9; // or valid until for unbound tickets
     Time arrivalTime = 9; // or valid until for unbound tickets
     uint articleNumber = 12;
     uint64 articleNumber = 12;
     optional Block1_2_13 _13 = 13; // present for zoned tickets
     optional ZoneInformation zoneInfo = 13; // present for zoned tickets
     varint _14 = 14;
     uint64 _14 = 14;
     string tariff = 15; // parenthesis enclosed abbreviations, string is also printed on the ticket
     string tariff = 15; // parenthesis enclosed abbreviations, string is also printed on the ticket
     varint _17 = 17;
     uint64 _17 = 17;
     varint _19 = 19;
     uint64 _19 = 19;
     varint _20 = 20;
     uint64 _20 = 20;
}
}


Line 46: Line 50:
     string lastName = 4;
     string lastName = 4;
     Time birthDay = 5;
     Time birthDay = 5;
     string discountProgram = 7; // "HALBTAX"
     string passengerTariff = 7; // "HALBTAX"/"PERSON_16+"/"PERSON_25+"
}
 
message Block1_4 {
    // never observed with content so far, only observed present but empty in "Geneva Transport Card"
}
}


message Block1_5 {
message TicketIssuer {
     Time issueTime = 1;
     Time issueTime = 1;
     varint _2 = 2;
     optional uint64 _2 = 2;
     varint _3 = 3;
     optional uint64 _3 = 3;
     varint _4 = 4;
     uint64 issuingOrg = 4;
}
 
message Block1_7 {
    optional string _2 = 2; // "Ou: Nb adultes = 1, Nb enfants = 0" - free text group validity info? only observed in "Geneva Transport Card" so far
}
}


message PaymentData {
message PaymentData {
     string paymentMethod = 1; // "PCD", "MC", "VIS"  
     string paymentMethod = 1; // "PCD", "MC", "VIS", "FAK"  
     string currency = 2; // "CHF"
     string currency = 2; // "CHF"
     string price = 3;
     string price = 3;
Line 64: Line 76:
message TrainData {
message TrainData {
     string trainName = 11;
     string trainName = 11;
     string coach = 12;
     optional string coach = 12;
     string seat = 13; // or "space" for bike reservation
     optional string seat = 13; // or "space" for bike reservation
     varint _14 = 14;
     uint64 _14 = 14;
     varint _15 = 15;
     uint64 _15 = 15;
}
}


message Block1 {
message TicketData {
     uint64 ticketId = 1;
     uint64 ticketId = 1;
     TripData tripData = 2;
     TripData tripData = 2;
     TravelerData traveler = 3;
     TravelerData traveler = 3;
     Block1_5 _5 = 5;
     optional Block1_4 _4 = 4;
     PaymentData payment = 6;
    TicketIssuer ticketIssuer = 5;
     string _7 = 7;
     PaymentData payment = 6; // can be empty but present, probably means all entries in PaymentData are optional rather than this field being optional?
     optional repeated TrainData trainData = 8; // not present in unbound tickets
     Block1_7 _7 = 7;
     varint _9 = 9;
     repeated TrainData trainData = 8; // not present in unbound tickets
     uint64 trainDataCount = 9; // number of entries in field 8
}
}


message Block2 {
message Block2 {
     varint _1 = 1;
     uint64 _1 = 1;
}
 
message SigningKey { // maybe signing key selector??
    string rics = 1; // 4 digit number (3342 in all samples, RICS of Verband öffentlicher Verkehr)
    string keyId = 2; // 5 digit number (00001 in all samples)
}
}


message Block4 { // maybe signing key selector??
message Signature {
     string _1 = 1; // 4 digit number (3342 in all samples)
     repeated bytes signature = 0; // probably concatenated r and s of DSA1024 signature
     string _2 = 2; // 5 digit number (00001 in all samples)
     uint64 _6 = 6;
}
}


message Ticket { // the top-level element
message Ticket { // the top-level element
     Block1 _1 = 1;
     TicketData ticketData = 1;
     Block2 _2 = 2;
     Block2 _2 = 2;
     Block4 _4 = 4;
     SigningKey signingKey = 4;
     Block5 _5 = 5; // signature, might be mis-decoded/mis-detected
     Signature signature = 5;
}
}
</pre>
</pre>

Latest revision as of 15:54, 10 January 2025

Ticket Structure

  • The QR code content is encoded using protobuf.
  • There is a trailing signature, unclear whether that is part of the protobuf structure or just concatenated.

You can find Org IDs and some Article Numbers at https://www.allianceswisspass.ch/de/tarife-vorschriften/uebersicht under "Anwendungsbereich" and "Sortiment automatisches Ticketing".

Protobuf structure based on running existing samples through the generic protobuf decoder (names are therefore all guesses or placeholders):

syntax = "proto3";

message Time {
    uint64 msecsSinceEpoch = 1;
}

message Block1_2_1 {
    uint64 _1 = 1; // "4" in all samples from SBB, "2" in "Geneva Transport Card", "1" for "Veloplatzreservierung"
    string tariffName = 2; // "Point-to-point Ticket", "Supersaver Ticket" 
}

message ZoneInformation {
    uint64 zone = 2;
    uint64 zoneOrg = 3;
}

message TripData {
    Block1_2_1 _1 = 1;
    optional string departureStation = 2; // missing for zoned tickets
    optional string arrivalStation = 3;
    uint64 classOfTransport = 4;
    optional uint64 _5 = 5;
    optional string via = 6; // or covered zone in case of zoned tickets
    uint64 _7 = 7;
    Time departureTime = 8; // or valid from for unbound tickets
    Time arrivalTime = 9; // or valid until for unbound tickets
    uint64 articleNumber = 12;
    optional ZoneInformation zoneInfo = 13; // present for zoned tickets
    uint64 _14 = 14;
    string tariff = 15; // parenthesis enclosed abbreviations, string is also printed on the ticket
    uint64 _17 = 17;
    uint64 _19 = 19;
    uint64 _20 = 20;
}

message TravelerData {
    string sbbCustomerId = 1; // only the first 8 digits (of 10) though 
    string customerId = 2; // UUID, field tkid of https://www.swisspass.ch/private/api/benutzer/v1/benutzer
    string firstName = 3;
    string lastName = 4;
    Time birthDay = 5;
    string passengerTariff = 7; // "HALBTAX"/"PERSON_16+"/"PERSON_25+"
}

message Block1_4 {
    // never observed with content so far, only observed present but empty in "Geneva Transport Card"
}

message TicketIssuer {
    Time issueTime = 1;
    optional uint64 _2 = 2;
    optional uint64 _3 = 3;
    uint64 issuingOrg = 4;
}

message Block1_7 {
    optional string _2 = 2; // "Ou: Nb adultes = 1, Nb enfants = 0" - free text group validity info? only observed in "Geneva Transport Card" so far
}

message PaymentData {
    string paymentMethod = 1; // "PCD", "MC", "VIS", "FAK" 
    string currency = 2; // "CHF"
    string price = 3;
}

message TrainData {
    string trainName = 11;
    optional string coach = 12;
    optional string seat = 13; // or "space" for bike reservation
    uint64 _14 = 14;
    uint64 _15 = 15;
}

message TicketData {
    uint64 ticketId = 1;
    TripData tripData = 2;
    TravelerData traveler = 3;
    optional Block1_4 _4 = 4;
    TicketIssuer ticketIssuer = 5;
    PaymentData payment = 6; // can be empty but present, probably means all entries in PaymentData are optional rather than this field being optional?
    Block1_7 _7 = 7;
    repeated TrainData trainData = 8; // not present in unbound tickets
    uint64 trainDataCount = 9; // number of entries in field 8
}

message Block2 {
    uint64 _1 = 1;
}

message SigningKey { // maybe signing key selector??
    string rics = 1; // 4 digit number (3342 in all samples, RICS of Verband öffentlicher Verkehr) 
    string keyId = 2; // 5 digit number (00001 in all samples) 
}

message Signature {
    repeated bytes signature = 0; // probably concatenated r and s of DSA1024 signature
    uint64 _6 = 6;
}

message Ticket { // the top-level element
    TicketData ticketData = 1;
    Block2 _2 = 2;
    SigningKey signingKey = 4;
    Signature signature = 5;
}

General Observations (obsolete)

  • QR code, content has variable length at around 330 bytes.
  • Contains readable strings as well as binary parts.
  • Seems to be a sequence of variable length records, rather than a fixed binary layout, could be some form of TLV encoding (there are some similarities to ASN.1 BER/DER for example).
  • Records consist of a 1 byte type field, a length field and N content bytes.
  • The type field does not seem to describe semantics, as values repeat for different values (rather than a data type of some form?). This would suggest that the record order defines their semantics. The presence of null records suggests that too, as well as the same record sequence in all samples.
  • "European Union Agency For Railways - Technical Document - Digital Security Elements For Rail Passenger Ticketing - TAP TSI TD B.12 - §11 FCB - Flexible Content Barcode" describes something that seems similar, using ASN.1 UPER encoding.

Record Structure (obsolete)

  • 1 byte type, although no idea what that indicates. Largely doesn't match the BER/DER type byte.
  • Length:
    • for values <= 127 byte this is just one byte
    • for value larger than 127 this is encoded in two bytes. This is quite different from BER/DER multi-byte length encoding however. It looks like a little endian layout , with the most significant bit of the first length byte being removed and the second byte being shifted by one bit to fill that space.
  • N bytes of content, with varying data types.

Data Types

Strings

  • Strings seem to be UTF-8 encoded.
  • The length is the amount of bytes needed to represent the UTF-8 string, not the amount of characters.

Date/Time

This is largely speculation at this point!

There's 5 7 byte sequences included that could be date/time values. These could be date of purchase/issue, begin of validity, end of validity, traveler birth date. All of those are printed on the ticket.

  • the first byte is 0x08 in all samples
  • the first half of the second byte seems to be 0x8 for date fields and 0xC for date/time fields. This would seem consistent with optional ASN.1 UPER field encoding.
  • there's a surprising amount of entropy in those values, esp. when looking at differences at close-by dates.
  • differential view shows no obvious correlation beyond multiple tickets, but close-by values do "look" similar nevertheless.
  • the suspected birthday field is the same in all samples for the same traveler
  • this does not seem to use a UNIX timestamp
  • this does not seem to use BCD encoding
  • this does not seem to use sub-byte per-component encoding

Record Sequence (obsolete)

Nesting Depth Type Id Content Type Meaning Notes
0 0x0A date/time, nested record ? speculative, given the 7 byte field there matches suspected date/time values below
1 0x12 nested record
2 0x0A 2bytes followed by nested records ? 0x08 04
3 0x12 string ticket type? "Point-to-point Ticket", "Supersaver Ticket"
2 0x12 string Departure station
2 0x1A string Arrival station
2 0x20 2 byte ? 0x2801 in all samples
2 0x32 string Via
2 0x38 1 byte ? 0x42 in all samples
2 0x42 7 byte ? date/time?
2 0x4A 7 byte ? date/time?
2 0x60 variable, null terminated ? 2-3 bytes in all samples, breaks TLV structure
2 0x7A string ticket type or tariff parenthesis enclosed abbreviations, string is also printed on the ticket
2 0x88 1 byte ? null, optional
2 0x98 1 byte ? 0x02
2 0xA0 1 byte ? null
1 0x1A nested record traveler information could also be loyalty program info?
2 0x0A string SBB customer id only the first 8 digits (of 10) though
2 0x12 string Customer identifier 128 bit as a 36 byte hex string with for separator dashes (uuid-like formatting) Field tkid of https://www.swisspass.ch/private/api/benutzer/v1/benutzer
2 0x1A string family name
2 0x22 string given name
2 0x2A 7 byte ? date/time? - possibly traveler birth date
2 0x3A string tariff information? "HALBTAX"
1 0x2A nested record ?
2 0x0A 7 byte ? date/time?
2 0x10 4 byte ? followed by 0x200B outside of TLV structures?
1 0x32 nested record price information?
2 0x0A string payment method? "PCD", "MC", "VIS"
2 0x12 string currency "CHF"
2 0x1A string ticket price
1 0x3A null ?
1 0x42 nested record train information not present in unbound tickets
2 0x5A string train number
2 0x70 null
2 0x78 null
1 0x48 1 byte, no length byte!?! ?
0 0x12 2 byte ? fixed 0x0801
0 0x22 nested record ?
1 0x0A string ? 4 digit number (3342 in all samples)
1 0x12 string ? 5 digit number (00001 in all samples)
1 0x2A nested record
2 0x30 nested record DSA signature in ASN.1/BER encoding, as found in UIC 918.3 containers as well
3 0x02 20-21 byte r DSA signature r value
3 0x02 20-21 byte s DSA signature s value