From 507b59e10c8c1d905506e5784f339ab3f5c1b6c6 Mon Sep 17 00:00:00 2001 From: Matthew Carroll Date: Wed, 7 Oct 2026 05:24:55 -0700 Subject: [PATCH 1/6] lfrfid: fix Keri reads producing a wrong credential (#4449) `rfid read` cannot read a Keri tag. It will not even read back a Keri card the Flipper itself just wrote. Every decode it produces from a genuine T5577 Keri card is wrong; validate_count usually stops those reaching the UI, but `rfid raw_analyze` has no such gate and prints the wrong value as a credential, with a plausible facility code and card number. A Keri frame is never DC balanced -- its preamble alone carries a run of 29 zeros, and on the tag measured here only 10 of 64 bits are set. On a frame that unbalanced the PSK slicer reports every high run ~135 us long and every low run ~135 us short, past the +/-127 us that the decoder's bit-period rounding tolerates, so every run of ones decodes one bit too long. Long runs of zeros survive, because the true bit period is nearer 256 us than KERI_US_PER_BIT's 255 and over a long enough run that difference offsets the skew. The same T5577 rewritten as Idteck, whose frame is near half ones, shows a skew of only +40/-42 us, which is why the other PSK protocols are unaffected. The "wrong phase synced data" correction was meant for exactly this, but it shifts the boundary the wrong way: it lengthens the high level, which here is already too long, by a further 120 us. That adds a bit to every run of ones and takes one off every short run of zeros, which keeps the frame 64 bits long -- close enough that jitter on a marginal capture lets both preamble checks pass over a mangled payload, turning a failed read into a printed credential whose value varies from read to read. Correct the skew in both directions instead. Keri has no parity and no checksum, so also require the two frames in the shift register to carry the same ID before accepting a decode. Because the mis-slicing is not identical from frame to frame -- which is why the wrong values varied between reads of one card -- frame-to-frame agreement is what turns a marginal read into a failed read rather than a credential. That needs the register to hold two whole frames, 16 bytes rather than 13; at 13 the second frame's ID would have been a three byte read past the end of the buffer. The wider buffers also align every preamble load, which at the old 13 byte stride were misaligned 32-bit reads on three buffers in four. Two unit tests cover the two changes. The first replays one frame of a captured tag and asserts 80 00 30 39; uncorrected those runs total 66 bits, the preamble never aligns, and nothing decodes. The second alternates that frame with one whose 2H/6L run pair reads as 3H/5L, carrying 0x80003839 -- each frame decodes on its own, so only frame to frame disagreement can reject the alternation, and without the ID check it reads out as 80 00 30 39. Costs 44 bytes of RAM. On a signal both versions can read, time to a validated read does not regress: first sync needs 128 bit periods rather than 104, but the decoder then settles into a clean 64 period cadence instead of alternating 120 and 8, so validate_count = 6 is satisfied after 448 bit periods rather than 506. Also drops KERI_PREAMBLE_DATA_SIZE, whose only use was the old buffer sizing, and the two commented out bit_lib_get_bit lines copied from protocol_indala26.c. The latter were not merely dead: the bit 60 test would have rejected this very tag. Co-authored-by: Claude Opus 5 --- .../tests/lfrfid/lfrfid_protocols.c | 89 ++++++++++++++ lib/lfrfid/protocols/protocol_keri.c | 115 +++++++++++------- 2 files changed, 161 insertions(+), 43 deletions(-) diff --git a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c index 032fe115e..4308eae8e 100644 --- a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c +++ b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c @@ -223,6 +223,92 @@ const int8_t fdxb_test_timings[FDXB_TEST_EMULATION_TIMINGS_COUNT] = { 16, -16, 16, -16, 16, -16, 16, -16, 16, -16, 16, -16, 16, -16, 16, -16, }; +#define KERI_TEST_DATA {0x80, 0x00, 0x30, 0x39} +#define KERI_TEST_DATA_SIZE 4 +#define KERI_TEST_RUNS_COUNT 4 +#define KERI_TEST_FRAME_COUNT 40 + +// One frame of a real RF/32 PSK1 Keri tag (internal ID 12345, T5577 blocks +// 00000004 / 000181CF) captured with `rfid raw_read psk`: the high and low +// length of each run, in microseconds. 64 bits in runs of 4H/29L, 1H/17L, 2H/6L, +// 3H/2L totalling 16373 us, so ~255.8 us per bit, with every high run ~135 us +// long and every low run ~135 us short. Uncorrected these total 66 bits, the +// preamble never aligns, and the frame does not decode at all. Fed straight to +// the decoder rather than through PulseGlue, as the PSK read path does. +static const uint16_t keri_test_timings[KERI_TEST_RUNS_COUNT][2] = { + {1158, 7288}, + {392, 4214}, + {647, 1396}, + {902, 376}, +}; + +// The same frame with the 2H/6L run pair read as 3H/5L, setting encoded bit 52 +// so it carries 0x80003839 rather than 0x80003039. Each decodes on its own, so +// alternating them is a tag whose two frames disagree. +static const uint16_t keri_test_timings_alt[KERI_TEST_RUNS_COUNT][2] = { + {1158, 7288}, + {392, 4214}, + {902, 1141}, + {902, 376}, +}; + +MU_TEST(test_lfrfid_protocol_keri_read_simple) { + ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); + mu_assert_int_eq(KERI_TEST_DATA_SIZE, protocol_dict_get_data_size(dict, LFRFIDProtocolKeri)); + mu_assert_string_eq("Keri", protocol_dict_get_name(dict, LFRFIDProtocolKeri)); + mu_assert_string_eq("Keri", protocol_dict_get_manufacturer(dict, LFRFIDProtocolKeri)); + + const uint8_t data[KERI_TEST_DATA_SIZE] = KERI_TEST_DATA; + + protocol_dict_decoders_start(dict); + + ProtocolId protocol = PROTOCOL_NO; + + for(size_t i = 0; i < KERI_TEST_RUNS_COUNT * KERI_TEST_FRAME_COUNT; i++) { + const uint16_t* run = keri_test_timings[i % KERI_TEST_RUNS_COUNT]; + + protocol = protocol_dict_decoders_feed(dict, true, run[0]); + if(protocol != PROTOCOL_NO) break; + + protocol = protocol_dict_decoders_feed(dict, false, run[1]); + if(protocol != PROTOCOL_NO) break; + } + + mu_assert_int_eq(LFRFIDProtocolKeri, protocol); + uint8_t received_data[KERI_TEST_DATA_SIZE] = {0}; + protocol_dict_get_data(dict, protocol, received_data, KERI_TEST_DATA_SIZE); + + mu_assert_mem_eq(data, received_data, KERI_TEST_DATA_SIZE); + + protocol_dict_free(dict); +} + +// Frames that disagree must not decode. Without the ID check the stream reads +// out as whichever frame lands first, a credential the tag never presented. +MU_TEST(test_lfrfid_protocol_keri_read_mismatched_frames) { + ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); + + protocol_dict_decoders_start(dict); + + ProtocolId protocol = PROTOCOL_NO; + + for(size_t i = 0; i < KERI_TEST_RUNS_COUNT * KERI_TEST_FRAME_COUNT; i++) { + const uint16_t* run = ((i / KERI_TEST_RUNS_COUNT) % 2 == 0) ? + keri_test_timings[i % KERI_TEST_RUNS_COUNT] : + keri_test_timings_alt[i % KERI_TEST_RUNS_COUNT]; + + protocol = protocol_dict_decoders_feed(dict, true, run[0]); + if(protocol != PROTOCOL_NO) break; + + protocol = protocol_dict_decoders_feed(dict, false, run[1]); + if(protocol != PROTOCOL_NO) break; + } + + mu_assert_int_eq(PROTOCOL_NO, protocol); + + protocol_dict_free(dict); +} + MU_TEST(test_lfrfid_protocol_em_read_simple) { ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax); mu_assert_int_eq(EM_TEST_DATA_SIZE, protocol_dict_get_data_size(dict, LFRFIDProtocolEM4100)); @@ -538,6 +624,9 @@ MU_TEST_SUITE(test_lfrfid_protocols_suite) { MU_RUN_TEST(test_lfrfid_protocol_inadala26_emulate_simple); + MU_RUN_TEST(test_lfrfid_protocol_keri_read_simple); + MU_RUN_TEST(test_lfrfid_protocol_keri_read_mismatched_frames); + MU_RUN_TEST(test_lfrfid_protocol_fdxb_read_simple); MU_RUN_TEST(test_lfrfid_protocol_fdxb_emulate_simple); } diff --git a/lib/lfrfid/protocols/protocol_keri.c b/lib/lfrfid/protocols/protocol_keri.c index 666369dce..b8a865722 100644 --- a/lib/lfrfid/protocols/protocol_keri.c +++ b/lib/lfrfid/protocols/protocol_keri.c @@ -3,11 +3,11 @@ #include #include "lfrfid_protocols.h" -#define KERI_PREAMBLE_BIT_SIZE (33) -#define KERI_PREAMBLE_DATA_SIZE (5) +#define KERI_PREAMBLE_BIT_SIZE (33) #define KERI_ENCODED_BIT_SIZE (64) -#define KERI_ENCODED_DATA_SIZE (((KERI_ENCODED_BIT_SIZE) / 8) + KERI_PREAMBLE_DATA_SIZE) +// Two whole frames, so that the decoder can compare their payloads +#define KERI_ENCODED_DATA_SIZE (((KERI_ENCODED_BIT_SIZE) * 2) / 8) #define KERI_ENCODED_DATA_LAST ((KERI_ENCODED_BIT_SIZE) / 8) #define KERI_DECODED_BIT_SIZE (28) @@ -16,6 +16,13 @@ #define KERI_US_PER_BIT (255) #define KERI_ENCODER_PULSES_PER_BIT (16) +// A Keri frame carries 29 zeros in its preamble, so it is never DC balanced, and +// the PSK slicer then reports one level long and the other short by ~135 us -- +// past the half bit period KERI_US_PER_BIT rounding tolerates, so every run of +// ones decodes one bit long. The correction only has to land the residual back +// inside +/-127 us, so its exact value is not critical. +#define KERI_EDGE_SKEW_US (120) + typedef struct { uint8_t data_index; uint8_t bit_clock_index; @@ -27,8 +34,10 @@ typedef struct { typedef struct { uint8_t encoded_data[KERI_ENCODED_DATA_SIZE]; uint8_t negative_encoded_data[KERI_ENCODED_DATA_SIZE]; - uint8_t corrupted_encoded_data[KERI_ENCODED_DATA_SIZE]; - uint8_t corrupted_negative_encoded_data[KERI_ENCODED_DATA_SIZE]; + uint8_t high_long_encoded_data[KERI_ENCODED_DATA_SIZE]; + uint8_t high_long_negative_encoded_data[KERI_ENCODED_DATA_SIZE]; + uint8_t high_short_encoded_data[KERI_ENCODED_DATA_SIZE]; + uint8_t high_short_negative_encoded_data[KERI_ENCODED_DATA_SIZE]; uint8_t data[KERI_DECODED_DATA_SIZE]; ProtocolKeriEncoder encoder; @@ -50,8 +59,10 @@ uint8_t* protocol_keri_get_data(ProtocolKeri* protocol) { void protocol_keri_decoder_start(ProtocolKeri* protocol) { memset(protocol->encoded_data, 0, KERI_ENCODED_DATA_SIZE); memset(protocol->negative_encoded_data, 0, KERI_ENCODED_DATA_SIZE); - memset(protocol->corrupted_encoded_data, 0, KERI_ENCODED_DATA_SIZE); - memset(protocol->corrupted_negative_encoded_data, 0, KERI_ENCODED_DATA_SIZE); + memset(protocol->high_long_encoded_data, 0, KERI_ENCODED_DATA_SIZE); + memset(protocol->high_long_negative_encoded_data, 0, KERI_ENCODED_DATA_SIZE); + memset(protocol->high_short_encoded_data, 0, KERI_ENCODED_DATA_SIZE); + memset(protocol->high_short_negative_encoded_data, 0, KERI_ENCODED_DATA_SIZE); } static bool protocol_keri_check_preamble(uint8_t* data, size_t bit_index) { @@ -63,9 +74,13 @@ static bool protocol_keri_check_preamble(uint8_t* data, size_t bit_index) { static bool protocol_keri_can_be_decoded(uint8_t* data) { if(!protocol_keri_check_preamble(data, 0)) return false; - if(!protocol_keri_check_preamble(data, 64)) return false; - ///if(bit_lib_get_bit(data, 61) != 0) return false; - //if(bit_lib_get_bit(data, 60) != 0) return false; + if(!protocol_keri_check_preamble(data, KERI_ENCODED_BIT_SIZE)) return false; + // Both frames must carry the same ID. Keri has no parity and no checksum, so + // frame to frame agreement is the only integrity check available, and a + // marginal capture is mis-sliced differently in each frame. + if(bit_lib_get_bits_32(data, 32, 32) != + bit_lib_get_bits_32(data, KERI_ENCODED_BIT_SIZE + 32, 32)) + return false; return true; } @@ -120,50 +135,64 @@ static void protocol_keri_decoder_save(uint8_t* data_to, const uint8_t* data_fro data_to[0] = (uint8_t)(id >>= 8); } +// Feed one level/duration into a pair of buffers: one that takes the level as +// the bit value, one that takes its complement. On success the decoded ID is +// written to protocol->data. +static bool protocol_keri_decoder_feed_pair( + ProtocolKeri* protocol, + bool level, + uint32_t duration, + uint8_t* positive, + uint8_t* negative) { + if(protocol_keri_decoder_feed_internal(level, duration, positive)) { + protocol_keri_decoder_save(protocol->data, positive); + return true; + } + + if(protocol_keri_decoder_feed_internal(!level, duration, negative)) { + protocol_keri_decoder_save(protocol->data, negative); + return true; + } + + return false; +} + bool protocol_keri_decoder_feed(ProtocolKeri* protocol, bool level, uint32_t duration) { - bool result = false; - if(duration > (KERI_US_PER_BIT / 2)) { - if(protocol_keri_decoder_feed_internal(level, duration, protocol->encoded_data)) { - protocol_keri_decoder_save(protocol->data, protocol->encoded_data); - result = true; - return result; - } - - if(protocol_keri_decoder_feed_internal(!level, duration, protocol->negative_encoded_data)) { - protocol_keri_decoder_save(protocol->data, protocol->negative_encoded_data); - result = true; - return result; + if(protocol_keri_decoder_feed_pair( + protocol, level, duration, protocol->encoded_data, protocol->negative_encoded_data)) { + return true; } } if(duration > (KERI_US_PER_BIT / 4)) { - // Try to decode wrong phase synced data - if(level) { - duration += 120; - } else { - if(duration > 120) { - duration -= 120; - } + // Correct the slicer's skew both ways round, the direction real tags + // show first. A duration at or below the skew is left as is rather than + // clamped to zero: either way it rounds to no bits. + const uint32_t shortened = + (duration > KERI_EDGE_SKEW_US) ? (duration - KERI_EDGE_SKEW_US) : duration; + const uint32_t lengthened = duration + KERI_EDGE_SKEW_US; + + if(protocol_keri_decoder_feed_pair( + protocol, + level, + level ? shortened : lengthened, + protocol->high_long_encoded_data, + protocol->high_long_negative_encoded_data)) { + return true; } - if(protocol_keri_decoder_feed_internal(level, duration, protocol->corrupted_encoded_data)) { - protocol_keri_decoder_save(protocol->data, protocol->corrupted_encoded_data); - - result = true; - return result; - } - - if(protocol_keri_decoder_feed_internal( - !level, duration, protocol->corrupted_negative_encoded_data)) { - protocol_keri_decoder_save(protocol->data, protocol->corrupted_negative_encoded_data); - - result = true; - return result; + if(protocol_keri_decoder_feed_pair( + protocol, + level, + level ? lengthened : shortened, + protocol->high_short_encoded_data, + protocol->high_short_negative_encoded_data)) { + return true; } } - return result; + return false; } bool protocol_keri_encoder_start(ProtocolKeri* protocol) { From b87154fa912e64dd9d7ef0a395adde4158232353 Mon Sep 17 00:00:00 2001 From: Matthew Carroll Date: Wed, 7 Oct 2026 06:00:30 -0700 Subject: [PATCH 2/6] furi_hal_rfid: report exact capture durations by letting the counter free-run (#4442) The capture channels latch edge instants in hardware, but the counter had no hardware zero as slave-mode reset is disabled, so the origin was written by software inside the ISR. That happens at edge + L, not at the edge, while CCR4 was latched AT the edge, so every reported duration came out short by one interrupt latency. Letting the counter free-run and differencing consecutive CCR4 latches removes it. CCR4 already holds the exact instant of the rising edge, so the difference is exact and carries no latency. Reported values keep their existing meaning - true is the high width, false is the rising-to-rising period - so no consumer needs changing. uint32_t arithmetic makes wraparound self-correcting. At the timer's 1 MHz the counter wraps every ~71 minutes against captures measured in hundreds of milliseconds. L is small but it is a bias, not noise. It has the same sign every cycle. A consumer that quantises each duration independently rounds it away, one that integrates durations into an absolute time axis accumulates it linearly. Measured on the LF read path: 1.46 us, spread 0.05 us, which over a 72-half-bit frame at RF/32 accumulates to ~48 us against a 64 us sampling budget. Tested the LF protocol matrix before and after on three firmware trees - OFW 24/24 protocols, Momentum 26/26, Unleashed 6 targeted rows where the decoders differ - with identical decodes throughout, each tag written and verified by a Proxmark immediately before each read. Read rate unchanged (21/40 vs 32/70, p=0.55). This is a correctness and jitter-immunity fix, not a performance fix. --- targets/f7/furi_hal/furi_hal_rfid.c | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/targets/f7/furi_hal/furi_hal_rfid.c b/targets/f7/furi_hal/furi_hal_rfid.c index 34bb3a516..56eda9899 100644 --- a/targets/f7/furi_hal/furi_hal_rfid.c +++ b/targets/f7/furi_hal/furi_hal_rfid.c @@ -238,6 +238,12 @@ static void furi_hal_rfid_tim_emulate(void) { LL_TIM_GenerateEvent_UPDATE(FURI_HAL_RFID_EMULATE_TIMER); } +// counter value latched at the last rising edge, written only by the capture ISR +static uint32_t furi_hal_rfid_capture_origin = 0; + +// Counter free-runs; consecutive captures are differenced. Zeroing it here happened one interrupt +// latency after the edge, so every reported duration read short by it. uint32_t wraparound is +// self-correcting. CC3 (falling) must precede CC4 (rising) so a high width uses its own cycle's origin. static void furi_hal_capture_dma_isr(void* context) { UNUSED(context); @@ -245,15 +251,18 @@ static void furi_hal_capture_dma_isr(void* context) { if(LL_TIM_IsActiveFlag_CC3(RFID_CAPTURE_TIM)) { LL_TIM_ClearFlag_CC3(RFID_CAPTURE_TIM); furi_hal_rfid->read_capture_callback( - true, LL_TIM_IC_GetCaptureCH3(RFID_CAPTURE_TIM), furi_hal_rfid->context); + true, + LL_TIM_IC_GetCaptureCH3(RFID_CAPTURE_TIM) - furi_hal_rfid_capture_origin, + furi_hal_rfid->context); } // Channel 4, overall level if(LL_TIM_IsActiveFlag_CC4(RFID_CAPTURE_TIM)) { LL_TIM_ClearFlag_CC4(RFID_CAPTURE_TIM); - LL_TIM_SetCounter(RFID_CAPTURE_TIM, 0); - furi_hal_rfid->read_capture_callback( - false, LL_TIM_IC_GetCaptureCH4(RFID_CAPTURE_TIM), furi_hal_rfid->context); + const uint32_t rise = LL_TIM_IC_GetCaptureCH4(RFID_CAPTURE_TIM); + const uint32_t period = rise - furi_hal_rfid_capture_origin; + furi_hal_rfid_capture_origin = rise; + furi_hal_rfid->read_capture_callback(false, period, furi_hal_rfid->context); } } @@ -303,6 +312,8 @@ void furi_hal_rfid_tim_read_capture_start(FuriHalRfidReadCaptureCallback callbac LL_TIM_CC_EnableChannel(RFID_CAPTURE_TIM, RFID_CAPTURE_IND_CH); LL_TIM_CC_EnableChannel(RFID_CAPTURE_TIM, RFID_CAPTURE_DIR_CH); LL_TIM_SetCounter(RFID_CAPTURE_TIM, 0); + // must precede EnableCounter, and hence any interrupt + furi_hal_rfid_capture_origin = 0; LL_TIM_EnableCounter(RFID_CAPTURE_TIM); furi_hal_rfid_comp_start(); From a43a5277828807a4580a92a6814259f1b2fb9745 Mon Sep 17 00:00:00 2001 From: nikachu <75615396+nikachu2012@users.noreply.github.com> Date: Wed, 7 Oct 2026 22:08:35 +0900 Subject: [PATCH 3/6] NFC FeliCa: Add Standard card emulation support (#4383) * NFC FeliCa: Support Polling, Search Service Code, Request System Code, Read / Write without Encryption * NFC FeliCa: Support Request Service * NFC FeliCa: Delete comment and Add Test * NFC FeliCa Fix: Add Request Service max nodes limit * NFC FeliCa: Add Mode and Request Response command * NFC FeliCa: Add Standard card test fixture Co-Authored-By: Claude Sonnet 4.6 * NFC FeliCa Fix: Init Key version * NFC FeliCa Fix: Correct multi-System reading and emulation on real hardware Reading: drop the invalid 0xFFFF sentinel from the Request Service key version query - it broke traversal of every System after the first on real cards. System key version is now sourced from Area 0000 instead. Emulation: track the currently selected System and use it consistently across Read/Write/Request Service/Search Service Code/Request System Code handlers instead of always assuming System 0. Derive each System's IDm from the base IDm (unchanged for System 0, index encoded in the upper nibble otherwise). Widen PMm response-time bytes so real readers don't time out waiting on the slower software emulation. Co-Authored-By: Claude Sonnet 5 * NFC FeliCa Fix: Select multi-System reads via real Polling, not IDm patching Locally rewriting the upper nibble of the base IDm to "select" a System happened to work against one physical card but times out against stricter implementations (e.g. Android HCE FeliCa emulation), since that IDm was never actually granted by the card for that System. Poll again with each System's own Code to obtain its real IDm/PMm before traversing it, matching the FeliCa spec. If Polling for a System fails, leave that System empty and continue with the rest instead of failing the whole read. Restore the card's own IDm/PMm (from the initial activation) once reading finishes. Co-Authored-By: Claude Sonnet 5 * fbt format * NFC FeliCa: bump data format version to 3 The Standard card format gained area end codes and system/area/service key versions, so bump the FeliCa data format version from 2 to 3. Loading older files keeps working through the existing fallbacks. Update the Standard test fixture to v3 and drop the leftover key lines that no loader reads. Area 0000 represents the system itself, so its key version now matches the system one, which is what the poller expects. Add felica_standard_file_test: a minimal parse-only check of the v3 fields, with no poller or listener involved. Co-Authored-By: Claude Opus 5 * NFC FeliCa: branch the loader on the data format version The Standard parser used to sniff the format instead of trusting the version it had just read: three nested sscanf fallbacks for areas, and a strstr for the system and service key versions. That left the version number decorative and turned malformed lines into silent defaults. Read the version once and branch on it, so every field has a single expected format. Version 2 files keep loading, with the fields they predate filled in with FELICA_KEY_VERSION_UNKNOWN and FELICA_AREA_END_CODE_UNKNOWN. Also fix the sscanf result checks: !sscanf(...) treats the EOF return as success, so a completely unparsable line went through unnoticed. Add a version 2 fixture and felica_standard_v2_file_test to cover the legacy branch. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Sonnet 4.6 --- .../unit_tests/nfc/Felica_Standard_v2.nfc | 28 ++ .../nfc/Felica_Standard_with_keys.nfc | 28 ++ .../debug/unit_tests/tests/nfc/nfc_test.c | 151 ++++++ lib/nfc/protocols/felica/felica.c | 86 +++- lib/nfc/protocols/felica/felica.h | 13 + lib/nfc/protocols/felica/felica_i.c | 2 + lib/nfc/protocols/felica/felica_listener.c | 452 +++++++++++++++++- lib/nfc/protocols/felica/felica_listener_i.c | 35 +- lib/nfc/protocols/felica/felica_listener_i.h | 16 + lib/nfc/protocols/felica/felica_poller.c | 105 +++- lib/nfc/protocols/felica/felica_poller_i.c | 49 ++ lib/nfc/protocols/felica/felica_poller_i.h | 16 + 12 files changed, 954 insertions(+), 27 deletions(-) create mode 100644 applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_v2.nfc create mode 100644 applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_with_keys.nfc diff --git a/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_v2.nfc b/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_v2.nfc new file mode 100644 index 000000000..9d26cf591 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_v2.nfc @@ -0,0 +1,28 @@ +Filetype: Flipper NFC device +Version: 4 +# Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare DESFire, SLIX, ST25TB +Device type: FeliCa +# UID is common for all formats +UID: 01 12 34 56 78 9A BC DE +# FeliCa specific data +Data format version: 2 +Manufacture id: 01 12 34 56 78 9A BC DE +Manufacture parameter: 0B 0B 08 20 20 20 FF FF +IC Type: FeliCa Standard RC-S9X4, Japan Transit IC + +# Felica Standard specific data +System found: 1 + + +System 00: 0003 + +Area found: 1 +Area 000: | Code 0000 | Services #000-#001 | + +Service found: 2 +Service 000: | Code 0009 | Attrib. 09 | Public | Random | Read/Write | +Service 001: | Code 000B | Attrib. 0B | Public | Random | Read Only | + +Public blocks read: 2 +Block 0000: | Service code 0009 | Block index 00 | Data: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 | +Block 0001: | Service code 000B | Block index 00 | Data: A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 | diff --git a/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_with_keys.nfc b/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_with_keys.nfc new file mode 100644 index 000000000..58b41c1f4 --- /dev/null +++ b/applications/debug/unit_tests/resources/unit_tests/nfc/Felica_Standard_with_keys.nfc @@ -0,0 +1,28 @@ +Filetype: Flipper NFC device +Version: 4 +# Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare DESFire, SLIX, ST25TB +Device type: FeliCa +# UID is common for all formats +UID: 01 12 34 56 78 9A BC DE +# FeliCa specific data +Data format version: 3 +Manufacture id: 01 12 34 56 78 9A BC DE +Manufacture parameter: 0B 0B 08 20 20 20 FF FF +IC Type: FeliCa Standard RC-S9X4, Japan Transit IC + +# Felica Standard specific data +System found: 1 + + +System 00: 0003 | Key version 0001 | + +Area found: 1 +Area 000: | Code 0000 | End FFFE | Services #000-#001 | Key version 0001 | + +Service found: 2 +Service 000: | Code 0009 | Attrib. 09 | Public | Random | Read/Write | Key version 0003 | +Service 001: | Code 000B | Attrib. 0B | Public | Random | Read Only | Key version 0004 | + +Public blocks read: 2 +Block 0000: | Service code 0009 | Block index 00 | Data: 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F 10 | +Block 0001: | Service code 000B | Block index 00 | Data: A1 A2 A3 A4 A5 A6 A7 A8 A9 AA AB AC AD AE AF B0 | diff --git a/applications/debug/unit_tests/tests/nfc/nfc_test.c b/applications/debug/unit_tests/tests/nfc/nfc_test.c index caeb3b9d7..c35898bd0 100644 --- a/applications/debug/unit_tests/tests/nfc/nfc_test.c +++ b/applications/debug/unit_tests/tests/nfc/nfc_test.c @@ -689,6 +689,154 @@ MU_TEST(felica_read) { felica_free(felica_data); } +// Minimal check of the FeliCa file format v3: no radio involved, just parsing +MU_TEST(felica_standard_file_test) { + NfcDevice* nfc_device = nfc_device_alloc(); + mu_assert( + nfc_device_load(nfc_device, EXT_PATH("unit_tests/nfc/Felica_Standard_with_keys.nfc")), + "nfc_device_load() failed"); + + const FelicaData* data = nfc_device_get_data(nfc_device, NfcProtocolFelica); + mu_assert(data->workflow_type == FelicaStandard, "workflow_type != FelicaStandard"); + mu_assert(simple_array_get_count(data->systems) == 1, "system count != 1"); + + const FelicaSystem* system = simple_array_cget(data->systems, 0); + mu_assert(system->system_code == 0x0003, "system_code != 0x0003"); + mu_assert(system->key_version == 0x0001, "system key_version != 0x0001"); + + mu_assert(simple_array_get_count(system->areas) == 1, "area count != 1"); + const FelicaArea* area = simple_array_cget(system->areas, 0); + mu_assert(area->code == 0x0000, "area code != 0x0000"); + mu_assert(area->end_code == 0xFFFE, "area end_code != 0xFFFE"); + // Area 0000 is the system itself, so its key version mirrors the system one + mu_assert(area->key_version == 0x0001, "area key_version != 0x0001"); + + mu_assert(simple_array_get_count(system->services) == 2, "service count != 2"); + const FelicaService* service0 = simple_array_cget(system->services, 0); + mu_assert(service0->code == 0x0009, "service[0] code != 0x0009"); + mu_assert(service0->key_version == 0x0003, "service[0] key_version != 0x0003"); + const FelicaService* service1 = simple_array_cget(system->services, 1); + mu_assert(service1->code == 0x000B, "service[1] code != 0x000B"); + mu_assert(service1->key_version == 0x0004, "service[1] key_version != 0x0004"); + + nfc_device_free(nfc_device); +} + +// Version 2 files carry neither area end codes nor key versions +MU_TEST(felica_standard_v2_file_test) { + NfcDevice* nfc_device = nfc_device_alloc(); + mu_assert( + nfc_device_load(nfc_device, EXT_PATH("unit_tests/nfc/Felica_Standard_v2.nfc")), + "nfc_device_load() failed"); + + const FelicaData* data = nfc_device_get_data(nfc_device, NfcProtocolFelica); + mu_assert(data->workflow_type == FelicaStandard, "workflow_type != FelicaStandard"); + mu_assert(simple_array_get_count(data->systems) == 1, "system count != 1"); + + const FelicaSystem* system = simple_array_cget(data->systems, 0); + mu_assert(system->system_code == 0x0003, "system_code != 0x0003"); + mu_assert(system->key_version == FELICA_KEY_VERSION_UNKNOWN, "system key_version is known"); + + mu_assert(simple_array_get_count(system->areas) == 1, "area count != 1"); + const FelicaArea* area = simple_array_cget(system->areas, 0); + mu_assert(area->code == 0x0000, "area code != 0x0000"); + mu_assert(area->end_code == FELICA_AREA_END_CODE_UNKNOWN, "area end_code is known"); + mu_assert(area->key_version == FELICA_KEY_VERSION_UNKNOWN, "area key_version is known"); + mu_assert(area->first_idx == 0, "area first_idx != 0"); + mu_assert(area->last_idx == 1, "area last_idx != 1"); + + mu_assert(simple_array_get_count(system->services) == 2, "service count != 2"); + const FelicaService* service0 = simple_array_cget(system->services, 0); + mu_assert(service0->code == 0x0009, "service[0] code != 0x0009"); + mu_assert(service0->key_version == FELICA_KEY_VERSION_UNKNOWN, "service[0] key_version known"); + const FelicaService* service1 = simple_array_cget(system->services, 1); + mu_assert(service1->code == 0x000B, "service[1] code != 0x000B"); + mu_assert(service1->key_version == FELICA_KEY_VERSION_UNKNOWN, "service[1] key_version known"); + + mu_assert(simple_array_get_count(system->public_blocks) == 2, "public block count != 2"); + + nfc_device_free(nfc_device); +} + +MU_TEST(felica_standard_read) { + NfcDeviceData* nfc_device = nfc_device_alloc(); + mu_assert( + nfc_device_load(nfc_device, EXT_PATH("unit_tests/nfc/Felica_Standard_with_keys.nfc")), + "nfc_device_load() failed"); + + Nfc* poller = nfc_alloc(); + Nfc* listener = nfc_alloc(); + NfcListener* felica_listener = nfc_listener_alloc( + listener, NfcProtocolFelica, nfc_device_get_data(nfc_device, NfcProtocolFelica)); + nfc_listener_start(felica_listener, NULL, NULL); + + FelicaData* felica_data = felica_alloc(); + FelicaError error = felica_poller_sync_read(poller, felica_data, NULL); + + nfc_listener_stop(felica_listener); + nfc_listener_free(felica_listener); + nfc_free(listener); + nfc_free(poller); + nfc_device_free(nfc_device); + + mu_assert(error == FelicaErrorNone, "felica_poller_sync_read() failed"); + mu_assert(simple_array_get_count(felica_data->systems) == 1, "system count != 1"); + + const FelicaSystem* system = simple_array_cget(felica_data->systems, 0); + mu_assert(system->key_version == 0x0001, "system key_version != 0x0001"); + mu_assert(simple_array_get_count(system->public_blocks) == 2, "public block count != 2"); + + const FelicaPublicBlock* pb0 = simple_array_cget(system->public_blocks, 0); + mu_assert(pb0->service_code == 0x0009, "block[0] service_code != 0x0009"); + mu_assert(pb0->block_idx == 0, "block[0] block_idx != 0"); + static const uint8_t expected_block0[FELICA_DATA_BLOCK_SIZE] = { + 0x01, + 0x02, + 0x03, + 0x04, + 0x05, + 0x06, + 0x07, + 0x08, + 0x09, + 0x0A, + 0x0B, + 0x0C, + 0x0D, + 0x0E, + 0x0F, + 0x10}; + mu_assert( + memcmp(pb0->block.data, expected_block0, FELICA_DATA_BLOCK_SIZE) == 0, + "block[0] data mismatch"); + + const FelicaPublicBlock* pb1 = simple_array_cget(system->public_blocks, 1); + mu_assert(pb1->service_code == 0x000B, "block[1] service_code != 0x000B"); + mu_assert(pb1->block_idx == 0, "block[1] block_idx != 0"); + static const uint8_t expected_block1[FELICA_DATA_BLOCK_SIZE] = { + 0xA1, + 0xA2, + 0xA3, + 0xA4, + 0xA5, + 0xA6, + 0xA7, + 0xA8, + 0xA9, + 0xAA, + 0xAB, + 0xAC, + 0xAD, + 0xAE, + 0xAF, + 0xB0}; + mu_assert( + memcmp(pb1->block.data, expected_block1, FELICA_DATA_BLOCK_SIZE) == 0, + "block[1] data mismatch"); + + felica_free(felica_data); +} + MU_TEST(felica_read_auth) { FelicaData* felica_data = felica_alloc(); FelicaCardKey card_key; @@ -866,6 +1014,9 @@ MU_TEST_SUITE(nfc) { MU_RUN_TEST(mf_classic_dict_test); MU_RUN_TEST(felica_read); MU_RUN_TEST(felica_read_auth); + MU_RUN_TEST(felica_standard_file_test); + MU_RUN_TEST(felica_standard_v2_file_test); + MU_RUN_TEST(felica_standard_read); MU_RUN_TEST(slix_file_with_capabilities_test); MU_RUN_TEST(slix_set_password_default_cap_correct_pass); diff --git a/lib/nfc/protocols/felica/felica.c b/lib/nfc/protocols/felica/felica.c index 47292c850..b7c0981c6 100644 --- a/lib/nfc/protocols/felica/felica.c +++ b/lib/nfc/protocols/felica/felica.c @@ -12,7 +12,11 @@ #define FELICA_MANUFACTURE_ID "Manufacture id" #define FELICA_MANUFACTURE_PARAMETER "Manufacture parameter" -static const uint32_t felica_data_format_version = 2; +// Version history: +// 1 - everything was saved as FeliCa Lite +// 2 - FeliCa Standard systems/areas/services/public blocks +// 3 - area end codes and system/area/service key versions +static const uint32_t felica_data_format_version = 3; /** @brief This is used in felica_prepare_first_block to define which * type of block needs to be prepared. @@ -98,6 +102,7 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { furi_check(data); bool parsed = false; + uint32_t data_format_version = 0; FuriString* str_key_buffer = furi_string_alloc(); FuriString* str_data_buffer = furi_string_alloc(); @@ -105,7 +110,6 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { do { if(version < NFC_UNIFIED_FORMAT_VERSION) break; - uint32_t data_format_version = 0; if(!flipper_format_read_uint32(ff, FELICA_DATA_FORMAT_VERSION, &data_format_version, 1)) break; @@ -159,6 +163,9 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { } while(false); break; case FelicaStandard: + // Key versions and area end codes appeared in version 3 + const bool has_key_versions = data_format_version >= 3; + uint32_t systems_total = 0; if(!flipper_format_read_uint32(ff, "System found", &systems_total, 1)) break; if(systems_total == 0) break; @@ -176,8 +183,19 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { ff, furi_string_get_cstr(str_key_buffer), str_data_buffer)) break; - if(!sscanf(furi_string_get_cstr(str_data_buffer), "%04hX", &system_code)) { - break; + system->key_version = FELICA_KEY_VERSION_UNKNOWN; + if(has_key_versions) { + if(sscanf( + furi_string_get_cstr(str_data_buffer), + "%04hX | Key version %04hX |", + &system_code, + &system->key_version) != 2) { + break; + } + } else { + if(sscanf(furi_string_get_cstr(str_data_buffer), "%04hX", &system_code) != 1) { + break; + } } system->system_code = system_code; @@ -200,13 +218,29 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { break; } FelicaArea* area = simple_array_get(system->areas, i); - if(sscanf( - furi_string_get_cstr(str_data_buffer), - "| Code %04hX | Services #%03hX-#%03hX |", - &area->code, - &area->first_idx, - &area->last_idx) != 3) { - break; + if(has_key_versions) { + if(sscanf( + furi_string_get_cstr(str_data_buffer), + "| Code %04hX | End %04hX | Services #%03hX-#%03hX | Key version %04hX |", + &area->code, + &area->end_code, + &area->first_idx, + &area->last_idx, + &area->key_version) != 5) { + break; + } + } else { + // Version 2 knew neither the area end code nor its key version + area->end_code = FELICA_AREA_END_CODE_UNKNOWN; + area->key_version = FELICA_KEY_VERSION_UNKNOWN; + if(sscanf( + furi_string_get_cstr(str_data_buffer), + "| Code %04hX | Services #%03hX-#%03hX |", + &area->code, + &area->first_idx, + &area->last_idx) != 3) { + break; + } } } } while(false); @@ -230,13 +264,25 @@ bool felica_load(FelicaData* data, FlipperFormat* ff, uint32_t version) { FelicaService* service = simple_array_get(system->services, i); // all unread in the beginning. reserved for future block load - if(!sscanf( + if(sscanf( furi_string_get_cstr(str_data_buffer), "| Code %04hX |", - &service->code)) { + &service->code) != 1) { break; } service->attr = service->code & 0x3F; + + // The key version trails the human readable attribute text, + // so it can not be picked up by the sscanf above + service->key_version = FELICA_KEY_VERSION_UNKNOWN; + if(has_key_versions) { + const char* svc_kv_ptr = + strstr(furi_string_get_cstr(str_data_buffer), "Key version "); + if(!svc_kv_ptr || + sscanf(svc_kv_ptr, "Key version %04hX", &service->key_version) != 1) { + break; + } + } } } while(false); @@ -360,7 +406,11 @@ bool felica_save(const FelicaData* data, FlipperFormat* ff) { furi_string_reset(str_data_buffer); furi_string_reset(str_key_buffer); furi_string_printf(str_key_buffer, "\n\nSystem %02X", (uint8_t)sys_idx); - furi_string_printf(str_data_buffer, "%04X", system->system_code); + furi_string_printf( + str_data_buffer, + "%04X | Key version %04X |", + system->system_code, + system->key_version); if(!flipper_format_write_string( ff, furi_string_get_cstr(str_key_buffer), str_data_buffer)) break; @@ -384,10 +434,12 @@ bool felica_save(const FelicaData* data, FlipperFormat* ff) { furi_string_printf(str_key_buffer, "Area %03X", i); furi_string_printf( str_data_buffer, - "| Code %04X | Services #%03X-#%03X |", + "| Code %04X | End %04X | Services #%03X-#%03X | Key version %04X |", area->code, + area->end_code, area->first_idx, - area->last_idx); + area->last_idx, + area->key_version); if(!flipper_format_write_string( ff, furi_string_get_cstr(str_key_buffer), str_data_buffer)) break; @@ -409,6 +461,8 @@ bool felica_save(const FelicaData* data, FlipperFormat* ff) { service->code, service->attr); felica_service_get_attribute_string(service, str_data_buffer); + furi_string_cat_printf( + str_data_buffer, " Key version %04X |", service->key_version); if(!flipper_format_write_string( ff, furi_string_get_cstr(str_key_buffer), str_data_buffer)) break; diff --git a/lib/nfc/protocols/felica/felica.h b/lib/nfc/protocols/felica/felica.h index bc83ff846..cdb51b0c2 100644 --- a/lib/nfc/protocols/felica/felica.h +++ b/lib/nfc/protocols/felica/felica.h @@ -19,6 +19,11 @@ extern "C" { #define FELICA_SERVICE_RW_ACCESS (0x0009U) #define FELICA_SERVICE_RO_ACCESS (0x000BU) +/** @brief Placeholders for data that files older than format version 3 do not carry. + * Both match what a card returns for a node it does not know about. */ +#define FELICA_KEY_VERSION_UNKNOWN (0xFFFFU) +#define FELICA_AREA_END_CODE_UNKNOWN (0xFFFFU) + #define FELICA_BLOCKS_TOTAL_COUNT (28U) #define FELICA_BLOCK_INDEX_REG (0x0EU) #define FELICA_BLOCK_INDEX_RC (0x80U) @@ -50,6 +55,10 @@ extern "C" { #define FELICA_TIME_SLOT_8 (0x07U) #define FELICA_TIME_SLOT_16 (0x0FU) +#define FELICA_CMD_REQUEST_SERVICE 0x02 +#define FELICA_CMD_REQUEST_SERVICE_RESP 0x03 +#define FELICA_CMD_REQUEST_RESPONSE 0x04 +#define FELICA_CMD_REQUEST_RESPONSE_RESP 0x05 #define FELICA_CMD_LIST_SERVICE_CODE 0x0A #define FELICA_CMD_LIST_SERVICE_CODE_RESP 0x0B #define FELICA_CMD_REQUEST_SYSTEM_CODE 0x0C @@ -170,12 +179,15 @@ typedef union { typedef struct { uint16_t code; uint8_t attr; + uint16_t key_version; } FelicaService; typedef struct { uint16_t code; + uint16_t end_code; uint16_t first_idx; uint16_t last_idx; + uint16_t key_version; } FelicaArea; typedef struct { @@ -187,6 +199,7 @@ typedef struct { typedef struct { uint8_t system_code_idx; uint16_t system_code; + uint16_t key_version; SimpleArray* services; SimpleArray* areas; SimpleArray* public_blocks; diff --git a/lib/nfc/protocols/felica/felica_i.c b/lib/nfc/protocols/felica/felica_i.c index c5ecdf121..22197e23d 100644 --- a/lib/nfc/protocols/felica/felica_i.c +++ b/lib/nfc/protocols/felica/felica_i.c @@ -3,6 +3,7 @@ void felica_system_init(FelicaSystem* system) { system->system_code = 0; system->system_code_idx = 0; + system->key_version = 0; system->services = simple_array_alloc(&felica_service_array_cfg); system->areas = simple_array_alloc(&felica_area_array_cfg); system->public_blocks = simple_array_alloc(&felica_public_block_array_cfg); @@ -26,6 +27,7 @@ void felica_system_copy(FelicaSystem* system, const FelicaSystem* other) { furi_check(other); system->system_code = other->system_code; system->system_code_idx = other->system_code_idx; + system->key_version = other->key_version; simple_array_copy(system->services, other->services); simple_array_copy(system->areas, other->areas); simple_array_copy(system->public_blocks, other->public_blocks); diff --git a/lib/nfc/protocols/felica/felica_listener.c b/lib/nfc/protocols/felica/felica_listener.c index 3bf109366..f0a7e839f 100644 --- a/lib/nfc/protocols/felica/felica_listener.c +++ b/lib/nfc/protocols/felica/felica_listener.c @@ -36,7 +36,17 @@ FelicaListener* felica_listener_alloc(Nfc* nfc, FelicaData* data) { memcpy(instance->mc_shadow.data, instance->data->data.fs.mc.data, FELICA_DATA_BLOCK_SIZE); instance->data->data.fs.state.data[0] = 0; + instance->mode = 0; + instance->current_system_idx = 0; nfc_config(instance->nfc, NfcModeListener, NfcTechFelica); + + // PMm bytes 2-6 encode the max response time a reader should allow per command + // (Request Service / Request Response / Read / Write / Auth). Cards saved from a + // real (hardware-fast) FeliCa IC carry short values here; our software emulation + // is slower to respond, so real readers following those short timeouts abandon + // the session mid-transaction. Widen them to the max so readers wait long enough. + memset(data->pmm.data + 2, 0xFF, FELICA_PMM_SIZE - 2); + const uint16_t system_code = *(uint16_t*)data->data.fs.sys_c.data; nfc_felica_listener_set_sensf_res_data( nfc, data->idm.data, sizeof(data->idm), data->pmm.data, sizeof(data->pmm), system_code); @@ -147,15 +157,429 @@ static FelicaError felica_listener_command_handler_write( return felica_listener_frame_exchange(instance, instance->tx_buffer); } +// Returns the currently selected System (instance->current_system_idx), or NULL if +// there is none - callers must not assume System 0 is the active one, since Polling +// with a specific System Code can switch this mid-session. +static const FelicaSystem* felica_listener_get_current_system(const FelicaListener* instance) { + uint32_t system_count = simple_array_get_count(instance->data->systems); + if(instance->current_system_idx >= system_count) return NULL; + return simple_array_cget(instance->data->systems, instance->current_system_idx); +} + +static FelicaSystem* felica_listener_get_current_system_mut(FelicaListener* instance) { + uint32_t system_count = simple_array_get_count(instance->data->systems); + if(instance->current_system_idx >= system_count) return NULL; + return simple_array_get(instance->data->systems, instance->current_system_idx); +} + +// Max blocks for Standard read that fit within the 128-byte tx buffer (with CRC) +#define FELICA_STANDARD_READ_BLOCK_MAX (7U) + +static FelicaError felica_listener_command_handler_standard_read( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + const uint8_t* raw = (const uint8_t*)generic_request; + uint8_t service_num = raw[10]; + if(service_num == 0 || service_num > 16) { + return FelicaErrorProtocol; + } + + uint16_t service_codes[16]; + for(uint8_t i = 0; i < service_num; i++) { + service_codes[i] = (uint16_t)(raw[11 + i * 2] | ((uint16_t)raw[12 + i * 2] << 8)); + } + + uint8_t block_count = raw[11 + service_num * 2]; + if(block_count > FELICA_STANDARD_READ_BLOCK_MAX) { + block_count = FELICA_STANDARD_READ_BLOCK_MAX; + } + const uint8_t* bptr = raw + 12 + service_num * 2; + + const FelicaSystem* system = felica_listener_get_current_system(instance); + + uint8_t sf1 = 0x00, sf2 = 0x00; + uint8_t block_data[FELICA_STANDARD_READ_BLOCK_MAX][FELICA_DATA_BLOCK_SIZE]; + uint8_t actual_block_count = 0; + + for(uint8_t i = 0; i < block_count; i++) { + uint8_t svc_idx = bptr[0] & 0x0F; + bool is_2byte = (bptr[0] >> 7) != 0; + uint8_t blk_num; + + if(is_2byte) { + blk_num = bptr[1]; + bptr += 2; + } else { + blk_num = bptr[1]; // lower byte of LE block number; upper byte (bptr[2]) must be 0 + bptr += 3; + } + + if(svc_idx >= service_num || !system) { + sf1 = 0xFF; + sf2 = 0xA8; + break; + } + + uint16_t svc_code = service_codes[svc_idx]; + bool found = false; + uint32_t pb_count = simple_array_get_count(system->public_blocks); + for(uint32_t j = 0; j < pb_count; j++) { + const FelicaPublicBlock* pb = simple_array_cget(system->public_blocks, j); + if(pb->service_code == svc_code && pb->block_idx == blk_num) { + memcpy(block_data[actual_block_count], pb->block.data, FELICA_DATA_BLOCK_SIZE); + found = true; + break; + } + } + + if(!found) { + sf1 = 0xFF; + sf2 = 0xA8; + break; + } + actual_block_count++; + } + + size_t resp_size = + (sf1 == 0) ? (size_t)(12 + 1 + actual_block_count * FELICA_DATA_BLOCK_SIZE) : 12; + uint8_t* resp_buf = malloc(resp_size); + resp_buf[0] = (uint8_t)resp_size; + resp_buf[1] = FELICA_LISTENER_RESPONSE_CODE_READ; + const FelicaIDm current_idm = felica_listener_get_current_idm(instance); + memcpy(resp_buf + 2, current_idm.data, 8); + resp_buf[10] = sf1; + resp_buf[11] = sf2; + if(sf1 == 0) { + resp_buf[12] = actual_block_count; + for(uint8_t i = 0; i < actual_block_count; i++) { + memcpy( + resp_buf + 13 + i * FELICA_DATA_BLOCK_SIZE, block_data[i], FELICA_DATA_BLOCK_SIZE); + } + } + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, resp_size); + free(resp_buf); + + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + +static FelicaError felica_listener_command_handler_standard_write( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + const uint8_t* raw = (const uint8_t*)generic_request; + uint8_t service_num = raw[10]; + if(service_num == 0 || service_num > 16) { + return FelicaErrorProtocol; + } + + uint16_t service_codes[16]; + for(uint8_t i = 0; i < service_num; i++) { + service_codes[i] = (uint16_t)(raw[11 + i * 2] | ((uint16_t)raw[12 + i * 2] << 8)); + } + + uint8_t block_count = raw[11 + service_num * 2]; + const uint8_t* bptr = raw + 12 + service_num * 2; + + FelicaSystem* system = felica_listener_get_current_system_mut(instance); + + uint8_t sf1 = 0x00, sf2 = 0x00; + + struct { + uint16_t svc_code; + uint8_t blk_num; + } targets[16]; + uint8_t valid_count = 0; + + for(uint8_t i = 0; i < block_count && i < 16; i++) { + uint8_t svc_idx = bptr[0] & 0x0F; + bool is_2byte = (bptr[0] >> 7) != 0; + uint8_t blk_num; + + if(is_2byte) { + blk_num = bptr[1]; + bptr += 2; + } else { + blk_num = bptr[1]; + bptr += 3; + } + + if(svc_idx >= service_num || !system) { + sf1 = 0xFF; + sf2 = 0xA8; + break; + } + + uint16_t svc_code = service_codes[svc_idx]; + + uint32_t svc_count = simple_array_get_count(system->services); + bool svc_found = false; + for(uint32_t k = 0; k < svc_count; k++) { + const FelicaService* svc = simple_array_cget(system->services, k); + if(svc->code == svc_code) { + svc_found = true; + if(svc->attr & FELICA_SERVICE_ATTRIBUTE_READ_ONLY) { + sf1 = 0xFF; + sf2 = 0xA6; + } + break; + } + } + if(!svc_found || sf1 != 0) break; + + targets[i].svc_code = svc_code; + targets[i].blk_num = blk_num; + valid_count++; + } + + if(sf1 == 0 && system) { + for(uint8_t i = 0; i < valid_count; i++) { + bool found = false; + uint32_t pb_count = simple_array_get_count(system->public_blocks); + for(uint32_t j = 0; j < pb_count; j++) { + FelicaPublicBlock* pb = simple_array_get(system->public_blocks, j); + if(pb->service_code == targets[i].svc_code && + pb->block_idx == targets[i].blk_num) { + memcpy( + pb->block.data, bptr + i * FELICA_DATA_BLOCK_SIZE, FELICA_DATA_BLOCK_SIZE); + found = true; + break; + } + } + if(!found) { + sf1 = 0xFF; + sf2 = 0xA8; + break; + } + } + } + + uint8_t resp_buf[12]; + resp_buf[0] = 12; + resp_buf[1] = FELICA_LISTENER_RESPONSE_CODE_WRITE; + const FelicaIDm current_idm = felica_listener_get_current_idm(instance); + memcpy(resp_buf + 2, current_idm.data, 8); + resp_buf[10] = sf1; + resp_buf[11] = sf2; + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, 12); + + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + +static FelicaError felica_listener_command_handler_request_response( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + UNUSED(generic_request); + + // Response: length(1) + RC(1) + IDm(8) + mode(1) = 11 bytes + const size_t resp_size = 11; + uint8_t resp_buf[resp_size]; + resp_buf[0] = (uint8_t)resp_size; + resp_buf[1] = FELICA_CMD_REQUEST_RESPONSE_RESP; + const FelicaIDm current_idm = felica_listener_get_current_idm(instance); + memcpy(resp_buf + 2, current_idm.data, 8); + resp_buf[10] = instance->mode; + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, resp_size); + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + +static FelicaError felica_listener_command_handler_request_service( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + const uint8_t* raw = (const uint8_t*)generic_request; + uint8_t n = raw[10]; + FURI_LOG_D(TAG, "Request Service cmd, n=%u", n); + + // Clamp: response is 11 + 2n bytes; tx buffer is FELICA_LISTENER_MAX_BUFFER_SIZE bytes (incl. 2-byte CRC) + const uint8_t n_max = (FELICA_LISTENER_MAX_BUFFER_SIZE - 2 - 11) / 2; + if(n > n_max) n = n_max; + + const FelicaSystem* system = felica_listener_get_current_system(instance); + + size_t resp_size = 11 + (size_t)n * 2; + uint8_t* resp_buf = malloc(resp_size); + resp_buf[0] = (uint8_t)resp_size; + resp_buf[1] = FELICA_CMD_REQUEST_SERVICE_RESP; + const FelicaIDm current_idm = felica_listener_get_current_idm(instance); + memcpy(resp_buf + 2, current_idm.data, 8); + resp_buf[10] = n; + + for(uint8_t i = 0; i < n; i++) { + uint16_t req_code = (uint16_t)(raw[11 + i * 2] | ((uint16_t)raw[12 + i * 2] << 8)); + uint16_t kv = 0xFFFF; + + if(req_code == 0xFFFF) { + kv = system ? system->key_version : 0xFFFF; + } else if(system) { + uint32_t area_count = simple_array_get_count(system->areas); + for(uint32_t j = 0; j < area_count; j++) { + const FelicaArea* area = simple_array_cget(system->areas, j); + if(area->code == req_code) { + kv = area->key_version; + break; + } + } + if(kv == 0xFFFF) { + uint32_t svc_count = simple_array_get_count(system->services); + for(uint32_t j = 0; j < svc_count; j++) { + const FelicaService* svc = simple_array_cget(system->services, j); + if(svc->code == req_code) { + kv = svc->key_version; + break; + } + } + } + } + + resp_buf[11 + i * 2] = (uint8_t)(kv & 0xFF); + resp_buf[12 + i * 2] = (uint8_t)(kv >> 8); + } + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, resp_size); + free(resp_buf); + + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + +static FelicaError felica_listener_command_handler_search_service_code( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + const uint8_t* raw_req = (const uint8_t*)generic_request; + uint16_t counter = (uint16_t)(raw_req[10] | ((uint16_t)raw_req[11] << 8)); + FURI_LOG_D(TAG, "Search Service Code cmd, counter=%04X", counter); + + const FelicaSystem* system = felica_listener_get_current_system(instance); + + uint32_t area_count = system ? simple_array_get_count(system->areas) : 0; + uint32_t service_count = system ? simple_array_get_count(system->services) : 0; + + uint32_t pos = 0; + uint32_t ai = 0; + uint32_t si = 0; + bool found = false; + bool is_area = false; + uint16_t code_lo = 0xFFFF; + uint16_t code_hi = 0; + + while(true) { + bool pick_area = false; + if(ai < area_count) { + const FelicaArea* a = simple_array_cget(system->areas, ai); + pick_area = (si >= a->first_idx); + } + + if(pick_area) { + if(pos == counter) { + const FelicaArea* a = simple_array_cget(system->areas, ai); + is_area = true; + code_lo = a->code; + code_hi = a->end_code; + found = true; + break; + } + ai++; + pos++; + } else if(si < service_count) { + if(pos == counter) { + const FelicaService* svc = simple_array_cget(system->services, si); + is_area = false; + code_lo = svc->code; + found = true; + break; + } + si++; + pos++; + } else { + break; + } + } + + uint8_t resp_data_size = (found && is_area) ? 4 : 2; + size_t resp_size = sizeof(FelicaCommandHeaderRaw) + resp_data_size; + + uint8_t* resp_buf = malloc(resp_size); + FelicaListServiceCommandResponse* resp = (FelicaListServiceCommandResponse*)resp_buf; + + resp->header.length = (uint8_t)resp_size; + resp->header.command = FELICA_CMD_LIST_SERVICE_CODE_RESP; + resp->header.idm = felica_listener_get_current_idm(instance); + + if(found && is_area) { + resp->data[0] = (uint8_t)(code_lo & 0xFF); + resp->data[1] = (uint8_t)(code_lo >> 8); + resp->data[2] = (uint8_t)(code_hi & 0xFF); + resp->data[3] = (uint8_t)(code_hi >> 8); + } else { + // Service code or end-of-list (0xFFFF) + resp->data[0] = (uint8_t)(code_lo & 0xFF); + resp->data[1] = (uint8_t)(code_lo >> 8); + } + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, resp_size); + free(resp_buf); + + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + +static FelicaError felica_listener_command_handler_request_system_code( + FelicaListener* instance, + const FelicaListenerGenericRequest* const generic_request) { + UNUSED(generic_request); + FURI_LOG_D(TAG, "Request System Code cmd"); + + uint32_t system_count = simple_array_get_count(instance->data->systems); + size_t resp_size = sizeof(FelicaCommandHeaderRaw) + 1 + system_count * 2; + + uint8_t* resp_buf = malloc(resp_size); + FelicaListSystemCodeCommandResponse* resp = (FelicaListSystemCodeCommandResponse*)resp_buf; + + resp->header.length = (uint8_t)resp_size; + resp->header.command = FELICA_CMD_REQUEST_SYSTEM_CODE_RESP; + resp->header.idm = felica_listener_get_current_idm(instance); + resp->system_count = (uint8_t)system_count; + + for(uint32_t i = 0; i < system_count; i++) { + const FelicaSystem* system = simple_array_cget(instance->data->systems, i); + resp->system_code[i * 2] = (system->system_code >> 8) & 0xFF; + resp->system_code[i * 2 + 1] = system->system_code & 0xFF; + } + + bit_buffer_reset(instance->tx_buffer); + bit_buffer_append_bytes(instance->tx_buffer, resp_buf, resp_size); + free(resp_buf); + + return felica_listener_frame_exchange(instance, instance->tx_buffer); +} + static FelicaError felica_listener_process_request( FelicaListener* instance, const FelicaListenerGenericRequest* generic_request) { const uint8_t cmd_code = generic_request->header.code; switch(cmd_code) { + case FELICA_CMD_REQUEST_RESPONSE: + return felica_listener_command_handler_request_response(instance, generic_request); + case FELICA_CMD_REQUEST_SERVICE: + return felica_listener_command_handler_request_service(instance, generic_request); case FELICA_CMD_READ_WITHOUT_ENCRYPTION: + if(instance->data->workflow_type == FelicaStandard) { + return felica_listener_command_handler_standard_read(instance, generic_request); + } return felica_listener_command_handler_read(instance, generic_request); case FELICA_CMD_WRITE_WITHOUT_ENCRYPTION: + if(instance->data->workflow_type == FelicaStandard) { + return felica_listener_command_handler_standard_write(instance, generic_request); + } return felica_listener_command_handler_write(instance, generic_request); + case FELICA_CMD_LIST_SERVICE_CODE: + return felica_listener_command_handler_search_service_code(instance, generic_request); + case FELICA_CMD_REQUEST_SYSTEM_CODE: + return felica_listener_command_handler_request_system_code(instance, generic_request); default: FURI_LOG_E(TAG, "FeliCa incorrect command"); return FelicaErrorNotPresent; @@ -165,7 +589,7 @@ static FelicaError felica_listener_process_request( static void felica_listener_populate_polling_response_header( FelicaListener* instance, FelicaListenerPollingResponseHeader* resp) { - resp->idm = instance->data->idm; + resp->idm = felica_listener_get_current_idm(instance); resp->pmm = instance->data->pmm; resp->response_code = FELICA_LISTENER_RESPONSE_POLLING; } @@ -179,10 +603,16 @@ static bool felica_listener_check_system_code( generic_request->polling.system_code == (code | 0xFF00U)); } +// Returns the matched System's wire-format code, or FELICA_SYSTEM_CODE_CODE if none +// matched. When the match is a real entry in instance->data->systems (as opposed to +// the virtual NDEF/Lite-S codes), *matched_idx is set to its index so the caller can +// derive that System's IDm; otherwise *matched_idx is left at 0. static uint16_t felica_listener_get_response_system_code( FelicaListener* instance, - const FelicaListenerGenericRequest* const generic_request) { + const FelicaListenerGenericRequest* const generic_request, + uint8_t* matched_idx) { uint16_t resp_system_code = FELICA_SYSTEM_CODE_CODE; + *matched_idx = 0; if(felica_listener_check_system_code(generic_request, FELICA_LISTENER_SYSTEM_CODE_NDEF) && instance->data->data.fs.mc.data[FELICA_MC_SYS_OP] == 1) { // NDEF @@ -191,6 +621,17 @@ static uint16_t felica_listener_get_response_system_code( generic_request, FELICA_LISTENER_SYSTEM_CODE_LITES)) { // Lite-S resp_system_code = FELICA_LISTENER_SYSTEM_CODE_LITES; + } else { + uint32_t system_count = simple_array_get_count(instance->data->systems); + for(uint32_t i = 0; i < system_count; i++) { + const FelicaSystem* system = simple_array_cget(instance->data->systems, i); + uint16_t wire_code = __builtin_bswap16(system->system_code); + if(felica_listener_check_system_code(generic_request, wire_code)) { + resp_system_code = wire_code; + *matched_idx = (uint8_t)i; + break; + } + } } return resp_system_code; } @@ -200,10 +641,15 @@ static FelicaError felica_listener_process_system_code( const FelicaListenerGenericRequest* const generic_request) { FelicaError result = FelicaErrorFeatureUnsupported; do { + uint8_t matched_idx = 0; uint16_t resp_system_code = - felica_listener_get_response_system_code(instance, generic_request); + felica_listener_get_response_system_code(instance, generic_request, &matched_idx); if(resp_system_code == FELICA_SYSTEM_CODE_CODE) break; + // Switch context to the newly selected System before building the response, + // since the response IDm (and all subsequent commands) must reflect it. + instance->current_system_idx = matched_idx; + FelicaListenerPollingResponse* resp = malloc(sizeof(FelicaListenerPollingResponse)); felica_listener_populate_polling_response_header(instance, &resp->header); diff --git a/lib/nfc/protocols/felica/felica_listener_i.c b/lib/nfc/protocols/felica/felica_listener_i.c index d69c8c66a..1329ea916 100644 --- a/lib/nfc/protocols/felica/felica_listener_i.c +++ b/lib/nfc/protocols/felica/felica_listener_i.c @@ -180,6 +180,20 @@ bool felica_listener_check_block_list_size( furi_assert(instance); furi_assert(req); + if(req->header.code == FELICA_CMD_REQUEST_SYSTEM_CODE || + req->header.code == FELICA_CMD_LIST_SERVICE_CODE || + req->header.code == FELICA_CMD_REQUEST_SERVICE || + req->header.code == FELICA_CMD_REQUEST_RESPONSE) { + return true; + } + + // Standard mode uses multi-service packet format; parsing done in command handlers + if(instance->data->workflow_type == FelicaStandard && + (req->header.code == FELICA_CMD_READ_WITHOUT_ENCRYPTION || + req->header.code == FELICA_CMD_WRITE_WITHOUT_ENCRYPTION)) { + return true; + } + FelicaListenerRequest* request = (FelicaListenerRequest*)req; bool valid = true; @@ -198,12 +212,27 @@ bool felica_listener_check_block_list_size( return valid; } +FelicaIDm felica_listener_get_current_idm(const FelicaListener* instance) { + furi_assert(instance); + + FelicaIDm idm = instance->data->idm; + // System 0 uses the card's base IDm untouched - only non-zero System indices get + // their index encoded into the upper nibble. Unconditionally masking byte 0 here + // would corrupt the base IDm's real upper nibble for cards where it isn't already + // zero, making the emulated card report an IDm inconsistent with what the reader + // saw during the initial (hardware-handled) wildcard Polling. + if(instance->current_system_idx != 0) { + idm.data[0] = (uint8_t)((idm.data[0] & 0x0F) | (instance->current_system_idx << 4)); + } + return idm; +} + bool felica_listener_check_idm(const FelicaListener* instance, const FelicaIDm* request_idm) { furi_assert(instance); furi_assert(request_idm); - const FelicaIDm* idm = &instance->data->idm; - return memcmp(idm->data, request_idm->data, 8) == 0; + const FelicaIDm idm = felica_listener_get_current_idm(instance); + return memcmp(idm.data, request_idm->data, 8) == 0; } void felica_listener_reset(FelicaListener* instance) { @@ -213,6 +242,8 @@ void felica_listener_reset(FelicaListener* instance) { instance->auth.context.auth_status.external = false; instance->data->data.fs.state.data[0] = 0; instance->rc_written = false; + instance->mode = 0; + instance->current_system_idx = 0; memset(instance->auth.session_key.data, 0, FELICA_DATA_BLOCK_SIZE); memcpy(instance->data->data.fs.mc.data, instance->mc_shadow.data, FELICA_DATA_BLOCK_SIZE); diff --git a/lib/nfc/protocols/felica/felica_listener_i.h b/lib/nfc/protocols/felica/felica_listener_i.h index f64a598cc..abc473882 100644 --- a/lib/nfc/protocols/felica/felica_listener_i.h +++ b/lib/nfc/protocols/felica/felica_listener_i.h @@ -103,6 +103,12 @@ struct FelicaListener { uint8_t requested_blocks[FELICA_LISTENER_READ_BLOCK_COUNT_MAX]; uint8_t mac_calc_start; bool rc_written; + uint8_t mode; + // Index of the currently selected System (via Polling with a specific System + // Code). Real multi-system cards derive a distinct IDm per System by encoding + // this index into the upper nibble of the base IDm's first byte - mirrors the + // convention used on the poller side when switching Systems. + uint8_t current_system_idx; BitBuffer* tx_buffer; BitBuffer* rx_buffer; @@ -134,6 +140,16 @@ void felica_wcnt_increment(FelicaData* data); */ bool felica_listener_check_idm(const FelicaListener* instance, const FelicaIDm* request_idm); +/** Computes the IDm for the currently selected System (instance->current_system_idx), + * derived from the card's base IDm by encoding the System index into the upper + * nibble of the first byte. + * + * @param instance pointer to the listener instance to be used. + * + * @return IDm to use for responses/comparisons in the current System context. + */ +FelicaIDm felica_listener_get_current_idm(const FelicaListener* instance); + /** This is the first request validation function. * * Its main aim is to check whether the input request is valid in general by diff --git a/lib/nfc/protocols/felica/felica_poller.c b/lib/nfc/protocols/felica/felica_poller.c index c37b84be7..b3dd2fec0 100644 --- a/lib/nfc/protocols/felica/felica_poller.c +++ b/lib/nfc/protocols/felica/felica_poller.c @@ -46,6 +46,8 @@ static FelicaPoller* felica_poller_alloc(Nfc* nfc) { instance->systems_read = 0; instance->systems_total = 0; + memset(&instance->card_idm, 0, sizeof(instance->card_idm)); + memset(&instance->card_pmm, 0, sizeof(instance->card_pmm)); return instance; } @@ -88,6 +90,8 @@ NfcCommand felica_poller_state_handler_activate(FelicaPoller* instance) { FelicaError error = felica_poller_activate(instance, instance->data); if(error == FelicaErrorNone) { + instance->card_idm = instance->data->idm; + instance->card_pmm = instance->data->pmm; furi_hal_random_fill_buf(instance->data->data.fs.rc.data, FELICA_DATA_BLOCK_SIZE); felica_get_workflow_type(instance->data); @@ -151,10 +155,35 @@ NfcCommand felica_poller_state_handler_list_system(FelicaPoller* instance) { NfcCommand felica_poller_state_handler_select_system_idx(FelicaPoller* instance) { FURI_LOG_D(TAG, "Select System Index %d", instance->systems_read); - uint8_t system_index_mask = instance->systems_read << 4; - instance->data->idm.data[0] &= 0x0F; - instance->data->idm.data[0] |= system_index_mask; - instance->state = FelicaPollerStateTraverseStandardSystem; + + // Each System on a multi-system card can only be addressed after Polling again + // with that System's own Code, which returns the IDm/PMm valid for it. Not every + // card tolerates commands sent with a locally-guessed IDm (e.g. Android HCE + // FeliCa emulation times out), so this must be a real over-the-air exchange. + const FelicaSystem* system = + simple_array_cget(instance->data->systems, instance->systems_read); + const FelicaPollerPollingCommand polling_cmd = { + .system_code = __builtin_bswap16(system->system_code), + .request_code = 0, + .time_slot = FELICA_TIME_SLOT_1, + }; + FelicaPollerPollingResponse polling_resp = {}; + + FelicaError error = felica_poller_polling(instance, &polling_cmd, &polling_resp); + if(error == FelicaErrorNone) { + instance->data->idm = polling_resp.idm; + instance->data->pmm = polling_resp.pmm; + instance->state = FelicaPollerStateTraverseStandardSystem; + } else { + // Couldn't switch into this System - leave it empty and move on rather than + // failing the whole multi-System read over one unreachable System. + FURI_LOG_W(TAG, "Polling for System %d failed: %d", instance->systems_read, error); + FelicaSystem* mut_system = + simple_array_get(instance->data->systems, instance->systems_read); + simple_array_reset(mut_system->services); + simple_array_reset(mut_system->areas); + instance->state = FelicaPollerStateReadStandardBlocks; + } return NfcCommandContinue; } @@ -305,9 +334,12 @@ NfcCommand felica_poller_state_handler_traverse_standard_system(FelicaPoller* in } if(len == 0x0E) { + uint16_t code_end = + (uint16_t)(list_service_payload[2] | (list_service_payload[3] << 8)); FelicaArea* area = felica_area_array_push_raw(area_buffer); memset(area, 0, sizeof *area); area->code = code_begin; + area->end_code = code_end; area->first_idx = (uint16_t)felica_service_array_size(service_buffer); area->last_idx = 0; } else { @@ -353,6 +385,61 @@ NfcCommand felica_poller_state_handler_traverse_standard_system(FelicaPoller* in simple_array_get_count(system->services), simple_array_get_count(system->areas)); + // Request key versions for all discovered areas and services. + // FeliCa spec limits n ≤ 32 per Request Service command; batch if needed. + // Note: 0xFFFF is not a valid Area/Service code and must never be queried here - + // some real cards stop responding to the whole exchange when sent an out-of-range + // code, which previously broke traversal of every system after the first. + do { + uint32_t svc_count = simple_array_get_count(system->services); + uint32_t area_cnt = simple_array_get_count(system->areas); + uint32_t total = area_cnt + svc_count; + if(total == 0) break; + + uint16_t* codes = malloc(total * sizeof(uint16_t)); + uint16_t* key_vers = malloc(total * sizeof(uint16_t)); + + for(uint32_t i = 0; i < area_cnt; i++) + codes[i] = ((const FelicaArea*)simple_array_cget(system->areas, i))->code; + for(uint32_t i = 0; i < svc_count; i++) + codes[area_cnt + i] = + ((const FelicaService*)simple_array_cget(system->services, i))->code; + + bool all_ok = true; + for(uint32_t offset = 0; offset < total; offset += FELICA_REQUEST_SERVICE_MAX_NODES) { + uint32_t remaining = total - offset; + uint8_t batch = (remaining > FELICA_REQUEST_SERVICE_MAX_NODES) ? + FELICA_REQUEST_SERVICE_MAX_NODES : + (uint8_t)remaining; + FelicaError err = + felica_poller_request_service(instance, codes + offset, batch, key_vers + offset); + if(err != FelicaErrorNone) { + FURI_LOG_W(TAG, "Request service failed (offset=%lu, err=%d)", offset, err); + for(uint32_t i = offset; i < total; i++) + key_vers[i] = 0xFFFF; + all_ok = false; + break; + } + } + + for(uint32_t i = 0; i < area_cnt; i++) { + FelicaArea* area = (FelicaArea*)simple_array_get(system->areas, i); + area->key_version = key_vers[i]; + // Area 0000 represents the System itself; mirror its key version. + if(area->code == 0x0000) { + system->key_version = key_vers[i]; + } + } + for(uint32_t i = 0; i < svc_count; i++) + ((FelicaService*)simple_array_get(system->services, i))->key_version = + key_vers[area_cnt + i]; + + FURI_LOG_I(TAG, "System key version: %04X (all_ok=%d)", system->key_version, all_ok); + + free(codes); + free(key_vers); + } while(false); + felica_service_array_clear(service_buffer); felica_area_array_clear(area_buffer); @@ -501,7 +588,10 @@ NfcCommand felica_poller_state_handler_read_success(FelicaPoller* instance) { instance->felica_event.type = FelicaPollerEventTypeReady; } - instance->data->idm.data[0] &= 0x0F; + if(instance->data->workflow_type == FelicaStandard) { + instance->data->idm = instance->card_idm; + instance->data->pmm = instance->card_pmm; + } instance->felica_event_data.error = FelicaErrorNone; return instance->callback(instance->general_event, instance->context); @@ -510,7 +600,10 @@ NfcCommand felica_poller_state_handler_read_success(FelicaPoller* instance) { NfcCommand felica_poller_state_handler_read_failed(FelicaPoller* instance) { FURI_LOG_D(TAG, "Read Fail"); instance->callback(instance->general_event, instance->context); - instance->data->idm.data[0] &= 0x0F; + if(instance->data->workflow_type == FelicaStandard) { + instance->data->idm = instance->card_idm; + instance->data->pmm = instance->card_pmm; + } return NfcCommandStop; } diff --git a/lib/nfc/protocols/felica/felica_poller_i.c b/lib/nfc/protocols/felica/felica_poller_i.c index bbbb824d3..44d426e2d 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.c +++ b/lib/nfc/protocols/felica/felica_poller_i.c @@ -267,6 +267,55 @@ FelicaError felica_poller_list_service_by_cursor( return error; } +FelicaError felica_poller_request_service( + FelicaPoller* instance, + const uint16_t* codes, + uint8_t code_count, + uint16_t* key_versions_out) { + furi_assert(instance); + furi_assert(codes); + furi_assert(key_versions_out); + + // data = n(1) + codes(2n) + size_t data_len = 1 + (size_t)code_count * 2; + uint8_t* data = malloc(data_len); + data[0] = code_count; + for(uint8_t i = 0; i < code_count; i++) { + data[1 + i * 2] = (uint8_t)(codes[i] & 0xFF); + data[2 + i * 2] = (uint8_t)(codes[i] >> 8); + } + + felica_poller_prepare_tx_buffer_raw( + instance, FELICA_CMD_REQUEST_SERVICE, data, (uint8_t)data_len); + free(data); + bit_buffer_reset(instance->rx_buffer); + + FelicaError error = felica_poller_frame_exchange( + instance, instance->tx_buffer, instance->rx_buffer, FELICA_POLLER_POLLING_FWT); + if(error != FelicaErrorNone) { + FURI_LOG_E(TAG, "Request service failed with error: %d", error); + return error; + } + + size_t rx_len = bit_buffer_get_size_bytes(instance->rx_buffer); + // Response: length(1) + RC(1) + IDm(8) + n(1) + key_versions(2n) = 11 + 2n + if(rx_len < (size_t)(11 + (size_t)code_count * 2)) { + return FelicaErrorProtocol; + } + + const uint8_t* rx = bit_buffer_get_data(instance->rx_buffer); + uint8_t resp_n = rx[10]; + uint8_t actual = (resp_n < code_count) ? resp_n : code_count; + for(uint8_t i = 0; i < actual; i++) { + key_versions_out[i] = (uint16_t)(rx[11 + i * 2] | ((uint16_t)rx[12 + i * 2] << 8)); + } + for(uint8_t i = actual; i < code_count; i++) { + key_versions_out[i] = 0xFFFF; + } + + return FelicaErrorNone; +} + FelicaError felica_poller_list_system_code( FelicaPoller* instance, FelicaListSystemCodeCommandResponse** const response_ptr) { diff --git a/lib/nfc/protocols/felica/felica_poller_i.h b/lib/nfc/protocols/felica/felica_poller_i.h index 446f34c43..38873e4b3 100644 --- a/lib/nfc/protocols/felica/felica_poller_i.h +++ b/lib/nfc/protocols/felica/felica_poller_i.h @@ -11,6 +11,9 @@ extern "C" { #define FELICA_POLLER_POLLING_FWT (200000U) +// FeliCa spec section 4.4.3: 1 ≤ n ≤ 32 +#define FELICA_REQUEST_SERVICE_MAX_NODES (32U) + #define FELICA_POLLER_CMD_POLLING_REQ_CODE (0x00U) #define FELICA_POLLER_CMD_POLLING_RESP_CODE (0x01U) @@ -46,6 +49,13 @@ struct FelicaPoller { uint8_t block_index; uint8_t systems_read; uint8_t systems_total; + // IDm/PMm obtained from the initial (wildcard) activation Polling - this is the + // card's own identity, distinct from whatever System is currently selected via + // instance->data->idm while traversing. Restored into instance->data on success/ + // failure so the saved/displayed IDm always reflects the card, not the last + // System visited. + FelicaIDm card_idm; + FelicaPMm card_pmm; void* context; }; @@ -124,6 +134,12 @@ FelicaError felica_poller_list_system_code( FelicaPoller* instance, FelicaListSystemCodeCommandResponse** response_ptr); +FelicaError felica_poller_request_service( + FelicaPoller* instance, + const uint16_t* codes, + uint8_t code_count, + uint16_t* key_versions_out); + #ifdef __cplusplus } #endif From 610f8ae90b063e79b76b8994bc91d4756aa2e19e Mon Sep 17 00:00:00 2001 From: hypery11 Date: Wed, 7 Oct 2026 21:17:32 +0800 Subject: [PATCH 4/6] appmanifest: reject float fap_version with a clear error (refs ufbt#52) (#4375) * appmanifest: validate fap_version type; accept int, reject float * appmanifest: narrow fap_version validation * appmanifest: normalize list fap_version, reject other types clearly Lists like fap_version=[2, 3] are used by six catalog apps and must keep building, so accept them and convert to a tuple. Anything that is not a str, tuple or list now raises FlipperManifestException naming the type instead of failing later with a TypeError from len(). --- scripts/fbt/appmanifest.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/scripts/fbt/appmanifest.py b/scripts/fbt/appmanifest.py index 4da23b3dd..fbcd9ffb3 100644 --- a/scripts/fbt/appmanifest.py +++ b/scripts/fbt/appmanifest.py @@ -112,6 +112,19 @@ class FlipperApplication: raise FlipperManifestException( f"Invalid appid '{self.appid}'. Must match regex '{self.APP_ID_REGEX}'" ) + + # Reject float explicitly. Python float literals silently drop trailing + # zeros (1.10 becomes 1.1), so a float is never a safe way to write a + # version number. Surface the problem here with a clear message rather + # than letting a cryptic "'float' object is not iterable" TypeError + # appear deep in the build step (see flipperdevices/flipperzero-ufbt#52). + if isinstance(self.fap_version, float): + raise FlipperManifestException( + f"Invalid fap_version {self.fap_version!r}: floats are not accepted " + f"because Python drops trailing zeros (1.10 becomes 1.1). " + f'Write the version as a string ("1.0") or a tuple ((1, 0)).' + ) + if isinstance(self.fap_version, str): try: self.fap_version = tuple(int(v) for v in self.fap_version.split(".")) @@ -119,8 +132,15 @@ class FlipperApplication: raise FlipperManifestException( f"Invalid version '{self.fap_version}'. Must be in the form 'major.minor'" ) - if len(self.fap_version) < 2: - raise ValueError("Not enough version components") + elif isinstance(self.fap_version, (tuple, list)): + self.fap_version = tuple(self.fap_version) + else: + raise FlipperManifestException( + f"Invalid fap_version {self.fap_version!r}: expected a string like " + f'"1.2" or a tuple like (1, 2), got {type(self.fap_version).__name__}' + ) + if len(self.fap_version) < 2: + raise ValueError("Not enough version components") class AppManager: From 356b15e31e3a0c1548f43bdae50b0346bbd518b2 Mon Sep 17 00:00:00 2001 From: David Coles Date: Wed, 7 Oct 2026 22:26:31 +0900 Subject: [PATCH 5/6] SubGhz: Add Security+2.0 433.92 MHz (#4346) This is used by several Merlin (Chamberlain) garage door openers in Australia. --- .../main/subghz/helpers/subghz_custom_event.h | 1 + .../main/subghz/scenes/subghz_scene_set_type.c | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/applications/main/subghz/helpers/subghz_custom_event.h b/applications/main/subghz/helpers/subghz_custom_event.h index 571f3feb9..af1c2865d 100644 --- a/applications/main/subghz/helpers/subghz_custom_event.h +++ b/applications/main/subghz/helpers/subghz_custom_event.h @@ -22,6 +22,7 @@ typedef enum { SubmenuIndexSecPlus_v2_310_00, SubmenuIndexSecPlus_v2_315_00, SubmenuIndexSecPlus_v2_390_00, + SubmenuIndexSecPlus_v2_433_92, //SubGhzCustomEvent SubGhzCustomEventSceneDeleteSuccess = 100, diff --git a/applications/main/subghz/scenes/subghz_scene_set_type.c b/applications/main/subghz/scenes/subghz_scene_set_type.c index f76bd9e27..9c13c72c1 100644 --- a/applications/main/subghz/scenes/subghz_scene_set_type.c +++ b/applications/main/subghz/scenes/subghz_scene_set_type.c @@ -109,6 +109,12 @@ void subghz_scene_set_type_on_enter(void* context) { SubmenuIndexSecPlus_v2_390_00, subghz_scene_set_type_submenu_callback, subghz); + submenu_add_item( + subghz->submenu, + "Security+2.0_433", + SubmenuIndexSecPlus_v2_433_92, + subghz_scene_set_type_submenu_callback, + subghz); submenu_set_selected_item( subghz->submenu, scene_manager_get_scene_state(subghz->scene_manager, SubGhzSceneSetType)); @@ -212,6 +218,11 @@ bool subghz_scene_set_type_on_event(void* context, SceneManagerEvent event) { generated_protocol = subghz_txrx_gen_secplus_v2_protocol( subghz->txrx, "AM650", 390000000, key, 0x68, 0xE500000); break; + case SubmenuIndexSecPlus_v2_433_92: + key = (key & 0x7FFFF3FC); // 850LM pairing + generated_protocol = subghz_txrx_gen_secplus_v2_protocol( + subghz->txrx, "AM650", 433920000, key, 0x68, 0xE500000); + break; default: return false; break; From 76bddb682f907928afab3bbd9e015bc4e3e4ee6e Mon Sep 17 00:00:00 2001 From: Vladimir Kuznichenkov Date: Wed, 7 Oct 2026 17:00:22 +0300 Subject: [PATCH 6/6] feat: add Indala 224-bit (long format) RFID protocol support (#4343) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add Indala 224-bit (long format) RFID protocol support Add complete Indala 224-bit protocol: decode, encode, emulate, and write/clone via T5577. Uses PSK2 modulation (RF/32, bitrate 2) with a 30-bit preamble (1 + 29 zeros) and 224-bit data payload. Decoder performs PSK2 differential decoding (data = phase XOR next_phase) from the shift register, handles both normal and inverted preambles for phase-alternating cards, and validates across 6 consecutive frames. Writer configures T5577 with block 0 = 0x000820E0 matching Proxmark3 reference implementation. Encoder uses PSK2 carrier-flip modulation where carrier phase inverts on data bit = 1. Verified against Proxmark3 lf indala clone output: 80000001B23523A6C2E31EBA3CBEE4AFB3C6AD1FCF649393928C14E5 Includes unit tests for roundtrip encode/decode and PSK2 phase alternation edge case. * Append Indala224 to protocol enum, move its test registration * fbt format * Indala224: don't drop a full-frame phase run --------- Co-authored-by: Štefan Croitoru <66339601+turbospok@users.noreply.github.com> --- .../tests/lfrfid/lfrfid_protocols.c | 131 ++++++++ lib/lfrfid/protocols/lfrfid_protocols.c | 2 + lib/lfrfid/protocols/lfrfid_protocols.h | 1 + lib/lfrfid/protocols/protocol_indala224.c | 283 ++++++++++++++++++ lib/lfrfid/protocols/protocol_indala224.h | 4 + 5 files changed, 421 insertions(+) create mode 100644 lib/lfrfid/protocols/protocol_indala224.c create mode 100644 lib/lfrfid/protocols/protocol_indala224.h diff --git a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c index 4308eae8e..ce8ab338e 100644 --- a/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c +++ b/applications/debug/unit_tests/tests/lfrfid/lfrfid_protocols.c @@ -612,6 +612,134 @@ 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); @@ -629,6 +757,9 @@ MU_TEST_SUITE(test_lfrfid_protocols_suite) { MU_RUN_TEST(test_lfrfid_protocol_fdxb_read_simple); MU_RUN_TEST(test_lfrfid_protocol_fdxb_emulate_simple); + + MU_RUN_TEST(test_lfrfid_protocol_indala224_roundtrip); + MU_RUN_TEST(test_lfrfid_protocol_indala224_alternating_phase); } int run_minunit_test_lfrfid_protocols(void) { diff --git a/lib/lfrfid/protocols/lfrfid_protocols.c b/lib/lfrfid/protocols/lfrfid_protocols.c index c6cc57a68..c3c49f5f8 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 a33e87990..ba1ed09df 100644 --- a/lib/lfrfid/protocols/lfrfid_protocols.h +++ b/lib/lfrfid/protocols/lfrfid_protocols.h @@ -33,6 +33,7 @@ typedef enum { LFRFIDProtocolSecurakey, LFRFIDProtocolGProxII, LFRFIDProtocolNoralsy, + LFRFIDProtocolIndala224, LFRFIDProtocolMax, } LFRFIDProtocol; diff --git a/lib/lfrfid/protocols/protocol_indala224.c b/lib/lfrfid/protocols/protocol_indala224.c new file mode 100644 index 000000000..15dd96e7e --- /dev/null +++ b/lib/lfrfid/protocols/protocol_indala224.c @@ -0,0 +1,283 @@ +#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; + + // A payload of all zeros holds the PSK2 carrier phase for a whole frame, so a run of + // exactly INDALA224_ENCODED_BIT_SIZE bits is legitimate and must not be discarded. + 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 000000000..de23d8034 --- /dev/null +++ b/lib/lfrfid/protocols/protocol_indala224.h @@ -0,0 +1,4 @@ +#pragma once +#include + +extern const ProtocolBase protocol_indala224;