Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ This project uses the changelog in accordance with [keepchangelog](http://keepac

## [unreleased][unreleased]
- Fix for static nested key recovery (@jekkos)
- Added `hf 14a config` to deal with badly configured cards (@azuwis)

## [v2.1.0][2025-09-02]
- Added UV, formatter and linter. Contribution guidelines. (@GameTec-live)
Expand Down
18 changes: 18 additions & 0 deletions firmware/application/src/app_cmd.c
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,21 @@ static data_frame_tx_t *cmd_processor_hf14a_raw(uint16_t cmd, uint16_t status, u
return data_frame_make(cmd, status, resp_length, resp);
}

static data_frame_tx_t *cmd_processor_hf14a_get_config(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
hf14a_config_t *hc = get_hf14a_config();
return data_frame_make(cmd, STATUS_SUCCESS, sizeof(hf14a_config_t), (uint8_t *)hc);
}

static data_frame_tx_t *cmd_processor_hf14a_set_config(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
if (length != sizeof(hf14a_config_t)) {
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
}
hf14a_config_t hc;
memcpy(&hc, data, sizeof(hf14a_config_t));
set_hf14a_config(&hc);
return data_frame_make(cmd, STATUS_SUCCESS, 0, NULL);
}

static data_frame_tx_t *cmd_processor_mf1_manipulate_value_block(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
typedef struct {
uint8_t src_type;
Expand Down Expand Up @@ -1610,6 +1625,9 @@ static cmd_data_map_t m_data_cmd_map[] = {
{ DATA_CMD_HF14A_SET_FIELD_ON, before_reader_run, cmd_processor_hf14a_set_field_on, NULL },
{ DATA_CMD_HF14A_SET_FIELD_OFF, before_reader_run, cmd_processor_hf14a_set_field_off, NULL },

{ DATA_CMD_HF14A_GET_CONFIG, NULL, cmd_processor_hf14a_get_config, NULL },
{ DATA_CMD_HF14A_SET_CONFIG, NULL, cmd_processor_hf14a_set_config, NULL },

#endif

{ DATA_CMD_HF14A_GET_ANTI_COLL_DATA, NULL, cmd_processor_hf14a_get_anti_coll_data, NULL },
Expand Down
3 changes: 3 additions & 0 deletions firmware/application/src/data_cmd.h
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@
#define DATA_CMD_HF14A_SET_FIELD_ON (2100)
#define DATA_CMD_HF14A_SET_FIELD_OFF (2101)

#define DATA_CMD_HF14A_GET_CONFIG (2200)
#define DATA_CMD_HF14A_SET_CONFIG (2201)

//
// ******************************************************************

Expand Down
65 changes: 63 additions & 2 deletions firmware/application/src/rfid/reader/hf/rc522.c
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ static autotimer *g_timeout_auto_timer;
#define SPI_INSTANCE 0 /**< SPI instance index. */
static const nrf_drv_spi_t s_spiHandle = NRF_DRV_SPI_INSTANCE(SPI_INSTANCE); // SPI instance

/*
Default HF 14a config is set to:
forcebcc = 0 (expect valid BCC)
forcecl2 = 0 (auto)
forcecl3 = 0 (auto)
forcerats = 0 (auto)
*/
static hf14a_config_t hf14aconfig = { 0, 0, 0, 0 };

#define ONCE_OPT __attribute__((optimize("O3")))

/**
Expand Down Expand Up @@ -690,6 +699,7 @@ uint8_t pcd_14a_reader_scan_once(picc_14a_tag_t *tag) {
uint8_t status;
uint8_t do_cascade = 1;
uint8_t cascade_level = 0;
bool do_rats = false;

// OK we will select at least at cascade 1, lets see if first byte of UID was 0x88 in
// which case we need to make a cascade 2 request and select - this is a long UID
Expand Down Expand Up @@ -727,7 +737,12 @@ uint8_t pcd_14a_reader_scan_once(picc_14a_tag_t *tag) {
uint8_t bcc = sel_uid[2] ^ sel_uid[3] ^ sel_uid[4] ^ sel_uid[5]; // calculate BCC
if (sel_uid[6] != bcc) {
NRF_LOG_INFO("BCC%d incorrect, got 0x%02x, expected 0x%02x\n", cascade_level, sel_uid[6], bcc);
return STATUS_HF_ERR_BCC;

if (hf14aconfig.forcebcc == 0) {
return STATUS_HF_ERR_BCC;
} else if (hf14aconfig.forcebcc == 1) {
sel_uid[6] = bcc;
} // else use card BCC
}

crc_14a_append(sel_uid, 7); // calculate and add CRC
Expand All @@ -745,6 +760,20 @@ uint8_t pcd_14a_reader_scan_once(picc_14a_tag_t *tag) {
// If UID is 0X88 The beginning of the form shows that the UID is not complete
// In the next cycle, we need to make an increased level, return to the end of the anti -rushing collision, and complete the level
do_cascade = (((tag->sak & 0x04) /* && uid_resp[0] == 0x88 */) > 0);

if (cascade_level == 0) {
if (hf14aconfig.forcecl2 == 2) {
do_cascade = false;
} else if (hf14aconfig.forcecl2 == 1) {
do_cascade = true;
} // else 0==auto
} else if (cascade_level == 1) {
if (hf14aconfig.forcecl3 == 2) {
do_cascade = false;
} else if (hf14aconfig.forcecl3 == 1) {
do_cascade = true;
} // else 0==auto
}
if (do_cascade) {
// Remove first byte, 0x88 is not an UID byte, it CT, see page 3 of:
// http://www.nxp.com/documents/application_note/AN10927.pdf
Expand All @@ -761,7 +790,17 @@ uint8_t pcd_14a_reader_scan_once(picc_14a_tag_t *tag) {
// Therefore + 1
tag->cascade = cascade_level + 1;
}
if (tag->sak & 0x20) {

if (hf14aconfig.forcerats == 2) {
do_rats = false;
NRF_LOG_INFO("Skipping RATS according to hf 14a config");
} else if (hf14aconfig.forcerats == 1) {
do_rats = true;
NRF_LOG_INFO("Forcing RATS according to hf 14a config");
} else {
do_rats = tag->sak & 0x20;
}
if (do_rats) {
// Tag supports 14443-4, sending RATS
uint16_t ats_size;
status = pcd_14a_reader_ats_request(tag->ats, &ats_size, 0xFF * 8);
Expand Down Expand Up @@ -1539,3 +1578,25 @@ uint8_t pcd_14a_reader_raw_cmd(bool openRFField, bool waitResp, bool appendCrc,

return status;
}

void set_hf14a_config(const hf14a_config_t *hc) {
if ((hc->forcebcc >= 0) && (hc->forcebcc <= 2)) {
hf14aconfig.forcebcc = hc->forcebcc;
}

if ((hc->forcecl2 >= 0) && (hc->forcecl2 <= 2)) {
hf14aconfig.forcecl2 = hc->forcecl2;
}

if ((hc->forcecl3 >= 0) && (hc->forcecl3 <= 2)) {
hf14aconfig.forcecl3 = hc->forcecl3;
}

if ((hc->forcerats >= 0) && (hc->forcerats <= 2)) {
hf14aconfig.forcerats = hc->forcerats;
}
}

hf14a_config_t *get_hf14a_config(void) {
return &hf14aconfig;
}
11 changes: 11 additions & 0 deletions firmware/application/src/rfid/reader/hf/rc522.h
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,17 @@ typedef struct {
uint8_t ats_len; // 14443-4 answer to select size
} PACKED picc_14a_tag_t;

// A struct used to send hf14a-configs
typedef struct {
int8_t forcebcc; // 0:expect valid BCC 1:force using computed BCC 2:force using card BCC
int8_t forcecl2; // 0:auto 1:force executing CL2 2:force skipping CL2
int8_t forcecl3; // 0:auto 1:force executing CL3 2:force skipping CL3
int8_t forcerats; // 0:auto 1:force executing RATS 2:force skipping RATS
} PACKED hf14a_config_t;

hf14a_config_t *get_hf14a_config(void);
void set_hf14a_config(const hf14a_config_t *hc);

#ifdef __cplusplus
extern "C" {
#endif
Expand Down
83 changes: 83 additions & 0 deletions software/script/chameleon_cli_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import serial.tools.list_ports
import threading
import struct
from enum import Enum
from multiprocessing import Pool, cpu_count
from typing import Union
from pathlib import Path
Expand Down Expand Up @@ -754,6 +755,88 @@ def on_exec(self, args: argparse.Namespace):
print(f' - Chameleon {model}, Version: {fw_version} ({git_version})')


@hf_14a.command('config')
class HF14AConfig(DeviceRequiredUnit):
class Config(Enum):
def __new__(cls, value, desc):
obj = object.__new__(cls)
obj._value_ = value
obj.desc = desc
return obj

@classmethod
def choices(cls):
return [elem.name for elem in cls]

@classmethod
def format(cls, index):
item = cls(index)
color = CG if index == 0 else CR
return f' - {cls.__name__.upper()} override: {color_string((color, item.name))} ( {item.desc} )'

@classmethod
def help(cls):
return ' / '.join([f'{elem.desc}' for elem in cls])

class Bcc(Config):
std = (0, "follow standard")
fix = (1, "fix bad BCC")
ignore = (2, "ignore bad BCC, always use card BCC")

class Cl2(Config):
std = (0, "follow standard")
force = (1, "always do CL2")
skip = (2, "always skip CL2")

class Cl3(Config):
std = (0, "follow standard")
force = (1, "always do CL3")
skip = (2, "always skip CL3")

class Rats(Config):
std = (0, "follow standard")
force = (1, "always do RATS")
skip = (2, "always skip RATS")

def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = 'Configure 14a settings (use with caution)'
parser.add_argument('--std', action='store_true', help='Reset default configuration (follow standard)')
parser.add_argument('--bcc', type=str, choices=self.Bcc.choices(), help=self.Bcc.help())
parser.add_argument('--cl2', type=str, choices=self.Cl2.choices(), help=self.Cl2.help())
parser.add_argument('--cl3', type=str, choices=self.Cl3.choices(), help=self.Cl3.help())
parser.add_argument('--rats', type=str, choices=self.Rats.choices(), help=self.Rats.help())
return parser

def on_exec(self, args: argparse.Namespace):
change_requested = False
if args.std:
config = {'bcc': 0, 'cl2': 0, 'cl3': 0, 'rats': 0}
change_requested = True
else:
config = self.cmd.hf14a_get_config()
if args.bcc:
config['bcc'] = self.Bcc[args.bcc].value
change_requested = True
if args.cl2:
config['cl2'] = self.Cl2[args.cl2].value
change_requested = True
if args.cl3:
config['cl3'] = self.Cl3[args.cl3].value
change_requested = True
if args.rats:
config['rats'] = self.Rats[args.rats].value
change_requested = True
if change_requested:
self.cmd.hf14a_set_config(config)
config = self.cmd.hf14a_get_config()
print('HF 14a config')
print(self.Bcc.format(config['bcc']))
print(self.Cl2.format(config['cl2']))
print(self.Cl3.format(config['cl3']))
print(self.Rats.format(config['rats']))


@hf_14a.command('scan')
class HF14AScan(ReaderRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
Expand Down
26 changes: 26 additions & 0 deletions software/script/chameleon_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,32 @@ def mf1_static_encrypted_nested_acquire(self, backdoor_key, sector_count, starti
i += 14
return resp

@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):
"""
Expand Down
2 changes: 2 additions & 0 deletions software/script/chameleon_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ class Command(enum.IntEnum):
MF1_HARDNESTED_ACQUIRE = 2013
MF1_ENC_NESTED_ACQUIRE = 2014
MF1_CHECK_KEYS_ON_BLOCK = 2015
HF14A_GET_CONFIG = 2200
HF14A_SET_CONFIG = 2201

EM410X_SCAN = 3000
EM410X_WRITE_TO_T55XX = 3001
Expand Down
Loading