style
[RRG-proxmark3.git] / client / pyscripts / fm11rf08_full.py
blobe684f904d85421239b44b9a6e8d9649da6442a3f
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 author = "@csBlueChip"
17 script_ver = "1.2.0"
19 # Copyright @csBlueChip
21 # This program is free software: you can redistribute it and/or modify
22 # it under the terms of the GNU General Public License as published by
23 # the Free Software Foundation, either version 3 of the License, or
24 # (at your option) any later version.
26 # This program is distributed in the hope that it will be useful,
27 # but WITHOUT ANY WARRANTY; without even the implied warranty of
28 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
29 # GNU General Public License for more details.
31 # See LICENSE.txt for the text of the license.
33 # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
35 # The original version of this script can be found at:
36 # https://github.com/csBlueChip/Proxmark_Stuff/tree/main/MiFare_Docs/Fudan_RF08(S)/PM3_Script
37 # The original version is released with an MIT Licence.
38 # Or please reach out to me [BlueChip] personally for alternative licenses.
41 # optional color support .. `pip install ansicolors`
42 try:
43 from colors import color
44 except ModuleNotFoundError:
45 def color(s, fg=None):
46 _ = fg
47 return str(s)
50 def initlog():
51 """Print and Log: init globals
53 globals:
54 - logbuffer (W)
55 - logfile (W)
56 """
57 global logbuffer
58 global logfile
59 logbuffer = ''
60 logfile = None
63 def startlog(uid, dpath, append=False):
64 """Print and Log: set logfile and flush logbuffer
66 globals:
67 - logbuffer (RW)
68 - logfile (RW)
69 """
70 global logfile
71 global logbuffer
73 logfile = f"{dpath}hf-mf-{uid.hex().upper()}-log.txt"
74 if append is False:
75 with open(logfile, 'w'):
76 pass
77 if logbuffer != '':
78 with open(logfile, 'a') as f:
79 f.write(logbuffer)
80 logbuffer = ''
83 def lprint(s='', end='\n', flush=False, prompt="[" + color("=", fg="yellow") + "] ", log=True):
84 """Print and Log
86 globals:
87 - logbuffer (RW)
88 - logfile (R)
89 """
91 s = f"{prompt}" + f"\n{prompt}".join(s.split('\n'))
92 print(s, end=end, flush=flush)
94 if log is True:
95 global logbuffer
96 if logfile is not None:
97 with open(logfile, 'a') as f:
98 f.write(s + end)
99 else:
100 # buffering
101 logbuffer += s + end
104 def main():
105 """== MAIN ==
107 globals:
108 - p (W)
110 global p
111 p = pm3.pm3() # console interface
112 initlog()
114 if not checkVer():
115 return
116 dpath = getPrefs()
117 args = parseCli()
119 # No logfile name yet
120 lprint("Fudan FM11RF08[S] full card recovery")
121 lprint("\nDump folder... " + color(f"{dpath}", fg="yellow"))
123 # FIXME: script is announced as for RF08 and for RF08S but it comprises RF32N key
124 # and if RF08 is supported, all other NXP/Infineon with same backdoor can be treated
125 # by the same script (once properly implemented, see other FIXME)
126 bdkey, blk0 = getBackdoorKey()
127 if bdkey is None:
128 return
129 uid = getUIDfromBlock0(blk0)
130 startlog(uid, dpath, append=False)
131 decodeBlock0(blk0)
132 fudanValidate(blk0, args.validate)
134 mad = False
135 keyfile = f"{dpath}hf-mf-{uid.hex().upper()}-key.bin"
137 # FIXME: nr of sectors depend on the tag. RF32N is 40, RF32 is 64, RF08 is 16, RF08S is 16+1
138 # Currently loadKeys is hardcoded for RF08S
139 if args.force or (key := loadKeys(keyfile)) is None:
140 if args.recover is False:
141 s = color("--recover", fg="yellow")
142 lprint(f" Keys not loaded, use {s} to run recovery script [slow]", prompt="[" + color("!", fg="red") + "]")
143 else:
144 # FIXME: recovery() is only for RF08S. TODO for the other ones with a "darknested" attack
145 keyfile = recoverKeys()
146 key = loadKeys(keyfile)
148 if key is not None:
149 ret, mad, key = verifyKeys(key)
150 if ret is False:
151 if args.nokeys is False:
152 s = color("--nokeys", fg="yellow")
153 lprint(f" Use {s} to keep going past this point", prompt="[" + color("!", fg="red") + "]")
154 return
156 # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8
157 # Currently readBlocks is hardcoded for RF08S
158 data, blkn = readBlocks(bdkey, args.fast)
159 data = patchKeys(data, key)
161 dump18 = diskDump(data, uid, dpath) # save it before you do anything else
163 dumpData(data, blkn)
165 # FIXME: nr of blocks depend on the tag. RF32 is 256, RF08 is 64, RF08S is 64+8,
166 # Currently dumpAcl is hardcoded for RF08S
167 dumpAcl(data)
169 if (mad is True) or (args.mad is True):
170 dumpMad(dump18)
172 if (args.bambu is True) or (detectBambu(data) is True):
173 dumpBambu(data)
175 lprint("\nTadah!")
177 return
180 def getPrefs():
181 """Get PM3 preferences
183 globals:
184 - p (R)
186 p.console("prefs show --json")
187 prefs = json.loads(p.grabbed_output)
188 dpath = prefs['file.default.dumppath'] + os.path.sep
189 return dpath
192 def checkVer():
193 """Assert python version"""
194 required_version = (3, 8)
195 if sys.version_info < required_version:
196 print(f"Python version: {sys.version}")
197 print(f"The script needs at least Python v{required_version[0]}.{required_version[1]}. Abort.")
198 return False
199 return True
202 def parseCli():
203 """Parse the CLi arguments"""
204 parser = argparse.ArgumentParser(description='Full recovery of Fudan FM11RF08* cards.')
206 parser.add_argument('-n', '--nokeys', action='store_true', help='extract data even if keys are missing')
207 parser.add_argument('-r', '--recover', action='store_true', help='run key recovery script if required')
208 parser.add_argument('-f', '--force', action='store_true', help='force recovery of keys')
209 parser.add_argument('-b', '--bambu', action='store_true', help='force Bambu tag decode')
210 parser.add_argument('-m', '--mad', action='store_true', help='force M.A.D. decode')
211 parser.add_argument('-v', '--validate', action='store_true', help='check Fudan signature (requires internet)')
212 parser.add_argument('--fast', action='store_true', help='use ecfill for faster card transactions')
214 args = parser.parse_args()
216 if args.force is True:
217 args.recover = True
218 return args
221 def getBackdoorKey():
222 """Find backdoor key
223 [=] # | sector 00 / 0x00 | ascii
224 [=] ----+-------------------------------------------------+-----------------
225 [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p.
227 globals:
228 - p (R)
231 # FM11RF08S FM11RF08 FM11RF32
232 dklist = ["A396EFA4E24F", "A31667A8CEC1", "518b3354E760"]
234 lprint("\nTrying known backdoor keys...")
236 bdkey = ""
237 for k in dklist:
238 cmd = f"hf mf rdbl -c 4 --key {k} --blk 0"
239 lprint(f"\n`{cmd}`", end='', flush=True)
240 res = p.console(cmd)
241 for line in p.grabbed_output.split('\n'):
242 if " | " in line and "# | s" not in line:
243 blk0 = line[10:56+1]
244 if res == 0:
245 s = color('ok', fg='green')
246 lprint(f" ( {s} )", prompt='')
247 bdkey = k
248 break
249 s = color('fail', fg='yellow')
250 lprint(f" ( {s} ) [{res}]", prompt='')
252 if bdkey == "":
253 lprint("\n Unknown key, or card not detected.", prompt="[" + color("!", fg="red") + "]")
254 return None, None
255 return bdkey, blk0
258 def getUIDfromBlock0(blk0):
259 """Extract UID from block 0"""
260 uids = blk0[0:11] # UID string : "11 22 33 44"
261 uid = bytes.fromhex(uids.replace(' ', '')) # UID (bytes) : 11223344
262 return uid
265 def decodeBlock0(blk0):
266 """Extract data from block 0"""
267 lprint()
268 lprint(" UID BCC ++----- RF08 ID -----++")
269 lprint(" ! ! SAK !! !!")
270 lprint(" ! ! ! ATQA !! Fudan Sig !!")
271 lprint(" !---------. !. !. !---. VV .---------------. VV")
272 # 0 12 15 18 24 27 45
273 # ! ! ! ! ! ! !
274 # 00 11 22 33 44 55 66 77 88 99 AA BB CC DD EE FF
275 lprint(f" Block 0 : {blk0}")
277 # --- decode block 0 ---
279 uid = getUIDfromBlock0(blk0)
280 bcc = int(blk0[12:14], 16) # BCC
281 chk = 0 # calculate checksum
282 for h in uid:
283 chk ^= h
285 sak = int(blk0[15:17], 16) # SAK
286 atqa = int(blk0[18:23].replace(' ', ''), 16) # 0x7788
288 fida = int(blk0[24:26], 16) # Fudan ID 0x88
289 fidb = int(blk0[45:47], 16) # Fudan ID 0xFF
290 # fid = (fida<<8)|fidb # Fudan ID 0x88FF
292 hash = blk0[27:44] # Fudan hash "99 AA BB CC DD EE"
294 type = f"[{fida:02X}:{fidb:02X}]" # type/name
295 if fidb == 0x90:
296 if fida == 0x01 or fida == 0x03 or fida == 0x04:
297 type += " - Fudan FM11RF08S"
299 elif fidb == 0x1D:
300 if fida == 0x01 or fida == 0x02 or fida == 0x03:
301 type += " - Fudan FM11RF08"
303 elif fidb == 0x91 or fidb == 0x98:
304 type += " - Fudan FM11RF08 (never seen in the wild)"
306 else:
307 type += " - Unknown (please report)"
309 # --- show results ---
311 lprint()
313 if bcc == chk:
314 desc = "verified"
315 else:
316 desc = f"fail. Expected {chk:02X}"
317 lprint(f" UID/BCC : {uid.hex().upper()}/{bcc:02X} - {desc}")
319 if sak == 0x01:
320 desc = "NXP MIFARE TNP3xxx 1K"
321 elif sak == 0x08:
322 desc = "NXP MIFARE CLASSIC 1k | Plus 1k | Ev1 1K"
323 elif sak == 0x09:
324 desc = "NXP MIFARE Mini 0.3k"
325 elif sak == 0x10:
326 desc = "NXP MIFARE Plus 2k"
327 elif sak == 0x18:
328 desc = "NXP MIFARE Classic 4k | Plus 4k | Ev1 4k"
329 else:
330 desc = "{unknown}"
331 lprint(f" SAK : {sak:02X} - {desc}")
332 lprint(f" ATQA : {atqa:04X}") # show ATQA
333 lprint(f" Fudan ID : {type}") # show type
334 lprint(f" Fudan Sig: {hash}") # show ?Partial HMAC?
337 def fudanValidate(blk0, live=False):
338 """Fudan validation"""
339 url = "https://rfid.fm-uivs.com/nfcTools/api/M1KeyRest"
340 hdr = "Content-Type: application/text; charset=utf-8"
341 post = f"{blk0.replace(' ', '')}"
343 lprint(f"\n Validator:\n`wget -q -O -"
344 f" --header=\"{hdr}\""
345 f" --post-data \"{post}\""
346 f" {url}"
347 " | json_pp`")
349 if live:
350 # Warning, this import causes a "double free or corruption" crash if the script is called twice...
351 # So for now we limit the import only when really needed
352 try:
353 import requests
354 except ModuleNotFoundError:
355 s = color("not found", fg="red")
356 lprint(f"Python module 'requests' {s}, please install!", prompt="[" + color("!", fg="red") + "] ")
357 return
358 lprint("\nCheck Fudan signature (requires internet)...")
360 headers = {"Content-Type": "application/text; charset=utf-8"}
361 resp = requests.post(url, headers=headers, data=post)
363 if resp.status_code != 200:
364 lprint(f"HTTP Error {resp.status_code} - check request not processed")
366 else:
367 r = json.loads(resp.text)
368 if r['data'] is not None:
369 desc = f" {{{r['data']}}}"
370 else:
371 desc = ""
372 lprint(f"The man from Fudan, he say: {r['code']} - {r['message']}{desc}")
373 else:
374 s = color('--validate', fg="yellow")
375 lprint(f'\n Use {s} to perform Fudan signature check automatically', prompt='[?]')
378 def loadKeys(keyfile):
379 """Load keys from file
381 If keys cannot be loaded AND --recover is specified, then run key recovery
383 key = [[b'' for _ in range(2)] for _ in range(17)] # create a fresh array
385 lprint("\nLoad keys from file... " + color(f"{keyfile}", fg="yellow"))
387 try:
388 with (open(keyfile, "rb")) as fh:
389 for ab in [0, 1]:
390 for sec in range((16+2)-1):
391 key[sec][ab] = fh.read(6)
393 except IOError:
394 return None
396 return key
399 def recoverKeys():
400 """Run key recovery script"""
401 badrk = 0 # 'bad recovered key' count (ie. not recovered)
403 lprint("\nRunning recovery script, ETA: Less than 30 minutes")
405 lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
407 r = recovery(quiet=False)
408 keyfile = r['keyfile']
409 rkey = r['found_keys']
410 # fdump = r['dumpfile']
411 # rdata = r['data']
413 lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
415 for k in range(0, 16+1):
416 for ab in [0, 1]:
417 if rkey[k][ab] == "":
418 if badrk == 0:
419 lprint("Some keys were not recovered: ", end='')
420 else:
421 lprint(", ", end='', prompt='')
422 badrk += 1
424 kn = k
425 if kn > 15:
426 kn += 16
427 lprint(f"[{kn}/", end='', prompt='')
428 lprint("A]" if ab == 0 else "B]", end='', prompt='')
429 if badrk > 0:
430 lprint()
431 return keyfile
434 def verifyKeys(key):
435 """Verify keys
437 globals:
438 - p (R)
441 badk = 0
442 mad = False
444 lprint("Checking keys...")
446 for sec in range(0, 16+1): # 16 normal, 1 dark
447 sn = sec
448 if (sn > 15):
449 sn = sn + 16
451 for ab in [0, 1]:
452 bn = (sec * 4) + 3
453 if bn >= 64:
454 bn += 64
456 cmd = f"hf mf rdbl -c {ab} --key {key[sec][ab].hex()} --blk {bn}"
457 lprint(f" `{cmd}`", end='', flush=True)
459 res = p.console(cmd, capture=False)
460 lprint(" " * (3-len(str(bn))), end='', prompt='')
461 if res == 0:
462 s = color("ok", fg="green")
463 lprint(f" ( {s} )", end='', prompt='')
464 else:
465 s = color("fail", fg="red")
466 lprint(f" ( {s} )", end='', prompt='')
467 badk += 1
468 key[sec][ab] = b''
470 # check for Mifare Application Directory
471 if (sec == 0) and (ab == 0) \
472 and (key[0][0] == b'\xa0\xa1\xa2\xa3\xa4\xa5'):
473 mad = True
474 lprint(" - MAD Key", prompt='')
475 else:
476 lprint("", prompt='')
478 if badk > 0:
479 s = color(f'{badk}', fg="red")
480 e = "s exist" if badk != 1 else " exists"
481 lprint(f" {s} bad key{e}", prompt="[" + color("!", fg="red") + "]")
482 rv = False, mad, key
484 else:
485 lprint("All keys verified")
486 rv = True, mad, key
488 if mad is True:
489 lprint("MAD key detected")
491 return rv
494 def readBlocks(bdkey, fast=False):
496 Read all block data - INCLUDING advanced verification blocks
498 [=] # | sector 00 / 0x00 | ascii
499 [=] ----+-------------------------------------------------+-----------------
500 [=] 0 | 5C B4 9C A6 D2 08 04 00 04 59 92 25 BF 5F 70 90 | \\........Y.%._p.
502 globals:
503 - p (R)
505 data = []
506 blkn = list(range(0, 63 + 1)) + list(range(128, 135 + 1))
508 lprint("\nLoad blocks {0..63, 128..135}[64 + 8 = 72] from the card")
510 blkn_todo = blkn
511 if fast:
512 # Try fast dump first
513 # The user uses keyhole #0 (-a)
514 # The vendor uses keyhole #1 (-b)
515 # The thief uses keyhole #4 (backdoor)
516 # |___
517 cmd = f"hf mf ecfill -c 4 --key {bdkey}"
518 lprint(f"`{cmd}`", flush=True, log=False)
519 p.console(cmd)
520 for line in p.grabbed_output.split('\n'):
521 if "ok" in line:
522 cmd = "hf mf eview"
523 lprint(f"`{cmd}`", flush=True, log=False)
524 p.console(cmd)
525 for line in p.grabbed_output.split('\n'):
526 if " | " in line and "sec | blk | data" not in line:
527 lsub = line[11:83]
528 data.append(lsub)
529 blkn_todo = list(range(128, 135+1))
531 bad = 0
532 for n in blkn_todo:
533 cmd = f"hf mf rdbl -c 4 --key {bdkey} --blk {n}"
534 lprint(f" `{cmd}`", flush=True, log=False, end='')
536 for retry in range(5):
537 p.console(cmd)
539 found = False
540 for line in p.grabbed_output.split('\n'):
541 if " | " in line and "# | s" not in line:
542 lsub = line[4:76]
543 data.append(lsub)
544 found = True
545 if found:
546 break
548 s = color("ok", fg="green")
549 if not found:
550 data.append(f"{n:3d} | -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- | ----------------")
551 bad += 1
552 s = color("fail", fg="red")
554 lprint(" " * (3 - len(str(n))), flush=True, log=False, end='', prompt='')
555 lprint(f' ( {s} )', flush=True, log=False, prompt='')
557 s = color("ok", fg="green")
558 if bad > 0:
559 s = color("fail", fg="red")
561 lprint(f'Loading ( {s} )', log=False)
562 return data, blkn
565 def patchKeys(data, key):
566 """Patch keys in to data
567 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
569 lprint("\nPatching keys in to data")
571 for sec in range(0, 16 + 1):
572 blk = (sec * 4) + 3 # find "trailer" for this sector
573 if key is not None:
574 if key[sec][0] == b'':
575 keyA = "-- -- -- -- -- -- "
576 else:
577 kstr = key[sec][0].hex()
578 keyA = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])
580 if key[sec][1] == b'':
581 keyB = "-- -- -- -- -- -- "
582 else:
583 kstr = key[sec][1].hex()
584 keyB = "".join([kstr[i:i+2] + " " for i in range(0, len(kstr), 2)])
586 data[blk] = data[blk][:6] + keyA + data[blk][24:36] + keyB
588 else:
589 data[blk] = data[blk][:6] + "-- -- -- -- -- -- " + data[blk][24:36] + "-- -- -- -- -- --"
590 return data
593 def dumpData(data, blkn):
594 """Dump data"""
595 lprint()
596 lprint("===========")
597 lprint(" Card Data")
598 lprint("===========")
599 lprint()
601 cnt = 0
602 for n in blkn:
603 sec = (cnt // 4)
604 if sec > 15:
605 sec = sec + 16
607 if (n % 4 == 0):
608 lprint(f"{sec:2d}:{data[cnt]}")
609 else:
610 lprint(f" :{data[cnt]}")
612 cnt += 1
613 if (cnt % 4 == 0) and (n != blkn[-1]): # Space between sectors
614 lprint()
617 def detectBambu(data):
618 """Let's try to detect a Bambu card by the date strings..."""
619 try:
620 dl = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')
621 dls = dl[2:13]
622 ds = bytes.fromhex(data[13][6:41]).decode('ascii').rstrip('\x00')
623 except Exception:
624 return False
626 # ds 24_03_22_16
627 # dl 2024_03_22_16_29
628 # yy y y m m d d h h m m
629 exp = r"20[2-3][0-9]_[0-1][0-9]_[0-3][0-9]_[0-2][0-9]_[0-5][0-9]"
631 if re.search(exp, dl) and (ds == dls):
632 lprint("\nBambu date strings detected.")
633 return True
634 else:
635 lprint("\nBambu date strings not detected.")
636 return False
639 def dumpBambu(data):
640 """Dump bambu details
642 https://github.com/Bambu-Research-Group/RFID-Tag-Guide/blob/main/README.md
644 6 18 30 42 53
645 | | | | |
646 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
648 try:
649 lprint()
650 lprint("===========")
651 lprint(" Bambu Tag")
652 lprint("===========")
653 lprint()
654 lprint("Decompose as Bambu tag .. ", end='')
656 MaterialVariantIdentifier_s = bytes.fromhex(data[1][6:29]).decode('ascii').rstrip('\x00')
657 UniqueMaterialIdentifier_s = bytes.fromhex(data[1][30:53]).decode('ascii').rstrip('\x00') # [**] 8not16
659 FilamentType_s = bytes.fromhex(data[2][6:53]).decode('ascii').rstrip('\x00')
661 DetailedFilamentType_s = bytes.fromhex(data[4][6:53]).decode('ascii').rstrip('\x00')
663 Colour_rgba = int(data[5][6:17].replace(' ', ''), 16)
664 SpoolWeight_g = int(data[5][21:23] + data[5][18:20], 16)
665 Block5_7to8 = data[5][24:29]
666 FilamentDiameter_mm = struct.unpack('f', bytes.fromhex(data[5][30:41].replace(' ', '')))[0]
667 Block5_12to15 = data[5][42:50]
669 DryingTemperature_c = int(data[6][9:11] + data[6][6:8], 16)
670 DryingTime_h = int(data[6][15:17] + data[6][12:14], 16)
671 BedTemperatureType_q = int(data[6][21:23] + data[6][18:20], 16)
672 BedTemperature_c = int(data[6][27:29] + data[6][24:26], 16)
673 MaxTemperatureForHotend_c = int(data[6][33:35] + data[6][30:32], 16)
674 MinTemperatureForHotend_c = int(data[6][39:41] + data[6][36:38], 16)
675 Block6_12to15 = data[6][42:50]
677 # XCamInfo_x = bytes.fromhex(data[8][6:41].replace(' ', ''))
678 XCamInfo_x = data[8][6:41]
679 NozzleDiameter_q = struct.unpack('f', bytes.fromhex(data[8][42:53].replace(' ', '')))[0]
681 # TrayUID_s = bytes.fromhex(data[9][6:53]).decode('ascii').rstrip('\x00') #[**] !ascii
682 TrayUID_s = data[9][6:53]
684 Block10_0to3 = data[10][6:17]
685 SpoolWidth_um = int(data[10][21:23] + data[14][18:20], 16)
686 Block10_6to15 = data[10][24:50]
688 ProductionDateTime_s = bytes.fromhex(data[12][6:53]).decode('ascii').rstrip('\x00')
690 ShortProductionDateTime_s = bytes.fromhex(data[13][6:53]).decode('ascii').rstrip('\x00')
692 # Block14_0to3 = data[14][6:17]
693 FilamentLength_m = int(data[14][21:23] + data[14][18:20], 16)
694 # Block14_6to15 = data[14][24:51]
696 # (16blocks * 16bytes = 256) * 8bits = 2048 bits
697 hblk = [42,
698 44, 45, 46,
699 48, 49, 50,
700 52, 53, 54,
701 56, 57, 58,
702 60, 61, 62]
703 Hash = []
704 for b in hblk:
705 Hash.append(data[b][6:53])
707 lprint("[offset:length]", prompt='')
708 lprint(" Block 1:")
709 lprint(f" [ 0: 8] MaterialVariantIdentifier_s = \"{MaterialVariantIdentifier_s}\"")
710 lprint(f" [ 8: 8] UniqueMaterialIdentifier_s = \"{UniqueMaterialIdentifier_s}\"")
711 lprint(" Block 2:")
712 lprint(f" [ 0:16] FilamentType_s = \"{FilamentType_s}\"")
713 lprint(" Block 4:")
714 lprint(f" [ 0:16] DetailedFilamentType_s = \"{DetailedFilamentType_s}\"")
715 lprint(" Block 5:")
716 lprint(f" [ 0: 4] Colour_rgba = 0x{Colour_rgba:08X}")
717 lprint(f" [ 4: 2] SpoolWeight_g = {SpoolWeight_g}g")
718 lprint(f" [ 6: 2] Block5_7to8 = {{{Block5_7to8}}}")
719 lprint(f" [ 8: 4] FilamentDiameter_mm = {FilamentDiameter_mm}mm")
720 lprint(f" [12: 4] Block5_12to15 = {{{Block5_12to15}}}")
721 lprint(" Block 6:")
722 lprint(f" [ 0: 2] DryingTemperature_c = {DryingTemperature_c}^C")
723 lprint(f" [ 2: 2] DryingTime_h = {DryingTime_h}hrs")
724 lprint(f" [ 4: 4] BedTemperatureType_q = {BedTemperatureType_q}")
725 lprint(f" [ 6: 2] BedTemperature_c = {BedTemperature_c}^C")
726 lprint(f" [ 8: 2] MaxTemperatureForHotend_c = {MaxTemperatureForHotend_c}^C")
727 lprint(f" [10: 2] MinTemperatureForHotend_c = {MinTemperatureForHotend_c}^C")
728 lprint(f" [12: 4] Block6_12to15 = {{{Block6_12to15}}}")
729 lprint(" Block 8:")
730 lprint(f" [ 0:12] XCamInfo_x = {{{XCamInfo_x}}}")
731 lprint(f" [12: 4] NozzleDiameter_q = {NozzleDiameter_q:.6f}__")
732 lprint(" Block 9:")
733 # lprint(f" [ 0:16] TrayUID_s = \"{TrayUID_s}\"")
734 lprint(f" [ 0:16] TrayUID_s = {{{TrayUID_s}}} ; not ASCII")
735 lprint(" Block 10:")
736 lprint(f" [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
737 lprint(f" [ 4: 2] SpoolWidth_um = {SpoolWidth_um}um")
738 lprint(f" [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
739 lprint(" Block 12:")
740 lprint(f" [ 0:16] ProductionDateTime_s = \"{ProductionDateTime_s}\"")
741 lprint(" Block 13:")
742 lprint(f" [ 0:16] ShortProductionDateTime_s = \"{ShortProductionDateTime_s}\"")
743 lprint(" Block 14:")
744 lprint(f" [ 0: 4] Block10_0to3 = {{{Block10_0to3}}}")
745 lprint(f" [ 4: 2] FilamentLength_m = {FilamentLength_m}m")
746 lprint(f" [ 6:10] Block10_6to15 = {{{Block10_6to15}}}")
747 lprint(f"\n Blocks {hblk}:")
748 for i in range(0, len(hblk)):
749 lprint(f" [ 0:16] HashBlock[{i:2d}] = {{{Hash[i]}}} // #{hblk[i]:2d}")
751 except Exception as e:
752 lprint(prompt='')
753 lprint(f"Failed: {e}")
756 # +=============================================================================
757 # Dump ACL
759 # ,-------------------.
760 # ( 2.2 : ACCESS BITS )
761 # `-------------------'
763 # The Access bits on both (used) Sectors is the same: 78 77 88
765 # Let's reorganise that according to the official spec Fig 9.
766 # Access C1 C2 C3
767 # ========== ===========
768 # 78 77 88 --> 78 87 87
769 # ab cd ef --> cb fa ed
771 # The second nybble of each byte is the inverse of the first nybble.
772 # It is there to trap tranmission errors, so we can just ignore it/them.
774 # So our Access Control value is : {c, f, e} == {7, 8, 8}
776 # Let's convert those nybbles to binary
777 # (c) 7 --> 0111
778 # (f) 8 --> 1000
779 # (e) 8 --> 1000
780 # |||| ...and transpose them:
781 # ||||
782 # |||`--- 100 - Block 0 Access bits
783 # ||`---- 100 - Block 1 Access bits
784 # |`----- 100 - Block 2 Access bits
785 # `------ 011 - Block 3 Access bits [Sector Trailer]
787 # Now we can use the lookup table [Table 3] to work out what we can do
788 # with the Sector Trailer (Block(S,3)):
790 # | Key A | | Access Bits | | Key B |
791 # | read ¦ write | | read ¦ write | | read ¦ write |
792 # +------¦-------+ +------¦-------+ +------¦-------+
793 # 000 : | -- ¦ KeyA | | KeyA ¦ -- | | KeyA ¦ KeyA |
794 # 001 : | -- ¦ KeyA | | KeyA ¦ KeyA | | KeyA ¦ KeyA | Transport Mode
795 # 010 : | -- ¦ -- | | KeyA ¦ -- | | KeyA ¦ -- |
797 # 011 : | -- ¦ KeyB | | A+B ¦ KeyB | | -- ¦ KeyB | <-- Our Card!
799 # 100 : | -- ¦ KeyB | | A+B ¦ -- | | -- ¦ KeyB |
800 # 101 : | -- ¦ -- | | A+B ¦ KeyB | | -- ¦ -- |
801 # 110 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | }__
802 # 111 : | -- ¦ -- | | A+B ¦ -- | | -- ¦ -- | } The Same!?
804 # Our card uses 011, for (both of) the (used) Sector Trailer(s). So:
805 # Both Key A and Key B can READ the Access Bits
806 # Key B can (additionally) WRITE to Key A, Key B (itself), and the Access Bits
808 # Then we can do a similar lookup for the 3 data Blocks (in this Sector)
809 # This time using [Table 4]
811 # | Data | Counter |
812 # | read ¦ write | Inc ¦ Dec |
813 # +------¦-------+------¦------+
814 # 000 : | A+B ¦ A+B | A+B ¦ A+B | Transport Mode
815 # 001 : | A+B ¦ -- | -- ¦ A+B |
816 # 010 : | A+B ¦ -- | -- ¦ -- |
817 # 011 : | KeyB ¦ KeyB | -- ¦ -- |
819 # 100 : | A+B ¦ KeyB | -- ¦ -- | <-- Our Card!
821 # 101 : | KeyB ¦ -- | -- ¦ -- |
822 # 110 : | A+B ¦ KeyB | KeyB ¦ A+B |
823 # 111 : | -- ¦ -- | -- ¦ -- |
825 # Our card uses 100, for all of the (used) Sectors. So:
826 # Both Key A and Key B can READ the Block
827 # Only Key B can WRITE to the Block
828 # The block cannot be used as a "counter" because:
829 # Neither key can perform increment nor decrement commands
831 # WARNING:
832 # IF YOU PLAN TO CHANGE ACCESS BITS, RTFM, THERE IS MUCH TO CONSIDER !
833 # ==============================================================================
834 def dumpAcl(data):
835 """Dump ACL
837 6 18 24 27 30 33 42 53
838 | | | | | | | |
839 3 | 00 00 00 00 00 00 87 87 87 69 00 00 00 00 00 00 | .........i......
840 ab cd ef
842 aclkh = [] # key header
843 aclk = [""] * 8 # key lookup
844 aclkx = [] # key output
846 lprint()
847 lprint("=====================")
848 lprint(" Access Control List")
849 lprint("=====================")
850 lprint()
852 aclkh.append(" _______________________________________________________ ")
853 aclkh.append("| | Sector Trailers |")
854 aclkh.append("| |----------------------------------------------|")
855 aclkh.append("| Sector |____Key_A_____||_Access_Bits__||____Key_B_____|")
856 aclkh.append("| | read ¦ write || read ¦ write || read ¦ write |")
857 aclkh.append("|--------+------¦-------++------¦-------++------¦-------|")
858 # "| xx | -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA |"
859 aclk[0] = "| -- ¦ KeyA || KeyA ¦ -- || KeyA ¦ KeyA | [000]" # noqa: E222
860 aclk[1] = "| -- ¦ KeyA || KeyA ¦ KeyA || KeyA ¦ KeyA | [001]" # noqa: E222
861 aclk[2] = "| -- ¦ -- || KeyA ¦ -- || KeyA ¦ -- | [010]" # noqa: E222
862 aclk[3] = "| -- ¦ KeyB || A+B ¦ KeyB || -- ¦ KeyB | [011]" # noqa: E222
863 aclk[4] = "| -- ¦ KeyB || A+B ¦ -- || -- ¦ KeyB | [100]" # noqa: E222
864 aclk[5] = "| -- ¦ -- || A+B ¦ KeyB || -- ¦ -- | [101]" # noqa: E222
865 aclk[6] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [110]" # noqa: E222 # yes, the same!?
866 aclk[7] = "| -- ¦ -- || A+B ¦ -- || -- ¦ -- | [111]" # noqa: E222 # ...
868 acldh = [] # data header
869 acld = [""] * 8 # data lookup
870 acldx = [] # data output
872 acldh.append(" _____________________________________ ")
873 acldh.append("| | Data Blocks |")
874 acldh.append("| |-----------------------------|")
875 acldh.append("| Block | Data || Counter |")
876 acldh.append("| | read ¦ write || Inc ¦ Dec |")
877 acldh.append("|-------+------¦-------++------¦------+")
878 # "| xxx | A+B ¦ A+B || A+B ¦ A+B | "
879 acld[0] = "| A+B ¦ A+B || A+B ¦ A+B | [000]" # noqa: E222
880 acld[1] = "| A+B ¦ -- || -- ¦ A+B | [001]" # noqa: E222
881 acld[2] = "| A+B ¦ -- || -- ¦ -- | [010]" # noqa: E222
882 acld[3] = "| KeyB ¦ KeyB || -- ¦ -- | [011]" # noqa: E222
883 acld[4] = "| A+B ¦ KeyB || -- ¦ -- | [100]" # noqa: E222
884 acld[5] = "| KeyB ¦ -- || -- ¦ -- | [101]" # noqa: E222
885 acld[6] = "| A+B ¦ KeyB || KeyB ¦ A+B | [110]" # noqa: E222
886 acld[7] = "| -- ¦ -- || -- ¦ -- | [111]" # noqa: E222
888 idx = [[]] * (16+2)
890 # --- calculate the ACL indices for each sector:block ---
891 for d in data:
892 bn = int(d[0:3], 10)
894 if ((bn % 4) == 3):
895 sn = (bn // 4)
896 sec = sn if sn < 16 else sn - 16
898 c = int(d[27], 16)
899 f = int(d[31], 16)
900 e = int(d[30], 16)
901 r0 = ((c & (2**0)) << 2) | ((f & (2**0)) << 1) | ((e & (2**0)) ) # noqa: E202
902 r1 = ((c & (2**1)) << 1) | ((f & (2**1)) ) | ((e & (2**1)) >> 1) # noqa: E202
903 r2 = ((c & (2**2)) ) | ((f & (2**2)) >> 1) | ((e & (2**2)) >> 2) # noqa: E202
904 r3 = ((c & (2**3)) >> 1) | ((f & (2**3)) >> 2) | ((e & (2**3)) >> 3) # noqa: E221
905 idx[sec] = [r0, r1, r2, r3]
907 # --- build the ACL conversion table ---
908 for d in data:
909 bn = int(d[0:3], 10)
910 sn = (bn // 4)
911 sec = sn if sn < 16 else sn - 16
913 if ((bn % 4) == 3):
914 aclkx.append(f"| {sn:2d} " + aclk[idx[sec][bn % 4]]
915 + f" {{{d[24:32]}}} -> {{{d[27]}{d[31]}{d[30]}}}")
916 else:
917 acldx.append(f"| {bn:3d} " + acld[idx[sec][bn % 4]])
919 # --- print it all out ---
920 for line in aclkh:
921 lprint(f" {line}")
922 i = 0
923 for line in aclkx:
924 lprint(f" {line}")
925 if (i % 4) == 3:
926 lprint(" | | ¦ || ¦ || ¦ |")
927 i += 1
929 lprint()
931 for line in acldh:
932 lprint(f" {line}")
933 i = 0
934 for line in acldx:
935 lprint(f" {line}")
936 if (i % 3) == 2:
937 lprint(" | | ¦ || ¦ |")
938 i += 1
941 def diskDump(data, uid, dpath):
942 """Full Dump"""
943 dump18 = f'{dpath}hf-mf-{uid.hex().upper()}-dump18.bin'
945 lprint(f'\nDump card data to file... ' + color(dump18, fg='yellow'))
947 bad = False
948 try:
949 with open(dump18, 'wb') as f:
950 for d in data:
951 if '--' in d[6:53]:
952 bad = True
953 b = bytes.fromhex(d[6:53].replace(' ', '').replace('--', 'FF'))
954 f.write(b)
955 if bad:
956 lprint('Bad data exists, and has been saved as 0xFF')
958 s = color('ok', fg='green')
959 lprint(f' Save file operations ( {s} )', prompt='[+]')
961 except Exception as e:
962 s = color('fail', fg='red')
963 lprint(f' Save file operations: {e} ( {s} )', prompt='[!]')
965 return dump18
968 def dumpMad(dump18):
969 """Dump MAD
971 globals:
972 - p (R)
975 lprint()
976 lprint("====================================")
977 lprint(" MiFare Application Directory (MAD)")
978 lprint("====================================")
979 lprint()
981 cmd = f"hf mf mad --force --verbose --file {dump18}"
982 lprint(f"`{cmd}`", log=False)
984 lprint('\n`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,\n')
986 p.console(cmd)
988 for line in p.grabbed_output.split('\n'):
989 lprint(line, prompt='')
991 lprint('`-._,-\'"`-._,-"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,-\'"`-._,')
994 if __name__ == "__main__":
995 main()