Skip to content

Commit fff811f

Browse files
committed
feat(indala): add Indala Python client support
- Add TagSpecificType.Indala (310) and command enums (SET/GET_EMU_ID, SCAN, WRITE_TO_T55XX) to chameleon_enum.py - Add indala_scan(), indala_set_emu_id(), indala_get_emu_id(), and indala_write_to_t55xx() methods to chameleon_cmd.py - Add CLI commands: lf indala read, write, econfig with FC/CN and raw hex input modes - Add indala_encode_raw/indala_decode_raw helpers and slot list display
1 parent f1d8ada commit fff811f

File tree

3 files changed

+260
-1
lines changed

3 files changed

+260
-1
lines changed

software/script/chameleon_cli_unit.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,45 @@ def on_exec(self, args: argparse.Namespace):
718718
raise NotImplementedError("Please implement this")
719719

720720

721+
class LFIndalaIdArgsUnit(DeviceRequiredUnit):
722+
@staticmethod
723+
def add_card_arg(parser: ArgumentParserNoExit, required=False):
724+
group = parser.add_mutually_exclusive_group(required=required)
725+
group.add_argument("-r", "--raw", type=str,
726+
help="Raw 64-bit frame (16 hex chars)",
727+
metavar="<hex>")
728+
group.add_argument("--fc", type=int, dest="_indala_fc",
729+
help="Facility code (26-bit format, use with --cn)",
730+
metavar="<dec>")
731+
parser.add_argument("--cn", type=int, dest="_indala_cn",
732+
help="Card number (26-bit format, use with --fc)",
733+
metavar="<dec>")
734+
return parser
735+
736+
def before_exec(self, args: argparse.Namespace):
737+
if not super().before_exec(args):
738+
return False
739+
fc = getattr(args, '_indala_fc', None)
740+
cn = getattr(args, '_indala_cn', None)
741+
if fc is not None or cn is not None:
742+
if fc is None or cn is None:
743+
raise ArgsParserError("--fc and --cn must be used together")
744+
if not (0 <= fc <= 255):
745+
raise ArgsParserError("FC must be 0-255")
746+
if not (0 <= cn <= 65535):
747+
raise ArgsParserError("CN must be 0-65535")
748+
args.raw = indala_encode_raw(fc, cn).hex()
749+
if args.raw is not None and not re.match(r"^[a-fA-F0-9]{16}$", args.raw):
750+
raise ArgsParserError("Raw must be exactly 16 hex characters")
751+
return True
752+
753+
def args_parser(self) -> ArgumentParserNoExit:
754+
raise NotImplementedError("Please implement this")
755+
756+
def on_exec(self, args: argparse.Namespace):
757+
raise NotImplementedError("Please implement this")
758+
759+
721760
class TagTypeArgsUnit(DeviceRequiredUnit):
722761
@staticmethod
723762
def add_type_args(parser: ArgumentParserNoExit):
@@ -763,6 +802,7 @@ def on_exec(self, args: argparse.Namespace):
763802
lf_ioprox = lf.subgroup("ioprox", "ioProx commands")
764803
lf_pac = lf.subgroup("pac", "PAC/Stanley commands")
765804
lf_viking = lf.subgroup("viking", "Viking commands")
805+
lf_indala = lf.subgroup("indala", "Indala commands")
766806
lf_generic = lf.subgroup("generic", "Generic commands")
767807

768808

@@ -6477,6 +6517,12 @@ def on_exec(self, args: argparse.Namespace):
64776517
raw = pac_encode_raw(id)
64786518
print(f" {'CN:':40}{color_string((CY, id_ascii))}")
64796519
print(f" {'Raw:':40}{color_string((CY, raw.hex().upper()))}")
6520+
if lf_tag_type == TagSpecificType.Indala:
6521+
raw = self.cmd.indala_get_emu_id()
6522+
fc, cn = indala_decode_raw(raw)
6523+
print(f" {'Raw:':40}{color_string((CY, raw.hex().upper()))}")
6524+
print(f" {'FC:':40}{color_string((CG, fc))}")
6525+
print(f" {'Card:':40}{color_string((CG, cn))}")
64806526
if current != selected:
64816527
self.cmd.set_active_slot(selected)
64826528

@@ -6639,6 +6685,166 @@ def on_exec(self, args: argparse.Namespace):
66396685
print(f"ID: {response.hex().upper()}")
66406686

66416687

