Merge pull request #2593 from Akury83/master
[RRG-proxmark3.git] / client / pyscripts / intertic.py
blob82d39f57f95074ba7b782b19fc2db833af892e40
1 # Benjamin DELPY `gentilkiwi`
2 # https://blog.gentilkiwi.com/
3 # benjamin@gentilkiwi.com
5 # Basic script to try to interpret Intertic data on ST25TB / SRT512 in french transports
6 # For Proxmark3 with love <3
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # See LICENSE.txt for the text of the license.
20 import sys, os
21 from datetime import datetime, timedelta
22 from bitarray import bitarray
23 from bitarray.util import ba2int
24 from typing import NamedTuple
26 class BitMe:
27 def __init__(self):
28 self.data = bitarray(endian = 'big')
29 self.idx = 0
31 def reset(self):
32 self.idx = 0
34 def addBits(self, bits):
35 self.data += bits
37 def addBytes(self, bytes):
38 self.data.frombytes(bytes)
40 def nom_bits(self, cb):
41 ret = self.data[self.idx:self.idx + cb]
42 self.idx += cb
43 return ret
45 def nom(self, cb):
46 return ba2int(self.nom_bits(cb))
48 def nom_bits_left(self):
49 return self.data[self.idx:None]
51 def isEmpty(self):
52 return (len(self.data) == 0)
54 '''
55 A generic Describe_Usage function with variable number of bits between stamps will be more optimal
56 At this time I want to keep more places/functions to try to parse other fields in 'unk1' and 'left'
57 '''
59 TYPE_EventCode_Nature = {
60 0x1: 'urban bus',
61 0x2: 'interurban bus',
62 0x3: 'metro',
63 0x4: 'tramway',
64 0x5: 'train',
65 0x8: 'parking',
68 TYPE_EventCode_Type = {
69 0x1: 'entry validation',
70 0x2: 'exit validation',
71 0x4: 'ticket inspecting',
72 0x6: 'connection entry validation',
73 0x14: 'test validation',
74 0x15: 'connection exit validation',
75 0x16: 'canceled validation',
76 0x17: 'invalidation',
77 0x18: 'distribution',
80 TYPE_EventGeoRoute_Direction = {
81 0: 'undefined',
82 1: 'outward',
83 2: 'inward',
84 3: 'circular',
87 def Describe_Usage_1(Usage, ContractMediumEndDate, Certificate):
88 EventDateStamp = Usage.nom(10)
89 EventTimeStamp = Usage.nom(11)
90 unk = Usage.nom_bits(65)
91 EventValidityTimeFirstStamp = Usage.nom(11)
93 print(' EventDateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
94 print(' EventTimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
95 print(' unk1... :', unk);
96 print(' EventValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
97 print(' left... :', Usage.nom_bits_left());
98 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
100 def Describe_Usage_1_1(Usage, ContractMediumEndDate, Certificate):
101 EventDateStamp = Usage.nom(10)
102 EventTimeStamp = Usage.nom(11)
103 unk0 = Usage.nom_bits(8)
104 EventCode_Nature = Usage.nom(5)
105 EventCode_Type = Usage.nom(5)
106 unk1 = Usage.nom_bits(11)
107 EventGeoVehicleId = Usage.nom(16)
108 EventGeoRouteId = Usage.nom(14)
109 EventGeoRoute_Direction = Usage.nom(2)
110 EventCountPassengers_mb = Usage.nom(4)
111 EventValidityTimeFirstStamp = Usage.nom(11)
113 print(' DateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
114 print(' TimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
115 print(' unk0... :', unk0);
116 print(' Code/Nature : 0x{:x} ({})'.format(EventCode_Nature, TYPE_EventCode_Nature.get(EventCode_Nature, '?')))
117 print(' Code/Type : 0x{:x} ({})'.format(EventCode_Type, TYPE_EventCode_Type.get(EventCode_Type, '?')))
118 print(' unk1... :', unk1);
119 print(' GeoVehicleId : {}'. format(EventGeoVehicleId))
120 print(' GeoRouteId : {}'. format(EventGeoRouteId))
121 print(' Direction : {} ({})'. format(EventGeoRoute_Direction, TYPE_EventGeoRoute_Direction.get(EventGeoRoute_Direction, '?')))
122 print(' Passengers(?) : {}'. format(EventCountPassengers_mb))
123 print(' ValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
124 print(' left... :', Usage.nom_bits_left());
125 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
127 def Describe_Usage_1_2(Usage, ContractMediumEndDate, Certificate):
128 EventDateStamp = Usage.nom(10)
129 EventTimeStamp = Usage.nom(11)
130 EventCount_mb = Usage.nom(6)
131 unk0 = Usage.nom_bits(4)
132 EventCode_Nature_mb = Usage.nom(4)
133 EventCode_Type_mb = Usage.nom(4)
134 unk1 = Usage.nom_bits(11)
135 EventGeoVehicleId = Usage.nom(16)
136 EventGeoRouteId = Usage.nom(14)
137 EventGeoRoute_Direction = Usage.nom(2)
138 EventCountPassengers_mb = Usage.nom(4)
139 EventValidityTimeFirstStamp = Usage.nom(11)
141 TYPE_EventCode_Nature_Reims = { # usually it's the opposite, but ... ?
142 0x4: 'urban bus',
143 0x1: 'tramway',
146 print(' DateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
147 print(' TimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
148 print(' Count(?) : {}'. format(EventCount_mb))
149 print(' unk0... :', unk0);
150 print(' Code/Nature(?) : 0x{:x} ({})'.format(EventCode_Nature_mb, TYPE_EventCode_Nature_Reims.get(EventCode_Nature_mb, '?')))
151 print(' Code/Type(?) : 0x{:x} ({})'.format(EventCode_Type_mb, TYPE_EventCode_Type.get(EventCode_Type_mb, '?')))
152 print(' unk1... :', unk1);
153 print(' GeoVehicleId : {}'. format(EventGeoVehicleId))
154 print(' GeoRouteId : {}'. format(EventGeoRouteId))
155 print(' Direction : {} ({})'. format(EventGeoRoute_Direction, TYPE_EventGeoRoute_Direction.get(EventGeoRoute_Direction, '?')))
156 print(' Passengers(?) : {}'. format(EventCountPassengers_mb))
157 print(' ValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
158 print(' left... :', Usage.nom_bits_left());
159 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
162 def Describe_Usage_2(Usage, ContractMediumEndDate, Certificate):
163 EventDateStamp = Usage.nom(10)
164 EventTimeStamp = Usage.nom(11)
165 unk0 = Usage.nom_bits(8)
166 EventCode_Nature = Usage.nom(5)
167 EventCode_Type = Usage.nom(5)
168 unk1 = Usage.nom_bits(11)
169 EventGeoRouteId = Usage.nom(14)
170 EventGeoRoute_Direction = Usage.nom(2)
171 EventCountPassengers_mb = Usage.nom(4)
172 EventValidityTimeFirstStamp = Usage.nom(11)
174 print(' DateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
175 print(' TimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
176 print(' unk0... :', unk0);
177 print(' Code/Nature : 0x{:x} ({})'.format(EventCode_Nature, TYPE_EventCode_Nature.get(EventCode_Nature, '?')))
178 print(' Code/Type : 0x{:x} ({})'.format(EventCode_Type, TYPE_EventCode_Type.get(EventCode_Type, '?')))
179 print(' unk1... :', unk1);
180 print(' GeoRouteId : {}'. format(EventGeoRouteId))
181 print(' Direction : {} ({})'. format(EventGeoRoute_Direction, TYPE_EventGeoRoute_Direction.get(EventGeoRoute_Direction, '?')))
182 print(' Passengers(?) : {}'. format(EventCountPassengers_mb))
183 print(' ValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
184 print(' left... :', Usage.nom_bits_left());
185 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
187 def Describe_Usage_3(Usage, ContractMediumEndDate, Certificate):
188 EventDateStamp = Usage.nom(10)
189 EventTimeStamp = Usage.nom(11)
190 unk = Usage.nom_bits(27)
191 EventValidityTimeFirstStamp = Usage.nom(11)
193 print(' EventDateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
194 print(' EventTimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
195 print(' unk1... :', unk);
196 print(' EventValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
197 print(' left... :', Usage.nom_bits_left());
198 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
200 def Describe_Usage_4(Usage, ContractMediumEndDate, Certificate):
201 EventDateStamp = Usage.nom(10)
202 EventTimeStamp = Usage.nom(11)
203 unk = Usage.nom_bits(63)
204 EventValidityTimeFirstStamp = Usage.nom(11)
206 print(' EventDateStamp : {} ({})'.format(EventDateStamp, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate - EventDateStamp)).strftime('%Y-%m-%d')));
207 print(' EventTimeStamp : {} ({:02d}:{:02d})'. format(EventTimeStamp, EventTimeStamp // 60, EventTimeStamp % 60))
208 print(' unk1... :', unk);
209 print(' EventValidityTimeFirstStamp: {} ({:02d}:{:02d})'. format(EventValidityTimeFirstStamp, EventValidityTimeFirstStamp // 60, EventValidityTimeFirstStamp % 60))
210 print(' left... :', Usage.nom_bits_left());
211 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
213 def Describe_Usage_Generic(Usage, ContractMediumEndDate, Certificate):
214 print(' !!! GENERIC DUMP - please provide full file dump to benjamin@gentilkiwi.com - especially if NOT empty !!!')
215 print(' left... :', Usage.nom_bits_left());
216 print(' [CER] Usage : {:04x}'.format(Certificate.nom(16)))
217 print(' !!! Trying Usage_1 (the most common) !!!')
218 Usage.reset()
219 Certificate.reset()
220 Describe_Usage_1(Usage, ContractMediumEndDate, Certificate)
222 class InterticHelper(NamedTuple):
223 OrganizationalAuthority: str
224 ContractProvider: str
225 UsageDescribeFunction: callable = None
227 ISO_Countries = {
228 0x250: 'France',
231 FRA_OrganizationalAuthority_Contract_Provider = {
232 0x000: {
233 5: InterticHelper('Lille', 'Ilévia / Keolis', Describe_Usage_1_1),
234 7: InterticHelper('Lens-Béthune', 'Tadao / Transdev', Describe_Usage_1_1),
236 0x006: {
237 1: InterticHelper('Amiens', 'Ametis / Keolis'),
239 0x008: {
240 15: InterticHelper('Angoulême', 'STGA', Describe_Usage_1_1), # May have a problem with date ?
242 0x021: {
243 1: InterticHelper('Bordeaux', 'TBM / Keolis', Describe_Usage_1_1),
245 0x057: {
246 1: InterticHelper('Lyon', 'TCL / Keolis', Describe_Usage_1), # Strange usage ?, kept on generic 1
248 0x072: {
249 1: InterticHelper('Tours', 'filbleu / Keolis', Describe_Usage_1_1),
251 0x078: {
252 4: InterticHelper('Reims', 'Citura / Transdev', Describe_Usage_1_2),
254 0x091: {
255 1: InterticHelper('Strasbourg', 'CTS', Describe_Usage_4), # More dump needed, not only tram !
257 0x502: {
258 83: InterticHelper('Annecy', 'Sibra', Describe_Usage_2),
259 10: InterticHelper('Clermont-Ferrand', 'T2C'),
261 0x907: {
262 1: InterticHelper('Dijon', 'Divia / Keolis'),
264 0x908: {
265 1: InterticHelper('Rennes', 'STAR / Keolis', Describe_Usage_2),
266 8: InterticHelper('Saint-Malo', 'MAT / RATP', Describe_Usage_1_1),
268 0x911: {
269 5: InterticHelper('Besançon', 'Ginko / Keolis'),
271 0x912: {
272 3: InterticHelper('Le Havre', 'Lia / Transdev', Describe_Usage_1_1),
273 35: InterticHelper('Cherbourg-en-Cotentin', 'Cap Cotentin / Transdev'),
275 0x913: {
276 3: InterticHelper('Nîmes', 'Tango / Transdev', Describe_Usage_3),
278 0x915: {
279 1: InterticHelper('Metz', 'Le Met\' / TAMM'),
281 0x917: {
282 4: InterticHelper('Angers', 'Irigo / RATP', Describe_Usage_1_2),
283 7: InterticHelper('Saint-Nazaire', 'Stran'),
288 def main():
290 print('Basic script to try to interpret Intertic data on ST25TB / SRT512 in french transports')
291 print('--------------------------------------------------------------------------------------\n')
293 if(len(sys.argv) != 2):
294 print('\tUsage : {0} <dumpfile.bin>\n\tExample: {0} hf-14b-D00233787DFBB4D5-dump.bin\n'.format(sys.argv[0]))
295 return 1
297 binaryDumpFileName = sys.argv[1]
299 data = BitMe()
301 print('Using \'{}\' as binary dump file...'.format(binaryDumpFileName))
302 file = open(binaryDumpFileName, mode='rb')
304 file.seek(0, os.SEEK_END)
305 size = file.tell()
306 file.seek(0, os.SEEK_SET)
308 if (size != 68):
309 print('\'{}\' file size is not 68 bytes'.format(binaryDumpFileName))
310 return 2
312 while True:
313 chunk = file.read(4)
314 if not chunk:
315 break
316 data.addBytes(chunk[::-1])
318 file.close()
320 Distribution_Data = BitMe()
321 Block0Left = BitMe()
322 # Usage_DAT = BitMe()
323 # Usage_CER = BitMe()
324 Usage_A_DAT = BitMe()
325 Usage_A_CER = BitMe()
326 Usage_B_DAT = BitMe()
327 Usage_B_CER = BitMe()
328 Distribution_Cer = BitMe()
330 SWAP = None
331 RELOADING1 = None
332 COUNTER1 = None
333 # RELOADING2 = None
334 # COUNTER2 = None
335 Describe_Usage = None
337 Block0Left.addBits(data.nom_bits(23))
338 KeyId = data.nom(4)
339 PID = data.nom(5)
341 match PID:
343 case 0x10:
344 Distribution_Data.addBits(data.nom_bits(2 * 32))
345 Distribution_Data.addBits(Block0Left.nom_bits_left())
346 Usage_A_DAT.addBits(data.nom_bits(2 * 32))
347 RELOADING1 = data.nom(8)
348 COUNTER1 = data.nom(24)
349 SWAP = data.nom(32)
350 Usage_A_DAT.addBits(data.nom_bits(2 * 32))
351 Usage_A_DAT.addBits(data.nom_bits(16))
352 Usage_A_CER.addBits(data.nom_bits(16))
353 Usage_B_DAT.addBits(data.nom_bits(4 * 32))
354 Usage_B_DAT.addBits(data.nom_bits(16))
355 Usage_B_CER.addBits(data.nom_bits(16))
356 Distribution_Cer.addBits(data.nom_bits(32))
358 case 0x11 | 0x19:
359 Distribution_Data.addBits(data.nom_bits(4 * 32))
360 Distribution_Data.addBits(Block0Left.nom_bits_left())
361 RELOADING1 = data.nom(8)
362 COUNTER1 = data.nom(24)
363 SWAP = data.nom(32)
364 Usage_A_DAT.addBits(data.nom_bits(3 * 32))
365 Usage_A_DAT.addBits(data.nom_bits(16))
366 Usage_A_CER.addBits(data.nom_bits(16))
367 Usage_B_DAT.addBits(data.nom_bits(3 * 32))
368 Usage_B_DAT.addBits(data.nom_bits(16))
369 Usage_B_CER.addBits(data.nom_bits(16))
370 Distribution_Cer.addBits(data.nom_bits(32))
372 case _:
373 print('PID not (yet?) supported: 0x{:02x}'.format(PID))
374 return 3
377 print('PID (product): 0x{:02x} (flipflop?: {})'.format(PID, (PID & 0x10) != 0));
378 print('KeyId : 0x{:1x}'.format(KeyId))
379 print()
382 DISTRIBUTION
383 ------------
384 Not very well documented but seems standard for this part
386 if not Distribution_Data.isEmpty():
388 ContractNetworkId = Distribution_Data.nom_bits(24)
389 CountryCode = ba2int(ContractNetworkId[0:0+12])
390 OrganizationalAuthority = ba2int(ContractNetworkId[12:12+12])
392 ContractApplicationVersionNumber = Distribution_Data.nom(6)
393 ContractProvider = Distribution_Data.nom(8)
394 ContractTariff = Distribution_Data.nom(16)
395 ContractMediumEndDate = Distribution_Data.nom(14)
397 Distribution_left = Distribution_Data.nom_bits_left()
399 print('DISTRIBUTION')
400 print(' CountryCode : {:03x} - {}'.format(CountryCode, ISO_Countries.get(CountryCode, '?')));
401 print(' OrganizationalAuthority : {:03x}'.format(OrganizationalAuthority));
402 print(' ContractApplicationVersionNumber:', ContractApplicationVersionNumber);
403 print(' ContractProvider :', ContractProvider);
404 if (CountryCode == 0x250):
405 oa = FRA_OrganizationalAuthority_Contract_Provider.get(OrganizationalAuthority)
406 if (oa is not None):
407 s = oa.get(ContractProvider)
408 if (s is not None):
409 print(' ~ Authority & Provider ~ : {} ({})'.format(s.OrganizationalAuthority, s.ContractProvider))
410 Describe_Usage = s.UsageDescribeFunction
411 print(' ContractTariff :', ContractTariff);
412 print(' ContractMediumEndDate : {} ({})'.format(ContractMediumEndDate, (datetime(1997, 1, 1) + timedelta(days = ContractMediumEndDate)).strftime('%Y-%m-%d')));
413 print(' left... :', Distribution_left);
414 print(' [CER] Distribution : {:08x}'.format(Distribution_Cer.nom(32)))
415 print()
417 if(Describe_Usage is None):
418 Describe_Usage = Describe_Usage_Generic
420 if COUNTER1 is not None:
421 print('[1] Counter: 0x{:06x} - Reloading available: 0x{:02x}'.format(COUNTER1, RELOADING1))
422 # if COUNTER2 is not None:
423 # print('[2] Counter: 0x{:06x} - Reloading available: 0x{:02x}'.format(COUNTER2, RELOADING2))
424 if SWAP is not None:
425 print('[S] SWAP : 0x{:08x} - last usage on USAGE_{}'.format(SWAP, 'B' if SWAP & 0b1 else 'A'))
429 USAGE
430 -----
431 No real documentation about Usage
432 Nearly all is left... - did not seen implementation with 2 counters or 1 Usage
435 if not Usage_A_DAT.isEmpty():
436 print()
437 print('USAGE_A')
438 Describe_Usage(Usage_A_DAT, ContractMediumEndDate, Usage_A_CER)
440 if not Usage_B_DAT.isEmpty():
441 print()
442 print('USAGE_B')
443 Describe_Usage(Usage_B_DAT, ContractMediumEndDate, Usage_B_CER)
446 return 0
449 if __name__ == '__main__':
450 sys.exit(main())