Skip to content

Commit afb35e8

Browse files
committed
extend hidprox scan with match info
Add optional HIDPROX_SCAN flags for match metadata, align HPP32 with Proxmark3, and score 32-bit auto-detect to prefer the closest format. CLI shows all matches with confidence when no format hint is provided. Accept zero-length HIDPROX_SCAN payloads for GUI compatibility.
1 parent b108c84 commit afb35e8

File tree

6 files changed

+244
-15
lines changed

6 files changed

+244
-15
lines changed

firmware/application/src/app_cmd.c

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
#include "settings.h"
1515
#include "delayed_reset.h"
1616
#include "netdata.h"
17+
#include "protocols/hidprox.h"
18+
#include "protocols/wiegand.h"
1719

1820

1921
#define NRF_LOG_MODULE_NAME app_cmd
@@ -705,13 +707,57 @@ static data_frame_tx_t *cmd_processor_hidprox_write_to_t55xx(uint16_t cmd, uint1
705707
return data_frame_make(cmd, status, 0, NULL);
706708
}
707709

710+
#define HIDPROX_SCAN_FLAG_ALL_MATCHES (0x01)
711+
712+
static uint16_t append_hidprox_matches(uint8_t *buffer, uint16_t buffer_size) {
713+
wiegand_match_info_t info = {0};
714+
if (!wiegand_get_match_info(&info)) {
715+
return 0;
716+
}
717+
718+
uint16_t index = 13;
719+
if (index + 8 > buffer_size) {
720+
return 0;
721+
}
722+
723+
buffer[index++] = 0xD2;
724+
buffer[index++] = info.count;
725+
num_to_bytes(info.raw, 6, buffer + index);
726+
index += 6;
727+
728+
for (uint8_t i = 0; i < info.count; i++) {
729+
if (index + 9 > buffer_size) {
730+
break;
731+
}
732+
buffer[index++] = info.entries[i].format;
733+
buffer[index++] = info.entries[i].has_parity;
734+
buffer[index++] = info.entries[i].fixed_mismatches;
735+
num_to_bytes(info.entries[i].repacked, 6, buffer + index);
736+
index += 6;
737+
}
738+
739+
return index;
740+
}
741+
708742
static data_frame_tx_t *cmd_processor_hidprox_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {
709-
uint8_t card_data[16] = {0x00};
710-
status = scan_hidprox(card_data, data[0]);
743+
uint8_t card_data[HIDPROX_DATA_SIZE] = {0x00};
744+
if (length > 2) {
745+
return data_frame_make(cmd, STATUS_PAR_ERR, 0, NULL);
746+
}
747+
uint8_t format_hint = (length >= 1) ? data[0] : 0;
748+
uint8_t flags = (length == 2) ? data[1] : 0;
749+
status = scan_hidprox(card_data, format_hint);
711750
if (status != STATUS_LF_TAG_OK) {
712751
return data_frame_make(cmd, status, 0, NULL);
713752
}
714-
return data_frame_make(cmd, STATUS_LF_TAG_OK, sizeof(card_data), card_data);
753+
uint16_t out_len = 13;
754+
if ((flags & HIDPROX_SCAN_FLAG_ALL_MATCHES) != 0) {
755+
uint16_t matches_len = append_hidprox_matches(card_data, sizeof(card_data));
756+
if (matches_len > 0) {
757+
out_len = matches_len;
758+
}
759+
}
760+
return data_frame_make(cmd, STATUS_LF_TAG_OK, out_len, card_data);
715761
}
716762