6688+
def indala_decode_raw(raw: bytes):
6689+
"""Decode Indala 26-bit FC/CN from 8-byte raw frame (PM3-compatible bit mapping)."""
6690+
bits = []
6691+
for b in raw:
6692+
for i in range(7, -1, -1):
6693+
bits.append((b >> i) & 1)
6694+
6695+
fc = 0
6696+
fc |= bits[57] << 7
6697+
fc |= bits[49] << 6
6698+
fc |= bits[44] << 5
6699+
fc |= bits[47] << 4
6700+
fc |= bits[48] << 3
6701+
fc |= bits[53] << 2
6702+
fc |= bits[39] << 1
6703+
fc |= bits[58] << 0
6704+
6705+
cn = 0
6706+
cn |= bits[42] << 15
6707+
cn |= bits[45] << 14
6708+
cn |= bits[43] << 13
6709+
cn |= bits[40] << 12
6710+
cn |= bits[52] << 11
6711+
cn |= bits[36] << 10
6712+
cn |= bits[35] << 9
6713+
cn |= bits[51] << 8
6714+
cn |= bits[46] << 7
6715+
cn |= bits[33] << 6
6716+
cn |= bits[37] << 5
6717+
cn |= bits[54] << 4
6718+
cn |= bits[56] << 3
6719+
cn |= bits[59] << 2
6720+
cn |= bits[50] << 1
6721+
cn |= bits[41] << 0
6722+
6723+
return fc, cn
6724+
6725+
6726+
def indala_encode_raw(fc: int, cn: int) -> bytes:
6727+
"""Encode FC/CN into 8-byte Indala 26-bit raw frame (PM3-compatible bit mapping)."""
6728+
bits = [0] * 64
6729+
6730+
# preamble
6731+
bits[0] = 1
6732+
bits[2] = 1
6733+
bits[32] = 1
6734+
6735+
# fc
6736+
bits[57] = (fc >> 7) & 1
6737+
bits[49] = (fc >> 6) & 1
6738+
bits[44] = (fc >> 5) & 1
6739+
bits[47] = (fc >> 4) & 1
6740+
bits[48] = (fc >> 3) & 1
6741+
bits[53] = (fc >> 2) & 1
6742+
bits[39] = (fc >> 1) & 1
6743+
bits[58] = fc & 1
6744+
6745+
# cn
6746+
bits[42] = (cn >> 15) & 1
6747+
bits[45] = (cn >> 14) & 1
6748+
bits[43] = (cn >> 13) & 1
6749+
bits[40] = (cn >> 12) & 1
6750+
bits[52] = (cn >> 11) & 1
6751+
bits[36] = (cn >> 10) & 1
6752+
bits[35] = (cn >> 9) & 1
6753+
bits[51] = (cn >> 8) & 1
6754+
bits[46] = (cn >> 7) & 1
6755+
bits[33] = (cn >> 6) & 1
6756+
bits[37] = (cn >> 5) & 1
6757+
bits[54] = (cn >> 4) & 1
6758+
bits[56] = (cn >> 3) & 1
6759+
bits[59] = (cn >> 2) & 1
6760+
bits[50] = (cn >> 1) & 1
6761+
bits[41] = cn & 1
6762+
6763+
# checksum (sum of specific cn bits)
6764+
chk = sum([
6765+
(cn >> 14) & 1, (cn >> 12) & 1, (cn >> 9) & 1, (cn >> 8) & 1,
6766+
(cn >> 6) & 1, (cn >> 5) & 1, (cn >> 2) & 1, cn & 1,
6767+
])
6768+
if chk % 2 == 0:
6769+
bits[62], bits[63] = 1, 0
6770+
else:
6771+
bits[62], bits[63] = 0, 1
6772+
6773+
# parity
6774+
p1, p2 = 1, 1
6775+
for i in range(33, 64):
6776+
if i % 2:
6777+
p1 ^= bits[i]
6778+
else:
6779+
p2 ^= bits[i]
6780+
bits[34] = p1
6781+
bits[38] = p2
6782+
6783+
raw = bytearray(8)
6784+
for i in range(64):
6785+
if bits[i]:
6786+
raw[i // 8] |= 1 << (7 - (i % 8))
6787+
return bytes(raw)
6788+
6789+
6790+
def indala_format_output(raw: bytes) -> str:
6791+
"""Format Indala raw bytes as PM3-style output string."""
6792+
fc, cn = indala_decode_raw(raw)
6793+
lines = [f"Indala (len 64) Raw: {raw.hex().upper()}"]
6794+
lines.append(f" Fmt 26 FC: {fc} Card: {cn}")
6795+
return "\n".join(lines)
6796+
6797+
6798+
@lf_indala.command("read")
6799+
class LFIndalaRead(ReaderRequiredUnit):
6800+
def args_parser(self) -> ArgumentParserNoExit:
6801+
parser = ArgumentParserNoExit()
6802+
parser.description = "Scan for an Indala tag"
6803+
return parser
6804+
6805+
def on_exec(self, args: argparse.Namespace):
6806+
resp = self.cmd.indala_scan()
6807+
if resp.status != Status.LF_TAG_OK:
6808+
print(f" Indala scan failed: {resp.status}")
6809+
return
6810+
print(f" {indala_format_output(resp.parsed)}")
6811+
6812+
6813+
@lf_indala.command("write")
6814+
class LFIndalaWriteT55xx(LFIndalaIdArgsUnit, ReaderRequiredUnit):
6815+
def args_parser(self) -> ArgumentParserNoExit:
6816+
parser = ArgumentParserNoExit()
6817+
parser.description = "Clone Indala tag to T55XX (use -r <hex>, or --fc <n> --cn <n>)"
6818+
return self.add_card_arg(parser, required=True)
6819+
6820+
def on_exec(self, args: argparse.Namespace):
6821+
id_bytes = bytes.fromhex(args.raw)
6822+
self.cmd.indala_write_to_t55xx(id_bytes)
6823+
print(f" {indala_format_output(id_bytes)}")
6824+
print(f" Write done. Verify with 'lf indala read'.")
6825+
6826+
6827+
@lf_indala.command("econfig")
6828+
class LFIndalaEconfig(SlotIndexArgsAndGoUnit, LFIndalaIdArgsUnit):
6829+
def args_parser(self) -> ArgumentParserNoExit:
6830+
parser = ArgumentParserNoExit()
6831+
parser.description = "Set or get emulated Indala card (use -r/--fc+--cn to set, omit to get)"
6832+
self.add_slot_args(parser)
6833+
self.add_card_arg(parser)
6834+
return parser
6835+
6836+
def on_exec(self, args: argparse.Namespace):
6837+
if args.raw is not None:
6838+
id_bytes = bytes.fromhex(args.raw)
6839+
self.cmd.indala_set_emu_id(id_bytes)
6840+
print(f" - Set emulated Indala:")
6841+
print(f" {indala_format_output(id_bytes)}")
6842+
else:
6843+
response = self.cmd.indala_get_emu_id()
6844+
print(f" - Get emulated Indala:")
6845+
print(f" {indala_format_output(response)}")
6846+
6847+
66426848
@hw_slot.command("nick")
66436849
class HWSlotNick(SlotIndexArgsUnit, SenseTypeArgsUnit):
66446850
def args_parser(self) -> ArgumentParserNoExit:

software/script/chameleon_cmd.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,7 +571,54 @@ def ioprox_compose_id(self, ver, fc, cn):
571571
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
572572
return resp
573573

574+
def indala_scan(self):
575+
"""
576+
Read the card number of Indala.
577+
578+
:return:
579+
"""
580+
resp = self.device.send_cmd_sync(Command.INDALA_SCAN)
581+
if resp.status == Status.LF_TAG_OK:
582+
resp.parsed = resp.data
583+
return resp
574584

585+
@expect_response(Status.SUCCESS)
586+
def indala_set_emu_id(self, id: bytes):
587+
"""
588+
Set the card number emulated by Indala.
589+
590+
:param id: byte of the card number
591+
:return:
592+
"""
593+
if len(id) != 8:
594+
raise ValueError("Indala ID must be 8 bytes")
595+
return self.device.send_cmd_sync(Command.INDALA_SET_EMU_ID, id)
596+
597+
@expect_response(Status.SUCCESS)
598+
def indala_get_emu_id(self):
599+
"""
600+
Get the emulated Indala card id
601+
"""
602+
resp = self.device.send_cmd_sync(Command.INDALA_GET_EMU_ID)
603+
if resp.status == Status.SUCCESS:
604+
resp.parsed = resp.data
605+
return resp
606+
607+
@expect_response(Status.LF_TAG_OK)
608+
def indala_write_to_t55xx(self, id_bytes: bytes, fc8_override: bool = False):
609+
"""
610+
Write Indala card number into T55XX.
611+
612+
:param id_bytes: 8-byte raw Indala frame
613+
:param fc8_override: Use fc/8 PSK modulation for CU self-test
614+
:return:
615+
"""
616+
if len(id_bytes) != 8:
617+
raise ValueError("The id bytes length must equal 8")
618+
data = struct.pack(f'!8s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
619+
if fc8_override:
620+
data += b'\x01'
621+
return self.device.send_cmd_sync(Command.INDALA_WRITE_TO_T55XX, data)
575622

576623
def lf_sniff(self, timeout_ms: int = 2000):
577624
"""

software/script/chameleon_enum.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ class Command(enum.IntEnum):
148148
PAC_GET_EMU_ID = 5007
149149
IOPROX_SET_EMU_ID = 5008
150150
IOPROX_GET_EMU_ID = 5009
151+
INDALA_SET_EMU_ID = 5026
152+
INDALA_GET_EMU_ID = 5027
153+
INDALA_SCAN = 3035
154+
INDALA_WRITE_TO_T55XX = 3036
151155
EM4X05_SCAN = 3030
152156
EM4X05_READSNIFF = 3032
153157
LF_SNIFF = 3031
@@ -298,7 +302,7 @@ class TagSpecificType(enum.IntEnum):
298302
# Paradox
299303

300304
# PSK Tag-Talk-First 300
301-
# Indala
305+
Indala = 310
302306
# Keri
303307
# NexWatch
304308

@@ -382,6 +386,8 @@ def __str__(self):
382386
return "PAC/Stanley"
383387
elif self == TagSpecificType.Viking:
384388
return "Viking"
389+
elif self == TagSpecificType.Indala:
390+
return "Indala"
385391
elif self == TagSpecificType.MIFARE_Mini:
386392
return "Mifare Mini"
387393
elif self == TagSpecificType.MIFARE_1024:

0 commit comments

Comments
 (0)