MX пре 2 година
родитељ
комит
f1c2d7bcb8
42 измењених фајлова са 4964 додато и 1 уклоњено
  1. 3 1
      ReadMe.md
  2. 20 0
      non_catalog_apps/rolling_flaws/CHANGELOG.md
  3. 21 0
      non_catalog_apps/rolling_flaws/LICENSE
  4. 956 0
      non_catalog_apps/rolling_flaws/README.md
  5. 13 0
      non_catalog_apps/rolling_flaws/application.fam
  6. BIN
      non_catalog_apps/rolling_flaws/assets/Lock_10x8.png
  7. BIN
      non_catalog_apps/rolling_flaws/assets/Unlock_10x8.png
  8. BIN
      non_catalog_apps/rolling_flaws/docs/clone-fz-remote.png
  9. BIN
      non_catalog_apps/rolling_flaws/docs/enc00-attack.png
  10. BIN
      non_catalog_apps/rolling_flaws/docs/keeloq-codes.png
  11. BIN
      non_catalog_apps/rolling_flaws/docs/kgb-attack.png
  12. BIN
      non_catalog_apps/rolling_flaws/docs/pair-fz-remote.png
  13. BIN
      non_catalog_apps/rolling_flaws/docs/replay-attack-diagram.png
  14. BIN
      non_catalog_apps/rolling_flaws/docs/replay-attack-failed-diagram.png
  15. BIN
      non_catalog_apps/rolling_flaws/docs/rollback-attack.png
  16. BIN
      non_catalog_apps/rolling_flaws/docs/rolljam-attack.png
  17. BIN
      non_catalog_apps/rolling_flaws/docs/test-attack.png
  18. BIN
      non_catalog_apps/rolling_flaws/docs/unknown-mf-attack.png
  19. BIN
      non_catalog_apps/rolling_flaws/docs/window-future-attack.png
  20. BIN
      non_catalog_apps/rolling_flaws/docs/window-next-attack.png
  21. 421 0
      non_catalog_apps/rolling_flaws/rolling_flaws.c
  22. BIN
      non_catalog_apps/rolling_flaws/rolling_flaws.png
  23. 34 0
      non_catalog_apps/rolling_flaws/rolling_flaws_about.h
  24. 226 0
      non_catalog_apps/rolling_flaws/rolling_flaws_keeloq.c
  25. 7 0
      non_catalog_apps/rolling_flaws/rolling_flaws_keeloq.h
  26. 125 0
      non_catalog_apps/rolling_flaws/rolling_flaws_send_keeloq.c
  27. 5 0
      non_catalog_apps/rolling_flaws/rolling_flaws_send_keeloq.h
  28. 285 0
      non_catalog_apps/rolling_flaws/rolling_flaws_settings.c
  29. 21 0
      non_catalog_apps/rolling_flaws/rolling_flaws_settings.h
  30. 53 0
      non_catalog_apps/rolling_flaws/rolling_flaws_structs.h
  31. 140 0
      non_catalog_apps/rolling_flaws/rolling_flaws_subghz_receive.c
  32. 48 0
      non_catalog_apps/rolling_flaws/rolling_flaws_subghz_receive.h
  33. 110 0
      non_catalog_apps/rolling_flaws/rolling_flaws_utils.c
  34. 29 0
      non_catalog_apps/rolling_flaws/rolling_flaws_utils.h
  35. 1 0
      non_catalog_apps/sd_spi/.gitkeep
  36. 674 0
      non_catalog_apps/sd_spi/LICENSE
  37. 33 0
      non_catalog_apps/sd_spi/README.md
  38. 14 0
      non_catalog_apps/sd_spi/application.fam
  39. 949 0
      non_catalog_apps/sd_spi/sd_spi.c
  40. 204 0
      non_catalog_apps/sd_spi/sd_spi.h
  41. 572 0
      non_catalog_apps/sd_spi/sd_spi_app.c
  42. BIN
      non_catalog_apps/sd_spi/sd_spi_app_10px.png

+ 3 - 1
ReadMe.md

@@ -13,7 +13,7 @@ Apps contains changes needed to compile them on latest firmware, fixes has been
 
 The Flipper and its community wouldn't be as rich as it is without your contributions and support. Thank you for all you have done.
 
-### Apps checked & updated at `28 Aug 06:05 GMT +3`
+### Apps checked & updated at `4 Sep 20:11 GMT +3`
 
 
 # Default pack