717763
static data_frame_tx_t *cmd_processor_viking_scan(uint16_t cmd, uint16_t status, uint16_t length, uint8_t *data) {

firmware/application/src/rfid/nfctag/lf/protocols/hidprox.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include "utils/fskdemod.h"
55
#include "wiegand.h"
66

7-
#define HIDPROX_DATA_SIZE (16)
7+
#define HIDPROX_DATA_SIZE (80)
88

99
typedef enum {
1010
STATE_SOF,
@@ -30,4 +30,4 @@ typedef struct {
3030

3131
extern const protocol hidprox;
3232

33-
uint8_t hidprox_t55xx_writer(wiegand_card_t *card, uint32_t *blks);
33+
uint8_t hidprox_t55xx_writer(wiegand_card_t *card, uint32_t *blks);

firmware/application/src/rfid/nfctag/lf/protocols/wiegand.c

Lines changed: 95 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,64 @@ const uint8_t indasc27_cn_map[14] = {3, 15, 5, 8, 24, 1, 13, 6, 9, 12, 11, 23, 2
3535
const uint8_t tecom27_fc_map[11] = {24, 23, 12, 16, 20, 8, 4, 3, 2, 7, 11};
3636
const uint8_t tecom27_cn_map[16] = {21, 22, 15, 18, 19, 1, 5, 9, 10, 6, 0, 17, 14, 13, 25, 26};
3737

38+
static wiegand_match_info_t g_wiegand_match_info = {0};
39+
40+
static void match_reset(uint64_t raw) {
41+
g_wiegand_match_info.valid = 1;
42+
g_wiegand_match_info.count = 0;
43+
g_wiegand_match_info.raw = raw;
44+
memset(g_wiegand_match_info.entries, 0, sizeof(g_wiegand_match_info.entries));
45+
}
46+
47+
static void match_add(uint8_t format, bool has_parity, uint8_t fixed_mismatches, uint64_t repacked) {
48+
if (!g_wiegand_match_info.valid) {
49+
return;
50+
}
51+
if (g_wiegand_match_info.count >= WIEGAND_MATCH_MAX_FORMATS) {
52+
return;
53+
}
54+
wiegand_match_entry_t *entry = &g_wiegand_match_info.entries[g_wiegand_match_info.count++];
55+
entry->format = format;
56+
entry->has_parity = has_parity ? 1 : 0;
57+
entry->fixed_mismatches = fixed_mismatches;
58+
entry->repacked = repacked;
59+
}
60+
61+
static uint64_t validation_mask(uint8_t length, card_format_t format) {
62+
if (length != 32) {
63+
return (1ULL << 38) - 1; // HID Prox payload size (preamble + Wiegand)
64+
}
65+
66+
switch (format) {
67+
case HCP32:
68+
return ((1ULL << 24) - 1) << 7; // CN bits (30..7)
69+
case HPP32:
70+
return (1ULL << 31) - 1; // FC+CN bits (30..0)
71+
case KANTECH:
72+
return ((1ULL << 24) - 1) << 1; // FC+CN bits (24..1)
73+
case WIE32:
74+
return (1ULL << 28) - 1; // FC+CN bits (27..0)
75+
case KASTLE:
76+
return (1ULL << 32) - 1; // full payload (parity + fixed bit)
77+
default:
78+
return (1ULL << 38) - 1;
79+
}
80+
}
81+
3882
wiegand_card_t *wiegand_card_alloc() {
3983
wiegand_card_t *card = (wiegand_card_t *)malloc(sizeof(wiegand_card_t));
4084
memset(card, 0, sizeof(wiegand_card_t));
4185
return card;
4286
}
4387

88+
bool wiegand_get_match_info(wiegand_match_info_t *out) {
89+
if (!g_wiegand_match_info.valid || out == NULL) {
90+
return false;
91+
}
92+
*out = g_wiegand_match_info;
93+
return true;
94+
}
95+
4496
static uint64_t get_nonlinear_fields(uint64_t n, const uint8_t *map, size_t size) {
4597
uint64_t bits = 0x0;
4698
for (int i = 0; (i < size) && (n > 0); i++) {
@@ -276,14 +328,14 @@ static uint64_t pack_hpp32(wiegand_card_t *card) {
276328
uint64_t bits = PREAMBLE_32BIT;
277329
bits <<= 1;
278330
bits = (bits << 12) | (card->facility_code & 0xfff);
279-
bits = (bits << 19) | (card->card_number & 0x7ffff);
331+
bits = (bits << 19) | ((card->card_number >> 10) & 0x7ffff);
280332
return bits;
281333
}
282334

283335
static wiegand_card_t *unpack_hpp32(uint64_t hi, uint64_t lo) {
284336
wiegand_card_t *d = wiegand_card_alloc();
285337
d->facility_code = (lo >> 19) & 0xfff;
286-
d->card_number = (lo >> 0) & 0x7ffff;
338+
d->card_number = ((lo >> 0) & 0x7ffff) << 10;
287339
return d;
288340
}
289341

@@ -831,7 +883,7 @@ static const card_format_table_t formats[] = {
831883
{ATSW30, pack_atsw30, unpack_atsw30, 30, {1, 0xFFF, 0xFFFF, 0, 0}}, // ATS Wiegand 30-bit
832884
{ADT31, pack_adt31, unpack_adt31, 31, {0, 0xF, 0x7FFFFF, 0, 0}}, // HID ADT 31-bit
833885
{HCP32, pack_hcp32, unpack_hcp32, 32, {0, 0, 0x3FFF, 0, 0}}, // HID Check Point 32-bit
834-
{HPP32, pack_hpp32, unpack_hpp32, 32, {0, 0xFFF, 0x7FFFF, 0, 0}}, // HID Hewlett-Packard 32-bit
886+
{HPP32, pack_hpp32, unpack_hpp32, 32, {0, 0xFFF, 0x1FFFFFFF, 0, 0}}, // HID Hewlett-Packard 32-bit
835887
{KASTLE, pack_kastle, unpack_kastle, 32, {1, 0xFF, 0xFFFF, 0x1F, 0}}, // Kastle 32-bit
836888
{KANTECH, pack_kantech, unpack_kantech, 32, {0, 0xFF, 0xFFFF, 0, 0}}, // Indala/Kantech KFS 32-bit
837889
{WIE32, pack_wie32, unpack_wie32, 32, {0, 0xFFF, 0xFFFF, 0, 0}}, // Wiegand 32-bit
@@ -868,6 +920,13 @@ uint64_t pack(wiegand_card_t *card) {
868920
}
869921

870922
wiegand_card_t *unpack(uint8_t format_hint, uint8_t length, uint64_t hi, uint64_t lo) {
923+
if (length == 32 && format_hint == 0) {
924+
match_reset(lo);
925+
} else {
926+
g_wiegand_match_info.valid = 0;
927+
}
928+
wiegand_card_t *best_card = NULL;
929+
uint8_t best_mismatches = 0xFF;
871930
for (int i = 0; i < ARRAY_SIZE(formats); i++) {
872931
if (format_hint != 0 && format_hint != formats[i].format) {
873932
continue;
@@ -883,7 +942,40 @@ wiegand_card_t *unpack(uint8_t format_hint, uint8_t length, uint64_t hi, uint64_
883942
continue;
884943
}
885944
card->format = formats[i].format;
945+
if (formats[i].pack != NULL) {
946+
uint64_t repacked = formats[i].pack(card);
947+
if (repacked == 0) {
948+
free(card);
949+
continue;
950+
}
951+
uint64_t mask = validation_mask(length, formats[i].format);
952+
bool passed = ((repacked & mask) == (lo & mask));
953+
if (!passed) {
954+
free(card);
955+
continue;
956+
}
957+
if (length == 32 && format_hint == 0) {
958+
uint64_t payload_mask = (1ULL << 38) - 1;
959+
uint64_t fixed_mask = payload_mask & ~mask;
960+
uint64_t fixed_diff = (repacked ^ lo) & fixed_mask;
961+
uint8_t mismatches = (uint8_t)__builtin_popcountll(fixed_diff);
962+
match_add(formats[i].format, formats[i].fields.has_parity, mismatches, repacked);
963+
if (mismatches < best_mismatches) {
964+
if (best_card != NULL) {
965+
free(best_card);
966+
}
967+
best_card = card;
968+
best_mismatches = mismatches;
969+
continue;
970+
}
971+
free(card);
972+
continue;
973+
}
974+
}
886975
return card;
887976
}
977+
if (best_card != NULL) {
978+
return best_card;
979+
}
888980
return NULL;
889981
}

firmware/application/src/rfid/nfctag/lf/protocols/wiegand.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,23 @@ typedef struct {
8484
card_format_descriptor_t fields;
8585
} card_format_table_t;
8686

87+
#define WIEGAND_MATCH_MAX_FORMATS (5)
88+
89+
typedef struct {
90+
uint8_t format;
91+
uint8_t has_parity;
92+
uint8_t fixed_mismatches;
93+
uint64_t repacked;
94+
} wiegand_match_entry_t;
95+
96+
typedef struct {
97+
uint8_t valid;
98+
uint8_t count;
99+
uint64_t raw;
100+
wiegand_match_entry_t entries[WIEGAND_MATCH_MAX_FORMATS];
101+
} wiegand_match_info_t;
102+
87103
extern uint64_t pack(wiegand_card_t *card);
88104
extern wiegand_card_t *unpack(uint8_t format_hint, uint8_t length, uint64_t hi, uint64_t lo);
89-
extern wiegand_card_t *wiegand_card_alloc();
105+
extern wiegand_card_t *wiegand_card_alloc();
106+
extern bool wiegand_get_match_info(wiegand_match_info_t *out);

software/script/chameleon_cli_unit.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ def check_limits(format: int, fc: Union[int, None], cn: Union[int, None], il: Un
440440
HIDFormat.ATSW30: [0xFFF, 0xFFFF, 0, 0],
441441
HIDFormat.ADT31: [0xF, 0x7FFFFF, 0, 0],
442442
HIDFormat.HCP32: [0, 0x3FFF, 0, 0],
443-
HIDFormat.HPP32: [0xFFF, 0x7FFFF, 0, 0],
443+
HIDFormat.HPP32: [0xFFF, 0x1FFFFFFF, 0, 0],
444444
HIDFormat.KASTLE: [0xFF, 0xFFFF, 0x1F, 0],
445445
HIDFormat.KANTECH: [0xFF, 0xFFFF, 0, 0],
446446
HIDFormat.WIE32: [0xFFF, 0xFFFF, 0, 0],
@@ -3864,11 +3864,18 @@ def args_parser(self) -> ArgumentParserNoExit:
38643864

38653865
def on_exec(self, args: argparse.Namespace):
38663866
format = 0
3867+
include_matches = args.format is None
38673868
if args.format is not None:
38683869
format = HIDFormat[args.format].value
3869-
(format, fc, cn1, cn2, il, oem) = self.cmd.hidprox_scan(format)
3870+
resp = self.cmd.hidprox_scan(format, include_matches=include_matches)
3871+
3872+
(format, fc, cn1, cn2, il, oem) = resp['fields']
38703873
cn = (cn1 << 32) + cn2
3871-
print(f"HIDProx/{HIDFormat(format)}")
3874+
try:
3875+
format_label = str(HIDFormat(format))
3876+
except ValueError:
3877+
format_label = f"Unknown({format})"
3878+
print(f"HIDProx/{format_label}")
38723879
if fc > 0:
38733880
print(f" FC: {color_string((CG, fc))}")
38743881
if il > 0:
@@ -3877,6 +3884,38 @@ def on_exec(self, args: argparse.Namespace):
38773884
print(f" OEM: {color_string((CG, oem))}")
38783885
print(f" CN: {color_string((CG, cn))}")
38793886

3887+
if include_matches and resp.get('matches'):
3888+
def confidence_label(has_parity: int, mismatches: int) -> str:
3889+
if mismatches == 0 and has_parity:
3890+
return 'high'
3891+
if mismatches == 0 or mismatches == 1:
3892+
return 'medium'
3893+
return f"low (mismatch {mismatches})"
3894+
3895+
raw = resp.get('raw')
3896+
if raw is not None:
3897+
print(f" Raw: 0x{raw:012X}")
3898+
3899+
print(" Matches:")
3900+
matches = sorted(
3901+
resp['matches'],
3902+
key=lambda item: (item['mismatches'], -item['has_parity'])
3903+
)
3904+
for item in matches:
3905+
fmt = item['format']
3906+
mismatches = item['mismatches']
3907+
has_parity = item['has_parity']
3908+
repacked = item['repacked']
3909+
try:
3910+
fmt_name = str(HIDFormat(fmt))
3911+
except ValueError:
3912+
fmt_name = f"format({fmt})"
3913+
confidence = confidence_label(has_parity, mismatches)
3914+
parity_label = "parity" if has_parity else "no parity"
3915+
print(
3916+
f" {fmt_name}: confidence={confidence}, {parity_label}, repacked=0x{repacked:012X}"
3917+
)
3918+
38803919
@lf_hid_prox.command("write")
38813920
class LFHIDProxWriteT55xx(LFHIDIdArgsUnit, ReaderRequiredUnit):
38823921
def args_parser(self) -> ArgumentParserNoExit:

software/script/chameleon_cmd.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -485,15 +485,50 @@ def em410x_write_to_t55xx(self, id_bytes: bytes):
485485
raise ValueError("The id bytes length must equal 5 (EM410X) or 13 (Electra)")
486486

487487
@expect_response(Status.LF_TAG_OK)
488-
def hidprox_scan(self, format: int):
488+
def hidprox_scan(self, format: int, include_matches: bool = False):
489489
"""
490490
Read the length, facility code and card number of HID Prox.
491491
492492
:return:
493493
"""
494-
resp = self.device.send_cmd_sync(Command.HIDPROX_SCAN, struct.pack('!B', format))
494+
if include_matches:
495+
flags = 0x01
496+
payload = struct.pack('!BB', format, flags)
497+
else:
498+
payload = struct.pack('!B', format)
499+
resp = self.device.send_cmd_sync(Command.HIDPROX_SCAN, payload)
495500
if resp.status == Status.LF_TAG_OK:
496-
resp.parsed = struct.unpack('>BIBIBH', resp.data[:13])
501+
fields = struct.unpack('>BIBIBH', resp.data[:13])
502+
matches = []
503+
if len(resp.data) >= 21 and resp.data[13] == 0xD2:
504+
count = resp.data[14]
505+
raw = int.from_bytes(resp.data[15:21], 'big')
506+
offset = 21
507+
for _ in range(count):
508+
if offset + 9 > len(resp.data):
509+
break
510+
fmt = resp.data[offset]
511+
has_parity = resp.data[offset + 1]
512+
mismatches = resp.data[offset + 2]
513+
repacked = int.from_bytes(resp.data[offset + 3:offset + 9], 'big')
514+
matches.append({
515+
'format': fmt,
516+
'has_parity': has_parity,
517+
'mismatches': mismatches,
518+
'repacked': repacked,
519+
})
520+
offset += 9
521+
resp.parsed = {
522+
'fields': fields,
523+
'matches': matches,
524+
'raw': raw,
525+
}
526+
else:
527+
resp.parsed = {
528+
'fields': fields,
529+
'matches': matches,
530+
'raw': None,
531+
}
497532
return resp
498533

499534
@expect_response(Status.LF_TAG_OK)

0 commit comments

Comments
 (0)