* 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
* 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>
* Expose some useful BLE/USB enums/macros
* Allow max possible length of BLE name
* BadUSB: Full customization of USB/BLE parameters
* BadUSB: Allow pressing modifier with no key combo
* BadUSB: Show current line numbers, simplify draw code
* BadUSB: Show script elapsed/run timer
* Some cleanup
* BadUSB: Add de-DE-mac and fi-FI keyboard layouts
* Update docs for BLE_ID and BT_ID
* Use __builtin_bswap16()
* Update naming
* Mark as Random Static Address
* Validate before copying to user config
* Show Done popup for randomize options
* Options to restore USB and BLE defaults
* Naming
* Focus top of config menu when entering
* Simpler HID profile name generation
* Use a BLE profile wrapper for just setting parameters
---------
Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com>
Co-authored-by: hedger <hedger@users.noreply.github.com>
* NFC: Implement Type 4 Tag reading
* Fix reading empty NDEF message
* NFC: Implement Type 4 Tag saving
* Fix reading NLEN header in NDEF message
* NFC: Implement Type 4 Tag NDEF parsing
* Sort and add missing protocol mentions
* Fix event struct passed to callbacks
* Add more specific error types
* Cleanup includes
* NFC: Expose iso14443_3a_listener functions (OFW issue 4059)
* BitBuffer: Allow copy right/left on same instance
* NFC: Rudimentary iso14443_4a_listener_send_block() (no PCB handling) (OFW issue 4059)
* NFC: Implement Type 4 Tag rudimentary emulation
* Support larger NDEF messages
* Fix sending error responses
* NFC: Initial ISO14443-4 PCB listener handling
* Fix chunk sizes
* Fix ISO READ BINARY params definitions
* Fix emulate after save
* Allow selecting PICC level too
* Refactor CC parsing and dumping
* Use flags for ISO14443-4 layer decode result
* For now send NACK to all R blocks
* NFC: Handle writes to Type 4 Tag while emulating
* Refactor ISO APDUs in T4T poller
* Keep prev selection when new selection fails
* NFC: Implement Type 4 Tag writing biolerplate and structure
* Improve ISO SELECT naming and handling
* Make APDU arrays static
* Better handling of read/write failure
* NFC: Implement Type 4 Tag writing to initialized tags
* NFC: Detect Type 4 Tag platform (eg DESFire)
* Fix writing with platform detect
* NFC: Support creating NDEF AID on DESFire
* Rename mf_desfire_send_chunks() to mf_desfire_poller_send_chunks()
* NFC: Implement DESFire ISO 7816 command wrapping
* NFC: T4T interact with DESFire without state reset
* Fix some message strings
* Static define T4T DESFire app ids
* Simplify return here
* Fix some edge cases with max NDEF size
* NFC: Support creating CC and NDEF files on DESFire
* GUI: Add submenu_remove_item()
* NFC: Refactor Write actions into ProtocolSupport
* NFC: Pressing back on MFC Update doesn't retry
* NFC: Fix duplicated ProtocolSupport handling, fix some inconsistent naming
* Fix writing retrying after success
* NFC: Implement basic NTAG4xx detection
* Small cleanup
* Check HWMajorVersion like NXP commands \o/
See: https://www.nxp.com/docs/en/application-note/AN10833.pdf
* NFC: Detect NTAG4xx in Type 4 Tag poller
* NFC: Refactor common NXP Native command handling
* NFC: Show version info for MIFARE Plus (was implemented but not shown)
* NFC: Only show MFP GetVersion info when available
* NFC: Fix detecting MIFARE Plus SE 1K SL1/3
See: https://www.nxp.com/docs/en/application-note/AN10833.pdf
* Expose dESFire NDEF size
* Stop poller and blinking on write success/fail
* GUI: Add submenu_remove_item()
* NFC: Refactor write scenes into protocol support
* NFC: Cleanup duplicate event handling
* NFC: Refactor DESFire poller to NxpNativeCommand helper
* NFC: Sort protocol defs and add missing ones
* NFC: Support NTAG4xx detection and basic info
* NFC: Allow choosing DESFire poller cmd mode, fix send_chunks() naming
* NFC: Check MIFARE types with HWType lower nibble
See: https://www.nxp.com/docs/en/application-note/AN10833.pdf
* NFC: Refactor MIFARE Plus with NxpNativeCommand helper
* NFC: Fix detecting MIFARE Plus SE 1K SL1/3
See: https://www.nxp.com/docs/en/application-note/AN10833.pdf
* NFC: Show MIFARE Plus EV1/2 GetVersion info like DESFire
* Revert "BitBuffer: Allow copy right/left on same instance"
This reverts commit cfeddbf8b6320a21749238d648eff7e69450d850.
* Use dedicated buffer
* Simpler naming scheme
* Update TODOs that are out of scope for the project
* Fix typos
* Fix merge
* NFC: Handle PPS request in ISO14443-4 layer
* NFC: Fix some typos in Type4Tag protocol
* NFC: Fix CLI with NTAG4xx and Type 4 Tag support
* Keep mf_desfire_send_chunks symbol, regenerate API tables
* Reword TODO comments as plain notes
---------
Co-authored-by: Willy-JL <49810075+Willy-JL@users.noreply.github.com>
Co-authored-by: hedger <hedger@users.noreply.github.com>
Co-authored-by: Štefan Croitoru <66339601+turbospok@users.noreply.github.com>
* 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>
* 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>
`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>
DESFire Light takes only ISO 7816-wrapped commands. A plain native frame gets
SW 6700 ("wrong length"), so every command the DESFire poller sent was refused,
detection died at GetVersion and nothing in the ISO14443-4A family claimed the
card. mf_desfire_poller_read_version_any_mode() now settles on the mode the card
answers and keeps it for the session; only a refusal of the frame itself earns a
second attempt, so the EV series keeps its plain frames and MIFARE Plus / NTAG
4xx still cost one exchange. The wrapped path and its 0xAF chaining already
existed in nxp_native_command.c for type_4_tag and ntag4xx.
Light also reports its own hardware type (0x08), which mf_desfire_version_parse
rejected, and its hw_major 0x30 is outside the EV1/EV2/EV3 series, so the type is
classified from hw_type first. It reads as "Mifare DESFire Light".
Light answers 0x1C to GetFreeMemory, GetKeySettings and GetKeyVersion, so the
read has to survive those refusals. Unmodelled status codes became
MfDesfireErrorRejected (mirroring MfPlusErrorRejected) instead of sharing
MfDesfireErrorProtocol with a broken exchange, and the two handlers that each
tolerated one specific refusal now tolerate any refusal via
mf_desfire_error_is_refusal(), which also covers an EV card with a locked PICC.
A transport failure still fails the read.
Detection no longer sends GetKeyVersion(0) ahead of GetVersion: the 28-byte
layout plus the hardware type nibble already separate the families, and the probe
only answered whether key 0 was readable.
Also fixed: the key-version refusal path zeroes max_keys with the array it
empties (save writes one line per element, load reads max_keys of them, so a
stale count made the file the app had just written unloadable) and honours
is_free_directory_list; mf_desfire_reset clears master_key_settings; read_success
fills the event error field; refusals log at FURI_LOG_W, which release builds keep;
the device name appends the size only when known.
mf_desfire_get_type_from_version is exported for the NFC app, as ntag4xx, slix and
mf_ultralight already do. API 88.16.
Hardware-verified on a 7-byte-UID MF2DL(H)x0: nfc scanner reports "Mifare DESFire"
where it reported "Iso14443-4a", and nfc dump -p des saves a complete file whose
PICC Version matches the Proxmark3 reading byte for byte.
Reading the Light's file contents is not part of this.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* LF RFID: say what the write-target masking actually fixes
The comment landed in #1164 credited the change with stopping the
settings page writing unknown bits back into the file. It cannot:
lfrfid_settings_set_write_targets() has masked its input since the
feature landed in #1146 and is the only writer of that file, so those
bits were always dropped on save. The claim came from the issue and I
carried it through review without checking it.
What the mask on read is really for: in tree, lfrfid_scene_write.c
stashes the raw value as scene state and tests it against 0, so a
newer-only bit made "everything switched off" report "No enabled chip
can write this protocol" instead of pointing at Settings. Out of tree,
the getter is exported, and an app cannot re-mask against an enum it
does not have.
Also moves the obligation next to the rule that creates it, at the top
of the file where the next target gets appended, and drops a clause that
restated the log line below it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LF RFID: port the last two iButton settings guards
#1164 aligned the masking but left the rest of the divergence in place.
_Static_assert: lfrfid_settings_set_write_targets() builds the whole
LFRFIDSettings from a designated initialiser, so a second setting added
later would be zeroed on every save with no error and no symptom until a
user noticed a choice that would not stick. ibutton_settings.c has
guarded this since #1153; this is the same assert with the same message.
The unchecked storage_simply_mkdir() return gets the twin's second
sentence, so the next reader does not have to re-derive that the save
below reports the failure.
LFRFID_WRITE_TARGET_MASK_ALL's doc pointed at MASK_DEFAULT, which has
never existed anywhere in the tree; ibutton_write_targets.h already
names the function correctly.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: correct the LF RFID write-targets entry
The entry described a settings-corruption round trip that the code could
not produce - the setter has always masked. Replaced with the symptom
that was real.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LF RFID: bound the getter's promise to this firmware, not the app's SDK
Review of the previous commit caught it making the same shape of
over-claim it was written to delete. "every bit it is handed is
nameable" is not true for an app: the mask applied is this firmware's
LFRFID_WRITE_TARGET_MASK_ALL, and an app built against an older SDK has
a narrower LFRFIDWriteTargetMax, so its own loop never reaches the bit.
The firmware can name that target; the app has no way to learn it
exists. What actually holds is the bound - what comes back is this
firmware's targets, never whatever the file happened to contain.
The CHANGELOG quoted one of the two write-screen messages and
paraphrased the other, so half of it could not be grepped; both are
quoted now. The file-top clause becomes its own sentence rather than a
fourth one hanging off an already long one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LF RFID: mask the stored write targets on read as well as on write
Appending a write target is deliberately not a layout change, so the
saved_struct version does not move and a settings file written by a
firmware that knows more targets is still accepted by an older build -
carrying bits that build has no meaning for. Nothing iterates past
LFRFIDWriteTargetMax today, but the settings page seeds itself from
this getter and writes the result back, so a downgrade plus one
unrelated toggle round-trips those unknown bits.
Same one-line guard the iButton twin already applies in both
directions. The load-failure log moves with it: it used to print the
stat description for a file that had just stat'ed FSE_OK, so a corrupt
or wrong-version file was reported as "unreadable (OK)".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: LF RFID write-target settings compatibility fix
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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>
* iButton: choose which blank types a write may try, and show the current one
A Dallas key carries no hint of which blank is in front of the reader, so a
write attempt walks the blank types in turn and keeps the one whose read-back
matches. Until now that chain was hidden inside dallas_ds1990_write_id(): the
app could not say which type it was attempting, and the user could not stop it
attempting one. Mirrors the LFRFID write-target design.
iButtonWriteTarget names the four writers (RW1990.1, RW1990.2, TM2004, TM01x)
and a protocol declares which of them apply to it, so DS1420 no longer has to
repeat DS1990's chain by hand. The group walks that mask, announcing each
target before it is tried - from outside the critical section, because the
announcement reaches the UI thread.
Settings > Write Blanks toggles them, stored via saved_struct and honoured by
the app and the CLI. Honouring it is opt-in: a worker starts at
ibutton_write_targets_default(), which is all four, matching the behaviour
before this existed.
ibutton_protocols_write_id() keeps its signature and delegates with no context,
so the API change is additive (88.12) and existing FAPs still link.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: address review of the write-target work
Every Dallas protocol now goes through write_targets. ds_generic, ds1971,
ds1992 and ds1996 each had a write_id that called tm2004_write() directly, so
they took a fallback branch that consulted neither mask and wrote a TM2004
with TM2004 switched off - the setting covered two protocols out of six. With
all six converted the fallback branch, the write_id slot in the Dallas
descriptor and the "zero means it writes itself" sentinel all go away, and the
ROM comes from get_editable_data() rather than a cast onto the front of the
protocol's data.
A mask that resolves to nothing no longer looks like an empty reader: the
worker reports iButtonWorkerWriteNoEnabledTarget, and the write screen
distinguishes "no blanks enabled" from "no enabled blank can write this key" -
reachable without disabling everything, since DS1420 cannot use TM01x. The CLI
says so too instead of spinning.
Settings are saved on Back while the plugin is still mapped, and a failure
raises a storage error dialog instead of only a log line, which is what the
plugin ABI comment always claimed happened.
Also: iButtonWriteTargetContext moves beside the types it is built from, so
ibutton_protocols.h stops pulling protocol_group_base.h and protocol_common_i.h
into the SDK tree; write_chip_name is cleared per write session; the settings
loader masks on read and no longer logs "unreadable (OK)" for a corrupt file;
static asserts pin the enum bit positions the saved mask depends on; and the
success screen names the blank that accepted the key.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: simplify the write-target code
The blank name on the write screen was always one target behind. Worker, app
and GUI threads all run at Normal priority, so posting the progress event does
not yield, and the next line masks the scheduler for most of a second - the
label queued before a target was painted only once that target had finished.
A 1 ms delay after the announcement is what actually lets the screen repaint.
Nothing-enabled is terminal, not something a retry can fix while the screen is
up, so it now reports once and drops the worker to idle instead of rebuilding
the screen and re-blinking every second forever.
The write context is normalised once at the API boundary, so the group stops
testing it for NULL twice; write_chip_name's 16-byte buffer and its snprintf
become the target itself, with the name derived on read; and the four places a
write reports collapse into one helper.
ibutton_protocols_get_write_targets, _write_id_targets, ibutton_write_target_write
and ibutton_write_targets_default have no caller outside lib/ibutton, so they
drop to '-' and stop costing FAP export thunks - the same treatment
lfrfid_write_target_type already gets.
Also: write_id's doc block had been orphaned by an insertion above it; the four
bit-position asserts collapse into one; .write_targets sits with the data fields
in all six descriptors rather than where write_id used to be; read() and
write_copy() stop the bus outside their critical sections, matching write_id;
and a dead scene-state enum, an overstated settings comment and three verbose
blocks go.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: let the write screen keep up with the blank being tried
The label showed only TM01x - the last blank of the pass. Announcing a target
and then masking the scheduler microseconds later just queues the events up, so
the app got to paint once, at the end, with the final name. 1 ms was nowhere
near a widget rebuild plus a GUI frame; 50 ms lets each one land, costing
200 ms on a pass where all four are tried and fail.
With nothing enabled the scene also flashed up a writing screen that the first
tick immediately replaced with an error. It already knows the mask before the
worker starts, so it now says so directly. The narrower case - something
enabled, but nothing that fits this key - still needs the worker to answer,
since the app cannot ask which blanks a protocol supports.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: changelog for the write-target selection
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: address review of the write-target selection
Honour the documented NULL write_ctx instead of dereferencing it, and let
ibutton_protocols_write_id() be the NULL case it describes.
The blank-type setting governs Write ID only, so an empty list no longer
blocks Full Writing, which rewrites the same chip and never consults it.
Do not crash on a write result this build has not heard of: the app is a
separate .fap, so a newer firmware must not take an older one down with it.
The switch keeps no default, so -Wswitch still catches in-tree additions.
Drop an unreadable settings file rather than fail every later read, which
left the settings page presenting defaults as a saved choice and skipping
the save that would have replaced it. Make the setter read-modify-write so
a setting added beside this one is not zeroed.
Clear the blank-type label when a pass gives up, reset the app-wide Popup
before the plugin error uses it, name the remedy instead of printing a
PluginManagerError ordinal, return before touching the bus when no target
is enabled, and check the ROM size the blank writers assume.
Export ibutton_write_targets_default(), which an exported header already
told apps a worker starts from.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* iButton: simplify the write-target selection
Derive the WriteId feature from write_targets instead of storing the same
fact twice in all six Dallas tables, which also deletes the runtime checks
that existed to catch the two disagreeing.
Charge the announce delay to the callback that repaints, not to the 1-Wire
path: the CLI registers a write callback but drops the progress result, so
it paid 50 ms per target for a name it never prints.
Drop the read-modify-write in the settings setter - it read a value it
overwrote on the next line, costing a stat and a full load on every save.
A static assert on the struct size forces the question when a second
setting is actually added.
Stop deleting an unreadable settings file from inside the getter: it is a
filesystem write on a read path, and "unreadable" includes a version a
newer firmware wrote. The page now saves on the way out even when nothing
changed, which replaces a bad file without destroying a good one.
Remove the "none enabled at all" branch and the scene state behind it. The
early return in on_enter means that event can only arrive with targets
enabled, so the distinction was unreachable.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Toolbox: compare complete simple array elements
* NFC: compare FeliCa systems and DESFire applications by content
Both element types hold nothing but scalars and pointers to nested arrays
of their own, so comparing the elements bytewise compares those pointers:
once the comparison covers the whole element, two copies of one card can
never be equal, and every exit from FeliCa emulation would write a shadow
file. Compare the entries themselves instead.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Changelog entry for the SimpleArray equality fix
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Maksim Neroba <mneroba@MB14.local>
Co-authored-by: Mykhailo Shevchuk <myte@ukr.net>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Key reuse fills sectors ahead of the sector cursor, so a card that shares keys across its sectors can be solved while the pass is still on sector 0. The pass kept asking for keys to sectors already read, and the scene then started the next dictionary phase on the same solved card.
Checked in three places, one per state machine that could keep working: the key request handler (the state every continuing transition comes through), the nested controller entry (covering all three callers that route into it), and the CUID -> user -> system phase chain in the dict attack scene.
Closes#1150
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
* LFRFID: let a caller restrict which chips a write may target
The write loop tried every writable chip in turn - T5577, EM4305, then each
Hitag micro password variant - and verified after each one. A verify read
costs up to 2 s, so a pass that does not find the chip in front of you can
take 10 s before it starts over, most of it spent on chips the user knows are
not there.
Adds LFRFIDWriteTarget, which names a chip rather than an encoding: the Hitag
micro variants share one write type but differ by password, so each is its own
target. lfrfid_worker_set_write_targets() takes a mask of them, defaulting to
all, so a caller that never sets one behaves exactly as before.
lfrfid_settings holds that mask on the SD card next to the saved keys, so the
app, the CLI and any app writing through LFRFIDWorker agree on it without one
having to hand it to another.
The loop now intersects the mask with what the protocol can actually encode
for, once, before writing anything. That replaces the per-pass skip counter
(which re-fired "cannot be written" on every pass) and flattens the nested
variant loop into the single pass over targets.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LFRFID: add a Settings menu with a Write Chips page
Settings sits at the bottom of the app menu and holds one page for now, where
each writable chip - T5577, EM4305 and the three Hitag micro / ID82xx variants
- can be switched on or off. Everything starts enabled, so a user who never
opens it sees no change.
The page itself lives in a plugin, loaded when the scene opens and unloaded
when it closes, since it is only reachable from one menu entry. The plugin
resolves against the firmware API alone - it needs no app-private symbols, so
there is no composite resolver or app symbol table here - and it is handed a
three-field context rather than LfRfid, which keeps the app's own layout out
of the plugin ABI.
The app keeps the VariableItemList: removing a view while it is the one on
screen latches the event loop stop, and the scene manager runs on_exit before
the previous scene's on_enter, so a plugin-owned view would be removed at
exactly the wrong moment. The list is reset before the plugin is freed, since
its change callbacks point into plugin code.
A missing or stale .fal costs only the ability to edit the setting: writes
read the mask from the settings file, not from the plugin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LFRFID: say why a write cannot start instead of spinning
With every chip enabled the write screen always had something to try, so the
only failure it reported was the protocol being unwritable, and only after a
full pass. Now that chips can be switched off, two more ways to have nothing
to do exist, and they are worth telling apart: nothing enabled at all, and
nothing enabled that this protocol can be written to (HID, for instance, only
encodes for a T5577).
The check runs before the worker starts, so the error is immediate rather than
10 s of "Writing" first. Probing re-encodes the key, hence the snapshot before
it and the restore after.
The worker is only stopped on exit if it was actually started - stopping a
thread that never ran is not allowed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LFRFID: honour the write chip setting in the CLI
rfid write drives the same worker as the app, so leaving it on the default
would have it quietly write to a chip the user switched off on the device.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: RFID write chip settings
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LFRFID: rework the write chip settings after review
A 5-agent review and a 4-agent cleanup pass over the previous commits. The
feature is unchanged; what it costs and how it fails are not.
Settings file layout is no longer SDK ABI. saved_struct_load() copies as many
bytes as the *firmware* thinks the struct is, so exporting LFRFIDSettings meant
adding a field later would overflow the stack object of any .fap built against
the older header - and only the major API version gates loading, which this
would not have bumped. The struct, path, magic and version are now private to
lfrfid_settings.c and the SDK gets two scalar accessors.
A chip the user switched off is now its own outcome. The worker reported
LFRFIDWorkerWriteProtocolCannotBeWritten for both "this protocol has no
writable chip" and "every chip that could write it is disabled", so the CLI
told users their protocol was unwritable when the real fix was a setting. It
now reports LFRFIDWorkerWriteNoEnabledTarget for the second, which also lets
the write screen drop its own pre-flight probe: the scene ran the same
lfrfid_write_targets_supported() sweep the worker was about to run, purely to
pick the message, and paid a data snapshot/restore and a started-the-worker
flag for the privilege.
The settings page no longer leaves anything behind. variable_item_list_reset()
does not clear the enter callback - it lives outside the model and
set_enter_callback() rejects NULL - so a pointer into the unloaded plugin stayed
parked in app-owned state; the app now installs its own. The cached VariableItem
pointers are gone: they were justified by a deadlock that cannot happen (the
view model mutex is recursive, and subghz already calls
variable_item_list_get() from a change callback) and would have dangled once
the list outgrew M*LIB's initial capacity of 16.
A failed save is no longer silent. It reported nothing, and re-entering the page
showed the old values as though the user had chosen them. Saving moved to the
Back event, where the screen still belongs to this scene, and a failure goes
through dialog_message_show_storage_error() like the app's other storage errors.
The VariableItemList is allocated on first use. variable_item_list_alloc()
starts a periodic 333 ms timer it never stops, so allocating it at startup
capped tickless idle for the whole RFID session over a screen most users never
open.
Also: the static assert forbade the append the header told you to make, so
appending a target broke the build with a message about Hitag; the two ordering
asserts and the variant subtraction are replaced by a table whose rows state
their own variant. lfrfid_write_target_type() lost its default label, so
-Wswitch now forces a new target to be classified at compile time. The plugin
ABI lost its context struct - a page gets the list and the scene owns the view
switch, matching subghz. Four helpers with no caller outside the firmware are
marked "-" rather than exported for good. The plugin load shows the loading
animation and names the loader error, which separates a missing .fal from a
stale one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* LFRFID: second review pass over the write chip settings
Nine more agents over the reworked code. Four real defects, the rest comment
and shape corrections.
The lazily allocated VariableItemList was allocated before the plugin load, so
a failed load still started the periodic 333 ms timer the laziness exists to
avoid - for the rest of the session, on a screen the user could not use. It now
happens only once the page is known to be there.
lfrfid_settings_get_write_targets() stat'ed the file after trying to load it,
purely to pick a log level, so the missing-file case still went through
saved_struct_load()'s E log. It checks first and skips the load, which makes
the default path - every device whose owner never opened the screen - silent
again on every write.
The plugin-error popup was one character too long for the buffer and rendered
as "(error 1", because lfrfid_text_store_set() passed LFRFID_TEXT_STORE_SIZE
against a buffer that carries the extra byte for the terminator. Fixed at the
helper, so every caller gets its last character back.
The CLI never handled LFRFIDWorkerWriteTooLongToWrite, so a non-writable card
left it printing nothing while the app said "Still Trying to Write" on screen.
Both write callbacks now log an unhandled result instead of sending custom
event 0, which is not a LfRfidCustomEvent and would be dropped in silence.
Shape: the write scene's file static moved into the scene manager's own
per-scene state, lfrfid_settings_load() folded into its only caller, the write
loop's "done" flag stopped standing in for "never started", and the write type
switch lost its default so -Wswitch covers it like the target switch (a probe
build confirms an unclassified enumerator is now a build error). The <= 32
assert became < 32: at exactly 32 targets LFRFID_WRITE_TARGET_MASK_ALL shifts
by the full width of its type.
Comments: five said things that were not true - that the error code tells a
missing .fal from a stale one (plugin_manager collapses both into one value),
that a failed settings read had no other trace (saved_struct logs all four
causes), that the loading view swallows input (leaving it resets the input
queue), that opening the settings page writes a file, and that a scene's screen
belongs to the next scene by the time on_exit runs.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: annotate the RFID write chip entry with its issue and PR
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com>
* SubGHz: give the feature plugins one shared loader
The Frequency Analyzer's loader owned the composite API resolver outright: it
allocated one on the way in and freed it on the way out. A second feature plugin
loading against the same SubGhz would have overwritten that pointer and then
freed a resolver the other one was still resolving through.
Move the mapping to subghz_feature_plugin.c, which refcounts the resolver, and
put the rest of what hosting a .fal costs this app with it: the plugin API
version, the directory they live in, and the missing-plugin report, which was an
error string, a custom event and a scene push repeated per plugin. One event now
serves all of them, so a third does not grow the enum every .fal compiles in, and
the loader raises the report itself - a caller that forgot to would leave the
user on a loading view that swallows Back, with no way out.
Two behaviour changes fall out of sharing, both in the analyzer's favour. It now
raises the loading animation while its .fal is read, instead of leaving the menu
the start scene had already cleared on screen. And a .fal that loads but exports
a null entry point is rejected rather than returned, so "nothing is left
allocated on failure" holds for that case too.
The plugin API version goes 1 -> 2: struct SubGhz gains a field, which shifts
every offset after it, so the resource pack has to be rebuilt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGHz: export the Marantec CRC helper through the SDK API
Manual Marantec key generation calls subghz_protocol_marantec_crc8, so once that
code ships as a .fal it has to resolve the symbol like any other app. Move the
declaration to public_api.h, the header the SDK exports from, and include it from
marantec.h the way keeloq.h already does.
API 88.7 -> 88.8.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGHz: extract Add Manually into a loadable plugin
Add Manually and Add Manually [Advanced] are one flow of six scenes plus the
generator table behind them, and none of it is touched unless someone is building
a remote by hand - but all of it sat in the firmware image. It now ships as
subghz_add_manually.fal, mapped when the flow is entered and dropped once it is
done, which frees 9,584 bytes of flash: usable free flash goes from 16,748 to
26,332 bytes.
The six scenes stay in the app as shims that forward to the plugin, because the
scene handler tables are built at compile time and save_name still looks for
SubGhzSceneSetType on the stack. That is the same shape the NFC app uses for its
plugin-provided scenes, hand-written thunks included. Everything else - the
96-entry protocol menu, the GenInfo filler and the nineteen generators - moved.
Unmapping is deliberately not symmetric with mapping. The flow's scenes push one
another, so a forward transition runs the current scene's on_exit from inside
plugin code; dropping the image there would carry on executing freed memory. The
start scene is where every exit from the flow arrives with none of its scenes
left on the stack - after a save, by way of the saved list - so it unmaps there,
and app teardown does the same for a kill that runs no scene handler.
Unmapping also has to take back what the plugin left in the app's views:
byte_input keeps its header string by reference and draws through it, and the
string the last scene installed is a literal inside the image - including the
empty one its own on_exit puts there.
GenInfo stays app-allocated, as it was, because byte_input holds a pointer into
it. Its contents do not survive an unmap either - mod, name and manuf point into
the plugin - but nothing reads them across one: re-entering the flow refills them.
Only two symbols could not be resolved from the firmware SDK, so the app's
private table gains subghz_txrx_set_preset and subghz_file_name_clear; the
fourteen subghz_protocol_*_create_data functions the generators call were already
exported. The APPCHK warning naming those two matches the one the Frequency
Analyzer plugin already produces - the import check cannot see a host app's
private table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Changelog
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* 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>
* NFC: save recovered MIFARE Classic keys to the user dictionary
Keys the app recovers had no way out of the card they came from: they reach
MfClassicData, the dump and the per-UID .keys cache, but never the shared user
dictionary that every other consumer reads. The per-UID (CUID) dictionary used
for static-encrypted-nonce cards is the worst case - those keys are visible to
nothing else, so a card cloned to a magic tag cannot later be wiped.
Add a "Save Keys to Dictionary" action to the MIFARE Classic read and saved
menus, next to "Show Keys" and shown only when the card holds a key. It works
from the found masks rather than from whichever attack filled them, so one path
covers the CUID dictionary, nested, MFKey32, a hand-entered key or an imported
dump.
The import filters against both dictionaries with one pass each instead of
keys_dict_is_key_present() per key, which rescans the whole file every time -
80 keys against a 1000-key dictionary would be 80 full walks. Comparing parsed
bytes rather than formatted strings also means a hand-edited lowercase-hex line
counts as present. Filtering against the system dictionary is what stops a
default-keyed card from doubling the user dictionary.
Both files are read in full and keys_dict_alloc() walks each one again just to
count, so the scene puts up the animated loading_label view first rather than
freezing on the menu it came from.
The helper lives app-side, next to the other key dictionary scenes, so the
plugin only picks the dictionary type and pushes the scene: no api_symbols.csv
change and no icon export through the app API table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: save MIFARE Classic keys to the user dictionary
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* keys_dict: append instead of rewriting the file for every added key
keys_dict_add_key() seeks to end of file and then *inserts*, which routes through
file_stream_delete_and_insert(): a scratch file is created, the whole dictionary
is copied into it, the new line is appended, the lot is copied back, the original
is truncated and the scratch file is deleted. At end of file with nothing to
shift, all of that is waste - and quadratic for the two callers that add keys in
a loop (MFKey32 writing back recovered keys, and now the key import scene).
Write the line instead. Every caller already seeks to end of file first, so the
observable result is identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* NFC: report the key import honestly, and check what it assumed
Review follow-ups to the key import scene, no change to what it does on a
successful run:
- A missing system dictionary silently defeated the filter the whole feature
rests on: every factory key on the card would be appended to the user
dictionary while the screen still read "Already known: 0". Log it, the way the
MIFARE Plus dictionary attack already does.
- A partial write reported as a total failure. The append stops at the first
failure but the counts up to that point are accurate, so say "Saved N of M"
instead of leaving the user unable to tell it from nothing having happened.
- The loaded device and the dictionary type are picked independently, so a
mismatch was possible and would have crashed inside nfc_device_get_data()
with nothing pointing back at the pair that caused it. Check it at the
boundary instead.
- Collapse the two "already known" counters into one. Nothing branched on which
dictionary held a key, both display sites summed them, and only a log line
read them apart. The candidate total moves onto the struct so the result
screen needs nothing but the stats.
- Correct two comments. keys_dict_alloc() leaves the stream closed for a missing
file, not for an empty one, and the system dictionary is 4,082 keys, not the
~1,200 first claimed. Document that import wants a duplicate-free list, which
is what lets the compare loop stop at its first match.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* CHANGELOG: append instead of rewriting a user dictionary on key add
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGhz: share the remaining duplicated serialize handlers
Follow-up to af2370573. Two families of decoder serialize handlers were still
copied per protocol: the one writing generic.data_2 as "Data" (beninca_arc,
jarolift, kinggates_stylo_4k) and the one writing TE (holtek_th12x, legrand,
smc5326). Both now live in common.c and are pointed at straight from the
vtable, so the per-protocol functions and their declarations are gone.
The data_2 block also existed three more times in the encoder create_data
paths of those same files; they share the helper too. princeton is that TE
handler plus a Guard_time write, so it calls the helper and adds its own
field. honeywell had generic and decoder in the reverse order, which was the
only reason af2370573 could not fold six of its handlers - swapping the two
members folds them.
The helpers log through generic.protocol_name instead of each file's TAG, so
they match the vtable slot signature and need no per-protocol wrapper. Two
diagnostics-only consequences: converted paths log under the protocol's
user-facing name rather than TAG, and the encoder create_data failure message
merges with the serialize one. Nothing in the tree filters on log tags.
SUBGHZ_ASSERT_*_LAYOUT pins the punned prefixes at compile time. Pinning the
tail offset alone would not have caught honeywell's swap: decoder and generic
are both multiples of 8, so te lands at the same offset either way. Verified
by restoring the old order and watching the build fail.
.text 696,740 -> 695,828 (-912 B), .rodata unchanged, api_symbols.csv untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGhz: share the protocol alloc bodies
48 decoder and 48 encoder alloc functions differed only in the struct type,
the protocol descriptor and, for encoders, the repeat and upload-buffer
constants. They now call subghz_protocol_{decoder,encoder}_common_alloc and
keep a thunk each - the vtable slot passes only SubGhzEnvironment*, so the
per-protocol constants have to come from somewhere.
Keeping the thunk rather than making alloc data-driven is deliberate. Hanging
instance_size/repeat/size_upload off SubGhzProtocol and letting alloc be NULL
would remove all 96 thunks, but it moves the same constants into .rodata at
about 12 B per protocol across the whole registry, adds a dispatch branch, and
changes a struct that is FAP ABI (raw.h is an exported SDK header and includes
base.h). Net saving is roughly zero.
SubGhzProtocolEncoderCommon stays {base, encoder} so protocols that only use
free/stop/yield are not forced to carry a generic; the alloc helper puns
through a nested SubGhzProtocolEncoderCommonGeneric instead, mirroring how
SubGhzProtocolDecoderCommonTe extends the decoder type.
SUBGHZ_ASSERT_*_LAYOUT now covers all 114 structs any shared helper puns
through, not just the ones converted here - 14 of those were already being
punned by free/stop/yield/reset/serialize with no guard at all. The asserts
cost no flash.
23 allocs keep their own body for doing real extra work (keystore setup,
rainbow-table names, extra buffers). Most could call the helper and then add
their own lines the way princeton does for serialize, worth roughly 440 B;
left for a follow-up so this commit stays mechanical.
.text 695,828 -> 693,396 (-2,432 B), .rodata unchanged, api_symbols.csv untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGhz: share the deserialize bodies that only differ by a callback
Two more shared helpers in common.c:
- subghz_protocol_encoder_common_deserialize(ctx, ff, min_count_bit, get_upload)
covers 11 encoders whose bodies were identical apart from the bit count and
their get_upload
- subghz_protocol_decoder_common_deserialize_te(ctx, ff, min_count_bit) covers
the three decoders that read TE back, mirroring the serialize_te helper
The per-protocol get_upload functions now take void* instead of being cast to
the callback type. Calling through an incompatible function pointer type is
formally undefined even though it is harmless on AAPCS, and enabling LTO is on
the roadmap for this same effort - not a combination worth keeping.
Deliberately not converted:
The 35 decoders whose deserialize is just a tail call to
subghz_block_generic_deserialize_check_count_bit compile to 8 bytes each. A
decoder thunk is 6 bytes, so folding them would gain about 70 B - real, but a
poor return for touching 35 files and adding a second hop to a call that is
already a tail call into shared code.
The six encoders that call a per-protocol check_remote_controller before
get_upload would need a fifth argument, which spills to the stack on AAPCS and
costs more per thunk than it saves. They can instead be folded into the helper
above by giving their get_upload a bool return and moving the check inside it,
which also reaches allstar_firefly/elplast and, once the shared stop resets
encoder.front, came_twee/marantec/power_smart/hormann/magellan - about 390 B
across 13 protocols with no new helper. Left as a follow-up because it changes
those functions rather than just moving them.
.text 693,396 -> 692,900 (-496 B), .rodata unchanged, api_symbols.csv untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Storage: share the command-dispatch bodies
Twelve public storage calls had the same S_API macro body and differed only in
the StorageCommand they sent. Four static helpers now carry it:
storage_file_command_bool file_truncate, file_sync, file_eof, dir_rewind
storage_file_command_uint64 file_tell, file_size
storage_sd_command sd_format, sd_unmount, sd_mount, sd_status
storage_path_command common_remove, common_mkdir
Split by return macro on purpose - S_RETURN_BOOL, S_RETURN_UINT64 and
S_RETURN_ERROR read different members of the SAReturn union, so one helper for
all twelve would read the wrong member. Each pairing was checked against what
storage_processing.c actually writes.
The public symbols stay as one-line thunks: all twelve are exported in
api_symbols.csv, so the bodies could move but the symbols could not.
file_close and dir_close share the shape but do work between the epilogue and
the return, so they keep their own bodies.
.text 692,900 -> 692,296 (-604 B), .rodata unchanged, api_symbols.csv untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* SubGhz: fold the allocs and deserializes the earlier reviews left behind
Follow-up to 84aca75eb and 5f4f4cc2b, both of which deliberately stopped at the
mechanical cases and listed what they skipped.
15 more allocs now call the shared helpers and keep only their own extra work -
keystore lookups, rainbow-table names, extra buffers: alutech_at_4n, beninca_arc,
bin_raw, dickert_mahs, faac_slh, jarolift, keeloq, kinggates_stylo_4k,
nice_flor_s, telcoma_edge.
8 more encoders join subghz_protocol_encoder_common_deserialize. Their
get_upload had no failure path and returned void, so it becomes
bool/void* and returns true; five of them also move their
check_remote_controller call from deserialize into the top of get_upload,
which needs a forward declaration since get_upload is defined above it.
min_count_bit widens from uint8_t to uint16_t in both deserialize helpers.
subghz_block_generic_deserialize_check_count_bit compares it against a
uint16_t, so the narrower parameter would have silently truncated any protocol
above 255 bits. None exists today; the tree's largest is 128.
The get_upload doc says "true Always; this encoder has no failure path" rather
than "on success". These eight have no bound check, unlike the eleven converted
in 5f4f4cc2b, and claiming success would imply a validation none of them do.
They cannot overrun today - deserialize_check_count_bit pins data_count_bit by
equality before get_upload runs - but keyfinder has only 4 spare entries in a
60-entry buffer, so adding the guards is worth its own commit.
Not done here, deliberately: the five moved check_remote_controller calls write
only serial/btn/cnt on the encoder instance, which nothing reads, so they could
be deleted outright rather than moved. Deleting a store is a behaviour change
and belongs with the encoder.front reset in its own commit.
.text 692,296 -> 691,848 (-448 B), .rodata unchanged, api_symbols.csv untouched.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* upd changelog
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mishamyte pls revert if those findings are wrong
this changes logic in favour of fixing missing last key check and iteration changes were applied here too, i'm not 100% sure on the fix not breaking other functionality
* SubGHz: Superrollo GW60 (HCS361) support via KeeLoq
GW60 roller-shutter remotes are HCS361 KeeLoq with an asymmetric preamble
(Te HIGH / 2Te LOW), a 10*Te sync-high pulse, and a 67-bit frame (64 payload
+ VLOW + 2-bit CRC). The crypto and PWM bit timings already match KeeLoq, so
the decoder is taught to lock onto that framing and the encoder to emit it
(Te=450 with its own preamble/sync/CRC), keyed off the "Superrollo" manufacturer.
Adds a "KL: Superrollo 433MHz" Add-Manually entry like the other KeeLoq variants.
The manufacturer key in keeloq_mfcodes (Normal Learning) does the rest.
* add changelog, supported systems, and apply fbt format
* add key
* supported systems
---------
Co-authored-by: MX <10697207+xMasterX@users.noreply.github.com>
* NFC: do not probe the default password when AUTHLIM is unreadable
try_default_pass reads access.authlim straight out of the in-memory card
image and probes the default password when it is zero. On a card with
PROT=1 and AUTH0 low enough to cover the ACCESS page, that page is never
returned by the card, so it is still the zero fill from malloc - and
zero decodes as "no authentication limit".
The result is a PWD_AUTH with the default password on every plain read
of exactly the cards that count failed attempts. Per the MF0ULX1 data
sheet each negative verification is counted, and once the counter
reaches AUTHLIM the next one permanently locks the protected memory, at
which point every PWD_AUTH fails regardless of the password.
Gate the probe on the ACCESS page having actually been read. AUTHLIM
unknown now means "assume protected" instead of "assume unlimited".
Cards with PROT=0 keep default-password detection, since their config
pages stay readable; the ones that lose it are the ones where probing
was unsafe.
Write mode is exempt. The app deliberately skips the read-phase auth
when writing to a target, so for password-protected types this probe is
the only thing that can set auth_success - gating it there would turn a
working write into "Card locked" for any target still carrying the
factory password. That path is user-initiated, unlike a plain read.
Also take the return of mf_ultralight_get_config_page() instead of
dropping it - it only fails for types with no config page, which the
feature check above already excludes, but the next line dereferences the
pointer it would have left NULL.
Fixes#1087
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* changelog: default password not tried when AUTHLIM is unreadable
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>