@@ -186,6 +186,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 | Atomic Dice Roller | ![GPIO Badge] | [by nmrr](https://github.com/nmrr/flipperzero-atomicdiceroller) |  | ![None Badge] |
 | NRF24 Channel Scanner | ![GPIO Badge] | [by htotoo](https://github.com/htotoo/NRF24ChannelScanner) |  | [![Author Badge]](https://lab.flipper.net/apps/nrf24channelscanner) |
 | Mx2125 - Step Counter | ![GPIO Badge] | [by grugnoymeme](https://github.com/grugnoymeme/flipperzero-stepcounter-fap) |  | ![None Badge] |
+| SD SPI | ![GPIO Badge] | [by Gl1tchub](https://github.com/Gl1tchub/Flipperzero-SD-SPI) |  | ![None Badge] |
 | IR Remote | ![IR Badge] | [by Hong5489](https://github.com/Hong5489/ir_remote) | improvements [by friebel](https://github.com/RogueMaster/flipperzero-firmware-wPlugins/pull/535) - Hold Option, RAW support [by d4ve10](https://github.com/d4ve10/ir_remote/tree/infrared_hold_option) | ![None Badge] |
 | IR Intervalometer for Sony Cameras | ![IR Badge] | [by Nitepone](https://github.com/Nitepone/flipper-intervalometer) |  | [![UFW Badge]](https://lab.flipper.net/apps/sony_intervalometer) |
 | IR Xbox Controller | ![IR Badge] | [by gebeto](https://github.com/gebeto/flipper-xbox-controller) |  | [![Author Badge]](https://lab.flipper.net/apps/xbox_controller) |
@@ -205,6 +206,7 @@ The Flipper and its community wouldn't be as rich as it is without your contribu
 | (Q)M100 UHF RFID | ![RFID Badge] | [by frux-c](https://github.com/frux-c/uhf_rfid) | WIP -> (+Added icon by @xMasterX) | ![None Badge] |
 | Enhanced Sub-GHz Chat | ![SubGhz Badge] | [by twisted-pear](https://github.com/twisted-pear/esubghz_chat) |  | ![None Badge] |
 | TPMS Reader | ![SubGhz Badge] | [by wosk](https://github.com/wosk/flipperzero-tpms/tree/main) |  | ![None Badge] |
+| Rolling Flaws | ![SubGhz Badge] | [by CodeAllNight & jamisonderek](https://github.com/jamisonderek/flipper-zero-tutorials/tree/main/subghz/apps/rolling-flaws) |  | ![None Badge] |
 | Analog Clock | ![Tools Badge] | [by scrolltex](https://github.com/scrolltex/flipper_analog_clock) |  | [![UFW Badge]](https://lab.flipper.net/apps/analog_clock) |
 | Brainfuck interpreter | ![Tools Badge] | [by nymda](https://github.com/nymda/FlipperZeroBrainfuck) |  | [![UFW Badge]](https://lab.flipper.net/apps/brainfuck) |
 | Ceasar Cipher | ![Tools Badge] | [by panki27](https://github.com/panki27/caesar-cipher) |  | [![UFW Badge]](https://lab.flipper.net/apps/caesar_cipher) |

+ 20 - 0
non_catalog_apps/rolling_flaws/CHANGELOG.md

@@ -0,0 +1,20 @@
+# changelog
+
+This file contains all changelogs for latest releases, from 1.3 onward.
+
+## v1.4
+
+### Fixed
+If received signal is less than 500ms from last decoded signal, we ignore it now.  In the future, we can consider checking the "Key" to see if something in the signal changed, but for now, we just ignore it.
+
+In some firmware, the MF fails to parse because it is mising a \n at the end of the file. This is now fixed.
+
+In some firmware, the SN fails to parse because it is mising from keeloq.c; the application will now use Fix data in that case.
+
+## v1.3
+
+### Added
+Added this change log file.
+
+### Fixed
+In some firmware, the retry count on KeeLoq was 100 transmissions, which is too much. Now it stops transmitting after 1 second (or the end of the transmissions) whichever comes first.

+ 21 - 0
non_catalog_apps/rolling_flaws/LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Derek Jamison
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 956 - 0
non_catalog_apps/rolling_flaws/README.md

@@ -0,0 +1,956 @@
+# Rolling Flaws
+
+Rolling Flaws (version 1.4) by [@CodeAllNight](https://twitter.com/codeallnight).
+
+[YouTube demo](https://youtu.be/gMnGuDC9EQo?si=4HLZpkC4XWhh97uQ) of using Rolling Flaws application.  The video shows how to use the application to simulate a receiver that has a Replay attack flaw, Pairing FZ to a receiver, Cloning sequence attack, Future attack, Rollback attack & KGB attack.  The Rolling Flaws application also supports things like "ENC00" attack & window-next attacks, which are described in scenarios below but was not in video.  Rolljam is discussed in document, but discouraged to test since it is [illegal to jam signals](https://www.fcc.gov/general/jammer-enforcement) in the US.  If you have additional ideas, please let me know!
+
+Note: This application works with Official firmware & most of the other firmware versions as well.  Xtreme firmware needs to be on DEV channel to build.
+
+- Discord invite: [https://discord.com/invite/NsjCvqwPAd](https://discord.com/invite/NsjCvqwPAd)
+- YouTube: [https://youtube.com/@MrDerekJamison](https://youtube.com/@MrDerekJamison)
+- GitHub: [https://github.com/jamisonderek/flipper-zero-tutorials/blob/main/subghz/apps/rolling-flaws](https://github.com/jamisonderek/flipper-zero-tutorials/blob/main/subghz/apps/rolling-flaws)
+- Support my work: [ko-fi.com/codeallnight](ko-fi.com/codeallnight)
+
+This application is intended to help you learn about rolling code flaws.  
+
+- [Introduction](#introduction)
+- [Helpful hints](#helpful-hints)
+- [Installation](#installation)
+- [Menu Options](#menu-options)
+- [Settings](#settings)
+- [Tutorial](#tutorial)
+  - [Scenario 1: clone RAW signal, replay attack on](#scenario-1-clone-raw-signal-replay-attack-on)
+  - [Scenario 2: clone RAW signal, replay attack off](#scenario-2-clone-raw-signal-replay-attack-off)
+  - [Scenario 3: pair remote, send next code](#scenario-3-pair-remote-send-next-code)
+  - [Scenario 4: clone remote, send next code](#scenario-4-clone-remote-send-next-code)
+  - [Scenario 5: skip ahead, within window-next](#scenario-5-skip-ahead-within-window-next)
+  - [Scenario 6: future attack](#scenario-6-future-attack)
+  - [Scenario 7: rollback attack](#scenario-7-rollback-attack)
+  - [Scenario 8: rolljam attack](#scenario-8-rolljam-attack)
+  - [Scenario 9: KGB/Subaru MF attack](#scenario-9-kgbsubaru-mf-attack)
+  - [Scenario 10: unknown MF attack](#scenario-10-unknown-mf-attack)
+  - [Scenario 11: enc00 attack](#scenario-11-enc00-attack)
+  - [Scenario 12: test transmitter](#scenario-12-test-transmitter)
+- [Contact info](#contact-info)
+- [Future features](#future-features)
+
+## Introduction
+**Educational use only.** This application is intended to be used for educational purposes only.  It is intended to help you learn about rolling code flaws.  IIf you use this information to attack devices, you are responsible for any damage you cause.
+
+<img src="./docs/keeloq-codes.png" width="50%" />
+
+The Keeloq protocol has a FIX (button + serial number) and a HOP (encrypted data that can be decrypted into a count + some validation information, such as the end of the serial number). The receiver has a current count for the serial number, like 0x1E00 in the diagram above.  There are a set of "Next" codes that will Open the device.  When one of those codes is received, the beginning of the Next block will start with the received code.  There are also a set of "Future" codes.  When two adjacent codes are received, then typically the Next block will start with the second received code.  The remaining set of codes are considered "Past" codes.  Different manufacturers handle past codes differently. As a new count is accepted, the door opens and the location of the Next, Future and Past codes change.  For more details, see [this video](https://youtu.be/x4ml1JAH1q0) along with the [rolling code playlist](https://www.youtube.com/playlist?list=PLM1cyTMe-PYJfnlDk3NjM85kU5VyCViNp).
+
+Sending signals to a real receiver has the potential to desync the remote and can even cause the remote to no longer be valid.  The reason this application was built was so that you DO NOT mess with equipment, unless you are pen testing it with permission.  Even then, you can still mess things up & require service or replacement (for example, HCS300 overflow bits get cleared and you reach 0xFFFF count then bad things may happen).  Please use this application instead of an actual device.
+
+This application is intended to simulate various KeeLoq receivers that you may encounter.  You can configure the receiver to simulate the device you want to practice on.  Use a second Flipper Zero or HackRF or whatever to try to get the "Opened!" message.
+
+In the future, I hope to offload this application to an ESP32+CC1101 so that you can use a single Flipper to practice rolling codes.
+
+
+## Helpful hints
+You can rename the file ``SD Card\subghz\assets\keeloq_mfcodes``, so that a .sub file with KeeLoq protocol will be sent **without incrementing** counts.  This will also cause all signals to be decoded as "KL Unknown".  Be sure to rename it back when you are done.
+
+Firmware other than Official does support encoding rolling code signals.  This makes it easier to save and transmit rolling codes.  This can be a good thing when you are pen testing, but a bad thing when you are just clicking around and don't understand the potential risks.  In official firmware, you can still decode signals, write the keys down and then use the "Add Manually" option to create a signal, and then edit the keys to match what was decoded.  It's more work, but then you won't accidently be sending signals that you don't understand.  Unofficial firmware may also unlock the TX frequency list, which may be illegal in your region.  Only broadcast on frequencies that are legal in your region.  If you choose to install Unofficial firmware, it is not supported by the Flipper Zero team, so you will need to get support from the community.  If you are using unofficial firmware, you should be aware of the risks.
+
+This rest of this section assumes you are familiar with building your own firmware.  If you are not familiar, [this video](https://youtu.be/gqovwRkn2xw) will walk you through the process of being able to build and deplay the firmware.  Note: Due to recent firmware changes, you need to do "[Debug] Flash (USB, with Resources)" [instead of *without* Resources] since some of the subghz code has moved into Resources.
+
+If you want a Bin_RAW file, you can build a custom firmware without knowledge of keeloq.  In ``.\lib\subghz\protocols\protocol_items.c`` replace the line ``&subghz_protocol_keeloq,`` with ``// &subghz_protocol_keeloq,``.  Then build the firmware.  Now you can use the "Read" option with Bin_RAW set to "On" to get a Bin_RAW file.  Be sure to edit the file back when you are done.
+
+If you want to generate a custom SUB file for a specific key and count, you can replace the ``case SubmenuIndexDoorHan_433_92`` code in the ``.\applications\main\subghz\scenes\subghz_scene_set_type.c`` file.  You will need to build and deploy the firmware with resources.  This will replace the implmentation of "Add Manually/DoorHan_433".  For example, if you want to generate a SUB file for a DoorHan remote with key=0x084EE9D5, button=0x2, and count of 0xEC00, at 433.920MHz; you would use the following...
+```c
+        case SubmenuIndexDoorHan_433_92:
+            generated_protocol = subghz_txrx_gen_keeloq_protocol(
+                subghz->txrx, "AM650", 433920000, "DoorHan", 0x084EE9D5, 0x2, 0xEC00);
+            if(generated_protocol != SubGhzProtocolStatusOk) {
+                furi_string_set(
+                    subghz->error_str, "Function requires\nan SD card with\nfresh databases.");
+                scene_manager_next_scene(subghz->scene_manager, SubGhzSceneShowError);
+            }
+            break;
+```
+
+If you want the Flipper Zero to be able to decode the same signal multiple times, in ``.\lib\subghz\protocols\protocol_items.c`` after the two occurances of ``instance->decoder.decode_count_bit = 0;`` add the line ``instance->generic.data = 0;``.  This will cause the Flipper Zero to forget the previous data, so it will decode the same signal multiple times.  Be sure to edit the file back when you are done.
+
+To scan for more interesting sequences, make this breaking change to keeloq.c file that will keep incrementing the key until it finds a DoorHan code (but it leaves the FIX value the same).  This is one technique to search for ENC00 sequences.  Be sure to edit the file back when you are done.
+```c
+void subghz_protocol_decoder_keeloq_get_string(void* context, FuriString* output) {
+    furi_assert(context);
+    SubGhzProtocolDecoderKeeloq* instance = context;
+
+    // Uncomment the next line if you want to ALWAYS jump ahead.
+    // instance->generic.data += 0x100000000L;
+    while(true) {
+        subghz_protocol_keeloq_check_remote_controller(
+            &instance->generic, instance->keystore, &instance->manufacture_name);
+        if(strcmp(instance->manufacture_name, "Unknown") == 0) {
+            // UNKNOWN
+        } else if(strcmp(instance->manufacture_name, "DoorHan") != 0) {
+            FURI_LOG_E(
+                TAG,
+                "Wrong manufacturer name: %s  high-bytes:%08lX  cnt:%08lX",
+                instance->manufacture_name,
+                (uint32_t)(instance->generic.data >> 32),
+                instance->generic.cnt);
+        } else {
+            FURI_LOG_I(
+                TAG,
+                "Found manufacturer name: %s %08lX",
+                instance->manufacture_name,
+                (uint32_t)(instance->generic.data >> 32));
+            break;
+        }
+        instance->generic.data += 0x100000000L;
+    }
+
+    // Continue with the original code.
+```
+
+Two common hardware implementations of KeeLoq are the HCS300, which uses 10 bits in discriminator & the HCS200, which uses 8 bits.  The Flipper Zero software implementation decodes using 8 bits.  If you make a custom change to the ``.\lib\subghz\protocols\keeloq.c`` file you can return the encoded data, which will be used by the Rolling Flaws application.  For "SN00/cfw*" set to "No" to work properly, you will need these changes.  For "SN bits/cfw*" set to "10 (dec)", you will also need this changes.  These changes allow the application to see the encrypted data, which is needed for the "SN00/cfw*" and "SN bits/cfw*" features to work properly.
+
+Step 1. Change the two occurances of ``decrypt & 0x0000FFFF`` to read ``decrypt``.
+Step 2. Change the printf at the bottom of the file...
+  - In particular, we use ``instance->generic.cnt & 0xFFFF`` instead of ``instance->generic.cnt``.
+  - We added ``"Enc:%04lX\r\n"`` to the end of the printf string.
+  - We added a final parameter ``instance->generic.cnt >> 16`` to the end of the printf.
+```c
+    furi_string_cat_printf(
+        output,
+        "%s %dbit\r\n"
+        "Key:%08lX%08lX\r\n"
+        "Fix:0x%08lX    Cnt:%04lX\r\n"
+        "Hop:0x%08lX    Btn:%01X\r\n"
+        "MF:%s\r\n"
+        "Sn:0x%07lX \r\n"
+        "Enc:%04lX\r\n",
+        instance->generic.protocol_name,
+        instance->generic.data_count_bit,
+        code_found_hi,
+        code_found_lo,
+        code_found_reverse_hi,
+        instance->generic.cnt & 0xFFFF,
+        code_found_reverse_lo,
+        instance->generic.btn,
+        instance->manufacture_name,
+        instance->generic.serial,
+        instance->generic.cnt >> 16);
+```
+
+## Installation
+- Connect your Flipper to your computer.
+- Close any programs that may be using your Flipper (putty, lab.flipper.net, qFlipper, etc.)
+
+Method 1: (easiest)
+- Load [flipc.org](https://flipc.org) using a web browser such as Chrome or Edge; which will show an "Install" button.
+- Select the channel and firmware that you are running on your Flipper Zero.
+- Click the Install button.
+- The application will appear under "Apps/subghz/rolling_flaws".
+
+Method 2: (command-line + allows for "SN00/cfw*" and "SN bits/cfw*" features + allows for replay feature)
+- Clone the firmware repository (make sure you use ``git clone --recursive``).
+- Copy the ``rolling_flaws`` folder into your firmware's ``applications_user`` folder.
+- Build the firmware using ``fbt updater_package``.
+
+Method 3: (VS Code + allows for "SN00/cfw*" and "SN bits/cfw*" features + allows for replay feature)
+- Install VS Code
+- Clone the firmware repository (make sure you use ``git clone --recursive``).
+- Make sure you have run ``fbt vscode_dist`` at least once, so VSCode works properly.
+- Copy the ``rolling_flaws`` folder into your firmware's ``applications_user`` folder.
+- Ctrl+Shift+B
+- Select "[Debug] Launch App on Flipper"
+
+Method 4: (command-line)
+- Install [ufbt](https://github.com/flipperdevices/flipperzero-ufbt)
+- Switch into the ``rolling_flaws`` folder.
+- Install and launch the app using ``ufbt launch``.
+
+## Menu Options
+### Config
+This is where you can configure the settings.  The settings are reset whenever the application restarts.
+
+### Reset count to 0
+This will reset the count to 0.  This is useful when you want to start over or want to test some rollback scenarios.
+
+### Transmit Signal
+This will transmit the signal.  This is useful to capture the next signal.
+
+### Receive Signals
+This will receive signals.  This is the primary purpose of the application.
+
+### Sync Remote
+This will sync the configuration using a remote signal.  This is useful when you want to pair the Flipper Zero to your remote.
+
+### About
+This will show information about the application.
+
+## Settings
+### Frequency
+Frequency is the frequency that the receiver and transmitter will use.  This should typically be set to 433.92 unless prohibited in your region.
+
+### Protocol
+- "KL (DH)" is the KeeLoq protocol with the manufacturer key from DoorHan.
+- "KL (All)" is the KeeLoq protocol with any manufacturer key.  So the Flipper will try all known keys.
+- "KL(Custom)" is set when doing a 'Sync Remote' operation.  The manufacture used during the sync will be used for comparisons.
+
+### Fix [SN+Btn]
+This is the button and serial number to decode.
+- 0x20000000 is considered a special test transmitter (right after a reset).
+- 0x284EE9D5 is used by many of the sample files from this project.
+- Custom is set when doing a 'Sync Remote' operation.
+
+### Replay attack
+If this is set to "yes" then it is possible to do a replay attack.  NOTE: The flipper has built in code that prevents it from receiving a duplicate signal, so you will need custom firmware if you want to receive the same code twice.
+
+### Window [next]
+This is how many counts forward from the existing count are considered acceptable.  For example, if the current count is 0x0001 and the window is 16, then the next count can be 0x0011.
+
+### Window [future]
+This is how many counts forward from the existing count are considered future.  For example, if the current count is 0x0001 and the window is 32768, then 0x5011 would be considered a future count, but 0xEC00 would be considered a past count.
+
+### Window [gap]
+This is how close two future counts need to be from each other for them to be considered within the gap.  When a second count is received and is within the gap, the next count will be advanced to the last count sent.
+
+### SN00/cfw*
+If this is set to "yes" then if the decoded data serial number bytes match 0x00, then any serial number will be considered a match.  If this is set to "no" then the serial number must match exactly.  For this feature to validate the serial number, you will need custom firmware.
+
+### SN bits/cfw*
+By default the firmware only checks 8 bits of the serial number.  If this is set to "10 (dec)" and you have custom firmware, then 10 bits from the decoded data will need to match the serial number.
+
+### Count 0 opens
+This will cause the receiver to open when it receives a count of 0.  This is a very bad idea, but I have multiple devices that implement this strategy.  They also implement "KL (All)", which means an Unknown MF with matching FIX can open the gate.  This is a very bad idea, but it is a real world example.
+
+## Tutorial
+This tutorial assumes you have two Flipper Zeros.  The first Flipper Zero will run this application and the second Flipper Zero will be used to send Sub-GHz signals.
+
+
+### Setup
+Flipper #1:
+- [Install](#installation) the "Rolling Flaws" application.
+- NOTE: If you want to use the "SN00/cfw*" or "SN bits/cfw*" features, or enable receiving the same code twice, you will need to make custom changes to your firmware so that the encrypted data can be accessed.  See the [Helpful hints](#helpful-hints) section above.
+
+Flipper #2:
+- By default, this project uses 433.92 MHz.  If 433.92 MHz is not supported in your country, you can use 315000000 or 390000000 instead.  In the .sub files replace "Frequency: 433920000" with the frequency you want to use.
+- Copy all of the .SUB files from this project onto your second Flipper Zero (in the ``SD Card\subghz`` folder).
+- NOTE: Official firmware will display "Error in protocol parameters description" when trying to send "Unknown" or "KGB/Subaru" signals.  For this Flipper Zero, it may be helpful to run an unofficial firmware that allows sending these signals.  Running unofficial firmware may not be legal in your region & may prevent you from getting support in the Official Flipper Zero forum.  I've also included Bin_RAW signals for these two MF, if you would like to stay on official firmware.
+
+
+### Matching Signals
+You need to make sure your Flipper Zero is using the same **frequency** as the remote.  You can use the "Frequency Analyzer" option in the "Sub-GHz" application to determine the frequency of the remote.  In some cases, you can also go to https://fccid.io and enter the FCC ID number and it will tell you the frequency being used.  In some cases you may need to add custom frequencies to the firmware. For this tutorial "433.92MHz" is the frequency we will be using (but you can change it to "315MHz" or "390MHz", as long as you also edit the .SUB files to have the matching frequency).
+
+You need to make sure your Flipper Zero is using the same **modulation** as the remote.  In some cases, you can go to https://fccid.io and enter the FCC ID number and it will tell you the modulation being used.  Otherwise, you can try each of the modulations until you find the one that works.  For this tutorial "AM650" is the modulation we will be using.
+
+### Scenario 1: clone RAW signal, replay attack on
+<img src="./docs/replay-attack-diagram.png" width="50%" />
+
+The first attack we will try is called a "Replay attack".  This is a very common attack to do with the Flipper Zero.  For static codes (codes that don't change every time they are sent) this approach works really well.  For dynamic codes, where the code changes each time it is sent, this attack will only work if the device has a replay attack flaw.
+
+WARNING: With some receivers, when receiver detects a Replay attack (the count didn't increment) it can stop responding to the remote. It may be necessary to take the receiver to an authorized dealer to get it reset.  This is rare, but it is something to be aware of, since it could be an expensive mistake.
+
+Flipper #1: **Enable Replay attack**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set "Replay attack" to "Yes".
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Freq Analyzer**
+- Launch "Sub-GHz" application.
+- Select "Frequency Analyzer" option.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show the frequency (if not: try again).
+
+Flipper #2: **RX signal, determine frequency**
+- Notice the frequency is the same as the one you set in the "Rolling Flaws" application.
+- We use this technique to determine the frequency of the remote.
+
+Flipper #2: **Read RAW record**
+- Press BACK button to return to Sub-GHz menu.
+- Select "Read RAW" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Set "RSSI Threshold" to "-75".
+- Leave all the rest of the settings default & click the BACK button.
+- Press OK button to start recording.
+
+Flipper #1: **Reset count, TX signal once**
+- Select "Reset count to 0".  This resets the rolling code.
+- Select "Transmit Signal" option once.
+  - Flipper #1 will increment the count (to 1).
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Stop recording**
+- Press OK button to stop recording.
+- You should see options for "Erase" (LEFT), "Send" (OK), and "Save" (RIGHT).
+
+Flipper #1: **Receive signal**
+- Select "Receive Signals" option.
+- Flipper #1 has "Count: 0001".
+- Flipper #1 has "Fix: 284EE9D5" (which means button=2 and serial number=0x84EE9D5).
+- Flipper #1 has "CLOSED" message.
+
+Flipper #2: **Send signal**
+- Press OK button to replay the signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has "Count: 0001".
+- Flipper #1 (bottom right) has reason as "REPLAY".
+
+Congratulations!  You have successfully cloned and replayed a signal.
+
+### Scenario 2: clone RAW signal, replay attack off
+<img src="./docs/replay-attack-failed-diagram.png" width="50%" />
+
+In the previous attack we successfully performed a "Replay attack".  This is because our dynamic codes receiver had a replay attack flaw.  Let's try the above steps again, but without the flaw.
+
+WARNING: With some receivers, when receiver detects a Replay attack (the count didn't increment) it can stop responding to the remote. It may be necessary to take the receiver to an authorized dealer to get it reset.  This is rare, but it is something to be aware of, since it could be an expensive mistake.
+
+Flipper #1: **Disable Replay attack**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Confirm "Replay attack" is set to "No".
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Freq Analyzer**
+- Launch "Sub-GHz" application.
+- Select "Frequency Analyzer" option.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show the frequency (if not: try again).
+
+Flipper #2: **RX signal, determine frequency**
+- Notice the frequency is the same as the one you set in the "Rolling Flaws" application.
+- We use this technique to determine the frequency of the remote.
+
+Flipper #2: **Read RAW record**
+- Press BACK button to return to Sub-GHz menu.
+- Select "Read RAW" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Set "RSSI Threshold" to "-75".
+- Leave all the rest of the settings default & click the BACK button.
+- Press OK button to start recording.
+
+Flipper #1: **Reset count, TX signal once**
+- Select "Reset count to 0".  This resets the rolling code.
+- Select "Transmit Signal" option once.
+  - Flipper #1 will increment the count (to 1).
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Stop recording**
+- Press OK button to stop recording.
+- You should see options for "Erase" (LEFT), "Send" (OK), and "Save" (RIGHT).
+
+Flipper #1: **Receive signal**
+- Select "Receive Signals" option.
+- Flipper #1 has "Count: 0001".
+- Flipper #1 has "Fix: 284EE9D5" (which means button=2 and serial number=0x84EE9D5).
+- Flipper #1 has "CLOSED" message.
+
+Flipper #2: **Send signal**
+- Press OK button to replay the signal.
+
+Sadly, this time the last step will be...
+Flipper #1: **Closed**
+- Flipper #1 has "CLOSED" message.
+- Flipper #1 has "Count: 0001".
+- Flipper #1 (bottom right) has reason as "PAST".
+
+### Scenario 3: pair remote, send next code
+<img src="./docs/pair-fz-remote.png" width="50%" />
+Sometimes you aren't trying to attack a device, you just want to use your Flipper Zero as a Universal Remote.  For this you will need to know the protocol that the remote is using.  You can use the "Sub-GHz -> Read" application to determine the protocol.  In this example, we will use a "DoorHan" remote.  Since you will pair the Flipper Zero as a new remote to your receiver, it should not impact the existing remotes and you don't have to worry about getting things out of sync.  
+
+NOTE: Some receivers have a limited number of remotes that can be paired, so you may want to check the manual to see if this is a concern for you.
+
+Flipper #1: **Set Frequency**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Read**
+- Launch "Sub-GHz" application.
+- Select "Read" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Determine protocol**
+- Notice the protocol is "Keeloq DoorHan".  We can click on this entry to see even more information.  This is how we determine what protocol the remote is using.  We can use this information to determine what protocol to use when we add the signal to our Flipper Zero.
+- Press BACK button to return to Sub-GHz menu.
+
+Flipper #2: **Add Manually**
+- Launch "Sub-GHz" application.
+- Select "Add Manually" option.
+- Select "DoorHan_433" option (or 315 if you are using 315MHz).
+- Enter a name for your signal (We will use "Dh433_man") and choose "Save".
+
+Flipper #2: **Emulate signal**
+(Same Flipper Zero as previous steps)
+- Select the file you just created (in the future, you can access this list under "Saved" option).
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Sync Remote**
+- Select "Sync Remote" option.
+- Flipper #1 has "WAITING FOR SIGNAL"
+
+Flipper #2: **Send signal**
+- Press OK button to send the signal.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Flipper #1 has "Fix" matching the remote.
+- Notice the current "Count" from the remote.
+- Flipper #1 (bottom right) has reason as "SYNCED"
+
+Flipper #2: **Send signal**
+- Press OK button to send the NEXT signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "NEXT".
+
+Congratulations!  You have successfully opened a gate by pairing the receiver to your Flipper and sending the next code in the sequence.  You can continue to press OK on Flipper #2 and the gate will continue to open and the "Count" will continue to increase!  
+
+### Scenario 4: clone remote, send next code
+<img src="./docs/clone-fz-remote.png" width="50%" />
+
+In this example, we will clone an existing remote.  This is a **"bad idea"** as you are likely to get the receiver out of sync with the original remote.  There is a high probability that the original remote will no longer work & using the original remote could cause the Flipper Zero remote to no longer work.  This is why it is better to use the "Pair Remote" option instead of the "Clone Remote" option.
+
+This scenario WILL most likely cause problems for you.  **You should only do this if you are pen-testing a device and you are willing to take the risk of getting the receiver out of sync.**
+
+WARNING: Your original remote will be considered as performing a "Replay attack" since it's codes will be in the past. With some receivers, when receiver detects a Replay attack (the count didn't increment on your original remote) it can stop responding to the remote. It may be necessary to take the receiver to an authorized dealer to get it reset.  This is rare, but it is something to be aware of, since it could be an expensive mistake.  More commonly, you will have to pair the originial remote one or more times (which may also require an authorized dealer).
+
+**The "Rolling Flaws" application has no consequences for getting the receiver out of sync, it was made to help you learn. Let me know if you think the application should punish you for getting the receiver out of sync -- for example, we could store a list of banned FIX values & no longer allow them to work until you enter a special code.**
+
+Flipper #1: **Set Frequency**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Read**
+- Launch "Sub-GHz" application.
+- Select "Read" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Determine protocol**
+- Notice the protocol is "Keeloq DoorHan".  
+- Click on the entry to see more information.
+- Write down the KEY value (16-character code, put spaces every two characters).
+  - For example "Key:AD045814AB977214" should be written as "AD 04 58 14 AB 97 72 14".
+
+Flipper #2: **Create a SUB file & emulate it**
+- If your firmware has a "Send" button, skip ahead to the next step.
+- **WARNING:** There is a reason why your firmware does not have a "Send" button.  This WILL most likely cause problems for you.  You should only do this if you are pen-testing a device and you are willing to take the risk of getting the receiver out of sync.
+- If your firmware does not have a "Send" button, you will need to create a .SUB file.
+- Copy the "k-unknown-sn84EE9D5-hop6A2C4803.sub" file to a new file "dh433_clone.sub".
+- Edit the "dh433_clone.sub" file and change the "Key" value to match the key you wrote down.
+- Copy the "dh433_clone.sub" file to your Flipper Zero (in the ``SD Card\subghz`` folder).
+- Press the BACK button as needed to return to the Sub-GHz menu.
+- Select "Saved" option.
+- Select the "dh433_clone.sub" file.
+- Select "Emulate" option.
+
+Flipper #2: **Add Manually**
+- Launch "Sub-GHz" application.
+- Select "Add Manually" option.
+- Select "DoorHan_433" option (or 315 if you are using 315MHz).
+- Enter a name for your signal (We will use "Dh433_man") and choose "Save".
+
+Flipper #2: **Emulate signal**
+(Same Flipper Zero as previous steps)
+- Select the file you just created (in the future, you can access this list under "Saved" option).
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Sync Remote**
+- Select "Sync Remote" option.
+- Flipper #1 has "WAITING FOR SIGNAL"
+
+Flipper #2: **Send signal**
+- Press OK button to send the signal.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Flipper #1 has "Fix" matching the remote.
+- Notice the current "Count" from the remote.
+- Flipper #1 (bottom right) has reason as "SYNCED"
+
+Flipper #2: **Send signal**
+- Press OK button to send the NEXT signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "NEXT".
+
+Congratulations!  You have successfully opened a gate by pairing the receiver to your Flipper and sending the next code in the sequence.  You can continue to press OK on Flipper #2 and the gate will continue to open and the "Count" will continue to increase! This cloning worked because the protocol was known by the Flipper and it contained the manufacturer keys.  If the protocol was unknown, the Flipper would not have been able to clone the remote. 
+
+### Scenario 5: skip ahead, within window-next
+<img src="./docs/window-next-attack.png" width="50%" />
+
+In this example, we somehow have a file with a matching FIX to our remote.  This file also happens to have a count that is only a little bit larger than the current count.  This is a very unlikely scenario, but it is possible.  This is why it is important to use a receiver that has a small "window-next" value.
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Notice the "Window [next]" setting is set to 16.  
+  - This means the next code must be within 16 codes of the current one.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Emulate signal**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- Select the file "k-dh-sn84EE9D5-cnt000B"
+  - This is KeeLoq with MF=DoorHan
+  - The Fix matches our remote.
+  - The current count should be 000B.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Notice the current "Count" from the remote, is "0000".
+
+Flipper #2: **Send signal**
+- Press OK button to send the NEXT signal.
+  - This should be a count of "000C".
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count" ("000C").
+- Flipper #1 (bottom right) has reason as "NEXT".
+
+Congratulations!  You have successfully opened a gate by skipping ahead to another code that is in the expected range.  We jumped ahead and now the count is "000C".  You can continue to press OK on Flipper #2 and the gate will continue to open and the "Count" will continue to increase!
+
+### Scenario 6: future attack
+<img src="./docs/window-future-attack.png" width="50%" />
+
+In this example, we somehow have a file with a matching FIX to our remote.  This file also happens to have a count that is quite a ways in the future (but less than the receiver's window-future value).  For this attack to work, we need to also have a signal containing a count right after it (or a small gap).  When we send those signals in sequence and if the count is less than the receivers window-future, when the receiver detects the second signal it will resyncronize the current count to the second signal.  In some cases it will open the door (and some will require a third signal to be sent, within the window-next range).  Most only require two signals to be sent, because to reduce the number of times the user has to press the button.
+
+If your receiver becomes out of sync with your remote (because you were pressing the remote too many times, so your count is too far in the future) then pressing the button 2-3 times may resync things; depending on the receiver firmware.  If your reciever becomes out of sync with your remote (because a cloned remote sent a future signal) then you would want to press the button many times on the remote (out of range of the receiver, so as not to do a replay/past attack) until the remote was in the window-next or window-future range.
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Notice the "Window [future]" setting is set to 32768 (0x8000 in hex)  
+  - This means the future code must be within 32768 codes of the current one.
+- Notice the "Window [gap]" setting is set to 2.
+  - This means after a future code, the next code must be within 2 codes of the future one.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Emulate signal**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- Select the file "k-dh-sn84EE9D5-cnt3E90"
+  - This is KeeLoq with MF=DoorHan
+  - The Fix matches our remote.
+  - The current count should be 3E90.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Notice the current "Count" from the remote, is "0000".
+
+Flipper #2: **Send future signal**
+- Press OK button to send the NEXT signal.
+  - This should be a count of "3E91".
+
+Flipper #1: **Opened!**
+- Flipper #1 has "Future" set to "3E91".
+- Flipper #1 has "Count" still set to "0000".
+- Flipper #1 has "CLOSED" message.
+- Flipper #1 (bottom right) has reason as "FUTURE".
+
+Flipper #2: **Send next future signal**
+- Press OK button to send the NEXT signal (gap is 1 from previous).
+  - This should be a count of "3E92".
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count" ("3E92").
+- Flipper #1 (bottom right) has reason as "GAP".
+
+Congratulations!  You have successfully opened a gate by skipping ahead to future code sequence.  You can continue to press OK on Flipper #2 and the gate will continue to open and the "Count" will continue to increase!  At this point, your old remote is way in the past and is no longer useful.
+
+### Scenario 7: rollback attack
+<img src="./docs/rollback-attack.png" width="50%" />
+
+This is very similar to a future attack, but instead of using codes from the future, we record and use codes from the past.  Typically this attack will only work if the Window [future] is set to "all".  What is most likely happening is the previous codes are considered as part of the far future, and when you play back two sequencial codes, it resyncs the remote to the second code.  This is a very rare attack, but it is possible on some receivers.
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency, future all**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set the "Window [future]" setting to "All".
+  - Effectively we are saying past codes are considered as part of the future.
+- Notice the "Window [gap]" setting is set to 2.
+  - This means after a future code, the next code must be within 2 codes of the future one.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Read RAW record**
+- Launch "Sub-GHz" application.
+- Select "Read RAW" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Set "RSSI Threshold" to "-75".
+- Leave all the rest of the settings default & click the BACK button.
+- Press OK button to start recording.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Stop & save recording**
+- Press OK button to stop recording.
+- You should see options for "Erase" (LEFT), "Send" (OK), and "Save" (RIGHT).
+- Select "Save" option.
+- Give signal a name (we will use "signal-1").
+
+Flipper #2: **Read RAW record**
+(Same Flipper Zero as previous step)
+- Select "New" option (press LEFT button).
+- Press OK button to start recording.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Stop & save recording**
+- Press OK button to stop recording.
+- You should see options for "Erase" (LEFT), "Send" (OK), and "Save" (RIGHT).
+- Select "Save" option.
+- Give signal a name (we will use "signal-2").
+
+Flipper #1: **TX signal**
+- NOTE: This is an optional step, we are doing this just to increase the count further & show normal usage. 
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+
+Flipper #2: **Emulate signal**
+- Press BACK button, until you are at Sub-GHz menu.
+- Select "Saved" option.
+- Select the file "signal-1"
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Notice the current "Count" from the remote.
+
+Flipper #2: **Send signal**
+- Press OK button to send the NEXT signal.
+- Notice: Flipper #1 should show "FUTURE" with some count in the past.
+
+Flipper #2: **Emulate and send next signal**
+- Press BACK button, until you are at Sub-GHz menu.
+- Select "Saved" option.
+- Select the file "signal-2"
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+- Press OK button to send the NEXT signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "GAP".
+
+Congratulations!  You have successfully opened a gate by replaying signals from back in time (that were considered future codes).  This attack worked using RAW signals, where we don't need to ability to decode the data, we just needed to record two open commands to play back later!
+
+### Scenario 8: rolljam attack
+<img src="./docs/rolljam-attack.png" width="50%" />
+
+The concept is when the first signal is sent from the remote, you somehow record the signal (narrow bandwidth) while preventing the receiver from getting the signal (perhaps interference near the receiver).  The user of the remote then sends the second signal, which you again somehow record the signal (narrow bandwidth) while preventing the receiver from getting the signal.  You then send the first signal without interference and the device will open, so user doesn't realize anything strange.  You still have a second signal that you can use to open the device.  Depending on other flaws, you may need to use the second signal before the next open signal (or before any signal, including "close/lock" signals). Samy Kamkar released videos on this attack years ago.
+
+https://www.fcc.gov/general/jammer-enforcement (illegal to jam signals in the US) so you should not practice this technique in the US.
+
+### Scenario 9: KGB/Subaru MF attack
+<img src="./docs/kgb-attack.png" width="50%" />
+
+The concept here is that perhaps the receiver knows how to decode many manufacturers, instead of just one.  If we send a FIX value that matches the expected value, but encode the count using a different manufacturer key, then perhaps it will open (you will typically combine this attack with a future attack, because you don't know what the count should be).  For universal receivers, there is a chance they forget to actually store the manufacturer associated with the remote but instead loop through all of the keys they know about.  For this demo, we use KGB/Subaru as the manufacturer, but you can use any manufacturer that the receiver knows about.
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency, KeeLoq all**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set the "Protocol" setting to "KL (All)".
+  - Effectively we are saying any MF Flipper Zero knows about is treated the same.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Freq Analyzer**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- Select the "k-dh-sn84EE9D5-cndEC01" file.
+- Select "Emulate" option.
+
+Flipper #1: **Sync Remote**
+- Select "Sync Remote" option.
+- Flipper #1 has "WAITING FOR SIGNAL"
+
+Flipper #2: **Send signal**
+- Press OK button to send the signal.
+- Flipper #1 should go back to the menu.
+
+Flipper #2: **Prepare to Read signal**
+(same Flipper as previous step)
+- Press BACK button to return to Sub-GHz menu.
+- Select "Read" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #1: **Send signal**
+- Select "Transmit Signal" option.
+- Flipper #1 will send the signal.
+- Flipper #1 should vibrate (but it will stay on same menu option).
+- Flipper #2 should show some signal get received.
+
+Flipper #2: **View signal details**
+- Press OK button to see details of the signal.
+- Notice the "Fix" value is "284EE9D5".  
+- Also the end of the Key is "AB977214" (Fix with bits from right to left)
+- This technique works even when the receiver doesn't know the manufacturer.  We now know what data "AB 97 72 14" is required to be at the end of our key.  We can then encrypt a count using a different manufacturer key.  This is how we can use a KGB key to open a DoorHan gate.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Notice the current "Count" from the remote, is "EC03".
+
+Flipper #2: **Emulate and send KGB signal**
+- Press BACK button, until you are at Sub-GHz menu.
+- Select "Saved" option.
+- Select the file "k-subaru-sn84E9D5-cntEC0D".  On some firmware, where non-DoorHan signals cannot be sent, you may be required to use "b-subaru-sn84E9D5-cntEC0D" instead; which is a Bin-RAW file.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+- Press OK button to send the NEXT signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "NEXT".
+
+Congratulations!  You have successfully opened a gate by using a different manufacturer key to encrypt the count.  This attack worked because the receiver didn't save the manufacturer of the remote, so it tried all of the keys it knew about.
+
+### Scenario 10: unknown MF attack
+<img src="./docs/unknown-mf-attack.png" width="50%" />
+
+The concept here is that when the receiver can't decode the manufacturer, it uses a count of 0.  For universal receivers, there is a chance they open when the count is 0.  For this demo, we use a "KeeLoq Unknown" as the manufacturer, but you can use any manufacturer that the receiver doesn't know about.
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency, KeeLoq (All)**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set the "Protocol" setting to "KL (All)".
+  - Effectively we are saying any MF Flipper Zero knows about is treated the same.
+  - This also will all the receiver to get a "KL Unknown" as the MF.
+- Set the "Count 0 opens" to "Yes".
+  - Effectively we are saying that a count of 0 will open the door.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Emulate signal**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- Select the "k-unknown-sn84EE9D5-hop6A2C4803" file. On some firmware, where non-DoorHan signals cannot be sent, you may be required to use "b-unknown-sn84EE9D5-hop6A2C4803" instead; which is a Bin-RAW file.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Transmit signal**
+- Select "Transmit Signal" option.
+- Flipper #1 will send the signal.
+- NOTE: We are just doing this step, so the count is not 0.
+
+Flipper #1: **Receive signals**
+(On the same Flipper Zero as the previous step.)
+- Select "Receive signals"
+- Notice the current "Count" from the remote, is "0001".
+
+Flipper #2: **Emulate and send Unknown signal**
+- Press OK button to send the NEXT signal.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "COUNT0".
+
+
+### Scenario 11: enc00 attack
+<img src="./docs/enc00-attack.png" width="50%" />
+
+The software implementation of KeeLoq used by the Flipper Zero treats a decoded serial number of "00" as a special case that matches ANY serial number.  This is interesting, because once we find a HOP code for a manufacturer that decodes to "00" for the SN and a Button value we want, we can replace the FIX code with any other SN. The FIX code (Button+SN) is sent in the clear for KeeLoq.
+
+For example, DoorHan with Key of "C0 00 0B D4 AB 97 72 14" decrypts to a count 3DC9 with a "00" SN.  The last digit of the key is 4 (0100) which written backwards is "0010" so button 2.  Now if someone transmits a button 2 DoorHan signal, say "AD 04 58 14 AB 97 72 14" we can replace their "AD 04 58 14" with "C0 00 0B D4", getting "C0 00 0B D4 AB 97 72 14" which will decrypt to a count of 3DC9 but using their serial number! We can then use Key "C0 02 8A 33" prefix to get a count of 3DCA with a "00" SN, which is the next code in the sequence.  Likely this would resync the receiver to our remote!  (NOTE: if "3DC9" was in the past, we would need to use a different KEY to jump to a different count, like count of A247 or FD75.)
+
+At this point the original remote would no longer work, since it was in the PAST.  If we jumped even further into the future, at some point it _might_ work when we press the remote button twice?
+
+**WARNING: Some receivers have a limit as to how many times you can cycle!**  The Overflow bits make it so that you can only cycle 0, 1 or 2 times.  "This can be done by programming OVR0 and OVR1 to 1s at the time of production. The encoder will automatically clear OVR0 the first time that the synchronization value wraps from 0xFFFF to 0x0000 and clear OVR1 the second time the counter wraps. Once cleared, OVR0 and OVR1 cannot be set again, thereby creating a permanent record of the counter overflow. This prevents fast cycling of 64K
+counter. If the decoder system is programmed to track the overflow bits, then the effective number of unique synchronization values can be extended to 196,608." -- [HCS300 datasheet](https://ww1.microchip.com/downloads/aemDocuments/documents/MCU08/ProductDocuments/DataSheets/21137G.pdf)
+
+Previous warnings still apply.
+
+Flipper #1: **Set Frequency, SN00/cfw**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set "SN00/cfw*" to "Yes" to allow for 00 to match any.  (If you don't have custom FW this is the behavior regardless of the setting.)
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Read**
+- Launch "Sub-GHz" application.
+- Select "Read" option.
+- Select "Config" option (LEFT button).
+- Set "Frequency" to match Flipper #1.
+- Set "Modulation" to "AM650".
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #1: **TX signal**
+- Make sure the two Flipper Zeros are at least 6" apart.
+- Select "Transmit Signal" option.
+  - Flipper #1 will send the signal.
+  - Flipper #1 should vibrate (but it will stay on same menu option).
+  - Flipper #2 should show some signal get received.
+
+Flipper #2: **Determine key (FIX:BTN+SN)**
+- Press OK to view the key.  In particular we are interested in the last 8 digits.
+- Press BACK button to return to Sub-GHz menu.
+
+Flipper #2: **Load ENC00 file**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- If needed edit the "k-dh-enc00-cnt3DC9" on your PC, replacing the last 8 digits of the Key with the last 8 digits from the previous step.  (The provided file Key ends with "AB 97 72 14" which matches the tutorials "sn84EE9D5" and "button2".)
+- Select "k-dh-enc00-cnt3DC9" file.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Flipper #1 has "Fix" matching the remote.
+- Notice the current "Count" is some small number.
+
+Flipper #2: **Send signal**
+- Press OK button to send the 3DC9 signal with a "00SN".
+
+Flipper #1: **Future**
+- Flipper #1 has "Future" set to "3DCA".
+- Flipper #1 has "Count" still set to the small number.
+
+Flipper #2: **Load next ENC00 file**
+- Press BACK to "Sub-GHz" menu.
+- Select "Saved" option.
+- If needed edit the "k-dh-enc00-cnt3DCA" on your PC, replacing the last 8 digits of the Key with the last 8 digits from the previous step.  (The provided file Key ends with "AB 97 72 14" which matches the tutorials "sn84EE9D5" and "button2".)
+- Select "k-dh-enc00-cnt3DCA" file.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #2: **Send signal**
+- Press OK button to send the 3DCA signal with a "00SN".
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "GAP".
+
+Congratulations!  You have successfully opened a gate by using a different serial number.  This attack worked because the receiver didn't check the serial number ending of the remote when the decoded serial number was "00".
+
+### Scenario 12: test transmitter
+<img src="./docs/test-attack.png" width="50%" />
+
+"The HCS512 decoder will automatically add a test transmitter each time an Erase All Function is done. A test transmitter is defined as a transmitter with a serial number of zero. After an Erase All, the test transmitter will always work without learning and will not check the
+synchronization counter of the transmitter. Learning of any new transmitters will erase the test transmitter." -- [HCS512 datasheet](https://www.microchip.com/content/dam/mchp/documents/MCU08/ProductDocuments/DataSheets/40151E.pdf)
+
+Flipper #1: **Set Frequency, SN0000000**
+- Exit and relaunch the "Rolling Flaws" application (resets all settings).
+- Select "Config" option.
+- Set "Frequency" to the frequency you want to use.
+- Set "Fix [Btn+SN]" to "0x20000000" to allow for test transmitter.
+- Leave all the rest of the settings default & click the BACK button.
+
+Flipper #2: **Load sn0000000 file**
+- Launch "Sub-GHz" application.
+- Select "Saved" option.
+- Select "k-dh-sn0000000" file.
+- Select "Emulate" option.
+- Flipper #2 should show "Send" (OK) option.
+
+Flipper #1: **Receive signals**
+- Select "Receive signals"
+- Notice the current "Count" is some small number.
+
+Flipper #2: **Send signal**
+- Press OK button to send the test signal with a SN of all zeros.
+
+Flipper #1: **Opened!**
+- Flipper #1 has "OPENED!" message.
+- Flipper #1 has new "Count".
+- Flipper #1 (bottom right) has reason as "TEST".
+
+## Contact info
+Rolling Flaws by [@CodeAllNight](https://twitter.com/codeallnight).
+- Discord invite: [https://discord.com/invite/NsjCvqwPAd](https://discord.com/invite/NsjCvqwPAd)
+- YouTube: [https://youtube.com/@MrDerekJamison](https://youtube.com/@MrDerekJamison)
+- GitHub: [https://github.com/jamisonderek/flipper-zero-tutorials/blob/main/subghz/apps/rolling-flaws](https://github.com/jamisonderek/flipper-zero-tutorials/blob/main/subghz/apps/rolling-flaws)
+- Support my work: [ko-fi.com/codeallnight](ko-fi.com/codeallnight)
+
+## Future features
+- Add GPIO feature to Flipper app so you can program an HCS301/HCS512 chip to have your own MF code and settings. This would help you secure your KeeLoq devices, since the MF code would only be known to you. 
+- Toggle a GPIO pin when Open is displayed. 
+- Send IR signal when Open is displayed. 
+- Make it "painful" to reset the device when it gets out of sync (so people understand getting things out of sync can end up costing money or time).
+- Additional support for more rolling-code protocols.
+- Porting the application to ESP32+CC1101, so it doesn't require a second Flipper Zero to use.

+ 13 - 0
non_catalog_apps/rolling_flaws/application.fam

@@ -0,0 +1,13 @@
+App(
+    appid="rolling_flaws",
+    name="Subghz Rolling Flaws",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="rolling_flaws_app",
+    requires=["gui", "subghz"],
+    stack_size=2 * 1024,
+    fap_version=(1, 4),
+    fap_icon="rolling_flaws.png",
+    fap_category="Sub-GHz",
+    fap_icon_assets="assets",
+    fap_description="Rolling code receiver (version 1.4), used to learn about rolling code flaws.  Watch video at https://youtu.be/gMnGuDC9EQo",
+)

BIN
non_catalog_apps/rolling_flaws/assets/Lock_10x8.png


BIN
non_catalog_apps/rolling_flaws/assets/Unlock_10x8.png


BIN
non_catalog_apps/rolling_flaws/docs/clone-fz-remote.png


BIN
non_catalog_apps/rolling_flaws/docs/enc00-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/keeloq-codes.png


BIN
non_catalog_apps/rolling_flaws/docs/kgb-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/pair-fz-remote.png


BIN
non_catalog_apps/rolling_flaws/docs/replay-attack-diagram.png


BIN
non_catalog_apps/rolling_flaws/docs/replay-attack-failed-diagram.png


BIN
non_catalog_apps/rolling_flaws/docs/rollback-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/rolljam-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/test-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/unknown-mf-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/window-future-attack.png


BIN
non_catalog_apps/rolling_flaws/docs/window-next-attack.png


+ 421 - 0
non_catalog_apps/rolling_flaws/rolling_flaws.c

@@ -0,0 +1,421 @@
+/*
+
+Wish list:
+
+1.  variable_item_set_current_value_text allows for large text, but
+it trucates it to X characters.  It would be nice it it scrolled.
+
+
+*/
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view.h>
+#include <gui/view_dispatcher.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/variable_item_list.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include "rolling_flaws_icons.h"
+
+#include <lib/subghz/receiver.h>
+#include <lib/subghz/protocols/protocol_items.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+#include <lib/subghz/devices/devices.h>
+
+#include "rolling_flaws_subghz_receive.h"
+#include "rolling_flaws_settings.h"
+#include "rolling_flaws_about.h"
+#include "rolling_flaws_keeloq.h"
+#include "rolling_flaws_send_keeloq.h"
+
+#ifdef TAG
+#undef TAG
+#endif
+#define TAG "RollingFlawsSubGHzApp"
+
+// Comment this line if you don't want the backlight to be continuously on.
+#define BACKLIGHT_ALWAYS_ON yes
+
+typedef enum {
+    RollingFlawsSubmenuIndexConfigure,
+    RollingFlawsSubmenuIndexResetCountToZero,
+    RollingFlawsSubmenuIndexTransmit,
+    RollingFlawsSubmenuIndexReceive,
+    RollingFlawsSubmenuIndexSyncRemote,
+    RollingFlawsSubmenuIndexAbout,
+} RollingFlawsSubmenuIndex;
+
+typedef enum {
+    RollingFlawsViewSubmenu,
+    RollingFlawsViewConfigure,
+    RollingFlawsViewReceiveSignals,
+    RollingFlawsViewReceiveSync,
+    RollingFlawsViewAbout,
+} RollingFlawsView;
+
+typedef enum {
+    RollingFlawsEventIdReceivedSignal,
+} RollingFlawsEventId;
+
+static bool decode_packet(FuriString* buffer, void* ctx) {
+    RollingFlaws* context = ctx;
+    if(furi_string_start_with_str(buffer, "KeeLoq 64bit")) {
+        if(!furi_string_start_with_str(
+               buffer, rolling_flaws_setting_protocol_base_name_get(context->model))) {
+            FURI_LOG_I(TAG, "KeeLoq 64bit protocol is not enabled");
+            return true;
+        }
+        decode_keeloq(context->model, buffer, false);
+    } else {
+        FURI_LOG_I(TAG, "Unknown protocol");
+    }
+
+    return true;
+}
+
+static bool sync_packet(FuriString* buffer, void* ctx) {
+    RollingFlaws* context = ctx;
+    if(furi_string_start_with_str(buffer, "KeeLoq 64bit")) {
+        if(!furi_string_start_with_str(
+               buffer, rolling_flaws_setting_protocol_base_name_get(context->model))) {
+            FURI_LOG_I(TAG, "KeeLoq 64bit protocol is not enabled");
+            return true;
+        }
+        decode_keeloq(context->model, buffer, true);
+        view_dispatcher_send_custom_event(
+            context->view_dispatcher, RollingFlawsEventIdReceivedSignal);
+        return false;
+    } else {
+        FURI_LOG_I(TAG, "Unknown protocol");
+    }
+
+    return true;
+}
+
+/**
+ * @brief      Callback for navigation events
+ * @details    This function is called when user press back button.  We return VIEW_NONE to
+ *            indicate that we want to exit the application.
+ * @param      context  The context
+ * @return     next view id
+*/
+uint32_t rolling_flaws_navigation_exit_callback(void* context) {
+    UNUSED(context);
+    return VIEW_NONE;
+}
+
+/**
+ * @brief      Callback for navigation events
+ * @details    This function is called when user press back button.  We return VIEW_NONE to
+ *            indicate that we want to exit the application.
+ * @param      context  The context
+ * @return     next view id
+*/
+uint32_t rolling_flaws_navigation_submenu_callback(void* context) {
+    UNUSED(context);
+
+    return RollingFlawsViewSubmenu;
+}
+
+/**
+ * @brief      Callback for navigation events
+ * @details    This function is called when user press back button.  We return VIEW_NONE to
+ *            indicate that we want to exit the application.
+ * @param      context  The context
+ * @return     next view id
+*/
+uint32_t rolling_flaws_navigation_submenu_stop_receiving_callback(void* context) {
+    RollingFlaws* app = (RollingFlaws*)context;
+    stop_listening(app->subghz);
+
+    return RollingFlawsViewSubmenu;
+}
+
+uint32_t rolling_flaws_navigation_submenu_stop_sync_callback(void* context) {
+    RollingFlaws* app = (RollingFlaws*)context;
+    stop_listening(app->subghz);
+
+    return RollingFlawsViewSubmenu;
+}
+
+bool rolling_flaws_view_dispatcher_custom_event_callback(void* context, uint32_t event) {
+    FURI_LOG_I(TAG, "Custom event received: %ld", event);
+    if(event == RollingFlawsEventIdReceivedSignal) {
+        RollingFlaws* app = (RollingFlaws*)context;
+        stop_listening(app->subghz);
+
+        furi_hal_vibro_on(true);
+        furi_delay_ms(200);
+        furi_hal_vibro_on(false);
+        furi_delay_ms(100);
+
+        furi_hal_vibro_on(true);
+        furi_delay_ms(100);
+        furi_hal_vibro_on(false);
+
+        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewSubmenu);
+        return true;
+    }
+
+    return false;
+}
+
+void rolling_flaws_submenu_callback(void* context, uint32_t index) {
+    RollingFlaws* app = (RollingFlaws*)context;
+
+    switch(index) {
+    case RollingFlawsSubmenuIndexConfigure:
+        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewConfigure);
+        break;
+    case RollingFlawsSubmenuIndexResetCountToZero:
+        app->model->count = 0x0;
+        app->model->future_count = 0xFFFFFFFF;
+        furi_hal_vibro_on(true);
+        furi_delay_ms(200);
+        furi_hal_vibro_on(false);
+        break;
+    case RollingFlawsSubmenuIndexTransmit:
+        app->model->count++;
+        app->model->future_count = 0xFFFFFFFF;
+        send_keeloq_count(
+            rolling_flaws_setting_fix_get(app->model),
+            app->model->count - 2,
+            rolling_flaws_setting_protocol_mf_name_get(app->model),
+            rolling_flaws_setting_frequency_get(app->model));
+        furi_hal_vibro_on(true);
+        furi_delay_ms(100);
+        furi_hal_vibro_on(false);
+        furi_delay_ms(100);
+        furi_hal_vibro_on(true);
+        furi_delay_ms(200);
+        furi_hal_vibro_on(false);
+        break;
+    case RollingFlawsSubmenuIndexReceive: {
+        uint32_t frequency = rolling_flaws_setting_frequency_get(app->model);
+        app->model->opened = false;
+        start_listening(app->subghz, frequency, decode_packet, app);
+        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewReceiveSignals);
+    } break;
+    case RollingFlawsSubmenuIndexSyncRemote: {
+        uint32_t frequency = rolling_flaws_setting_frequency_get(app->model);
+        start_listening(app->subghz, frequency, sync_packet, app);
+        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewReceiveSync);
+    } break;
+    case RollingFlawsSubmenuIndexAbout:
+        view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewAbout);
+        break;
+    default:
+        break;
+    }
+}
+
+void rolling_flaws_receive_sync_draw_callback(Canvas* canvas, void* model) {
+    UNUSED(model);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 13, 30, "Syncing rolling code:");
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 13, 45, "Press remote button now.");
+}
+
+void rolling_flaws_receive_signal_draw_callback(Canvas* canvas, void* model) {
+    RollingFlawsModel* my_model = ((RollingFlawsRefModel*)model)->model;
+
+    FuriString* str = furi_string_alloc(32);
+
+    canvas_set_bitmap_mode(canvas, 1);
+    canvas_set_font(canvas, FontPrimary);
+    canvas_draw_str(canvas, 13, 8, "Rolling code receiver");
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(str, "Count:   %04X", (uint16_t)my_model->count);
+    canvas_draw_str(canvas, 2, 34, furi_string_get_cstr(str));
+    canvas_set_font(canvas, FontSecondary);
+    if(my_model->future_count > 0xFFFF) {
+        canvas_draw_str(canvas, 2, 44, "Future:   none");
+    } else {
+        furi_string_printf(str, "Future:   %04X", (uint16_t)my_model->future_count);
+        canvas_draw_str(canvas, 2, 44, furi_string_get_cstr(str));
+    }
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 3, 20, rolling_flaws_setting_protocol_display_name_get(my_model));
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(str, "Fix: %08lX", rolling_flaws_setting_fix_get(my_model));
+    canvas_draw_str(canvas, 2, 54, furi_string_get_cstr(str));
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(str, "RX: %s", furi_string_get_cstr(my_model->key));
+    canvas_draw_str(canvas, 2, 64, furi_string_get_cstr(str));
+    if(my_model->opened) {
+        canvas_draw_icon(canvas, 100, 15, &I_Unlock_10x8);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 82, 33, "OPENED!");
+    } else {
+        canvas_draw_icon(canvas, 100, 15, &I_Lock_10x8);
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str(canvas, 85, 33, "CLOSED");
+    }
+
+    canvas_set_font(canvas, FontSecondary);
+    furi_string_printf(str, "%sMHz", rolling_flaws_setting_frequency_name_get(my_model));
+    canvas_draw_str(canvas, 75, 43, furi_string_get_cstr(str));
+
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str(canvas, 82, 54, furi_string_get_cstr(my_model->status));
+
+    furi_string_free(str);
+}
+
+bool rolling_flaws_view_input_callback(InputEvent* event, void* context) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+RollingFlaws* rolling_flaws_alloc() {
+    RollingFlaws* app = (RollingFlaws*)malloc(sizeof(RollingFlaws));
+
+    Gui* gui = furi_record_open(RECORD_GUI);
+
+    app->subghz = rolling_flaws_subghz_alloc();
+
+    app->model = malloc(sizeof(RollingFlawsModel));
+    app->model->key = furi_string_alloc();
+    app->model->custom_mf = furi_string_alloc();
+    app->model->status = furi_string_alloc();
+    app->model->custom_fix = 0x24321234;
+    app->model->count = 0x0;
+    app->model->future_count = 0xFFFFFFFF;
+    app->model->opened = false;
+
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(
+        app->view_dispatcher, rolling_flaws_view_dispatcher_custom_event_callback);
+
+    app->submenu = submenu_alloc();
+    submenu_add_item(
+        app->submenu,
+        "Config",
+        RollingFlawsSubmenuIndexConfigure,
+        rolling_flaws_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Reset count to 0",
+        RollingFlawsSubmenuIndexResetCountToZero,
+        rolling_flaws_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Transmit Signal",
+        RollingFlawsSubmenuIndexTransmit,
+        rolling_flaws_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Receive Signals",
+        RollingFlawsSubmenuIndexReceive,
+        rolling_flaws_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu,
+        "Sync Remote",
+        RollingFlawsSubmenuIndexSyncRemote,
+        rolling_flaws_submenu_callback,
+        app);
+    submenu_add_item(
+        app->submenu, "About", RollingFlawsSubmenuIndexAbout, rolling_flaws_submenu_callback, app);
+    view_set_previous_callback(
+        submenu_get_view(app->submenu), rolling_flaws_navigation_exit_callback);
+    view_dispatcher_add_view(
+        app->view_dispatcher, RollingFlawsViewSubmenu, submenu_get_view(app->submenu));
+    view_dispatcher_switch_to_view(app->view_dispatcher, RollingFlawsViewSubmenu);
+
+    rolling_flaw_populate_variable_item_list(app);
+    view_set_previous_callback(
+        variable_item_list_get_view(app->variable_item_list_config),
+        rolling_flaws_navigation_submenu_callback);
+    view_dispatcher_add_view(
+        app->view_dispatcher,
+        RollingFlawsViewConfigure,
+        variable_item_list_get_view(app->variable_item_list_config));
+
+    app->view_receive_signals = view_alloc();
+    view_set_context(app->view_receive_signals, app);
+    view_set_draw_callback(app->view_receive_signals, rolling_flaws_receive_signal_draw_callback);
+    view_set_input_callback(app->view_receive_signals, rolling_flaws_view_input_callback);
+    view_set_previous_callback(
+        app->view_receive_signals, rolling_flaws_navigation_submenu_stop_receiving_callback);
+    view_allocate_model(
+        app->view_receive_signals, ViewModelTypeLockFree, sizeof(RollingFlawsRefModel));
+    RollingFlawsRefModel* refmodel = view_get_model(app->view_receive_signals);
+    refmodel->model = app->model;
+    view_dispatcher_add_view(
+        app->view_dispatcher, RollingFlawsViewReceiveSignals, app->view_receive_signals);
+
+    app->view_receive_sync = view_alloc();
+    view_set_context(app->view_receive_sync, app);
+    view_set_draw_callback(app->view_receive_sync, rolling_flaws_receive_sync_draw_callback);
+    view_set_input_callback(app->view_receive_sync, rolling_flaws_view_input_callback);
+    view_set_previous_callback(
+        app->view_receive_sync, rolling_flaws_navigation_submenu_stop_sync_callback);
+    view_allocate_model(
+        app->view_receive_sync, ViewModelTypeLockFree, sizeof(RollingFlawsRefModel));
+    refmodel = view_get_model(app->view_receive_sync);
+    refmodel->model = app->model;
+    view_dispatcher_add_view(
+        app->view_dispatcher, RollingFlawsViewReceiveSync, app->view_receive_sync);
+
+    app->widget_about = widget_alloc();
+    widget_add_text_scroll_element(app->widget_about, 0, 0, 128, 64, ROLLING_FLAWS_ABOUT_TEXT);
+    view_set_previous_callback(
+        widget_get_view(app->widget_about), rolling_flaws_navigation_submenu_callback);
+    view_dispatcher_add_view(
+        app->view_dispatcher, RollingFlawsViewAbout, widget_get_view(app->widget_about));
+
+    app->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+#ifdef BACKLIGHT_ALWAYS_ON
+    notification_message(app->notifications, &sequence_display_backlight_enforce_on);
+#endif
+
+    return app;
+}
+
+void rolling_flaws_free(RollingFlaws* app) {
+#ifdef BACKLIGHT_ALWAYS_ON
+    notification_message(app->notifications, &sequence_display_backlight_enforce_auto);
+#endif
+    furi_record_close(RECORD_NOTIFICATION);
+
+    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewAbout);
+    widget_free(app->widget_about);
+    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewReceiveSignals);
+    view_free(app->view_receive_signals);
+    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewReceiveSync);
+    view_free(app->view_receive_sync);
+    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewConfigure);
+    variable_item_list_free(app->variable_item_list_config);
+    view_dispatcher_remove_view(app->view_dispatcher, RollingFlawsViewSubmenu);
+    submenu_free(app->submenu);
+    view_dispatcher_free(app->view_dispatcher);
+    furi_record_close(RECORD_GUI);
+
+    rolling_flaws_subghz_free(app->subghz);
+
+    free(app);
+}
+
+int32_t rolling_flaws_app(void* p) {
+    UNUSED(p);
+
+    RollingFlaws* app = rolling_flaws_alloc();
+    view_dispatcher_run(app->view_dispatcher);
+
+    rolling_flaws_free(app);
+    return 0;
+}

BIN
non_catalog_apps/rolling_flaws/rolling_flaws.png


+ 34 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_about.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#define ROLLING_FLAWS_ABOUT_TEXT                                \
+    "Rolling code receiver\n version 1.4\n"                     \
+    "---\n"                                                     \
+    "Practice rolling code attacks without risking a desync!\n" \
+    "This app is for educational\n"                             \
+    "purposes only.\n---\n"                                     \
+    "Protocol KeeLoq (DoorHan) is\n"                            \
+    "currently supported.  More\n"                              \
+    "protocols added in future.\n\n"                            \
+    ":::Config supported:::\n"                                  \
+    "Frequency: The\n frequency to TX/RX.\n"                    \
+    "Protocol: KL(DH) = KeeLoq\n MF=DoorHan.\n"                 \
+    " KL(All) = KeeLoq (any MF)\n"                              \
+    " KL(Custom) = KeeLoq\n"                                    \
+    " (specific MF) set when doing\n a 'Sync Remote'.\n"        \
+    "Fix [SN+Btn]: The SN+button to decode. (20000000 is\n"     \
+    " test decode).\n"                                          \
+    "Replay attack: Allow replay\n attacks.\n"                  \
+    "Window [next]: How many\n"                                 \
+    " counts forward are\n acceptable?\n"                       \
+    "Window [future]: How many\n"                               \
+    " counts forward are\n considered future?\n"                \
+    "Window [gap]: How far can\n"                               \
+    " two sequential future counts\n be apart?\n"               \
+    "SN00 attack: Allow decoded\n 00 to wildcard SN.\n"         \
+    "SN bits [cfw*]: Number of\n"                               \
+    " bits to compare. (custom fw\n only?)\n"                   \
+    "Count 0 opens: Count of 0 is\n treated as a match.\n"      \
+    "=========\n"                                               \
+    "author: @codeallnight\n"                                   \
+    "https://discord.com/invite/NsjCvqwPAd\n"                   \
+    "https://youtube.com/@MrDerekJamison"

+ 226 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_keeloq.c

@@ -0,0 +1,226 @@
+#include "rolling_flaws_keeloq.h"
+
+#include "rolling_flaws_utils.h"
+#include "rolling_flaws_settings.h"
+
+typedef struct {
+    uint32_t fix;
+    uint32_t hop;
+    uint32_t sn;
+    uint32_t btn;
+    uint32_t cnt;
+    uint32_t enc;
+    FuriString* mf;
+} KeeLoqData;
+
+KeeLoqData* keeloq_data_alloc() {
+    KeeLoqData* data = malloc(sizeof(KeeLoqData));
+    data->mf = furi_string_alloc();
+    return data;
+}
+
+void keeloq_data_free(KeeLoqData* data) {
+    furi_string_free(data->mf);
+    free(data);
+}
+
+static uint32_t get_forward_distance(uint32_t current_count, uint32_t new_count) {
+    uint32_t distance = 0;
+    if(new_count >= current_count) {
+        distance = new_count - current_count;
+    } else {
+        distance = (0xFFFF - current_count) + new_count;
+    }
+
+    return distance;
+}
+
+static bool is_open(RollingFlawsModel* model, KeeLoqData* data) {
+    bool any_mf = rolling_flaws_setting_protocol_mf_name_get(model)[0] == '*';
+    if(!any_mf &&
+       furi_string_cmp(data->mf, rolling_flaws_setting_protocol_mf_name_get(model)) != 0) {
+        FURI_LOG_I(
+            TAG,
+            "Wrong MF.  Expected >%s< but got >%s<",
+            rolling_flaws_setting_protocol_mf_name_get(model),
+            furi_string_get_cstr(data->mf));
+        furi_string_set(model->status, "BAD MF");
+        return false;
+    }
+
+    if(data->fix != rolling_flaws_setting_fix_get(model)) {
+        FURI_LOG_I(
+            TAG,
+            "Wrong fix.  Expected >%08lX< but got >%08lX<",
+            rolling_flaws_setting_fix_get(model),
+            data->fix);
+        furi_string_set(model->status, "BAD FIX");
+        return false;
+    }
+
+    if((rolling_flaws_setting_fix_get(model) & 0xFFFFFFF) == 0) {
+        FURI_LOG_I(TAG, "Fix is test. Not checking data.");
+        furi_string_set(model->status, "TEST");
+        model->future_count = 0xFFFFFFFF;
+        model->count = data->cnt;
+        return true;
+    }
+
+    if(data->enc != FAILED_TO_PARSE) {
+        FURI_LOG_I(TAG, "Encrypted payload is %08lX", data->enc);
+
+        if(!rolling_flaws_setting_sn_zero_get(model)) {
+            FURI_LOG_I(TAG, "SN wildcard by 00 disabled.");
+            if((data->fix & 0xFF) != 0) {
+                FURI_LOG_I(TAG, "SN does not end in 00, validating enc %08lX.", data->enc);
+
+                if((data->enc & 0xFF) == 0) {
+                    FURI_LOG_I(TAG, "Encrypted payload SN is zero.");
+                    furi_string_set(model->status, "SN 00");
+                    return false;
+                }
+            }
+        }
+
+        uint8_t match_bits = rolling_flaws_setting_sn_bits_get(model);
+        if(match_bits != 0) {
+            uint32_t mask = 0xFFFFFFFF;
+            mask = mask >> (32 - match_bits);
+            uint32_t fix_sn = data->fix & mask;
+            uint32_t enc_sn = data->enc & mask;
+            if(fix_sn != enc_sn) {
+                FURI_LOG_I(TAG, "SN does not match.  Fix: %08lX Enc: %08lX", fix_sn, enc_sn);
+                furi_string_set(model->status, "BAD SN");
+                return false;
+            } else {
+                FURI_LOG_I(TAG, "SN matches.  Fix: %08lX Enc: %08lX", fix_sn, enc_sn);
+            }
+        }
+    }
+
+    uint32_t distance = get_forward_distance(model->count, data->cnt);
+    FURI_LOG_I(TAG, "Distance: %08lX", distance);
+    if(distance == 0 && rolling_flaws_setting_replay_get(model)) {
+        FURI_LOG_I(TAG, "Replay attack detected");
+        furi_string_set(model->status, "REPLAY");
+        model->future_count = 0xFFFFFFFF;
+        model->count = data->cnt;
+        return true;
+    }
+
+    if(rolling_flaws_setting_count_zero_get(model) && data->cnt == 0) {
+        FURI_LOG_I(TAG, "Count zero allowed.");
+        furi_string_set(model->status, "COUNT0");
+        model->future_count = 0xFFFFFFFF;
+        // We don't reset count in this case.
+        return true;
+    }
+
+    if(distance == 0) {
+        distance = 0x10000;
+    }
+
+    if(distance <= rolling_flaws_setting_window_next_get(model)) {
+        FURI_LOG_I(TAG, "Within next window");
+        furi_string_set(model->status, "NEXT");
+        model->future_count = 0xFFFFFFFF;
+        model->count = data->cnt;
+        return true;
+    }
+
+    if(distance <= rolling_flaws_setting_window_future_get(model)) {
+        FURI_LOG_I(TAG, "Within future window");
+
+        if(model->future_count > 0xFFFF) {
+            FURI_LOG_I(TAG, "Set future value to %08lX.", data->cnt);
+            furi_string_set(model->status, "FUTURE");
+            model->future_count = data->cnt;
+            return false;
+        }
+
+        uint32_t future_gap = get_forward_distance(model->future_count, data->cnt);
+        if(future_gap > 0 && future_gap <= rolling_flaws_setting_window_future_gap_get(model)) {
+            FURI_LOG_I(TAG, "Future gap accepted. Gap is %08lX", future_gap);
+            furi_string_set(model->status, "GAP");
+            model->future_count = 0xFFFFFFFF;
+            model->count = data->cnt;
+            return true;
+        }
+
+        if(future_gap == 0) {
+            FURI_LOG_I(TAG, "Future gap is zero.  Set future value to %08lX.", data->cnt);
+            furi_string_set(model->status, "FUTURE");
+            model->future_count = data->cnt;
+            return false;
+        }
+
+        FURI_LOG_I(
+            TAG,
+            "Future gap too large.  %08lX > %08lX",
+            future_gap,
+            rolling_flaws_setting_window_future_gap_get(model));
+        furi_string_set(model->status, "BAD GAP");
+        model->future_count = data->cnt;
+        return false;
+    }
+
+    FURI_LOG_I(TAG, "Signal must be from the past (non-future).");
+    furi_string_set(model->status, "PAST");
+    return false;
+}
+
+uint32_t last_decode = 0;
+void decode_keeloq(RollingFlawsModel* model, FuriString* buffer, bool sync) {
+    FURI_LOG_T(TAG, "Decoding KeeLoq 64bit");
+    uint32_t now = furi_get_tick();
+    if(now - last_decode < furi_ms_to_ticks(500)) {
+        FURI_LOG_D(TAG, "Ignoring decode.  Too soon.");
+        last_decode = now;
+        return;
+    }
+    last_decode = now;
+
+    KeeLoqData* data = keeloq_data_alloc();
+    __furi_string_extract_string_until(buffer, 0, "MF:", '\r', data->mf);
+    __furi_string_extract_string(buffer, 0, "Key:", '\r', model->key);
+
+    data->fix = __furi_string_extract_int(buffer, "Fix:0x", ' ', FAILED_TO_PARSE);
+    data->hop = __furi_string_extract_int(buffer, "Hop:0x", ' ', FAILED_TO_PARSE);
+    data->sn = __furi_string_extract_int(buffer, "Sn:0x", ' ', FAILED_TO_PARSE);
+    if(data->sn == FAILED_TO_PARSE) {
+        FURI_LOG_I(TAG, "Sn:0x not found.  Using Fix data.");
+        data->sn = data->fix & 0x0FFFFFFF;
+    }
+    data->btn = __furi_string_extract_int(buffer, "Btn:", '\r', FAILED_TO_PARSE);
+    data->cnt = __furi_string_extract_int(buffer, "Cnt:", '\r', FAILED_TO_PARSE);
+    // NOTE: "Enc:" needs to be added to "keeloq.c" subghz_protocol_decoder_keeloq_get_string() method.
+    data->enc = __furi_string_extract_int(buffer, "Enc:", '\r', FAILED_TO_PARSE);
+    FURI_LOG_I(
+        TAG,
+        "fix: %08lX hop: %08lX sn: %08lX btn: %08lX cnt: %08lX enc:%08lX key:%s mf:%s",
+        data->fix,
+        data->hop,
+        data->sn,
+        data->btn,
+        data->cnt,
+        data->enc,
+        furi_string_get_cstr(model->key),
+        furi_string_get_cstr(data->mf));
+
+    if(!sync) {
+        model->opened = is_open(model, data);
+        if(model->opened) {
+            model->count = data->cnt;
+        }
+        __gui_redraw();
+    } else {
+        model->custom_fix = data->fix;
+        model->count = data->cnt;
+        model->future_count = 0xFFFFFFFF;
+        model->opened = false;
+        rolling_flaws_setting_protocol_custom_mf_set(model, data->mf);
+        furi_string_set(model->status, "SYNCED");
+    }
+
+    keeloq_data_free(data);
+}

+ 7 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_keeloq.h

@@ -0,0 +1,7 @@
+#pragma once
+
+#include "rolling_flaws_structs.h"
+
+#define FAILED_TO_PARSE 0x0BADC0DE
+
+void decode_keeloq(RollingFlawsModel* model, FuriString* buffer, bool sync);

+ 125 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_send_keeloq.c

@@ -0,0 +1,125 @@
+#include "rolling_flaws_send_keeloq.h"
+
+#include <lib/subghz/transmitter.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+#include <lib/subghz/protocols/protocol_items.h>
+#include <lib/subghz/devices/devices.h>
+
+#ifdef TAG
+#undef TAG
+#endif
+#define TAG "RollingFlawsSendKeeloq"
+
+static SubGhzEnvironment* load_environment() {
+    SubGhzEnvironment* environment = subghz_environment_alloc();
+    subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME);
+    subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME);
+    subghz_environment_set_came_atomo_rainbow_table_file_name(
+        environment, SUBGHZ_CAME_ATOMO_DIR_NAME);
+    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
+        environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
+    subghz_environment_set_nice_flor_s_rainbow_table_file_name(
+        environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
+    subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
+    return environment;
+}
+
+static void send_keeloq(
+    uint32_t frequency,
+    uint32_t serial,
+    uint8_t btn,
+    uint16_t cnt,
+    const char* name_sysmem) {
+    if(!furi_hal_region_is_frequency_allowed(frequency)) {
+        // TODO: Show friendly UI message if frequency is not allowed.
+        FURI_LOG_E(TAG, "Frequency %lu is not allowed in this region.", frequency);
+        return;
+    }
+
+    FURI_LOG_I(TAG, "Sending signal on frequency %lu", frequency);
+
+    // Populate the CC101 device list.
+    subghz_devices_init();
+
+    // Get the internal radio device.
+    const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+
+    // Get the Princeton SubGhzTransmitter (for decoding our file format).
+    SubGhzEnvironment* environment = load_environment();
+    subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
+    SubGhzTransmitter* transmitter =
+        subghz_transmitter_alloc_init(environment, SUBGHZ_PROTOCOL_KEELOQ_NAME);
+
+    // Load the payload we want to send into flipper_format.
+    FlipperFormat* flipper_format = flipper_format_string_alloc();
+
+    SubGhzRadioPreset* preset = malloc(sizeof(SubGhzRadioPreset));
+    preset->frequency = frequency;
+    preset->name = furi_string_alloc();
+    furi_string_set(preset->name, "AM650");
+    preset->data = NULL;
+    preset->data_size = 0;
+
+    SubGhzProtocolEncoderBase* encoder = subghz_transmitter_get_protocol_instance(transmitter);
+    // sadly, in some firmware this has a Repeat of 100, which is too much for our purposes.
+
+    subghz_protocol_keeloq_create_data(
+        encoder, flipper_format, serial, btn, cnt, name_sysmem, preset);
+
+    // Fill out the SubGhzProtocolDecoderPrinceton (which includes SubGhzBlockGeneric data) in our transmitter based on parsing flipper_format.
+    // initance->encoder.upload[] gets filled out with duration and level information (You can think of this as the RAW data).
+    SubGhzProtocolStatus status = subghz_transmitter_deserialize(transmitter, flipper_format);
+    furi_assert(status == SubGhzProtocolStatusOk);
+
+    // Currently unused for internal radio, but good idea to still invoke it.
+    subghz_devices_begin(device);
+
+    // Initializes the CC1101 SPI bus
+    subghz_devices_reset(device);
+
+    // Use one of the presets in subghz_device_cc1101_int_interconnect_load_preset.  If the first argument is FuriHalSubGhzPresetCustom, then the second argument is
+    // a custom register table (Reg, value, reg, value, ...,0, 0, PATable [0..7] entries).
+    subghz_devices_load_preset(device, FuriHalSubGhzPresetOok650Async, NULL);
+
+    // Set the frequency, RF switch path (band), calibrates the oscillator on the CC1101.
+    frequency = subghz_devices_set_frequency(device, frequency);
+
+    // Stop charging the battery while transmitting.
+    furi_hal_power_suppress_charge_enter();
+
+    // Start transmitting (keeps the DMA buffer filled with the encoder.upload[] data)
+    if(subghz_devices_start_async_tx(device, subghz_transmitter_yield, transmitter)) {
+        int max_counter = 10;
+
+        // Wait for the transmission to complete, or counter to expire (about 1 second).
+        while(max_counter-- && !(subghz_devices_is_async_complete_tx(device))) {
+            furi_delay_ms(100);
+        }
+
+        // Stop transmitting, debug log (tag="FuriHalSubGhz") the duty cycle information.
+        subghz_devices_stop_async_tx(device);
+    }
+
+    // clean up and shutdown cc1101
+    subghz_devices_sleep(device);
+
+    // also does a shutdown of cc1101
+    subghz_devices_end(device);
+
+    // remove the devices from the registry
+    subghz_devices_deinit();
+
+    // Allow the battery to charge again.
+    furi_hal_power_suppress_charge_exit();
+
+    // Free resources we allocated.
+    flipper_format_free(flipper_format);
+    subghz_transmitter_free(transmitter);
+    subghz_environment_free(environment);
+}
+
+void send_keeloq_count(uint32_t fix, uint32_t count, const char* name, uint32_t frequency) {
+    uint32_t serial = fix & 0x0FFFFFFF;
+    uint8_t btn = fix >> 28;
+    send_keeloq(frequency, serial, btn, count, name);
+}

+ 5 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_send_keeloq.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include <furi.h>
+
+void send_keeloq_count(uint32_t fix, uint32_t count, const char* name, uint32_t frequency);

+ 285 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_settings.c

@@ -0,0 +1,285 @@
+#include "rolling_flaws_settings.h"
+
+void rolling_flaws_setting_change(VariableItem *item, char **names, uint8_t *new_index)
+{
+    uint8_t index = variable_item_get_current_value_index(item);
+    variable_item_set_current_value_text(item, names[index]);
+    *new_index = index;
+}
+
+uint32_t setting_frequency_values[] = {315000000, 390000000, 433920000};
+char *setting_frequency_names[] = {"315.00", "390.00", "433.92"};
+void rolling_flaws_setting_frequency_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_frequency_names, &app->model->frequency_index);
+}
+char *rolling_flaws_setting_frequency_name_get(RollingFlawsModel *model)
+{
+    return setting_frequency_names[model->frequency_index];
+}
+uint32_t rolling_flaws_setting_frequency_get(RollingFlawsModel *model)
+{
+    return setting_frequency_values[model->frequency_index];
+}
+
+uint32_t setting_fix_values[] = {0x20000000, 0x201EA8D8, 0x284EE9D5, 0xCAFECAFE};
+char *setting_fix_names[] = {"0x20000000", "0x201EA8D8", "0x284EE9D5", "Custom"};
+void rolling_flaws_setting_fix_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_fix_names, &app->model->fix_index);
+}
+uint32_t rolling_flaws_setting_fix_get(RollingFlawsModel *model)
+{
+    if (model->fix_index == COUNT_OF(setting_fix_values) - 1)
+    {
+        return model->custom_fix;
+    }
+    return setting_fix_values[model->fix_index];
+}
+char *rolling_flaws_setting_fix_display_name_get(RollingFlawsModel *model)
+{
+    return setting_fix_names[model->fix_index];
+}
+
+char *setting_protocol_values_mf_name[] = {"DoorHan", "*", "Custom"};
+char *setting_protocol_values_base_name[] = {"KeeLoq 64bit", "KeeLoq 64bit", "KeeLoq 64bit"};
+char *setting_protocol_names[] = {"KL (DH)", "KL (All)", "KL (Custom)"};
+void rolling_flaws_setting_protocol_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_protocol_names, &app->model->protocol_index);
+}
+char *rolling_flaws_setting_protocol_base_name_get(RollingFlawsModel *model)
+{
+    return setting_protocol_values_base_name[model->protocol_index];
+}
+char *rolling_flaws_setting_protocol_display_name_get(RollingFlawsModel *model)
+{
+    return setting_protocol_names[model->protocol_index];
+}
+const char *rolling_flaws_setting_protocol_mf_name_get(RollingFlawsModel *model)
+{
+    if (model->protocol_index == COUNT_OF(setting_protocol_values_mf_name) - 1)
+    {
+        return furi_string_get_cstr(model->custom_mf);
+    }
+    return setting_protocol_values_mf_name[model->protocol_index];
+}
+void rolling_flaws_setting_protocol_custom_mf_set(RollingFlawsModel *model, FuriString *mf)
+{
+    model->protocol_index = COUNT_OF(setting_protocol_values_mf_name) - 1;
+    variable_item_set_current_value_index(model->variable_item_protocol, model->protocol_index);
+    variable_item_set_current_value_text(
+        model->variable_item_protocol, rolling_flaws_setting_protocol_display_name_get(model));
+
+    model->fix_index = COUNT_OF(setting_fix_values) - 1;
+    variable_item_set_current_value_index(model->variable_item_fix, model->fix_index);
+    variable_item_set_current_value_text(
+        model->variable_item_fix, rolling_flaws_setting_fix_display_name_get(model));
+
+    furi_string_set(model->custom_mf, furi_string_get_cstr(mf));
+}
+
+bool setting_replay_values[] = {false, true};
+char *setting_replay_names[] = {"No", "Yes"};
+void rolling_flaws_setting_replay_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_replay_names, &app->model->replay_index);
+}
+bool rolling_flaws_setting_replay_get(RollingFlawsModel *model)
+{
+    return setting_replay_values[model->replay_index];
+}
+
+/// @brief The window_next_values have precedence over past/future values.
+uint32_t setting_window_next_values[] = {4, 8, 16, 256, 16384, 32768, 65536};
+char *setting_window_next_names[] = {"4", "8", "16", "256", "16384", "32768", "All"};
+void rolling_flaws_setting_window_next_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_window_next_names, &app->model->window_next_index);
+}
+uint32_t rolling_flaws_setting_window_next_get(RollingFlawsModel *model)
+{
+    return setting_window_next_values[model->window_next_index];
+}
+
+uint32_t setting_window_future_values[] = {1, 8, 16, 256, 16384, 32768, 65536};
+char *setting_window_future_names[] = {"1", "8", "16", "256", "16384", "32768", "All"};
+void rolling_flaws_setting_window_future_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(
+        item, setting_window_future_names, &app->model->window_future_index);
+}
+uint32_t rolling_flaws_setting_window_future_get(RollingFlawsModel *model)
+{
+    return setting_window_future_values[model->window_future_index];
+}
+
+uint32_t setting_window_future_gap_values[] = {1, 2, 3, 4};
+char *setting_window_future_gap_names[] = {"1", "2", "3", "4"};
+void rolling_flaws_setting_window_future_gap_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(
+        item, setting_window_future_gap_names, &app->model->window_future_gap_index);
+}
+uint32_t rolling_flaws_setting_window_future_gap_get(RollingFlawsModel *model)
+{
+    return setting_window_future_gap_values[model->window_future_gap_index];
+}
+
+bool setting_sn_zero_values[] = {false, true};
+char *setting_sn_zero_names[] = {"No", "Yes"};
+void rolling_flaws_setting_sn_zero_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_sn_zero_names, &app->model->sn_zero_index);
+}
+bool rolling_flaws_setting_sn_zero_get(RollingFlawsModel *model)
+{
+    return setting_sn_zero_values[model->sn_zero_index];
+}
+
+// HCS300 uses 10 bits in discriminator, HCS200 uses 8 bits
+uint8_t setting_sn_bits_values[] = {8, 10};
+char *setting_sn_bits_names[] = {"8", "10 (dec)"};
+void rolling_flaws_setting_sn_bits_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_sn_bits_names, &app->model->sn_bits_index);
+}
+uint8_t rolling_flaws_setting_sn_bits_get(RollingFlawsModel *model)
+{
+    return setting_sn_bits_values[model->sn_bits_index];
+}
+
+bool setting_count_zero_values[] = {false, true};
+char *setting_count_zero_names[] = {"No", "Yes"};
+void rolling_flaws_setting_count_zero_change(VariableItem *item)
+{
+    RollingFlaws *app = variable_item_get_context(item);
+    rolling_flaws_setting_change(item, setting_sn_zero_names, &app->model->count_zero_index);
+}
+bool rolling_flaws_setting_count_zero_get(RollingFlawsModel *model)
+{
+    return setting_count_zero_values[model->count_zero_index];
+}
+
+void rolling_flaw_populate_variable_item_list(RollingFlaws *app)
+{
+    app->variable_item_list_config = variable_item_list_alloc();
+    variable_item_list_reset(app->variable_item_list_config);
+
+    VariableItem *item;
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Frequency",
+        COUNT_OF(setting_frequency_names),
+        rolling_flaws_setting_frequency_change,
+        app);
+    app->model->frequency_index = 2; // 433.92
+    variable_item_set_current_value_index(item, app->model->frequency_index);
+    variable_item_set_current_value_text(
+        item, setting_frequency_names[app->model->frequency_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Protocol",
+        COUNT_OF(setting_protocol_names),
+        rolling_flaws_setting_protocol_change,
+        app);
+    app->model->protocol_index = 0; // KeeLoq (DoorHan)
+    variable_item_set_current_value_index(item, app->model->protocol_index);
+    variable_item_set_current_value_text(item, setting_protocol_names[app->model->protocol_index]);
+    app->model->variable_item_protocol = item;
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Fix [Btn+SN]",
+        COUNT_OF(setting_fix_names),
+        rolling_flaws_setting_fix_change,
+        app);
+    app->model->fix_index = 2; // 0x284EE9D5
+    variable_item_set_current_value_index(item, app->model->fix_index);
+    variable_item_set_current_value_text(item, setting_fix_names[app->model->fix_index]);
+    app->model->variable_item_fix = item;
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Replay attack",
+        COUNT_OF(setting_replay_names),
+        rolling_flaws_setting_replay_change,
+        app);
+    app->model->replay_index = 0; // Disabled
+    variable_item_set_current_value_index(item, app->model->replay_index);
+    variable_item_set_current_value_text(item, setting_replay_names[app->model->replay_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Window [next]",
+        COUNT_OF(setting_window_next_names),
+        rolling_flaws_setting_window_next_change,
+        app);
+    app->model->window_next_index = 2; // 16 codes.
+    variable_item_set_current_value_index(item, app->model->window_next_index);
+    variable_item_set_current_value_text(
+        item, setting_window_next_names[app->model->window_next_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Window [future]",
+        COUNT_OF(setting_window_future_names),
+        rolling_flaws_setting_window_future_change,
+        app);
+    app->model->window_future_index = 5; // 32768 codes.
+    variable_item_set_current_value_index(item, app->model->window_future_index);
+    variable_item_set_current_value_text(
+        item, setting_window_future_names[app->model->window_future_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Window [gap]",
+        COUNT_OF(setting_window_future_gap_names),
+        rolling_flaws_setting_window_future_gap_change,
+        app);
+    app->model->window_future_gap_index = 1; // 2 codes.
+    variable_item_set_current_value_index(item, app->model->window_future_gap_index);
+    variable_item_set_current_value_text(
+        item, setting_window_future_gap_names[app->model->window_future_gap_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "SN00/cfw*",
+        COUNT_OF(setting_sn_zero_names),
+        rolling_flaws_setting_sn_zero_change,
+        app);
+    app->model->sn_zero_index = 0; // Disabled
+    variable_item_set_current_value_index(item, app->model->sn_zero_index);
+    variable_item_set_current_value_text(item, setting_sn_zero_names[app->model->sn_zero_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "SN bits/cfw*",
+        COUNT_OF(setting_sn_bits_names),
+        rolling_flaws_setting_sn_bits_change,
+        app);
+    app->model->sn_bits_index = 0; // 8-bits
+    variable_item_set_current_value_index(item, app->model->sn_bits_index);
+    variable_item_set_current_value_text(item, setting_sn_bits_names[app->model->sn_bits_index]);
+
+    item = variable_item_list_add(
+        app->variable_item_list_config,
+        "Count 0 opens",
+        COUNT_OF(setting_count_zero_names),
+        rolling_flaws_setting_count_zero_change,
+        app);
+    app->model->count_zero_index = 0; // No - count of 0 is not an open.
+    variable_item_set_current_value_index(item, app->model->count_zero_index);
+    variable_item_set_current_value_text(
+        item, setting_count_zero_names[app->model->count_zero_index]);
+}

+ 21 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_settings.h

@@ -0,0 +1,21 @@
+#pragma once
+
+#include <furi.h>
+#include "rolling_flaws_structs.h"
+
+uint32_t rolling_flaws_setting_frequency_get(RollingFlawsModel* model);
+char* rolling_flaws_setting_frequency_name_get(RollingFlawsModel* model);
+char* rolling_flaws_setting_protocol_base_name_get(RollingFlawsModel* model);
+char* rolling_flaws_setting_protocol_display_name_get(RollingFlawsModel* model);
+const char* rolling_flaws_setting_protocol_mf_name_get(RollingFlawsModel* model);
+void rolling_flaws_setting_protocol_custom_mf_set(RollingFlawsModel* model, FuriString* mf);
+bool rolling_flaws_setting_replay_get(RollingFlawsModel* model);
+uint32_t rolling_flaws_setting_window_next_get(RollingFlawsModel* model);
+uint32_t rolling_flaws_setting_window_future_get(RollingFlawsModel* model);
+uint32_t rolling_flaws_setting_window_future_gap_get(RollingFlawsModel* model);
+uint32_t rolling_flaws_setting_fix_get(RollingFlawsModel* model);
+bool rolling_flaws_setting_sn_zero_get(RollingFlawsModel* model);
+uint8_t rolling_flaws_setting_sn_bits_get(RollingFlawsModel* model);
+bool rolling_flaws_setting_count_zero_get(RollingFlawsModel* model);
+
+void rolling_flaw_populate_variable_item_list(RollingFlaws* app);

+ 53 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_structs.h

@@ -0,0 +1,53 @@
+#pragma once
+
+#include <furi.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/widget.h>
+#include <gui/modules/variable_item_list.h>
+#include <gui/view_dispatcher.h>
+#include <notification/notification.h>
+
+#include "rolling_flaws_subghz_receive.h"
+
+typedef struct {
+    uint8_t protocol_index;
+    FuriString* custom_mf;
+    uint8_t frequency_index;
+    uint8_t replay_index; // allow replay attack?
+    uint8_t window_next_index; // how many codes forward are acceptable?
+    uint8_t window_future_index; // how many codes forward are considered future?
+    uint8_t window_future_gap_index; // how far can two sequential future codes be?
+    uint8_t fix_index; // SN+button
+    uint32_t custom_fix; // Fix value if custom is selected.
+    uint8_t sn_zero_index; // allow decoded 00 to wildcard SN.
+    uint8_t sn_bits_index; // number of bits to compare. (custom fw only?)
+    uint8_t count_zero_index; // allow count of 0 to be considered an open.
+
+    uint32_t count; // 0 to 0xFFFF
+    uint32_t future_count; // 0 to 0xFFFF, use 0xFFFFFFFF if cleared.
+    bool opened;
+
+    FuriString* key;
+    FuriString* status;
+    VariableItem* variable_item_protocol;
+    VariableItem* variable_item_fix;
+} RollingFlawsModel;
+
+typedef struct {
+    RollingFlawsModel* model;
+} RollingFlawsRefModel;
+
+typedef struct {
+    NotificationApp* notifications;
+
+    ViewDispatcher* view_dispatcher;
+
+    Submenu* submenu;
+    VariableItemList* variable_item_list_config;
+    View* view_receive_signals;
+    View* view_receive_sync;
+    Widget* widget_about;
+
+    RollingFlawsModel* model;
+    RollingFlawsSubGhz* subghz;
+} RollingFlaws;

+ 140 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_subghz_receive.c

@@ -0,0 +1,140 @@
+#include "rolling_flaws_subghz_receive.h"
+
+static SubGhzEnvironment* load_environment() {
+    SubGhzEnvironment* environment = subghz_environment_alloc();
+    subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_NAME);
+    subghz_environment_load_keystore(environment, SUBGHZ_KEYSTORE_DIR_USER_NAME);
+    subghz_environment_set_came_atomo_rainbow_table_file_name(
+        environment, SUBGHZ_CAME_ATOMO_DIR_NAME);
+    subghz_environment_set_alutech_at_4n_rainbow_table_file_name(
+        environment, SUBGHZ_ALUTECH_AT_4N_DIR_NAME);
+    subghz_environment_set_nice_flor_s_rainbow_table_file_name(
+        environment, SUBGHZ_NICE_FLOR_S_DIR_NAME);
+    subghz_environment_set_protocol_registry(environment, (void*)&subghz_protocol_registry);
+    return environment;
+}
+
+RollingFlawsSubGhz* rolling_flaws_subghz_alloc() {
+    RollingFlawsSubGhz* subghz = malloc(sizeof(RollingFlawsSubGhz));
+    subghz->status = SUBGHZ_RECEIVER_UNINITIALIZED;
+    subghz->environment = load_environment();
+    subghz->stream = furi_stream_buffer_alloc(sizeof(LevelDuration) * 1024, sizeof(LevelDuration));
+    furi_check(subghz->stream);
+    subghz->overrun = false;
+    return subghz;
+}
+
+void rolling_flaws_subghz_free(RollingFlawsSubGhz* subghz) {
+    subghz_environment_free(subghz->environment);
+    furi_stream_buffer_free(subghz->stream);
+    free(subghz);
+}
+
+static void
+    rx_callback(SubGhzReceiver* receiver, SubGhzProtocolDecoderBase* decoder_base, void* cxt) {
+    RollingFlawsSubGhz* context = (RollingFlawsSubGhz*)cxt;
+    FuriString* buffer = furi_string_alloc();
+    subghz_protocol_decoder_base_get_string(decoder_base, buffer);
+    subghz_receiver_reset(receiver);
+    FURI_LOG_I(TAG, "PACKET:\r\n%s", furi_string_get_cstr(buffer));
+    if(context->callback) {
+        if(!context->callback(buffer, context->callback_context)) {
+            context->status = SUBGHZ_RECEIVER_SYNCHRONIZED;
+        }
+    }
+    furi_string_free(buffer);
+}
+
+static void rx_capture_callback(bool level, uint32_t duration, void* context) {
+    RollingFlawsSubGhz* instance = context;
+
+    LevelDuration level_duration = level_duration_make(level, duration);
+    if(instance->overrun) {
+        instance->overrun = false;
+        level_duration = level_duration_reset();
+    }
+    size_t ret =
+        furi_stream_buffer_send(instance->stream, &level_duration, sizeof(LevelDuration), 0);
+    if(sizeof(LevelDuration) != ret) {
+        instance->overrun = true;
+    }
+}
+
+static int32_t listen_rx(void* ctx) {
+    RollingFlawsSubGhz* context = (RollingFlawsSubGhz*)ctx;
+    context->status = SUBGHZ_RECEIVER_LISTENING;
+    LevelDuration level_duration;
+    FURI_LOG_I(TAG, "listen_rx started...");
+    while(context->status == SUBGHZ_RECEIVER_LISTENING) {
+        int ret = furi_stream_buffer_receive(
+            context->stream, &level_duration, sizeof(LevelDuration), 10);
+
+        if(ret == sizeof(LevelDuration)) {
+            if(level_duration_is_reset(level_duration)) {
+                subghz_receiver_reset(context->receiver);
+            } else {
+                bool level = level_duration_get_level(level_duration);
+                uint32_t duration = level_duration_get_duration(level_duration);
+                subghz_receiver_decode(context->receiver, level, duration);
+            }
+        }
+    }
+    FURI_LOG_I(TAG, "listen_rx exiting...");
+    context->status = SUBGHZ_RECEIVER_NOTLISTENING;
+    return 0;
+}
+
+void start_listening(
+    RollingFlawsSubGhz* context,
+    uint32_t frequency,
+    SubghzPacketCallback callback,
+    void* callback_context) {
+    context->status = SUBGHZ_RECEIVER_INITIALIZING;
+
+    context->callback = callback;
+    context->callback_context = callback_context;
+    subghz_devices_init();
+    const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    if(!subghz_devices_is_frequency_valid(device, frequency)) {
+        FURI_LOG_E(TAG, "Frequency not in range. %lu\r\n", frequency);
+        subghz_devices_deinit();
+        return;
+    }
+
+    context->receiver = subghz_receiver_alloc_init(context->environment);
+    subghz_receiver_set_filter(context->receiver, SubGhzProtocolFlag_Decodable);
+    subghz_receiver_set_rx_callback(context->receiver, rx_callback, context);
+
+    subghz_devices_begin(device);
+    subghz_devices_reset(device);
+    subghz_devices_load_preset(device, FuriHalSubGhzPresetOok650Async, NULL);
+    frequency = subghz_devices_set_frequency(device, frequency);
+
+    furi_hal_power_suppress_charge_enter();
+
+    subghz_devices_start_async_rx(device, rx_capture_callback, context);
+
+    FURI_LOG_I(TAG, "Listening at frequency: %lu\r\n", frequency);
+
+    context->thread = furi_thread_alloc_ex("RX", 1024, listen_rx, context);
+    furi_thread_start(context->thread);
+}
+
+void stop_listening(RollingFlawsSubGhz* context) {
+    if(context->status == SUBGHZ_RECEIVER_UNINITIALIZED) {
+        return;
+    }
+
+    context->status = SUBGHZ_RECEIVER_UNINITIALING;
+    FURI_LOG_D(TAG, "Stopping listening...");
+    furi_thread_join(context->thread);
+    furi_thread_free(context->thread);
+    furi_hal_power_suppress_charge_exit();
+
+    const SubGhzDevice* device = subghz_devices_get_by_name(SUBGHZ_DEVICE_CC1101_INT_NAME);
+    subghz_devices_stop_async_rx(device);
+
+    subghz_receiver_free(context->receiver);
+    subghz_devices_deinit();
+    context->status = SUBGHZ_RECEIVER_UNINITIALIZED;
+}

+ 48 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_subghz_receive.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#include <lib/subghz/receiver.h>
+#include <lib/subghz/protocols/protocol_items.h>
+#include <lib/subghz/devices/cc1101_int/cc1101_int_interconnect.h>
+#include <lib/subghz/devices/devices.h>
+
+#include "./rolling_flaws_utils.h"
+
+#ifdef TAG
+#undef TAG
+#endif
+#define TAG "RollingFlawsSubGHzReceive"
+
+typedef bool (*SubghzPacketCallback)(FuriString* buffer, void* context);
+
+typedef enum {
+    SUBGHZ_RECEIVER_INITIALIZING,
+    SUBGHZ_RECEIVER_LISTENING,
+    SUBGHZ_RECEIVER_SYNCHRONIZED,
+    SUBGHZ_RECEIVER_NOTLISTENING,
+    SUBGHZ_RECEIVER_UNINITIALING,
+    SUBGHZ_RECEIVER_UNINITIALIZED,
+} SubghzReceiverState;
+
+typedef struct {
+    SubGhzEnvironment* environment;
+    FuriStreamBuffer* stream;
+    FuriThread* thread;
+    SubGhzReceiver* receiver;
+    bool overrun;
+    SubghzReceiverState status;
+    SubghzPacketCallback callback;
+    void* callback_context;
+} RollingFlawsSubGhz;
+
+RollingFlawsSubGhz* rolling_flaws_subghz_alloc();
+void rolling_flaws_subghz_free(RollingFlawsSubGhz* subghz);
+
+void start_listening(
+    RollingFlawsSubGhz* context,
+    uint32_t frequency,
+    SubghzPacketCallback callback,
+    void* callback_context);
+void stop_listening(RollingFlawsSubGhz* context);

+ 110 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_utils.c

@@ -0,0 +1,110 @@
+#include "rolling_flaws_utils.h"
+#include <gui/gui.h>
+
+size_t __furi_string_extract_string(
+    FuriString* buffer,
+    size_t start_index,
+    char* text,
+    char delim,
+    FuriString* result) {
+    size_t len = strlen(text);
+    size_t valid_index = furi_string_size(buffer) - 1;
+    size_t field = furi_string_search_str(buffer, text, start_index) + len;
+    size_t term = -1;
+    if(field < valid_index) {
+        term = furi_string_search_char(buffer, delim, field);
+        if(term < valid_index) {
+            furi_string_reset(result);
+            furi_string_set_n(result, buffer, field, term - field);
+            FURI_LOG_I(TAG, "%s data is >>%s<<", text, furi_string_get_cstr(result));
+        } else {
+            FURI_LOG_E(TAG, "Failed to find terminator for >>%s<<", text);
+        }
+    } else {
+        FURI_LOG_E(TAG, "Failed to find >>%s<<", text);
+    }
+
+    return term;
+}
+
+size_t __furi_string_extract_string_until(
+    FuriString* buffer,
+    size_t start_index,
+    char* text,
+    char delim,
+    FuriString* result) {
+    size_t len = strlen(text);
+    size_t valid_index = furi_string_size(buffer) - 1;
+    size_t field = furi_string_search_str(buffer, text, start_index) + len;
+    size_t term = -1;
+    if(field < valid_index) {
+        term = furi_string_search_char(buffer, delim, field);
+        if(term < valid_index) {
+            furi_string_reset(result);
+            furi_string_set_n(result, buffer, field, term - field);
+            FURI_LOG_I(TAG, "%s data is >>%s<<", text, furi_string_get_cstr(result));
+        } else {
+            term = furi_string_size(buffer);
+            furi_string_reset(result);
+            furi_string_set_n(result, buffer, field, term - field);
+            FURI_LOG_E(TAG, "Failed to find terminator for >>%s<<, using end of string", text);
+            FURI_LOG_I(TAG, "%s data is >>%s<<", text, furi_string_get_cstr(result));
+        }
+    } else {
+        FURI_LOG_E(TAG, "Failed to find >>%s<<", text);
+    }
+
+    return term;
+}
+
+uint32_t
+    __furi_string_extract_int(FuriString* buffer, char* text, char delim, uint32_t default_value) {
+    uint32_t value = default_value;
+    size_t len = strlen(text);
+    size_t valid_index = furi_string_size(buffer) - 1;
+    size_t field = furi_string_search_str(buffer, text, 0) + len;
+    size_t term = -1;
+    FURI_LOG_I(TAG, "Extracting %s from field %d len is %d", text, field, len);
+    if(field < valid_index && len <= field) {
+        term = furi_string_search_char(buffer, delim, field);
+        if(term < valid_index) {
+            FuriString* result = furi_string_alloc();
+            furi_string_set_n(result, buffer, field, term - field);
+            value = __furi_string_hex_to_uint32(result);
+            FURI_LOG_D(TAG, "%s data is >>%s<<", text, furi_string_get_cstr(result));
+            furi_string_free(result);
+        } else {
+            FURI_LOG_E(TAG, "Failed to find terminator for >>%s<<", text);
+        }
+    } else {
+        FURI_LOG_E(TAG, "Failed to find >>%s<<", text);
+    }
+
+    return value;
+}
+
+uint32_t __furi_string_hex_to_uint32(FuriString* str) {
+    uint32_t result = 0;
+    for(size_t i = 0; i < furi_string_size(str); i++) {
+        char ch = furi_string_get_char(str, i);
+        result *= 16;
+        if(ch >= '0' && ch <= '9') {
+            result += ch - '0';
+        } else if(ch >= 'A' && ch <= 'F') {
+            result += ch - 'A' + 10;
+        } else if(ch >= 'a' && ch <= 'f') {
+            result += ch - 'a' + 10;
+        } else {
+            FURI_LOG_E(TAG, "Invalid hex character %c", ch);
+        }
+    }
+
+    return result;
+}
+
+void __gui_redraw() {
+    // Redraw screen
+    Gui* gui = furi_record_open(RECORD_GUI);
+    gui_direct_draw_acquire(gui);
+    gui_direct_draw_release(gui);
+}

+ 29 - 0
non_catalog_apps/rolling_flaws/rolling_flaws_utils.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <furi.h>
+
+#ifdef TAG
+#undef TAG
+#endif
+#define TAG "RollingFlawsUtils"
+
+size_t __furi_string_extract_string(
+    FuriString* buffer,
+    size_t start_index,
+    char* text,
+    char delim,
+    FuriString* result);
+
+size_t __furi_string_extract_string_until(
+    FuriString* buffer,
+    size_t start_index,
+    char* text,
+    char until_delim,
+    FuriString* result);
+
+uint32_t
+    __furi_string_extract_int(FuriString* buffer, char* text, char delim, uint32_t default_value);
+
+uint32_t __furi_string_hex_to_uint32(FuriString* str);
+
+void __gui_redraw();

+ 1 - 0
non_catalog_apps/sd_spi/.gitkeep

@@ -0,0 +1 @@
+

+ 674 - 0
non_catalog_apps/sd_spi/LICENSE

@@ -0,0 +1,674 @@
+                    GNU GENERAL PUBLIC LICENSE
+                       Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU General Public License is a free, copyleft license for
+software and other kinds of works.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+the GNU General Public License is intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.  We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors.  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights.  Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received.  You must make sure that they, too, receive
+or can get the source code.  And you must show them these terms so they
+know their rights.
+
+  Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+  For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software.  For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+  Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so.  This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software.  The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable.  Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products.  If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+  Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary.  To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Use with the GNU Affero General Public License.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU Affero General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+  Each version is given a distinguishing version number.  If the
+Program specifies that a certain numbered version of the GNU General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+    <program>  Copyright (C) <year>  <name of author>
+    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU GPL, see
+<https://www.gnu.org/licenses/>.
+
+  The GNU General Public License does not permit incorporating your program
+into proprietary programs.  If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.  But first, please read
+<https://www.gnu.org/licenses/why-not-lgpl.html>.

+ 33 - 0
non_catalog_apps/sd_spi/README.md

@@ -0,0 +1,33 @@
+# Flipperzero-SD-SPI
+Flipper Zero FAP for Lock and Unlock SD card / Micro SD card through SPI protocol (CMD42).
+
+<p align="center">
+<img src="SDSPI.gif" />
+</p>
+
+## Pinout ##
+
+Without Flipper Zero SDBoard the SD card it must be connected as in the table below
+
+Flipper Zero  | SD Card
+------------- | -------------
+9/3.3V  | +3.3V
+8/GND  | GND
+2/A7  | Mosi
+3/A6  | Miso
+4/A4  | CS
+5/B3  | SCK
+
+<p align="center">
+<img src="scheme.png" />
+</p>
+
+## Usage ##
+
+Whenever an sd card is connected it is required make a "Init", if the operation is successul in the "status" tab R1 is "NO ERROR" and it is possible execute other commands.
+
+"Lock" and "Unlock" work with password set in namesake tab.
+
+Force Erase allow the removal of unknown password from SD but erases all content.
+
+After the first save, the password is stored in apps_data/sdspi/pwd.txt. you can change it to use characters not found on the flipper keyboard.

+ 14 - 0
non_catalog_apps/sd_spi/application.fam

@@ -0,0 +1,14 @@
+App(
+    appid="sd_spi_app",  # Must be unique
+    name="SD SPI",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="sd_spi_app",
+    stack_size=2 * 1024,
+    fap_category="GPIO",
+    # Optional values
+    # fap_version=(0, 1),  # (major, minor)
+    fap_icon="sd_spi_app_10px.png",  # 10x10 1-bit PNG
+    fap_description="SD SPI Lock Management",
+    # fap_weburl="https://github.com/Gl1tchub/Flipperzero-SD-SPI",
+    # fap_icon_assets="images",  # Image assets to compile for this application
+)

+ 949 - 0
non_catalog_apps/sd_spi/sd_spi.c

@@ -0,0 +1,949 @@
+#include "sd_spi.h"
+// #include "sector_cache.h"
+#include <furi.h>
+#include <furi_hal.h>
+#include <furi/core/core_defines.h>
+
+#define SD_SPI_DEBUG 1
+#define TAG "SdSpi"
+
+#ifdef SD_SPI_DEBUG
+#define sd_spi_debug(...) FURI_LOG_I(TAG, __VA_ARGS__)
+#else
+#define sd_spi_debug(...)
+#endif
+
+#define SD_CMD_LENGTH 6
+#define SD_DUMMY_BYTE 0xFF
+#define SD_ANSWER_RETRY_COUNT 8
+#define SD_IDLE_RETRY_COUNT 100
+
+#define FLAG_SET(x, y) (((x) & (y)) == (y))
+
+static bool sd_high_capacity = false;
+SdSpiCmdAnswer cmd_answer = {
+    .r1 = SD_DUMMY_BYTE,
+    .r2 = SD_DUMMY_BYTE,
+    .r3 = SD_DUMMY_BYTE,
+    .r4 = SD_DUMMY_BYTE,
+    .r5 = SD_DUMMY_BYTE,
+};
+
+typedef enum {
+    SdSpiDataResponceOK = 0x05,
+    SdSpiDataResponceCRCError = 0x0B,
+    SdSpiDataResponceWriteError = 0x0D,
+    SdSpiDataResponceOtherError = 0xFF,
+} SdSpiDataResponce;
+
+typedef enum {
+    SdSpiCmdAnswerTypeR1,
+    SdSpiCmdAnswerTypeR1B,
+    SdSpiCmdAnswerTypeR2,
+    SdSpiCmdAnswerTypeR3,
+    SdSpiCmdAnswerTypeR4R5,
+    SdSpiCmdAnswerTypeR7,
+} SdSpiCmdAnswerType;
+
+typedef enum {
+    SD_CMD0_GO_IDLE_STATE = 0,
+    SD_CMD1_SEND_OP_COND = 1,
+    SD_CMD8_SEND_IF_COND = 8,
+    SD_CMD9_SEND_CSD = 9,
+    SD_CMD10_SEND_CID = 10,
+    SD_CMD12_STOP_TRANSMISSION = 12,
+    SD_CMD13_SEND_STATUS = 13,
+    SD_CMD16_SET_BLOCKLEN = 16,
+    SD_CMD17_READ_SINGLE_BLOCK = 17,
+    SD_CMD18_READ_MULT_BLOCK = 18,
+    SD_CMD23_SET_BLOCK_COUNT = 23,
+    SD_CMD24_WRITE_SINGLE_BLOCK = 24,
+    SD_CMD25_WRITE_MULT_BLOCK = 25,
+    SD_CMD27_PROG_CSD = 27,
+    SD_CMD28_SET_WRITE_PROT = 28,
+    SD_CMD29_CLR_WRITE_PROT = 29,
+    SD_CMD30_SEND_WRITE_PROT = 30,
+    SD_CMD32_SD_ERASE_GRP_START = 32,
+    SD_CMD33_SD_ERASE_GRP_END = 33,
+    SD_CMD34_UNTAG_SECTOR = 34,
+    SD_CMD35_ERASE_GRP_START = 35,
+    SD_CMD36_ERASE_GRP_END = 36,
+    SD_CMD37_UNTAG_ERASE_GROUP = 37,
+    SD_CMD38_ERASE = 38,
+    SD_CMD41_SD_APP_OP_COND = 41,
+    SD_CMD42_LOCK_UNLOCK = 42,
+    SD_CMD55_APP_CMD = 55,
+    SD_CMD58_READ_OCR = 58,
+} SdSpiCmd;
+
+/** Data tokens */
+typedef enum {
+    SD_TOKEN_START_DATA_SINGLE_BLOCK_READ = 0xFE,
+    SD_TOKEN_START_DATA_MULTIPLE_BLOCK_READ = 0xFE,
+    SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE = 0xFE,
+    SD_TOKEN_START_DATA_MULTIPLE_BLOCK_WRITE = 0xFC,
+    SD_TOKEN_STOP_DATA_MULTIPLE_BLOCK_WRITE = 0xFD,
+} SdSpiToken;
+
+
+static inline void sd_spi_select_card() {
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, false);
+    furi_delay_us(10); // Entry guard time for some SD cards
+}
+
+static inline void sd_spi_deselect_card() {
+    furi_delay_us(10); // Exit guard time for some SD cards
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->cs, true);
+}
+
+// void sd_bytes_debug(uint8_t* bytes, size_t size){
+//   char out[size];
+//   for(size_t i = 0; i < size; i++)
+//     snprintf(out+i*3, 255, "%02x ", bytes[i]);
+//   FURI_LOG_T(TAG, out);
+// }
+// void sd_byte_debug(uint8_t byte){
+//   char out[3];
+//   snprintf(out, 255, "%02x ", byte);
+//   FURI_LOG_T(TAG, out);
+// }
+
+static void sd_spi_bus_to_ground() {
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->miso,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->mosi,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->sck,
+        GpioModeOutputPushPull,
+        GpioPullNo,
+        GpioSpeedVeryHigh,
+        GpioAltFnUnused);
+
+    sd_spi_select_card();
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->miso, false);
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->mosi, false);
+    furi_hal_gpio_write(furi_hal_sd_spi_handle->sck, false);
+}
+
+static void sd_spi_bus_rise_up() {
+    sd_spi_deselect_card();
+
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->miso,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->mosi,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+    furi_hal_gpio_init_ex(
+        furi_hal_sd_spi_handle->sck,
+        GpioModeAltFunctionPushPull,
+        GpioPullUp,
+        GpioSpeedVeryHigh,
+        GpioAltFn5SPI2);
+}
+
+static inline uint8_t sd_spi_read_byte(void) {
+    uint8_t responce;
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, NULL, &responce, 1, SD_TIMEOUT_MS));
+    return responce;
+}
+
+static inline void sd_spi_write_byte(uint8_t data) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, &data, NULL, 1, SD_TIMEOUT_MS));
+}
+
+static inline uint8_t sd_spi_write_and_read_byte(uint8_t data) {
+    uint8_t responce;
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, &data, &responce, 1, SD_TIMEOUT_MS));
+    return responce;
+}
+
+static inline void sd_spi_write_bytes(uint8_t* data, uint32_t size) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, data, NULL, size, SD_TIMEOUT_MS));
+}
+
+static inline void sd_spi_read_bytes(uint8_t* data, uint32_t size) {
+    furi_check(furi_hal_spi_bus_trx(furi_hal_sd_spi_handle, NULL, data, size, SD_TIMEOUT_MS));
+}
+
+static inline void sd_spi_write_bytes_dma(uint8_t* data, uint32_t size) {
+
+    uint32_t timeout_mul = (size / 512) + 1;
+    furi_check(furi_hal_spi_bus_trx_dma(
+        furi_hal_sd_spi_handle, data, NULL, size, SD_TIMEOUT_MS * timeout_mul));
+}
+
+static inline void sd_spi_read_bytes_dma(uint8_t* data, uint32_t size) {
+    uint32_t timeout_mul = (size / 512) + 1;
+    furi_check(furi_hal_spi_bus_trx_dma(
+        furi_hal_sd_spi_handle, NULL, data, size, SD_TIMEOUT_MS * timeout_mul));
+}
+
+static uint8_t sd_spi_wait_for_data_and_read(void) {
+    uint8_t retry_count = SD_ANSWER_RETRY_COUNT;
+    uint8_t responce;
+
+    // Wait until we get a valid data
+    do {
+        responce = sd_spi_read_byte();
+        retry_count--;
+
+    } while((responce == SD_DUMMY_BYTE) && retry_count);
+
+    return responce;
+}
+
+static SdSpiStatus sd_spi_wait_for_data(uint8_t data, uint32_t timeout_ms) {
+    FuriHalCortexTimer timer = furi_hal_cortex_timer_get(timeout_ms * 1000);
+    uint8_t byte;
+
+    do {
+        byte = sd_spi_read_byte();
+        if(furi_hal_cortex_timer_is_expired(timer)) {
+            return SdSpiStatusTimeout;
+        }
+    } while((byte != data));
+
+    return SdSpiStatusOK;
+}
+
+static inline void sd_spi_deselect_card_and_purge() {
+    sd_spi_deselect_card();
+    sd_spi_read_byte();
+}
+
+static inline void sd_spi_purge_crc() {
+    sd_spi_read_byte();
+    sd_spi_read_byte();
+}
+
+static SdSpiCmdAnswer
+    sd_spi_send_cmd(SdSpiCmd cmd, uint32_t arg, uint8_t crc, SdSpiCmdAnswerType answer_type) {
+    uint8_t frame[SD_CMD_LENGTH];
+
+    cmd_answer.r1 = SD_DUMMY_BYTE;
+    cmd_answer.r2 = SD_DUMMY_BYTE;
+    cmd_answer.r3 = SD_DUMMY_BYTE;
+    cmd_answer.r4 = SD_DUMMY_BYTE;
+    cmd_answer.r5 = SD_DUMMY_BYTE;
+
+    frame[0] = ((uint8_t)cmd | 0x40);
+    frame[1] = (uint8_t)(arg >> 24);
+    frame[2] = (uint8_t)(arg >> 16);
+    frame[3] = (uint8_t)(arg >> 8);
+    frame[4] = (uint8_t)(arg);
+    frame[5] = (crc | 0x01);
+
+    sd_spi_select_card();
+    sd_spi_write_bytes(frame, sizeof(frame));
+
+    switch(answer_type) {
+    case SdSpiCmdAnswerTypeR1:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        break;
+    case SdSpiCmdAnswerTypeR1B:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+
+        // reassert card
+        sd_spi_deselect_card();
+        furi_delay_us(1000);
+        sd_spi_deselect_card();
+
+        // and wait for it to be ready
+        while(sd_spi_read_byte() != 0xFF) {
+        };
+
+        break;
+    case SdSpiCmdAnswerTypeR2:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        cmd_answer.r2 = sd_spi_read_byte();
+        break;
+    case SdSpiCmdAnswerTypeR3:
+    case SdSpiCmdAnswerTypeR7:
+        cmd_answer.r1 = sd_spi_wait_for_data_and_read();
+        cmd_answer.r2 = sd_spi_read_byte();
+        cmd_answer.r3 = sd_spi_read_byte();
+        cmd_answer.r4 = sd_spi_read_byte();
+        cmd_answer.r5 = sd_spi_read_byte();
+        break;
+    default:
+        break;
+    }
+    return cmd_answer;
+}
+
+static SdSpiDataResponce sd_spi_get_data_response(uint32_t timeout_ms) {
+    SdSpiDataResponce responce = sd_spi_read_byte();
+    // read busy response byte
+    sd_spi_read_byte();
+
+    switch(responce & 0x1F) {
+    case SdSpiDataResponceOK:
+        // TODO: check timings
+        sd_spi_deselect_card();
+        sd_spi_select_card();
+
+        // wait for 0xFF
+        if(sd_spi_wait_for_data(0xFF, timeout_ms) == SdSpiStatusOK) {
+            return SdSpiDataResponceOK;
+        } else {
+            return SdSpiDataResponceOtherError;
+        }
+    case SdSpiDataResponceCRCError:
+        return SdSpiDataResponceCRCError;
+    case SdSpiDataResponceWriteError:
+        return SdSpiDataResponceWriteError;
+    default:
+        return SdSpiDataResponceOtherError;
+    }
+}
+
+static SdSpiStatus sd_spi_init_spi_mode_v1(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count = 0;
+
+    sd_spi_debug("Init SD card in SPI mode v1");
+
+    do {
+        retry_count++;
+
+        // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+        sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        // ACMD41 (SD_APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+        response = sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            return SdSpiStatusError;
+        }
+    } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+
+    sd_spi_debug("Init SD card in SPI mode v1 done");
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_init_spi_mode_v2(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count = 0;
+
+    sd_spi_debug("Init SD card in SPI mode v2");
+
+    do {
+        retry_count++;
+        // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+        sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        // ACMD41 (APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+        response =
+            sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0x40000000, 0xFF, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            sd_spi_debug("ACMD41 failed");
+            return SdSpiStatusError;
+        }
+    } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+
+    if(FLAG_SET(response.r1, SdSpi_R1_ILLEGAL_COMMAND)) {
+        sd_spi_debug("ACMD41 is illegal command");
+        retry_count = 0;
+        do {
+            retry_count++;
+            // CMD55 (APP_CMD) before any ACMD command: R1 response (0x00: no errors)
+            response = sd_spi_send_cmd(SD_CMD55_APP_CMD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+            sd_spi_deselect_card_and_purge();
+
+            if(response.r1 != SdSpi_R1_IN_IDLE_STATE) {
+                sd_spi_debug("CMD55 failed");
+                return SdSpiStatusError;
+            }
+            // ACMD41 (SD_APP_OP_COND) to initialize SDHC or SDXC cards: R1 response (0x00: no errors)
+            response = sd_spi_send_cmd(SD_CMD41_SD_APP_OP_COND, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+            sd_spi_deselect_card_and_purge();
+
+            if(retry_count >= SD_IDLE_RETRY_COUNT) {
+                sd_spi_debug("ACMD41 failed");
+                return SdSpiStatusError;
+            }
+        } while(response.r1 == SdSpi_R1_IN_IDLE_STATE);
+    }
+
+    sd_spi_debug("Init SD card in SPI mode v2 done");
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_init_spi_mode(void) {
+    SdSpiCmdAnswer response;
+    uint8_t retry_count;
+
+    // CMD0 (GO_IDLE_STATE) to put SD in SPI mode and
+    // wait for In Idle State Response (R1 Format) equal to 0x01
+    retry_count = 0;
+    do {
+        retry_count++;
+        response = sd_spi_send_cmd(SD_CMD0_GO_IDLE_STATE, 0, 0x95, SdSpiCmdAnswerTypeR1);
+        sd_spi_deselect_card_and_purge();
+
+        if(retry_count >= SD_IDLE_RETRY_COUNT) {
+            sd_spi_debug("CMD0 failed");
+            return SdSpiStatusError;
+        }
+    } while(response.r1 != SdSpi_R1_IN_IDLE_STATE);
+
+    // CMD8 (SEND_IF_COND) to check the power supply status
+    // and wait until response (R7 Format) equal to 0xAA and
+    response = sd_spi_send_cmd(SD_CMD8_SEND_IF_COND, 0x1AA, 0x87, SdSpiCmdAnswerTypeR7);
+    sd_spi_deselect_card_and_purge();
+
+    if(FLAG_SET(response.r1, SdSpi_R1_ILLEGAL_COMMAND)) {
+        if(sd_spi_init_spi_mode_v1() != SdSpiStatusOK) {
+            sd_spi_debug("Init mode v1 failed");
+            return SdSpiStatusError;
+        }
+        sd_high_capacity = 0;
+    } else if(response.r1 == SdSpi_R1_IN_IDLE_STATE) {
+        if(sd_spi_init_spi_mode_v2() != SdSpiStatusOK) {
+            sd_spi_debug("Init mode v2 failed");
+            return SdSpiStatusError;
+        }
+
+        // CMD58 (READ_OCR) to initialize SDHC or SDXC cards: R3 response
+        response = sd_spi_send_cmd(SD_CMD58_READ_OCR, 0, 0xFF, SdSpiCmdAnswerTypeR3);
+        sd_spi_deselect_card_and_purge();
+
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_debug("CMD58 failed");
+            return SdSpiStatusError;
+        }
+        sd_high_capacity = (response.r2 & 0x40) >> 6;
+    } else {
+        return SdSpiStatusError;
+    }
+
+    sd_spi_debug("SD card is %s", sd_high_capacity ? "SDHC or SDXC" : "SDSC");
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_get_csd(SD_CSD* csd) {
+    uint16_t counter = 0;
+    uint8_t csd_data[16];
+    SdSpiStatus ret = SdSpiStatusError;
+    SdSpiCmdAnswer response;
+
+    // CMD9 (SEND_CSD): R1 format (0x00 is no errors)
+    response = sd_spi_send_cmd(SD_CMD9_SEND_CSD, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, SD_TIMEOUT_MS) ==
+           SdSpiStatusOK) {
+            // read CSD data
+            for(counter = 0; counter < 16; counter++) {
+                csd_data[counter] = sd_spi_read_byte();
+            }
+
+            sd_spi_purge_crc();
+
+            /*************************************************************************
+            CSD header decoding
+            *************************************************************************/
+
+            csd->CSDStruct = (csd_data[0] & 0xC0) >> 6;
+            csd->Reserved1 = csd_data[0] & 0x3F;
+            csd->TAAC = csd_data[1];
+            csd->NSAC = csd_data[2];
+            csd->MaxBusClkFrec = csd_data[3];
+            csd->CardComdClasses = (csd_data[4] << 4) | ((csd_data[5] & 0xF0) >> 4);
+            csd->RdBlockLen = csd_data[5] & 0x0F;
+            csd->PartBlockRead = (csd_data[6] & 0x80) >> 7;
+            csd->WrBlockMisalign = (csd_data[6] & 0x40) >> 6;
+            csd->RdBlockMisalign = (csd_data[6] & 0x20) >> 5;
+            csd->DSRImpl = (csd_data[6] & 0x10) >> 4;
+
+            /*************************************************************************
+            CSD v1/v2 decoding
+            *************************************************************************/
+
+            if(sd_high_capacity == 0) {
+                csd->version.v1.Reserved1 = ((csd_data[6] & 0x0C) >> 2);
+                csd->version.v1.DeviceSize = ((csd_data[6] & 0x03) << 10) | (csd_data[7] << 2) |
+                                             ((csd_data[8] & 0xC0) >> 6);
+                csd->version.v1.MaxRdCurrentVDDMin = (csd_data[8] & 0x38) >> 3;
+                csd->version.v1.MaxRdCurrentVDDMax = (csd_data[8] & 0x07);
+                csd->version.v1.MaxWrCurrentVDDMin = (csd_data[9] & 0xE0) >> 5;
+                csd->version.v1.MaxWrCurrentVDDMax = (csd_data[9] & 0x1C) >> 2;
+                csd->version.v1.DeviceSizeMul = ((csd_data[9] & 0x03) << 1) |
+                                                ((csd_data[10] & 0x80) >> 7);
+            } else {
+                csd->version.v2.Reserved1 = ((csd_data[6] & 0x0F) << 2) |
+                                            ((csd_data[7] & 0xC0) >> 6);
+                csd->version.v2.DeviceSize = ((csd_data[7] & 0x3F) << 16) | (csd_data[8] << 8) |
+                                             csd_data[9];
+                csd->version.v2.Reserved2 = ((csd_data[10] & 0x80) >> 8);
+            }
+
+            csd->EraseSingleBlockEnable = (csd_data[10] & 0x40) >> 6;
+            csd->EraseSectorSize = ((csd_data[10] & 0x3F) << 1) | ((csd_data[11] & 0x80) >> 7);
+            csd->WrProtectGrSize = (csd_data[11] & 0x7F);
+            csd->WrProtectGrEnable = (csd_data[12] & 0x80) >> 7;
+            csd->Reserved2 = (csd_data[12] & 0x60) >> 5;
+            csd->WrSpeedFact = (csd_data[12] & 0x1C) >> 2;
+            csd->MaxWrBlockLen = ((csd_data[12] & 0x03) << 2) | ((csd_data[13] & 0xC0) >> 6);
+            csd->WriteBlockPartial = (csd_data[13] & 0x20) >> 5;
+            csd->Reserved3 = (csd_data[13] & 0x1F);
+            csd->FileFormatGrouop = (csd_data[14] & 0x80) >> 7;
+            csd->CopyFlag = (csd_data[14] & 0x40) >> 6;
+            csd->PermWrProtect = (csd_data[14] & 0x20) >> 5;
+            csd->TempWrProtect = (csd_data[14] & 0x10) >> 4;
+            csd->FileFormat = (csd_data[14] & 0x0C) >> 2;
+            csd->Reserved4 = (csd_data[14] & 0x03);
+            csd->crc = (csd_data[15] & 0xFE) >> 1;
+            csd->Reserved5 = (csd_data[15] & 0x01);
+
+            ret = SdSpiStatusOK;
+        }
+    }
+
+    sd_spi_deselect_card_and_purge();
+
+    return ret;
+}
+
+static SdSpiStatus sd_spi_get_cid(SD_CID* Cid) {
+    uint16_t counter = 0;
+    uint8_t cid_data[16];
+    SdSpiStatus ret = SdSpiStatusError;
+    SdSpiCmdAnswer response;
+
+    // CMD10 (SEND_CID): R1 format (0x00 is no errors)
+    response = sd_spi_send_cmd(SD_CMD10_SEND_CID, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+    if(response.r1 == SdSpi_R1_NO_ERROR) {
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, SD_TIMEOUT_MS) ==
+           SdSpiStatusOK) {
+            // read CID data
+            for(counter = 0; counter < 16; counter++) {
+                cid_data[counter] = sd_spi_read_byte();
+            }
+
+            sd_spi_purge_crc();
+
+            Cid->ManufacturerID = cid_data[0];
+            memcpy(Cid->OEM_AppliID, cid_data + 1, 2);
+            memcpy(Cid->ProdName, cid_data + 3, 5);
+            Cid->ProdRev = cid_data[8];
+            Cid->ProdSN = cid_data[9] << 24;
+            Cid->ProdSN |= cid_data[10] << 16;
+            Cid->ProdSN |= cid_data[11] << 8;
+            Cid->ProdSN |= cid_data[12];
+            Cid->Reserved1 = (cid_data[13] & 0xF0) >> 4;
+            Cid->ManufactYear = (cid_data[13] & 0x0F) << 4;
+            Cid->ManufactYear |= (cid_data[14] & 0xF0) >> 4;
+            Cid->ManufactMonth = (cid_data[14] & 0x0F);
+            Cid->CID_CRC = (cid_data[15] & 0xFE) >> 1;
+            Cid->Reserved2 = 1;
+
+            ret = SdSpiStatusOK;
+        }
+    }
+
+    sd_spi_deselect_card_and_purge();
+
+    return ret;
+}
+
+static SdSpiStatus
+    sd_spi_cmd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    uint32_t block_address = address;
+    uint32_t offset = 0;
+
+    // CMD16 (SET_BLOCKLEN): R1 response (0x00: no errors)
+    SdSpiCmdAnswer response =
+        sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0xFF, SdSpiCmdAnswerTypeR1);
+    sd_spi_deselect_card_and_purge();
+
+    if(response.r1 != SdSpi_R1_NO_ERROR) {
+        return SdSpiStatusError;
+    }
+
+    if(!sd_high_capacity) {
+        block_address = address * SD_BLOCK_SIZE;
+    }
+
+    while(blocks--) {
+        // CMD17 (READ_SINGLE_BLOCK): R1 response (0x00: no errors)
+        response =
+            sd_spi_send_cmd(SD_CMD17_READ_SINGLE_BLOCK, block_address, 0xFF, SdSpiCmdAnswerTypeR1);
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        // Wait for the data start token
+        if(sd_spi_wait_for_data(SD_TOKEN_START_DATA_SINGLE_BLOCK_READ, timeout_ms) ==
+           SdSpiStatusOK) {
+            // Read the data block
+            sd_spi_read_bytes_dma((uint8_t*)data + offset, SD_BLOCK_SIZE);
+            sd_spi_purge_crc();
+
+            // increase offset
+            offset += SD_BLOCK_SIZE;
+
+            // increase block address
+            if(sd_high_capacity) {
+                block_address += 1;
+            } else {
+                block_address += SD_BLOCK_SIZE;
+            }
+        } else {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        sd_spi_deselect_card_and_purge();
+    }
+
+    return SdSpiStatusOK;
+}
+
+static SdSpiStatus sd_spi_cmd_write_blocks(
+    uint32_t* data,
+    uint32_t address,
+    uint32_t blocks,
+    uint32_t timeout_ms) {
+    uint32_t block_address = address;
+    uint32_t offset = 0;
+
+    // CMD16 (SET_BLOCKLEN): R1 response (0x00: no errors)
+    SdSpiCmdAnswer response =
+        sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, SD_BLOCK_SIZE, 0xFF, SdSpiCmdAnswerTypeR1);
+    sd_spi_deselect_card_and_purge();
+
+    if(response.r1 != SdSpi_R1_NO_ERROR) {
+        return SdSpiStatusError;
+    }
+
+    if(!sd_high_capacity) {
+        block_address = address * SD_BLOCK_SIZE;
+    }
+
+    while(blocks--) {
+        // CMD24 (WRITE_SINGLE_BLOCK): R1 response (0x00: no errors)
+        response = sd_spi_send_cmd(
+            SD_CMD24_WRITE_SINGLE_BLOCK, block_address, 0xFF, SdSpiCmdAnswerTypeR1);
+        if(response.r1 != SdSpi_R1_NO_ERROR) {
+            sd_spi_deselect_card_and_purge();
+            return SdSpiStatusError;
+        }
+
+        // Send dummy byte for NWR timing : one byte between CMD_WRITE and TOKEN
+        // TODO: check bytes count
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+
+        // Send the data start token
+        sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+        sd_spi_write_bytes_dma((uint8_t*)data + offset, SD_BLOCK_SIZE);
+        sd_spi_purge_crc();
+
+        // Read data response
+        SdSpiDataResponce data_responce = sd_spi_get_data_response(timeout_ms);
+        sd_spi_deselect_card_and_purge();
+
+        if(data_responce != SdSpiDataResponceOK) {
+            return SdSpiStatusError;
+        }
+
+        // increase offset
+        offset += SD_BLOCK_SIZE;
+
+        // increase block address
+        if(sd_high_capacity) {
+            block_address += 1;
+        } else {
+            block_address += SD_BLOCK_SIZE;
+        }
+    }
+
+    return SdSpiStatusOK;
+}
+
+uint8_t sd_max_mount_retry_count() {
+    return 10;
+}
+
+SdSpiStatus sd_init(bool power_reset) {
+
+    sd_spi_debug("sd_init");
+
+    furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+    furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+    furi_hal_gpio_init(&gpio_ext_pa4,GpioModeOutputPushPull, GpioPullUp, GpioSpeedVeryHigh);
+
+    if(power_reset) {
+        sd_spi_debug("Power reset");
+
+        // disable power and set low on all bus pins
+        furi_hal_power_disable_external_3_3v();
+        sd_spi_bus_to_ground();
+        hal_sd_detect_set_low();
+        furi_delay_ms(250);
+
+        // reinit bus and enable power
+        sd_spi_bus_rise_up();
+        hal_sd_detect_init();
+        furi_hal_power_enable_external_3_3v();
+        furi_delay_ms(100);
+    }
+
+    SdSpiStatus status = SdSpiStatusError;
+
+    // Send 80 dummy clocks with CS high
+    sd_spi_deselect_card();
+    for(uint8_t i = 0; i < 80; i++) {
+        sd_spi_write_byte(SD_DUMMY_BYTE);
+    }
+
+    for(uint8_t i = 0; i < 128; i++) {
+    // for(uint8_t i = 0; i < 4; i++) {
+        status = sd_spi_init_spi_mode();
+        if(status == SdSpiStatusOK) {
+            // SD initialized and init to SPI mode properly
+            sd_spi_debug("SD init OK after %d retries", i);
+            break;
+        }
+    }
+
+    status = sd_get_card_state();
+
+    furi_hal_sd_spi_handle = NULL;
+    // furi_hal_spi_release(&furi_hal_spi_bus_handle_sd_slow);
+    furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+
+    // Init sector cache
+    // sector_cache_init();
+
+    return status;
+}
+
+SdSpiStatus sd_get_card_state(void) {
+    SdSpiCmdAnswer response;
+
+    // Send CMD13 (SEND_STATUS) to get SD status
+    response = sd_spi_send_cmd(SD_CMD13_SEND_STATUS, 0, 0xFF, SdSpiCmdAnswerTypeR2);
+    sd_spi_deselect_card_and_purge();
+
+    // Return status OK if response is valid
+    if((response.r1 == SdSpi_R1_NO_ERROR) && (response.r2 == SdSpi_R2_NO_ERROR || response.r2 == SdSpi_R2_CARD_LOCKED)) {
+        return SdSpiStatusOK;
+    }
+
+    return SdSpiStatusError;
+}
+
+SdSpiStatus sd_get_card_info(SD_CardInfo* card_info) {
+    SdSpiStatus status;
+
+    status = sd_spi_get_csd(&(card_info->Csd));
+
+    if(status != SdSpiStatusOK) {
+        return status;
+    }
+
+    status = sd_spi_get_cid(&(card_info->Cid));
+
+    if(status != SdSpiStatusOK) {
+        return status;
+    }
+
+    if(sd_high_capacity == 1) {
+        card_info->LogBlockSize = 512;
+        card_info->CardBlockSize = 512;
+        card_info->CardCapacity = ((uint64_t)card_info->Csd.version.v2.DeviceSize + 1UL) * 1024UL *
+                                  (uint64_t)card_info->LogBlockSize;
+        card_info->LogBlockNbr = (card_info->CardCapacity) / (card_info->LogBlockSize);
+    } else {
+        card_info->CardCapacity = (card_info->Csd.version.v1.DeviceSize + 1);
+        card_info->CardCapacity *= (1UL << (card_info->Csd.version.v1.DeviceSizeMul + 2));
+        card_info->LogBlockSize = 512;
+        card_info->CardBlockSize = 1UL << (card_info->Csd.RdBlockLen);
+        card_info->CardCapacity *= card_info->CardBlockSize;
+        card_info->LogBlockNbr = (card_info->CardCapacity) / (card_info->LogBlockSize);
+    }
+
+    return status;
+}
+
+SdSpiStatus sd_set_pwd(char* pwd) {
+
+  sd_spi_debug("sd_set_pwd");
+  sd_spi_debug(pwd);
+
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+  response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+  if(response.r1 == SdSpi_R1_NO_ERROR) {
+    sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+    uint8_t data[512] = {0xFF};
+    data[0] = 0x05;
+    data[1] = strlen(pwd);
+    for(int i = 0; i < (int)strlen(pwd); i++){
+      data[i+2] = pwd[i];
+    }
+
+    sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+    sd_spi_write_bytes_dma(data, SD_BLOCK_SIZE);
+    sd_spi_purge_crc();
+
+    SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+    sd_spi_deselect_card_and_purge();
+
+    if(data_responce == SdSpiDataResponceOK) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+
+      if(sd_get_card_state()==SdSpiStatusOK) {
+        if(cmd_answer.r2==SdSpi_R2_CARD_LOCKED) { status = SdSpiStatusOK; }
+      }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+  }
+  else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+SdSpiStatus sd_clr_pwd(char* pwd) {
+  sd_spi_debug("sd_clr_pwd");
+  sd_spi_debug(pwd);
+
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+  response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+  if(response.r1 == SdSpi_R1_NO_ERROR) {
+    sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+    uint8_t data[512] = {0xFF};
+    data[0] = 0x02;
+    data[1] = strlen(pwd);
+    for(int i = 0; i < (int)strlen(pwd); i++){
+      data[i+2] = pwd[i];
+    }
+    sd_spi_write_byte(SD_TOKEN_START_DATA_SINGLE_BLOCK_WRITE);
+    sd_spi_write_bytes_dma(data, SD_BLOCK_SIZE);
+    sd_spi_purge_crc();
+
+    SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+    sd_spi_deselect_card_and_purge();
+
+    if(data_responce == SdSpiDataResponceOK) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+
+      if(sd_get_card_state()==SdSpiStatusOK) {
+        if(cmd_answer.r2==SdSpi_R2_NO_ERROR) { status = SdSpiStatusOK; }
+      }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+  }
+  else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+SdSpiStatus sd_force_erase(void) {
+  SdSpiStatus status = SdSpiStatusError;
+  SdSpiCmdAnswer response;
+
+  furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+  furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+  sd_spi_debug("SD_CMD16_SET_BLOCKLEN 1");
+  sd_spi_send_cmd(SD_CMD16_SET_BLOCKLEN, 1, 0xFF, SdSpiCmdAnswerTypeR1);
+  sd_spi_deselect_card_and_purge();
+
+  sd_spi_debug("SD_CMD42_LOCK_UNLOCK");
+  response = sd_spi_send_cmd(SD_CMD42_LOCK_UNLOCK, 0, 0xFF, SdSpiCmdAnswerTypeR1);
+
+  if(response.r1 == SdSpi_R1_NO_ERROR) {
+
+    sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_NO_ERROR");
+    uint8_t data[2] = {0xfe,0x08};
+    sd_spi_write_bytes_dma(data, sizeof(data));
+    sd_spi_purge_crc();
+
+    SdSpiDataResponce data_responce = sd_spi_get_data_response(SD_TIMEOUT_MS);
+    sd_spi_deselect_card_and_purge();
+
+    status = SdSpiStatusOK;
+
+    if(data_responce == SdSpiDataResponceOK) {
+      sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceOK");
+      if(sd_get_card_state()==SdSpiStatusOK) {
+        if(cmd_answer.r2==SdSpi_R2_NO_ERROR) { status = SdSpiStatusOK; }
+      }
+    }
+    else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK SdSpiDataResponceError"); }
+  }
+  else { sd_spi_debug("SD_CMD42_LOCK_UNLOCK R1_ERROR"); }
+
+  furi_hal_sd_spi_handle = NULL;
+  furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+  return status;
+}
+
+SdSpiStatus
+    sd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    SdSpiStatus status = sd_spi_cmd_read_blocks(data, address, blocks, timeout_ms);
+    return status;
+}
+
+SdSpiStatus
+    sd_write_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms) {
+    SdSpiStatus status = sd_spi_cmd_write_blocks(data, address, blocks, timeout_ms);
+    return status;
+}
+
+SdSpiStatus sd_get_cid(SD_CID* cid) {
+    SdSpiStatus status;
+    furi_hal_spi_acquire(&furi_hal_spi_bus_handle_external);
+    furi_hal_sd_spi_handle = &furi_hal_spi_bus_handle_external;
+
+    memset(cid, 0, sizeof(SD_CID));
+    status = sd_spi_get_cid(cid);
+
+    furi_hal_sd_spi_handle = NULL;
+    furi_hal_spi_release(&furi_hal_spi_bus_handle_external);
+
+    return status;
+}

+ 204 - 0
non_catalog_apps/sd_spi/sd_spi.h

@@ -0,0 +1,204 @@
+#pragma once
+#include <stdint.h>
+#include <stdbool.h>
+
+#define __IO volatile
+
+#define SD_TIMEOUT_MS 500//(1000)
+#define SD_BLOCK_SIZE 512
+
+#define VERSION_APP "0.1"
+#define DEVELOPED " "
+#define GITHUB "github.com/Gl1tchub/Flipperzero-SD-SPI"
+
+
+typedef enum {
+    SdSpiStatusOK,
+    SdSpiStatusError,
+    SdSpiStatusTimeout,
+} SdSpiStatus;
+
+/** R1 answer value */
+typedef enum {
+    SdSpi_R1_NO_ERROR = 0x00,
+    SdSpi_R1_IN_IDLE_STATE = 0x01,
+    SdSpi_R1_ERASE_RESET = 0x02,
+    SdSpi_R1_ILLEGAL_COMMAND = 0x04,
+    SdSpi_R1_COM_CRC_ERROR = 0x08,
+    SdSpi_R1_ERASE_SEQUENCE_ERROR = 0x10,
+    SdSpi_R1_ADDRESS_ERROR = 0x20,
+    SdSpi_R1_PARAMETER_ERROR = 0x40,
+} SdSpiR1;
+
+/** R2 answer value */
+typedef enum {
+    /* R2 answer value */
+    SdSpi_R2_NO_ERROR = 0x00,
+    SdSpi_R2_CARD_LOCKED = 0x01,
+    SdSpi_R2_LOCKUNLOCK_ERROR = 0x02,
+    SdSpi_R2_ERROR = 0x04,
+    SdSpi_R2_CC_ERROR = 0x08,
+    SdSpi_R2_CARD_ECC_FAILED = 0x10,
+    SdSpi_R2_WP_VIOLATION = 0x20,
+    SdSpi_R2_ERASE_PARAM = 0x40,
+    SdSpi_R2_OUTOFRANGE = 0x80,
+} SdSpiR2;
+
+/**
+ * @brief Card Specific Data: CSD Register
+ */
+typedef struct {
+    /* Header part */
+    uint8_t CSDStruct : 2; /* CSD structure */
+    uint8_t Reserved1 : 6; /* Reserved */
+    uint8_t TAAC : 8; /* Data read access-time 1 */
+    uint8_t NSAC : 8; /* Data read access-time 2 in CLK cycles */
+    uint8_t MaxBusClkFrec : 8; /* Max. bus clock frequency */
+    uint16_t CardComdClasses : 12; /* Card command classes */
+    uint8_t RdBlockLen : 4; /* Max. read data block length */
+    uint8_t PartBlockRead : 1; /* Partial blocks for read allowed */
+    uint8_t WrBlockMisalign : 1; /* Write block misalignment */
+    uint8_t RdBlockMisalign : 1; /* Read block misalignment */
+    uint8_t DSRImpl : 1; /* DSR implemented */
+
+    /* v1 or v2 struct */
+    union csd_version {
+        struct {
+            uint8_t Reserved1 : 2; /* Reserved */
+            uint16_t DeviceSize : 12; /* Device Size */
+            uint8_t MaxRdCurrentVDDMin : 3; /* Max. read current @ VDD min */
+            uint8_t MaxRdCurrentVDDMax : 3; /* Max. read current @ VDD max */
+            uint8_t MaxWrCurrentVDDMin : 3; /* Max. write current @ VDD min */
+            uint8_t MaxWrCurrentVDDMax : 3; /* Max. write current @ VDD max */
+            uint8_t DeviceSizeMul : 3; /* Device size multiplier */
+        } v1;
+        struct {
+            uint8_t Reserved1 : 6; /* Reserved */
+            uint32_t DeviceSize : 22; /* Device Size */
+            uint8_t Reserved2 : 1; /* Reserved */
+        } v2;
+    } version;
+
+    uint8_t EraseSingleBlockEnable : 1; /* Erase single block enable */
+    uint8_t EraseSectorSize : 7; /* Erase group size multiplier */
+    uint8_t WrProtectGrSize : 7; /* Write protect group size */
+    uint8_t WrProtectGrEnable : 1; /* Write protect group enable */
+    uint8_t Reserved2 : 2; /* Reserved */
+    uint8_t WrSpeedFact : 3; /* Write speed factor */
+    uint8_t MaxWrBlockLen : 4; /* Max. write data block length */
+    uint8_t WriteBlockPartial : 1; /* Partial blocks for write allowed */
+    uint8_t Reserved3 : 5; /* Reserved */
+    uint8_t FileFormatGrouop : 1; /* File format group */
+    uint8_t CopyFlag : 1; /* Copy flag (OTP) */
+    uint8_t PermWrProtect : 1; /* Permanent write protection */
+    uint8_t TempWrProtect : 1; /* Temporary write protection */
+    uint8_t FileFormat : 2; /* File Format */
+    uint8_t Reserved4 : 2; /* Reserved */
+    uint8_t crc : 7; /* Reserved */
+    uint8_t Reserved5 : 1; /* always 1*/
+
+} SD_CSD;
+
+/**
+ * @brief Card Identification Data: CID Register
+ */
+typedef struct {
+    uint8_t ManufacturerID; /* ManufacturerID */
+    char OEM_AppliID[2]; /* OEM/Application ID */
+    char ProdName[5]; /* Product Name */
+    uint8_t ProdRev; /* Product Revision */
+    uint32_t ProdSN; /* Product Serial Number */
+    uint8_t Reserved1; /* Reserved1 */
+    uint8_t ManufactYear; /* Manufacturing Year */
+    uint8_t ManufactMonth; /* Manufacturing Month */
+    uint8_t CID_CRC; /* CID CRC */
+    uint8_t Reserved2; /* always 1 */
+} SD_CID;
+
+/**
+ * @brief SD Card information structure
+ */
+ typedef struct {
+     SD_CSD Csd;
+     SD_CID Cid;
+     uint64_t CardCapacity; /*!< Card Capacity */
+     uint32_t CardBlockSize; /*!< Card Block Size */
+     uint32_t LogBlockNbr; /*!< Specifies the Card logical Capacity in blocks   */
+     uint32_t LogBlockSize; /*!< Specifies logical block size in bytes           */
+ } SD_CardInfo;
+
+ typedef struct {
+     uint8_t r1;
+     uint8_t r2;
+     uint8_t r3;
+     uint8_t r4;
+     uint8_t r5;
+ } SdSpiCmdAnswer;
+
+ extern SdSpiCmdAnswer cmd_answer;
+
+
+/**
+ * @brief SD card max mount retry count
+ *
+ * @return uint8_t
+ */
+uint8_t sd_max_mount_retry_count();
+
+/**
+ * @brief Init sd card
+ *
+ * @param power_reset reset card power
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_init(bool power_reset);
+
+/**
+ * @brief Get card state
+ *
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_card_state(void);
+
+/**
+ * @brief Get card info
+ *
+ * @param card_info
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_card_info(SD_CardInfo* card_info);
+
+/**
+ * @brief Read blocks
+ *
+ * @param data
+ * @param address
+ * @param blocks
+ * @param timeout_ms
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_read_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms);
+
+/**
+ * @brief Write blocks
+ *
+ * @param data
+ * @param address
+ * @param blocks
+ * @param timeout_ms
+ * @return SdSpiStatus
+ */
+SdSpiStatus
+    sd_write_blocks(uint32_t* data, uint32_t address, uint32_t blocks, uint32_t timeout_ms);
+
+/**
+ * @brief Get card CSD register
+ *
+ * @param Cid
+ * @return SdSpiStatus
+ */
+SdSpiStatus sd_get_cid(SD_CID* cid);
+
+SdSpiStatus sd_set_pwd(char* pwd);
+SdSpiStatus sd_clr_pwd(char* pwd);
+SdSpiStatus sd_force_erase(void);

+ 572 - 0
non_catalog_apps/sd_spi/sd_spi_app.c

@@ -0,0 +1,572 @@
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/icon_i.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <gui/modules/submenu.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/dialog_ex.h>
+
+#include <furi_hal_spi.h>
+#include <notification/notification.h>
+#include <notification/notification_messages.h>
+#include <storage/storage.h>
+#include <toolbox/path.h>
+#include <gui/modules/widget.h>
+
+
+#include "sd_spi.h"
+
+#define TAG "sd-spi-app"
+
+/* generated by fbt from .png files in images folder */
+// #include <sd_spi_app_icons.h>
+
+#define TEXT_BOX_STORE_SIZE (4096)
+#define PASSWORD_MAX_LEN (16)
+#define ALERT_MAX_LEN 32
+
+#define STORAGE_LOCKED_FILE "pwd.txt"
+
+/** ids for all scenes used by the app */
+typedef enum {
+    AppScene_MainMenu,
+    AppScene_Status,
+    AppScene_Confirmation,
+    AppScene_Password,
+    AppScene_Info,
+    AppScene_count
+} AppScene;
+
+/** ids for the 2 types of view used by the app */
+typedef enum { AppView_Menu, AppView_Status, AppView_Dialog, AppView_TextInput, AppView_Info} SDSPIAppView;
+
+/** the app context struct */
+typedef struct {
+    SceneManager* scene_manager;
+    ViewDispatcher* view_dispatcher;
+    Submenu* menu;
+    TextBox* tb_status;
+    Widget* widget_about;
+    TextInput* text_input;
+    DialogEx* dialog;
+    char* input_pwd;
+} SDSPIApp;
+
+/** all custom events */
+typedef enum { AppEvent_Status, AppEvent_Confirmation, AppEvent_Password, AppEvent_Info } AppEvent;
+
+/** indices for menu items */
+typedef enum { AppMenuSelection_Init, AppMenuSelection_Status, AppMenuSelection_SDLock, AppMenuSelection_SDUnLock, Confirmation_Dialog, AppMenuSelection_Password, AppMenuSelection_Info } AppMenuSelection;
+
+bool notify_sequence(SdSpiStatus status){
+  NotificationApp* notifications = furi_record_open(RECORD_NOTIFICATION);
+  if(status == SdSpiStatusOK){ notification_message(notifications, &sequence_success); return true; }
+  else{ notification_message(notifications, &sequence_error); }
+  return false;
+  furi_record_close(RECORD_NOTIFICATION);
+}
+
+/** main menu callback - sends a custom event to the scene manager based on the menu selection */
+void app_menu_callback_main_menu(void* context, uint32_t index) {
+    FURI_LOG_T(TAG, "app_menu_callback_main_menu");
+    SDSPIApp* app = context;
+    switch(index) {
+    case AppMenuSelection_Status:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Status);
+      break;
+    case AppMenuSelection_Init:
+      {
+        SdSpiStatus sdStatus;
+        sdStatus = sd_init(false);
+        notify_sequence(sdStatus);
+      }
+      break;
+    case AppMenuSelection_SDLock:
+      FURI_LOG_T(TAG, "AppMenuSelection_SDLock");
+      {
+        SdSpiStatus sdStatus;
+        sdStatus = sd_set_pwd(app->input_pwd);
+        notify_sequence(sdStatus);
+      }
+
+      break;
+    case AppMenuSelection_SDUnLock:
+      FURI_LOG_T(TAG, "AppMenuSelection_SDUnLock");
+      {
+        SdSpiStatus sdStatus;// = sd_init(false);
+        sdStatus = sd_clr_pwd(app->input_pwd);
+        notify_sequence(sdStatus);
+      }
+      break;
+    case AppMenuSelection_Password:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Password);
+      break;
+    case Confirmation_Dialog:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Confirmation);
+      break;
+    case AppMenuSelection_Info:
+      scene_manager_handle_custom_event(app->scene_manager, AppEvent_Info);
+      break;
+    }
+}
+
+/** resets the menu, gives it content, callbacks and selection enums */
+void app_scene_on_enter_main_menu(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_main_menu");
+    SDSPIApp* app = context;
+    submenu_reset(app->menu);
+
+    submenu_add_item(
+      app->menu,
+      "SD Init",
+      AppMenuSelection_Init,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Status",
+      AppMenuSelection_Status,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Lock",
+      AppMenuSelection_SDLock,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Unlock",
+      AppMenuSelection_SDUnLock,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "SD Force Erase",
+      Confirmation_Dialog,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "Password",
+      AppMenuSelection_Password,
+      app_menu_callback_main_menu,
+      app);
+    submenu_add_item(
+      app->menu,
+      "About",
+      AppMenuSelection_Info,
+      app_menu_callback_main_menu,
+      app);
+
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Menu);
+}
+
+/** main menu event handler - switches scene based on the event */
+bool app_scene_on_event_main_menu(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_main_menu");
+    SDSPIApp* app = context;
+    bool consumed = false;
+    switch(event.type) {
+    case SceneManagerEventTypeCustom:
+        switch(event.event) {
+        case AppEvent_Status:
+            scene_manager_next_scene(app->scene_manager, AppScene_Status);
+            consumed = true;
+            break;
+        case AppEvent_Confirmation:
+            scene_manager_next_scene(app->scene_manager, AppScene_Confirmation);
+            consumed = true;
+            break;
+        case AppEvent_Password:
+            scene_manager_next_scene(app->scene_manager, AppScene_Password);
+            consumed = true;
+            break;
+        case AppEvent_Info:
+            scene_manager_next_scene(app->scene_manager, AppScene_Info);
+            consumed = true;
+            break;
+        }
+        break;
+    default:
+        consumed = false;
+        break;
+    }
+    return consumed;
+}
+
+void app_scene_on_exit_main_menu(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_main_menu");
+    SDSPIApp* app = context;
+    submenu_reset(app->menu);
+}
+
+bool text_input_validator(const char* text, FuriString* error, void* context) {
+    UNUSED(context);
+    bool validated = true;
+    if(strlen(text) > PASSWORD_MAX_LEN || strlen(text) < 1) {
+        furi_string_set(error, "the pwd\nmust have\nfrom 1 to\n16 chars");
+        validated = false;
+    }
+    return validated;
+}
+void text_input_done_callback(void* context){
+  SDSPIApp* app = context;
+  Storage* storage = furi_record_open(RECORD_STORAGE);
+  FuriString* path;
+  path = furi_string_alloc();
+  furi_string_set_str(path, EXT_PATH("apps_data/sdspi"));
+  if(!storage_file_exists(storage,EXT_PATH("apps_data"))) { storage_common_mkdir(storage, EXT_PATH("apps_data")); }
+  if(!storage_file_exists(storage,EXT_PATH("apps_data/sdspi"))) { storage_common_mkdir(storage, EXT_PATH("apps_data/sdspi")); }
+
+  path_append(path,STORAGE_LOCKED_FILE);
+  File* file = storage_file_alloc(storage);
+  storage_simply_remove(storage, furi_string_get_cstr(path));
+  if(!storage_file_open(file, furi_string_get_cstr(path), FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
+      FURI_LOG_E(TAG, "Failed to open file");
+  }
+  if(!storage_file_write(file, app->input_pwd, strlen(app->input_pwd))) {
+      FURI_LOG_E(TAG, "Failed to write to file");
+  }
+  furi_string_free(path);
+  storage_file_close(file);
+  storage_file_free(file);
+  furi_record_close(RECORD_STORAGE);
+  scene_manager_previous_scene(app->scene_manager);
+}
+
+/* App Scene Select Password */
+void app_scene_on_enter_password(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_password");
+    SDSPIApp* app = context;
+    text_input_set_header_text(app->text_input,"Enter password");
+    text_input_set_validator(app->text_input, text_input_validator, context);
+    text_input_set_result_callback(
+        app->text_input,
+        text_input_done_callback,
+        app,
+        app->input_pwd,
+        PASSWORD_MAX_LEN,
+        false);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_TextInput);
+}
+bool app_scene_on_event_password(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_password");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_password(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_password");
+    SDSPIApp* app = context;
+    text_input_reset(app->text_input);
+}
+
+/* App Scene Confirm SD Force Erase */
+void app_dialog_erase_callback(DialogExResult result, void* context) {
+  SDSPIApp* app = context;
+  if(result == DialogExResultLeft) { scene_manager_previous_scene(app->scene_manager); }
+  else if(result == DialogExResultRight) {
+    {
+      SdSpiStatus sdStatus;
+      sdStatus = sd_force_erase();
+      if(notify_sequence(sdStatus)){ scene_manager_previous_scene(app->scene_manager); };
+    }
+  }
+}
+void app_scene_on_enter_dialog(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_dialog");
+    SDSPIApp* app = context;
+    dialog_ex_reset(app->dialog);
+
+    dialog_ex_set_result_callback(app->dialog, app_dialog_erase_callback);
+    dialog_ex_set_context(app->dialog, app);
+    dialog_ex_set_left_button_text(app->dialog, "Back");
+    dialog_ex_set_right_button_text(app->dialog, "Erase");
+    // dialog_ex_set_center_button_text(app->dialog, "Menu List");
+    dialog_ex_set_header(app->dialog, "Erase SD card?", 128/2, 12, AlignCenter, AlignTop);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Dialog);
+}
+bool app_scene_on_event_dialog(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_dialog");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_dialog(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_dialog");
+    SDSPIApp* app = context;
+    dialog_ex_reset(app->dialog);
+}
+
+/* App Scene About */
+void app_scene_on_enter_info(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_info");
+    SDSPIApp* app = context;
+    widget_reset(app->widget_about);
+    widget_add_text_box_element(
+        app->widget_about,
+        0,
+        2,
+        128,
+        14,
+        AlignCenter,
+        AlignBottom,
+        "\e#\e!            SD Spi           \e!\n",
+        false);
+    FuriString* temp_str = furi_string_alloc();
+    furi_string_cat_printf(temp_str, "Version: %s\n", VERSION_APP);
+    furi_string_cat_printf(temp_str, "Developed by: %s\n", DEVELOPED);
+    furi_string_cat_printf(temp_str, "Github: %s\n\n", GITHUB);
+
+    widget_add_text_scroll_element(app->widget_about, 0, 16, 128, 50, furi_string_get_cstr(temp_str));
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Info);
+}
+bool app_scene_on_event_info(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_info");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_info(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_info");
+    SDSPIApp* app = context;
+    widget_reset(app->widget_about);
+}
+
+/* App Scene SD Status */
+void app_scene_on_enter_status(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_enter_status");
+    SDSPIApp* app = context;
+    text_box_reset(app->tb_status);
+
+    FuriString* fs_status = furi_string_alloc();
+    furi_string_reserve(fs_status, TEXT_BOX_STORE_SIZE);
+    furi_string_set_char(fs_status, 0, 0);
+    furi_string_set_str(fs_status, "Sd Status:");
+
+    furi_string_cat_str(fs_status, "\nR1");
+    if(cmd_answer.r1 != 0xff) {
+      if(cmd_answer.r1 == SdSpi_R1_NO_ERROR){ furi_string_cat_str(fs_status, "\nNO_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ERASE_RESET){ furi_string_cat_str(fs_status, "\nERASE_RESET"); }
+      if(cmd_answer.r1 & SdSpi_R1_IN_IDLE_STATE){ furi_string_cat_str(fs_status, "\nIN_IDLE_STATE"); }
+      if(cmd_answer.r1 & SdSpi_R1_ILLEGAL_COMMAND){ furi_string_cat_str(fs_status, "\nILLEGAL_COMMAND"); }
+      if(cmd_answer.r1 & SdSpi_R1_COM_CRC_ERROR){ furi_string_cat_str(fs_status, "\nCOM_CRC_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ERASE_SEQUENCE_ERROR){ furi_string_cat_str(fs_status, "\nERASE_SEQUENCE_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_ADDRESS_ERROR){ furi_string_cat_str(fs_status, "\nADDRESS_ERROR"); }
+      if(cmd_answer.r1 & SdSpi_R1_PARAMETER_ERROR){ furi_string_cat_str(fs_status, "\nPARAMETER_ERROR"); }
+    }
+
+    furi_string_cat_str(fs_status, "\nR2");
+    if(cmd_answer.r2 != 0xff) {
+      if(cmd_answer.r2 == SdSpi_R2_NO_ERROR){ furi_string_cat_str(fs_status, "\nNO_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CARD_LOCKED){ furi_string_cat_str(fs_status, "\nCARD_LOCKED"); }
+      if(cmd_answer.r2 & SdSpi_R2_LOCKUNLOCK_ERROR){ furi_string_cat_str(fs_status, "\nLOCKUNLOCK_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_ERROR){ furi_string_cat_str(fs_status, "\nERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CC_ERROR){ furi_string_cat_str(fs_status, "\nCC_ERROR"); }
+      if(cmd_answer.r2 & SdSpi_R2_CARD_ECC_FAILED){ furi_string_cat_str(fs_status, "\nCARD_ECC_FAILED"); }
+      if(cmd_answer.r2 & SdSpi_R2_WP_VIOLATION){ furi_string_cat_str(fs_status, "\nWP_VIOLATION"); }
+      if(cmd_answer.r2 & SdSpi_R2_ERASE_PARAM){ furi_string_cat_str(fs_status, "\nERASE_PARAM"); }
+      if(cmd_answer.r2 & SdSpi_R2_OUTOFRANGE){ furi_string_cat_str(fs_status, "\nOUTOFRANGE"); }
+    }
+
+    text_box_set_text(app->tb_status, furi_string_get_cstr(fs_status));
+    text_box_set_focus(app->tb_status, TextBoxFocusEnd);
+    view_dispatcher_switch_to_view(app->view_dispatcher, AppView_Status);
+}
+bool app_scene_on_event_status(void* context, SceneManagerEvent event) {
+    FURI_LOG_T(TAG, "app_scene_on_event_status");
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+void app_scene_on_exit_status(void* context) {
+    FURI_LOG_T(TAG, "app_scene_on_exit_status");
+    SDSPIApp* app = context;
+    text_box_reset(app->tb_status);
+}
+
+
+
+/** collection of all scene on_enter handlers - in the same order as their enum */
+void (*const app_scene_on_enter_handlers[])(void*) = {
+    app_scene_on_enter_main_menu,
+    app_scene_on_enter_status,
+    // app_scene_on_enter_info,
+    app_scene_on_enter_dialog,
+    app_scene_on_enter_password,
+    app_scene_on_enter_info};
+
+/** collection of all scene on event handlers - in the same order as their enum */
+bool (*const app_scene_on_event_handlers[])(void*, SceneManagerEvent) = {
+    app_scene_on_event_main_menu,
+    app_scene_on_event_status,
+    // app_scene_on_event_info,
+    app_scene_on_event_dialog,
+    app_scene_on_event_password,
+    app_scene_on_event_info};
+
+/** collection of all scene on exit handlers - in the same order as their enum */
+void (*const app_scene_on_exit_handlers[])(void*) = {
+    app_scene_on_exit_main_menu,
+    app_scene_on_exit_status,
+    // app_scene_on_exit_info,
+    app_scene_on_exit_dialog,
+    app_scene_on_exit_password,
+    app_scene_on_exit_info};
+
+/** collection of all on_enter, on_event, on_exit handlers */
+const SceneManagerHandlers app_scene_event_handlers = {
+    .on_enter_handlers = app_scene_on_enter_handlers,
+    .on_event_handlers = app_scene_on_event_handlers,
+    .on_exit_handlers = app_scene_on_exit_handlers,
+    .scene_num = AppScene_count};
+
+/** custom event handler - passes the event to the scene manager */
+bool app_scene_manager_custom_event_callback(void* context, uint32_t custom_event) {
+    FURI_LOG_T(TAG, "app_scene_manager_custom_event_callback");
+    furi_assert(context);
+    SDSPIApp* app = context;
+    return scene_manager_handle_custom_event(app->scene_manager, custom_event);
+}
+
+/** navigation event handler - passes the event to the scene manager */
+bool app_scene_manager_navigation_event_callback(void* context) {
+    FURI_LOG_T(TAG, "app_scene_manager_navigation_event_callback");
+    furi_assert(context);
+    SDSPIApp* app = context;
+    return scene_manager_handle_back_event(app->scene_manager);
+}
+
+/** initialise the scene manager with all handlers */
+void app_scene_manager_init(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_scene_manager_init");
+    app->scene_manager = scene_manager_alloc(&app_scene_event_handlers, app);
+}
+
+
+/** initialise the views, and initialise the view dispatcher with all views */
+void app_view_dispatcher_init(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_view_dispatcher_init");
+    app->view_dispatcher = view_dispatcher_alloc();
+    view_dispatcher_enable_queue(app->view_dispatcher);
+
+    // allocate each view
+    FURI_LOG_D(TAG, "app_view_dispatcher_init allocating views");
+    app->menu = submenu_alloc();
+    app->tb_status = text_box_alloc();
+    app->text_input = text_input_alloc();
+    app->widget_about = widget_alloc();
+    app->dialog = dialog_ex_alloc();
+
+    app->input_pwd = "";
+
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    FuriString* path;
+    path = furi_string_alloc();
+    furi_string_set_str(path, EXT_PATH("apps_data/sdspi"));
+    path_append(path,STORAGE_LOCKED_FILE);
+    if(storage_file_exists(storage,furi_string_get_cstr(path))) {
+      File* file = storage_file_alloc(storage);
+      if(storage_file_open(file, furi_string_get_cstr(path), FSAM_READ, FSOM_OPEN_EXISTING)) {
+          FURI_LOG_E(TAG, "File pwd loading");
+          char data[PASSWORD_MAX_LEN] = {0};
+          if(storage_file_read(file, data, PASSWORD_MAX_LEN)>0){
+            FURI_LOG_E(TAG, "File pwd laoded");
+            // app->input_pwd = data;
+            strncpy(app->input_pwd,data,PASSWORD_MAX_LEN);
+            FURI_LOG_E(TAG, data);
+          }
+          storage_file_close(file);
+      }
+      else{
+        FURI_LOG_E(TAG, "File pwd not found");
+      }
+      storage_file_free(file);
+    }
+    furi_string_free(path);
+    furi_record_close(RECORD_STORAGE);
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init setting callbacks");
+    view_dispatcher_set_event_callback_context(app->view_dispatcher, app);
+    view_dispatcher_set_custom_event_callback(app->view_dispatcher, app_scene_manager_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(app->view_dispatcher, app_scene_manager_navigation_event_callback);
+
+    // add views to the dispatcher, indexed by their enum value
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view menu");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Menu, submenu_get_view(app->menu));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view textbox");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Status, text_box_get_view(app->tb_status));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view dialog");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Dialog, dialog_ex_get_view(app->dialog));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view password");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_TextInput, text_input_get_view(app->text_input));
+
+    FURI_LOG_D(TAG, "app_view_dispatcher_init adding view about");
+    view_dispatcher_add_view(app->view_dispatcher, AppView_Info, widget_get_view(app->widget_about));
+}
+
+/** initialise app data, scene manager, and view dispatcher */
+SDSPIApp* app_init() {
+    FURI_LOG_T(TAG, "app_init");
+    SDSPIApp* app = malloc(sizeof(SDSPIApp));
+    app_scene_manager_init(app);
+    app_view_dispatcher_init(app);
+    return app;
+}
+
+/** free all app data, scene manager, and view dispatcher */
+void app_free(SDSPIApp* app) {
+    FURI_LOG_T(TAG, "app_free");
+    scene_manager_free(app->scene_manager);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Menu);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Status);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_TextInput);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Dialog);
+    view_dispatcher_remove_view(app->view_dispatcher, AppView_Info);
+    view_dispatcher_free(app->view_dispatcher);
+    submenu_free(app->menu);
+    text_box_free(app->tb_status);
+    widget_free(app->widget_about);
+    dialog_ex_free(app->dialog);
+    text_input_free(app->text_input);
+    free(app);
+}
+
+/** go to trace log level in the dev environment */
+void app_set_log_level() {
+#ifdef FURI_DEBUG
+    furi_log_set_level(FuriLogLevelTrace);
+#else
+    furi_log_set_level(FuriLogLevelInfo);
+#endif
+}
+
+/** entrypoint */
+int32_t sd_spi_app(void* p) {
+    UNUSED(p);
+    app_set_log_level();
+
+    // create the app context struct, scene manager, and view dispatcher
+    FURI_LOG_I(TAG, "sd_spi_app starting...");
+    SDSPIApp* app = app_init();
+
+    // set the scene and launch the main loop
+    Gui* gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen);
+    scene_manager_next_scene(app->scene_manager, AppScene_MainMenu);
+    FURI_LOG_D(TAG, "Starting dispatcher...");
+    view_dispatcher_run(app->view_dispatcher);
+
+    // free all memory
+    FURI_LOG_I(TAG, "app finishing...");
+    furi_record_close(RECORD_GUI);
+    app_free(app);
+    return 0;
+}

BIN
non_catalog_apps/sd_spi/sd_spi_app_10px.png