Skip to content

Commit 6d30d33

Browse files
authored
Merge pull request RfidResearchGroup#357 from fmuk/pr/nfcimport-v2
feat: add Flipper Zero .nfc file importer for MFU/NTAG slots
2 parents 93c1e15 + dc950c4 commit 6d30d33

2 files changed

Lines changed: 237 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac
66
- Added PAC/Stanley LF protocol support: read, emulate and T55xx clone (@kevihiiin, @danieltwagner)
77
- Fix firmware application USB serial number (@taichunmin)
88
- Added ioProx LF protocol support (read, emulate and T55xx clone)
9+
- Added `hf mfu nfcimport` to import Flipper Zero `.nfc` files into MFU/NTAG emulator slots, with `--amiibo` flag for automatic PWD/PACK derivation (@fmuk)
910
- Added commands to dump and clone Mifare tags
1011
- Fix bad missing tools warning (@suut)
1112
- Fix for FAST_READ command for nfc - mf0 tags

software/script/chameleon_cli_unit.py

Lines changed: 236 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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')
55585793
class LFEMRead(ReaderRequiredUnit):
55595794
def args_parser(self) -> ArgumentParserNoExit:
55605795
parser = ArgumentParserNoExit()

0 commit comments

Comments
 (0)