diff --git a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c index 032fe115e33f..248a7b11bb1b 100644 --- a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c +++ b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c @@ -526,6 +526,139 @@ MU_TEST(test_lfrfid_protocol_fdxb_read_simple) { protocol_dict_free(dict); } +// Indala224: 224-bit PSK2 frame, no known FC/CN descramble, +// test data uses the Proxmark3-verified reference (lf indala reader output) +#define INDALA224_TEST_DATA_SIZE 28 +#define INDALA224_TEST_DATA \ + { \ + 0x80, 0x00, 0x00, 0x01, 0xB2, 0x35, 0x23, 0xA6, 0xC2, 0xE3, 0x1E, 0xBA, 0x3C, 0xBE, \ + 0xE4, 0xAF, 0xB3, 0xC6, 0xAD, 0x1F, 0xCF, 0x64, 0x93, 0x93, 0x92, 0x8C, 0x14, 0xE5 \ + } +#define INDALA224_BITS_PER_FRAME 224 +#define INDALA224_US_PER_BIT 255 +#define INDALA224_FRAMES_TO_DECODE 3 + +MU_TEST(test_lfrfid_protocol_indala224_roundtrip) { + ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); + mu_assert_int_eq( + INDALA224_TEST_DATA_SIZE, protocol_dict_get_data_size(dict, LFRFIDProtocolIndala224)); + mu_assert_string_eq("Indala224", protocol_dict_get_name(dict, LFRFIDProtocolIndala224)); + mu_assert_string_eq( + "Motorola", protocol_dict_get_manufacturer(dict, LFRFIDProtocolIndala224)); + + const uint8_t data[INDALA224_TEST_DATA_SIZE] = INDALA224_TEST_DATA; + + // Encode: extract bit-level polarity from encoder output. + // PSK encoder yields carrier-level oscillation (duration=1 per half-cycle). + // PulseGlue produces ~16us outputs which are below the decoder's 127us threshold, + // so we extract the bit-level polarity directly: sample the encoder's phase state + // at the start of each bit period (every 32 yields = 16 carrier cycles). + protocol_dict_set_data(dict, LFRFIDProtocolIndala224, data, INDALA224_TEST_DATA_SIZE); + mu_check(protocol_dict_encoder_start(dict, LFRFIDProtocolIndala224)); + + const size_t total_bits = INDALA224_BITS_PER_FRAME * INDALA224_FRAMES_TO_DECODE; + bool* bit_levels = malloc(total_bits); + mu_check(bit_levels != NULL); + + for(size_t i = 0; i < total_bits; i++) { + LevelDuration ld = protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224); + bit_levels[i] = level_duration_get_level(ld); + // Skip remaining 31 yields for this bit period + for(size_t skip = 0; skip < 31; skip++) { + protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224); + } + } + + // Decode: convert bit-level polarities to run-length timing pairs + // and feed directly to decoder (simulates hardware PSK demodulator output). + protocol_dict_decoders_start(dict); + ProtocolId protocol = PROTOCOL_NO; + + size_t i = 0; + while(i < total_bits) { + bool cur = bit_levels[i]; + size_t run = 1; + while(i + run < total_bits && bit_levels[i + run] == cur) { + run++; + } + uint32_t duration = (uint32_t)(run * INDALA224_US_PER_BIT); + + protocol = protocol_dict_decoders_feed(dict, cur, duration); + if(protocol != PROTOCOL_NO) break; + + i += run; + } + + free(bit_levels); + + mu_assert_int_eq(LFRFIDProtocolIndala224, protocol); + uint8_t received_data[INDALA224_TEST_DATA_SIZE] = {0}; + protocol_dict_get_data(dict, protocol, received_data, INDALA224_TEST_DATA_SIZE); + mu_assert_mem_eq(data, received_data, INDALA224_TEST_DATA_SIZE); + + protocol_dict_free(dict); +} + +// Indala224 phase-alternating test: data with odd number of 1-bits +// causes PSK2 carrier phase to invert between consecutive frames. +// Decoder must accept inverted preamble at the second frame boundary. +#define INDALA224_ALT_TEST_DATA \ + { \ + 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, \ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 \ + } + +MU_TEST(test_lfrfid_protocol_indala224_alternating_phase) { + ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); + + const uint8_t data[INDALA224_TEST_DATA_SIZE] = INDALA224_ALT_TEST_DATA; + + protocol_dict_set_data(dict, LFRFIDProtocolIndala224, data, INDALA224_TEST_DATA_SIZE); + mu_check(protocol_dict_encoder_start(dict, LFRFIDProtocolIndala224)); + + const size_t total_bits = INDALA224_BITS_PER_FRAME * INDALA224_FRAMES_TO_DECODE; + bool* bit_levels = malloc(total_bits); + mu_check(bit_levels != NULL); + + for(size_t i = 0; i < total_bits; i++) { + LevelDuration ld = protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224); + bit_levels[i] = level_duration_get_level(ld); + for(size_t skip = 0; skip < 31; skip++) { + protocol_dict_encoder_yield(dict, LFRFIDProtocolIndala224); + } + } + + // Verify phase alternation: frame 1 and frame 2 start with opposite polarity + mu_check(bit_levels[0] != bit_levels[INDALA224_BITS_PER_FRAME]); + + protocol_dict_decoders_start(dict); + ProtocolId protocol = PROTOCOL_NO; + + size_t i = 0; + while(i < total_bits) { + bool cur = bit_levels[i]; + size_t run = 1; + while(i + run < total_bits && bit_levels[i + run] == cur) { + run++; + } + uint32_t duration = (uint32_t)(run * INDALA224_US_PER_BIT); + + protocol = protocol_dict_decoders_feed(dict, cur, duration); + if(protocol != PROTOCOL_NO) break; + + i += run; + } + + free(bit_levels); + + mu_assert_int_eq(LFRFIDProtocolIndala224, protocol); + uint8_t received_data[INDALA224_TEST_DATA_SIZE] = {0}; + protocol_dict_get_data(dict, protocol, received_data, INDALA224_TEST_DATA_SIZE); + mu_assert_mem_eq(data, received_data, INDALA224_TEST_DATA_SIZE); + + protocol_dict_free(dict); +} + MU_TEST_SUITE(test_lfrfid_protocols_suite) { MU_RUN_TEST(test_lfrfid_protocol_em_read_simple); MU_RUN_TEST(test_lfrfid_protocol_em_emulate_simple); @@ -538,6 +671,9 @@ MU_TEST_SUITE(test_lfrfid_protocols_suite) { MU_RUN_TEST(test_lfrfid_protocol_inadala26_emulate_simple); + MU_RUN_TEST(test_lfrfid_protocol_indala224_roundtrip); + MU_RUN_TEST(test_lfrfid_protocol_indala224_alternating_phase); + MU_RUN_TEST(test_lfrfid_protocol_fdxb_read_simple); MU_RUN_TEST(test_lfrfid_protocol_fdxb_emulate_simple); } diff --git a/lib/lfrfid/protocols/lfrfid_protocols.c b/lib/lfrfid/protocols/lfrfid_protocols.c index c6cc57a68446..c3c49f5f89f6 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.c +++ b/lib/lfrfid/protocols/lfrfid_protocols.c @@ -4,6 +4,7 @@ #include "protocol_h10301.h" #include "protocol_idteck.h" #include "protocol_indala26.h" +#include "protocol_indala224.h" #include "protocol_io_prox_xsf.h" #include "protocol_awid.h" #include "protocol_fdx_a.h" @@ -30,6 +31,7 @@ const ProtocolBase* const lfrfid_protocols[] = { [LFRFIDProtocolH10301] = &protocol_h10301, [LFRFIDProtocolIdteck] = &protocol_idteck, [LFRFIDProtocolIndala26] = &protocol_indala26, + [LFRFIDProtocolIndala224] = &protocol_indala224, [LFRFIDProtocolIOProxXSF] = &protocol_io_prox_xsf, [LFRFIDProtocolAwid] = &protocol_awid, [LFRFIDProtocolFDXA] = &protocol_fdx_a, diff --git a/lib/lfrfid/protocols/lfrfid_protocols.h b/lib/lfrfid/protocols/lfrfid_protocols.h index a33e87990aa6..cb3c9353cfcc 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -16,6 +16,7 @@ typedef enum { LFRFIDProtocolH10301, LFRFIDProtocolIdteck, LFRFIDProtocolIndala26, + LFRFIDProtocolIndala224, LFRFIDProtocolIOProxXSF, LFRFIDProtocolAwid, LFRFIDProtocolFDXA, diff --git a/lib/lfrfid/protocols/protocol_indala224.c b/lib/lfrfid/protocols/protocol_indala224.c new file mode 100644 index 000000000000..55350966991e --- /dev/null +++ b/lib/lfrfid/protocols/protocol_indala224.c @@ -0,0 +1,281 @@ +#include +#include +#include +#include "lfrfid_protocols.h" + +#define INDALA224_PREAMBLE_BIT_SIZE (30) +#define INDALA224_PREAMBLE_DATA_SIZE (4) + +#define INDALA224_ENCODED_BIT_SIZE (224) +#define INDALA224_ENCODED_DATA_SIZE \ + (((INDALA224_ENCODED_BIT_SIZE) / 8) + INDALA224_PREAMBLE_DATA_SIZE) +#define INDALA224_ENCODED_DATA_LAST ((INDALA224_ENCODED_BIT_SIZE) / 8) + +#define INDALA224_DECODED_BIT_SIZE (224) +#define INDALA224_DECODED_DATA_SIZE (28) + +#define INDALA224_US_PER_BIT (255) +#define INDALA224_ENCODER_PULSES_PER_BIT (16) + +typedef struct { + uint8_t data_index; + uint8_t bit_clock_index; + bool current_polarity; + bool pulse_phase; +} ProtocolIndala224Encoder; + +typedef struct { + uint8_t encoded_data[INDALA224_ENCODED_DATA_SIZE]; + uint8_t negative_encoded_data[INDALA224_ENCODED_DATA_SIZE]; + + uint8_t data[INDALA224_DECODED_DATA_SIZE]; + ProtocolIndala224Encoder encoder; +} ProtocolIndala224; + +ProtocolIndala224* protocol_indala224_alloc(void) { + ProtocolIndala224* protocol = malloc(sizeof(ProtocolIndala224)); + return protocol; +} + +void protocol_indala224_free(ProtocolIndala224* protocol) { + free(protocol); +} + +uint8_t* protocol_indala224_get_data(ProtocolIndala224* protocol) { + return protocol->data; +} + +void protocol_indala224_decoder_start(ProtocolIndala224* protocol) { + memset(protocol->encoded_data, 0, INDALA224_ENCODED_DATA_SIZE); + memset(protocol->negative_encoded_data, 0, INDALA224_ENCODED_DATA_SIZE); +} + +static bool protocol_indala224_check_preamble(uint8_t* data, size_t bit_index) { + // Normal preamble: 1 followed by 29 zeros + if(bit_lib_get_bits(data, bit_index, 8) != 0b10000000) return false; + if(bit_lib_get_bits(data, bit_index + 8, 8) != 0) return false; + if(bit_lib_get_bits(data, bit_index + 16, 8) != 0) return false; + if(bit_lib_get_bits(data, bit_index + 24, 6) != 0) return false; + return true; +} + +static bool protocol_indala224_check_inverted_preamble(uint8_t* data, size_t bit_index) { + // Inverted preamble: 0 followed by 29 ones (phase-alternating PSK2 cards) + if(bit_lib_get_bits(data, bit_index, 8) != 0b01111111) return false; + if(bit_lib_get_bits(data, bit_index + 8, 8) != 0xFF) return false; + if(bit_lib_get_bits(data, bit_index + 16, 8) != 0xFF) return false; + if(bit_lib_get_bits(data, bit_index + 24, 6) != 0b111111) return false; + return true; +} + +static bool protocol_indala224_can_be_decoded(uint8_t* data) { + if(!protocol_indala224_check_preamble(data, 0)) return false; + // Second frame may have same or inverted preamble (PSK2 phase alternation) + if(!protocol_indala224_check_preamble(data, INDALA224_ENCODED_BIT_SIZE) && + !protocol_indala224_check_inverted_preamble(data, INDALA224_ENCODED_BIT_SIZE)) { + return false; + } + return true; +} + +static bool protocol_indala224_decoder_feed_internal(bool polarity, uint32_t time, uint8_t* data) { + time += (INDALA224_US_PER_BIT / 2); + + size_t bit_count = (time / INDALA224_US_PER_BIT); + bool result = false; + + if(bit_count < INDALA224_ENCODED_BIT_SIZE) { + for(size_t i = 0; i < bit_count; i++) { + bit_lib_push_bit(data, INDALA224_ENCODED_DATA_SIZE, polarity); + if(protocol_indala224_can_be_decoded(data)) { + result = true; + break; + } + } + } + + return result; +} + +static void protocol_indala224_decoder_save(uint8_t* data_to, const uint8_t* data_from) { + // PSK2 differential decode: the shift register contains phase values, not data. + // T5577 PSK2 encodes data as phase transitions: data[n] = phase[n] XOR phase[n+1]. + // Bit 224 (start of second frame) provides phase[n+1] for the last data bit. + for(size_t i = 0; i < INDALA224_DECODED_BIT_SIZE; i++) { + bool phase_current = bit_lib_get_bit(data_from, i); + bool phase_next = bit_lib_get_bit(data_from, i + 1); + bit_lib_set_bit(data_to, i, phase_current ^ phase_next); + } +} + +bool protocol_indala224_decoder_feed(ProtocolIndala224* protocol, bool level, uint32_t duration) { + bool result = false; + + if(duration > (INDALA224_US_PER_BIT / 2)) { + if(protocol_indala224_decoder_feed_internal(level, duration, protocol->encoded_data)) { + protocol_indala224_decoder_save(protocol->data, protocol->encoded_data); + FURI_LOG_D("Indala224", "Positive"); + result = true; + return result; + } + + if(protocol_indala224_decoder_feed_internal( + !level, duration, protocol->negative_encoded_data)) { + protocol_indala224_decoder_save(protocol->data, protocol->negative_encoded_data); + FURI_LOG_D("Indala224", "Negative"); + result = true; + return result; + } + } + + return result; +} + +bool protocol_indala224_encoder_start(ProtocolIndala224* protocol) { + // Store raw data for PSK2 emulation - the yield function handles PSK2 modulation. + memcpy(protocol->encoded_data, protocol->data, INDALA224_DECODED_DATA_SIZE); + + protocol->encoder.data_index = 0; + protocol->encoder.current_polarity = true; + protocol->encoder.pulse_phase = true; + protocol->encoder.bit_clock_index = 0; + + return true; +} + +LevelDuration protocol_indala224_encoder_yield(ProtocolIndala224* protocol) { + LevelDuration level_duration; + ProtocolIndala224Encoder* encoder = &protocol->encoder; + + if(encoder->pulse_phase) { + level_duration = level_duration_make(encoder->current_polarity, 1); + encoder->pulse_phase = false; + } else { + level_duration = level_duration_make(!encoder->current_polarity, 1); + encoder->pulse_phase = true; + + encoder->bit_clock_index++; + if(encoder->bit_clock_index >= INDALA224_ENCODER_PULSES_PER_BIT) { + encoder->bit_clock_index = 0; + + // PSK2: carrier phase flips when data bit is 1 + bool current_bit = bit_lib_get_bit(protocol->encoded_data, encoder->data_index); + if(current_bit) { + encoder->current_polarity = !encoder->current_polarity; + } + + bit_lib_increment_index(encoder->data_index, INDALA224_ENCODED_BIT_SIZE); + } + } + + return level_duration; +} + +static void protocol_indala224_render_data_internal( + ProtocolIndala224* protocol, + FuriString* result, + bool brief) { + if(brief) { + furi_string_printf( + result, + "Raw: %02X%02X%02X%02X\n" + " %02X%02X%02X%02X...", + protocol->data[0], + protocol->data[1], + protocol->data[2], + protocol->data[3], + protocol->data[4], + protocol->data[5], + protocol->data[6], + protocol->data[7]); + } else { + furi_string_printf( + result, + "Raw: %02X%02X%02X%02X %02X%02X%02X%02X\n" + "%02X%02X%02X%02X %02X%02X%02X%02X\n" + "%02X%02X%02X%02X %02X%02X%02X%02X\n" + "%02X%02X%02X%02X", + protocol->data[0], + protocol->data[1], + protocol->data[2], + protocol->data[3], + protocol->data[4], + protocol->data[5], + protocol->data[6], + protocol->data[7], + protocol->data[8], + protocol->data[9], + protocol->data[10], + protocol->data[11], + protocol->data[12], + protocol->data[13], + protocol->data[14], + protocol->data[15], + protocol->data[16], + protocol->data[17], + protocol->data[18], + protocol->data[19], + protocol->data[20], + protocol->data[21], + protocol->data[22], + protocol->data[23], + protocol->data[24], + protocol->data[25], + protocol->data[26], + protocol->data[27]); + } +} + +void protocol_indala224_render_data(ProtocolIndala224* protocol, FuriString* result) { + protocol_indala224_render_data_internal(protocol, result, false); +} + +void protocol_indala224_render_brief_data(ProtocolIndala224* protocol, FuriString* result) { + protocol_indala224_render_data_internal(protocol, result, true); +} + +bool protocol_indala224_write_data(ProtocolIndala224* protocol, void* data) { + LFRFIDWriteRequest* request = (LFRFIDWriteRequest*)data; + bool result = false; + + if(request->write_type == LFRFIDWriteTypeT5577) { + // Config: PSK2, RF/32, 7 data blocks (matches Proxmark3 T55X7_INDALA_224_CONFIG_BLOCK) + // T5577 data blocks contain raw data bits; the chip handles PSK2 modulation. + request->t5577.block[0] = LFRFID_T5577_BITRATE_RF_32 | LFRFID_T5577_MODULATION_PSK2 | + (7 << LFRFID_T5577_MAXBLOCK_SHIFT); + request->t5577.block[1] = bit_lib_get_bits_32(protocol->data, 0, 32); + request->t5577.block[2] = bit_lib_get_bits_32(protocol->data, 32, 32); + request->t5577.block[3] = bit_lib_get_bits_32(protocol->data, 64, 32); + request->t5577.block[4] = bit_lib_get_bits_32(protocol->data, 96, 32); + request->t5577.block[5] = bit_lib_get_bits_32(protocol->data, 128, 32); + request->t5577.block[6] = bit_lib_get_bits_32(protocol->data, 160, 32); + request->t5577.block[7] = bit_lib_get_bits_32(protocol->data, 192, 32); + request->t5577.blocks_to_write = 8; + result = true; + } + return result; +} + +const ProtocolBase protocol_indala224 = { + .name = "Indala224", + .manufacturer = "Motorola", + .data_size = INDALA224_DECODED_DATA_SIZE, + .features = LFRFIDFeaturePSK, + .validate_count = 6, + .alloc = (ProtocolAlloc)protocol_indala224_alloc, + .free = (ProtocolFree)protocol_indala224_free, + .get_data = (ProtocolGetData)protocol_indala224_get_data, + .decoder = + { + .start = (ProtocolDecoderStart)protocol_indala224_decoder_start, + .feed = (ProtocolDecoderFeed)protocol_indala224_decoder_feed, + }, + .encoder = + { + .start = (ProtocolEncoderStart)protocol_indala224_encoder_start, + .yield = (ProtocolEncoderYield)protocol_indala224_encoder_yield, + }, + .render_data = (ProtocolRenderData)protocol_indala224_render_data, + .render_brief_data = (ProtocolRenderData)protocol_indala224_render_brief_data, + .write_data = (ProtocolWriteData)protocol_indala224_write_data, +}; diff --git a/lib/lfrfid/protocols/protocol_indala224.h b/lib/lfrfid/protocols/protocol_indala224.h new file mode 100644 index 000000000000..de23d80349af --- /dev/null +++ b/lib/lfrfid/protocols/protocol_indala224.h @@ -0,0 +1,4 @@ +#pragma once +#include + +extern const ProtocolBase protocol_indala224;