227 Commits
Author SHA1 Message Date
Rafael Fadel 82ed10e91c SubGHz: fix subghz rx, tx and tx_from_file over the CLI failing on the internal radio (#1216)
The CLI checks the result of subghz_devices_begin() since #1171, but the
internal CC1101 has no begin hook and subghz_devices_begin() returned false
for any device without one. Every CLI rx/tx on device 0 stopped at
"Device 0 failed to start", and the failure path shut the radio down.

A device with no begin hook has nothing to start, so starting it succeeds.
The external module still reports its real self-test result.

Fixes #1215
2026-11-13 13:00:50 +03:00
Mykhailo ShevchukandClaude Opus 5 3433331e31 Infrared: fix ir universal rebooting the device, and the universal record lookup behind it (#1211)
The dictionary lookup in infrared_brute_force_calculate_messages was guarded by
furi_assert(record), which is compiled out whenever FURI_DEBUG is undefined. On every
release build, a database name the caller had not registered dereferenced NULL.

`ir universal <remote> <signal>` registers exactly one name and then walks the whole
database, so the first name that is not the one asked for crashed the device. Every
shipped database holds several distinct names, so the command rebooted the Flipper every
time it ran and could never have worked. The same crash was reachable from the Universal
screens with any user-supplied .ir database holding a button the screen does not use; the
six shipped ones are safe only because each scene happens to register a list that matches
its asset exactly.

A miss is skipped now, which is also the semantics the CLI wants - collect the records it
asked for, ignore the rest - so the command sends instead of crashing.

Three more things in the same code:

- calculate_messages reported success when a signal failed validation: error stayed
  InfraredErrorCodeNone, the loop broke, and the function returned clean. So a database
  holding an invalid signal validated clean.
- set_raw_signal ignores a signal with no timings and returns without touching the
  destination, so a raw entry with an empty data: left the previously read signal in
  place. Read as the entry after a parsed one, it passed infrared_signal_is_valid against
  that stale message, and its file position went into the dictionary to be transmitted
  later as whatever the signal object happened to hold. Reported at the read now, where
  every caller sees it - and it is the concrete asset that reaches the validation exit
  above.
- infrared_brute_force_reset did not clear db_filename, so a caller that forgot
  set_db_filename silently re-read the previous database. The CLI also stopped the
  instance a second time if the database disappeared between calculate_messages and
  start, since start stops itself on a failed open; dead code until the lookup fix made
  the path reachable.

The four universal database tests no longer register any names. Their assertion is that
every signal in the shipped asset parses and is valid, and the name lists were four
hand-maintained copies of the ones the app scenes keep - the drift that produced the test
bug fixed in #1199. fans.ir and leds.ir are covered now, so all six shipped databases are
tested instead of four, and one new test covers the dictionary hit, which nothing
exercised once the lists went.

Verified on an f7. Before, `ir universal tv Power` dropped USB and reset uptime from
137h3m52s to 12s. After, it reports "Sending 295 signal(s)..." and transmits; all six
remotes send (tv 295, audio 131, fans 116, leds 51, projectors 44, ac 31), the Universal
screens load and send unchanged, and uptime ran unbroken across the whole session. All
seven infrared unit tests pass on device.

Fixes #1201

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-10 14:35:04 +02:00
Dinko KorunicandMX 57d471868b SubGHz: Add Proteco MOVER protocol (433.92MHz AM650, 24 bit static, decoder + encoder) (#1210)
* SubGHz: add Proteco MOVER protocol

Proteco MOVER gate remotes (433.92 MHz AM650, 24 bit fixed code) use CAME
line coding at Te ~440us instead of 320us, so the CAME decoder rejected them
and they only worked as RAW. Adds decoder and encoder, unit tests and docs.
https://github.com/flipperdevices/flipperzero-firmware/issues/2345

* SubGHz: Proteco Add Manually entry, B/C button decoder tests

Add Manually: "Proteco 433MHz", a random 24 bit key with the low 2 bits
set to button A (gate), as on the Proteco TX3 remote (A = 0x2, B = 0x0,
C = 0x1). Advanced mode lets the key be edited.

Unit tests: one-press RAW captures of buttons B and C from a TX3 remote,
next to the existing button A capture.

* proteco custom buttons, guard checked against frame te

---------

Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com>
2026-11-10 01:52:26 +03:00
MX 648fad7793 Merge remote-tracking branch 'OFW/dev' into dev 2026-11-09 01:52:26 +03:00
Leptopt1los 3060cee9ba fix(*): file renaming case sensitivity fixed (#4512) 2026-11-08 22:00:04 +03:00
MX 069616d840 Merge remote-tracking branch 'OFW/dev' into dev 2026-11-08 19:47:27 +03:00
Leptopt1los 80e69c093f iButton: TM01x cleanup/refactor (#4511)
* refactor(*): tm01x refactor

* feat(*): ibutton write refusal notification added
2026-11-08 02:50:12 +03:00
Leptopt1los 0aa3d5b74f iButton: TM01x Cyfral && Metakom write support (#4508)
* feat(*): tm01 write cyfral and metakom impl

* refactor(*): reuse lib functions

* refactor(*): cleanup
2026-11-07 04:54:58 +03:00
Leptopt1los edc3bc6fc1 iButton: TM01x Cyfral && Metakom write support - #4508 (#1209)
* feat(*): tm01 write cyfral and metakom impl

* refactor(*): reuse bitlib

* refactor(*): fix review
2026-11-07 04:43:12 +03:00
MX 2e9d147ef5 new subghz remote analyzer app and new protocol [ci skip]
also improve some stuff in existing apps
2026-11-04 02:52:09 +03:00
Mykhailo ShevchukandClaude Opus 5 1cdda9a446 SubGHz: fix the 6 test_subghz failures (#1207)
Five encoder round-trips and the random decode count. Two of the five are
protocol bugs, three were the test feeding the decoder something no radio
ever sends.

The round trip fed the encoder's upload straight to the decoder. Nothing
on the device does that: every transmission passes through
furi_hal_subghz_async_tx_middleware_get_duration(), which sums
consecutive same-level durations and emits one per level change, because
the timer only toggles. Legrand emits its sync and the first key bit as
two lows, Dooya and Revers RB2 the frame's last low and the next repeat's
first low - Revers RB2's 320 us tail is meant to join the next frame's
leading 250 us into the ~570 us gap its decoder syncs on. On air each of
those is one carrier gap; fed separately the decoder never syncs. The
test merges them the same way, which also makes the round trip a test of
what we transmit rather than of an intermediate buffer. Only the merging
is mirrored: the middleware also drops a leading low and a trailing
pending low, since the timer can only toggle, but the carrier is off
either side of a burst regardless, so a receiver does see both.

Marantec24 could not decode a frame the Flipper itself sends. The gap
between frames is te_long * 9 + te_short - 15200 us, which is what the
encoder emits - but the decoder centred its window on te_long * 9 and
allowed te_delta * 4, putting that gap exactly 800 us away, and
DURATION_DIFF is compared with '<'. Captured remotes decoded only because
they fall a little short of it: measured across the 99 long lows in
marantec24_raw.sub they run 15118-15188 us, so the worst real frame
cleared the old window by 12 us. The window is centred on the gap now, so
those frames sit 12-82 us off centre instead, and the expression is a
macro the encoder uses too - the two had drifted apart, which is the bug.

Linear Delta 3 decoded one key in three frames instead of one in two, and
none at all from our own encoder. On a complete key it set the step to
SaveDuration without clearing the counters, and LinearDecoderStepReset is
the only other place they are cleared, so the count ran past 8; the next
long gap does leave the parser in Reset, but resyncing from there costs
two more frames, and the encoder sends three repeats in total. The
counters are cleared on a complete key now, as the five sibling decoders
using the same two-identical-keys check already do.

Linear Delta 3 also sends a fourth repeat now. One decode was the whole
margin and it came from the stream's last entry, which the middleware
drops; a fourth frame gives the two-identical-keys check a complete
interior pair. It is the encoder's default rather than an override in the
test fixture, because a receiver needs that frame whoever is listening -
the first frame is spent finding the header, leaving three for a check
that needs two identical keys after it.

Holtek HT12X gets its own decoder reset, clearing last_data as its four
siblings do. The shared reset writes only parser_step, so last_data
survived it and the first frame after a receiver reset could be confirmed
against a key from before it.

Two more things the review turned up:

  * Both test helpers now reset the receiver, so a case starts from a
    known decoder state instead of inheriting one - the Linear Delta 3
    decoder test runs before its encoder test and leaves last_data
    behind. It clears last_data only for the protocols that define their
    own reset, which is why Holtek HT12X needed one; no reset clears the
    bit counters, which only a header re-sync does.
  * Both loops that read the file encoder worker skip a wait marker.
    level_duration_wait() sets only the level field, so the duration bits
    are whatever they were, and the loops fed that to a decoder. It is
    reachable: the worker returns a wait when this loop drains its buffer
    faster than it refills, and feeding one moves the random test's
    count, which is the whole result of that test. The encoder loop needs
    no such check - subghz_transmitter_yield never returns a wait.
  * A test that fails now says why: a protocol missing from the
    receiver's decodable set, a fixture the transmitter would not
    deserialize, a protocol with no encoder (guarded rather than left to
    furi_check, which would take the suite down with it), and a round
    trip whose own decoder read nothing back all printed nothing before.

The random test prints what it decoded when the count differs from
TEST_RANDOM_COUNT_PARSE. That number is the entire result of the test and
it drifts whenever a protocol is added or made stricter - both fixes
above make their decoders fire more often - so it has to be visible to be
updatable. It is deliberately not guessed at here.

CHANGELOG: every entry is one line now, per the rule
`- Category: **Title** (by @author | PR #n | Closes #n)`. 64 entries had
grown a paragraph of prose each; the detail belongs in the PR, which is
linked from the entry.

Host-verified: both firmwares build, lint clean, and each state machine
was transcribed and run through the same merge to reproduce every failure
and then watch the fix take (legrand 0 to 2 decodes, dooya 0 to 2,
marantec24 0 to 1, linear delta 3 0 to 2 at its new default of four
repeats, and 0 at any repeat count below five without the clear). The run that
settles it needs a Flipper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-11-03 17:06:33 +02:00
Mykhailo Shevchuk 53f1ad9281 Unit tests: report honestly, and build the suite in CI (#1206) 2026-11-03 13:53:16 +02:00
Mykhailo ShevchukandClaude Opus 5 c9ff703513 Unit tests: cover the NFC supported-card parsers (#1204)
None of the 41 supported-card parsers had any test coverage, which is what
let #1091 - a checker that rejected roughly nine genuine cards in ten - ship
for four releases unnoticed. The gap looked structural: the parsers are
static code inside .fal plugins of the NFC app, so the test firmware cannot
link them.

It does not have to. A parser imports only symbols the firmware already
exports, so firmware_api_interface alone resolves one, and parse() takes a
loaded NfcDevice and a FuriString - no radio, no card. So a test plugin can
load a parser exactly as the app does and run it over a saved dump. Nothing
new is needed to deploy them either: PLUGIN is an "always deploy" app type,
so all 45 parser .fal files are already on the card under every app set,
including FIRMWARE_APP_SET=unit_tests.

Adds a test_nfc_parsers suite with five tests. Four cover the Bambu Lab
parser, the first one under this harness: a full-string render of a known
spool, the trailing-zero variant id a real tag spells, a near-miss card it
must decline, and a factory-blank card from nfc_data_generator. The fifth
walks every parser on the card, loads each one and asserts it exposes a
non-NULL parse and a valid protocol. That one is the point: an unresolved
import in a plugin is only a build warning - do_strict_import_checks is
false for FlipperAppType.PLUGIN - and nfc_supported_cards.c then skips a
parser that fails to load in silence, so a parser can stop working with
nothing in the project saying so.

The fixtures are synthesised, never dumps of real cards. That is safe here
only because every Bambu validation rule is externally documented: a "GF"
magic prefix, a known filament-type string, a printable-ASCII test and a
diameter range. A parser gated on a computed check digit, CRC or MAC must
not get a hand-built fixture - it would be built with the same algorithm the
validator uses, so the test would pass green while the validator still
rejected real cards. #1091 would not have been caught that way. Those
parsers need real dumps, and therefore the privacy question #1095 raises.

NFC_SUPPORTED_CARDS_PLUGIN_SUFFIX moves from the app's helper into the
plugin contract header it belongs to, so the test can use the same value
rather than repeating it. No exported symbol changes, so the API stays at
88.23.

Verified on hardware (f7, COM12): `unit_tests test_nfc_parsers` reports
Failed tests: 0, Leaked: 0. A full run still shows only the six known
subghz failures.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-11-03 11:31:14 +02:00
Mykhailo ShevchukandClaude Opus 5 6aa1e55dc0 Unit tests: fix FIRMWARE_APP_SET=unit_tests not building (#1199)
* Unit tests: drop the JS tests so the test firmware links again

js_app became FlipperAppType.EXTERNAL in 1a402a800, so its symbols now live
in a .fap instead of the firmware image. unit_test_api_table_i.h still took
the address of four of them, which failed the firmware link and took all 30
test suites down with it, not just the JS ones.

The test runner lives in the firmware and resolves plugin imports against
unit_tests_api_table, so it cannot reach a symbol that lives inside an app.
Dropping the JS tests is the only fix that does not put the JS engine back
into the firmware image.

Closes #1185

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Unit tests: document the build flags the test firmware needs

The command in UnitTests.md used the repo defaults, where fbt_options.py
sets DEBUG = 1. A debug image is ~13 KB larger than a release one and no
longer fits below the radio stack's flash region at 0x080D7000, so
scripts/update.py refuses to package it. Measured: unit_tests debug
887,873 B (7,233 B over), default app set debug 890,937 B (10,297 B over),
unit_tests with COMPACT=1 DEBUG=0 874,537 B (6,103 B under).

So this is not specific to the test firmware, and trimming its app set
would not help. The documented command now carries the flags CI already
uses, with a note that furi_assert() is compiled out in that build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* JS: drop the tests module that only served the deleted JS tests

modules/js_tests.c gave the four resources/unit_tests/js/*.js scripts their
assert_eq/fail helpers and nothing else. With those scripts gone nothing in
the tree can reach it, and it is listed unconditionally in both the js_app
and cli_js sources, so it kept compiling into every build while only the
registration sat behind #ifdef FW_CFG_unit_tests.

FW_CFG_unit_tests itself stays in use by lib/nfc/nfc.c and nfc_mock.c.

Also give the UnitTests.md note the measured overflow figures and the API
version they were taken at, so the claim does not rot, and say plainly that
the js_app app-set entry was hygiene rather than part of the link fix:
EXTERNAL_APP_TYPES_MAP marks EXTERNAL apps "always deploy", so js_app.fap
is built whatever the app set says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Unit tests: finish the JS removal and pin the flags to DEBUG=0

Review follow-ups, all consequences of the deletion rather than new work:

- packages/fz-sdk/tests/index.d.ts still declared fail/assert_eq/
  assert_float_close for a module that no longer exists in any build, so a
  script written against the SDK type-checked clean and then failed at
  runtime on require("tests"). The three declarations matched the deleted
  js_tests_create exports one for one. This tree is excluded from fbt lint,
  which is why nothing caught it.
- tsconfig.json included resources/unit_tests/js, now deleted.
- .vscode/example/tasks.json ran the unit-test flash without the flags, so
  the IDE task hit the very packaging failure UnitTests.md now documents.
- documentation/fbt.md gave the bare command too; it now carries the flags
  and links UnitTests.md for the reason.

Also correct two claims. COMPACT=1 is not required: fbt_options.py already
sets it, and under DEBUG=1 the COMPACT branch of firmwareopts.scons is
unreachable, so DEBUG=0 is the flag doing the work and the one the assert
caveat belongs to. And the CHANGELOG said "30 test suites" where 30 is the
count of test plugins - MU_RUN_SUITE appears 45 times, so "suite" means
something else in this directory.

The note now quotes the message the build actually prints, so it can be
found by searching for it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Infrared: fix the universal projector database test

Found by running the suite, which this branch makes possible again.

The test opened infrared/assets/projector.ir. The shipped asset and the
app's own projector scene both use projectors.ir, so the open failed and
the test reported the database invalid.

Fixing only the name made it worse: the test registered 4 button names
while the database carries 6, and the 20 Play/Pause records then hit the
dictionary lookup in infrared_brute_force_calculate_messages() that
returns NULL for an unregistered name. That is guarded by furi_assert
alone, which DEBUG=0 compiles out - the build the test firmware now
documents - so it was a NULL dereference and a reboot rather than a
failed assertion. Registering Play and Pause, as the app already does,
makes the test match its asset.

The other three database tests were checked the same way and already
match theirs exactly: ac 6/6, audio 8/8, tv 6/6.

This mattered beyond the one test: the runner stops at the first plugin
that fails, so 13 of the 29 plugins never ran while this was broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Docs: tighten the unit test build note

Review follow-ups on prose only, no behaviour change.

The NOTE opened by asserting DEBUG=0 is required and then proved it in the
next sentence, and said "not specific to the test firmware" immediately
before the default-app-set figure that demonstrates it. Both restatements
are gone; the error string, both measurements and the furi_assert caveat
stay, and the COMPACT=1 aside is shorter.

The CHANGELOG quoted "all 30 test plugins" on one line and "13 of the 29"
on the line below it. Both were right - 30 before this branch, 29 after -
but nothing said so, which reads as a contradiction. The first no longer
gives a number.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-11-02 20:22:34 +02:00
Mykhailo ShevchukandClaude Opus 5 2cbad71c92 T5577: Sniff Password, a user dictionary, and two fixes for the 38-bit frame hazard (#1192)
* RFID: do not send a T5577 password to a chip that is not asking

A passworded read and a plain write are both 38 bits and the tag tells
them apart by length alone, so a password frame that reaches a tag with
no password set is parsed as a write of `password << 1` into the block it
addressed - with the password's bit 31 landing on that block's lock bit,
which is one way.

A restore escalates to that frame when its read back fails, and until now
it decided to from the dump alone. Block 0 goes back last, so every write
lands on the chip as it was found while the read back lands on the one the
order has just made: the two take their answer from different places, and
`t5577_restore_password()` is where that is now said once.

Also here, from the same run:

- the plain detect is retried three times before escalating, because it is
  three reads of block 0 and a badly coupled tag can lose all three; a
  false negative there is what reaches for the dangerous frame
- the restore logs which of the two reasons left it unable to check, which
  the screen's "could not check" cannot distinguish
- a block is checked with t5577_block_holds() before its value is
  recovered. Block 1 of an EM4100 key is nine 1s and then long runs of one
  bit - the fewest transitions of any block to lock onto - so a key that
  was written perfectly is exactly where value recovery loses, and the
  restore then called it a tag that had moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: recover a password by listening to a reader or cloner

Sniff Password holds the coil against another device and decodes the
T5577 commands it sends. It transmits nothing - TIM2's external trigger
is wired to the carrier so the carrier resets the timer rather than
clocking it, and the timer only overflows when the carrier stops, which
is a gap. Every antenna pin stays low, so pointing it at hardware you do
not own cannot change anything.

Nothing is decoded in the handler. It records raw gap widths and the
periods between them into a ring, and the framing and decoding run
afterwards over the whole capture: a reader repeats one frame forever,
but a cloner sends each command once, and a framing rule applied in the
handler throws away the only copy.

Only the frame LENGTH picks the command, and 38 bits is a plain write or
a passworded read with the fields in different places. Both readings are
filled and neither is claimed unless the capture rules one out: a pause
too short for anything to have programmed rules out the write, and a
read-mode command after a programming-time wait rules out the read. That
command follows every read as well as every write, so on its own it
proves nothing - the wait is what separates them.

The bit threshold is measured per capture rather than fixed. A fixed one
cannot decode leading-zero mode at all, whose two classes both sit below
it, and a sender this has never met is the normal case. The classes are
the two peaks of a histogram filled from bursts whose length could be a
frame; clustering was tried and separates the data from its outliers
rather than one class from the other.

Validated against a blue cloner: its password recovered identically over
four runs, with its whole write sequence in clear, including the config
words that put the PWD bit on and take it off again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: do not report an unreadable reader from an empty coil

The verdict for "the carrier was cut but never at a width this can
measure" rested on `stalls > 0 && gaps == 0`, and the first half of that
is always true: the timer's trigger pin is never quiet, and open air
alone logs thousands of stalls with nothing in front of it. So an empty
coil reported a reader this could not read - a claim about somebody's
hardware drawn from a failure to parse, which is the one thing this
feature must not do.

Caught on a desk: "Can't measure - carrier cut, but never for a readable
width", with no reader present.

Removed. "Nothing heard" now carries the counts instead of advice. The
advice was also wrong in the case it mattered: "hold it on the reader's
coil" is unhelpful when the coil already is on one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* RFID: keep T5577 passwords the user found in a dictionary of their own

The shipped list says so itself - "Do not edit, this file will be
overwritten after firmware update" - so a password somebody recovered and
added to it survives until the next update and then does not. The NFC key
dictionaries solved this years ago with a system list and a user list;
this is the same split.

t5577_dict_user.lfrfid sits beside the shipped file and is read first. A
word somebody kept is likelier to open the tag in front of them than one
of the hundred-odd the vendor ships, and the order matters for more than
speed: every attempt before the right one is a password frame sent to a
tag that may not want it, and about a third of the shipped words have bit
31 set.

Both totals are counted at the start so the bar and the "of N" on the
status line do not change under the user when the sweep hands over, and
the hand-over happens when the first list runs out rather than when a
word count reaches its total - a list that stops early would otherwise
take the shipped words down with it.

Nothing creates the file. It opens as an empty list when absent, which is
what the sweep already does with a missing shipped list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: offer to keep a sniffed password

The app could learn a password off the air and had nowhere to put it, so
the next tag protected with it got the whole sweep again. Sniff Password
now offers Save, which appends to the user's own list.

Both lists are checked, not just the one being written to. The shipped
list holds a hundred-odd words a cloner is likely to be using - 51243648,
the first password this ever recovered off the air, is already line 79 of
it - so "already known" is the common answer for a stock cloner, and
saying so is more use than silently writing a duplicate. This follows
nfc_key_dict_import() rather than the hand-typed path, which checks only
the user list: a word that came off a cloner is far likelier to be one of
the shipped ones than a word somebody typed.

Offered only where the capture ESTABLISHED a password - a passworded read
or write, or a plain write into block 7. Never for the unresolved 38-bit
reading: that one is a guess between a password and a write, and the list
it would go into is one Clear Password later sends at tags, which is the
frame that cannot be taken back when it is wrong.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: show what the coil is picking up, not a progress bar

A capture has no total to count towards. One press of a cloner is a dozen
commands and every answer it holds, so a bar was wrong twice: first
counting commands against the frame table, which read as a target the
user was failing to fill, then counting down a window nobody had asked
about.

What somebody holding a Flipper against a cloner wants to know is whether
it is hearing anything, and how that changes as they move it. So the
listening screen now draws a trace - one bar per tick, tall where gaps
arrived and flat where none did, scrolling left. It shows activity and
never progress, and it is useful while positioning the coil rather than
only afterwards.

Its own view rather than a mode on the work view: that one's bar is
shared with Read, Restore, Wipe and Clear, where a total genuinely exists
and the bar is right.

A sample goes in every tick whatever happened, so a flat trace is a
measurement - a screen that only moved on activity could not be told from
one that had stopped updating. The scale puts a desk near the floor and a
sender at the ceiling: forty gaps in a tick fills it, against the five a
Flipper on a desk averages and the hundred-plus a cloner puts through one
mid-frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: make the write run findable in a capture full of idling

A sender idles either side of whatever it came to do. A blue cloner's
write run was twelve frames of forty-six, and a second press put the same
run in twice - so the list opened on junk, and its length read as a wall
of findings when most of it was the coil listening to a quiet room.

Two changes. The header now says how many of the frames decoded, so the
count that matters is on screen before any scrolling; it keeps the
dropped count, since the list silently stops at the table's end and a
short list is otherwise indistinguishable from a short capture.

And the Frames button opens on the frame the headline came from rather
than at the top. The verdict already knows which frame it drew the
password from - it just was not saying.

Nothing is hidden. The undecoded rows are the evidence that diagnoses a
sender this cannot frame, and their lengths are the whole of what is
known about them. This is about finding the run, not filtering the list.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: stop the sniff result's buttons running into each other

With a password found the screen carries three buttons, and the widget
centres the middle one and right-aligns the last - so their widths simply
have to fit. Measured off a device: "Again" 36 px, "Save" 37, "Frames"
44, which put the centre button's right edge at 82 and the right
button's left edge at 83. One pixel between them, and they read as one
shape.

"More" instead, which is what the sister app calls the same screen and
what the app's own read result does with its third button. The lines that
told the user where to look follow the button's name.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: drop the picture from the listening screen

It was the work view's, which shows a card being put ONTO a Flipper -
the opposite of what this screen asks for, where the Flipper goes
against somebody else's reader. A FAP cannot link the firmware's own
icons, so there was no better one to reach for, and drawing a cloner
would have replaced a wrong picture with a guess at one.

Removing it is the honest version and the better layout. The picture was
using sixty of the hundred and twenty-eight pixels, which had squeezed
the trace into a 62 px column and cut the status line to one word. The
trace now runs the full width and holds sixty ticks instead of
twenty-eight - about twelve seconds, so a write run is still on screen
after it finishes rather than scrolling away while the user looks up.

What the picture was failing to say is now said in words at the bottom.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: review fixes - say which read back failed, and fix the hazard's own wording

Five review agents over the parts of this branch that had never been looked
at. No crashes and no memory faults; what they found was the documentation
of the hazard being wrong about its own frames, and the screens being
silent about the one thing that cannot be taken back.

The rationale on t5577_restore_password() applied the 38-bit collision to
all eleven frames it governs. Ten of them are passworded WRITES at 70 bits,
which no other command is, so a chip that wants no password simply refuses
them. Only the read back - a passworded read, 38 bits, the same length as a
plain write - is the frame the flag exists to withhold. The derivation now
sits where it is true.

The same function claimed the caller gates it again. It does not: the
caller's test reduces to the same expression over the same dump. What
actually keeps the frame from going out first is the plain detect having
failed, which is evidence from the tag rather than from a file, and the
comment now says so. Three agents found this independently.

The restore's three "could not check" outcomes reached one sentence,
identical to what holding a Flipper over an empty desk produces. They are
now three, because they call for different things: a tag that wants a
password this dump does not hold, the password frame having gone out with
nothing answering it, and nothing answering at all. The detect also raises
its near miss - a chip that answered and then turned out not to be asking
means the frame was read as a write - instead of logging it as success.

Sniff's Save now asks before keeping a word the capture deduced from
neighbouring frames rather than read off a length. The headline already
said "if that was a read"; Save dropped the caveat and wrote it as fact
into a list that Clear Password later sends at tags.

"Nothing heard" claimed no readable gap while in-band gaps had been
counted. It reports both numbers now.

Tests: two read-back cases that a sixteen-mutation sweep left uncovered -
a dump with neither block 0 nor a password, which must not send a frame
carrying zero, and a protected dump whose block 7 was never read, which
must carry the word the chip was opened with rather than block 7's padding.
The host suite also only ever asserted t5577_sniff_commanded() false, so a
version returning false unconditionally passed all of it; both sides of
T5577_SNIFF_COMMAND_MIN are pinned now.

Also: the ring doubled to 2048 entries and three comments still said four
kilobytes; two comments still called the More button Frames; the app
struct called the feature Sniff Reader; the burst-versus-command note and
the why-not-a-bar note were each stated twice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: simplify after the review pass

Four cleanup agents. Nothing behavioural except where the review pass had
left two ways of saying one thing.

The dictionary handover wrote its failure path twice, verbatim including
the log line, nested two deep. It is a loop that can only run twice - the
user list goes to the shipped one once, and a second miss returns - so it
is a while now.

The restore result picked three sentences with an if/else-if over a
four-value enum, where this app's own convention is a switch with no
default so a value added later is a build failure rather than a wrong
sentence on screen. The enum's fourth value was never read: every path
that reaches the screen assigns one of the other three first, so Silent is
the zero and the initial state both.

The listen view took a level in pixels, which meant the scene owned both
halves of the scaling - how many gaps saturate a tick, which is a radio
measurement, and how tall a saturated tick is drawn, which is not. It now
takes the two numbers the scene measured and does the arithmetic once, and
the pixel count leaves the header.

The password-to-an-open-chip hazard was spelled out in full in three
places; scene_clear.c was pointed at the authoritative @warning during the
review and the other two were not. The read-back gate's branch selector
was a negated compound with the uninteresting case first, and said "block
0 goes back last" three times in one function.

Also: an unreachable saturation guard on the quiet counter, which the
capture's own timeout already bounds; a comment explaining a loop sixteen
lines below it; a kind name returning the same literal its own fallback
does; and four rationales that were each stated twice.

Two findings recorded and not taken. Spreading the restore's three detect
attempts across timer ticks would cut the worst-case input freeze from
5.76 s to 1.44 s and fits an existing 1-byte hole in the app struct, but
it changes the restore path and wants a tag to test against. Reordering
T5577SniffFrame saves 192 bytes of heap at the cost of separating fields
from the comments that explain them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* T5577: list, add and delete the user's own passwords

The app could already learn a password off the air and write it to the
user's dictionary, and it sweeps that dictionary in Clear Password. What
it could not do was show you what is in it.

Passwords, on the main menu, follows the NFC key dictionaries: List, Add
and Delete all act on the USER list only, and the shipped one is only
ever counted. That is not an omission - the shipped file is replaced
wholesale by the next firmware update, so editing it achieves nothing,
and its own header says as much.

Add checks BOTH lists even though it writes to one. The shipped list
holds a hundred and twenty-seven words a cloner is likely to be using, so
"already known" is a more useful answer than a silent duplicate - and it
is the common one. That check is the same helper the sniff result's Save
uses, moved into the app so a word typed in and a word recovered off the
air get the same four answers.

The user's file is still created in exactly one place, by a successful
add. Counting it, listing it and reading a word out of it all open it
OpenExisting, so looking at this screen does not bring a file into being
- which is what T5577_DICT_USER_PATH's documentation promises. NFC uses
OpenAlways throughout and would have created one; this is the one place
the parity is deliberately not followed.

Deleting the last word returns to the menu rather than to a list with
nothing in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: say "system dict" and "user dict", and lift the dialogs off the button

The Passwords screen now reads as the NFC key dictionaries do - same title
shape, same two count lines, same key beside them, Add and List in the
same places. It said "Shipped" and "Yours", which are the same two ideas
under names only this app used; somebody who has met the NFC screen should
not have to learn a second vocabulary for the same file pair.

The key is this app's own. A FAP cannot link the firmware's icons, so
I_Keychain_39x36 is not reachable and Key_39x36.png is drawn to sit in
the same box at the same place.

Separately, t5577_app_say() put its header at y=10 and its body at y=32.
A two line body then ran from 32 to 52 and pushed its descenders through
the OK button, with fourteen empty pixels sitting above it. The app's own
t5577_app_ask() has used 4 and 20 all along and looks right; say() was
the odd one out and now matches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbBdNcYKs8cAxYufLgVavW

* CHANGELOG: the T5577 sniffer, the user dictionary, and the restore fix

PR numbers go in once the branch is pushed. fap_version stays at 1.1 - the
app is the same app, with more in it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: look for the tag a tick at a time, so Back is answered

Every other step of the restore does one unit of work per tick; the detect was
the exception, spending three plain reads and then the password read back in a
single handler. That is around six seconds in which no input is read, so Back
queued unanswered and the presses arrived together at the end - which also left
the dump's own row in the file browser dead until the list was navigated away
from and back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: the screens say "cloner", since that is what is held against them

A door reader works too - the feature was proven against one - but a cloner is
what a user brings to these screens, and "reader" next to a Flipper reads as
the Flipper. Both lines keep the length they had, so neither can start wrapping.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: a plain read before the passworded one, a tick each

Reading with a password was the last flow in the app to send the 38 bit frame
on nothing but the user's word. A chip that asks for no password reads it as a
write of that word shifted up, its top bit into the addressed block's lock bit
and not coming back out - so the plain read goes first here as it already does
in Clear Password and in the restore's read back, and a tag that answers it
never needed the frame.

The two detects are separate ticks. Each is most of a second, and two in one
handler is what left Back unanswered in the restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* CHANGELOG: the unfreeze, and the plain read that now goes first everywhere

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: keep the host tests that were only ever in a scratch folder

21 cases over the sniffer's framing and decode, built on synthetic rings whose
answer is known before any of it meets a cloner, and the MU_TEST body for
t5577_restore_password run on the host. Both found real bugs while the feature
was being written and neither lived anywhere a second person could run them.

They are host tests rather than unit_tests cases because unit_tests cannot link
in this tree - a pre-existing DWARF error, nothing to do with this app.

application.fam has to say "!test": the default source glob is "*.c*" and it
recurses, so the suites would otherwise be compiled into the app, where their
host stubs cannot build and their main() collides with the entry point.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: warn about the frame that went out, not the word that was typed

Review of the previous commit. Before a plain read ran first, password_entered
implied the 38 bit frame had been sent, so the read result screen could say
"that frame wrote block 0" on password_entered && !with_pass. The plain read
broke that: it succeeds with with_pass clear and no frame sent at all, so the
one path added to AVOID the hazard became the path reporting it - and the
remedy it offered was to wipe a healthy tag. The fact is now recorded where
the frame is actually built.

Also from the review, all comment or reporting rather than behaviour:
- "That frame is 38 bits" sat above the PLAIN read, which is 6 bits and the
  safe one. It now names the passworded frame and gives both lengths.
- An attempt costs most of two seconds, not "most of a second": a detect is
  three reads of block 0, one per rate. Both places said the smaller figure.
- The restore's "no read back" line credited the tag with an answer it never
  gave - `asks` reads the dump, and by there nothing has been heard from the
  tag at all. It now says so, and logs how many plain reads were spent.
- The restore now says "Trying password" on the tick before the risky frame,
  as the read already did; it showed "Finding tag" throughout.
- CHANGELOG: the menu item is "Passwords", not the heading on the screen it
  opens; a password typed at the prompt is not kept, only one added by hand.
  And the Back entry counted a bug state that never shipped - the three
  attempt loop lands on this same branch, so what users have is two detects.
  The dead file browser row came out with it: that went in on observation and
  file_browser.c drops OK for a row outside the loaded window all by itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: cut the MU_TEST body out of the unit test instead of copying it

The copy committed with the harness was already two cases behind the original:
it predated f13ee509c, which added the `blind` and `kept` cases - and those are
exactly the two the README advertised as the gaps mutation testing had found.
So the suite claimed coverage it did not run, and the "keep it in step" note
had been violated before it was written.

run.py now slices the named MU_TEST out of lfrfid_protocols.c on every run and
writes it to a gitignored file the harness includes, so editing that test
cannot leave this suite behind. 164 lines now, against the copy's 144.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: say why the tests live on the host, including the reason that outlives the bug

The DWARF link error is the reason they were written here; it is not the reason
they belong here. unit_tests is its own app set in fbt_options.py, replacing
main_apps and system_apps, so a case there costs a second firmware and a flash -
which is the wrong price for arithmetic over a synthetic ring buffer. Worth
writing down, so that fixing the link error does not read as an invitation to
move them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: six seconds, not seven - the figure is four attempts of 1.47 s

Derived rather than estimated: a failed block 0 read is 450 ms settle plus a
30 ms silence break plus two frames, about 490 ms, and a failed detect pays
that at all three rates. Three plain attempts and the password read back come
to 5.88 s for a silent tag; seven is only reached when the capture window runs
its full 150 ms each time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: fold the step into the find, and stop saying things twice

Moving on to the blocks is what finding the tag means, so it belongs in
t5577_app_scene_read_found() rather than at both call sites - where one of the
two wrote a step the scene never reads again, the failure path leaving for the
result screen without looking at it.

The restore's password doc comment ended by saying a plain read goes first,
which the paragraph four lines above it now says: that sentence earned its
place when this function still contained the loop.

run.py: the suite names carried their own trailing spaces, stripped back off at
both failure sites and still not lining up with the extract line; the format
string does it now. HERE is gone from the restore include path - the only
quoted include is the generated body, which sits beside the file including it.
And the docstring restated the README's own paragraphs, so two files had to
agree about the same facts.

Left alone on the reviewers' own advice: the restore's three-level detect
branch, where every flatter form trades the nesting for a subtler obscurity;
open_try as a struct field, which is where this app keeps every per-run
counter; and password_entered beside password_sent, which have different
lifetimes as well as different meanings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* CHANGELOG: PR #1192

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* T5577: drop the host test harness from the repo

Not something this repo carries. The MU_TESTs for t5577_restore_password stay
where they belong, in applications/debug/unit_tests/tests/lfrfid.

application.fam goes back to the default source glob with them: "!test" was
only there to keep the harness out of the app build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-10-29 23:24:15 +02:00
Mykhailo ShevchukandClaude Opus 5 1672f7c54d LF RFID: full T5577 support - read a tag's blocks, save them, and write them back (#1179)
Adds the T5577 block read to lib/lfrfid, the .t5577 dump format, and a bundled
T5577 app: read a tag's blocks and save them, look them over, edit any block or
compose block 0 by name, write a dump back with every block read back again,
test which blocks a chip really takes, wipe a tag, and clear its password.

Wipe and Clear Password move here from RFID -> Extra Actions, since both
address blocks and are judged by reading one back. Their password list moves
out of internal flash to /ext/lfrfid/assets/t5577_dict.lfrfid, read with
keys_dict the same way the NFC key dictionaries are.

Also rebuilds the lfrfid "Write and set password" flow on verified blocks, and
bounds a verify read that could wait forever - the two together left a tag
protected with the screen still on "Writing with password".

Fixes #1180.

API 88.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MaBUeP1J6NwqCJjhbnTUND
2026-10-21 16:02:47 +03:00
Mykhailo ShevchukandClaude Opus 5 c1c54e4cad NFC: Texkom (Техком) key support (#1175)
* NFC: Fix two reboots on protocols the CLI and Add Manually did not expect
* NFC: Texkom (Техком) key support - read, emulate, save
* NFC: Texkom in Add Manually and the emulate CLI

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BR7WSui94mnbwav1bFZvvT
2026-10-10 13:02:56 +03:00
MX 5b0936292e Merge remote-tracking branch 'OFW/dev' into dev 2026-10-09 16:24:00 +03:00
MaxandŠtefan Croitoru ad3d55bacc NFC: Fix handling of FeliCa system key version (#4462)
* NFC: Fix handling of FeliCa system key version

* FeliCa: bump data format version to 4

---------

Co-authored-by: Štefan Croitoru <66339601+turbospok@users.noreply.github.com>
2026-10-09 16:09:00 +03:00
Vladimir KuznichenkovandŠtefan Croitoru 76bddb682f feat: add Indala 224-bit (long format) RFID protocol support (#4343)
* 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>
2026-10-07 17:00:22 +03:00
nikachuandClaude Opus 5 a43a527782 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-10-07 16:08:35 +03:00
Matthew CarrollandClaude Opus 5 507b59e10c 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 <noreply@anthropic.com>
2026-10-07 15:24:55 +03:00
MX 08afbae1b2 indala224 protocol enum order fix 2026-10-01 20:57:21 +03:00
matthewb-wortheandClaude Opus 5 9effa4b44f RFID: enter cards by facility code and card number, read HID formats and Casi-Rusco badges (#1149)
Add Manually now asks whether to enter a card as FC/ID or as hex. FC/ID takes the
numbers printed on the card, one number input per field, and lays them out in the
protocol data itself: EM4100, HID H10301, ioProx XSF, AWID and Pyramid 26-bit, and
Gallagher. Parity and checksums are added by the firmware encoders as for hex-entered
data. Formats whose scrambling and checksums are not exported by the protocol library
go straight to hex as before.

HID H10302, H10304, H10306, AMAG S10401 and Corporate 1000 35-bit are read on top of
the firmware's Generic HIDProx protocol, whose data is the raw 44-bit field. The read
and saved-card screens add a line for every format the frame fits by length and parity;
a 37-bit frame fits more than one, so the second and later readings are prefixed "or".
Layouts follow the Proxmark3 wiegand_formats.c reference except S10401, whose even
parity covers bits 1-17 rather than 18: of 42 badges exported from a live access
system, all 42 pass that and only the 13 with bit 18 clear pass Proxmark3's.

Casi-Rusco C10106 badges transmit a plain EM4100 frame at RF/32, so the app reads the
badge id out of those 40 bits - a 19-bit credential beginning with 15, then a 19-bit
card field, less 66606 when its top bit is set. The frame carries no parity, so the
line is shown as the alternative reading and the credential range keeps it off almost
all other EM4100 cards.

The Generic HIDProx render now gives the frame length alone. Its "Data:" line repeated
the hex every caller already prints, and read the frame one nibble at a time from
offsets that assumed a full first nibble, so a frame whose length is not a multiple of
four lost bits after its first digit. A unit test covers every size header.

A number input whose range excludes 0 opened on an empty field that could not be
confirmed, because 0 is drawn as empty and the starting value was only clamped when it
was not 0. It is now clamped unconditionally, so the field is only ever empty when 0 is
in range, and an empty field reads back as 0 and is range-checked like any other value.

Co-authored-by: matthewb-worthe <135917560+matthewb-worthe@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-21 09:13:42 +03:00
Mykhailo ShevchukandClaude Opus 5 49238e1bd3 RFID: write an EM4100 id onto ID8268 / Hitag S clone chips (#1148)
* RFID: write an EM4100 id onto ID8268 / Hitag S clone chips

Adds the ID82xx Hitag S "magic" clones (F8268, 8211 and relatives) as a
write target beside T5577, EM4305 and the Hitag micro variants, so a saved
EM4100 key can be cloned onto one from the app and the rfid CLI. The chips
ship streaming pages 4 and 5 as an EM4100 frame, so writing the 64-bit frame
into those two pages changes the id they emit.

Unlike every other write target this one cannot be blind: SELECT needs the
tag's own UID. The Flipper's LF envelope detector cannot resolve an
anticollision '1', so the reply is decoded from its '0' cells and the two
remaining unknowns - the leading bits and the frame alignment - are put back
to the tag as AC SEQUENCE prefixes, which only the tag whose UID starts that
way answers. A UID is used only once the tag itself has confirmed it, and a
negative control plus a re-verify guard against a chattering field.

That read doubles as a presence check no blind writer can make, so when no
such chip answers the write and its verify read are both skipped rather than
costing a 2 s verify per pass.

The target is opt-in: lfrfid_write_target_is_default() classifies every
target through a -Wswitch'd switch, so appending one fails the build until
someone decides whether it is safe to run unasked. Pages 4 and 5 hold
application data on a genuine Hitag S and nothing readable here tells a
genuine tag from a clone, so this must not fire while cloning a key with a
real Hitag S in range.

EM4100 at RF/64 only - the rate the factory TTF config emits; the faster
variants would need the config page rewritten too, so they are refused.

Ported from a standalone reader and writer validated over 575+ reads on
three sample tags; written to two tags on hardware.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* CHANGELOG: ID8268 / Hitag S EM4100 write target

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-14 17:49:36 +03:00
MX 946a52a640 merge some rfid related PRs 2026-09-13 23:59:18 +03:00
Mykhailo ShevchukandClaude Opus 5 944219d0ba Plugin loader: skip what it cannot load, and let a scan filter by file name (#1133)
* FlipperApplication: skip a plugin that fails to load instead of ending the scan

plugin_manager_load_all() broke out of the directory loop on the first file it
could not load, so a single stray or stale .fal silently cost every plugin
listed after it. It then returned PluginManagerErrorNone unconditionally, which
made every caller's error check dead code - including the one in the Sub-GHz
radio device registry, whose "Failed to load all libs" could never fire.

Skip the file and carry on, and return the first error the scan hit, so a
caller can tell a complete load from a partial one.

An app id mismatch is deliberately not one of those errors. A scan selects, and
a plugin belonging to another app is a normal thing to meet in a shared
directory - reporting it would hand the caller an error for a folder that is
working exactly as intended. That verdict belongs with the check itself rather
than at the call site, so the body of plugin_manager_load_single() moves into a
static helper that knows whether it is scanning: naming one file and getting a
mismatch is still an error, meeting one mid-scan is logged at debug.

Both examples checked the result and gave up, which was unreachable until now
and would have thrown away the plugins that did load - the opposite of the
point. They are the template a third-party plugin host gets copied from, so
they log and carry on instead, and the do/while(0) whose only break this was
goes with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* FlipperApplication: tell a failed directory read from the end of the directory

storage_dir_read() returns false both when it has handed back the last entry
and when the read itself failed - storage_ext_dir_read() sets FSE_NOT_EXIST for
the former and leaves FSE_NOT_READY, FSE_INTERNAL and friends for the latter,
so the only way to tell them apart is storage_file_get_error().

The scan loop treated the two the same, so a card pulled or a controller
hiccup partway through left a half-populated manager reporting complete
success. Check the error before the directory is closed, and report a read that
ended the scan early.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* FlipperApplication: let a plugin directory scan filter by file name prefix

A plugin's app id only becomes readable once its whole image has been mapped
and relocated, its init arrays run and its entry point called - see
flipper_application_plugin_get_descriptor(). So a scan of a directory shared
with plugins of another kind pays for a foreign image in full, and runs its
constructors against an API interface it was not built for, before it can tell
the plugin is not the one it wanted. And apps_data/<parent appid>/plugins is
fbt's default location for every plugin of an app, so sharing is the norm.

Add plugin_manager_load_all_prefixed(), which takes an optional file name
prefix and passes over anything that does not start with it. Same idea as
CliCommandExternalConfig::fal_prefix in the CLI registry, which has its own
loader for exactly this reason.

plugin_manager_load_all() keeps its signature and becomes the NULL-prefix case,
so nothing already built has to change. API 88.6 -> 88.7. Exporting the new
entry point rather than keeping it firmware-internal costs 8 bytes of API
table, and is what lets a plugin-hosting FAP fix the same hazard in its own
directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: pick the radio device plugins out of their folder by file name

subghz_device_registry_init() scans apps_data/subghz/plugins on every
subghz_devices_init(), which runs on every Sub-GHz app start and from the CLI
and JS module. Anything else that ends up in that folder - the natural place
for a Sub-GHz plugin, by fbt's own convention - was mapped, relocated and run
in full before the app id said it was not a radio driver. For the Frequency
Analyzer plugin that is 5,103 bytes of heap and ~7 KB of SD reads, every time.

Radio device plugins are all named radio_device_*.fal already, so require that
prefix and let the registry pass over the rest for the cost of a string
compare. Its "Failed to load all libs" check now means something, and says
which error it means.

The cost of keying on the file name is that a driver named anything else stops
being picked up, and a skip is only logged at debug level, which release builds
compile out. Since a missing driver otherwise shows up as nothing more than the
external module no longer being offered, warn when the scan ends with no driver
at all, and add the assertion to the Sub-GHz unit test suite - which already
inits the registry - that the one driver we ship is still found. The warning
cannot see one of several drivers going missing; the test is what covers that
if a second one is ever added.

A third-party radio driver under some other name is the one compatibility cost
here; radio_device_cc1101_ext.fal is the only driver in the tree and the only
naming an out-of-tree one could have copied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* SubGhz: put the Frequency Analyzer plugin back in the default plugin directory

PR #1131 deployed it to apps_data/subghz/plugins/features/ so the radio device
registry's non-recursive scan would never see it, and added an fal_path field
to the app manifest to allow that. With the registry filtering by file name and
the loader skipping what it cannot load, the folder is safe to share, so the
plugin goes back to apps_data/subghz/plugins/ and fal_path goes away with it.

Nothing else used fal_path, and it has not shipped in a release, so no SDK
consumer can be relying on it; every other plugin already deploys to the
default apps_data/<parent appid>/plugins. It was also a divergence in
scripts/fbt from OFW, which is rebase surface we do not need to carry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* CHANGELOG: plugin loader scan fix, API 88.7

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-08 13:19:37 +03:00
Maksims NerobaandMaksim Neroba 51a56fc401 NFC: reject oversized FeliCa Lite dumps (#1106)
Co-authored-by: Maksim Neroba <mneroba@MB14.local>
2026-08-26 04:00:00 +03:00
MX 8d1a8b35d5 Merge remote-tracking branch 'OFW/dev' into dev [ci skip] 2026-07-30 11:49:10 +03:00
6bf5e4f149 CLI: consistent quoted path handling for loader and Sub-GHz (#4430)
* CLI: consistent quoted path handling for loader and Sub-GHz

Fix issue #4248 where CLI commands disagreed on how paths with spaces
must be quoted:

- loader open: strip quotes from the application argument path so
  `loader open subghz "/ext/subghz/Some File.sub"` works (quotes were
  previously left in the path string passed to the app).
- subghz tx_from_file / decode_raw / keystore path args: use
  args_read_probably_quoted_string_and_trim (same as storage CLI).
- args_read_probably_quoted_string_and_trim: treat missing closing
  quote as failure (FURI_STRING_FAILURE), not a bogus zero index.
- Unit tests for quoted path extraction.

storage CLI already used the quoted helper; infrared/lfrfid already did
for file paths. This aligns loader + subghz with that convention.

Fixes #4248

* fbt format

---------

Co-authored-by: drkemp187 <193144151+drkemp187@users.noreply.github.com>
Co-authored-by: Stefan <66339601+turbospok@users.noreply.github.com>
2026-07-30 11:47:39 +03:00
MX b19b4f6cd6 Merge OFW PR 4383: NFC FeliCa: Add Standard card emulation sup [ci skip]
https://github.com/flipperdevices/flipperzero-firmware/pull/4383/changes
2026-07-27 23:19:24 +03:00
Mykhailo ShevchukandClaude Opus 4.8 45c3762c06 [NFC]: Added support for Mifare Plus in SL3 to NFC (#1032)
* [NFC] Add MIFARE Plus SL3 crypto primitives (mf_plus_crypto)

Foundation for native MIFARE Plus SL3 support: AES-128 ECB/CBC (firmware mbedtls), AES-CMAC + the MFP 8-byte truncation, AN10922 session-key derivation, MAC assembly, and the data-encryption IV builders.

IV layout follows Proxmark3 mfp_data_crypt(), verified against source: single low counter byte; read R_lo@[0/4/8]+TI, write W_lo@[7/11/15]+TI. The write-direction IV is not yet hardware-validated -- flagged in-header for confirmation before the write/emulation paths (PR6/7).

No callers yet; poller/listener land in later PRs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [NFC] Add RFC 4493 self-test for MIFARE Plus SL3 crypto

Known-answer vectors for mf_plus_crypto: the four RFC 4493 AES-CMAC cases (empty / single / partial-final / multi-block) plus the cmac8 odd-index truncation, and a regression test locking the PM3-matched data-IV byte layout (read R_lo@[0/4/8]+TI, write W_lo@[7/11/15]+TI, low-byte-only).

Added to the nfc unit-test suite; the four mf_plus_crypto_* symbols are exported via the test-only API table (unit_test_api_table, api 0.0) so the test plugin resolves them at runtime. Builds clean (firmware + test_nfc plugin); on-device run via the unit_tests CLI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [NFC] Extend MfPlusData with SL3 block/key model + versioned dump

PR2 of native MIFARE Plus SL3. MfPlusData now holds SL3 content: data blocks + a read bitmap, per-sector AES KeyA/KeyB (+ found masks), admin keys (CMK/CCK/L3-switch/SL1-auth), config blocks (0xB0xx), and the originality signature. Keys are stored separately from block data (SL3 keys are in the 0x40xx keyspace, not data memory, unlike Classic's sector-trailer layout).

Adds a versioned .nfc save/load mirroring mf_classic (dense 'hex or ??' per-block lines, a 'Data format version' key, backward-compatible with legacy metadata-only dumps). is_equal now diffs blocks+keys+config+signature so emulation writeback to .shd can be detected.

Review hardening: mf_plus_str_to_bytes parses into a local and commits on success (out untouched on failure), guards a truncated-line OOB read, and logs corrupt hex vs a legitimate '??'; load refuses an unknown newer data-format version; alloc zeroes via reset; compile-time asserts lock the mask-fits-domain invariants.

Builds clean (fbt firmware_all). Gated: code/silent-failure/type-design/comment review + simplify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [NFC] Add MIFARE Plus SL3 read poller (auth + read + signature)

PR3 of native MIFARE Plus SL3. Extends the previously detection-only mf_plus poller with the SL3 secure-messaging read flow, composing over the existing iso14443_4a poller.

Primitives (ported byte-faithfully from the flipperzero-mfp-reader fork onto the PR1 crypto): AuthFirst (0x70/0x72) with the rrot(RndA)/rol(RndB) token + session-key derivation, encrypted+MAC'd block read (0x31) with the pre/post-increment r_ctr counter model, and the plaintext originality signature (Read_Sig 0x3C).

State machine (mirroring mf_classic's mode/event API): after detection, RequestMode -> ReadSignature -> per-sector RequestKey -> AuthSector -> ReadSectorBlocks -> ReadSuccess, gated on SL3 + read mode. The app supplies candidate keys via RequestKey and tracks progress via DataUpdate; RequestMode defaults to Info so the existing detect/info scan is unchanged. The app-side dictionary driver lands in PR4.

Also lands the PR2-deferred accessor family + per-sector geometry and routes the serializer through the atomic setters.

Review hardening: a genuine wrong key (Auth) advances to the next candidate while a comms fault fails loudly instead of silently dropping a key; a response-MAC mismatch aborts the sector (counter desync) rather than cascading; the SL3 path is logged throughout.

Builds clean (fbt firmware_all). Gated: code + silent-failure review + simplify. On-device read of a real SL3 card is manual QA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* [NFC] Add MIFARE Plus SL3 dictionary attack

Recover SL3 sector AES keys by dictionary and read all sectors, exposed as
"Unlock with Dictionary" on the read and saved menus (gated on SL3; SL0/1/2
never reach this path).

- New scene nfc_scene_mf_plus_dict_attack: the SL3 poller has no
  resume-from-prior-data path, so the user and built-in system dictionaries are
  consumed in a SINGLE poller pass (user keys first) rather than as separate
  runs, which would discard already-recovered keys. Restarts the combined key
  stream per (sector, key type) since the poller has no NextSector event.
- System dictionary asset (mf_plus_dict.nfc): union of PM3
  g_mifare_plus_default_keys and client/dictionaries/mfp_default_keys.dic,
  deduplicated and upper-cased (33 AES-128 keys).
- Reuses the shared dict_attack view in its sector-oriented layout; sets the
  type explicitly on entry (the shared view keeps whatever type the last scene
  left it in).
- mf_plus_is_card_read(): a block-completeness check used to grade the outcome
  full vs semi success, instead of the poller's sector counter (which also
  counts sectors that aborted mid-read). A terminal ReadFailed (lost card /
  comms fault) is logged and always graded semi-success, never a clean read.
- Expose mf_plus_get_sector_count and mf_plus_is_card_read in the public SDK
  API (the NFC app is an external FAP and calls them), mirroring the mf_classic
  geometry/status helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Add MIFARE Plus user key management scenes

Add "MIFARE Plus Keys" to Extra Actions, mirroring the MIFARE Classic and
Ultralight C key managers, so users can grow the SL3 dictionary the "Unlock
with Dictionary" attack consults.

- Scenes: keys (Add/List menu with system+user key counts), keys_add (byte
  input, rejects duplicates), keys_list (pick a user key), keys_delete (confirm
  + delete), keys_warn_duplicate. Keys are 16-byte AES-128, stored in the
  lazily-created user dictionary (NFC_APP_MF_PLUS_DICT_USER_PATH).
- keys_delete hoists the load-key-at-index walk into a helper shared by
  on_enter and on_event and tolerates a stale index (falls back to the list)
  instead of asserting; keys_list uses a distinct inner loop index rather than
  shadowing the outer one.
- Route save/delete success back to the MIFARE Plus keys menu.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Recover MIFARE Plus SL3 admin keys and config blocks

Extend the SL3 read scan with an admin-key phase after the sectors: recover the
0x90xx admin keys by dictionary and, once the Card Master or Config key is
found, read the 0xB0xx configuration blocks with it.

- Generalize the auth primitive to a raw 16-bit key address
  (mf_plus_poller_authenticate_key_id); the sector variant now delegates to it.
  Admin key addresses: CMK 0x9000, CCK 0x9001, L3 switch 0x9003, SL1 auth 0x9004
  (the enum skips 0x9002, so a lookup table maps them). Auth is the same
  0x70/0x72 flow as sector keys, little-endian address.
- Generalize the encrypted read with a block-address high byte: data blocks
  pass 0x00, config blocks pass 0xB0 (both in the frame and the command MAC).
- New states RequestAdminKey -> AuthAdminKey -> ReadConfig. Config is read
  interleaved while the CMK/CCK session is fresh, and tracked by the per-block
  config mask so a CCK pass fills blocks a CMK pass could not read.
- The admin/config phase is best-effort (the sectors are already captured before
  it runs): a wrong candidate asks for the next, an absent admin slot (AUTH_FIRST
  rejection) is skipped, and a lost card finishes with the complete data instead
  of failing an otherwise-complete read.
- The dictionary-attack scene answers admin-key requests from the same
  dictionaries and restarts the key stream when the request target changes
  (sector key <-> admin key, or which admin key).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Render recovered MIFARE Plus SL3 content

The "More info" view showed only GetVersion data; now it also dumps the SL3
content recovered by the dictionary attack: originality signature, sector keys
A/B, admin keys (0x90xx), config blocks (0xB0xx) and the data blocks, each shown
as hex or "??" when not recovered. Section is gated on SL3 (the poller populates
this content only for SL3 read mode).

- Expose the read-only accessors (mf_plus_get_block_count, mf_plus_is_block_read,
  mf_plus_is_key_found, mf_plus_is_admin_key_found, mf_plus_is_config_block_read)
  and a mf_plus_get_admin_key_name getter in the public SDK API so the NFC app
  (an external FAP) can render them; the matching set_* accessors stay internal.
  Mirrors the mf_classic public accessor surface. Folded into the existing 87.12
  API bump for this feature (no further version bump).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Add MIFARE Plus SL3 native listener (auth + read emulation)

Emulate a recovered SL3 card so a reader can authenticate and read it. The
listener is the card-side mirror of the read poller, reusing the same
mf_plus_crypto primitives with reader/card roles reversed.

- mf_plus_listener composes over the ISO14443-4A listener (registered in
  nfc_listener_defs.c). run() dispatches received APDUs on the command byte.
- AUTH_FIRST (0x70): resolve the sector/admin key from the recovered MfPlusData,
  generate RndB, reply E(key, RndB). AUTH_CONTINUE (0x72): verify the RndB echo,
  derive the session keys from rrot(RndA)+RndB (same inputs as the poller), and
  echo rol(RndA) so both this port's poller and a PM3-style reader accept it.
- READ (0x31): verify the command MAC (pre-increment r_ctr), then reply with the
  encrypted block + response MAC using the post-increment counter/IV, byte-for-
  byte matching the poller's read. Serves data blocks and 0xB0xx config blocks.
- READ_SIG (0x3C): returns the stored originality signature. Unknown commands
  (GetVersion, WritePerso probe) are left unanswered so the reader falls back to
  ATS identification.
- Runtime get_features gate: SL3 -> EmulateFull, SL0/1/2 -> EmulateUid only, so
  non-SL3 cards are not offered full emulation. Emulate scene allocates the
  native MfPlus listener for SL3 (required for a future .shd writeback) and
  falls back to a UID-only ISO14443-4A listener otherwise.

Reader-write handling and .shd writeback come next; the write path is not yet
included here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MFP: sector read-progress helper + drop the empty signature dump line

- mf_plus_get_read_sectors_and_keys(): counts fully-read sectors (all blocks
  captured) and recovered sector keys, for the read-progress screens (mirrors
  mf_classic_get_read_sectors_and_keys). Public so the app FAP can use it.
- The originality signature is now written to the .nfc last and only when the
  card actually has one; a card with no signature (MFP X/EV0) no longer gets a
  misleading "Signature: ??" line, and its absence loads as "no signature".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MFP: align read-success and info screens with MIFARE Classic

The read-success and info screens now show name / UID / Keys Found / Sectors
Read (and Tech / ATQA / SAK on the full info), matching the MIFARE Classic
layout via nfc_render_iso14443_3a_info + the new sectors/keys helper. The
ISO14443-4 protocol detail (ATS, bit rates) moved from the info screen into
"More info", where the full SL3 dump lives.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MFP: auto-run the dictionary attack after reading an SL3 card

The read poller callback now reports the SL3 identity scan as "incomplete" and
the read scene auto-continues into the dictionary attack, so recovering keys no
longer requires the manual "Unlock with Dictionary" tap -- mirroring the MIFARE
Classic read flow. SL0/SL1/SL2 finish at the read-success screen as before.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MFP: group Keys under MIFARE Classic; shorten the save-name prefix

- Move "MIFARE Plus Keys" directly under "MIFARE Classic Keys" in Extra Actions.
- Abbreviate " Plus" -> "P" in the auto-generated save name, so MIFARE Plus
  dumps prefill as "MFP..." (and NTAG I2C Plus as "NTAG I2CP...") instead of the
  "MF+" the generic " Plus" -> "+" rule produced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: support plaintext (0x33) block reads

Some SL3 cards require plaintext block reads (command 0x33) instead of the
encrypted default (0x31), governed by the per-block access conditions. A card
like the MIFARE Plus S rejects 0x31 with status 0x0B ("command not available
at current card state") on every data block, so it authenticates all keys yet
reads zero sectors.

Poller: mf_plus_poller_read_block() gains a `plain` flag selecting the opcode;
in plain mode the data travels in the clear (no CBC-decrypt) and the response
MAC is verified over the transmitted plaintext. A new MfPlusError value,
Rejected, distinguishes a card status-byte refusal (a bare status frame that
leaves r_ctr in sync) from a comms/RF fault (Protocol).

The sector-read handler probes the mode in-line: on a Rejected while the mode
is still unlocked, it re-authenticates the sector (resetting the counter) and
retries the SAME block in the other mode, adopting that mode card-wide only on
a successful read. A transient comms fault never flips the mode, and a block
refused in both modes is skipped while the next block re-probes, so neither an
encrypted nor a plain-only card can be mislocked by an unreadable first block.

Listener: mf_plus_listener_read_handler() answers both 0x31 and 0x33 (plaintext
+ response MAC), so an emulated S-style card round-trips.

SL0/SL1/SL2 are unaffected; config (0xB0xx) reads stay encrypted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: answer WritePerso probe in the listener

A reader's info scan (PM3 `hf mfp info`) identifies a MIFARE Plus card by
sending the SL0-only WritePerso command (0xA8) to the invalid block 0x9090 and
reading the rejection status byte: a card in SL3 refuses it with 0x0B ("command
not available at the current card state"), which the reader reads as Plus + SL3.

Our SL3 listener left 0xA8 unanswered, so an emulated card returned "No card
response" to that probe and the reader printed "Identification failed" even
though UID/ATQA/SAK/ATS were all emulated correctly. Add a stateless handler
that answers WritePerso with 0x0B.

WritePerso is rejected by a real card regardless of block address or
authentication state, so the handler ignores the frame and needs no session; it
touches neither the listener state nor the crypto counters. GetVersion (0x60)
and Read_Sig (0x3C) are intentionally still left unanswered for an EV0/X/SE
card, which genuinely has neither -- matching real-card behaviour.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: NAK unsupported commands in the listener

A reader's info scan (PM3 `hf mfp info` / `hf 14a info`) sends GetVersion (0x60)
and Read_Sig (0x3C) while identifying a card. Our SL3 listener left those (and
any other unimplemented command) unanswered, so an emulated card returned an
empty frame and the reader printed "No card response" for each -- an EV0/S/X
part has neither command, but a real one still answers rather than going mute.

A real MIFARE Plus S 2K (EV0) confirms this: to GetVersion it replies with a
bare 0x0B status ("command not available at the current card state"):

    Rdr | 02 90 60 00 00 00 ..  | GET VERSION
    Tag | 02 0B ..              | INF = 0x0B

Mirror that: unimplemented commands, and Read_Sig on a card with no signature,
now NAK with 0x0B via a small mf_plus_listener_send_status() helper (shared with
the WritePerso handler). The reader's scan completes instead of timing out, and
reading the emulation back still falls through to ATS-based typing (GetVersion
0x0B -> version parse fails -> ATS; Read_Sig 0x0B -> signature absent).

GetVersion is intentionally not answered with a fabricated version: 0x0B is what
a real EV0 returns. Replaying a stored version for EV1/EV2 is a later item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: drop the manual "Unlock with Dictionary" menu item

The SL3 dictionary attack now runs automatically on read (the read scene enters
the dict-attack scene on PollerIncomplete), so the manual "Unlock with
Dictionary" entry on the read and saved menus was redundant. Remove it: delete
the submenu item, its enum, and its event handler, and point both menus at the
common empty scene handlers. The standard Save/Emulate/Info/etc. items are added
by the framework and are unaffected.

The dict-attack scene stays reachable through the automatic read flow; only the
manual entry point is gone. mf_plus_is_sl3 is kept (the runtime feature gate
still uses it).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: NAK failed authentication in the listener

When a reader (or key check like PM3 `hf mfp chk`) authenticates with a wrong
key, our SL3 listener answered AUTH_FIRST but then went silent on the
AUTH_CONTINUE whose RndB echo didn't match. The reader timed out and desynced
its whole scan -- a sector with a custom key poisoned the checks of later
factory-key sectors.

A real MIFARE Plus S 2K (EV0) confirms the correct behaviour: to a wrong-key
AUTH_CONTINUE it replies with a bare 0x06 status (authentication error):

    Rdr | 0B 00 72 D0 78 ..  | SECOND AUTH STEP (wrong key)
    Tag | 0B 00 06 84 E9     | INF = 0x06

Mirror that. Every auth failure -- malformed AUTH_FIRST/AUTH_CONTINUE, a key
the emulated (partial) dump never recovered, AUTH_CONTINUE without a prior
AUTH_FIRST, and the wrong-key RndB-echo mismatch -- now NAKs with 0x06 via the
shared status helper instead of staying silent. The reader records a miss and
advances to the next key. Our own poller already reads a non-0x90 AUTH_CONTINUE
status as MfPlusErrorAuth ("wrong key, next candidate"), so reading the
emulation back is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: add a "Show Keys" screen

Mirror the MIFARE Classic "Show Keys" viewer for MIFARE Plus SL3. A new scene
lists the recovered per-sector AES keys (A/B) and the four admin keys (Card
Master, Card Config, L3 Switch, SL1 Card Auth) in a scrollable widget, offered
via a "Show Keys" item on both the read and saved menus for SL3 cards.

Like the MFC viewer it lists only sectors with a recovered key and omits the
unknown ones to stay compact on a partially-read card; keys print as compact
uppercase hex (16-byte AES keys are too long for per-byte spacing). It adds an
explicit "No keys recovered yet." line for the empty case and a footer with the
A / B / admin found counts. The item is always present for SL3 so the view is
reachable even before any key is recovered.

All accessors and key fields are already public, so no API change is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: switch data IV to the fork's validated two-counter layout

The data-encryption IV builders followed PM3's single-low-byte layout, which is
only PM3-source-derived and never hardware-validated for the write direction.
Switch both to the reference fork's layout instead: a little-endian
{R_ctr, W_ctr} word {R_lo, R_hi, W_lo, W_hi} repeated across the IV, with TI at
IV[12..15] for reads and IV[0..3] for writes. This is the layout validated
end-to-end by a U-Prox encoder writing keys to the fork's emulation, so it is
the trustworthy basis for the encrypted write path (0xA1) coming next.

For a read-only session (W_ctr == 0, R_ctr < 256) the word collapses to
{R_lo, 0, 0, 0} and the read IV is byte-for-byte identical to the old PM3
layout, so the hardware-validated read path is unaffected -- both read-IV
callers (poller, listener) simply pass their session's W_ctr (0 until a write).

Unit-test IV vectors updated to the new layout, including two full-counter
cases that pin every R/W byte position; the test-only API-table signatures
follow the new 4-arg form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: emulate writes (0xA1/0xA3) for .shd writeback

Add the card-side write handler to the SL3 listener so a reader writing to the
emulated card mutates MfPlusData, which the generic emulate-exit diff then
auto-saves as a .shd. This completes the emulation-with-change path (e.g. a
U-Prox encoder re-keying the emulated card).

The handler is a native port of the reference fork's validated handle_write:
- Frame [cmd, BNr_lo, BNr_hi, data(16), cmd_mac(8)]. Command MAC over
  {BNr_lo, BNr_hi, data-as-transmitted} with the pre-increment W_ctr; response
  is 0x90 + MAC over the post-increment W_ctr (mirrors the read counter model).
- 0xA1 (keys) CBC-decrypts the block with the write IV; 0xA3 (data) carries
  plaintext. W_ctr advances only on an accepted write, so a NAK (bad MAC, short
  frame, unknown address, not authenticated) never desyncs the reader.
- store_write resolves the 2-byte address to a sector key (0x40xx), admin key
  (0x90xx), data block (high 0x00) or config block (0xB0xx) and calls the
  matching mf_plus_set_* mutator (field + found/read mask), so is_equal detects
  the change and a key write rotates the stored key for the re-auth round-trip.

Not yet hardware-validated: the encrypted-write IV rests on the fork's
U-Prox-validated layout (see the crypto commit), to be confirmed on-device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: poller write path (write-to-card)

Add the reader-side write to the SL3 poller, the card-side counterpart of the
already-validated listener write handler. This is the protocol core of "Write
to Initial Card"; the UI wiring follows in a separate commit.

- mf_plus_poller_write_block(): 0xA1 encrypted (CBC-encrypt the block with the
  write IV) / 0xA3 plain, mirroring mf_plus_poller_read_block. Command MAC over
  {block_low, block_high, data-as-transmitted} with the pre-increment W_ctr; a
  status rejection (0x0B) returns MfPlusErrorRejected without advancing W_ctr,
  an accepted write advances it once and verifies the response MAC over the
  post-increment value.
- Write state machine: RequestWriteSector (ask the app for the sector's
  recovered key) -> AuthWriteSector -> RequestWriteBlock (ask for the block
  data) -> WriteBlock. Writes only the sectors/blocks present in the dump; a
  sector whose key isn't in the dump is skipped.
- Write-mode probe: like the read probe, the first block is tried encrypted and,
  on a status rejection, the sector is re-authenticated and the block retried
  plain, adopting whichever mode succeeds card-wide. The user's note that write,
  like read, has both plain and encrypted forms is handled here.
- New MfPlusPollerModeWrite + RequestWriteSector/RequestWriteBlock/WriteSuccess/
  WriteFailed events; the ISO14443-4a fault path reports WriteFailed in write
  mode.

Not yet hardware-validated on the poller side (the write crypto itself is
HW-proven via the listener + U-Prox); confirm with a real-card write.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: "Write to Initial Card" UI

Wire the app UI for writing a saved SL3 dump back to its source card, using the
generic write-scene machinery (the same approach as MIFARE Classic).

- Advertise NfcProtocolFeatureWrite for SL3 only (runtime get_features gate), so
  SL0/1/2 -- which have no recovered blocks -- never offer it.
- The generic "Write" item is relabelled "Write to Initial Card" on the saved
  menu and removed from the read menu (writing right after a read makes no
  sense), mirroring MIFARE Classic. Both menus keep "Show Keys".
- The write scene starts the poller in write mode; its callback gates on a UID
  match (write only the card the dump came from), then feeds the recovered
  sector keys (preferring key A) and data blocks from the loaded dump. A
  sector/block the dump lacks is skipped by leaving key/block_provided false.

Completes the "Write to Initial Card" feature (poller path landed in the
previous commit). Not yet hardware-validated end-to-end; confirm on a real card.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: "Update from Initial Card"

Add the re-read-and-refresh flow (card -> saved dump + shadow), mirroring
MIFARE Classic. Offered on the saved menu for SL3; runs no dictionary attack --
it re-reads the source card using the dump's already-recovered keys.

- Three scenes (update_initial + success + wrong_card) mirroring the MIFARE
  Classic ones. The worker runs the read poller in read mode: UID-gate on
  RequestMode, then feed the recovered sector/admin keys from the dump.
- Loop guard: the read poller re-requests a (sector, key) after a failed auth to
  get the next dictionary candidate, but here there is only the one recovered
  key -- so a re-keyed card would loop. feed_once() feeds each key exactly once
  (scene state = last-fed-id + 1) and answers "no key" on an immediate repeat.
- Merge, don't replace: the poller reports ReadSuccess even if its best-effort
  admin/config/signature phase is cut short (early card lift), so a wholesale
  set_data could silently drop keys/config the dump already had. New public
  helper mf_plus_merge_update() overlays every field the re-read recovered onto
  a clone of the old dump without clearing anything, so the refresh only adds/
  updates. Exposed in api_symbols.csv (still 87.12) for the FAP.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: detect S/X/SE by ATS nibbles, not full match

A real MIFARE Plus SE presents ATS historical bytes C1 05 21 30 00 77 C1 --
the trailing bytes (caps + CRC16) differ from the AN10833 values (...F6 D1),
so the exact 7-byte match failed and the SE was mis-detected as a generic Plus.

Match the discriminating nibbles instead, the way PM3 does. The block is
C1 05 <type/size> <gen> <caps> <crc16>; only the type/size nibbles are stable:
- S / X mask their size (byte2/3 = 2F 2F); the caps byte's SVC bit separates
  them (set = X, clear = S).
- SE reveals its size (byte2 = 0x21 = 1K, byte3 = 0x30).
The C1 05 + high-nibble-2 gate keeps a DESFire (also SAK 0x20) from being
mis-claimed. The SAK 0x08/0x18 SL1 cases now use the same matcher, so SE 1K SL1
is recognized too while S/X classify exactly as before.

Removes the now-unused mf_plus_ats_t1_tk_values table (its SE entries held the
unreliable AN10833 tail); the mf_classic SL1 probe keeps its own local S/X pair.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [gui] elements_text_box: word-wrap instead of breaking mid-word

The multiline text box wrapped character by character, so a long line broke in
the middle of a word (e.g. an emulated "MIFARE Plus S 2K SL3" name split as
"...2K S" / "L3"). Track the last space on the current line and, on overflow,
break there so the whole trailing word moves to the next line; fall back to the
original character wrap when a single word is itself wider than the box.

A word containing an inline font marker (\e#, \e*, \e!) still character-wraps:
rewinding across an already-applied marker would re-toggle the emphasis and
corrupt the width measurement, so `marker_since_space` blocks the word-wrap
branch for those words.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: MFC-style block dump + ISO14443-4 "More" page

Rework the SL3 "More info" so the block layout reads like MIFARE Classic and the
protocol/hardware detail moves to a second page.

- The block dump is now per-sector: each block as hex, and the sector trailer
  shows the recovered Key A + the trailer block's access bits (its bytes 6-9) +
  Key B. SL3 keeps the AES keys in a separate keyspace, so the raw trailer's key
  bytes are meaningless -- substituting the recovered keys is what makes it read
  like a Classic dump. The separate "Sector Keys" section is dropped (its keys
  now live in the trailers). Hex is emitted in space-separated 2-byte groups so
  long lines wrap between groups.
- nfc_render_mf_plus_data is split into nfc_render_mf_plus_dump (blocks + admin
  keys + config + signature) and nfc_render_mf_plus_iso4 (GetVersion +
  ISO14443-4). The block-dump page gains a "More" button to a new
  MfPlusIso4Info scene holding the ISO14443-4 details that used to be inline.
- mf_plus_sector_get_first_block / _block_count are promoted to the public
  header (+ api_symbols.csv, still 87.12) so the app-side render can walk the
  per-sector geometry.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: answer AUTH_FIRST for unrecovered sectors

When emulating a partially-recovered dump, a reader authenticating a sector
whose key we never recovered was NAK'd at AUTH_FIRST (phase 1) with 0x06. A real
card always answers AUTH_FIRST -- it holds every key and only rejects a wrong
key at AUTH_CONTINUE -- so a phase-1 NAK is unlike any real card, and some
readers (a U-Prox encoder) abort the whole exchange when they see it, leaving
the auth "unfinished".

Verified on hardware: to AUTH_FIRST the real card answers 90 02 BE AA.. (0x90 +
E(RndB)); our emulation answered 06 58 B3.

Now an unrecovered key runs the handshake with a random placeholder: phase 1
completes normally, and since the placeholder can't match the reader's key, the
reader's phase-2 token fails to echo our RndB and we NAK 0x06 at AUTH_CONTINUE
-- exactly as a real card rejects a wrong key. No false-successful auth (the
reader would have to guess our per-session random 16 bytes, and even then the
sector's blocks were never recovered so nothing is exposed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: restructure More-info into a hub + fix dump OOM

The Info screen's "More" button now opens a small hub (View Dump /
ISO14443-4 Data), DESFire-style, instead of the block dump directly:

- View Dump (SL3 only): the Classic-style block dump, now rendered in a
  memory-light text_box (stores the pointer, lays out visible lines
  lazily) rather than a text-scroll widget. The widget deep-copies the
  whole string AND allocates a FuriString per wrapped line, which for a
  multi-KB 2K/4K dump exploded into hundreds of allocations and OOM'd on
  the ~30 KB-free NFC thread. Also furi_string_reserve the dump up front
  so the many cat calls don't grow by doubling (that transient ~2x
  realloc was the intermittent "opened on the 3rd try" OOM).
- ISO14443-4 Data: the ISO14443-4 protocol details as a widget with a
  "More" button to a new GetVersion page.

Dump layout: block number on its own line with hex below, so a 16-byte
block wraps into two even 2-byte-group lines instead of colliding with
the "N:" prefix; dropped the leading blank line.

Splits nfc_render_mf_plus_iso4 into nfc_render_mf_plus_iso14443_4 +
nfc_render_mf_plus_version_info for the two new pages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: add 18 "Add Manually" variants

Add SE / S 2K / S 4K / X 2K / X 4K / EV1 2K+4K / EV2 2K+4K generators,
each in a 4-byte and 7-byte UID flavour, to the core nfc_data_generator
(they auto-appear in the app's "Add Manually" list, which iterates the
generator enum). All are built at SL3 -- the native ISO14443-4
presentation this app fully supports -- with a random NXP UID,
product-correct ATQA/SAK, and a well-formed ATS.

Defaults follow real/known values where available and sensible
placeholders otherwise:
- ATS historical bytes: SE uses a real capture (C1 05 21 30 00 77 C1);
  S/X use AN10833 (2F 2F, SVC bit distinguishes them) -- validated
  byte-identical against real S/X cards' ATS. EV1/EV2 identify via
  GetVersion so they reuse the generic Plus block. S/X share their ATS
  across 2K/4K (the size is masked there; ATQA carries it).
- EV1/EV2 get a plausible GetVersion response (hw_major 0x11/0x22,
  hw_storage 0x16/0x18) and a random 56-byte originality signature; the
  EV0 products (SE/S/X) have neither, matching real silicon.
- SL3 sector/admin keys and blocks are left empty -- a synthetic card
  has no recovered keys.

Generated cards round-trip through save/load and re-detect as the
intended product (type/size/SL are stored explicitly).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Add Manually: keep the list cursor on the picked type

Opening a card type from "Add Manually" and pressing Back re-entered the
type list at the top instead of on the row just opened. The set_type
scene rebuilds its submenu on every entry (including on Back) but never
restored the selection. Save the picked row in the scene state (stored
+1 so the default 0 means "nothing picked yet" -> top) and restore it in
on_enter. Applies to every protocol's Add Manually list, not just
MIFARE Plus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] iso14443-4: don't crash on an I-block with no INF field

Decoding a received I-block copied its INF field with
bit_buffer_copy_right(input, prologue_len), which furi_check-crashes
(bit_buffer.c: size must be > start_index) when the I-block carries no
INF at all -- total size <= its prologue length. A bare PCB-only
I-block, or a CID-flagged empty I-block on a CID-supporting card, trips
it. Every other malformed case in this decoder already returns Skip;
this one asserted and took the whole device down.

Guard it the same way: skip an I-block whose size is <= its prologue
length instead of asserting. Also catches a truncated frame whose PCB
claims a CID/NAD byte it is too short to contain.

Found on hardware emulating a MIFARE Plus SL3 card to an NXP TagInfo
reader: TagInfo desyncs into an R(NAK) retransmission storm and emits
empty I-blocks, crashing within seconds. Fix is in the shared 14443-4
transport, so it hardens all 14443-4 emulation (DESFire, Type-4, ...),
not just MIFARE Plus. HW-confirmed: no more crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: recognize the second SE ATS fingerprint

NXP TagInfo (v6.2.0, reverse-engineered) matches TWO Plus SE ATS
historical-byte fingerprints -- C1 05 21 30 00 77 C1 and
C1 05 20 30 00 AB 9B -- differing only in the size/gen nibble (byte2
0x21 vs 0x20). We only matched 0x21, so a real SE carrying the 0x20
fingerprint fell through to Unknown. Match SE on byte3 == 0x30 with
byte2 in {0x20, 0x21}; S/X keep byte3 == 0x2F, so no overlap and no
regression. Real S/X ATS are already byte-identical to TagInfo's table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus: emulate GetVersion for EV1/EV2

An emulated EV1/EV2 now answers GetVersion so a reader (NXP TagInfo,
PM3) classifies it correctly instead of falling back to a generic Plus.
GetVersion is an application-level command (no transport support), so
the listener crafts the chained response itself.

It replays the stored 28-byte MfPlusVersion as three frames, matching a
real EV1 capture byte-for-byte:
  0x60 -> 0xAF + 7 HW bytes
  0xAF -> 0xAF + 7 SW bytes
  0xAF -> 0x90 + 14 UID/batch/date bytes   (final status 0x90, NOT
                                            DESFire's 0x00)
Both the native form and the ISO7816-wrapped 0x90-CLA form (data first,
0x91 <status> trailer) are handled, with a small get_version_stage
chain-state on the listener (reset on field-off/halt and when any other
command interrupts the chain).

Gated on a real version being present (EV1/EV2: hw_vendor 0x04, family
nibble 0x02); EV0 parts (S/X/SE) have none and keep NAKing 0x0B, which
is faithful and is TagInfo's EV gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus Add Manually: blank-format memory + real EV1 version

Manually-added MIFARE Plus cards were identity-only: every block and key
read as "??", which is useless to emulate or inspect. Fill a
blank-formatted factory state, the MIFARE-Plus analogue of the Classic
generator's 0x00 blocks + default keys:
- data blocks zeroed and marked read; block 0 rebuilt as the manufacturer
  block (UID + BCC on 4-byte UIDs + SAK + ATQA + 0xFF), since
  mf_plus_set_uid doesn't populate block 0
- every sector and admin AES key set to the all-FF default and marked
  found; config blocks zeroed

Also align the EV1/EV2 generator version bytes to a real EV1 GetVersion
capture (hw_proto 0x04; the software version reports its own major/minor
0x01/0x01, independent of the hardware major), so an emulated Add-Manually
EV card's GetVersion matches real silicon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] docs: MIFARE Plus detection reference (NXP TagInfo RE)

Static-analysis notes on how NXP TagInfo classifies a MIFARE Plus card
(variant / security level / size) via SAK routing, ATS fingerprinting,
GetVersion, Read-Signature and the VCA applet -- the reference our
detection and emulation are aligned to.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: changelog entry + API 87.12

Add the "Native MIFARE Plus support in SL3" changelog line and bump the
listed Current API from 87.11 to 87.12 (the single API bump this feature
introduces).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Show full raw ATS on the ISO14443-4 info screen

Add the complete ATS frame (TL T0 [TA1] [TB1] [TC1] + historical bytes) as a
raw line at the top of the shared ISO14443-4a extra renderer, before the
decoded protocol info, so every 14443-4 protocol (MIFARE Plus, DESFire,
NTAG4xx, Type-4, plain 14443-4a) exposes the exact ATS. The dedicated
[Historical bytes] section is kept, mirroring PM3's separate ATS and
Historical bytes blocks.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] Cache recovered MIFARE Plus SL3 keys per UID

Mirror the MIFARE Classic key cache for SL3: on save, persist the recovered
sector AES keys (A/B) and 0x90xx admin keys to /ext/nfc/.cache/{UID}.mfpkeys.
On a later read, feed the cached key for each (sector, key type) / admin
target before the dictionaries, so a known card authenticates on the first
try instead of re-running the whole dictionary attack. A wrong cached key
(re-keyed card) falls through to the dictionaries, and a cache miss runs the
attack exactly as before.

A distinct .mfpkeys extension and file header keep it from colliding with the
MIFARE Classic {UID}.keys cache (a Plus card read in SL1 is MIFARE Classic).
The cache helper is bundled into the nfc_mf_plus plugin sources (its own link
unit), mirroring nfc_mf_classic. SL0/SL1/SL2 are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [NFC] MIFARE Plus SL3: note per-UID key cache in changelog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* [GUI] Fix elements_text_box line[] overflow on 8+ line boxes

elements_text_box bounded line_num only by the height break, which on a
near-full-height box (>=63px tall with 9px leading) commits a 7th line and then
writes line[ELEMENTS_MAX_LINES_NUM] -- index 7 of a 7-element array -- a stack
out-of-bounds write. The word-wrap change (less dense than char wrap) lets text
that previously fit in <=7 lines reach 8, exposing the latent gap. Add the
missing line_num >= ELEMENTS_MAX_LINES_NUM bound so line[] is never indexed past
its last slot. Hardens the shared primitive for every firmware text box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:01:10 +03:00
MX 0d123bdfa5 Merge remote-tracking branch 'OFW/dev' into dev 2026-07-01 01:50:39 +03:00
MMX 97e088ec29 SubGHz: Add Elplast protocol (#4309)
* add protocol

* add unit test
2026-06-30 20:38:54 +03:00
Benjamin TamasiandClaude Opus 4.8 f222b00ac6 SubGhz: add Telcoma/Cardin EDGE protocol
Static (fixed-code) 433.92 MHz OOK/AM Manchester remote from the Telcoma /
Cardin EDGE family (EDGE2 / EDGE4 / EDGE20; sold compatible with TANGO /
QUASAR / SLIM / SE). Frame = 0xFF preamble + per-remote serial + a one-hot
channel marker; decoded to a canonical 32-bit key where key[31:24]=0xFF,
key[23:3]=serial, key[2:0]=one-hot channel (gate=0, others 1/2/4). The
channel-aware encoder reproduces each channel's exact waveform (gate channel
verified against the original remote's RF; gate frame is 32 data bits + a
required HIGH stop half-bit, other channels are 33 bits with no stop).

This is the EDGE 433.92 fixed-code protocol; it is NOT the existing Cardin
S449 (FSK KeeLoq rolling) or S466 (27 MHz PWM), which are different products.

- decoder + encoder: lib/subghz/protocols/telcoma_edge.{c,h}
- registered in protocol_items.{c,h}
- unit tests + sample captures covering the 32-bit (gate) and 33-bit
  (channel) decode paths
- listed in documentation/SubGHzSupportedSystems.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 14:11:25 +02:00
MX aa75974395 feat: add Indala 224-bit (long format) RFID protocol support
OFW PR 4343 by kuzaxak
2026-03-07 23:53:22 +03:00
MX 50b5ee103c bipki removal procedure
don't worry, they are in other app
2026-01-12 08:13:05 +03:00
MX 22ee3bdae2 Merge remote-tracking branch 'OFW/dev' into dev 2025-11-06 20:29:16 +03:00
6a5ae6cc0d Infrared Universal remote DBs unit test & move infrared_signal / infrared_brute_force into lib (#4284)
* make infrared db unit tests

* fix the tests, no assets are in asset folder oh no

* fix formate

* ship ir app along with unit_test pkg

* libify ir signal and bruteforce parts

* small cleanup

* api: removed infrared methods (you can link with the lib if needed), unit_tests, infrared: adjusted to link with ir lib; api: added `__aeabi_f2d`

---------

Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: hedger <hedger@nanode.su>
2025-11-06 20:23:59 +04:00
MX 40f7c007d9 Merge remote-tracking branch 'OFW/dev' into dev 2025-10-01 19:37:50 +03:00
85b6b2b896 NFC FeliCa: Service Directory Traverse + Dump All Unencrypted-Readable Services' Blocks (#4254)
* SimpleArray attached to FelicaData

* tx rx done. response parsing done (in log)

* dynamic vector as buffer. rendering begin

* On screen render for directory tree

* flags in render to indicate is_public_readable

* beautify render flags

* format

* offload dynamic vector into individual files

* saving. exposed dir tree writing for double use

* save: additional formatting

* save: clean up and some additional notes

* load done

* delete unnecessary debug log

* Load: safer way to handle backward compatibility

`parsed` being true is only contingent on whether the header (device type, UID, etc) are correctly read. The detailed data can be absent if saved from previous versions.

Side effects:
1. The data format version number must not increment.
2. Newer sections of dumps must be appended in the end of the file.

* format

* handle block reading according to IC type

Old version was aimed for FeliCa Lite dumping, which doesn't apply to FeliCa standard. Thus they need to be diverged in the poller run workflow.

* read block content works. rendering begin

* Render Refactor: dir & dump view from submenu

* Render: show IC type name

* IC parsing function cleanup

* Revert "IC parsing function cleanup"

This reverts commit ee3f7bf125b54b10d238b0aeb657ba15f27f93ba.

* Load: Standard dump. Fully backward compatible

* format

* sync API version

* format saved file

* delete unused variable

* clean ups

* IC type addition

* correction

* beautify attribute parsing

* correction

* Lite save: delete extra line

* correction: FeliCa link in Lite-S mode

* format

* Save: simplify printing

* update IC type parsing

* conform to api standard: const resp ptr to ptr

also slightly faster and more readable block dump loop

* disambiguate workflow type vs ic type

It was too confusing to have the ic name string telling you one thing and ic_type enum saying the other. Might as well use better naming to indicate the use case for the two things

* beautify on device render

* reject dynamic_vector, embrace m-array

* lint

* use full variable name

* partial fix: poller context's data proper init

* edit unit test dump IC code

and a small bug fix for the Lite auth workflow

* unit test felica dump PMm correction

* Fixes for static analysis warnings

---------

Co-authored-by: hedger <hedger@nanode.su>
Co-authored-by: hedger <hedger@users.noreply.github.com>
2025-10-01 18:54:08 +04:00
MX 05c64d2b1f Merge remote-tracking branch 'OFW/dev' into dev 2025-10-01 17:20:46 +03:00
fad487df0e SubGHz: Added 9 new protocols, fixes to existing protocols (#4255)
* Fix Typos

* Tune decoders

* Better parsing, show more data in existing protocols

* Add new protocols

* Update keeloqs

* Add unit tests & raws

* Add honeywell unittest

* Comment until better solution is found

Adding GAPs to be sent first to make signal better suitable for decoder (decoding from only one signal sample) does nothing, needs something else
TODO: Fix encoders?

* suppressed missing issue warning

* subghz: re-enabled failing encoder tests

* Fix two?

3 left

* properly do gangqi and marantec for unit test and real use

* fix unit tests now

* fix possible memory leak

* reset decoder step too

* subghz: extra encoder safety; report random signal test results on failure

* unit_tests: subghz: renamed test file for consistency

* subghz: more explicit buffer position resets

* Fix gangqi samples

---------

Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: hedger <hedger@nanode.su>
2025-10-01 18:05:50 +04:00
MX f4529dc8c1 Merge remote-tracking branch 'OFW/dev' into dev 2025-09-29 23:11:36 +03:00
1e0f3a606f cli: Buzzer command (#4006)
* Add args_read_float_and_trim function

* Add args_read_duration function

* Add notes_frequency_from_name function

* Add cli_sleep function and sleep CLI command

* Update CLI top command to use cli_sleep

* Add buzzer CLI command

* toolbox: make args_read_duration less convoluted

* notification: make notification_messages_notes_frequency_from_name less convoluted

* unit_tests: better float checking

* fix formatting and f18

---------

Co-authored-by: Anna Antonenko <portasynthinca3@gmail.com>
Co-authored-by: hedger <hedger@nanode.su>
2025-09-29 21:53:10 +04:00
e7634d7563 NFC: Ultralight C App Key Management, Dictionary Attack (#4271)
* Upstream Ultralight C dictionary attack (squashed)

* linter: formatting

* unit_tests: nfc: split nfc data to named var

* Fix mf_ultralight_poller_sync_read_card

* linter: suppressed warnings on TODOs

---------

Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: hedger <hedger@nanode.su>
2025-09-29 21:05:06 +04:00
MX 93b4ad4595 Merge remote-tracking branch 'OFW/dev' into dev 2025-09-24 22:32:16 +03:00
hedger 0d5beedb01 BLE: improved pairing security (#4240)
* ble: use unique root security keys for new pairings after pairing reset; added migrations for existing pairing data; unit_tests: added migration tests

* bt: lower logging level

* hal: bt: updated doxygen strings

* hal: ble: Added checks for root_keys ptr

* service: ble: bt_keys_storage minor cleanup
2025-09-24 22:36:45 +04:00
MX 905b5726ad Merge remote-tracking branch 'OFW/dev' into dev 2025-09-24 17:58:32 +03:00
MMXandhedger 7554b32538 SubGHz: Linear and Dickert MAHS protocols fixes & improvements (#4267)
* Linear and dickert mahs fixes

* fix dip pattern

* proper fix

by WillyJL

* Fix unit tests

Linear Decoder now decodes more linear signals

---------

Co-authored-by: hedger <hedger@users.noreply.github.com>
2025-09-24 14:45:03 +04:00