Skip to content

Commit 1a09fba

Browse files
authored
Merge branch 'RfidResearchGroup:main' into t55write
2 parents 12284d5 + 6d30d33 commit 1a09fba

File tree

2 files changed

+237
-1
lines changed

2 files changed

+237
-1
lines changed

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
@@ -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')
55605795
class LFEMRead(ReaderRequiredUnit):
55615796
def args_parser(self) -> ArgumentParserNoExit:
55625797
parser = ArgumentParserNoExit()

0 commit comments

Comments
 (0)