mirror of
https://github.com/RfidResearchGroup/ChameleonUltra.git
synced 2026-09-11 18:30:14 -07:00
Jablotron uses differential biphase (inverted) at RF/64, 64-bit frames:
bits 0-15: 0xFFFF preamble
bits 16-55: 40-bit data (5 bytes), bit 16 must be 0
bits 56-63: 8-bit checksum = (sum of data bytes) XOR 0x3A
Firmware:
- rfid/nfctag/lf/protocols/jablotron.c - encoder/decoder codec
- rfid/nfctag/lf/utils/diphase.c - inverted-biphase state machine
(shared util, reusable by other diphase protocols)
- rfid/reader/lf/lf_jablotron_data.c - GPIO-interval reader path
- app_cmd.c: JABLOTRON_SCAN, JABLOTRON_WRITE_TO_T55XX,
JABLOTRON_SET_EMU_ID, JABLOTRON_GET_EMU_ID
- tag_base_type.h: TAG_TYPE_JABLOTRON enum
- t55xx.h: T5577_JABLOTRON_CONFIG (DIPHASE modulation, RF/64)
- lf_tag_em.c: load callback, factory-default data, save callback
Python CLI (software/script/):
- lf jablotron read - scan a real tag
- lf jablotron write --id - clone onto T55xx
- lf jablotron econfig -s N --id - set emulator ID on a slot
- hw slot list shows Jablotron ID and decimal card number
Python test (software/script/tests/test_jablotron_modulator.py):
Pure-Python round-trip validator that reimplements the modulator and
diphase decoder, expands PWM entries to an edge stream, and confirms
the decoded data matches the input. Regression guard for both the
firmware's double-frame encoding and the single-frame variant.
Notable PWM design choices:
- Constant-level diphase encoding uses the same PAC pattern:
CC=0 for LOW, CC=counter_top+1 for HIGH. counter_top=31 gives
exactly 32 carrier cycles per half-bit at NRF_PWM_CLK_125kHz.
- The 64-bit frame is encoded twice in the 256-entry PWM buffer with
the internal level variable persisting between the two passes.
This is required for clean PWM looping: a single 64-bit diphase
frame with an odd number of zero bits ends at a level opposite the
starting level, leaving no transition at the loop boundary where
the reader expects one. Encoding twice guarantees a continuous
diphase stream regardless of the data's zero-count parity.
Reference: Proxmark3 cmdlfjablotron.c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1697 lines
61 KiB
Python
1697 lines
61 KiB
Python
import struct
|
|
import ctypes
|
|
from typing import Union
|
|
|
|
import chameleon_com
|
|
from chameleon_utils import expect_response, reconstruct_full_nt, parity_to_str
|
|
from chameleon_enum import Command, SlotNumber, Status, TagSenseType, TagSpecificType
|
|
from chameleon_enum import ButtonPressFunction, ButtonType, MifareClassicDarksideStatus
|
|
from chameleon_enum import MfcKeyType, MfcValueBlockOperator
|
|
|
|
CURRENT_VERSION_SETTINGS = 5
|
|
|
|
new_key = b'\x20\x20\x66\x66'
|
|
old_keys = [b'\x51\x24\x36\x48', b'\x19\x92\x04\x27']
|
|
|
|
class ChameleonCMD:
|
|
"""
|
|
Chameleon cmd function
|
|
"""
|
|
|
|
def __init__(self, chameleon: chameleon_com.ChameleonCom):
|
|
"""
|
|
:param chameleon: chameleon instance, @see chameleon_device.Chameleon
|
|
"""
|
|
self.device = chameleon
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_app_version(self):
|
|
"""
|
|
Get firmware version number(application)
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_APP_VERSION)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('!BB', resp.data)
|
|
# older protocol, must upgrade!
|
|
if resp.status == 0 and resp.data == b'\x00\x01':
|
|
print("Chameleon does not understand new protocol. Please update firmware")
|
|
return chameleon_com.Response(cmd=Command.GET_APP_VERSION,
|
|
status=Status.NOT_IMPLEMENTED)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_chip_id(self):
|
|
"""
|
|
Get device chip id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_CHIP_ID)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data.hex()
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_address(self):
|
|
"""
|
|
Get device address
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_ADDRESS)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data.hex()
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_git_version(self):
|
|
resp = self.device.send_cmd_sync(Command.GET_GIT_VERSION)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data.decode('utf-8')
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_mode(self):
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed, = struct.unpack('!?', resp.data)
|
|
return resp
|
|
|
|
def is_device_reader_mode(self) -> bool:
|
|
"""
|
|
Get device mode, reader or tag.
|
|
|
|
:return: True is reader mode, else tag mode
|
|
"""
|
|
return self.get_device_mode()
|
|
|
|
# Note: Will return NOT_IMPLEMENTED if one tries to set reader mode on Lite
|
|
@expect_response(Status.SUCCESS)
|
|
def change_device_mode(self, mode):
|
|
data = struct.pack('!B', mode)
|
|
return self.device.send_cmd_sync(Command.CHANGE_DEVICE_MODE, data)
|
|
|
|
def set_device_reader_mode(self, reader_mode: bool = True):
|
|
"""
|
|
Change device mode, reader or tag.
|
|
|
|
:param reader_mode: True if reader mode, False if tag mode.
|
|
:return:
|
|
"""
|
|
self.change_device_mode(reader_mode)
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def hf14a_scan(self):
|
|
"""
|
|
14a tags in the scanning field.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.HF14A_SCAN)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
# uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen]
|
|
offset = 0
|
|
data = []
|
|
while offset < len(resp.data):
|
|
uidlen, = struct.unpack_from('!B', resp.data, offset)
|
|
offset += struct.calcsize('!B')
|
|
uid, atqa, sak, atslen = struct.unpack_from(f'!{uidlen}s2s1sB', resp.data, offset)
|
|
offset += struct.calcsize(f'!{uidlen}s2s1sB')
|
|
ats, = struct.unpack_from(f'!{atslen}s', resp.data, offset)
|
|
offset += struct.calcsize(f'!{atslen}s')
|
|
data.append({'uid': uid, 'atqa': atqa, 'sak': sak, 'ats': ats})
|
|
resp.parsed = data
|
|
return resp
|
|
|
|
def mf1_detect_support(self):
|
|
"""
|
|
Detect whether it is mifare classic tag.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF1_DETECT_SUPPORT)
|
|
return resp.status == Status.HF_TAG_OK
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_detect_prng(self):
|
|
"""
|
|
Detect mifare Class of classic nt vulnerabilities.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF1_DETECT_PRNG)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_detect_nt_dist(self, block_known, type_known, key_known):
|
|
"""
|
|
Detect the random number distance of the card.
|
|
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6s', type_known, block_known, key_known)
|
|
resp = self.device.send_cmd_sync(Command.MF1_DETECT_NT_DIST, data)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
uid, dist = struct.unpack('!II', resp.data)
|
|
resp.parsed = {'uid': uid, 'dist': dist}
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_nested_acquire(self, block_known, type_known, key_known, block_target, type_target):
|
|
"""
|
|
Collect the key NT parameters needed for Nested decryption
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target)
|
|
resp = self.device.send_cmd_sync(Command.MF1_NESTED_ACQUIRE, data)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
resp.parsed = [{'nt': nt, 'nt_enc': nt_enc, 'par': par}
|
|
for nt, nt_enc, par in struct.iter_unpack('!IIB', resp.data)]
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_darkside_acquire(self, block_target, type_target, first_recover: Union[int, bool], sync_max):
|
|
"""
|
|
Collect the key parameters needed for Darkside decryption.
|
|
|
|
:param block_target:
|
|
:param type_target:
|
|
:param first_recover:
|
|
:param sync_max:
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BBBB', type_target, block_target, first_recover, sync_max)
|
|
resp = self.device.send_cmd_sync(Command.MF1_DARKSIDE_ACQUIRE, data, timeout=sync_max * 10)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
if resp.data[0] == MifareClassicDarksideStatus.OK:
|
|
darkside_status, uid, nt1, par, ks1, nr, ar = struct.unpack('!BIIQQII', resp.data)
|
|
resp.parsed = (darkside_status, {'uid': uid, 'nt1': nt1, 'par': par, 'ks1': ks1, 'nr': nr, 'ar': ar})
|
|
else:
|
|
resp.parsed = (resp.data[0],)
|
|
return resp
|
|
|
|
@expect_response([Status.HF_TAG_OK, Status.MF_ERR_AUTH])
|
|
def mf1_auth_one_key_block(self, block, type_value: MfcKeyType, key):
|
|
"""
|
|
Verify the mf1 key, only verify the specified type of key for a single sector.
|
|
|
|
:param block:
|
|
:param type_value:
|
|
:param key:
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6s', type_value, block, key)
|
|
resp = self.device.send_cmd_sync(Command.MF1_AUTH_ONE_KEY_BLOCK, data)
|
|
resp.parsed = resp.status == Status.HF_TAG_OK
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_read_one_block(self, block, type_value: MfcKeyType, key):
|
|
"""
|
|
Read one mf1 block.
|
|
|
|
:param block:
|
|
:param type_value:
|
|
:param key:
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6s', type_value, block, key)
|
|
resp = self.device.send_cmd_sync(Command.MF1_READ_ONE_BLOCK, data)
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_write_one_block(self, block, type_value: MfcKeyType, key, block_data):
|
|
"""
|
|
Write mf1 single block.
|
|
|
|
:param block:
|
|
:param type_value:
|
|
:param key:
|
|
:param block_data:
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6s16s', type_value, block, key, block_data)
|
|
resp = self.device.send_cmd_sync(Command.MF1_WRITE_ONE_BLOCK, data)
|
|
resp.parsed = resp.status == Status.HF_TAG_OK
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def hf14a_raw(self, options, resp_timeout_ms=100, data=[], bitlen=None):
|
|
"""
|
|
Send raw cmd to 14a tag.
|
|
|
|
:param options:
|
|
:param resp_timeout_ms:
|
|
:param data:
|
|
:param bit_owned_by_the_last_byte:
|
|
:return:
|
|
"""
|
|
|
|
class CStruct(ctypes.BigEndianStructure):
|
|
_fields_ = [
|
|
("activate_rf_field", ctypes.c_uint8, 1),
|
|
("wait_response", ctypes.c_uint8, 1),
|
|
("append_crc", ctypes.c_uint8, 1),
|
|
("auto_select", ctypes.c_uint8, 1),
|
|
("keep_rf_field", ctypes.c_uint8, 1),
|
|
("check_response_crc", ctypes.c_uint8, 1),
|
|
("reserved", ctypes.c_uint8, 2),
|
|
]
|
|
|
|
cs = CStruct()
|
|
cs.activate_rf_field = options['activate_rf_field']
|
|
cs.wait_response = options['wait_response']
|
|
cs.append_crc = options['append_crc']
|
|
cs.auto_select = options['auto_select']
|
|
cs.keep_rf_field = options['keep_rf_field']
|
|
cs.check_response_crc = options['check_response_crc']
|
|
|
|
if bitlen is None:
|
|
bitlen = len(data) * 8 # bits = bytes * 8(bit)
|
|
else:
|
|
if len(data) == 0:
|
|
raise ValueError(f'bitlen={bitlen} but missing data')
|
|
if not ((len(data) - 1) * 8 < bitlen <= len(data) * 8):
|
|
raise ValueError(f'bitlen={bitlen} incompatible with provided data ({len(data)} bytes), '
|
|
f'must be between {((len(data) - 1) * 8)+1} and {len(data) * 8} included')
|
|
|
|
data = bytes(cs)+struct.pack(f'!HH{len(data)}s', resp_timeout_ms, bitlen, bytearray(data))
|
|
resp = self.device.send_cmd_sync(Command.HF14A_RAW, data, timeout=(resp_timeout_ms // 1000) + 1)
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_manipulate_value_block(self, src_block, src_type: MfcKeyType, src_key, operator: MfcValueBlockOperator, operand, dst_block, dst_type: MfcKeyType, dst_key):
|
|
"""
|
|
1. Increment: increments value from source block and write to dest block
|
|
2. Decrement: decrements value from source block and write to dest block
|
|
3. Restore: copy value from source block and write to dest block
|
|
|
|
|
|
:param src_block:
|
|
:param src_type:
|
|
:param src_key:
|
|
:param operator:
|
|
:param operand:
|
|
:param dst_block:
|
|
:param dst_type:
|
|
:param dst_key:
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6sBiBB6s', src_type, src_block, src_key, operator, operand, dst_type, dst_block, dst_key)
|
|
resp = self.device.send_cmd_sync(Command.MF1_MANIPULATE_VALUE_BLOCK, data)
|
|
resp.parsed = resp.status == Status.HF_TAG_OK
|
|
return resp
|
|
|
|
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO])
|
|
def mf1_check_keys_of_sectors(self, mask: bytes, keys: list[bytes]):
|
|
"""
|
|
Check keys of sectors.
|
|
:return:
|
|
"""
|
|
if len(mask) != 10:
|
|
raise ValueError("len(mask) should be 10")
|
|
if len(keys) < 1 or len(keys) > 83:
|
|
raise ValueError("Invalid len(keys)")
|
|
data = struct.pack(f'!10s{6*len(keys)}s', mask, b''.join(keys))
|
|
|
|
bitsCnt = 80 # maximum sectorKey_to_be_checked
|
|
for b in mask:
|
|
while b > 0:
|
|
[bitsCnt, b] = [bitsCnt - (b & 0b1), b >> 1]
|
|
if bitsCnt < 1:
|
|
# All sectorKey is masked
|
|
return chameleon_com.Response(
|
|
cmd=Command.MF1_CHECK_KEYS_OF_SECTORS,
|
|
status=Status.HF_TAG_OK,
|
|
parsed={'status': Status.HF_TAG_OK},
|
|
)
|
|
# base timeout: 1s
|
|
# auth: len(keys) * sectorKey_to_be_checked * 0.1s
|
|
# read keyB from trailer block: 0.1s
|
|
timeout = 1 + (bitsCnt + 1) * len(keys) * 0.1
|
|
|
|
resp = self.device.send_cmd_sync(Command.MF1_CHECK_KEYS_OF_SECTORS, data, timeout=timeout)
|
|
resp.parsed = {'status': resp.status}
|
|
if len(resp.data) == 490:
|
|
found = ''.join([format(i, '08b') for i in resp.data[0:10]])
|
|
# print(f'{found = }')
|
|
resp.parsed.update({
|
|
'found': resp.data[0:10],
|
|
'sectorKeys': {k: resp.data[6 * k + 10:6 * k + 16] for k, v in enumerate(found) if v == '1'}
|
|
})
|
|
return resp
|
|
|
|
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO, Status.MF_ERR_AUTH])
|
|
def mf1_check_keys_on_block(self, block: int, key_type: int, keys: list[bytes]):
|
|
if key_type not in [0x60, 0x61]:
|
|
raise ValueError("Wrong key type")
|
|
if len(keys) < 1 or len(keys) > 83:
|
|
raise ValueError("Invalid len(keys)")
|
|
data = struct.pack(f'!BBB{6*len(keys)}s', block, key_type, len(keys), b''.join(keys))
|
|
|
|
resp = self.device.send_cmd_sync(Command.MF1_CHECK_KEYS_ON_BLOCK, data, timeout=10)
|
|
|
|
if resp.status == Status.HF_TAG_OK and len(resp.data) == 7:
|
|
found, key = struct.unpack('!B6s', resp.data)
|
|
if found:
|
|
resp.parsed = key
|
|
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_static_nested_acquire(self, block_known, type_known, key_known, block_target, type_target):
|
|
"""
|
|
Collect the key NT parameters needed for StaticNested decryption
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB6sBB', type_known, block_known, key_known, type_target, block_target)
|
|
resp = self.device.send_cmd_sync(Command.MF1_STATIC_NESTED_ACQUIRE, data)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
resp.parsed = {
|
|
'uid': struct.unpack('!I', resp.data[0:4])[0],
|
|
'nts': [
|
|
{
|
|
'nt': nt,
|
|
'nt_enc': nt_enc
|
|
} for nt, nt_enc in struct.iter_unpack('!II', resp.data[4:])
|
|
]
|
|
}
|
|
return resp
|
|
|
|
@expect_response(Status.HF_TAG_OK)
|
|
def mf1_hard_nested_acquire(self, slow, block_known, type_known, key_known, block_target, type_target):
|
|
"""
|
|
Collect the NT_ENC list for HardNested decryption
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BBB6sBB', slow, type_known, block_known, key_known, type_target, block_target)
|
|
resp = self.device.send_cmd_sync(Command.MF1_HARDNESTED_ACQUIRE, data, timeout=30)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
resp.parsed = resp.data # we can return the raw nonces bytes
|
|
return resp
|
|
|
|
@expect_response([Status.HF_TAG_OK, Status.HF_TAG_NO])
|
|
def mf1_static_encrypted_nested_acquire(self, backdoor_key, sector_count, starting_sector):
|
|
data = struct.pack('!6sBB', backdoor_key, sector_count, starting_sector)
|
|
resp = self.device.send_cmd_sync(Command.MF1_ENC_NESTED_ACQUIRE, data, timeout=30)
|
|
if resp.status == Status.HF_TAG_OK:
|
|
resp.parsed = {
|
|
'uid': struct.unpack('!I', resp.data[0:4])[0],
|
|
'nts': {
|
|
'a': [],
|
|
'b': []
|
|
}
|
|
}
|
|
|
|
i = 4
|
|
|
|
while i < len(resp.data):
|
|
resp.parsed['nts']['a'].append(
|
|
{
|
|
'nt': reconstruct_full_nt(resp.data, i),
|
|
'nt_enc': int.from_bytes(resp.data[i + 3: i + 7], byteorder='big'),
|
|
'parity': parity_to_str(resp.data[i + 2])
|
|
}
|
|
)
|
|
|
|
resp.parsed['nts']['b'].append(
|
|
{
|
|
'nt': reconstruct_full_nt(resp.data, i + 7),
|
|
'nt_enc': int.from_bytes(resp.data[i + 10: i + 14], byteorder='big'),
|
|
'parity': parity_to_str(resp.data[i + 9])
|
|
}
|
|
)
|
|
|
|
i += 14
|
|
return resp
|
|
|
|
def hf14a_sniff(self, timeout_ms: int = 5000):
|
|
"""
|
|
Capture ISO14443A reader frames while CU acts as a tag emulator.
|
|
|
|
The firmware installs a sniff callback into the HF14A stack for the
|
|
requested duration, then returns all captured frames packed as:
|
|
[2 bytes: bit count, big-endian] [N bytes: frame data, ceil(bits/8)] ...
|
|
|
|
:param timeout_ms: Listen duration in ms (1-30000, default 5000)
|
|
:return: Raw response — check .status and .data
|
|
"""
|
|
timeout_ms = max(1, min(30000, timeout_ms))
|
|
payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF])
|
|
timeout_s = (timeout_ms // 1000) + 5
|
|
return self.device.send_cmd_sync(Command.HF14A_SNIFF, payload, timeout=timeout_s)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hf14a_get_config(self):
|
|
"""
|
|
Get hf 14a config
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.HF14A_GET_CONFIG)
|
|
if resp.status == Status.SUCCESS:
|
|
bcc, cl2, cl3, rats = struct.unpack('!bbbb', resp.data)
|
|
resp.parsed = {'bcc': bcc,
|
|
'cl2': cl2,
|
|
'cl3': cl3,
|
|
'rats': rats}
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hf14a_set_config(self, data):
|
|
"""
|
|
Set hf 14a config
|
|
|
|
:return:
|
|
"""
|
|
data = struct.pack('!bbbb', data['bcc'], data['cl2'], data['cl3'], data['rats'])
|
|
return self.device.send_cmd_sync(Command.HF14A_SET_CONFIG, data)
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def em410x_scan(self):
|
|
"""
|
|
Read the card number of EM410X.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.EM410X_SCAN)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
tag_type = struct.unpack('!H', resp.data[:2])[0]
|
|
if tag_type == TagSpecificType.EM410X_ELECTRA:
|
|
fmt = '!H13s'
|
|
else:
|
|
fmt = '!H5s'
|
|
resp.parsed = struct.unpack(fmt, resp.data[:struct.calcsize(fmt)]) # tag type + uid
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def em410x_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write EM410X card number into T55XX.
|
|
|
|
:param id_bytes: ID card number
|
|
:return:
|
|
"""
|
|
if len(id_bytes) == 5:
|
|
data = struct.pack(f'!5s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.EM410X_WRITE_TO_T55XX, data)
|
|
if len(id_bytes) == 13:
|
|
data = struct.pack(f'!13s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.EM410X_ELECTRA_WRITE_TO_T55XX, data)
|
|
raise ValueError("The id bytes length must equal 5 (EM410X) or 13 (Electra)")
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def hidprox_scan(self, format: int):
|
|
"""
|
|
Read the length, facility code and card number of HID Prox.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.HIDPROX_SCAN, struct.pack('!B', format))
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = struct.unpack('>BIBIBH', resp.data[:13])
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def hidprox_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write HID Prox card number into T55XX.
|
|
|
|
:param id_bytes: ID card number
|
|
:return:
|
|
"""
|
|
if len(id_bytes) != 13:
|
|
raise ValueError("The id bytes length must equal 13")
|
|
data = struct.pack(f'!13s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.HIDPROX_WRITE_TO_T55XX, data)
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def ioprox_scan(self):
|
|
"""
|
|
Read ioProx (XSF): version, facility, number, raw.
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.IOPROX_SCAN)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def ioprox_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write ioProx card data to a T55XX tag.
|
|
"""
|
|
if len(id_bytes) != 16:
|
|
raise ValueError("The ioProx id bytes length must equal 16")
|
|
|
|
# Pack id_bytes (16), new_key (4), and all old_keys (4 each) into one buffer
|
|
fmt = f'!16s4s{4 * len(old_keys)}s'
|
|
data = struct.pack(fmt, id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.IOPROX_WRITE_TO_T55XX, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def ioprox_decode_raw(self, raw8_bytes):
|
|
"""
|
|
Send 8 raw card bytes to firmware and return 16-byte card data structure.
|
|
Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding.
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.IOPROX_DECODE_RAW, data=raw8_bytes)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def ioprox_compose_id(self, ver, fc, cn):
|
|
"""
|
|
Encode ioProx parameters into a 16-byte card data structure via firmware.
|
|
Response layout: [0]=ver, [1]=fc, [2..3]=cn, [4..11]=raw8, [12..15]=padding.
|
|
"""
|
|
payload = struct.pack(">BBH", ver, fc, cn)
|
|
resp = self.device.send_cmd_sync(Command.IOPROX_COMPOSE_ID, data=payload)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
|
|
return resp
|
|
|
|
|
|
|
|
def lf_sniff(self, timeout_ms: int = 2000):
|
|
"""
|
|
Capture raw LF field ADC samples.
|
|
|
|
The ChameleonUltra samples the LF antenna at 125kHz (8µs/sample).
|
|
Each byte is an 8-bit ADC value: ~0x80 = field on, lower = gap/no field.
|
|
|
|
:param timeout_ms: Capture duration in ms (1-10000, default 2000)
|
|
:return: Raw response object — check .status and .data
|
|
"""
|
|
timeout_ms = max(1, min(10000, timeout_ms))
|
|
payload = bytes([(timeout_ms >> 8) & 0xFF, timeout_ms & 0xFF])
|
|
timeout_s = (timeout_ms // 1000) + 2
|
|
return self.device.send_cmd_sync(Command.LF_SNIFF, payload, timeout=timeout_s)
|
|
|
|
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def em4x05_scan(self, pwd: int = 0):
|
|
"""
|
|
Read an EM4x05 or EM4x69 tag (reader-talk-first).
|
|
|
|
Response payload (14 bytes, big-endian):
|
|
config 4 bytes — block 0 configuration word
|
|
uid 4 bytes — EM4x05 UID
|
|
uid_hi 4 bytes — EM4x69 uid_hi (zero for plain EM4x05)
|
|
is_em4x69 1 byte — 1 if a 64-bit EM4x69 UID was read
|
|
uid_block 1 byte — block number UID was read from
|
|
|
|
:param pwd: 32-bit password for LOGIN (default 0x00000000)
|
|
:return: parsed tuple (config, uid, uid_hi, is_em4x69, uid_block)
|
|
"""
|
|
pwd_bytes = struct.pack('!I', pwd & 0xFFFFFFFF)
|
|
resp = self.device.send_cmd_sync(Command.EM4X05_SCAN, pwd_bytes)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = struct.unpack('!IIIBB', resp.data[:14])
|
|
return resp
|
|
|
|
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def viking_scan(self):
|
|
"""
|
|
Read the card number of Viking.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.VIKING_SCAN)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = resp.data # uid
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def viking_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write Viking card number into T55XX.
|
|
|
|
:param id_bytes: ID card number
|
|
:return:
|
|
"""
|
|
if len(id_bytes) != 4:
|
|
raise ValueError("The id bytes length must equal 4")
|
|
data = struct.pack(f'!4s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.VIKING_WRITE_TO_T55XX, data)
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def pac_scan(self):
|
|
"""
|
|
Read the card ID of PAC/Stanley.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.PAC_SCAN)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = resp.data[:8]
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def pac_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write PAC/Stanley card data to a T55XX tag.
|
|
|
|
:param id_bytes: 8-byte ASCII card ID
|
|
:return:
|
|
"""
|
|
if len(id_bytes) != 8:
|
|
raise ValueError("The id bytes length must equal 8")
|
|
data = struct.pack(f'!8s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.PAC_WRITE_TO_T55XX, data)
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def jablotron_scan(self):
|
|
"""
|
|
Read the card number of Jablotron.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.JABLOTRON_SCAN)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = resp.data[:5]
|
|
return resp
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def jablotron_write_to_t55xx(self, id_bytes: bytes):
|
|
"""
|
|
Write Jablotron card number into T55XX.
|
|
|
|
:param id_bytes: 5-byte Jablotron card ID
|
|
:return:
|
|
"""
|
|
if len(id_bytes) != 5:
|
|
raise ValueError("The id bytes length must equal 5")
|
|
data = struct.pack(f'!5s4s{4*len(old_keys)}s', id_bytes, new_key, b''.join(old_keys))
|
|
return self.device.send_cmd_sync(Command.JABLOTRON_WRITE_TO_T55XX, data)
|
|
|
|
@expect_response(Status.LF_TAG_OK)
|
|
def adc_generic_read(self):
|
|
"""
|
|
Read the ADC when the field is on.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.ADC_GENERIC_READ, None)
|
|
if resp.status == Status.LF_TAG_OK:
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_slot_info(self):
|
|
"""
|
|
Get slots info.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_SLOT_INFO)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = [{'hf': hf, 'lf': lf}
|
|
for hf, lf in struct.iter_unpack('!HH', resp.data)]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_active_slot(self):
|
|
"""
|
|
Get selected slot.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_ACTIVE_SLOT)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_active_slot(self, slot_index: SlotNumber):
|
|
"""
|
|
Set the card slot currently active for use.
|
|
|
|
:param slot_index: Card slot index
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot_index not in slot range
|
|
data = struct.pack('!B', SlotNumber.to_fw(slot_index))
|
|
return self.device.send_cmd_sync(Command.SET_ACTIVE_SLOT, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_slot_tag_type(self, slot_index: SlotNumber, tag_type: TagSpecificType):
|
|
"""
|
|
Set the label type of the emulated card of the current card slot
|
|
Note: This operation will not change the data in the flash,
|
|
and the change of the data in the flash will only be updated at the next save.
|
|
|
|
:param slot_index: Card slot number
|
|
:param tag_type: label type
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot_index not in slot range
|
|
data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type)
|
|
return self.device.send_cmd_sync(Command.SET_SLOT_TAG_TYPE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def delete_slot_sense_type(self, slot_index: SlotNumber, sense_type: TagSenseType):
|
|
"""
|
|
Delete a sense type for a specific slot.
|
|
|
|
:param slot_index: Slot index
|
|
:param sense_type: Sense type to disable
|
|
:return:
|
|
"""
|
|
data = struct.pack('!BB', SlotNumber.to_fw(slot_index), sense_type)
|
|
return self.device.send_cmd_sync(Command.DELETE_SLOT_SENSE_TYPE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_slot_data_default(self, slot_index: SlotNumber, tag_type: TagSpecificType):
|
|
"""
|
|
Set the data of the emulated card in the specified card slot as the default data
|
|
Note: This API will set the data in the flash together.
|
|
|
|
:param slot_index: Card slot number
|
|
:param tag_type: The default label type to set
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot_index not in slot range
|
|
data = struct.pack('!BH', SlotNumber.to_fw(slot_index), tag_type)
|
|
return self.device.send_cmd_sync(Command.SET_SLOT_DATA_DEFAULT, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_slot_enable(self, slot_index: SlotNumber, sense_type: TagSenseType, enabled: bool):
|
|
"""
|
|
Set whether the specified card slot is enabled.
|
|
|
|
:param slot_index: Card slot number
|
|
:param enable: Whether to enable
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot_index not in slot range
|
|
data = struct.pack('!BBB', SlotNumber.to_fw(slot_index), sense_type, enabled)
|
|
return self.device.send_cmd_sync(Command.SET_SLOT_ENABLE, data)
|
|
|
|
def _get_active_lf_tag_type(self) -> TagSpecificType:
|
|
slotinfo = self.get_slot_info()
|
|
active_slot = SlotNumber.from_fw(self.get_active_slot())
|
|
lf_tag_value = slotinfo[active_slot - 1]['lf']
|
|
return TagSpecificType(lf_tag_value)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def em410x_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card number emulated by EM410x.
|
|
|
|
:param id_bytes: byte of the card number
|
|
:return:
|
|
"""
|
|
lf_tag_type = self._get_active_lf_tag_type()
|
|
if lf_tag_type == TagSpecificType.EM410X_ELECTRA:
|
|
expected_len = 13
|
|
elif lf_tag_type == TagSpecificType.EM410X:
|
|
expected_len = 5
|
|
else:
|
|
raise ValueError(f"Active LF slot type {lf_tag_type} is not EM410X")
|
|
|
|
if len(id) != expected_len:
|
|
raise ValueError(f"The id bytes length must equal {expected_len}")
|
|
|
|
data = struct.pack(f'!{expected_len}s', id)
|
|
return self.device.send_cmd_sync(Command.EM410X_SET_EMU_ID, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def em410x_get_emu_id(self):
|
|
"""
|
|
Get the emulated EM410x card id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.EM410X_GET_EMU_ID)
|
|
if resp.status == Status.SUCCESS:
|
|
data = resp.data
|
|
id_bytes = data
|
|
tag_type = None
|
|
|
|
if len(data) >= 2:
|
|
try:
|
|
candidate = TagSpecificType(int.from_bytes(data[:2], byteorder='big'))
|
|
except ValueError:
|
|
candidate = None
|
|
|
|
if candidate in (TagSpecificType.EM410X, TagSpecificType.EM410X_ELECTRA):
|
|
expected_len = 13 if candidate == TagSpecificType.EM410X_ELECTRA else 5
|
|
if len(data) == expected_len + 2:
|
|
tag_type = candidate
|
|
id_bytes = data[2:2 + expected_len]
|
|
|
|
if tag_type is None:
|
|
lf_tag_type = self._get_active_lf_tag_type()
|
|
if lf_tag_type == TagSpecificType.EM410X_ELECTRA:
|
|
expected_len = 13
|
|
elif lf_tag_type == TagSpecificType.EM410X:
|
|
expected_len = 5
|
|
else:
|
|
expected_len = len(data)
|
|
id_bytes = data[:expected_len]
|
|
|
|
resp.parsed = id_bytes
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hidprox_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card number emulated by HID Prox.
|
|
|
|
:param id_bytes: byte of the card number
|
|
:return:
|
|
"""
|
|
if len(id) != 13:
|
|
raise ValueError("The id bytes length must equal 13")
|
|
return self.device.send_cmd_sync(Command.HIDPROX_SET_EMU_ID, id)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hidprox_get_emu_id(self):
|
|
"""
|
|
Get the emulated HID Prox card id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.HIDPROX_GET_EMU_ID)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('>BIBIBH', resp.data[:13])
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def ioprox_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card number emulated by ioProx.
|
|
|
|
:param id_bytes: byte of the card number
|
|
:return:
|
|
"""
|
|
if len(id) != 16:
|
|
raise ValueError("The id bytes length must equal 16")
|
|
return self.device.send_cmd_sync(Command.IOPROX_SET_EMU_ID, id)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def ioprox_get_emu_id(self):
|
|
"""
|
|
Get the emulated ioProx card id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.IOPROX_GET_EMU_ID)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack(">BBH8sBBBB", resp.data[:16])
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def viking_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card number emulated by Viking.
|
|
|
|
:param id_bytes: byte of the card number
|
|
:return:
|
|
"""
|
|
if len(id) != 4:
|
|
raise ValueError("The id bytes length must equal 4")
|
|
data = struct.pack('4s', id)
|
|
return self.device.send_cmd_sync(Command.VIKING_SET_EMU_ID, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def viking_get_emu_id(self):
|
|
"""
|
|
Get the emulated Viking card id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.VIKING_GET_EMU_ID)
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def pac_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card ID emulated by PAC/Stanley.
|
|
|
|
:param id: 8-byte ASCII card ID
|
|
:return:
|
|
"""
|
|
if len(id) != 8:
|
|
raise ValueError("The id bytes length must equal 8")
|
|
data = struct.pack('8s', id)
|
|
return self.device.send_cmd_sync(Command.PAC_SET_EMU_ID, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def pac_get_emu_id(self):
|
|
"""
|
|
Get the emulated PAC/Stanley card ID
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.PAC_GET_EMU_ID)
|
|
resp.parsed = resp.data[:8]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def jablotron_set_emu_id(self, id: bytes):
|
|
"""
|
|
Set the card number emulated by Jablotron.
|
|
|
|
:param id: 5-byte Jablotron card ID
|
|
:return:
|
|
"""
|
|
if len(id) != 5:
|
|
raise ValueError("The id bytes length must equal 5")
|
|
data = struct.pack('5s', id)
|
|
return self.device.send_cmd_sync(Command.JABLOTRON_SET_EMU_ID, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def jablotron_get_emu_id(self):
|
|
"""
|
|
Get the emulated Jablotron card id
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.JABLOTRON_GET_EMU_ID)
|
|
resp.parsed = resp.data[:5]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_detection_enable(self, enabled: bool):
|
|
"""
|
|
Set whether to enable the detection of the current card slot.
|
|
|
|
:param enable: Whether to enable
|
|
:return:
|
|
"""
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_DETECTION_ENABLE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_get_detection_count(self):
|
|
"""
|
|
Get the statistics of the current detection records.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_COUNT)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed, = struct.unpack('!I', resp.data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_get_detection_log(self, index: int):
|
|
"""
|
|
Get detection logs from the specified index position.
|
|
|
|
:param index: start index
|
|
:return:
|
|
"""
|
|
data = struct.pack('!I', index)
|
|
resp = self.device.send_cmd_sync(Command.MF1_GET_DETECTION_LOG, data)
|
|
if resp.status == Status.SUCCESS:
|
|
# convert
|
|
result_list = []
|
|
pos = 0
|
|
while pos < len(resp.data):
|
|
block, bitfield, uid, nt, nr, ar = struct.unpack_from('!BB4s4s4s4s', resp.data, pos)
|
|
result_list.append({
|
|
'block': block,
|
|
'type': ['A', 'B'][bitfield & 0x01],
|
|
'is_nested': bool(bitfield & 0x02),
|
|
'uid': uid.hex(),
|
|
'nt': nt.hex(),
|
|
'nr': nr.hex(),
|
|
'ar': ar.hex()
|
|
})
|
|
pos += struct.calcsize('!BB4s4s4s4s')
|
|
resp.parsed = result_list
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_detection_enable(self):
|
|
"""
|
|
Get whether NTAG password detection is enabled.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_ENABLE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('!B', resp.data)[0] == 1
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_set_detection_enable(self, enabled: bool):
|
|
"""
|
|
Set whether to enable NTAG password detection.
|
|
|
|
:param enable: Whether to enable
|
|
:return:
|
|
"""
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_DETECTION_ENABLE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_detection_count(self):
|
|
"""
|
|
Get the statistics of the current NTAG password detection records.
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_COUNT)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('!I', resp.data)[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_detection_log(self, index: int):
|
|
"""
|
|
Get NTAG password detection logs from the specified index position.
|
|
|
|
:param index: start index
|
|
:return:
|
|
"""
|
|
data = struct.pack('!I', index)
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_DETECTION_LOG, data)
|
|
if resp.status == Status.SUCCESS:
|
|
# convert - each log entry is just a 4-byte password
|
|
result_list = []
|
|
pos = 0
|
|
while pos < len(resp.data):
|
|
password = resp.data[pos:pos+4]
|
|
result_list.append({
|
|
'password': password.hex()
|
|
})
|
|
pos += 4
|
|
resp.parsed = result_list
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_write_emu_block_data(self, block_start: int, block_data: bytes):
|
|
"""
|
|
Set the block data of the analog card of MF1.
|
|
|
|
:param block_start: Start setting the location of block data, including this location
|
|
:param block_data: The byte buffer of the block data to be set can contain multiple block data,
|
|
automatically from block_start increment
|
|
:return:
|
|
"""
|
|
data = struct.pack(f'!B{len(block_data)}s', block_start, block_data)
|
|
return self.device.send_cmd_sync(Command.MF1_WRITE_EMU_BLOCK_DATA, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_read_emu_block_data(self, block_start: int, block_count: int):
|
|
"""
|
|
Gets data for selected block range
|
|
"""
|
|
data = struct.pack('!BB', block_start, block_count)
|
|
resp = self.device.send_cmd_sync(Command.MF1_READ_EMU_BLOCK_DATA, data)
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_get_emu_pages_count(self):
|
|
"""
|
|
Gets the number of pages available in the current MF0 / NTAG slot
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_PAGE_COUNT)
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_read_emu_page_data(self, page_start: int, page_count: int):
|
|
"""
|
|
Gets data for selected block range
|
|
"""
|
|
data = struct.pack('!BB', page_start, page_count)
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_READ_EMU_PAGE_DATA, data)
|
|
resp.parsed = resp.data
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_write_emu_page_data(self, page_start: int, data: bytes):
|
|
"""
|
|
Gets data for selected block range
|
|
"""
|
|
count = len(data) >> 2
|
|
|
|
assert (len(data) % 4) == 0
|
|
assert (page_start >= 0) and (count + page_start) <= 256
|
|
|
|
data = struct.pack('!BB', page_start, count) + data
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_WRITE_EMU_PAGE_DATA, data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_read_emu_counter_data(self, index: int) -> tuple[int, bool]:
|
|
"""
|
|
Gets data for selected counter
|
|
"""
|
|
data = struct.pack('!B', index)
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_COUNTER_DATA, data)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = (((resp.data[2] << 16) | (resp.data[1] << 8) | resp.data[0]), resp.data[3] == 0xBD)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_write_emu_counter_data(self, index: int, value: int, reset_tearing: bool):
|
|
"""
|
|
Sets data for selected counter
|
|
"""
|
|
data = struct.pack('!BBBB', index | (int(reset_tearing) << 7),
|
|
value & 0xFF, (value >> 8) & 0xFF, (value >> 16) & 0xFF)
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_SET_COUNTER_DATA, data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mfu_reset_auth_cnt(self):
|
|
"""
|
|
Resets authentication counter
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_RESET_AUTH_CNT, bytes())
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hf14a_set_anti_coll_data(self, uid: bytes, atqa: bytes, sak: bytes, ats: bytes = b''):
|
|
"""
|
|
Set anti-collision data of current HF slot (UID/SAK/ATQA/ATS).
|
|
|
|
:param uid: uid bytes
|
|
:param atqa: atqa bytes
|
|
:param sak: sak bytes
|
|
:param ats: ats bytes (optional)
|
|
:return:
|
|
"""
|
|
data = struct.pack(f'!B{len(uid)}s2s1sB{len(ats)}s', len(uid), uid, atqa, sak, len(ats), ats)
|
|
return self.device.send_cmd_sync(Command.HF14A_SET_ANTI_COLL_DATA, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType, name: str):
|
|
"""
|
|
Set the nick name of the slot.
|
|
|
|
:param slot: Card slot number
|
|
:param sense_type: field type
|
|
:param name: Card slot nickname
|
|
:return:
|
|
"""
|
|
encoded_name = name.encode(encoding="utf8")
|
|
if len(encoded_name) > 32:
|
|
raise ValueError("Your tag nick name too long.")
|
|
# SlotNumber() will raise error for us if slot not in slot range
|
|
data = struct.pack(f'!BB{len(encoded_name)}s', SlotNumber.to_fw(slot), sense_type, encoded_name)
|
|
return self.device.send_cmd_sync(Command.SET_SLOT_TAG_NICK, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType):
|
|
"""
|
|
Get the nick name of the slot.
|
|
|
|
:param slot: Card slot number
|
|
:param sense_type: field type
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot not in slot range
|
|
data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type)
|
|
resp = self.device.send_cmd_sync(Command.GET_SLOT_TAG_NICK, data)
|
|
resp.parsed = resp.data.decode(encoding="utf8")
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_all_slot_nicks(self):
|
|
resp = self.device.send_cmd_sync(Command.GET_ALL_SLOT_NICKS, b'')
|
|
|
|
slots = []
|
|
i = 0
|
|
slot_index = 0
|
|
|
|
while i < len(resp.data) and slot_index < 8:
|
|
slot_names = {'hf': '', 'lf': ''}
|
|
|
|
if i < len(resp.data):
|
|
hf_len = resp.data[i]
|
|
i += 1
|
|
if hf_len > 0 and i + hf_len <= len(resp.data):
|
|
slot_names['hf'] = resp.data[i:i + hf_len].decode(encoding="utf8", errors="ignore")
|
|
i += hf_len
|
|
else:
|
|
i += hf_len
|
|
|
|
if i < len(resp.data):
|
|
lf_len = resp.data[i]
|
|
i += 1
|
|
if lf_len > 0 and i + lf_len <= len(resp.data):
|
|
slot_names['lf'] = resp.data[i:i + lf_len].decode(encoding="utf8", errors="ignore")
|
|
i += lf_len
|
|
else:
|
|
i += lf_len
|
|
|
|
slots.append(slot_names)
|
|
slot_index += 1
|
|
|
|
resp.parsed = slots
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def delete_slot_tag_nick(self, slot: SlotNumber, sense_type: TagSenseType):
|
|
"""
|
|
Delete the nick name of the slot.
|
|
|
|
:param slot: Card slot number
|
|
:param sense_type: field type
|
|
:return:
|
|
"""
|
|
# SlotNumber() will raise error for us if slot not in slot range
|
|
data = struct.pack('!BB', SlotNumber.to_fw(slot), sense_type)
|
|
return self.device.send_cmd_sync(Command.DELETE_SLOT_TAG_NICK, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_get_emulator_config(self):
|
|
"""
|
|
Get array of Mifare Classic emulators settings:
|
|
[0] - mf1_is_detection_enable (mfkey32)
|
|
[1] - mf1_is_gen1a_magic_mode
|
|
[2] - mf1_is_gen2_magic_mode
|
|
[3] - mf1_is_use_mf1_coll_res (use UID/BCC/SAK/ATQA from 0 block)
|
|
[4] - mf1_get_write_mode
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF1_GET_EMULATOR_CONFIG)
|
|
if resp.status == Status.SUCCESS:
|
|
b1, b2, b3, b4, b5 = struct.unpack('!????B', resp.data)
|
|
resp.parsed = {'detection': b1,
|
|
'gen1a_mode': b2,
|
|
'gen2_mode': b3,
|
|
'block_anti_coll_mode': b4,
|
|
'write_mode': b5}
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_gen1a_mode(self, enabled: bool):
|
|
"""
|
|
Set gen1a magic mode
|
|
"""
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_GEN1A_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_gen2_mode(self, enabled: bool):
|
|
"""
|
|
Set gen2 magic mode
|
|
"""
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_GEN2_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_block_anti_coll_mode(self, enabled: bool):
|
|
"""
|
|
Set 0 block anti-collision data
|
|
"""
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_BLOCK_ANTI_COLL_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_write_mode(self, mode: int):
|
|
"""
|
|
Set write mode
|
|
"""
|
|
data = struct.pack('!B', mode)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_WRITE_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def slot_data_config_save(self):
|
|
"""
|
|
Update the configuration and data of the card slot to flash.
|
|
:return:
|
|
"""
|
|
return self.device.send_cmd_sync(Command.SLOT_DATA_CONFIG_SAVE)
|
|
|
|
def enter_bootloader(self):
|
|
"""
|
|
Reboot into DFU mode (bootloader)
|
|
:return:
|
|
"""
|
|
self.device.send_cmd_auto(Command.ENTER_BOOTLOADER, close=True)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_animation_mode(self):
|
|
"""
|
|
Get animation mode value
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_ANIMATION_MODE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_enabled_slots(self):
|
|
"""
|
|
Get enabled slots
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_ENABLED_SLOTS)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = [{'hf': hf, 'lf': lf} for hf, lf in struct.iter_unpack('!BB', resp.data)]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_animation_mode(self, value: int):
|
|
"""
|
|
Set animation mode value
|
|
"""
|
|
data = struct.pack('!B', value)
|
|
return self.device.send_cmd_sync(Command.SET_ANIMATION_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def reset_settings(self):
|
|
"""
|
|
Reset settings stored in flash memory
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.RESET_SETTINGS)
|
|
resp.parsed = resp.status == Status.SUCCESS
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def save_settings(self):
|
|
"""
|
|
Store settings to flash memory
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.SAVE_SETTINGS)
|
|
resp.parsed = resp.status == Status.SUCCESS
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def wipe_fds(self):
|
|
"""
|
|
Reset to factory settings
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.WIPE_FDS)
|
|
resp.parsed = resp.status == Status.SUCCESS
|
|
self.device.close()
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_battery_info(self):
|
|
"""
|
|
Get battery info
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_BATTERY_INFO)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('!HB', resp.data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_button_press_config(self, button: ButtonType):
|
|
"""
|
|
Get config of button press function
|
|
"""
|
|
data = struct.pack('!B', button)
|
|
resp = self.device.send_cmd_sync(Command.GET_BUTTON_PRESS_CONFIG, data)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_button_press_config(self, button: ButtonType, function: ButtonPressFunction):
|
|
"""
|
|
Set config of button press function
|
|
"""
|
|
data = struct.pack('!BB', button, function)
|
|
return self.device.send_cmd_sync(Command.SET_BUTTON_PRESS_CONFIG, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_long_button_press_config(self, button: ButtonType):
|
|
"""
|
|
Get config of long button press function
|
|
"""
|
|
data = struct.pack('!B', button)
|
|
resp = self.device.send_cmd_sync(Command.GET_LONG_BUTTON_PRESS_CONFIG, data)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_long_button_press_config(self, button: ButtonType, function: ButtonPressFunction):
|
|
"""
|
|
Set config of long button press function
|
|
"""
|
|
data = struct.pack('!BB', button, function)
|
|
return self.device.send_cmd_sync(Command.SET_LONG_BUTTON_PRESS_CONFIG, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_ble_connect_key(self, key: str):
|
|
"""
|
|
Set config of ble connect key
|
|
"""
|
|
data_bytes = key.encode(encoding='ascii')
|
|
|
|
# check key length
|
|
if len(data_bytes) != 6:
|
|
raise ValueError("The ble connect key length must be 6")
|
|
|
|
data = struct.pack('6s', data_bytes)
|
|
return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_KEY, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_ble_pairing_key(self):
|
|
"""
|
|
Get config of ble connect key
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_BLE_PAIRING_KEY)
|
|
resp.parsed = resp.data.decode(encoding='ascii')
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def delete_all_ble_bonds(self):
|
|
"""
|
|
From peer manager delete all bonds.
|
|
"""
|
|
return self.device.send_cmd_sync(Command.DELETE_ALL_BLE_BONDS)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_capabilities(self):
|
|
"""
|
|
Get list of commands that client understands
|
|
"""
|
|
try:
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_CAPABILITIES)
|
|
except chameleon_com.CMDInvalidException:
|
|
print("Chameleon does not understand get_device_capabilities command. Please update firmware")
|
|
return chameleon_com.Response(cmd=Command.GET_DEVICE_CAPABILITIES,
|
|
status=Status.NOT_IMPLEMENTED)
|
|
else:
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = [x[0] for x in struct.iter_unpack('!H', resp.data)]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_model(self):
|
|
"""
|
|
Get device model
|
|
0 - Chameleon Ultra
|
|
1 - Chameleon Lite
|
|
"""
|
|
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_MODEL)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_device_settings(self):
|
|
"""
|
|
Get all possible settings
|
|
For version 5:
|
|
settings[0] = SETTINGS_CURRENT_VERSION; // current version
|
|
settings[1] = settings_get_animation_config(); // animation mode
|
|
settings[2] = settings_get_button_press_config('A'); // short A button press mode
|
|
settings[3] = settings_get_button_press_config('B'); // short B button press mode
|
|
settings[4] = settings_get_long_button_press_config('A'); // long A button press mode
|
|
settings[5] = settings_get_long_button_press_config('B'); // long B button press mode
|
|
settings[6] = settings_get_ble_pairing_enable(); // does device require pairing
|
|
settings[7:13] = settings_get_ble_pairing_key(); // BLE pairing key
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_DEVICE_SETTINGS)
|
|
if resp.status == Status.SUCCESS:
|
|
if resp.data[0] > CURRENT_VERSION_SETTINGS:
|
|
raise ValueError("Settings version in app older than Chameleon. "
|
|
"Please upgrade client")
|
|
if resp.data[0] < CURRENT_VERSION_SETTINGS:
|
|
raise ValueError("Settings version in app newer than Chameleon. "
|
|
"Please upgrade Chameleon firmware")
|
|
settings_version, animation_mode, btn_press_A, btn_press_B, btn_long_press_A, \
|
|
btn_long_press_B, ble_pairing_enable, ble_pairing_key = \
|
|
struct.unpack('!BBBBBBB6s', resp.data)
|
|
resp.parsed = {'settings_version': settings_version,
|
|
'animation_mode': animation_mode,
|
|
'btn_press_A': btn_press_A,
|
|
'btn_press_B': btn_press_B,
|
|
'btn_long_press_A': btn_long_press_A,
|
|
'btn_long_press_B': btn_long_press_B,
|
|
'ble_pairing_enable': ble_pairing_enable,
|
|
'ble_pairing_key': ble_pairing_key}
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def hf14a_get_anti_coll_data(self):
|
|
"""
|
|
Get anti-collision data from current HF slot (UID/SAK/ATQA/ATS)
|
|
|
|
:return:
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.HF14A_GET_ANTI_COLL_DATA)
|
|
if resp.status == Status.SUCCESS and len(resp.data) > 0:
|
|
# uidlen[1]|uid[uidlen]|atqa[2]|sak[1]|atslen[1]|ats[atslen]
|
|
offset = 0
|
|
uidlen, = struct.unpack_from('!B', resp.data, offset)
|
|
offset += struct.calcsize('!B')
|
|
uid, atqa, sak, atslen = struct.unpack_from(f'!{uidlen}s2s1sB', resp.data, offset)
|
|
offset += struct.calcsize(f'!{uidlen}s2s1sB')
|
|
ats, = struct.unpack_from(f'!{atslen}s', resp.data, offset)
|
|
offset += struct.calcsize(f'!{atslen}s')
|
|
resp.parsed = {'uid': uid, 'atqa': atqa, 'sak': sak, 'ats': ats}
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_uid_magic_mode(self):
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_UID_MAGIC_MODE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed, = struct.unpack('!?', resp.data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_set_uid_magic_mode(self, enabled: bool):
|
|
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_UID_MAGIC_MODE, struct.pack('?', enabled))
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_version_data(self):
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_VERSION_DATA)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[:8]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_set_version_data(self, data: bytes):
|
|
assert len(data) == 8
|
|
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_VERSION_DATA, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_signature_data(self):
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_SIGNATURE_DATA)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[:32]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_set_signature_data(self, data: bytes):
|
|
assert len(data) == 32
|
|
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_SIGNATURE_DATA, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_get_write_mode(self):
|
|
"""
|
|
Get write mode for MF0/NTAG
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.MF0_NTAG_GET_WRITE_MODE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = resp.data[0]
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf0_ntag_set_write_mode(self, mode: int):
|
|
"""
|
|
Set write mode for MF0/NTAG
|
|
"""
|
|
data = struct.pack('!B', mode)
|
|
return self.device.send_cmd_sync(Command.MF0_NTAG_SET_WRITE_MODE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def get_ble_pairing_enable(self):
|
|
"""
|
|
Is ble pairing enable?
|
|
|
|
:return: True if pairing is enable, False if pairing disabled
|
|
"""
|
|
resp = self.device.send_cmd_sync(Command.GET_BLE_PAIRING_ENABLE)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed, = struct.unpack('!?', resp.data)
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def set_ble_pairing_enable(self, enabled: bool):
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.SET_BLE_PAIRING_ENABLE, data)
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_get_field_off_do_reset(self):
|
|
resp = self.device.send_cmd_sync(Command.MF1_GET_FIELD_OFF_DO_RESET)
|
|
if resp.status == Status.SUCCESS:
|
|
resp.parsed = struct.unpack('!B', resp.data)[0] == 1
|
|
return resp
|
|
|
|
@expect_response(Status.SUCCESS)
|
|
def mf1_set_field_off_do_reset(self, enabled: bool):
|
|
data = struct.pack('!B', enabled)
|
|
return self.device.send_cmd_sync(Command.MF1_SET_FIELD_OFF_DO_RESET, data)
|
|
|
|
|
|
def test_fn():
|
|
# connect to chameleon
|
|
dev = chameleon_com.ChameleonCom()
|
|
try:
|
|
dev.open('com19')
|
|
except chameleon_com.OpenFailException:
|
|
dev.open('/dev/ttyACM0')
|
|
|
|
cml = ChameleonCMD(dev)
|
|
ver = cml.get_app_version()
|
|
print(f"Firmware number of application: {ver[0]}.{ver[1]}")
|
|
chip = cml.get_device_chip_id()
|
|
print(f"Device chip id: {chip}")
|
|
|
|
# change to reader mode
|
|
cml.set_device_reader_mode()
|
|
|
|
options = {
|
|
'activate_rf_field': 1,
|
|
'wait_response': 1,
|
|
'append_crc': 0,
|
|
'auto_select': 0,
|
|
'keep_rf_field': 1,
|
|
'check_response_crc': 0,
|
|
}
|
|
|
|
try:
|
|
# unlock 1
|
|
resp = cml.hf14a_raw(options=options, resp_timeout_ms=1000, data=[0x40], bitlen=7)
|
|
|
|
if resp[0] == 0x0a:
|
|
print("Gen1A unlock 1 success")
|
|
# unlock 2
|
|
resp = cml.hf14a_raw(options=options, resp_timeout_ms=1000, data=[0x43])
|
|
if resp[0] == 0x0a:
|
|
print("Gen1A unlock 2 success")
|
|
print("Start dump gen1a memory...")
|
|
# Transfer with crc
|
|
options['append_crc'] = 1
|
|
options['check_response_crc'] = 1
|
|
block = 0
|
|
while block < 64:
|
|
# Tag read block cmd
|
|
cmd_read_gen1a_block = [0x30, block]
|
|
if block == 63:
|
|
options['keep_rf_field'] = 0
|
|
resp = cml.hf14a_raw(options=options, resp_timeout_ms=100, data=cmd_read_gen1a_block)
|
|
|
|
print(f"Block {block} : {resp.hex()}")
|
|
block += 1
|
|
|
|
else:
|
|
print("Gen1A unlock 2 fail")
|
|
raise
|
|
else:
|
|
print("Gen1A unlock 1 fail")
|
|
raise
|
|
except Exception:
|
|
options['keep_rf_field'] = 0
|
|
options['wait_response'] = 0
|
|
cml.hf14a_raw(options=options)
|
|
|
|
# disconnect
|
|
dev.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
test_fn()
|