@@ -5556,7 +5556,242 @@ def on_exec(self, args: argparse.Namespace):
55565556 print (f"{ actual_index :3d} : { color_string ((CY , password .upper ()))} " )
55575557
55585558
5559- @lf_em_410x .command ("read" )
5559+ @hf_mfu .command ('nfcimport' )
5560+ class HFMFUNfcImport (SlotIndexArgsAndGoUnit , DeviceRequiredUnit ):
5561+ # Mapping from Flipper Zero device type strings to CU TagSpecificType
5562+ FLIPPER_TYPE_MAP = {
5563+ 'NTAG203' : TagSpecificType .NTAG_215 , # best-effort: no native NTAG203 support
5564+ 'NTAG210' : TagSpecificType .NTAG_210 ,
5565+ 'NTAG212' : TagSpecificType .NTAG_212 ,
5566+ 'NTAG213' : TagSpecificType .NTAG_213 ,
5567+ 'NTAG215' : TagSpecificType .NTAG_215 ,
5568+ 'NTAG216' : TagSpecificType .NTAG_216 ,
5569+ 'NTAGI2C1K' : TagSpecificType .NTAG_216 , # best-effort
5570+ 'NTAGI2C2K' : TagSpecificType .NTAG_216 , # best-effort
5571+ 'NTAGI2CPlus1K' : TagSpecificType .NTAG_216 , # best-effort
5572+ 'NTAGI2CPlus2K' : TagSpecificType .NTAG_216 , # best-effort
5573+ 'Mifare Ultralight' : TagSpecificType .MF0ICU1 ,
5574+ 'Mifare Ultralight C' : TagSpecificType .MF0ICU2 ,
5575+ 'Mifare Ultralight 11' : TagSpecificType .MF0UL11 ,
5576+ 'Mifare Ultralight 21' : TagSpecificType .MF0UL21 ,
5577+ # "Mifare Ultralight EV1" is disambiguated by page count in on_exec
5578+ }
5579+
5580+ def args_parser (self ) -> ArgumentParserNoExit :
5581+ parser = ArgumentParserNoExit ()
5582+ parser .description = 'Import a Flipper Zero .nfc file into a MIFARE Ultralight / NTAG emulator slot'
5583+ self .add_slot_args (parser )
5584+ parser .add_argument ('-f' , '--file' , required = True , type = str , help = "Path to Flipper Zero .nfc file" )
5585+ parser .add_argument ('--amiibo' , action = 'store_true' , default = False ,
5586+ help = "Derive and write correct PWD/PACK for amiibo (NTAG215)" )
5587+ return parser
5588+
5589+ def on_exec (self , args : argparse .Namespace ):
5590+ file_path = args .file
5591+ file_name = os .path .basename (file_path )
5592+
5593+ # --- Parse the .nfc file ---
5594+ try :
5595+ with open (file_path , 'r' ) as f :
5596+ lines = f .readlines ()
5597+ except FileNotFoundError :
5598+ print (color_string ((CR , f"File not found: { file_path } " )))
5599+ return
5600+ except OSError as e :
5601+ print (color_string ((CR , f"Error reading file: { e } " )))
5602+ return
5603+
5604+ device_type = None
5605+ uid = None
5606+ atqa = None
5607+ sak = None
5608+ signature = None
5609+ version = None
5610+ counters = {}
5611+ tearing = {}
5612+ pages_total = None
5613+ pages = {}
5614+
5615+ for line in lines :
5616+ line = line .strip ()
5617+ if line .startswith ('#' ) or not line :
5618+ continue
5619+
5620+ if line .startswith ('Device type:' ):
5621+ device_type = line .split (':' , 1 )[1 ].strip ()
5622+ elif line .startswith ('UID:' ):
5623+ uid = bytes .fromhex (line .split (':' , 1 )[1 ].strip ().replace (' ' , '' ))
5624+ elif line .startswith ('ATQA:' ):
5625+ atqa = bytes .fromhex (line .split (':' , 1 )[1 ].strip ().replace (' ' , '' ))
5626+ elif line .startswith ('SAK:' ):
5627+ sak = bytes .fromhex (line .split (':' , 1 )[1 ].strip ().replace (' ' , '' ))
5628+ elif line .startswith ('Signature:' ):
5629+ signature = bytes .fromhex (line .split (':' , 1 )[1 ].strip ().replace (' ' , '' ))
5630+ elif line .startswith ('Mifare version:' ):
5631+ version = bytes .fromhex (line .split (':' , 1 )[1 ].strip ().replace (' ' , '' ))
5632+ elif line .startswith ('Counter ' ):
5633+ match = re .match (r'Counter\s+(\d+):\s+(\d+)' , line )
5634+ if match :
5635+ counters [int (match .group (1 ))] = int (match .group (2 ))
5636+ elif line .startswith ('Tearing ' ):
5637+ match = re .match (r'Tearing\s+(\d+):\s+([0-9A-Fa-f]+)' , line )
5638+ if match :
5639+ tearing [int (match .group (1 ))] = int (match .group (2 ), 16 )
5640+ elif line .startswith ('Pages total:' ):
5641+ pages_total = int (line .split (':' , 1 )[1 ].strip ())
5642+ elif line .startswith ('Page ' ):
5643+ match = re .match (r'Page\s+(\d+):\s+(.*)' , line )
5644+ if match :
5645+ page_num = int (match .group (1 ))
5646+ page_data = bytes .fromhex (match .group (2 ).strip ().replace (' ' , '' ))
5647+ pages [page_num ] = page_data
5648+
5649+ # --- Validate required fields ---
5650+ if device_type is None :
5651+ print (color_string ((CR , "No 'Device type' found in .nfc file." )))
5652+ return
5653+ if uid is None :
5654+ print (color_string ((CR , "No 'UID' found in .nfc file." )))
5655+ return
5656+ if atqa is None :
5657+ print (color_string ((CR , "No 'ATQA' found in .nfc file." )))
5658+ return
5659+ if sak is None :
5660+ print (color_string ((CR , "No 'SAK' found in .nfc file." )))
5661+ return
5662+
5663+ # --- Map device type to TagSpecificType ---
5664+ tag_type = self .FLIPPER_TYPE_MAP .get (device_type )
5665+
5666+ if tag_type is None and device_type .startswith ('Mifare Ultralight EV1' ):
5667+ # Disambiguate EV1 by page count
5668+ nr = pages_total if pages_total else len (pages )
5669+ tag_type = TagSpecificType .MF0UL11 if nr <= 20 else TagSpecificType .MF0UL21
5670+
5671+ if tag_type is None :
5672+ print (color_string ((CR , f"Unsupported Flipper device type: '{ device_type } '" )))
5673+ print (f" Supported types: { ', ' .join (sorted (self .FLIPPER_TYPE_MAP .keys ()))} , Mifare Ultralight EV1" )
5674+ return
5675+
5676+ # --- Print summary ---
5677+ print (f"Importing Flipper NFC file: { file_name } " )
5678+ print (f" Device type: { device_type } -> { tag_type } " )
5679+ print (f" UID: { uid .hex (' ' ).upper ()} " )
5680+ print (f" ATQA: { atqa .hex (' ' ).upper ()} SAK: { sak .hex ().upper ()} " )
5681+ if version :
5682+ print (f" Version: { version .hex (' ' ).upper ()} " )
5683+ if signature :
5684+ print (f" Signature: { signature .hex (' ' ).upper ()} " )
5685+ if counters :
5686+ print (f" Counters: { ', ' .join (str (counters .get (i , 0 )) for i in range (max (counters .keys ()) + 1 ))} " )
5687+ nr_pages = pages_total if pages_total else len (pages )
5688+ print (f" Pages: { nr_pages } " )
5689+ print ()
5690+
5691+ # --- Step 1: Set slot tag type ---
5692+ print (f"Setting slot { self .slot_num } tag type to { tag_type } ..." )
5693+ self .cmd .set_slot_tag_type (self .slot_num , tag_type )
5694+ self .cmd .set_slot_data_default (self .slot_num , tag_type )
5695+ # Must re-activate slot after changing type so subsequent commands target the new type
5696+ self .cmd .set_active_slot (self .slot_num )
5697+
5698+ # --- Step 2: Set anti-collision data ---
5699+ print ("Setting anti-collision data..." )
5700+ self .cmd .hf14a_set_anti_coll_data (uid , atqa , sak )
5701+
5702+ # --- Step 3: Set version data ---
5703+ if version and len (version ) == 8 :
5704+ print ("Setting version data..." )
5705+ try :
5706+ self .cmd .mf0_ntag_set_version_data (version )
5707+ except (ValueError , chameleon_com .CMDInvalidException , TimeoutError ):
5708+ print (color_string ((CY , " Warning: tag type does not support GET_VERSION." )))
5709+
5710+ # --- Step 4: Set signature data ---
5711+ if signature and len (signature ) == 32 :
5712+ print ("Setting signature data..." )
5713+ try :
5714+ self .cmd .mf0_ntag_set_signature_data (signature )
5715+ except (ValueError , chameleon_com .CMDInvalidException , TimeoutError ):
5716+ print (color_string ((CY , " Warning: tag type does not support READ_SIG." )))
5717+
5718+ # --- Step 5: Set counter and tearing data ---
5719+ if counters :
5720+ print ("Setting counter data..." )
5721+ # NTAG types have a single counter accessed via NFC at index 2,
5722+ # but stored at firmware internal index 0
5723+ ntag_types = {
5724+ TagSpecificType .NTAG_210 , TagSpecificType .NTAG_212 ,
5725+ TagSpecificType .NTAG_213 , TagSpecificType .NTAG_215 ,
5726+ TagSpecificType .NTAG_216 ,
5727+ }
5728+ for i in sorted (counters .keys ()):
5729+ value = counters [i ]
5730+ if value > 0xFFFFFF :
5731+ print (color_string ((CY , f" Warning: counter { i } value { value :#x} exceeds 24-bit, skipping." )))
5732+ continue
5733+ # Map Flipper counter index to firmware internal index
5734+ if tag_type in ntag_types :
5735+ if i != 2 :
5736+ continue # NTAG only has counter at NFC index 2
5737+ fw_index = 0
5738+ else :
5739+ fw_index = i
5740+ # Reset tearing flag if tearing byte is BD (default / no tearing)
5741+ tearing_val = tearing .get (i , 0x00 )
5742+ reset_tearing = (tearing_val == 0xBD or tearing_val == 0x00 )
5743+ try :
5744+ self .cmd .mfu_write_emu_counter_data (fw_index , value , reset_tearing )
5745+ except (ValueError , chameleon_com .CMDInvalidException , UnexpectedResponseError , TimeoutError ):
5746+ print (color_string ((CY , f" Warning: could not set counter { i } ." )))
5747+
5748+ # --- Step 6: Write page data ---
5749+ if pages :
5750+ # Get total pages for the configured slot
5751+ slot_pages = self .cmd .mfu_get_emu_pages_count ()
5752+
5753+ # Build contiguous data from parsed pages
5754+ max_page = max (pages .keys ())
5755+ write_pages = min (max_page + 1 , slot_pages )
5756+
5757+ print (f"Writing { write_pages } pages..." , end = ' ' , flush = True )
5758+
5759+ page = 0
5760+ while page < write_pages :
5761+ cur_count = min (16 , write_pages - page )
5762+ batch = bytearray ()
5763+ for p in range (page , page + cur_count ):
5764+ batch .extend (pages .get (p , b'\x00 \x00 \x00 \x00 ' ))
5765+ self .cmd .mfu_write_emu_page_data (page , bytes (batch ))
5766+ page += cur_count
5767+
5768+ print ("done" )
5769+
5770+ # --- Step 7: Derive and write amiibo PWD/PACK ---
5771+ if args .amiibo :
5772+ if tag_type != TagSpecificType .NTAG_215 :
5773+ print (color_string ((CY , f" Warning: --amiibo flag ignored (tag type is { tag_type } , not NTAG 215)." )))
5774+ elif uid is None or len (uid ) != 7 :
5775+ print (color_string ((CY , " Warning: --amiibo flag ignored (UID is not 7 bytes)." )))
5776+ else :
5777+ pwd = bytes ([
5778+ 0xAA ^ uid [1 ] ^ uid [3 ],
5779+ 0x55 ^ uid [2 ] ^ uid [4 ],
5780+ 0xAA ^ uid [3 ] ^ uid [5 ],
5781+ 0x55 ^ uid [4 ] ^ uid [6 ],
5782+ ])
5783+ pack = bytes ([0x80 , 0x80 , 0x00 , 0x00 ])
5784+ print (f"Setting amiibo PWD: { pwd .hex (' ' ).upper ()} , PACK: { pack [:2 ].hex (' ' ).upper ()} ..." )
5785+ self .cmd .mfu_write_emu_page_data (133 , pwd )
5786+ self .cmd .mfu_write_emu_page_data (134 , pack )
5787+
5788+ self .cmd .set_slot_enable (self .slot_num , TagSenseType .HF , True )
5789+
5790+ print ()
5791+ print (f" - Import complete. Slot { self .slot_num } is now emulating { device_type } ({ file_name } )" )
5792+
5793+
5794+ @lf_em_410x .command ('read' )
55605795class LFEMRead (ReaderRequiredUnit ):
55615796 def args_parser (self ) -> ArgumentParserNoExit :
55625797 parser = ArgumentParserNoExit ()
0 commit comments