python script: start making changes as discussed in the MR, wip
[RRG-proxmark3.git] / client / pyscripts / fm11rf08_full.py
blob291795cf1d917d28d682d117fee0697bcf659803
1 #!/usr/bin/env python3
3 # ------------------------------------------------------------------------------
4 # Imports
6 import re
7 import os
8 import sys
9 import argparse
10 import pm3
11 import struct
12 import json
14 from fm11rf08s_recovery import recovery
16 # ------------------------------------------------------------------------------
17 # Revision log & Licence
18 # ------------------------------------------------------------------------------
19 '''
20 1.2.0 - BC - Proxmark3 Submission
21 '''
22 script_ver = "1.2.0"
24 # Copyright @csBlueChip
26 # This program is free software: you can redistribute it and/or modify
27 # it under the terms of the GNU General Public License as published by
28 # the Free Software Foundation, either version 3 of the License, or
29 # (at your option) any later version.
31 # This program is distributed in the hope that it will be useful,
32 # but WITHOUT ANY WARRANTY; without even the implied warranty of
33 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
34 # GNU General Public License for more details.
36 # See LICENSE.txt for the text of the license.
38 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
40 # The original version of this script can be found at:
41 # https://github.com/csBlueChip/Proxmark_Stuff/tree/main/MiFare_Docs/Fudan_RF08(S)/PM3_Script
42 # The original version is released with an MIT Licence.
43 # Or please reach out to me [BlueChip] personally for alternative licenses.
46 # optional color support .. `pip install ansicolors`
47 try:
48 from colors import color
49 except ModuleNotFoundError:
50 def color(s, fg=None):
51 _ = fg
52 return str(s)
55 # +=============================================================================
56 # Print and Log
57 # >> "logfile"
58 # ==============================================================================
59 def startlog(uid, append=False):
60 global logfile
62 logfile = f"{dpath}hf-mf-{uid:08X}-log.txt"
63 if append is False:
64 with open(logfile, 'w'):
65 pass
68 # +=========================================================
69 def lprint(s, end='\n', flush=False):
70 print(s, end=end, flush=flush)
72 if logfile is not None:
73 with open(logfile, 'a') as f:
74 f.write(s + end)
77 # ++============================================================================
78 # == MAIN ==
79 # >> "prompt"
80 # >> p. [console handle]
81 # >> "keyfile"
82 # ==============================================================================
83 def main():
84 global prompt
85 global p
87 prompt = "[=]"
88 p = pm3.pm3() # console interface
90 getPrefs()
91 if not checkVer():
92 return
93 parseCli()
95 print(f"{prompt} Fudan FM11RF08[S] full card recovery")
97 print(prompt)
98 print(f"{prompt} Dump folder: {dpath}")
100 if not getDarkKey():
101 return
102 decodeBlock0()
104 global keyfile
105 global mad
107 mad = False
108 keyfile = f"{dpath}hf-mf-{uid:08X}-key.bin"
109 keyok = False
111 if args.force is False and loadKeys() is True:
112 keyok = True
113 else:
114 if args.recover is False:
115 lprint(f"{prompt} * Keys not loaded, use --recover to run recovery script [slow]")
116 else:
117 recoverKeys()
118 if loadKeys() is True:
119 keyok = True
121 if keyok is True:
122 if verifyKeys() is False:
123 if args.nokeys is False:
124 lprint(f"{prompt} ! Use --nokeys to keep going past this point")
125 return
127 readBlocks()
128 patchKeys(keyok)
130 diskDump() # save it before you do anything else
132 dumpData()
133 dumpAcl()
135 if mad is True:
136 dumpMad()
138 if (args.bambu is True) or (detectBambu() is True):
139 dumpBambu()
141 lprint(prompt)
142 lprint(f"{prompt} Tadah!")
144 return
147 # +=============================================================================
148 # Get PM3 preferences
149 # >> "dpath"
150 # ==============================================================================
151 def getPrefs():
152 global dpath
154 p.console("prefs show --json")
155 prefs = json.loads(p.grabbed_output)
156 dpath = prefs['file.default.dumppath'] + os.path.sep
159 # +=============================================================================
160 # Assert python version
161 # ==============================================================================
162 def checkVer():
163 required_version = (3, 8)
164 if sys.version_info < required_version:
165 print(f"Python version: {sys.version}")
166 print(f"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
167 return False
168 return True
171 # +=============================================================================
172 # Parse the CLi arguments
173 # >> args.
174 # ==============================================================================
175 def parseCli():
176 global args
178 parser = argparse.ArgumentParser(description='Full recovery of Fudan FM11RF08* cards.')
180 parser.add_argument('-n', '--nokeys', action='store_true', help='extract data even if keys are missing')
181 parser.add_argument('-r', '--recover', action='store_true', help='run key recovery script if required')
182 parser.add_argument('-f', '--force', action='store_true', help='force recovery of keys')
183 parser.add_argument('-b', '--bambu', action='store_true', help='force Bambu tag decode')
184 parser.add_argument('-v', '--validate', action='store_true', help='check Fudan signature (requires internet)')
186 args = parser.parse_args()
188 if args.force is True:
189 args.recover = True
192 # +=============================================================================
193 # Find backdoor key
194 # >> "dkey"
195 # >> "blk0"
196 # [=] # | sector 00 / 0x00 | ascii
197 # [=] ----+-------------------------------------------------+-----------------
198 # [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
199 # ==============================================================================
200 def getDarkKey():
201 global dkey
202 global blk0
204 # FM11RF08S FM11RF08 FM11RF32
205 dklist = ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]
207 print(prompt)
208 print(f"{prompt} Trying known backdoor keys...")
210 dkey = ""
211 for k in dklist:
212 cmd = f"hf mf rdbl -c 4 --key {k} --blk 0"
213 print(f"{prompt} `{cmd}`", end='', flush=True)
214 res = p.console(f"{cmd}")
215 for line in p.grabbed_output.split('\n'):
216 if " | " in line and "# | s" not in line:
217 blk0 = line[10:56+1]
218 if res == 0:
219 print(" - success")
220 dkey = k
221 break
222 print(f" - fail [{res}]")
224 if dkey == "":
225 print(f"{prompt}")
226 print(f"{prompt} ! Unknown key, or card not detected.")
227 return False
228 return True
230 # +=============================================================================
231 # Extract data from block 0
232 # >> "uid"
233 # >> "uids"
234 # ==============================================================================
235 def decodeBlock0():
236 global uid
237 global uids
239 # We do this early so we can name the logfile!
240 uids = blk0[0:11] # UID string : "11 22 33 44"
241 uid = int(uids.replace(' ', ''), 16) # UID (value) : 0x11223344
242 startlog(uid, append=False)
244 lprint(prompt)
245 lprint(f"{prompt} UID BCC ++----- RF08 ID -----++")
246 lprint(f"{prompt} ! ! SAK !! !!")
247 lprint(f"{prompt} ! ! ! ATQA !! Fudan Sig !!")
248 lprint(f"{prompt} !---------. !. !. !---. VV .---------------. VV")
249 # 0 12 15 18 24 27 45
250 # ! ! ! ! ! ! !
251 # 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
252 lprint(f"{prompt} Block 0 : {blk0}")
254 # --- decode block 0 ---
256 bcc = int(blk0[12:14], 16) # BCC
257 chk = 0 # calculate checksum
258 for h in uids.split():
259 chk ^= int(h, 16)
261 sak = int(blk0[15:17], 16) # SAK
262 atqa = int(blk0[18:23].replace(' ', ''), 16) # 0x7788
264 fida = int(blk0[24:26], 16) # Fudan ID 0x88
265 fidb = int(blk0[45:47], 16) # Fudan ID 0xFF
266 # fid = (fida<<8)|fidb # Fudan ID 0x88FF
268 hash = blk0[27:44] # Fudan hash "99 AA BB CC DD EE"
270 type = f"[{fida:02X}:{fidb:02X}]" # type/name
271 if fidb == 0x90:
272 if fida == 0x01 or fida == 0x03 or fida == 0x04:
273 type += " - Fudan FM11RF08S"
275 elif fidb == 0x1D:
276 if fida == 0x01 or fida == 0x02 or fida == 0x03:
277 type += " - Fudan FM11RF08"
279 elif fidb == 0x91 or fidb == 0x98:
280 type += " - Fudan FM11RF08 (never seen in the wild)"
282 else:
283 type += " - Unknown (please report)"
285 # --- show results ---
287 lprint(prompt)
289 lprint(f"{prompt} UID/BCC : {uid:08X}/{bcc:02X} - ", end='')
290 if bcc == chk:
291 lprint("verified")
292 else:
293 lprint(f"fail. Expected {chk:02X}")
295 lprint(f"{prompt} SAK : {sak:02X} - ", end='')
296 if sak == 0x01:
297 lprint("NXP MIFARE TNP3xxx 1K")
298 elif sak == 0x08:
299 lprint("NXP MIFARE CLASSIC 1k | Plus 1k | Ev1 1K")
300 elif sak == 0x09:
301 lprint("NXP MIFARE Mini 0.3k")
302 elif sak == 0x10:
303 lprint("NXP MIFARE Plus 2k")
304 elif sak == 0x18:
305 lprint("NXP MIFARE Classic 4k | Plus 4k | Ev1 4k")
306 else:
307 lprint("{unknown}")
309 lprint(f"{prompt} ATQA : {atqa:04X}") # show ATQA
310 lprint(f"{prompt} Fudan ID : {type}") # show type
311 lprint(f"{prompt} Fudan Sig: {hash}") # show ?Partial HMAC?
312 lprint(f"{prompt} Dark Key : {dkey}") # show key
315 # +=============================================================================
316 # Fudan validation
317 # >> "blk0"
318 # ==============================================================================
319 def fudanValidate():
320 # Warning, this import causes a "double free or corruption" crash if the script is called twice...
321 # So for now we limit the import only when really needed
322 import requests
323 global blk0
325 url = "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
326 hdr = "Content-Type: application/text; charset=utf-8"
327 post = f"{blk0.replace(' ', '')}"
329 lprint(prompt)
330 lprint(f"{prompt} Validator: `wget -q -O -"
331 f" --header=\"{hdr}\""
332 f" --post-data \"{post}\""
333 f" {url}"
334 " | json_pp`")
336 if args.validate:
337 lprint(prompt)
338 lprint(f"{prompt} Check Fudan signature (requires internet)...")
340 headers = {"Content-Type": "application/text; charset=utf-8"}
341 resp = requests.post(url, headers=headers, data=post)
343 if resp.status_code != 200:
344 lprint(f"{prompt} HTTP Error {resp.status_code} - check request not processed")
346 else:
347 r = json.loads(resp.text)
348 lprint(f"{prompt} The man from Fudan, he say: {r['code']} - {r['message']}", end='')
349 if r['data'] is not None:
350 lprint(f" {{{r['data']}}}")
351 else:
352 lprint("")
353 else:
354 lprint(prompt)
355 lprint(f"{prompt} ...Use --validate to perform Fudan signature check automatically")
358 # +=============================================================================
359 # Load keys from file
360 # If keys cannot be loaded AND --recover is specified, then run key recovery
361 # >> "keyfile"
362 # >> "key[17][2]"
363 # ==============================================================================
366 def loadKeys():
367 global keyfile
368 global key
370 key = [[b'' for _ in range(2)] for _ in range(17)] # create a fresh array
372 lprint(prompt)
373 lprint(f"{prompt} Load Keys from file: |{keyfile}|")
375 try:
376 with (open(keyfile, "rb")) as fh:
377 for ab in [0, 1]:
378 for sec in range((16+2)-1):
379 key[sec][ab] = fh.read(6)
381 except IOError:
382 return False
384 return True
387 # +=============================================================================
388 # Run key recovery script
389 # >> "keyfile"
390 # ==============================================================================
391 def recoverKeys():
392 global keyfile
394 badrk = 0 # 'bad recovered key' count (ie. not recovered)
396 lprint(prompt)
397 lprint(f"{prompt} Running recovery script, ETA: Less than 30 minutes")
399 lprint(prompt)
400 lprint(f'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
402 r = recovery(quiet=False)
403 keyfile = r['keyfile']
404 rkey = r['found_keys']
405 # fdump = r['dumpfile']
406 # rdata = r['data']
408 lprint(f'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
410 for k in range(0, 16+1):
411 for ab in [0, 1]:
412 if rkey[k][ab] == "":
413 if badrk == 0:
414 lprint(f"{prompt} Some keys were not recovered: ", end='')
415 else:
416 lprint(", ", end='')
417 badrk += 1
419 kn = k
420 if kn > 15:
421 kn += 16
422 lprint(f"[{kn}/", end='')
423 lprint("A]" if ab == 0 else "B]", end='')
424 if badrk > 0:
425 lprint("")
428 # +=============================================================================
429 # Verify keys
430 # >> "key[][]"
431 # >> mad!
432 # ==============================================================================
433 def verifyKeys():
434 global key
435 global mad
437 badk = 0
439 lprint(f"{prompt} Check keys..")
441 for sec in range(0, 16+1): # 16 normal, 1 dark
442 sn = sec
443 if (sn > 15):
444 sn = sn + 16
446 for ab in [0, 1]:
447 bn = (sec * 4) + 3
448 if bn >= 64:
449 bn += 64
451 cmd = f"hf mf rdbl -c {ab} --key {key[sec][ab].hex()} --blk {bn}"
452 lprint(f"{prompt} `{cmd}`", end='', flush=True)
454 res = p.console(f"{cmd}", capture=False)
455 lprint(" " * (3-len(str(bn))), end="")
456 if res == 0:
457 lprint(" ... PASS", end="")
458 else:
459 lprint(" ... FAIL", end="")
460 badk += 1
461 key[sec][ab] = b''
463 # check for Mifare Application Directory
464 if (sec == 0) and (ab == 0) \
465 and (key[0][0] == b'\xa0\xa1\xa2\xa3\xa4\xa5'):
466 mad = True
467 lprint(" - MAD Key")
468 else:
469 lprint("")
471 if badk > 0:
472 lprint(f"{prompt} ! {badk} bad key", end='')
473 lprint("s exist" if badk != 1 else " exists")
474 rv = False
476 else:
477 lprint(f"{prompt} All keys verified OK")
478 rv = True
480 if mad is True:
481 lprint(f"{prompt} MAD key detected")
483 return rv
486 # +=============================================================================
487 # Read all block data - INCLUDING Dark blocks
488 # >> blkn
489 # >> "data[]"
490 # [=] # | sector 00 / 0x00 | ascii
491 # [=] ----+-------------------------------------------------+-----------------
492 # [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \........Y.%._p.
493 # ==============================================================================
494 def readBlocks():
495 global data
496 global blkn
498 data = []
499 blkn = list(range(0, 63+1)) + list(range(128, 135+1))
501 # The user uses keyhole #1 (-a)
502 # The vendor uses keyhole #2 (-b)
503 # The thief uses keyhole #4 (backdoor)
504 # |___
505 rdbl = f"hf mf rdbl -c 4 --key {dkey} --blk"
507 lprint(prompt)
508 lprint(prompt + " Load blocks {0..63, 128..135}[64+8=72] from the card")
510 bad = 0
511 for n in blkn:
512 cmd = f"{rdbl} {n}"
513 print(f"\r{prompt} `{cmd}`", end='', flush=True)
515 for retry in range(5):
516 p.console(f"{cmd}")
518 found = False
519 for line in p.grabbed_output.split('\n'):
520 if " | " in line and "# | s" not in line:
521 lsub = line[4:76]
522 data.append(lsub)
523 found = True
524 if found:
525 break
527 if not found:
528 data.append(f"{n:3d} | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | ----------------")
529 bad += 1
531 print(" .. OK")
534 # +=============================================================================
535 # Patch keys in to data
536 # >> "key[][]"
537 # >> "data[]"
538 # >> keyok!
539 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
540 # ==============================================================================
541 def patchKeys(keyok):
542 global key
543 global data
545 lprint(prompt)
546 lprint(f"{prompt} Patch keys in to data")
548 for sec in range(0, 16+1):
549 blk = (sec * 4) + 3 # find "trailer" for this sector
550 if keyok:
551 if key[sec][0] == b'':
552 keyA = "-- -- -- -- -- -- "
553 else:
554 kstr = key[sec][0].hex()
555 keyA = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])
557 if key[sec][1] == b'':
558 keyB = "-- -- -- -- -- -- "
559 else:
560 kstr = key[sec][1].hex()
561 keyB = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])
563 data[blk] = data[blk][:6] + keyA + data[blk][24:36] + keyB
565 else:
566 data[blk] = data[blk][:6] + "-- -- -- -- -- -- " + data[blk][24:36] + "-- -- -- -- -- --"
569 # +=============================================================================
570 # Dump data
571 # >> blkn
572 # >> "data[]"
573 # ==============================================================================
574 def dumpData():
575 global blkn
576 global data
578 lprint(prompt)
579 lprint(f"{prompt} ===========")
580 lprint(f"{prompt} Card Data")
581 lprint(f"{prompt} ===========")
582 lprint(f"{prompt}")
584 cnt = 0
585 for n in blkn:
586 sec = (cnt // 4)
587 if sec > 15:
588 sec = sec + 16
590 if (n % 4 == 0):
591 lprint(f"{prompt} {sec:2d}:{data[cnt]}")
592 else:
593 lprint(f"{prompt} :{data[cnt]}")
595 cnt += 1
596 if (cnt % 4 == 0) and (n != blkn[-1]): # Space between sectors
597 lprint(prompt)
600 # +=============================================================================
601 # Let's try to detect a Bambu card by the date strings...
602 # ==============================================================================
603 def detectBambu():
604 try:
605 dl = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')
606 dls = dl[2:13]
607 ds = bytes.fromhex(data[13][6:41]).decode('ascii').rstrip('\x00')
608 except Exception:
609 return False
611 # ds 24_03_22_16
612 # dl 2024_03_22_16_29
613 # yy y y m m d d h h m m
614 exp = r"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"
616 lprint(f"{prompt}")
617 if re.search(exp, dl) and (ds == dls):
618 lprint(f"{prompt} Bambu date strings detected.")
619 return True
620 else:
621 lprint(f"{prompt} Bambu date strings not detected.")
622 return False
625 # +=============================================================================
626 # Dump bambu details
627 # https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
628 # >> "data[]"
629 # 6 18 30 42 53
630 # | | | | |
631 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
632 # +=============================================================================
633 def dumpBambu():
634 global data
636 try:
637 lprint(f"{prompt}")
638 lprint(f"{prompt} ===========")
639 lprint(f"{prompt} Bambu Tag")
640 lprint(f"{prompt} ===========")
641 lprint(f"{prompt}")
642 lprint(f"{prompt} Decompose as Bambu tag .. ", end='')
644 MaterialVariantIdentifier_s = bytes.fromhex(data[1][6:29]).decode('ascii').rstrip('\x00')
645 UniqueMaterialIdentifier_s = bytes.fromhex(data[1][30:53]).decode('ascii').rstrip('\x00') # [**] 8not16
647 FilamentType_s = bytes.fromhex(data[2][6:53]).decode('ascii').rstrip('\x00')
649 DetailedFilamentType_s = bytes.fromhex(data[4][6:53]).decode('ascii').rstrip('\x00')
651 Colour_rgba = int(data[5][6:17].replace(' ', ''), 16)
652 SpoolWeight_g = int(data[5][21:23] + data[5][18:20], 16)
653 Block5_7to8 = data[5][24:29]
654 FilamentDiameter_mm = struct.unpack('f', bytes.fromhex(data[5][30:41].replace(' ', '')))[0]
655 Block5_12to15 = data[5][42:50]
657 DryingTemperature_c = int(data[6][9:11] + data[6][6: 8], 16)
658 DryingTime_h = int(data[6][15:17] + data[6][12:14], 16)
659 BedTemperatureType_q = int(data[6][21:23] + data[6][18:20], 16)
660 BedTemperature_c = int(data[6][27:29] + data[6][24:26], 16)
661 MaxTemperatureForHotend_c = int(data[6][33:35] + data[6][30:32], 16)
662 MinTemperatureForHotend_c = int(data[6][39:41] + data[6][36:38], 16)
663 Block6_12to15 = data[6][42:50]
665 # XCamInfo_x = bytes.fromhex(data[8][6:41].replace(' ', ''))
666 XCamInfo_x = data[8][6:41]
667 NozzleDiameter_q = struct.unpack('f', bytes.fromhex(data[8][42:53].replace(' ', '')))[0]
669 # TrayUID_s = bytes.fromhex(data[9][6:53]).decode('ascii').rstrip('\x00') #[**] !ascii
670 TrayUID_s = data[9][6:53]
672 Block10_0to3 = data[10][6:17]
673 SppolWidth_um = int(data[10][21:23] + data[14][18:20], 16)
674 Block10_6to15 = data[10][24:50]
676 ProductionDateTime_s = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')
678 ShortProductionDateTime_s = bytes.fromhex(data[13][6:53]).decode('ascii').rstrip('\x00')
680 # Block14_0to3 = data[14][6:17]
681 FilamentLength_m = int(data[14][21:23] + data[14][18:20], 16)
682 # Block14_6to15 = data[14][24:51]
684 # (16blocks * 16bytes = 256) * 8bits = 2048 bits
685 hblk = [42,
686 44, 45, 46,
687 48, 49, 50,
688 52, 53, 54,
689 56, 57, 58,
690 60, 61, 62]
691 Hash = []
692 for b in hblk:
693 Hash.append(data[b][6:53])
695 lprint("[offset:length]")
696 lprint(f"{prompt} Block 1:")
697 lprint(f"{prompt} [ 0: 8] MaterialVariantIdentifier_s = \"{MaterialVariantIdentifier_s}\"")
698 lprint(f"{prompt} [ 8: 8] UniqueMaterialIdentifier_s = \"{UniqueMaterialIdentifier_s}\"")
699 lprint(f"{prompt} Block 2:")
700 lprint(f"{prompt} [ 0:16] FilamentType_s = \"{FilamentType_s}\"")
701 lprint(f"{prompt} Block 4:")
702 lprint(f"{prompt} [ 0:16] DetailedFilamentType_s = \"{DetailedFilamentType_s}\"")
703 lprint(f"{prompt} Block 5:")
704 lprint(f"{prompt} [ 0: 4] Colour_rgba = 0x{Colour_rgba:08X}")
705 lprint(f"{prompt} [ 4: 2] SpoolWeight_g = {SpoolWeight_g}g")
706 lprint(f"{prompt} [6: 2] Block5_7to8 = {{{Block5_7to8}}}")
707 lprint(f"{prompt} [ 8: 4] FilamentDiameter_mm = {FilamentDiameter_mm}mm")
708 lprint(f"{prompt} [12: 4] Block5_12to15 = {{{Block5_12to15}}}")
709 lprint(f"{prompt} Block 6:")
710 lprint(f"{prompt} [ 0: 2] DryingTemperature_c = {DryingTemperature_c}^C")
711 lprint(f"{prompt} [ 2: 2] DryingTime_h = {DryingTime_h}hrs")
712 lprint(f"{prompt} [ 4: 4] BedTemperatureType_q = {BedTemperatureType_q}")
713 lprint(f"{prompt} [6: 2] BedTemperature_c = {BedTemperature_c}^C")
714 lprint(f"{prompt} [ 8: 2] MaxTemperatureForHotend_c = {MaxTemperatureForHotend_c}^C")
715 lprint(f"{prompt} [10: 2] MinTemperatureForHotend_c = {MinTemperatureForHotend_c}^C")
716 lprint(f"{prompt} [12: 4] Block6_12to15 = {{{Block6_12to15}}}")
717 lprint(f"{prompt} Block 8:")
718 lprint(f"{prompt} [ 0:12] XCamInfo_x = {{{XCamInfo_x}}}")
719 lprint(f"{prompt} [12: 4] NozzleDiameter_q = {NozzleDiameter_q:.6f}__")
720 lprint(f"{prompt} Block 9:")
721 # lprint(f"{prompt} [ 0:16] TrayUID_s = \"{TrayUID_s}\"")
722 lprint(f"{prompt} [ 0:16] TrayUID_s = {{{TrayUID_s}}} ; not ASCII")
723 lprint(f"{prompt} Block 10:")
724 lprint(f"{prompt} [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
725 lprint(f"{prompt} [ 4: 2] SppolWidth_um = {SppolWidth_um}um")
726 lprint(f"{prompt} [6:10] Block10_6to15 = {{{Block10_6to15}}}")
727 lprint(f"{prompt} Block 12:")
728 lprint(f"{prompt} [ 0:16] ProductionDateTime_s = \"{ProductionDateTime_s}\"")
729 lprint(f"{prompt} Block 13:")
730 lprint(f"{prompt} [ 0:16] ShortProductionDateTime_s = \"{ShortProductionDateTime_s}\"")
731 lprint(f"{prompt} Block 14:")
732 lprint(f"{prompt} [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
733 lprint(f"{prompt} [ 4: 2] FilamentLength_m = {FilamentLength_m}m")
734 lprint(f"{prompt} [6:10] Block10_6to15 = {{{Block10_6to15}}}")
735 lprint(f"{prompt}")
736 lprint(f"{prompt} Blocks {hblk}:")
737 for i in range(0, len(hblk)):
738 lprint(f"{prompt} [ 0:16] HashBlock[{i:2d}] = {{{Hash[i]}}} // #{hblk[i]:2d}")
740 except Exception as e:
741 lprint(f"Failed: {e}")
744 # +=============================================================================
745 # Dump ACL
746 # >> "data[][]"
747 # 6 18 24 27 30 33 42 53
748 # | | | | | | | |
749 # 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
750 # ab cd ef
752 # ,-------------------.
753 # ( 2.2 : ACCESS BITS )
754 # `-------------------'
756 # The Access bits on both (used) Sectors is the same: 78 77 88
758 # Let's reorganise that according to the official spec Fig 9.
759 # Access C1 C2 C3
760 # ========== ===========
761 # 78 77 88 --> 78 87 87
762 # ab cd ef --> cb fa ed
764 # The second nybble of each byte is the inverse of the first nybble.
765 # It is there to trap tranmission errors, so we can just ignore it/them.
767 # So our Access Control value is : {c, f, e} == {7, 8, 8}
769 # Let's convert those nybbles to binary
770 # (c) 7 --> 0111
771 # (f) 8 --> 1000
772 # (e) 8 --> 1000
773 # |||| ...and transpose them:
774 # ||||
775 # |||`--- 100 - Block 0 Access bits
776 # ||`---- 100 - Block 1 Access bits
777 # |`----- 100 - Block 2 Access bits
778 # `------ 011 - Block 3 Access bits [Sector Trailer]
780 # Now we can use the lookup table [Table 3] to work out what we can do
781 # with the Sector Trailer (Block(S,3)):
783 # | Key A | | Access Bits | | Key B |
784 # | read ¦ write | | read ¦ write | | read ¦ write |
785 # +------¦-------+ +------¦-------+ +------¦-------+
786 # 000 : | -- ¦ KeyA | | KeyA ¦ -- | | KeyA ¦ KeyA |
787 # 001 : | -- ¦ KeyA | | KeyA ¦ KeyA | | KeyA ¦ KeyA | Transport Mode
788 # 010 : | -- ¦ -- | | KeyA ¦ -- | | KeyA ¦ -- |
790 # 011 : | -- ¦ KeyB | | A+B ¦ KeyB | | -- ¦ KeyB | <-- Our Card!
792 # 100 : | -- ¦ KeyB | | A+B ¦ -- | | -- ¦ KeyB |
793 # 101 : | -- ¦ -- | | A+B ¦ KeyB | | -- ¦ -- |
794 # 110 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | }__
795 # 111 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | } The Same!?
797 # Our card uses 011, for (both of) the (used) Sector Trailer(s). So:
798 # Both Key A and Key B can READ the Access Bits
799 # Key B can (additionally) WRITE to Key A, Key B (itself), and the Access Bits
801 # Then we can do a similar lookup for the 3 data Blocks (in this Sector)
802 # This time using [Table 4]
804 # | Data | Counter |
805 # | read ¦ write | Inc ¦ Dec |
806 # +------¦-------+------¦------+
807 # 000 : | A+B ¦ A+B | A+B ¦ A+B | Transport Mode
808 # 001 : | A+B ¦ -- | -- ¦ A+B |
809 # 010 : | A+B ¦ -- | -- ¦ -- |
810 # 011 : | KeyB ¦ KeyB | -- ¦ -- |
812 # 100 : | A+B ¦ KeyB | -- ¦ -- | <-- Our Card!
814 # 101 : | KeyB ¦ -- | -- ¦ -- |
815 # 110 : | A+B ¦ KeyB | KeyB ¦ A+B |
816 # 111 : | -- ¦ -- | -- ¦ -- |
818 # Our card uses 100, for all of the (used) Sectors. So:
819 # Both Key A and Key B can READ the Block
820 # Only Key B can WRITE to the Block
821 # The block cannot be used as a "counter" because:
822 # Neither key can perform increment nor decrement commands
824 # WARNING:
825 # IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
826 # ==============================================================================
827 def dumpAcl():
828 global blkn
830 aclkh = [] # key header
831 aclk = [""] * 8 # key lookup
832 aclkx = [] # key output
834 lprint(f"{prompt}")
835 lprint(f"{prompt} =====================")
836 lprint(f"{prompt} Access Control List")
837 lprint(f"{prompt} =====================")
839 aclkh.append(" _______________________________________________________ ")
840 aclkh.append("| | Sector Trailers |")
841 aclkh.append("| |----------------------------------------------|")
842 aclkh.append("| Sector |____Key_A_____||_Access_Bits__||____Key_B_____|")
843 aclkh.append("| | read ¦ write || read ¦ write || read ¦ write |")
844 aclkh.append("|--------+------¦-------++------¦-------++------¦-------|")
845 # "| xx | -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA |"
846 aclk[0] = "| -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA | [000]" # noqa: E222
847 aclk[1] = "| -- ¦ KeyA || KeyA ¦ KeyA || KeyA ¦ KeyA | [001]" # noqa: E222
848 aclk[2] = "| -- ¦ -- || KeyA ¦ -- || KeyA ¦ -- | [010]" # noqa: E222
849 aclk[3] = "| -- ¦ KeyB || A+B ¦ KeyB || -- ¦ KeyB | [011]" # noqa: E222
850 aclk[4] = "| -- ¦ KeyB || A+B ¦ -- || -- ¦ KeyB | [100]" # noqa: E222
851 aclk[5] = "| -- ¦ -- || A+B ¦ KeyB || -- ¦ -- | [101]" # noqa: E222
852 aclk[6] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [110]" # noqa: E222 # yes, the same!?
853 aclk[7] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [111]" # noqa: E222 # ...
855 acldh = [] # data header
856 acld = [""] * 8 # data lookup
857 acldx = [] # data output
859 acldh.append(" _____________________________________ ")
860 acldh.append("| | Data Blocks |")
861 acldh.append("| |-----------------------------|")
862 acldh.append("| Block | Data || Counter |")
863 acldh.append("| | read ¦ write || Inc ¦ Dec |")
864 acldh.append("|-------+------¦-------++------¦------+")
865 # "| xxx | A+B ¦ A+B || A+B ¦ A+B | "
866 acld[0] = "| A+B ¦ A+B || A+B ¦ A+B | [000]" # noqa: E222
867 acld[1] = "| A+B ¦ -- || -- ¦ A+B | [001]" # noqa: E222
868 acld[2] = "| A+B ¦ -- || -- ¦ -- | [010]" # noqa: E222
869 acld[3] = "| KeyB ¦ KeyB || -- ¦ -- | [011]" # noqa: E222
870 acld[4] = "| A+B ¦ KeyB || -- ¦ -- | [100]" # noqa: E222
871 acld[5] = "| KeyB ¦ -- || -- ¦ -- | [101]" # noqa: E222
872 acld[6] = "| A+B ¦ KeyB || KeyB ¦ A+B | [110]" # noqa: E222
873 acld[7] = "| -- ¦ -- || -- ¦ -- | [111]" # noqa: E222
875 idx = [] * (16+2)
877 # --- calculate the ACL indices for each sector:block ---
878 for d in data:
879 bn = int(d[0:3], 10)
881 if ((bn % 4) == 3):
882 sn = (bn // 4)
883 sec = sn if sn < 16 else sn - 16
885 c = int(d[27], 16)
886 f = int(d[31], 16)
887 e = int(d[30], 16)
888 r0 = ((c & (2**0)) << 2) | ((f & (2**0)) << 1) | ((e & (2**0)) ) # noqa: E202
889 r1 = ((c & (2**1)) << 1) | ((f & (2**1)) ) | ((e & (2**1)) >> 1) # noqa: E202
890 r2 = ((c & (2**2)) ) | ((f & (2**2)) >> 1) | ((e & (2**2)) >> 2) # noqa: E202
891 r3 = ((c & (2**3)) >> 1) | ((f & (2**3)) >> 2) | ((e & (2**3)) >> 3) # noqa: E221
892 idx[sec] = [r0, r1, r2, r3]
894 # --- build the ACL conversion table ---
895 for d in data:
896 bn = int(d[0:3], 10)
897 sn = (bn // 4)
898 sec = sn if sn < 16 else sn - 16
900 if ((bn % 4) == 3):
901 aclkx.append(f"| {sn:2d} " + aclk[idx[sec][bn % 4]]
902 + f" {{{d[24:32]}}} -> {{{d[27]}{d[31]}{d[30]}}}")
903 else:
904 acldx.append(f"| {bn:3d} " + acld[idx[sec][bn % 4]])
906 # --- print it all out ---
907 for line in aclkh:
908 lprint(f"{prompt} {line}")
909 i = 0
910 for line in aclkx:
911 lprint(f"{prompt} {line}")
912 if (i % 4) == 3:
913 lprint(f"{prompt} | | ¦ || ¦ || ¦ |")
914 i += 1
916 lprint(f"{prompt}")
918 for line in acldh:
919 lprint(f"{prompt} {line}")
920 i = 0
921 for line in acldx:
922 lprint(f"{prompt} {line}")
923 if (i % 3) == 2:
924 lprint(f"{prompt} | | ¦ || ¦ |")
925 i += 1
928 # +=============================================================================
929 # Full Dump
930 # >> "uid"
931 # >> "dump18"
932 # ==============================================================================
933 def diskDump():
934 global uid
935 global dump18
937 dump18 = f"{dpath}hf-mf-{uid:08X}-dump18.bin"
939 lprint(prompt)
940 lprint(f"{prompt} Dump Card Data to file: {dump18}")
942 bad = False
943 with open(dump18, 'wb') as f:
944 for d in data:
945 if "--" in d[6:53]:
946 bad = True
947 b = bytes.fromhex(d[6:53].replace(" ", "").replace("--", "FF"))
948 f.write(b)
949 if bad:
950 lprint(f"{prompt} Bad data exists, and has been saved as 0xFF")
953 # +=============================================================================
954 # Dump MAD
955 # >> "dump18"
956 # ==============================================================================
957 def dumpMad():
958 global dump18
960 lprint(f"{prompt}")
961 lprint(f"{prompt} ====================================")
962 lprint(f"{prompt} MiFare Application Directory (MAD)")
963 lprint(f"{prompt} ====================================")
964 lprint(f"{prompt}")
966 cmd = f"hf mf mad --verbose --file {dump18}"
967 print(f"{prompt} `{cmd}`")
969 lprint(f"{prompt}")
970 lprint(f'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
972 lprint("")
973 p.console(f"{cmd}")
975 for line in p.grabbed_output.split('\n'):
976 lprint(line)
978 lprint(f'{prompt} `-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
981 # ++============================================================================
982 if __name__ == "__main__":
983 main()