mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-05-12 11:22:59 -07:00
Merge branch 'RfidResearchGroup:main' into t55write
This commit is contained in:
@@ -6,6 +6,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac
|
||||
- Added PAC/Stanley LF protocol support: read, emulate and T55xx clone (@kevihiiin, @danieltwagner)
|
||||
- Fix firmware application USB serial number (@taichunmin)
|
||||
- Added ioProx LF protocol support (read, emulate and T55xx clone)
|
||||
- Added `hf mfu nfcimport` to import Flipper Zero `.nfc` files into MFU/NTAG emulator slots, with `--amiibo` flag for automatic PWD/PACK derivation (@fmuk)
|
||||
- Added commands to dump and clone Mifare tags
|
||||
- Fix bad missing tools warning (@suut)
|
||||
- Fix for FAST_READ command for nfc - mf0 tags
|
||||
|
||||
@@ -5556,7 +5556,242 @@ class HFMFUEDetect(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
|
||||
print(f"{actual_index:3d}: {color_string((CY, password.upper()))}")
|
||||
|
||||
|
||||
@lf_em_410x.command("read")
|
||||
@hf_mfu.command('nfcimport')
|
||||
class HFMFUNfcImport(SlotIndexArgsAndGoUnit, DeviceRequiredUnit):
|
||||
# Mapping from Flipper Zero device type strings to CU TagSpecificType
|
||||
FLIPPER_TYPE_MAP = {
|
||||
'NTAG203': TagSpecificType.NTAG_215, # best-effort: no native NTAG203 support
|
||||
'NTAG210': TagSpecificType.NTAG_210,
|
||||
'NTAG212': TagSpecificType.NTAG_212,
|
||||
'NTAG213': TagSpecificType.NTAG_213,
|
||||
'NTAG215': TagSpecificType.NTAG_215,
|
||||
'NTAG216': TagSpecificType.NTAG_216,
|
||||
'NTAGI2C1K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2C2K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2CPlus1K': TagSpecificType.NTAG_216, # best-effort
|
||||
'NTAGI2CPlus2K': TagSpecificType.NTAG_216, # best-effort
|
||||
'Mifare Ultralight': TagSpecificType.MF0ICU1,
|
||||
'Mifare Ultralight C': TagSpecificType.MF0ICU2,
|
||||
'Mifare Ultralight 11': TagSpecificType.MF0UL11,
|
||||
'Mifare Ultralight 21': TagSpecificType.MF0UL21,
|
||||
# "Mifare Ultralight EV1" is disambiguated by page count in on_exec
|
||||
}
|
||||
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
parser.description = 'Import a Flipper Zero .nfc file into a MIFARE Ultralight / NTAG emulator slot'
|
||||
self.add_slot_args(parser)
|
||||
parser.add_argument('-f', '--file', required=True, type=str, help="Path to Flipper Zero .nfc file")
|
||||
parser.add_argument('--amiibo', action='store_true', default=False,
|
||||
help="Derive and write correct PWD/PACK for amiibo (NTAG215)")
|
||||
return parser
|
||||
|
||||
def on_exec(self, args: argparse.Namespace):
|
||||
file_path = args.file
|
||||
file_name = os.path.basename(file_path)
|
||||
|
||||
# --- Parse the .nfc file ---
|
||||
try:
|
||||
with open(file_path, 'r') as f:
|
||||
lines = f.readlines()
|
||||
except FileNotFoundError:
|
||||
print(color_string((CR, f"File not found: {file_path}")))
|
||||
return
|
||||
except OSError as e:
|
||||
print(color_string((CR, f"Error reading file: {e}")))
|
||||
return
|
||||
|
||||
device_type = None
|
||||
uid = None
|
||||
atqa = None
|
||||
sak = None
|
||||
signature = None
|
||||
version = None
|
||||
counters = {}
|
||||
tearing = {}
|
||||
pages_total = None
|
||||
pages = {}
|
||||
|
||||
for line in lines:
|
||||
line = line.strip()
|
||||
if line.startswith('#') or not line:
|
||||
continue
|
||||
|
||||
if line.startswith('Device type:'):
|
||||
device_type = line.split(':', 1)[1].strip()
|
||||
elif line.startswith('UID:'):
|
||||
uid = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('ATQA:'):
|
||||
atqa = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('SAK:'):
|
||||
sak = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Signature:'):
|
||||
signature = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Mifare version:'):
|
||||
version = bytes.fromhex(line.split(':', 1)[1].strip().replace(' ', ''))
|
||||
elif line.startswith('Counter '):
|
||||
match = re.match(r'Counter\s+(\d+):\s+(\d+)', line)
|
||||
if match:
|
||||
counters[int(match.group(1))] = int(match.group(2))
|
||||
elif line.startswith('Tearing '):
|
||||
match = re.match(r'Tearing\s+(\d+):\s+([0-9A-Fa-f]+)', line)
|
||||
if match:
|
||||
tearing[int(match.group(1))] = int(match.group(2), 16)
|
||||
elif line.startswith('Pages total:'):
|
||||
pages_total = int(line.split(':', 1)[1].strip())
|
||||
elif line.startswith('Page '):
|
||||
match = re.match(r'Page\s+(\d+):\s+(.*)', line)
|
||||
if match:
|
||||
page_num = int(match.group(1))
|
||||
page_data = bytes.fromhex(match.group(2).strip().replace(' ', ''))
|
||||
pages[page_num] = page_data
|
||||
|
||||
# --- Validate required fields ---
|
||||
if device_type is None:
|
||||
print(color_string((CR, "No 'Device type' found in .nfc file.")))
|
||||
return
|
||||
if uid is None:
|
||||
print(color_string((CR, "No 'UID' found in .nfc file.")))
|
||||
return
|
||||
if atqa is None:
|
||||
print(color_string((CR, "No 'ATQA' found in .nfc file.")))
|
||||
return
|
||||
if sak is None:
|
||||
print(color_string((CR, "No 'SAK' found in .nfc file.")))
|
||||
return
|
||||
|
||||
# --- Map device type to TagSpecificType ---
|
||||
tag_type = self.FLIPPER_TYPE_MAP.get(device_type)
|
||||
|
||||
if tag_type is None and device_type.startswith('Mifare Ultralight EV1'):
|
||||
# Disambiguate EV1 by page count
|
||||
nr = pages_total if pages_total else len(pages)
|
||||
tag_type = TagSpecificType.MF0UL11 if nr <= 20 else TagSpecificType.MF0UL21
|
||||
|
||||
if tag_type is None:
|
||||
print(color_string((CR, f"Unsupported Flipper device type: '{device_type}'")))
|
||||
print(f" Supported types: {', '.join(sorted(self.FLIPPER_TYPE_MAP.keys()))}, Mifare Ultralight EV1")
|
||||
return
|
||||
|
||||
# --- Print summary ---
|
||||
print(f"Importing Flipper NFC file: {file_name}")
|
||||
print(f" Device type: {device_type} -> {tag_type}")
|
||||
print(f" UID: {uid.hex(' ').upper()}")
|
||||
print(f" ATQA: {atqa.hex(' ').upper()} SAK: {sak.hex().upper()}")
|
||||
if version:
|
||||
print(f" Version: {version.hex(' ').upper()}")
|
||||
if signature:
|
||||
print(f" Signature: {signature.hex(' ').upper()}")
|
||||
if counters:
|
||||
print(f" Counters: {', '.join(str(counters.get(i, 0)) for i in range(max(counters.keys()) + 1))}")
|
||||
nr_pages = pages_total if pages_total else len(pages)
|
||||
print(f" Pages: {nr_pages}")
|
||||
print()
|
||||
|
||||
# --- Step 1: Set slot tag type ---
|
||||
print(f"Setting slot {self.slot_num} tag type to {tag_type}...")
|
||||
self.cmd.set_slot_tag_type(self.slot_num, tag_type)
|
||||
self.cmd.set_slot_data_default(self.slot_num, tag_type)
|
||||
# Must re-activate slot after changing type so subsequent commands target the new type
|
||||
self.cmd.set_active_slot(self.slot_num)
|
||||
|
||||
# --- Step 2: Set anti-collision data ---
|
||||
print("Setting anti-collision data...")
|
||||
self.cmd.hf14a_set_anti_coll_data(uid, atqa, sak)
|
||||
|
||||
# --- Step 3: Set version data ---
|
||||
if version and len(version) == 8:
|
||||
print("Setting version data...")
|
||||
try:
|
||||
self.cmd.mf0_ntag_set_version_data(version)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
|
||||
print(color_string((CY, " Warning: tag type does not support GET_VERSION.")))
|
||||
|
||||
# --- Step 4: Set signature data ---
|
||||
if signature and len(signature) == 32:
|
||||
print("Setting signature data...")
|
||||
try:
|
||||
self.cmd.mf0_ntag_set_signature_data(signature)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, TimeoutError):
|
||||
print(color_string((CY, " Warning: tag type does not support READ_SIG.")))
|
||||
|
||||
# --- Step 5: Set counter and tearing data ---
|
||||
if counters:
|
||||
print("Setting counter data...")
|
||||
# NTAG types have a single counter accessed via NFC at index 2,
|
||||
# but stored at firmware internal index 0
|
||||
ntag_types = {
|
||||
TagSpecificType.NTAG_210, TagSpecificType.NTAG_212,
|
||||
TagSpecificType.NTAG_213, TagSpecificType.NTAG_215,
|
||||
TagSpecificType.NTAG_216,
|
||||
}
|
||||
for i in sorted(counters.keys()):
|
||||
value = counters[i]
|
||||
if value > 0xFFFFFF:
|
||||
print(color_string((CY, f" Warning: counter {i} value {value:#x} exceeds 24-bit, skipping.")))
|
||||
continue
|
||||
# Map Flipper counter index to firmware internal index
|
||||
if tag_type in ntag_types:
|
||||
if i != 2:
|
||||
continue # NTAG only has counter at NFC index 2
|
||||
fw_index = 0
|
||||
else:
|
||||
fw_index = i
|
||||
# Reset tearing flag if tearing byte is BD (default / no tearing)
|
||||
tearing_val = tearing.get(i, 0x00)
|
||||
reset_tearing = (tearing_val == 0xBD or tearing_val == 0x00)
|
||||
try:
|
||||
self.cmd.mfu_write_emu_counter_data(fw_index, value, reset_tearing)
|
||||
except (ValueError, chameleon_com.CMDInvalidException, UnexpectedResponseError, TimeoutError):
|
||||
print(color_string((CY, f" Warning: could not set counter {i}.")))
|
||||
|
||||
# --- Step 6: Write page data ---
|
||||
if pages:
|
||||
# Get total pages for the configured slot
|
||||
slot_pages = self.cmd.mfu_get_emu_pages_count()
|
||||
|
||||
# Build contiguous data from parsed pages
|
||||
max_page = max(pages.keys())
|
||||
write_pages = min(max_page + 1, slot_pages)
|
||||
|
||||
print(f"Writing {write_pages} pages...", end=' ', flush=True)
|
||||
|
||||
page = 0
|
||||
while page < write_pages:
|
||||
cur_count = min(16, write_pages - page)
|
||||
batch = bytearray()
|
||||
for p in range(page, page + cur_count):
|
||||
batch.extend(pages.get(p, b'\x00\x00\x00\x00'))
|
||||
self.cmd.mfu_write_emu_page_data(page, bytes(batch))
|
||||
page += cur_count
|
||||
|
||||
print("done")
|
||||
|
||||
# --- Step 7: Derive and write amiibo PWD/PACK ---
|
||||
if args.amiibo:
|
||||
if tag_type != TagSpecificType.NTAG_215:
|
||||
print(color_string((CY, f" Warning: --amiibo flag ignored (tag type is {tag_type}, not NTAG 215).")))
|
||||
elif uid is None or len(uid) != 7:
|
||||
print(color_string((CY, " Warning: --amiibo flag ignored (UID is not 7 bytes).")))
|
||||
else:
|
||||
pwd = bytes([
|
||||
0xAA ^ uid[1] ^ uid[3],
|
||||
0x55 ^ uid[2] ^ uid[4],
|
||||
0xAA ^ uid[3] ^ uid[5],
|
||||
0x55 ^ uid[4] ^ uid[6],
|
||||
])
|
||||
pack = bytes([0x80, 0x80, 0x00, 0x00])
|
||||
print(f"Setting amiibo PWD: {pwd.hex(' ').upper()}, PACK: {pack[:2].hex(' ').upper()}...")
|
||||
self.cmd.mfu_write_emu_page_data(133, pwd)
|
||||
self.cmd.mfu_write_emu_page_data(134, pack)
|
||||
|
||||
self.cmd.set_slot_enable(self.slot_num, TagSenseType.HF, True)
|
||||
|
||||
print()
|
||||
print(f" - Import complete. Slot {self.slot_num} is now emulating {device_type} ({file_name})")
|
||||
|
||||
|
||||
@lf_em_410x.command('read')
|
||||
class LFEMRead(ReaderRequiredUnit):
|
||||
def args_parser(self) -> ArgumentParserNoExit:
|
||||
parser = ArgumentParserNoExit()
|
||||
|
||||
Reference in New Issue
Block a user