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