فهرست منبع

Add picopass from https://github.com/flipperdevices/flipperzero-good-faps

git-subtree-dir: picopass
git-subtree-mainline: a12b4091e7b2ffe805b63a0c6941832a372ecc86
git-subtree-split: 1b1df49168d39c764c37d4211580e458672436e2
Willy-JL 2 سال پیش
والد
کامیت
6220b6ce60
68فایلهای تغییر یافته به همراه6642 افزوده شده و 0 حذف شده
  1. 26 0
      picopass/.catalog/README.md
  2. 16 0
      picopass/.catalog/changelog.md
  3. BIN
      picopass/.catalog/screenshots/1.png
  4. BIN
      picopass/.catalog/screenshots/2.png
  5. BIN
      picopass/.catalog/screenshots/3.png
  6. 4 0
      picopass/.gitignore
  7. 1 0
      picopass/.gitsubtree
  8. BIN
      picopass/125_10px.png
  9. 25 0
      picopass/application.fam
  10. 41 0
      picopass/files/iclass_elite_dict.txt
  11. 47 0
      picopass/files/iclass_standard_dict.txt
  12. 164 0
      picopass/helpers/iclass_elite_dict.c
  13. 29 0
      picopass/helpers/iclass_elite_dict.h
  14. BIN
      picopass/icons/DolphinMafia_115x62.png
  15. BIN
      picopass/icons/DolphinNice_96x59.png
  16. BIN
      picopass/icons/Nfc_10px.png
  17. BIN
      picopass/icons/RFIDDolphinReceive_97x61.png
  18. BIN
      picopass/icons/RFIDDolphinSend_97x61.png
  19. 319 0
      picopass/lib/loclass/optimized_cipher.c
  20. 117 0
      picopass/lib/loclass/optimized_cipher.h
  21. 136 0
      picopass/lib/loclass/optimized_cipherutils.c
  22. 64 0
      picopass/lib/loclass/optimized_cipherutils.h
  23. 232 0
      picopass/lib/loclass/optimized_elite.c
  24. 58 0
      picopass/lib/loclass/optimized_elite.h
  25. 320 0
      picopass/lib/loclass/optimized_ikeys.c
  26. 66 0
      picopass/lib/loclass/optimized_ikeys.h
  27. 102 0
      picopass/loclass_writer.c
  28. 20 0
      picopass/loclass_writer.h
  29. 241 0
      picopass/picopass.c
  30. 3 0
      picopass/picopass.h
  31. 422 0
      picopass/picopass_device.c
  32. 151 0
      picopass/picopass_device.h
  33. 108 0
      picopass/picopass_i.h
  34. 8 0
      picopass/picopass_keys.c
  35. 10 0
      picopass/picopass_keys.h
  36. 1340 0
      picopass/picopass_worker.c
  37. 57 0
      picopass/picopass_worker.h
  38. 36 0
      picopass/picopass_worker_i.h
  39. 213 0
      picopass/rfal_picopass.c
  40. 75 0
      picopass/rfal_picopass.h
  41. 30 0
      picopass/scenes/picopass_scene.c
  42. 29 0
      picopass/scenes/picopass_scene.h
  43. 92 0
      picopass/scenes/picopass_scene_card_menu.c
  44. 21 0
      picopass/scenes/picopass_scene_config.h
  45. 58 0
      picopass/scenes/picopass_scene_delete.c
  46. 40 0
      picopass/scenes/picopass_scene_delete_success.c
  47. 104 0
      picopass/scenes/picopass_scene_device_info.c
  48. 172 0
      picopass/scenes/picopass_scene_elite_dict_attack.c
  49. 58 0
      picopass/scenes/picopass_scene_emulate.c
  50. 25 0
      picopass/scenes/picopass_scene_file_select.c
  51. 48 0
      picopass/scenes/picopass_scene_key_input.c
  52. 119 0
      picopass/scenes/picopass_scene_key_menu.c
  53. 88 0
      picopass/scenes/picopass_scene_loclass.c
  54. 61 0
      picopass/scenes/picopass_scene_read_card.c
  55. 178 0
      picopass/scenes/picopass_scene_read_card_success.c
  56. 80 0
      picopass/scenes/picopass_scene_read_factory_success.c
  57. 83 0
      picopass/scenes/picopass_scene_save_name.c
  58. 47 0
      picopass/scenes/picopass_scene_save_success.c
  59. 84 0
      picopass/scenes/picopass_scene_saved_menu.c
  60. 73 0
      picopass/scenes/picopass_scene_start.c
  61. 56 0
      picopass/scenes/picopass_scene_write_card.c
  62. 65 0
      picopass/scenes/picopass_scene_write_card_failure.c
  63. 70 0
      picopass/scenes/picopass_scene_write_card_success.c
  64. 53 0
      picopass/scenes/picopass_scene_write_key.c
  65. 281 0
      picopass/views/dict_attack.c
  66. 44 0
      picopass/views/dict_attack.h
  67. 110 0
      picopass/views/loclass.c
  68. 22 0
      picopass/views/loclass.h

+ 26 - 0
picopass/.catalog/README.md

@@ -0,0 +1,26 @@
+# Picopass
+
+
+This application allows you to read, write, save, and emulate legacy HID iClass cards and fobs (based on the picopass chipset).  Also supports saving the credential to the Flipper Zero LFRFID data format, changing the keys on the card, performing dictionary attack, and performing the 'online' part of the loclass attack.
+
+NOTE: Does not support iClass SE
+
+# Loclass
+
+The loclass attack emulates specific CSN and collects responses from the reader which can be used to calculate the elite or custom key configured for that reader.  This key is then used to read data on the cards used with that reader.
+
+## Online part
+
+1. Run _loclass_ from the picopass main menu
+2. Present the flipper to the reader.
+3. Collect responses until the progress bar is full.
+
+NOTE: If the screen says “Got std key” AND stays on 0/18, the reader is not elite or custom keyed.
+
+## Offline part
+
+1. Download the loclass log (_sdcard/apps_data/picopass/.loclass.log_) from your Flipper Zero.
+2. Use [loclass.ericbetts.dev](https://loclass.ericbetts.dev/) or a tool of your choice to calculate the key
+3. Copy the key to _iclass_elite_dict_user.txt_ and place in _sdcard/apps_data/picopass/_
+4. Run _Elite Dict. Attack_ from the picopass main menu
+5. Present card to the back of the Flipper Zero.

+ 16 - 0
picopass/.catalog/changelog.md

@@ -0,0 +1,16 @@
+## 1.6
+ - Faster loclass response collection
+ - Save as LF for all bit lengths
+ - Removes unvalidated H10301 parsing
+## 1.5
+ - New random filename API
+## 1.4
+ - Optimize crypto speed to fix compatibliity with Signo and OmniKey readers
+## 1.3
+ - Show standard key instead of hex bytes when detected
+## 1.2
+ - Sentinel bit remove
+## 1.1
+ - Key dicts moved to app assets 
+## 1.0
+ - Initial release

BIN
picopass/.catalog/screenshots/1.png


BIN
picopass/.catalog/screenshots/2.png


BIN
picopass/.catalog/screenshots/3.png


+ 4 - 0
picopass/.gitignore

@@ -0,0 +1,4 @@
+dist/*
+.vscode
+.clang-format
+.editorconfig

+ 1 - 0
picopass/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/flipperdevices/flipperzero-good-faps dev picopass

BIN
picopass/125_10px.png


+ 25 - 0
picopass/application.fam

@@ -0,0 +1,25 @@
+App(
+    appid="picopass",
+    name="PicoPass",
+    apptype=FlipperAppType.EXTERNAL,
+    targets=["f7"],
+    entry_point="picopass_app",
+    requires=[
+        "storage",
+        "gui",
+    ],
+    stack_size=4 * 1024,
+    fap_description="App to communicate with NFC tags using the PicoPass(iClass) format",
+    fap_version="1.6",
+    fap_icon="125_10px.png",
+    fap_category="NFC",
+    fap_libs=["mbedtls"],
+    fap_private_libs=[
+        Lib(
+            name="loclass",
+            cflags=["-O3"],
+        ),
+    ],
+    fap_icon_assets="icons",
+    fap_file_assets="files",
+)

+ 41 - 0
picopass/files/iclass_elite_dict.txt

@@ -0,0 +1,41 @@
+
+## From https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/iclass_default_keys.dic
+
+# key1/Kc from PicoPass 2k documentation
+7665544332211000
+# SAGEM
+0123456789ABCDEF
+# PicoPass Default Exchange Key
+5CBCF1DA45D5FB4F
+# From HID multiclassSE reader
+31ad7ebd2f282168
+# From pastebin: https://pastebin.com/uHqpjiuU
+6EFD46EFCBB3C875
+E033CA419AEE43F9
+
+# default picopass KD / Page 0 / Book 1
+FDCB5A52EA8F3090
+237FF9079863DF44
+5ADC25FB27181D32
+83B881F2936B2E49
+43644E61EE866BA5
+897034143D016080
+82D17B44C0122963
+4895CA7DE65E2025
+DADAD4C57BE271B7
+E41E9EDEF5719ABF
+293D275EC3AF9C7F
+C3C169251B8A70FB
+F41DAF58B20C8B91
+28877A609EC0DD2B
+66584C91EE80D5E5
+C1B74D7478053AE2
+
+# default iCLASS RFIDeas
+6B65797374726B72
+
+# CTF key
+5C100DF7042EAE64
+
+# iCopy-X DRM key (iCE product)
+2020666666668888

+ 47 - 0
picopass/files/iclass_standard_dict.txt

@@ -0,0 +1,47 @@
+
+## From https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/iclass_default_keys.dic
+
+# AA1
+AEA684A6DAB23278
+# key1/Kc from PicoPass 2k documentation
+7665544332211000
+# SAGEM
+0123456789ABCDEF
+# from loclass demo file.
+5b7c62c491c11b39
+# Kd from PicoPass 2k documentation
+F0E1D2C3B4A59687
+# PicoPass Default Exchange Key
+5CBCF1DA45D5FB4F
+# From HID multiclassSE reader
+31ad7ebd2f282168
+# From pastebin: https://pastebin.com/uHqpjiuU
+6EFD46EFCBB3C875
+E033CA419AEE43F9
+
+# iCopy-x DRM keys
+# iCL tags
+2020666666668888
+# iCS tags reversed from the SOs
+6666202066668888
+
+# default picopass KD / Page 0 / Book 1
+FDCB5A52EA8F3090
+237FF9079863DF44
+5ADC25FB27181D32
+83B881F2936B2E49
+43644E61EE866BA5
+897034143D016080
+82D17B44C0122963
+4895CA7DE65E2025
+DADAD4C57BE271B7
+E41E9EDEF5719ABF
+293D275EC3AF9C7F
+C3C169251B8A70FB
+F41DAF58B20C8B91
+28877A609EC0DD2B
+66584C91EE80D5E5
+C1B74D7478053AE2
+
+# default iCLASS RFIDeas
+6B65797374726B72

+ 164 - 0
picopass/helpers/iclass_elite_dict.c

@@ -0,0 +1,164 @@
+#include "iclass_elite_dict.h"
+
+#include <lib/toolbox/args.h>
+#include <lib/flipper_format/flipper_format.h>
+
+#define ICLASS_ELITE_DICT_FLIPPER_NAME APP_ASSETS_PATH("iclass_elite_dict.txt")
+#define ICLASS_STANDARD_DICT_FLIPPER_NAME APP_ASSETS_PATH("iclass_standard_dict.txt")
+#define ICLASS_ELITE_DICT_USER_NAME APP_DATA_PATH("assets/iclass_elite_dict_user.txt")
+
+#define TAG "IclassEliteDict"
+
+#define ICLASS_ELITE_KEY_LINE_LEN (17)
+#define ICLASS_ELITE_KEY_LEN (8)
+
+struct IclassEliteDict {
+    Stream* stream;
+    uint32_t total_keys;
+};
+
+bool iclass_elite_dict_check_presence(IclassEliteDictType dict_type) {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+
+    bool dict_present = false;
+    if(dict_type == IclassEliteDictTypeFlipper) {
+        dict_present =
+            (storage_common_stat(storage, ICLASS_ELITE_DICT_FLIPPER_NAME, NULL) == FSE_OK);
+    } else if(dict_type == IclassEliteDictTypeUser) {
+        dict_present = (storage_common_stat(storage, ICLASS_ELITE_DICT_USER_NAME, NULL) == FSE_OK);
+    } else if(dict_type == IclassStandardDictTypeFlipper) {
+        dict_present =
+            (storage_common_stat(storage, ICLASS_STANDARD_DICT_FLIPPER_NAME, NULL) == FSE_OK);
+    }
+
+    furi_record_close(RECORD_STORAGE);
+
+    return dict_present;
+}
+
+IclassEliteDict* iclass_elite_dict_alloc(IclassEliteDictType dict_type) {
+    IclassEliteDict* dict = malloc(sizeof(IclassEliteDict));
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    dict->stream = buffered_file_stream_alloc(storage);
+    FuriString* next_line = furi_string_alloc();
+
+    bool dict_loaded = false;
+    do {
+        if(dict_type == IclassEliteDictTypeFlipper) {
+            if(!buffered_file_stream_open(
+                   dict->stream, ICLASS_ELITE_DICT_FLIPPER_NAME, FSAM_READ, FSOM_OPEN_EXISTING)) {
+                buffered_file_stream_close(dict->stream);
+                break;
+            }
+        } else if(dict_type == IclassEliteDictTypeUser) {
+            if(!buffered_file_stream_open(
+                   dict->stream, ICLASS_ELITE_DICT_USER_NAME, FSAM_READ_WRITE, FSOM_OPEN_ALWAYS)) {
+                buffered_file_stream_close(dict->stream);
+                break;
+            }
+        } else if(dict_type == IclassStandardDictTypeFlipper) {
+            if(!buffered_file_stream_open(
+                   dict->stream,
+                   ICLASS_STANDARD_DICT_FLIPPER_NAME,
+                   FSAM_READ,
+                   FSOM_OPEN_EXISTING)) {
+                buffered_file_stream_close(dict->stream);
+                break;
+            }
+        }
+
+        // Read total amount of keys
+        while(true) { //-V547
+            if(!stream_read_line(dict->stream, next_line)) break;
+            if(furi_string_get_char(next_line, 0) == '#') continue;
+            if(furi_string_size(next_line) != ICLASS_ELITE_KEY_LINE_LEN) continue;
+            dict->total_keys++;
+        }
+        furi_string_reset(next_line);
+        stream_rewind(dict->stream);
+
+        dict_loaded = true;
+        FURI_LOG_I(TAG, "Loaded dictionary with %lu keys", dict->total_keys);
+    } while(false);
+
+    if(!dict_loaded) { //-V547
+        buffered_file_stream_close(dict->stream);
+        free(dict);
+        dict = NULL;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+    furi_string_free(next_line);
+
+    return dict;
+}
+
+void iclass_elite_dict_free(IclassEliteDict* dict) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    buffered_file_stream_close(dict->stream);
+    stream_free(dict->stream);
+    free(dict);
+}
+
+uint32_t iclass_elite_dict_get_total_keys(IclassEliteDict* dict) {
+    furi_assert(dict);
+
+    return dict->total_keys;
+}
+
+bool iclass_elite_dict_get_next_key(IclassEliteDict* dict, uint8_t* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    uint8_t key_byte_tmp = 0;
+    FuriString* next_line = furi_string_alloc();
+
+    bool key_read = false;
+    *key = 0ULL;
+    while(!key_read) {
+        if(!stream_read_line(dict->stream, next_line)) break;
+        if(furi_string_get_char(next_line, 0) == '#') continue;
+        if(furi_string_size(next_line) != ICLASS_ELITE_KEY_LINE_LEN) continue;
+        for(uint8_t i = 0; i < ICLASS_ELITE_KEY_LEN * 2; i += 2) {
+            args_char_to_hex(
+                furi_string_get_char(next_line, i),
+                furi_string_get_char(next_line, i + 1),
+                &key_byte_tmp);
+            key[i / 2] = key_byte_tmp;
+        }
+        key_read = true;
+    }
+
+    furi_string_free(next_line);
+    return key_read;
+}
+
+bool iclass_elite_dict_rewind(IclassEliteDict* dict) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    return stream_rewind(dict->stream);
+}
+
+bool iclass_elite_dict_add_key(IclassEliteDict* dict, uint8_t* key) {
+    furi_assert(dict);
+    furi_assert(dict->stream);
+
+    FuriString* key_str = furi_string_alloc();
+    for(size_t i = 0; i < 6; i++) {
+        furi_string_cat_printf(key_str, "%02X", key[i]);
+    }
+    furi_string_cat_printf(key_str, "\n");
+
+    bool key_added = false;
+    do {
+        if(!stream_seek(dict->stream, 0, StreamOffsetFromEnd)) break;
+        if(!stream_insert_string(dict->stream, key_str)) break;
+        key_added = true;
+    } while(false);
+
+    furi_string_free(key_str);
+    return key_added;
+}

+ 29 - 0
picopass/helpers/iclass_elite_dict.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <stdbool.h>
+#include <storage/storage.h>
+#include <lib/flipper_format/flipper_format.h>
+#include <lib/toolbox/stream/file_stream.h>
+#include <lib/toolbox/stream/buffered_file_stream.h>
+
+typedef enum {
+    IclassEliteDictTypeUser,
+    IclassEliteDictTypeFlipper,
+    IclassStandardDictTypeFlipper,
+} IclassEliteDictType;
+
+typedef struct IclassEliteDict IclassEliteDict;
+
+bool iclass_elite_dict_check_presence(IclassEliteDictType dict_type);
+
+IclassEliteDict* iclass_elite_dict_alloc(IclassEliteDictType dict_type);
+
+void iclass_elite_dict_free(IclassEliteDict* dict);
+
+uint32_t iclass_elite_dict_get_total_keys(IclassEliteDict* dict);
+
+bool iclass_elite_dict_get_next_key(IclassEliteDict* dict, uint8_t* key);
+
+bool iclass_elite_dict_rewind(IclassEliteDict* dict);
+
+bool iclass_elite_dict_add_key(IclassEliteDict* dict, uint8_t* key);

BIN
picopass/icons/DolphinMafia_115x62.png


BIN
picopass/icons/DolphinNice_96x59.png


BIN
picopass/icons/Nfc_10px.png


BIN
picopass/icons/RFIDDolphinReceive_97x61.png


BIN
picopass/icons/RFIDDolphinSend_97x61.png


+ 319 - 0
picopass/lib/loclass/optimized_cipher.c

@@ -0,0 +1,319 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+/*
+  This file contains an optimized version of the MAC-calculation algorithm. Some measurements on
+  a std laptop showed it runs in about 1/3 of the time:
+
+    Std: 0.428962
+    Opt: 0.151609
+
+  Additionally, it is self-reliant, not requiring e.g. bitstreams from the cipherutils, thus can
+  be easily dropped into a code base.
+
+  The optimizations have been performed in the following steps:
+  * Parameters passed by reference instead of by value.
+  * Iteration instead of recursion, un-nesting recursive loops into for-loops.
+  * Handling of bytes instead of individual bits, for less shuffling and masking
+  * Less creation of "objects", structs, and instead reuse of alloc:ed memory
+  * Inlining some functions via #define:s
+
+  As a consequence, this implementation is less generic. Also, I haven't bothered documenting this.
+  For a thorough documentation, check out the MAC-calculation within cipher.c instead.
+
+  -- MHS 2015
+**/
+
+/**
+
+  The runtime of opt_doTagMAC_2() with the MHS optimized version was 403 microseconds on Proxmark3.
+  This was still to slow for some newer readers which didn't want to wait that long.
+
+  Further optimizations to speedup the MAC calculations:
+  * Optimized opt_Tt logic
+  * Look up table for opt_select
+  * Removing many unnecessary bit maskings (& 0x1)
+  * updating state in place instead of alternating use of a second state structure
+  * remove the necessity to reverse bits of input and output bytes
+
+  opt_doTagMAC_2() now completes in 270 microseconds.
+
+  -- piwi 2019
+**/
+
+/**
+  add the possibility to do iCLASS on device only
+  -- iceman 2020
+**/
+
+#include "optimized_cipher.h"
+#include "optimized_elite.h"
+#include "optimized_ikeys.h"
+#include "optimized_cipherutils.h"
+
+static const uint8_t loclass_opt_select_LUT[256] = {
+    00, 03, 02, 01, 02, 03, 00, 01, 04, 07, 07, 04, 06, 07, 05, 04, 01, 02, 03, 00, 02, 03, 00, 01,
+    05, 06, 06, 05, 06, 07, 05, 04, 06, 05, 04, 07, 04, 05, 06, 07, 06, 05, 05, 06, 04, 05, 07, 06,
+    07, 04, 05, 06, 04, 05, 06, 07, 07, 04, 04, 07, 04, 05, 07, 06, 06, 05, 04, 07, 04, 05, 06, 07,
+    02, 01, 01, 02, 00, 01, 03, 02, 03, 00, 01, 02, 00, 01, 02, 03, 07, 04, 04, 07, 04, 05, 07, 06,
+    00, 03, 02, 01, 02, 03, 00, 01, 00, 03, 03, 00, 02, 03, 01, 00, 05, 06, 07, 04, 06, 07, 04, 05,
+    05, 06, 06, 05, 06, 07, 05, 04, 02, 01, 00, 03, 00, 01, 02, 03, 06, 05, 05, 06, 04, 05, 07, 06,
+    03, 00, 01, 02, 00, 01, 02, 03, 07, 04, 04, 07, 04, 05, 07, 06, 02, 01, 00, 03, 00, 01, 02, 03,
+    02, 01, 01, 02, 00, 01, 03, 02, 03, 00, 01, 02, 00, 01, 02, 03, 03, 00, 00, 03, 00, 01, 03, 02,
+    04, 07, 06, 05, 06, 07, 04, 05, 00, 03, 03, 00, 02, 03, 01, 00, 01, 02, 03, 00, 02, 03, 00, 01,
+    05, 06, 06, 05, 06, 07, 05, 04, 04, 07, 06, 05, 06, 07, 04, 05, 04, 07, 07, 04, 06, 07, 05, 04,
+    01, 02, 03, 00, 02, 03, 00, 01, 01, 02, 02, 01, 02, 03, 01, 00};
+
+/********************** the table above has been generated with this code: ********
+#include "util.h"
+static void init_opt_select_LUT(void) {
+    for (int r = 0; r < 256; r++) {
+        uint8_t r_ls2 = r << 2;
+        uint8_t r_and_ls2 = r & r_ls2;
+        uint8_t r_or_ls2  = r | r_ls2;
+        uint8_t z0 = (r_and_ls2 >> 5) ^ ((r & ~r_ls2) >> 4) ^ ( r_or_ls2 >> 3);
+        uint8_t z1 = (r_or_ls2 >> 6) ^ ( r_or_ls2 >> 1) ^ (r >> 5) ^ r;
+        uint8_t z2 = ((r & ~r_ls2) >> 4) ^ (r_and_ls2 >> 3) ^ r;
+        loclass_opt_select_LUT[r] = (z0 & 4) | (z1 & 2) | (z2 & 1);
+    }
+    print_result("", loclass_opt_select_LUT, 256);
+}
+***********************************************************************************/
+
+static inline void loclass_opt_successor(const uint8_t* k, LoclassState_t* s, uint8_t y) {
+    uint16_t Tt = s->t & 0xc533;
+    Tt = Tt ^ (Tt >> 1);
+    Tt = Tt ^ (Tt >> 4);
+    Tt = Tt ^ (Tt >> 10);
+    Tt = Tt ^ (Tt >> 8);
+
+    s->t = (s->t >> 1);
+    s->t |= (Tt ^ (s->r >> 7) ^ (s->r >> 3)) << 15;
+
+    uint8_t opt_B = s->b;
+    opt_B ^= s->b >> 6;
+    opt_B ^= s->b >> 5;
+    opt_B ^= s->b >> 4;
+
+    s->b = s->b >> 1;
+    s->b |= (opt_B ^ s->r) << 7;
+
+    uint8_t Tt1 = Tt & 0x01;
+    uint8_t opt_select = loclass_opt_select_LUT[s->r] ^ Tt1 ^ ((Tt1 ^ (y & 0x01)) << 1);
+
+    uint8_t r = s->r;
+    s->r = (k[opt_select] ^ s->b) + s->l;
+    s->l = s->r + r;
+}
+
+static inline void loclass_opt_suc(
+    const uint8_t* k,
+    LoclassState_t* s,
+    const uint8_t* in,
+    uint8_t length,
+    bool add32Zeroes) {
+    for(int i = 0; i < length; i++) {
+        uint8_t head = in[i];
+#pragma GCC unroll 8
+        for(int j = 0; j < 8; j++) {
+            loclass_opt_successor(k, s, head);
+            head >>= 1;
+        }
+    }
+    // For tag MAC, an additional 32 zeroes
+    if(add32Zeroes) {
+        for(int i = 0; i < 32; i++) {
+            loclass_opt_successor(k, s, 0);
+        }
+    }
+}
+
+static inline void loclass_opt_output(const uint8_t* k, LoclassState_t* s, uint8_t* buffer) {
+#pragma GCC unroll 4
+    for(uint8_t times = 0; times < 4; times++) {
+        uint8_t bout = 0;
+        bout |= (s->r & 0x4) >> 2;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) >> 1;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4);
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) << 1;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) << 2;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) << 3;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) << 4;
+        loclass_opt_successor(k, s, 0);
+        bout |= (s->r & 0x4) << 5;
+        loclass_opt_successor(k, s, 0);
+        buffer[times] = bout;
+    }
+}
+
+static void loclass_opt_MAC(uint8_t* k, uint8_t* input, uint8_t* out) {
+    LoclassState_t _init = {
+        ((k[0] ^ 0x4c) + 0xEC) & 0xFF, // l
+        ((k[0] ^ 0x4c) + 0x21) & 0xFF, // r
+        0x4c, // b
+        0xE012 // t
+    };
+
+    loclass_opt_suc(k, &_init, input, 12, false);
+    loclass_opt_output(k, &_init, out);
+}
+
+static void loclass_opt_MAC_N(uint8_t* k, uint8_t* input, uint8_t in_size, uint8_t* out) {
+    LoclassState_t _init = {
+        ((k[0] ^ 0x4c) + 0xEC) & 0xFF, // l
+        ((k[0] ^ 0x4c) + 0x21) & 0xFF, // r
+        0x4c, // b
+        0xE012 // t
+    };
+
+    loclass_opt_suc(k, &_init, input, in_size, false);
+    loclass_opt_output(k, &_init, out);
+}
+
+void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4]) {
+    uint8_t dest[] = {0, 0, 0, 0, 0, 0, 0, 0};
+    loclass_opt_MAC(div_key_p, cc_nr_p, dest);
+    memcpy(mac, dest, 4);
+}
+
+void loclass_opt_doReaderMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t mac[4],
+    const uint8_t* div_key_p) {
+    loclass_opt_suc(div_key_p, &_init, nr, 4, false);
+    loclass_opt_output(div_key_p, &_init, mac);
+}
+
+void loclass_doMAC_N(uint8_t* in_p, uint8_t in_size, uint8_t* div_key_p, uint8_t mac[4]) {
+    uint8_t dest[] = {0, 0, 0, 0, 0, 0, 0, 0};
+    loclass_opt_MAC_N(div_key_p, in_p, in_size, dest);
+    memcpy(mac, dest, 4);
+}
+
+void loclass_opt_doTagMAC(uint8_t* cc_p, const uint8_t* div_key_p, uint8_t mac[4]) {
+    LoclassState_t _init = {
+        ((div_key_p[0] ^ 0x4c) + 0xEC) & 0xFF, // l
+        ((div_key_p[0] ^ 0x4c) + 0x21) & 0xFF, // r
+        0x4c, // b
+        0xE012 // t
+    };
+    loclass_opt_suc(div_key_p, &_init, cc_p, 12, true);
+    loclass_opt_output(div_key_p, &_init, mac);
+}
+
+/**
+ * The tag MAC can be divided (both can, but no point in dividing the reader mac) into
+ * two functions, since the first 8 bytes are known, we can pre-calculate the state
+ * reached after feeding CC to the cipher.
+ * @param cc_p
+ * @param div_key_p
+ * @return the cipher state
+ */
+LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p) {
+    LoclassState_t _init = {
+        ((div_key_p[0] ^ 0x4c) + 0xEC) & 0xFF, // l
+        ((div_key_p[0] ^ 0x4c) + 0x21) & 0xFF, // r
+        0x4c, // b
+        0xE012 // t
+    };
+    loclass_opt_suc(div_key_p, &_init, cc_p, 8, false);
+    return _init;
+}
+
+/**
+ * The second part of the tag MAC calculation, since the CC is already calculated into the state,
+ * this function is fed only the NR, and internally feeds the remaining 32 0-bits to generate the tag
+ * MAC response.
+ * @param _init - precalculated cipher state
+ * @param nr - the reader challenge
+ * @param mac - where to store the MAC
+ * @param div_key_p - the key to use
+ */
+void loclass_opt_doTagMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t mac[4],
+    const uint8_t* div_key_p) {
+    loclass_opt_suc(div_key_p, &_init, nr, 4, true);
+    loclass_opt_output(div_key_p, &_init, mac);
+}
+
+/**
+ * The second part of the tag MAC calculation, since the CC is already calculated into the state,
+ * this function is fed only the NR, and generates both the reader and tag MACs.
+ * @param _init - precalculated cipher state
+ * @param nr - the reader challenge
+ * @param rmac - where to store the reader MAC
+ * @param tmac - where to store the tag MAC
+ * @param div_key_p - the key to use
+ */
+void loclass_opt_doBothMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t rmac[4],
+    uint8_t tmac[4],
+    const uint8_t* div_key_p) {
+    LoclassState_t* s = &_init;
+    loclass_opt_suc(div_key_p, s, nr, 4, false);
+    loclass_opt_output(div_key_p, s, rmac);
+    loclass_opt_output(div_key_p, s, tmac);
+}
+
+void loclass_iclass_calc_div_key(
+    const uint8_t* csn,
+    const uint8_t* key,
+    uint8_t* div_key,
+    bool elite) {
+    if(elite) {
+        uint8_t keytable[128] = {0};
+        uint8_t key_index[8] = {0};
+        uint8_t key_sel[8] = {0};
+        uint8_t key_sel_p[8] = {0};
+        loclass_hash2(key, keytable);
+        loclass_hash1(csn, key_index);
+        for(uint8_t i = 0; i < 8; i++) key_sel[i] = keytable[key_index[i]];
+
+        //Permute from iclass format to standard format
+        loclass_permutekey_rev(key_sel, key_sel_p);
+        loclass_diversifyKey(csn, key_sel_p, div_key);
+    } else {
+        loclass_diversifyKey(csn, key, div_key);
+    }
+}

+ 117 - 0
picopass/lib/loclass/optimized_cipher.h

@@ -0,0 +1,117 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// More recently from https://github.com/RfidResearchGroup/proxmark3
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#ifndef OPTIMIZED_CIPHER_H
+#define OPTIMIZED_CIPHER_H
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+/**
+* Definition 1 (Cipher state). A cipher state of iClass s is an element of F 40/2
+* consisting of the following four components:
+*   1. the left register l = (l 0 . . . l 7 ) ∈ F 8/2 ;
+*   2. the right register r = (r 0 . . . r 7 ) ∈ F 8/2 ;
+*   3. the top register t = (t 0 . . . t 15 ) ∈ F 16/2 .
+*   4. the bottom register b = (b 0 . . . b 7 ) ∈ F 8/2 .
+**/
+typedef struct {
+    uint8_t l;
+    uint8_t r;
+    uint8_t b;
+    uint16_t t;
+} LoclassState_t;
+
+/** The reader MAC is MAC(key, CC * NR )
+ **/
+void loclass_opt_doReaderMAC(uint8_t* cc_nr_p, uint8_t* div_key_p, uint8_t mac[4]);
+
+void loclass_opt_doReaderMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t mac[4],
+    const uint8_t* div_key_p);
+
+/**
+ * The tag MAC is MAC(key, CC * NR * 32x0))
+ */
+void loclass_opt_doTagMAC(uint8_t* cc_p, const uint8_t* div_key_p, uint8_t mac[4]);
+
+/**
+ * The tag MAC can be divided (both can, but no point in dividing the reader mac) into
+ * two functions, since the first 8 bytes are known, we can pre-calculate the state
+ * reached after feeding CC to the cipher.
+ * @param cc_p
+ * @param div_key_p
+ * @return the cipher state
+ */
+LoclassState_t loclass_opt_doTagMAC_1(uint8_t* cc_p, const uint8_t* div_key_p);
+/**
+ * The second part of the tag MAC calculation, since the CC is already calculated into the state,
+ * this function is fed only the NR, and internally feeds the remaining 32 0-bits to generate the tag
+ * MAC response.
+ * @param _init - precalculated cipher state
+ * @param nr - the reader challenge
+ * @param mac - where to store the MAC
+ * @param div_key_p - the key to use
+ */
+void loclass_opt_doTagMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t mac[4],
+    const uint8_t* div_key_p);
+
+/**
+ * The same as loclass_opt_doTagMAC_2, but calculates both the reader and tag MACs at the same time
+ * @param _init - precalculated cipher state
+ * @param nr - the reader challenge
+ * @param rmac - where to store the reader MAC
+ * @param tmac - where to store the tag MAC
+ * @param div_key_p - the key to use
+ */
+void loclass_opt_doBothMAC_2(
+    LoclassState_t _init,
+    uint8_t* nr,
+    uint8_t rmac[4],
+    uint8_t tmac[4],
+    const uint8_t* div_key_p);
+
+void loclass_doMAC_N(uint8_t* in_p, uint8_t in_size, uint8_t* div_key_p, uint8_t mac[4]);
+void loclass_iclass_calc_div_key(
+    const uint8_t* csn,
+    const uint8_t* key,
+    uint8_t* div_key,
+    bool elite);
+#endif // OPTIMIZED_CIPHER_H

+ 136 - 0
picopass/lib/loclass/optimized_cipherutils.c

@@ -0,0 +1,136 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#include "optimized_cipherutils.h"
+#include <stdint.h>
+
+/**
+ *
+ * @brief Return and remove the first bit (x0) in the stream : <x0 x1 x2 x3 ... xn >
+ * @param stream
+ * @return
+ */
+bool loclass_headBit(LoclassBitstreamIn_t* stream) {
+    int bytepos = stream->position >> 3; // divide by 8
+    int bitpos = (stream->position++) & 7; // mask out 00000111
+    return (*(stream->buffer + bytepos) >> (7 - bitpos)) & 1;
+}
+/**
+ * @brief Return and remove the last bit (xn) in the stream: <x0 x1 x2 ... xn>
+ * @param stream
+ * @return
+ */
+bool loclass_tailBit(LoclassBitstreamIn_t* stream) {
+    int bitpos = stream->numbits - 1 - (stream->position++);
+
+    int bytepos = bitpos >> 3;
+    bitpos &= 7;
+    return (*(stream->buffer + bytepos) >> (7 - bitpos)) & 1;
+}
+/**
+ * @brief Pushes bit onto the stream
+ * @param stream
+ * @param bit
+ */
+void loclass_pushBit(LoclassBitstreamOut_t* stream, bool bit) {
+    int bytepos = stream->position >> 3; // divide by 8
+    int bitpos = stream->position & 7;
+    *(stream->buffer + bytepos) |= (bit) << (7 - bitpos);
+    stream->position++;
+    stream->numbits++;
+}
+
+/**
+ * @brief Pushes the lower six bits onto the stream
+ * as b0 b1 b2 b3 b4 b5 b6
+ * @param stream
+ * @param bits
+ */
+void loclass_push6bits(LoclassBitstreamOut_t* stream, uint8_t bits) {
+    loclass_pushBit(stream, bits & 0x20);
+    loclass_pushBit(stream, bits & 0x10);
+    loclass_pushBit(stream, bits & 0x08);
+    loclass_pushBit(stream, bits & 0x04);
+    loclass_pushBit(stream, bits & 0x02);
+    loclass_pushBit(stream, bits & 0x01);
+}
+
+/**
+ * @brief loclass_bitsLeft
+ * @param stream
+ * @return number of bits left in stream
+ */
+int loclass_bitsLeft(LoclassBitstreamIn_t* stream) {
+    return stream->numbits - stream->position;
+}
+/**
+ * @brief numBits
+ * @param stream
+ * @return Number of bits stored in stream
+ */
+void loclass_x_num_to_bytes(uint64_t n, size_t len, uint8_t* dest) {
+    while(len--) {
+        dest[len] = (uint8_t)n;
+        n >>= 8;
+    }
+}
+
+uint64_t loclass_x_bytes_to_num(uint8_t* src, size_t len) {
+    uint64_t num = 0;
+    while(len--) {
+        num = (num << 8) | (*src);
+        src++;
+    }
+    return num;
+}
+
+uint8_t loclass_reversebytes(uint8_t b) {
+    b = (b & 0xF0) >> 4 | (b & 0x0F) << 4;
+    b = (b & 0xCC) >> 2 | (b & 0x33) << 2;
+    b = (b & 0xAA) >> 1 | (b & 0x55) << 1;
+    return b;
+}
+
+void loclass_reverse_arraybytes(uint8_t* arr, size_t len) {
+    uint8_t i;
+    for(i = 0; i < len; i++) {
+        arr[i] = loclass_reversebytes(arr[i]);
+    }
+}
+
+void loclass_reverse_arraycopy(uint8_t* arr, uint8_t* dest, size_t len) {
+    uint8_t i;
+    for(i = 0; i < len; i++) {
+        dest[i] = loclass_reversebytes(arr[i]);
+    }
+}

+ 64 - 0
picopass/lib/loclass/optimized_cipherutils.h

@@ -0,0 +1,64 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// More recently from https://github.com/RfidResearchGroup/proxmark3
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#ifndef CIPHERUTILS_H
+#define CIPHERUTILS_H
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+typedef struct {
+    uint8_t* buffer;
+    uint8_t numbits;
+    uint8_t position;
+} LoclassBitstreamIn_t;
+
+typedef struct {
+    uint8_t* buffer;
+    uint8_t numbits;
+    uint8_t position;
+} LoclassBitstreamOut_t;
+
+bool loclass_headBit(LoclassBitstreamIn_t* stream);
+bool loclass_tailBit(LoclassBitstreamIn_t* stream);
+void loclass_pushBit(LoclassBitstreamOut_t* stream, bool bit);
+int loclass_bitsLeft(LoclassBitstreamIn_t* stream);
+
+void loclass_push6bits(LoclassBitstreamOut_t* stream, uint8_t bits);
+void loclass_x_num_to_bytes(uint64_t n, size_t len, uint8_t* dest);
+uint64_t loclass_x_bytes_to_num(uint8_t* src, size_t len);
+uint8_t loclass_reversebytes(uint8_t b);
+void loclass_reverse_arraybytes(uint8_t* arr, size_t len);
+void loclass_reverse_arraycopy(uint8_t* arr, uint8_t* dest, size_t len);
+#endif // CIPHERUTILS_H

+ 232 - 0
picopass/lib/loclass/optimized_elite.c

@@ -0,0 +1,232 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#include "optimized_elite.h"
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include <mbedtls/des.h>
+#include "optimized_ikeys.h"
+
+/**
+ * @brief Permutes a key from standard NIST format to Iclass specific format
+ *  from http://www.proxmark.org/forum/viewtopic.php?pid=11220#p11220
+ *
+ *  If you loclass_permute [6c 8d 44 f9 2a 2d 01 bf]  you get  [8a 0d b9 88 bb a7 90 ea]  as shown below.
+ *
+ *  1 0 1 1 1 1 1 1  bf
+ *  0 0 0 0 0 0 0 1  01
+ *  0 0 1 0 1 1 0 1  2d
+ *  0 0 1 0 1 0 1 0  2a
+ *  1 1 1 1 1 0 0 1  f9
+ *  0 1 0 0 0 1 0 0  44
+ *  1 0 0 0 1 1 0 1  8d
+ *  0 1 1 0 1 1 0 0  6c
+ *
+ *  8 0 b 8 b a 9 e
+ *  a d 9 8 b 7 0 a
+ *
+ * @param key
+ * @param dest
+ */
+void loclass_permutekey(const uint8_t key[8], uint8_t dest[8]) {
+    int i;
+    for(i = 0; i < 8; i++) {
+        dest[i] = (((key[7] & (0x80 >> i)) >> (7 - i)) << 7) |
+                  (((key[6] & (0x80 >> i)) >> (7 - i)) << 6) |
+                  (((key[5] & (0x80 >> i)) >> (7 - i)) << 5) |
+                  (((key[4] & (0x80 >> i)) >> (7 - i)) << 4) |
+                  (((key[3] & (0x80 >> i)) >> (7 - i)) << 3) |
+                  (((key[2] & (0x80 >> i)) >> (7 - i)) << 2) |
+                  (((key[1] & (0x80 >> i)) >> (7 - i)) << 1) |
+                  (((key[0] & (0x80 >> i)) >> (7 - i)) << 0);
+    }
+}
+/**
+ * Permutes  a key from iclass specific format to NIST format
+ * @brief loclass_permutekey_rev
+ * @param key
+ * @param dest
+ */
+void loclass_permutekey_rev(const uint8_t key[8], uint8_t dest[8]) {
+    int i;
+    for(i = 0; i < 8; i++) {
+        dest[7 - i] = (((key[0] & (0x80 >> i)) >> (7 - i)) << 7) |
+                      (((key[1] & (0x80 >> i)) >> (7 - i)) << 6) |
+                      (((key[2] & (0x80 >> i)) >> (7 - i)) << 5) |
+                      (((key[3] & (0x80 >> i)) >> (7 - i)) << 4) |
+                      (((key[4] & (0x80 >> i)) >> (7 - i)) << 3) |
+                      (((key[5] & (0x80 >> i)) >> (7 - i)) << 2) |
+                      (((key[6] & (0x80 >> i)) >> (7 - i)) << 1) |
+                      (((key[7] & (0x80 >> i)) >> (7 - i)) << 0);
+    }
+}
+
+/**
+ * Helper function for loclass_hash1
+ * @brief loclass_rr
+ * @param val
+ * @return
+ */
+static uint8_t loclass_rr(uint8_t val) {
+    return val >> 1 | ((val & 1) << 7);
+}
+
+/**
+ * Helper function for loclass_hash1
+ * @brief rl
+ * @param val
+ * @return
+ */
+static uint8_t loclass_rl(uint8_t val) {
+    return val << 1 | ((val & 0x80) >> 7);
+}
+
+/**
+ * Helper function for loclass_hash1
+ * @brief loclass_swap
+ * @param val
+ * @return
+ */
+static uint8_t loclass_swap(uint8_t val) {
+    return ((val >> 4) & 0xFF) | ((val & 0xFF) << 4);
+}
+
+/**
+ * Hash1 takes CSN as input, and determines what bytes in the keytable will be used
+ * when constructing the K_sel.
+ * @param csn the CSN used
+ * @param k output
+ */
+void loclass_hash1(const uint8_t csn[], uint8_t k[]) {
+    k[0] = csn[0] ^ csn[1] ^ csn[2] ^ csn[3] ^ csn[4] ^ csn[5] ^ csn[6] ^ csn[7];
+    k[1] = csn[0] + csn[1] + csn[2] + csn[3] + csn[4] + csn[5] + csn[6] + csn[7];
+    k[2] = loclass_rr(loclass_swap(csn[2] + k[1]));
+    k[3] = loclass_rl(loclass_swap(csn[3] + k[0]));
+    k[4] = ~loclass_rr(csn[4] + k[2]) + 1;
+    k[5] = ~loclass_rl(csn[5] + k[3]) + 1;
+    k[6] = loclass_rr(csn[6] + (k[4] ^ 0x3c));
+    k[7] = loclass_rl(csn[7] + (k[5] ^ 0xc3));
+
+    k[7] &= 0x7F;
+    k[6] &= 0x7F;
+    k[5] &= 0x7F;
+    k[4] &= 0x7F;
+    k[3] &= 0x7F;
+    k[2] &= 0x7F;
+    k[1] &= 0x7F;
+    k[0] &= 0x7F;
+}
+/**
+Definition 14. Define the rotate key function loclass_rk : (F 82 ) 8 × N → (F 82 ) 8 as
+loclass_rk(x [0] . . . x [7] , 0) = x [0] . . . x [7]
+loclass_rk(x [0] . . . x [7] , n + 1) = loclass_rk(loclass_rl(x [0] ) . . . loclass_rl(x [7] ), n)
+**/
+static void loclass_rk(const uint8_t* key, uint8_t n, uint8_t* outp_key) {
+    memcpy(outp_key, key, 8);
+    uint8_t j;
+    while(n-- > 0) {
+        for(j = 0; j < 8; j++) outp_key[j] = loclass_rl(outp_key[j]);
+    }
+    return;
+}
+
+static mbedtls_des_context loclass_ctx_enc;
+static mbedtls_des_context loclass_ctx_dec;
+
+static void loclass_desdecrypt_iclass(uint8_t* iclass_key, uint8_t* input, uint8_t* output) {
+    uint8_t key_std_format[8] = {0};
+    loclass_permutekey_rev(iclass_key, key_std_format);
+    mbedtls_des_setkey_dec(&loclass_ctx_dec, key_std_format);
+    mbedtls_des_crypt_ecb(&loclass_ctx_dec, input, output);
+}
+
+static void loclass_desencrypt_iclass(const uint8_t* iclass_key, uint8_t* input, uint8_t* output) {
+    uint8_t key_std_format[8] = {0};
+    loclass_permutekey_rev(iclass_key, key_std_format);
+    mbedtls_des_setkey_enc(&loclass_ctx_enc, key_std_format);
+    mbedtls_des_crypt_ecb(&loclass_ctx_enc, input, output);
+}
+
+/**
+ * @brief Insert uint8_t[8] custom master key to calculate hash2 and return key_select.
+ * @param key unpermuted custom key
+ * @param loclass_hash1 loclass_hash1
+ * @param key_sel output key_sel=h[loclass_hash1[i]]
+ */
+void loclass_hash2(const uint8_t* key64, uint8_t* outp_keytable) {
+    /**
+     *Expected:
+     * High Security Key Table
+
+    00  F1 35 59 A1 0D 5A 26 7F 18 60 0B 96 8A C0 25 C1
+    10  BF A1 3B B0 FF 85 28 75 F2 1F C6 8F 0E 74 8F 21
+    20  14 7A 55 16 C8 A9 7D B3 13 0C 5D C9 31 8D A9 B2
+    30  A3 56 83 0F 55 7E DE 45 71 21 D2 6D C1 57 1C 9C
+    40  78 2F 64 51 42 7B 64 30 FA 26 51 76 D3 E0 FB B6
+    50  31 9F BF 2F 7E 4F 94 B4 BD 4F 75 91 E3 1B EB 42
+    60  3F 88 6F B8 6C 2C 93 0D 69 2C D5 20 3C C1 61 95
+    70  43 08 A0 2F FE B3 26 D7 98 0B 34 7B 47 70 A0 AB
+
+    **** The 64-bit HS Custom Key Value = 5B7C62C491C11B39 ******/
+    uint8_t key64_negated[8] = {0};
+    uint8_t z[8][8] = {{0}, {0}};
+    uint8_t temp_output[8] = {0};
+
+    //calculate complement of key
+    int i;
+    for(i = 0; i < 8; i++) key64_negated[i] = ~key64[i];
+
+    // Once again, key is on iclass-format
+    loclass_desencrypt_iclass(key64, key64_negated, z[0]);
+
+    uint8_t y[8][8] = {{0}, {0}};
+
+    // y[0]=DES_dec(z[0],~key)
+    // Once again, key is on iclass-format
+    loclass_desdecrypt_iclass(z[0], key64_negated, y[0]);
+
+    for(i = 1; i < 8; i++) {
+        loclass_rk(key64, i, temp_output);
+        loclass_desdecrypt_iclass(temp_output, z[i - 1], z[i]);
+        loclass_desencrypt_iclass(temp_output, y[i - 1], y[i]);
+    }
+
+    if(outp_keytable != NULL) {
+        for(i = 0; i < 8; i++) {
+            memcpy(outp_keytable + i * 16, y[i], 8);
+            memcpy(outp_keytable + 8 + i * 16, z[i], 8);
+        }
+    }
+}

+ 58 - 0
picopass/lib/loclass/optimized_elite.h

@@ -0,0 +1,58 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// More recently from https://github.com/RfidResearchGroup/proxmark3
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#ifndef ELITE_CRACK_H
+#define ELITE_CRACK_H
+
+#include <stdint.h>
+#include <stdlib.h>
+
+void loclass_permutekey(const uint8_t key[8], uint8_t dest[8]);
+/**
+ * Permutes  a key from iclass specific format to NIST format
+ * @brief loclass_permutekey_rev
+ * @param key
+ * @param dest
+ */
+void loclass_permutekey_rev(const uint8_t key[8], uint8_t dest[8]);
+/**
+ * Hash1 takes CSN as input, and determines what bytes in the keytable will be used
+ * when constructing the K_sel.
+ * @param csn the CSN used
+ * @param k output
+ */
+void loclass_hash1(const uint8_t* csn, uint8_t* k);
+void loclass_hash2(const uint8_t* key64, uint8_t* outp_keytable);
+
+#endif

+ 320 - 0
picopass/lib/loclass/optimized_ikeys.c

@@ -0,0 +1,320 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+
+/**
+From "Dismantling iclass":
+    This section describes in detail the built-in key diversification algorithm of iClass.
+    Besides the obvious purpose of deriving a card key from a master key, this
+    algorithm intends to circumvent weaknesses in the cipher by preventing the
+    usage of certain ‘weak’ keys. In order to compute a diversified key, the iClass
+    reader first encrypts the card identity id with the master key K, using single
+    DES. The resulting ciphertext is then input to a function called loclass_hash0 which
+    outputs the diversified key k.
+
+    k = loclass_hash0(DES enc (id, K))
+
+    Here the DES encryption of id with master key K outputs a cryptogram c
+    of 64 bits. These 64 bits are divided as c = x, y, z [0] , . . . , z [7] ∈ F 82 × F 82 × (F 62 ) 8
+    which is used as input to the loclass_hash0 function. This function introduces some
+    obfuscation by performing a number of permutations, complement and modulo
+    operations, see Figure 2.5. Besides that, it checks for and removes patterns like
+    similar key bytes, which could produce a strong bias in the cipher. Finally, the
+    output of loclass_hash0 is the diversified card key k = k [0] , . . . , k [7] ∈ (F 82 ) 8 .
+
+**/
+#include "optimized_ikeys.h"
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <inttypes.h>
+#include <mbedtls/des.h>
+#include "optimized_cipherutils.h"
+
+static const uint8_t loclass_pi[35] = {0x0F, 0x17, 0x1B, 0x1D, 0x1E, 0x27, 0x2B, 0x2D, 0x2E,
+                                       0x33, 0x35, 0x39, 0x36, 0x3A, 0x3C, 0x47, 0x4B, 0x4D,
+                                       0x4E, 0x53, 0x55, 0x56, 0x59, 0x5A, 0x5C, 0x63, 0x65,
+                                       0x66, 0x69, 0x6A, 0x6C, 0x71, 0x72, 0x74, 0x78};
+
+/**
+ * @brief The key diversification algorithm uses 6-bit bytes.
+ * This implementation uses 64 bit uint to pack seven of them into one
+ * variable. When they are there, they are placed as follows:
+ * XXXX XXXX N0 .... N7, occupying the last 48 bits.
+ *
+ * This function picks out one from such a collection
+ * @param all
+ * @param n bitnumber
+ * @return
+ */
+static uint8_t loclass_getSixBitByte(uint64_t c, int n) {
+    return (c >> (42 - 6 * n)) & 0x3F;
+}
+
+/**
+ * @brief Puts back a six-bit 'byte' into a uint64_t.
+ * @param c buffer
+ * @param z the value to place there
+ * @param n bitnumber.
+ */
+static void loclass_pushbackSixBitByte(uint64_t* c, uint8_t z, int n) {
+    //0x XXXX YYYY ZZZZ ZZZZ ZZZZ
+    //             ^z0         ^z7
+    //z0:  1111 1100 0000 0000
+
+    uint64_t masked = z & 0x3F;
+    uint64_t eraser = 0x3F;
+    masked <<= 42 - 6 * n;
+    eraser <<= 42 - 6 * n;
+
+    //masked <<= 6*n;
+    //eraser <<= 6*n;
+
+    eraser = ~eraser;
+    (*c) &= eraser;
+    (*c) |= masked;
+}
+/**
+ * @brief Swaps the z-values.
+ * If the input value has format XYZ0Z1...Z7, the output will have the format
+ * XYZ7Z6...Z0 instead
+ * @param c
+ * @return
+ */
+static uint64_t loclass_swapZvalues(uint64_t c) {
+    uint64_t newz = 0;
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 0), 7);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 1), 6);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 2), 5);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 3), 4);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 4), 3);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 5), 2);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 6), 1);
+    loclass_pushbackSixBitByte(&newz, loclass_getSixBitByte(c, 7), 0);
+    newz |= (c & 0xFFFF000000000000);
+    return newz;
+}
+
+/**
+* @return 4 six-bit bytes chunked into a uint64_t,as 00..00a0a1a2a3
+*/
+static uint64_t loclass_ck(int i, int j, uint64_t z) {
+    if(i == 1 && j == -1) {
+        // loclass_ck(1, −1, z [0] . . . z [3] ) = z [0] . . . z [3]
+        return z;
+    } else if(j == -1) {
+        // loclass_ck(i, −1, z [0] . . . z [3] ) = loclass_ck(i − 1, i − 2, z [0] . . . z [3] )
+        return loclass_ck(i - 1, i - 2, z);
+    }
+
+    if(loclass_getSixBitByte(z, i) == loclass_getSixBitByte(z, j)) {
+        //loclass_ck(i, j − 1, z [0] . . . z [i] ← j . . . z [3] )
+        uint64_t newz = 0;
+        int c;
+        for(c = 0; c < 4; c++) {
+            uint8_t val = loclass_getSixBitByte(z, c);
+            if(c == i)
+                loclass_pushbackSixBitByte(&newz, j, c);
+            else
+                loclass_pushbackSixBitByte(&newz, val, c);
+        }
+        return loclass_ck(i, j - 1, newz);
+    } else {
+        return loclass_ck(i, j - 1, z);
+    }
+}
+/**
+
+    Definition 8.
+    Let the function check : (F 62 ) 8 → (F 62 ) 8 be defined as
+    check(z [0] . . . z [7] ) = loclass_ck(3, 2, z [0] . . . z [3] ) · loclass_ck(3, 2, z [4] . . . z [7] )
+
+    where loclass_ck : N × N × (F 62 ) 4 → (F 62 ) 4 is defined as
+
+        loclass_ck(1, −1, z [0] . . . z [3] ) = z [0] . . . z [3]
+        loclass_ck(i, −1, z [0] . . . z [3] ) = loclass_ck(i − 1, i − 2, z [0] . . . z [3] )
+        loclass_ck(i, j, z [0] . . . z [3] ) =
+        loclass_ck(i, j − 1, z [0] . . . z [i] ← j . . . z [3] ),  if z [i] = z [j] ;
+        loclass_ck(i, j − 1, z [0] . . . z [3] ), otherwise
+
+    otherwise.
+**/
+
+static uint64_t loclass_check(uint64_t z) {
+    //These 64 bits are divided as c = x, y, z [0] , . . . , z [7]
+
+    // loclass_ck(3, 2, z [0] . . . z [3] )
+    uint64_t ck1 = loclass_ck(3, 2, z);
+
+    // loclass_ck(3, 2, z [4] . . . z [7] )
+    uint64_t ck2 = loclass_ck(3, 2, z << 24);
+
+    //The loclass_ck function will place the values
+    // in the middle of z.
+    ck1 &= 0x00000000FFFFFF000000;
+    ck2 &= 0x00000000FFFFFF000000;
+
+    return ck1 | ck2 >> 24;
+}
+
+static void loclass_permute(
+    LoclassBitstreamIn_t* p_in,
+    uint64_t z,
+    int l,
+    int r,
+    LoclassBitstreamOut_t* out) {
+    if(loclass_bitsLeft(p_in) == 0) return;
+
+    bool pn = loclass_tailBit(p_in);
+    if(pn) { // pn = 1
+        uint8_t zl = loclass_getSixBitByte(z, l);
+
+        loclass_push6bits(out, zl + 1);
+        loclass_permute(p_in, z, l + 1, r, out);
+    } else { // otherwise
+        uint8_t zr = loclass_getSixBitByte(z, r);
+
+        loclass_push6bits(out, zr);
+        loclass_permute(p_in, z, l, r + 1, out);
+    }
+}
+
+/**
+ * @brief
+ *Definition 11. Let the function loclass_hash0 : F 82 × F 82 × (F 62 ) 8 → (F 82 ) 8 be defined as
+ *  loclass_hash0(x, y, z [0] . . . z [7] ) = k [0] . . . k [7] where
+ * z'[i] = (z[i] mod (63-i)) + i      i =  0...3
+ * z'[i+4] = (z[i+4] mod (64-i)) + i  i =  0...3
+ * ẑ = check(z');
+ * @param c
+ * @param k this is where the diversified key is put (should be 8 bytes)
+ * @return
+ */
+void loclass_hash0(uint64_t c, uint8_t k[8]) {
+    c = loclass_swapZvalues(c);
+
+    //These 64 bits are divided as c = x, y, z [0] , . . . , z [7]
+    // x = 8 bits
+    // y = 8 bits
+    // z0-z7 6 bits each : 48 bits
+    uint8_t x = (c & 0xFF00000000000000) >> 56;
+    uint8_t y = (c & 0x00FF000000000000) >> 48;
+    uint64_t zP = 0;
+
+    for(int n = 0; n < 4; n++) {
+        uint8_t zn = loclass_getSixBitByte(c, n);
+        uint8_t zn4 = loclass_getSixBitByte(c, n + 4);
+        uint8_t _zn = (zn % (63 - n)) + n;
+        uint8_t _zn4 = (zn4 % (64 - n)) + n;
+        loclass_pushbackSixBitByte(&zP, _zn, n);
+        loclass_pushbackSixBitByte(&zP, _zn4, n + 4);
+    }
+
+    uint64_t zCaret = loclass_check(zP);
+    uint8_t p = loclass_pi[x % 35];
+
+    if(x & 1) //Check if x7 is 1
+        p = ~p;
+
+    LoclassBitstreamIn_t p_in = {&p, 8, 0};
+    uint8_t outbuffer[] = {0, 0, 0, 0, 0, 0, 0, 0};
+    LoclassBitstreamOut_t out = {outbuffer, 0, 0};
+    loclass_permute(&p_in, zCaret, 0, 4, &out); //returns 48 bits? or 6 8-bytes
+
+    //Out is now a buffer containing six-bit bytes, should be 48 bits
+    // if all went well
+    //Shift z-values down onto the lower segment
+
+    uint64_t zTilde = loclass_x_bytes_to_num(outbuffer, sizeof(outbuffer));
+
+    zTilde >>= 16;
+
+    for(int i = 0; i < 8; i++) {
+        // the key on index i is first a bit from y
+        // then six bits from z,
+        // then a bit from p
+
+        // Init with zeroes
+        k[i] = 0;
+        // First, place yi leftmost in k
+        //k[i] |= (y  << i) & 0x80 ;
+
+        // First, place y(7-i) leftmost in k
+        k[i] |= (y << (7 - i)) & 0x80;
+
+        uint8_t zTilde_i = loclass_getSixBitByte(zTilde, i);
+        // zTildeI is now on the form 00XXXXXX
+        // with one leftshift, it'll be
+        // 0XXXXXX0
+        // So after leftshift, we can OR it into k
+        // However, when doing complement, we need to
+        // again MASK 0XXXXXX0 (0x7E)
+        zTilde_i <<= 1;
+
+        //Finally, add bit from p or p-mod
+        //Shift bit i into rightmost location (mask only after complement)
+        uint8_t p_i = p >> i & 0x1;
+
+        if(k[i]) { // yi = 1
+            k[i] |= ~zTilde_i & 0x7E;
+            k[i] |= p_i & 1;
+            k[i] += 1;
+
+        } else { // otherwise
+            k[i] |= zTilde_i & 0x7E;
+            k[i] |= (~p_i) & 1;
+        }
+    }
+}
+/**
+ * @brief Performs Elite-class key diversification
+ * @param csn
+ * @param key
+ * @param div_key
+ */
+void loclass_diversifyKey(const uint8_t* csn, const uint8_t* key, uint8_t* div_key) {
+    mbedtls_des_context loclass_ctx_enc;
+
+    // Prepare the DES key
+    mbedtls_des_setkey_enc(&loclass_ctx_enc, key);
+
+    uint8_t crypted_csn[8] = {0};
+
+    // Calculate DES(CSN, KEY)
+    mbedtls_des_crypt_ecb(&loclass_ctx_enc, csn, crypted_csn);
+
+    //Calculate HASH0(DES))
+    uint64_t c_csn = loclass_x_bytes_to_num(crypted_csn, sizeof(crypted_csn));
+
+    loclass_hash0(c_csn, div_key);
+}

+ 66 - 0
picopass/lib/loclass/optimized_ikeys.h

@@ -0,0 +1,66 @@
+//-----------------------------------------------------------------------------
+// Borrowed initially from https://github.com/holiman/loclass
+// More recently from https://github.com/RfidResearchGroup/proxmark3
+// Copyright (C) 2014 Martin Holst Swende
+// Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
+//
+// 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.
+//
+// See LICENSE.txt for the text of the license.
+//-----------------------------------------------------------------------------
+// WARNING
+//
+// THIS CODE IS CREATED FOR EXPERIMENTATION AND EDUCATIONAL USE ONLY.
+//
+// USAGE OF THIS CODE IN OTHER WAYS MAY INFRINGE UPON THE INTELLECTUAL
+// PROPERTY OF OTHER PARTIES, SUCH AS INSIDE SECURE AND HID GLOBAL,
+// AND MAY EXPOSE YOU TO AN INFRINGEMENT ACTION FROM THOSE PARTIES.
+//
+// THIS CODE SHOULD NEVER BE USED TO INFRINGE PATENTS OR INTELLECTUAL PROPERTY RIGHTS.
+//-----------------------------------------------------------------------------
+// It is a reconstruction of the cipher engine used in iClass, and RFID techology.
+//
+// The implementation is based on the work performed by
+// Flavio D. Garcia, Gerhard de Koning Gans, Roel Verdult and
+// Milosch Meriac in the paper "Dismantling IClass".
+//-----------------------------------------------------------------------------
+#ifndef IKEYS_H
+#define IKEYS_H
+
+#include <inttypes.h>
+
+/**
+ * @brief
+ *Definition 11. Let the function loclass_hash0 : F 82 × F 82 × (F 62 ) 8 → (F 82 ) 8 be defined as
+ *  loclass_hash0(x, y, z [0] . . . z [7] ) = k [0] . . . k [7] where
+ * z'[i] = (z[i] mod (63-i)) + i        i =  0...3
+ * z'[i+4] = (z[i+4] mod (64-i)) + i    i =  0...3
+ * ẑ = check(z');
+ * @param c
+ * @param k this is where the diversified key is put (should be 8 bytes)
+ * @return
+ */
+void loclass_hash0(uint64_t c, uint8_t k[8]);
+/**
+ * @brief Performs Elite-class key diversification
+ * @param csn
+ * @param key
+ * @param div_key
+ */
+
+void loclass_diversifyKey(const uint8_t* csn, const uint8_t* key, uint8_t* div_key);
+/**
+ * @brief Permutes a key from standard NIST format to Iclass specific format
+ * @param key
+ * @param dest
+ */
+
+#endif // IKEYS_H

+ 102 - 0
picopass/loclass_writer.c

@@ -0,0 +1,102 @@
+#include "loclass_writer.h"
+
+#include <furi/furi.h>
+#include <furi_hal.h>
+#include <storage/storage.h>
+#include <stream/stream.h>
+#include <stream/buffered_file_stream.h>
+
+struct LoclassWriter {
+    Stream* file_stream;
+};
+
+#define LOCLASS_LOGS_PATH EXT_PATH("apps_data/picopass/.loclass.log")
+
+LoclassWriter* loclass_writer_alloc() {
+    LoclassWriter* instance = malloc(sizeof(LoclassWriter));
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    instance->file_stream = buffered_file_stream_alloc(storage);
+    if(!buffered_file_stream_open(
+           instance->file_stream, LOCLASS_LOGS_PATH, FSAM_WRITE, FSOM_OPEN_APPEND)) {
+        buffered_file_stream_close(instance->file_stream);
+        stream_free(instance->file_stream);
+        free(instance);
+        instance = NULL;
+    }
+
+    furi_record_close(RECORD_STORAGE);
+
+    return instance;
+}
+
+void loclass_writer_free(LoclassWriter* instance) {
+    furi_assert(instance != NULL);
+
+    buffered_file_stream_close(instance->file_stream);
+    stream_free(instance->file_stream);
+    free(instance);
+}
+
+bool loclass_writer_write_start_stop(LoclassWriter* instance, bool start) {
+    furi_assert(instance != NULL);
+
+    FuriHalRtcDateTime curr_dt;
+    furi_hal_rtc_get_datetime(&curr_dt);
+    uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
+
+    FuriString* str = furi_string_alloc_printf(
+        "loclass-v1-info ts %lu %s\n", curr_ts, start ? "started" : "finished");
+    bool write_success = stream_write_string(instance->file_stream, str);
+    furi_string_free(str);
+    return write_success;
+}
+
+bool loclass_writer_write_params(
+    LoclassWriter* instance,
+    uint8_t log_no,
+    const uint8_t csn[8],
+    const uint8_t epurse[8],
+    const uint8_t nr[4],
+    const uint8_t mac[4]) {
+    furi_assert(instance != NULL);
+
+    FuriHalRtcDateTime curr_dt;
+    furi_hal_rtc_get_datetime(&curr_dt);
+    uint32_t curr_ts = furi_hal_rtc_datetime_to_timestamp(&curr_dt);
+
+    FuriString* str = furi_string_alloc_printf(
+        "loclass-v1-mac ts %lu no %u "
+        "csn %02x%02x%02x%02x%02x%02x%02x%02x "
+        "cc %02x%02x%02x%02x%02x%02x%02x%02x "
+        "nr %02x%02x%02x%02x "
+        "mac %02x%02x%02x%02x\n",
+        curr_ts,
+        log_no,
+        csn[0],
+        csn[1],
+        csn[2],
+        csn[3],
+        csn[4],
+        csn[5],
+        csn[6],
+        csn[7],
+        epurse[0],
+        epurse[1],
+        epurse[2],
+        epurse[3],
+        epurse[4],
+        epurse[5],
+        epurse[6],
+        epurse[7],
+        nr[0],
+        nr[1],
+        nr[2],
+        nr[3],
+        mac[0],
+        mac[1],
+        mac[2],
+        mac[3]);
+    bool write_success = stream_write_string(instance->file_stream, str);
+    furi_string_free(str);
+    return write_success;
+}

+ 20 - 0
picopass/loclass_writer.h

@@ -0,0 +1,20 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef struct LoclassWriter LoclassWriter;
+
+LoclassWriter* loclass_writer_alloc();
+
+void loclass_writer_free(LoclassWriter* instance);
+
+bool loclass_writer_write_start_stop(LoclassWriter* instance, bool start);
+
+bool loclass_writer_write_params(
+    LoclassWriter* instance,
+    uint8_t log_no,
+    const uint8_t csn[8],
+    const uint8_t epurse[8],
+    const uint8_t nr[4],
+    const uint8_t mac[4]);

+ 241 - 0
picopass/picopass.c

@@ -0,0 +1,241 @@
+#include "picopass_i.h"
+
+#define TAG "PicoPass"
+
+bool picopass_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    return scene_manager_handle_custom_event(picopass->scene_manager, event);
+}
+
+bool picopass_back_event_callback(void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    return scene_manager_handle_back_event(picopass->scene_manager);
+}
+
+void picopass_tick_event_callback(void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    scene_manager_handle_tick_event(picopass->scene_manager);
+}
+
+Picopass* picopass_alloc() {
+    Picopass* picopass = malloc(sizeof(Picopass));
+
+    picopass->worker = picopass_worker_alloc();
+    picopass->view_dispatcher = view_dispatcher_alloc();
+    picopass->scene_manager = scene_manager_alloc(&picopass_scene_handlers, picopass);
+    view_dispatcher_enable_queue(picopass->view_dispatcher);
+    view_dispatcher_set_event_callback_context(picopass->view_dispatcher, picopass);
+    view_dispatcher_set_custom_event_callback(
+        picopass->view_dispatcher, picopass_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        picopass->view_dispatcher, picopass_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        picopass->view_dispatcher, picopass_tick_event_callback, 100);
+
+    // Picopass device
+    picopass->dev = picopass_device_alloc();
+
+    // Open GUI record
+    picopass->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(
+        picopass->view_dispatcher, picopass->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    picopass->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    picopass->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher, PicopassViewMenu, submenu_get_view(picopass->submenu));
+
+    // Popup
+    picopass->popup = popup_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher, PicopassViewPopup, popup_get_view(picopass->popup));
+
+    // Loading
+    picopass->loading = loading_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher, PicopassViewLoading, loading_get_view(picopass->loading));
+
+    // Text Input
+    picopass->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher,
+        PicopassViewTextInput,
+        text_input_get_view(picopass->text_input));
+
+    // Byte Input
+    picopass->byte_input = byte_input_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher,
+        PicopassViewByteInput,
+        byte_input_get_view(picopass->byte_input));
+
+    // Custom Widget
+    picopass->widget = widget_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher, PicopassViewWidget, widget_get_view(picopass->widget));
+
+    picopass->dict_attack = dict_attack_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher,
+        PicopassViewDictAttack,
+        dict_attack_get_view(picopass->dict_attack));
+
+    picopass->loclass = loclass_alloc();
+    view_dispatcher_add_view(
+        picopass->view_dispatcher, PicopassViewLoclass, loclass_get_view(picopass->loclass));
+
+    return picopass;
+}
+
+void picopass_free(Picopass* picopass) {
+    furi_assert(picopass);
+
+    // Picopass device
+    picopass_device_free(picopass->dev);
+    picopass->dev = NULL;
+
+    // Submenu
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewMenu);
+    submenu_free(picopass->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewPopup);
+    popup_free(picopass->popup);
+
+    // Loading
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewLoading);
+    loading_free(picopass->loading);
+
+    // TextInput
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewTextInput);
+    text_input_free(picopass->text_input);
+
+    // ByteInput
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewByteInput);
+    byte_input_free(picopass->byte_input);
+
+    // Custom Widget
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewWidget);
+    widget_free(picopass->widget);
+
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewDictAttack);
+    dict_attack_free(picopass->dict_attack);
+
+    view_dispatcher_remove_view(picopass->view_dispatcher, PicopassViewLoclass);
+    loclass_free(picopass->loclass);
+
+    // Worker
+    picopass_worker_stop(picopass->worker);
+    picopass_worker_free(picopass->worker);
+
+    // View Dispatcher
+    view_dispatcher_free(picopass->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(picopass->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    picopass->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    picopass->notifications = NULL;
+
+    free(picopass);
+}
+
+void picopass_text_store_set(Picopass* picopass, const char* text, ...) {
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(picopass->text_store, sizeof(picopass->text_store), text, args);
+
+    va_end(args);
+}
+
+void picopass_text_store_clear(Picopass* picopass) {
+    memset(picopass->text_store, 0, sizeof(picopass->text_store));
+}
+
+static const NotificationSequence picopass_sequence_blink_start_cyan = {
+    &message_blink_start_10,
+    &message_blink_set_color_cyan,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence picopass_sequence_blink_start_magenta = {
+    &message_blink_start_10,
+    &message_blink_set_color_magenta,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence picopass_sequence_blink_stop = {
+    &message_blink_stop,
+    NULL,
+};
+
+void picopass_blink_start(Picopass* picopass) {
+    notification_message(picopass->notifications, &picopass_sequence_blink_start_cyan);
+}
+
+void picopass_blink_emulate_start(Picopass* picopass) {
+    notification_message(picopass->notifications, &picopass_sequence_blink_start_magenta);
+}
+
+void picopass_blink_stop(Picopass* picopass) {
+    notification_message(picopass->notifications, &picopass_sequence_blink_stop);
+}
+
+void picopass_show_loading_popup(void* context, bool show) {
+    Picopass* picopass = context;
+    TaskHandle_t timer_task = xTaskGetHandle(configTIMER_SERVICE_TASK_NAME);
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        vTaskPrioritySet(timer_task, configMAX_PRIORITIES - 1);
+        view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoading);
+    } else {
+        // Restore default timer priority
+        vTaskPrioritySet(timer_task, configTIMER_TASK_PRIORITY);
+    }
+}
+
+static void picopass_migrate_from_old_folder() {
+    Storage* storage = furi_record_open(RECORD_STORAGE);
+    storage_common_migrate(storage, "/ext/picopass", STORAGE_APP_DATA_PATH_PREFIX);
+    furi_record_close(RECORD_STORAGE);
+}
+
+bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size) {
+    bool result = size > 0;
+    while(size > 0) {
+        result &= (*data == pattern);
+        data++;
+        size--;
+    }
+    return result;
+}
+
+int32_t picopass_app(void* p) {
+    UNUSED(p);
+    picopass_migrate_from_old_folder();
+
+    Picopass* picopass = picopass_alloc();
+
+    scene_manager_next_scene(picopass->scene_manager, PicopassSceneStart);
+
+    view_dispatcher_run(picopass->view_dispatcher);
+
+    picopass_free(picopass);
+
+    return 0;
+}

+ 3 - 0
picopass/picopass.h

@@ -0,0 +1,3 @@
+#pragma once
+
+typedef struct Picopass Picopass;

+ 422 - 0
picopass/picopass_device.c

@@ -0,0 +1,422 @@
+#include "picopass_device.h"
+
+#include <toolbox/path.h>
+#include <flipper_format/flipper_format.h>
+#include <picopass_icons.h>
+
+#include <toolbox/protocols/protocol_dict.h>
+#include <lfrfid/protocols/lfrfid_protocols.h>
+#include <lfrfid/lfrfid_dict_file.h>
+
+#define TAG "PicopassDevice"
+
+static const char* picopass_file_header = "Flipper Picopass device";
+static const uint32_t picopass_file_version = 1;
+
+const uint8_t picopass_iclass_decryptionkey[] =
+    {0xb4, 0x21, 0x2c, 0xca, 0xb7, 0xed, 0x21, 0x0f, 0x7b, 0x93, 0xd4, 0x59, 0x39, 0xc7, 0xdd, 0x36};
+
+PicopassDevice* picopass_device_alloc() {
+    PicopassDevice* picopass_dev = malloc(sizeof(PicopassDevice));
+    picopass_dev->dev_data.pacs.legacy = false;
+    picopass_dev->dev_data.pacs.se_enabled = false;
+    picopass_dev->dev_data.pacs.elite_kdf = false;
+    picopass_dev->dev_data.pacs.pin_length = 0;
+    picopass_dev->storage = furi_record_open(RECORD_STORAGE);
+    picopass_dev->dialogs = furi_record_open(RECORD_DIALOGS);
+    picopass_dev->load_path = furi_string_alloc();
+    return picopass_dev;
+}
+
+void picopass_device_set_name(PicopassDevice* dev, const char* name) {
+    furi_assert(dev);
+
+    strlcpy(dev->dev_name, name, PICOPASS_DEV_NAME_MAX_LEN);
+}
+
+static bool picopass_device_save_file_lfrfid(PicopassDevice* dev, FuriString* file_path) {
+    furi_assert(dev);
+    PicopassPacs* pacs = &dev->dev_data.pacs;
+    ProtocolDict* dict = protocol_dict_alloc(lfrfid_protocols, LFRFIDProtocolMax);
+    ProtocolId protocol = LFRFIDProtocolHidGeneric;
+
+    bool result = false;
+    uint64_t target = 0;
+    uint64_t sentinel = 1ULL << pacs->bitLength;
+    memcpy(&target, pacs->credential, RFAL_PICOPASS_BLOCK_LEN);
+    target = __builtin_bswap64(target);
+    FURI_LOG_D(TAG, "Original (%d): %016llx", pacs->bitLength, target);
+
+    if(pacs->bitLength == 26) {
+        //3 bytes
+        protocol = LFRFIDProtocolH10301;
+        // Remove parity
+        target = (target >> 1) & 0xFFFFFF;
+        // Reverse order since it'll get reversed again
+        target = __builtin_bswap64(target) >> (64 - 24);
+    } else if(pacs->bitLength < 44) {
+        // https://gist.github.com/blark/e8f125e402f576bdb7e2d7b3428bdba6
+        protocol = LFRFIDProtocolHidGeneric;
+        if(pacs->bitLength <= 36) {
+            uint64_t header = 1ULL << 37;
+            target = __builtin_bswap64((target | sentinel | header) << 4) >> (64 - 48);
+        } else {
+            target = __builtin_bswap64((target | sentinel) << 4) >> (64 - 48);
+        }
+    } else {
+        //8 bytes
+        protocol = LFRFIDProtocolHidExGeneric;
+        target = __builtin_bswap64(target);
+    }
+
+    size_t data_size = protocol_dict_get_data_size(dict, protocol);
+    uint8_t* data = malloc(data_size);
+    if(data_size < 8) {
+        memcpy(data, (void*)&target, data_size);
+    } else {
+        // data_size 12 for LFRFIDProtocolHidExGeneric
+        memcpy(data + 4, (void*)&target, 8);
+    }
+
+    protocol_dict_set_data(dict, protocol, data, data_size);
+    free(data);
+
+    FuriString* briefStr;
+    briefStr = furi_string_alloc();
+    protocol_dict_render_brief_data(dict, briefStr, protocol);
+    FURI_LOG_D(TAG, "LFRFID Brief: %s", furi_string_get_cstr(briefStr));
+    furi_string_free(briefStr);
+
+    result = lfrfid_dict_file_save(dict, protocol, furi_string_get_cstr(file_path));
+    if(result) {
+        FURI_LOG_D(TAG, "Written: %d", result);
+    } else {
+        FURI_LOG_D(TAG, "Failed to write");
+    }
+
+    protocol_dict_free(dict);
+    return result;
+}
+
+static bool picopass_device_save_file(
+    PicopassDevice* dev,
+    const char* dev_name,
+    const char* folder,
+    const char* extension,
+    bool use_load_path) {
+    furi_assert(dev);
+    FURI_LOG_D(TAG, "Save File");
+
+    bool saved = false;
+    FlipperFormat* file = flipper_format_file_alloc(dev->storage);
+    PicopassPacs* pacs = &dev->dev_data.pacs;
+    PicopassBlock* AA1 = dev->dev_data.AA1;
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+
+    do {
+        if(use_load_path && !furi_string_empty(dev->load_path)) {
+            // Get directory name
+            path_extract_dirname(furi_string_get_cstr(dev->load_path), temp_str);
+            // Make path to file to save
+            furi_string_cat_printf(temp_str, "/%s%s", dev_name, extension);
+        } else {
+            // First remove picopass device file if it was saved
+            furi_string_printf(temp_str, "%s/%s%s", folder, dev_name, extension);
+        }
+
+        if(dev->format == PicopassDeviceSaveFormatHF) {
+            // Open file
+            if(!flipper_format_file_open_always(file, furi_string_get_cstr(temp_str))) break;
+
+            // Write header
+            if(!flipper_format_write_header_cstr(file, picopass_file_header, picopass_file_version))
+                break;
+            if(!flipper_format_write_hex(
+                   file, "Credential", pacs->credential, RFAL_PICOPASS_BLOCK_LEN))
+                break;
+
+            // TODO: Add elite
+            if(!flipper_format_write_comment_cstr(file, "Picopass blocks")) break;
+            bool block_saved = true;
+
+            size_t app_limit = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] < PICOPASS_MAX_APP_LIMIT ?
+                                   AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] :
+                                   PICOPASS_MAX_APP_LIMIT;
+            for(size_t i = 0; i < app_limit; i++) {
+                furi_string_printf(temp_str, "Block %d", i);
+                if(!flipper_format_write_hex(
+                       file,
+                       furi_string_get_cstr(temp_str),
+                       AA1[i].data,
+                       RFAL_PICOPASS_BLOCK_LEN)) {
+                    block_saved = false;
+                    break;
+                }
+            }
+            if(!block_saved) break;
+        } else if(dev->format == PicopassDeviceSaveFormatLF) {
+            saved = picopass_device_save_file_lfrfid(dev, temp_str);
+        }
+        saved = true;
+    } while(0);
+
+    if(!saved) {
+        dialog_message_show_storage_error(dev->dialogs, "Can not save\nfile");
+    }
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+    return saved;
+}
+
+bool picopass_device_save(PicopassDevice* dev, const char* dev_name) {
+    if(dev->format == PicopassDeviceSaveFormatHF) {
+        return picopass_device_save_file(
+            dev, dev_name, STORAGE_APP_DATA_PATH_PREFIX, PICOPASS_APP_EXTENSION, true);
+    } else if(dev->format == PicopassDeviceSaveFormatLF) {
+        return picopass_device_save_file(dev, dev_name, ANY_PATH("lfrfid"), ".rfid", true);
+    }
+
+    return false;
+}
+
+static bool picopass_device_load_data(PicopassDevice* dev, FuriString* path, bool show_dialog) {
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(dev->storage);
+    PicopassBlock* AA1 = dev->dev_data.AA1;
+    PicopassPacs* pacs = &dev->dev_data.pacs;
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    bool deprecated_version = false;
+
+    if(dev->loading_cb) {
+        dev->loading_cb(dev->loading_cb_ctx, true);
+    }
+
+    do {
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
+
+        // Read and verify file header
+        uint32_t version = 0;
+        if(!flipper_format_read_header(file, temp_str, &version)) break;
+        if(furi_string_cmp_str(temp_str, picopass_file_header) ||
+           (version != picopass_file_version)) {
+            deprecated_version = true;
+            break;
+        }
+
+        // Parse header blocks
+        bool block_read = true;
+        for(size_t i = 0; i < 6; i++) {
+            furi_string_printf(temp_str, "Block %d", i);
+            if(!flipper_format_read_hex(
+                   file, furi_string_get_cstr(temp_str), AA1[i].data, RFAL_PICOPASS_BLOCK_LEN)) {
+                block_read = false;
+                break;
+            }
+        }
+
+        size_t app_limit = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0];
+        // Fix for unpersonalized cards that have app_limit set to 0xFF
+        if(app_limit > PICOPASS_MAX_APP_LIMIT) app_limit = PICOPASS_MAX_APP_LIMIT;
+        for(size_t i = 6; i < app_limit; i++) {
+            furi_string_printf(temp_str, "Block %d", i);
+            if(!flipper_format_read_hex(
+                   file, furi_string_get_cstr(temp_str), AA1[i].data, RFAL_PICOPASS_BLOCK_LEN)) {
+                block_read = false;
+                break;
+            }
+        }
+        if(!block_read) break;
+
+        if(picopass_device_parse_credential(AA1, pacs) != ERR_NONE) break;
+        if(picopass_device_parse_wiegand(pacs->credential, pacs) != ERR_NONE) break;
+
+        parsed = true;
+    } while(false);
+
+    if(dev->loading_cb) {
+        dev->loading_cb(dev->loading_cb_ctx, false);
+    }
+
+    if((!parsed) && (show_dialog)) {
+        if(deprecated_version) {
+            dialog_message_show_storage_error(dev->dialogs, "File format deprecated");
+        } else {
+            dialog_message_show_storage_error(dev->dialogs, "Can not parse\nfile");
+        }
+    }
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+void picopass_device_clear(PicopassDevice* dev) {
+    furi_assert(dev);
+
+    picopass_device_data_clear(&dev->dev_data);
+    memset(&dev->dev_data, 0, sizeof(dev->dev_data));
+    dev->format = PicopassDeviceSaveFormatHF;
+    furi_string_reset(dev->load_path);
+}
+
+void picopass_device_free(PicopassDevice* picopass_dev) {
+    furi_assert(picopass_dev);
+    picopass_device_clear(picopass_dev);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(picopass_dev->load_path);
+    free(picopass_dev);
+}
+
+bool picopass_file_select(PicopassDevice* dev) {
+    furi_assert(dev);
+
+    FuriString* picopass_app_folder;
+    picopass_app_folder = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, PICOPASS_APP_EXTENSION, &I_Nfc_10px);
+    browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+
+    bool res = dialog_file_browser_show(
+        dev->dialogs, dev->load_path, picopass_app_folder, &browser_options);
+
+    furi_string_free(picopass_app_folder);
+    if(res) {
+        FuriString* filename;
+        filename = furi_string_alloc();
+        path_extract_filename(dev->load_path, filename, true);
+        strncpy(dev->dev_name, furi_string_get_cstr(filename), PICOPASS_DEV_NAME_MAX_LEN);
+        res = picopass_device_load_data(dev, dev->load_path, true);
+        if(res) {
+            picopass_device_set_name(dev, dev->dev_name);
+        }
+        furi_string_free(filename);
+    }
+
+    return res;
+}
+
+void picopass_device_data_clear(PicopassDeviceData* dev_data) {
+    for(size_t i = 0; i < PICOPASS_MAX_APP_LIMIT; i++) {
+        memset(dev_data->AA1[i].data, 0, sizeof(dev_data->AA1[i].data));
+    }
+    dev_data->pacs.legacy = false;
+    dev_data->pacs.se_enabled = false;
+    dev_data->pacs.elite_kdf = false;
+    dev_data->pacs.pin_length = 0;
+}
+
+bool picopass_device_delete(PicopassDevice* dev, bool use_load_path) {
+    furi_assert(dev);
+
+    bool deleted = false;
+    FuriString* file_path;
+    file_path = furi_string_alloc();
+
+    do {
+        // Delete original file
+        if(use_load_path && !furi_string_empty(dev->load_path)) {
+            furi_string_set(file_path, dev->load_path);
+        } else {
+            furi_string_printf(
+                file_path, APP_DATA_PATH("%s%s"), dev->dev_name, PICOPASS_APP_EXTENSION);
+        }
+        if(!storage_simply_remove(dev->storage, furi_string_get_cstr(file_path))) break;
+        deleted = true;
+    } while(0);
+
+    if(!deleted) {
+        dialog_message_show_storage_error(dev->dialogs, "Can not remove file");
+    }
+
+    furi_string_free(file_path);
+    return deleted;
+}
+
+void picopass_device_set_loading_callback(
+    PicopassDevice* dev,
+    PicopassLoadingCallback callback,
+    void* context) {
+    furi_assert(dev);
+
+    dev->loading_cb = callback;
+    dev->loading_cb_ctx = context;
+}
+
+ReturnCode picopass_device_decrypt(uint8_t* enc_data, uint8_t* dec_data) {
+    uint8_t key[32] = {0};
+    memcpy(key, picopass_iclass_decryptionkey, sizeof(picopass_iclass_decryptionkey));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_dec(&ctx, key);
+    mbedtls_des3_crypt_ecb(&ctx, enc_data, dec_data);
+    mbedtls_des3_free(&ctx);
+    return ERR_NONE;
+}
+
+ReturnCode picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs) {
+    ReturnCode err;
+
+    pacs->biometrics = AA1[6].data[4];
+    pacs->pin_length = AA1[6].data[6] & 0x0F;
+    pacs->encryption = AA1[6].data[7];
+
+    if(pacs->encryption == PicopassDeviceEncryption3DES) {
+        FURI_LOG_D(TAG, "3DES Encrypted");
+        err = picopass_device_decrypt(AA1[7].data, pacs->credential);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "decrypt error %d", err);
+            return err;
+        }
+
+        err = picopass_device_decrypt(AA1[8].data, pacs->pin0);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "decrypt error %d", err);
+            return err;
+        }
+
+        err = picopass_device_decrypt(AA1[9].data, pacs->pin1);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "decrypt error %d", err);
+            return err;
+        }
+    } else if(pacs->encryption == PicopassDeviceEncryptionNone) {
+        FURI_LOG_D(TAG, "No Encryption");
+        memcpy(pacs->credential, AA1[7].data, RFAL_PICOPASS_BLOCK_LEN);
+        memcpy(pacs->pin0, AA1[8].data, RFAL_PICOPASS_BLOCK_LEN);
+        memcpy(pacs->pin1, AA1[9].data, RFAL_PICOPASS_BLOCK_LEN);
+    } else if(pacs->encryption == PicopassDeviceEncryptionDES) {
+        FURI_LOG_D(TAG, "DES Encrypted");
+    } else {
+        FURI_LOG_D(TAG, "Unknown encryption");
+    }
+
+    pacs->sio = (AA1[10].data[0] == 0x30); // rough check
+
+    return ERR_NONE;
+}
+
+ReturnCode picopass_device_parse_wiegand(uint8_t* credential, PicopassPacs* pacs) {
+    uint32_t* halves = (uint32_t*)credential;
+    if(halves[0] == 0) {
+        uint8_t leading0s = __builtin_clz(REVERSE_BYTES_U32(halves[1]));
+        pacs->bitLength = 31 - leading0s;
+    } else {
+        uint8_t leading0s = __builtin_clz(REVERSE_BYTES_U32(halves[0]));
+        pacs->bitLength = 63 - leading0s;
+    }
+
+    // Remove sentinel bit from credential.  Byteswapping to handle array of bytes vs 64bit value
+    uint64_t sentinel = __builtin_bswap64(1ULL << pacs->bitLength);
+    uint64_t swapped = 0;
+    memcpy(&swapped, credential, sizeof(uint64_t));
+    swapped = swapped ^ sentinel;
+    memcpy(credential, &swapped, sizeof(uint64_t));
+    FURI_LOG_D(TAG, "PACS: (%d) %016llx", pacs->bitLength, swapped);
+
+    return ERR_NONE;
+}

+ 151 - 0
picopass/picopass_device.h

@@ -0,0 +1,151 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <mbedtls/des.h>
+
+#include "rfal_picopass.h"
+#include "loclass_writer.h"
+#include <optimized_ikeys.h>
+#include <optimized_cipher.h>
+#include "helpers/iclass_elite_dict.h"
+
+#define LOCLASS_NUM_CSNS 9
+#ifndef LOCLASS_NUM_PER_CSN
+// Collect 2 MACs per CSN to account for keyroll modes by default
+#define LOCLASS_NUM_PER_CSN 2
+#endif
+#define LOCLASS_MACS_TO_COLLECT (LOCLASS_NUM_CSNS * LOCLASS_NUM_PER_CSN)
+
+#define PICOPASS_DEV_NAME_MAX_LEN 22
+#define PICOPASS_READER_DATA_MAX_SIZE 64
+#define PICOPASS_MAX_APP_LIMIT 32
+
+#define PICOPASS_CSN_BLOCK_INDEX 0
+#define PICOPASS_CONFIG_BLOCK_INDEX 1
+// These definitions for blocks above 2 only hold for secure cards.
+#define PICOPASS_SECURE_EPURSE_BLOCK_INDEX 2
+#define PICOPASS_SECURE_KD_BLOCK_INDEX 3
+#define PICOPASS_SECURE_KC_BLOCK_INDEX 4
+#define PICOPASS_SECURE_AIA_BLOCK_INDEX 5
+// Non-secure cards instead have an AIA at block 2
+#define PICOPASS_NONSECURE_AIA_BLOCK_INDEX 2
+// Only iClass cards
+#define PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX 6
+
+// Personalization Mode
+#define PICOPASS_FUSE_PERS 0x80
+// Crypt1 // 1+1 (crypt1+crypt0) means secured and keys changable
+#define PICOPASS_FUSE_CRYPT1 0x10
+// Crypt0 // 1+0 means secure and keys locked, 0+1 means not secured, 0+0 means disable auth entirely
+#define PICOPASS_FUSE_CRTPT0 0x08
+#define PICOPASS_FUSE_CRYPT10 (PICOPASS_FUSE_CRYPT1 | PICOPASS_FUSE_CRTPT0)
+// Read Access, 1 meanns anonymous read enabled, 0 means must auth to read applicaion
+#define PICOPASS_FUSE_RA 0x01
+
+#define PICOPASS_APP_FOLDER ANY_PATH("picopass")
+#define PICOPASS_APP_EXTENSION ".picopass"
+#define PICOPASS_APP_FILE_PREFIX "Picopass"
+#define PICOPASS_APP_SHADOW_EXTENSION ".pas"
+
+#define PICOPASS_DICT_KEY_BATCH_SIZE 10
+
+typedef void (*PicopassLoadingCallback)(void* context, bool state);
+
+typedef struct {
+    IclassEliteDict* dict;
+    IclassEliteDictType type;
+    uint8_t current_sector;
+} IclassEliteDictAttackData;
+
+typedef enum {
+    PicopassDeviceEncryptionUnknown = 0,
+    PicopassDeviceEncryptionNone = 0x14,
+    PicopassDeviceEncryptionDES = 0x15,
+    PicopassDeviceEncryption3DES = 0x17,
+} PicopassEncryption;
+
+typedef enum {
+    PicopassDeviceSaveFormatHF,
+    PicopassDeviceSaveFormatLF,
+} PicopassDeviceSaveFormat;
+
+typedef enum {
+    PicopassEmulatorStateHalt,
+    PicopassEmulatorStateIdle,
+    PicopassEmulatorStateActive,
+    PicopassEmulatorStateSelected,
+    PicopassEmulatorStateStopEmulation,
+} PicopassEmulatorState;
+
+typedef struct {
+    bool legacy;
+    bool se_enabled;
+    bool sio;
+    bool biometrics;
+    uint8_t key[8];
+    bool elite_kdf;
+    uint8_t pin_length;
+    PicopassEncryption encryption;
+    uint8_t bitLength;
+    uint8_t credential[8];
+    uint8_t pin0[8];
+    uint8_t pin1[8];
+} PicopassPacs;
+
+typedef struct {
+    uint8_t data[RFAL_PICOPASS_BLOCK_LEN];
+} PicopassBlock;
+
+typedef struct {
+    PicopassBlock AA1[PICOPASS_MAX_APP_LIMIT];
+    PicopassPacs pacs;
+    IclassEliteDictAttackData iclass_elite_dict_attack_data;
+} PicopassDeviceData;
+
+typedef struct {
+    PicopassEmulatorState state;
+    LoclassState_t cipher_state;
+    uint8_t key_block_num; // in loclass mode used to store csn#
+    bool loclass_mode;
+    bool loclass_got_std_key;
+    uint8_t loclass_mac_buffer[8 * LOCLASS_NUM_PER_CSN];
+    LoclassWriter* loclass_writer;
+} PicopassEmulatorCtx;
+
+typedef struct {
+    Storage* storage;
+    DialogsApp* dialogs;
+    PicopassDeviceData dev_data;
+    char dev_name[PICOPASS_DEV_NAME_MAX_LEN + 1];
+    FuriString* load_path;
+    PicopassDeviceSaveFormat format;
+    PicopassLoadingCallback loading_cb;
+    void* loading_cb_ctx;
+} PicopassDevice;
+
+PicopassDevice* picopass_device_alloc();
+
+void picopass_device_free(PicopassDevice* picopass_dev);
+
+void picopass_device_set_name(PicopassDevice* dev, const char* name);
+
+bool picopass_device_save(PicopassDevice* dev, const char* dev_name);
+
+bool picopass_file_select(PicopassDevice* dev);
+
+void picopass_device_data_clear(PicopassDeviceData* dev_data);
+
+void picopass_device_clear(PicopassDevice* dev);
+
+bool picopass_device_delete(PicopassDevice* dev, bool use_load_path);
+
+void picopass_device_set_loading_callback(
+    PicopassDevice* dev,
+    PicopassLoadingCallback callback,
+    void* context);
+
+ReturnCode picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs);
+ReturnCode picopass_device_parse_wiegand(uint8_t* credential, PicopassPacs* pacs);

+ 108 - 0
picopass/picopass_i.h

@@ -0,0 +1,108 @@
+#pragma once
+
+#include "picopass.h"
+#include "picopass_worker.h"
+#include "picopass_device.h"
+
+#include "rfal_picopass.h"
+
+#include <furi.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+
+#include <gui/modules/submenu.h>
+#include <gui/modules/popup.h>
+#include <gui/modules/loading.h>
+#include <gui/modules/text_input.h>
+#include <gui/modules/byte_input.h>
+#include <gui/modules/widget.h>
+
+#include <input/input.h>
+
+#include "scenes/picopass_scene.h"
+#include "views/dict_attack.h"
+#include "views/loclass.h"
+
+#include <storage/storage.h>
+#include <lib/toolbox/path.h>
+#include <picopass_icons.h>
+
+#define PICOPASS_TEXT_STORE_SIZE 128
+
+enum PicopassCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    PicopassCustomEventReserved = 100,
+
+    PicopassCustomEventViewExit,
+    PicopassCustomEventWorkerExit,
+    PicopassCustomEventByteInputDone,
+    PicopassCustomEventTextInputDone,
+    PicopassCustomEventDictAttackSkip,
+};
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+struct Picopass {
+    PicopassWorker* worker;
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    PicopassDevice* dev;
+
+    char text_store[PICOPASS_TEXT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+    uint8_t byte_input_store[RFAL_PICOPASS_BLOCK_LEN];
+
+    // Common Views
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    ByteInput* byte_input;
+    Widget* widget;
+    DictAttack* dict_attack;
+    Loclass* loclass;
+};
+
+typedef enum {
+    PicopassViewMenu,
+    PicopassViewPopup,
+    PicopassViewLoading,
+    PicopassViewTextInput,
+    PicopassViewByteInput,
+    PicopassViewWidget,
+    PicopassViewDictAttack,
+    PicopassViewLoclass,
+} PicopassView;
+
+Picopass* picopass_alloc();
+
+void picopass_text_store_set(Picopass* picopass, const char* text, ...);
+
+void picopass_text_store_clear(Picopass* picopass);
+
+void picopass_blink_start(Picopass* picopass);
+
+void picopass_blink_emulate_start(Picopass* picopass);
+
+void picopass_blink_stop(Picopass* picopass);
+
+void picopass_show_loading_popup(void* context, bool show);
+
+/** Check if memory is set to pattern
+ *
+ * @warning    zero size will return false
+ *
+ * @param[in]  data     Pointer to the byte array
+ * @param[in]  pattern  The pattern
+ * @param[in]  size     The byte array size
+ *
+ * @return     True if memory is set to pattern, false otherwise
+ */
+bool picopass_is_memset(const uint8_t* data, const uint8_t pattern, size_t size);

+ 8 - 0
picopass/picopass_keys.c

@@ -0,0 +1,8 @@
+#include "picopass_keys.h"
+
+const uint8_t picopass_iclass_key[] = {0xaf, 0xa7, 0x85, 0xa7, 0xda, 0xb3, 0x33, 0x78};
+const uint8_t picopass_factory_credit_key[] = {0x76, 0x65, 0x54, 0x43, 0x32, 0x21, 0x10, 0x00};
+const uint8_t picopass_factory_debit_key[] = {0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87};
+const uint8_t picopass_xice_key[] = {0x20, 0x20, 0x66, 0x66, 0x66, 0x66, 0x88, 0x88};
+const uint8_t picopass_xicl_key[] = {0x20, 0x20, 0x66, 0x66, 0x66, 0x66, 0x88, 0x88};
+const uint8_t picopass_xics_key[] = {0x66, 0x66, 0x20, 0x20, 0x66, 0x66, 0x88, 0x88};

+ 10 - 0
picopass/picopass_keys.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include "picopass_device.h"
+
+extern const uint8_t picopass_iclass_key[RFAL_PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_factory_credit_key[RFAL_PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_factory_debit_key[RFAL_PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xice_key[RFAL_PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xicl_key[RFAL_PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xics_key[RFAL_PICOPASS_BLOCK_LEN];

+ 1340 - 0
picopass/picopass_worker.c

@@ -0,0 +1,1340 @@
+#include "picopass_worker_i.h"
+
+#include <flipper_format/flipper_format.h>
+#include <lib/nfc/protocols/nfcv.h>
+
+#define TAG "PicopassWorker"
+
+#define HAS_MASK(x, b) ((x & b) == b)
+
+// CSNs from Proxmark3 repo
+static const uint8_t loclass_csns[LOCLASS_NUM_CSNS][RFAL_PICOPASS_BLOCK_LEN] = {
+    {0x01, 0x0A, 0x0F, 0xFF, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x0C, 0x06, 0x0C, 0xFE, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x10, 0x97, 0x83, 0x7B, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x13, 0x97, 0x82, 0x7A, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x07, 0x0E, 0x0D, 0xF9, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x14, 0x96, 0x84, 0x76, 0xF7, 0xFF, 0x12, 0xE0},
+    {0x17, 0x96, 0x85, 0x71, 0xF7, 0xFF, 0x12, 0xE0},
+    {0xCE, 0xC5, 0x0F, 0x77, 0xF7, 0xFF, 0x12, 0xE0},
+    {0xD2, 0x5A, 0x82, 0xF8, 0xF7, 0xFF, 0x12, 0xE0},
+};
+
+static void picopass_worker_enable_field() {
+    furi_hal_nfc_exit_sleep();
+    furi_hal_nfc_ll_txrx_on();
+    furi_hal_nfc_ll_poll();
+}
+
+static ReturnCode picopass_worker_disable_field(ReturnCode rc) {
+    furi_hal_nfc_ll_txrx_off();
+    furi_hal_nfc_start_sleep();
+    return rc;
+}
+
+/***************************** Picopass Worker API *******************************/
+
+PicopassWorker* picopass_worker_alloc() {
+    PicopassWorker* picopass_worker = malloc(sizeof(PicopassWorker));
+
+    // Worker thread attributes
+    picopass_worker->thread =
+        furi_thread_alloc_ex("PicopassWorker", 8 * 1024, picopass_worker_task, picopass_worker);
+
+    picopass_worker->callback = NULL;
+    picopass_worker->context = NULL;
+    picopass_worker->storage = furi_record_open(RECORD_STORAGE);
+
+    picopass_worker_change_state(picopass_worker, PicopassWorkerStateReady);
+
+    return picopass_worker;
+}
+
+void picopass_worker_free(PicopassWorker* picopass_worker) {
+    furi_assert(picopass_worker);
+
+    furi_thread_free(picopass_worker->thread);
+
+    furi_record_close(RECORD_STORAGE);
+
+    free(picopass_worker);
+}
+
+PicopassWorkerState picopass_worker_get_state(PicopassWorker* picopass_worker) {
+    return picopass_worker->state;
+}
+
+void picopass_worker_start(
+    PicopassWorker* picopass_worker,
+    PicopassWorkerState state,
+    PicopassDeviceData* dev_data,
+    PicopassWorkerCallback callback,
+    void* context) {
+    furi_assert(picopass_worker);
+    furi_assert(dev_data);
+
+    picopass_worker->callback = callback;
+    picopass_worker->context = context;
+    picopass_worker->dev_data = dev_data;
+    picopass_worker_change_state(picopass_worker, state);
+    furi_thread_start(picopass_worker->thread);
+}
+
+void picopass_worker_stop(PicopassWorker* picopass_worker) {
+    furi_assert(picopass_worker);
+    furi_assert(picopass_worker->thread);
+
+    if(furi_thread_get_state(picopass_worker->thread) == FuriThreadStateStopped) {
+        return;
+    }
+
+    if(picopass_worker->state == PicopassWorkerStateBroken ||
+       picopass_worker->state == PicopassWorkerStateReady) {
+        return;
+    }
+
+    if(picopass_worker->state != PicopassWorkerStateEmulate &&
+       picopass_worker->state != PicopassWorkerStateLoclass) {
+        // Can't do this while emulating in transparent mode as SPI isn't active
+        picopass_worker_disable_field(ERR_NONE);
+    }
+
+    if(furi_thread_get_state(picopass_worker->thread) != FuriThreadStateStopped) {
+        picopass_worker_change_state(picopass_worker, PicopassWorkerStateStop);
+        furi_thread_join(picopass_worker->thread);
+    }
+}
+
+void picopass_worker_change_state(PicopassWorker* picopass_worker, PicopassWorkerState state) {
+    picopass_worker->state = state;
+}
+
+/***************************** Picopass Worker Thread *******************************/
+
+ReturnCode picopass_detect_card(int timeout) {
+    UNUSED(timeout);
+
+    ReturnCode err;
+
+    err = rfalPicoPassPollerInitialize();
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerInitialize error %d", err);
+        return err;
+    }
+
+    err = rfalFieldOnAndStartGT();
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalFieldOnAndStartGT error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerCheckPresence();
+    if(err != ERR_RF_COLLISION) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerCheckPresence error %d", err);
+        return err;
+    }
+
+    return ERR_NONE;
+}
+
+ReturnCode picopass_read_preauth(PicopassBlock* AA1) {
+    rfalPicoPassIdentifyRes idRes;
+    rfalPicoPassSelectRes selRes;
+
+    ReturnCode err;
+
+    err = rfalPicoPassPollerIdentify(&idRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerSelect(idRes.CSN, &selRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err);
+        return err;
+    }
+
+    memcpy(AA1[PICOPASS_CSN_BLOCK_INDEX].data, selRes.CSN, sizeof(selRes.CSN));
+    FURI_LOG_D(
+        TAG,
+        "csn %02x%02x%02x%02x%02x%02x%02x%02x",
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[0],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[1],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[2],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[3],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[4],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[5],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[6],
+        AA1[PICOPASS_CSN_BLOCK_INDEX].data[7]);
+
+    rfalPicoPassReadBlockRes cfg = {0};
+    rfalPicoPassPollerReadBlock(PICOPASS_CONFIG_BLOCK_INDEX, &cfg);
+    memcpy(AA1[PICOPASS_CONFIG_BLOCK_INDEX].data, cfg.data, sizeof(cfg.data));
+    FURI_LOG_D(
+        TAG,
+        "config %02x%02x%02x%02x%02x%02x%02x%02x",
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[1],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[2],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[3],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[4],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[5],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[6],
+        AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7]);
+
+    rfalPicoPassReadBlockRes aia;
+    rfalPicoPassPollerReadBlock(PICOPASS_SECURE_AIA_BLOCK_INDEX, &aia);
+    memcpy(AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data, aia.data, sizeof(aia.data));
+    FURI_LOG_D(
+        TAG,
+        "aia %02x%02x%02x%02x%02x%02x%02x%02x",
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[0],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[1],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[2],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[3],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[4],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[5],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[6],
+        AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[7]);
+
+    return ERR_NONE;
+}
+
+static ReturnCode
+    picopass_auth_dict(PicopassWorker* picopass_worker, IclassEliteDictType dict_type) {
+    rfalPicoPassReadCheckRes rcRes;
+    rfalPicoPassCheckRes chkRes;
+    bool elite = (dict_type != IclassStandardDictTypeFlipper);
+
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+    PicopassBlock* AA1 = dev_data->AA1;
+    PicopassPacs* pacs = &dev_data->pacs;
+
+    uint8_t* csn = AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    uint8_t* div_key = AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data;
+
+    ReturnCode err = ERR_PARAM;
+
+    uint8_t mac[4] = {0};
+    uint8_t ccnr[12] = {0};
+
+    size_t index = 0;
+    uint8_t key[RFAL_PICOPASS_BLOCK_LEN] = {0};
+
+    if(!iclass_elite_dict_check_presence(dict_type)) {
+        FURI_LOG_E(TAG, "Dictionary not found");
+        return ERR_PARAM;
+    }
+
+    IclassEliteDict* dict = iclass_elite_dict_alloc(dict_type);
+    if(!dict) {
+        FURI_LOG_E(TAG, "Dictionary not allocated");
+        return ERR_PARAM;
+    }
+
+    FURI_LOG_D(TAG, "Loaded %lu keys", iclass_elite_dict_get_total_keys(dict));
+    while(iclass_elite_dict_get_next_key(dict, key)) {
+        FURI_LOG_D(
+            TAG,
+            "Try to %s auth with key %zu %02x%02x%02x%02x%02x%02x%02x%02x",
+            elite ? "elite" : "standard",
+            index++,
+            key[0],
+            key[1],
+            key[2],
+            key[3],
+            key[4],
+            key[5],
+            key[6],
+            key[7]);
+
+        err = rfalPicoPassPollerReadCheck(&rcRes);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
+            break;
+        }
+        memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
+
+        loclass_iclass_calc_div_key(csn, key, div_key, elite);
+        loclass_opt_doReaderMAC(ccnr, div_key, mac);
+
+        err = rfalPicoPassPollerCheck(mac, &chkRes);
+        if(err == ERR_NONE) {
+            memcpy(pacs->key, key, RFAL_PICOPASS_BLOCK_LEN);
+            break;
+        }
+
+        if(picopass_worker->state != PicopassWorkerStateDetect) break;
+    }
+
+    iclass_elite_dict_free(dict);
+
+    return err;
+}
+
+ReturnCode picopass_auth(PicopassWorker* picopass_worker) {
+    ReturnCode err;
+
+    FURI_LOG_I(TAG, "Starting system dictionary attack [Standard KDF]");
+    err = picopass_auth_dict(picopass_worker, IclassStandardDictTypeFlipper);
+    if(err == ERR_NONE) {
+        return ERR_NONE;
+    }
+
+    /* Because size of the user dictionary and could introduce confusing delay
+     * to the read screen (since there is no feedback), we omit checking it.
+     * It will be checked when the user uses Elite Dict. Attack, which has a progress bar
+     */
+
+    FURI_LOG_I(TAG, "Starting system dictionary attack [Elite KDF]");
+    err = picopass_auth_dict(picopass_worker, IclassEliteDictTypeFlipper);
+    if(err == ERR_NONE) {
+        return ERR_NONE;
+    }
+
+    return err;
+}
+
+ReturnCode picopass_read_card(PicopassBlock* AA1) {
+    ReturnCode err;
+
+    size_t app_limit = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] < PICOPASS_MAX_APP_LIMIT ?
+                           AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] :
+                           PICOPASS_MAX_APP_LIMIT;
+
+    for(size_t i = 2; i < app_limit; i++) {
+        if(i == PICOPASS_SECURE_KD_BLOCK_INDEX) {
+            // Skip over Kd block which is populated earlier (READ of Kd returns all FF's)
+            continue;
+        }
+
+        rfalPicoPassReadBlockRes block;
+        err = rfalPicoPassPollerReadBlock(i, &block);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "rfalPicoPassPollerReadBlock error %d", err);
+            return err;
+        }
+
+        FURI_LOG_D(
+            TAG,
+            "rfalPicoPassPollerReadBlock %d %02x%02x%02x%02x%02x%02x%02x%02x",
+            i,
+            block.data[0],
+            block.data[1],
+            block.data[2],
+            block.data[3],
+            block.data[4],
+            block.data[5],
+            block.data[6],
+            block.data[7]);
+
+        memcpy(AA1[i].data, block.data, sizeof(block.data));
+    }
+
+    return ERR_NONE;
+}
+
+ReturnCode picopass_write_card(PicopassBlock* AA1) {
+    rfalPicoPassIdentifyRes idRes;
+    rfalPicoPassSelectRes selRes;
+    rfalPicoPassReadCheckRes rcRes;
+    rfalPicoPassCheckRes chkRes;
+
+    ReturnCode err;
+
+    uint8_t div_key[8] = {0};
+    uint8_t mac[4] = {0};
+    uint8_t ccnr[12] = {0};
+
+    err = rfalPicoPassPollerIdentify(&idRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerSelect(idRes.CSN, &selRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerReadCheck(&rcRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
+        return err;
+    }
+    memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
+
+    loclass_iclass_calc_div_key(selRes.CSN, (uint8_t*)picopass_iclass_key, div_key, false);
+    loclass_opt_doReaderMAC(ccnr, div_key, mac);
+
+    err = rfalPicoPassPollerCheck(mac, &chkRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerCheck error %d", err);
+        return err;
+    }
+
+    for(size_t i = 6; i < 10; i++) {
+        FURI_LOG_D(TAG, "rfalPicoPassPollerWriteBlock %d", i);
+        uint8_t data[9] = {0};
+        data[0] = i;
+        memcpy(data + 1, AA1[i].data, RFAL_PICOPASS_BLOCK_LEN);
+        loclass_doMAC_N(data, sizeof(data), div_key, mac);
+        FURI_LOG_D(
+            TAG,
+            "loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x",
+            i,
+            data[1],
+            data[2],
+            data[3],
+            data[4],
+            data[5],
+            data[6],
+            data[7],
+            data[8],
+            mac[0],
+            mac[1],
+            mac[2],
+            mac[3]);
+
+        err = rfalPicoPassPollerWriteBlock(i, AA1[i].data, mac);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "rfalPicoPassPollerWriteBlock error %d", err);
+            return err;
+        }
+    }
+
+    return ERR_NONE;
+}
+
+ReturnCode picopass_write_block(PicopassBlock* AA1, uint8_t blockNo, uint8_t* newBlock) {
+    rfalPicoPassIdentifyRes idRes;
+    rfalPicoPassSelectRes selRes;
+    rfalPicoPassReadCheckRes rcRes;
+    rfalPicoPassCheckRes chkRes;
+
+    ReturnCode err;
+
+    uint8_t mac[4] = {0};
+    uint8_t ccnr[12] = {0};
+
+    err = rfalPicoPassPollerIdentify(&idRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerIdentify error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerSelect(idRes.CSN, &selRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerSelect error %d", err);
+        return err;
+    }
+
+    err = rfalPicoPassPollerReadCheck(&rcRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
+        return err;
+    }
+    memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
+
+    if(memcmp(selRes.CSN, AA1[PICOPASS_CSN_BLOCK_INDEX].data, RFAL_PICOPASS_BLOCK_LEN) != 0) {
+        FURI_LOG_E(TAG, "Wrong CSN for write");
+        return ERR_REQUEST;
+    }
+
+    loclass_opt_doReaderMAC(ccnr, AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data, mac);
+    err = rfalPicoPassPollerCheck(mac, &chkRes);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerCheck error %d", err);
+        return err;
+    }
+
+    FURI_LOG_D(TAG, "rfalPicoPassPollerWriteBlock %d", blockNo);
+    uint8_t data[9] = {
+        blockNo,
+        newBlock[0],
+        newBlock[1],
+        newBlock[2],
+        newBlock[3],
+        newBlock[4],
+        newBlock[5],
+        newBlock[6],
+        newBlock[7]};
+    loclass_doMAC_N(data, sizeof(data), AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data, mac);
+    FURI_LOG_D(
+        TAG,
+        "loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x",
+        blockNo,
+        data[1],
+        data[2],
+        data[3],
+        data[4],
+        data[5],
+        data[6],
+        data[7],
+        data[8],
+        mac[0],
+        mac[1],
+        mac[2],
+        mac[3]);
+
+    err = rfalPicoPassPollerWriteBlock(data[0], data + 1, mac);
+    if(err != ERR_NONE) {
+        FURI_LOG_E(TAG, "rfalPicoPassPollerWriteBlock error %d", err);
+        return err;
+    }
+
+    return ERR_NONE;
+}
+
+void picopass_worker_elite_dict_attack(PicopassWorker* picopass_worker) {
+    furi_assert(picopass_worker);
+    furi_assert(picopass_worker->callback);
+
+    picopass_device_data_clear(picopass_worker->dev_data);
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+    PicopassBlock* AA1 = dev_data->AA1;
+    PicopassPacs* pacs = &dev_data->pacs;
+
+    for(size_t i = 0; i < PICOPASS_MAX_APP_LIMIT; i++) {
+        memset(AA1[i].data, 0, sizeof(AA1[i].data));
+    }
+    memset(pacs, 0, sizeof(PicopassPacs));
+
+    IclassEliteDictAttackData* dict_attack_data =
+        &picopass_worker->dev_data->iclass_elite_dict_attack_data;
+    bool elite = (dict_attack_data->type != IclassStandardDictTypeFlipper);
+
+    rfalPicoPassReadCheckRes rcRes;
+    rfalPicoPassCheckRes chkRes;
+
+    ReturnCode err;
+    uint8_t mac[4] = {0};
+    uint8_t ccnr[12] = {0};
+
+    size_t index = 0;
+    uint8_t key[RFAL_PICOPASS_BLOCK_LEN] = {0};
+
+    // Load dictionary
+    IclassEliteDict* dict = dict_attack_data->dict;
+    if(!dict) {
+        FURI_LOG_E(TAG, "Dictionary not found");
+        picopass_worker->callback(PicopassWorkerEventNoDictFound, picopass_worker->context);
+        return;
+    }
+
+    do {
+        if(picopass_detect_card(1000) == ERR_NONE) {
+            picopass_worker->callback(PicopassWorkerEventCardDetected, picopass_worker->context);
+
+            // Process first found device
+            err = picopass_read_preauth(AA1);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_read_preauth error %d", err);
+                picopass_worker->callback(PicopassWorkerEventAborted, picopass_worker->context);
+                return;
+            }
+
+            // Thank you proxmark!
+            pacs->legacy = picopass_is_memset(AA1[5].data, 0xFF, 8);
+            pacs->se_enabled = (memcmp(AA1[5].data, "\xff\xff\xff\x00\x06\xff\xff\xff", 8) == 0);
+            if(pacs->se_enabled) {
+                FURI_LOG_D(TAG, "SE enabled");
+                picopass_worker->callback(PicopassWorkerEventAborted, picopass_worker->context);
+                return;
+            }
+
+            break;
+        } else {
+            picopass_worker->callback(PicopassWorkerEventNoCardDetected, picopass_worker->context);
+        }
+        if(picopass_worker->state != PicopassWorkerStateEliteDictAttack) break;
+
+        furi_delay_ms(100);
+    } while(true);
+
+    FURI_LOG_D(
+        TAG, "Start Dictionary attack, Key Count %lu", iclass_elite_dict_get_total_keys(dict));
+    while(iclass_elite_dict_get_next_key(dict, key)) {
+        FURI_LOG_T(TAG, "Key %zu", index);
+        if(++index % PICOPASS_DICT_KEY_BATCH_SIZE == 0) {
+            picopass_worker->callback(
+                PicopassWorkerEventNewDictKeyBatch, picopass_worker->context);
+        }
+
+        err = rfalPicoPassPollerReadCheck(&rcRes);
+        if(err != ERR_NONE) {
+            FURI_LOG_E(TAG, "rfalPicoPassPollerReadCheck error %d", err);
+            break;
+        }
+        memcpy(ccnr, rcRes.CCNR, sizeof(rcRes.CCNR)); // last 4 bytes left 0
+
+        uint8_t* csn = AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+        uint8_t* div_key = AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data;
+
+        loclass_iclass_calc_div_key(csn, key, div_key, elite);
+        loclass_opt_doReaderMAC(ccnr, div_key, mac);
+
+        err = rfalPicoPassPollerCheck(mac, &chkRes);
+        if(err == ERR_NONE) {
+            FURI_LOG_I(
+                TAG,
+                "Found key: %02x%02x%02x%02x%02x%02x%02x%02x",
+                key[0],
+                key[1],
+                key[2],
+                key[3],
+                key[4],
+                key[5],
+                key[6],
+                key[7]);
+
+            memcpy(pacs->key, key, RFAL_PICOPASS_BLOCK_LEN);
+            pacs->elite_kdf = elite;
+            err = picopass_read_card(AA1);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_read_card error %d", err);
+                picopass_worker->callback(PicopassWorkerEventFail, picopass_worker->context);
+                break;
+            }
+
+            err = picopass_device_parse_credential(AA1, pacs);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_device_parse_credential error %d", err);
+                picopass_worker->callback(PicopassWorkerEventFail, picopass_worker->context);
+                break;
+            }
+
+            err = picopass_device_parse_wiegand(pacs->credential, pacs);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_device_parse_wiegand error %d", err);
+                picopass_worker->callback(PicopassWorkerEventFail, picopass_worker->context);
+                break;
+            }
+            picopass_worker->callback(PicopassWorkerEventAborted, picopass_worker->context);
+            break;
+        }
+
+        if(picopass_worker->state != PicopassWorkerStateEliteDictAttack) break;
+    }
+    FURI_LOG_D(TAG, "Dictionary complete");
+    if(picopass_worker->state == PicopassWorkerStateEliteDictAttack) {
+        picopass_worker->callback(PicopassWorkerEventSuccess, picopass_worker->context);
+    } else {
+        picopass_worker->callback(PicopassWorkerEventAborted, picopass_worker->context);
+    }
+}
+
+int32_t picopass_worker_task(void* context) {
+    PicopassWorker* picopass_worker = context;
+
+    if(picopass_worker->state == PicopassWorkerStateDetect) {
+        picopass_worker_enable_field();
+        picopass_worker_detect(picopass_worker);
+    } else if(picopass_worker->state == PicopassWorkerStateWrite) {
+        picopass_worker_enable_field();
+        picopass_worker_write(picopass_worker);
+    } else if(picopass_worker->state == PicopassWorkerStateWriteKey) {
+        picopass_worker_enable_field();
+        picopass_worker_write_key(picopass_worker);
+    } else if(picopass_worker->state == PicopassWorkerStateEliteDictAttack) {
+        picopass_worker_enable_field();
+        picopass_worker_elite_dict_attack(picopass_worker);
+    } else if(picopass_worker->state == PicopassWorkerStateEmulate) {
+        picopass_worker_emulate(picopass_worker, false);
+    } else if(picopass_worker->state == PicopassWorkerStateLoclass) {
+        picopass_worker_emulate(picopass_worker, true);
+    } else if(picopass_worker->state == PicopassWorkerStateStop) {
+        FURI_LOG_D(TAG, "Worker state stop");
+        // no-op
+    } else {
+        FURI_LOG_W(TAG, "Unknown state %d", picopass_worker->state);
+    }
+    picopass_worker_disable_field(ERR_NONE);
+    picopass_worker_change_state(picopass_worker, PicopassWorkerStateReady);
+
+    return 0;
+}
+
+void picopass_worker_detect(PicopassWorker* picopass_worker) {
+    picopass_device_data_clear(picopass_worker->dev_data);
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+
+    PicopassBlock* AA1 = dev_data->AA1;
+    PicopassPacs* pacs = &dev_data->pacs;
+    ReturnCode err;
+
+    // reset device data
+    for(size_t i = 0; i < PICOPASS_MAX_APP_LIMIT; i++) {
+        memset(AA1[i].data, 0, sizeof(AA1[i].data));
+    }
+    memset(pacs, 0, sizeof(PicopassPacs));
+
+    PicopassWorkerEvent nextState = PicopassWorkerEventSuccess;
+
+    while(picopass_worker->state == PicopassWorkerStateDetect) {
+        if(picopass_detect_card(1000) == ERR_NONE) {
+            // Process first found device
+            err = picopass_read_preauth(AA1);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_read_preauth error %d", err);
+                nextState = PicopassWorkerEventFail;
+            }
+
+            // Thank you proxmark!
+            pacs->legacy = picopass_is_memset(AA1[5].data, 0xFF, 8);
+            pacs->se_enabled = (memcmp(AA1[5].data, "\xff\xff\xff\x00\x06\xff\xff\xff", 8) == 0);
+            if(pacs->se_enabled) {
+                FURI_LOG_D(TAG, "SE enabled");
+                nextState = PicopassWorkerEventFail;
+            }
+
+            if(nextState == PicopassWorkerEventSuccess) {
+                err = picopass_auth(picopass_worker);
+                if(err != ERR_NONE) {
+                    FURI_LOG_E(TAG, "picopass_try_auth error %d", err);
+                    nextState = PicopassWorkerEventFail;
+                }
+            }
+
+            if(nextState == PicopassWorkerEventSuccess) {
+                err = picopass_read_card(AA1);
+                if(err != ERR_NONE) {
+                    FURI_LOG_E(TAG, "picopass_read_card error %d", err);
+                    nextState = PicopassWorkerEventFail;
+                }
+            }
+
+            if(nextState == PicopassWorkerEventSuccess) {
+                err = picopass_device_parse_credential(AA1, pacs);
+                if(err != ERR_NONE) {
+                    FURI_LOG_E(TAG, "picopass_device_parse_credential error %d", err);
+                    nextState = PicopassWorkerEventFail;
+                }
+            }
+
+            if(nextState == PicopassWorkerEventSuccess) {
+                err = picopass_device_parse_wiegand(pacs->credential, pacs);
+                if(err != ERR_NONE) {
+                    FURI_LOG_E(TAG, "picopass_device_parse_wiegand error %d", err);
+                    nextState = PicopassWorkerEventFail;
+                }
+            }
+
+            // Notify caller and exit
+            if(picopass_worker->callback) {
+                picopass_worker->callback(nextState, picopass_worker->context);
+            }
+            break;
+        }
+        furi_delay_ms(100);
+    }
+}
+
+void picopass_worker_write(PicopassWorker* picopass_worker) {
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+    PicopassBlock* AA1 = dev_data->AA1;
+    ReturnCode err;
+    PicopassWorkerEvent nextState = PicopassWorkerEventSuccess;
+
+    while(picopass_worker->state == PicopassWorkerStateWrite) {
+        if(picopass_detect_card(1000) == ERR_NONE) {
+            err = picopass_write_card(AA1);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_write_card error %d", err);
+                nextState = PicopassWorkerEventFail;
+            }
+
+            // Notify caller and exit
+            if(picopass_worker->callback) {
+                picopass_worker->callback(nextState, picopass_worker->context);
+            }
+            break;
+        }
+        furi_delay_ms(100);
+    }
+}
+
+void picopass_worker_write_key(PicopassWorker* picopass_worker) {
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+    PicopassBlock* AA1 = dev_data->AA1;
+    PicopassPacs* pacs = &dev_data->pacs;
+    ReturnCode err;
+    PicopassWorkerEvent nextState = PicopassWorkerEventSuccess;
+
+    uint8_t* csn = AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    uint8_t* configBlock = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data;
+    uint8_t fuses = configBlock[7];
+    uint8_t* oldKey = AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data;
+
+    uint8_t newKey[RFAL_PICOPASS_BLOCK_LEN] = {0};
+    loclass_iclass_calc_div_key(csn, pacs->key, newKey, pacs->elite_kdf);
+
+    if((fuses & 0x80) == 0x80) {
+        FURI_LOG_D(TAG, "Plain write for personalized mode key change");
+    } else {
+        FURI_LOG_D(TAG, "XOR write for application mode key change");
+        // XOR when in application mode
+        for(size_t i = 0; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+            newKey[i] ^= oldKey[i];
+        }
+    }
+
+    while(picopass_worker->state == PicopassWorkerStateWriteKey) {
+        if(picopass_detect_card(1000) == ERR_NONE) {
+            err = picopass_write_block(AA1, PICOPASS_SECURE_KD_BLOCK_INDEX, newKey);
+            if(err != ERR_NONE) {
+                FURI_LOG_E(TAG, "picopass_write_block error %d", err);
+                nextState = PicopassWorkerEventFail;
+            }
+
+            // Notify caller and exit
+            if(picopass_worker->callback) {
+                picopass_worker->callback(nextState, picopass_worker->context);
+            }
+            break;
+        }
+        furi_delay_ms(100);
+    }
+}
+
+// from proxmark3 armsrc/iclass.c rotateCSN
+static void picopass_anticoll_csn(uint8_t* rotated_csn, const uint8_t* original_csn) {
+    for(uint8_t i = 0; i < 8; i++) {
+        rotated_csn[i] = (original_csn[i] >> 3) | (original_csn[(i + 1) % 8] << 5);
+    }
+}
+
+static void picopass_append_crc(uint8_t* buf, uint16_t size) {
+    uint16_t crc = rfalPicoPassCalculateCcitt(0xE012, buf, size);
+
+    buf[size] = crc & 0xFF;
+    buf[size + 1] = crc >> 8;
+}
+
+static inline void picopass_emu_read_blocks(
+    NfcVData* nfcv_data,
+    uint8_t* buf,
+    uint8_t block_num,
+    uint8_t block_count) {
+    memcpy(
+        buf,
+        nfcv_data->data + (block_num * RFAL_PICOPASS_BLOCK_LEN),
+        block_count * RFAL_PICOPASS_BLOCK_LEN);
+}
+
+static inline void picopass_emu_write_blocks(
+    NfcVData* nfcv_data,
+    const uint8_t* buf,
+    uint8_t block_num,
+    uint8_t block_count) {
+    memcpy(
+        nfcv_data->data + (block_num * RFAL_PICOPASS_BLOCK_LEN),
+        buf,
+        block_count * RFAL_PICOPASS_BLOCK_LEN);
+}
+
+static void picopass_init_cipher_state_key(
+    NfcVData* nfcv_data,
+    PicopassEmulatorCtx* ctx,
+    const uint8_t key[RFAL_PICOPASS_BLOCK_LEN]) {
+    uint8_t cc[RFAL_PICOPASS_BLOCK_LEN];
+    picopass_emu_read_blocks(nfcv_data, cc, PICOPASS_SECURE_EPURSE_BLOCK_INDEX, 1);
+
+    ctx->cipher_state = loclass_opt_doTagMAC_1(cc, key);
+}
+
+static void picopass_init_cipher_state(NfcVData* nfcv_data, PicopassEmulatorCtx* ctx) {
+    uint8_t key[RFAL_PICOPASS_BLOCK_LEN];
+
+    picopass_emu_read_blocks(nfcv_data, key, ctx->key_block_num, 1);
+
+    picopass_init_cipher_state_key(nfcv_data, ctx, key);
+}
+
+static void
+    loclass_update_csn(FuriHalNfcDevData* nfc_data, NfcVData* nfcv_data, PicopassEmulatorCtx* ctx) {
+    // collect LOCLASS_NUM_PER_CSN nonces in a row for each CSN
+    const uint8_t* csn =
+        loclass_csns[(ctx->key_block_num / LOCLASS_NUM_PER_CSN) % LOCLASS_NUM_CSNS];
+    memcpy(nfc_data->uid, csn, RFAL_PICOPASS_BLOCK_LEN);
+    picopass_emu_write_blocks(nfcv_data, csn, PICOPASS_CSN_BLOCK_INDEX, 1);
+
+    uint8_t key[RFAL_PICOPASS_BLOCK_LEN];
+    loclass_iclass_calc_div_key(csn, picopass_iclass_key, key, false);
+    picopass_emu_write_blocks(nfcv_data, key, PICOPASS_SECURE_KD_BLOCK_INDEX, 1);
+
+    picopass_init_cipher_state_key(nfcv_data, ctx, key);
+}
+
+static void picopass_emu_handle_packet(
+    FuriHalNfcTxRxContext* tx_rx,
+    FuriHalNfcDevData* nfc_data,
+    void* nfcv_data_in) {
+    NfcVData* nfcv_data = (NfcVData*)nfcv_data_in;
+    PicopassEmulatorCtx* ctx = nfcv_data->emu_protocol_ctx;
+    uint8_t response[34];
+    uint8_t response_length = 0;
+    uint8_t key_block_num = PICOPASS_SECURE_KD_BLOCK_INDEX;
+
+    const uint8_t block_ff[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+
+    if(nfcv_data->frame_length < 1 || ctx->state == PicopassEmulatorStateStopEmulation) {
+        return;
+    }
+
+    switch(nfcv_data->frame[0]) {
+    case RFAL_PICOPASS_CMD_ACTALL: // No args
+        if(nfcv_data->frame_length != 1) {
+            return;
+        }
+
+        if(ctx->state != PicopassEmulatorStateHalt) {
+            ctx->state = PicopassEmulatorStateActive;
+        }
+
+        // Send SOF only
+        break;
+    case RFAL_PICOPASS_CMD_ACT: // No args
+        if(nfcv_data->frame_length != 1 || ctx->state != PicopassEmulatorStateActive) {
+            return;
+        }
+
+        // Send SOF only
+        break;
+    case RFAL_PICOPASS_CMD_HALT: // No args
+        if(nfcv_data->frame_length != 1 || ctx->state != PicopassEmulatorStateSelected) {
+            return;
+        }
+
+        // Technically we should go to StateHalt, but since we can't detect the field dropping we drop to idle instead
+        ctx->state = PicopassEmulatorStateIdle;
+
+        // Send SOF only
+        break;
+    case RFAL_PICOPASS_CMD_READ_OR_IDENTIFY:
+        if(nfcv_data->frame_length == 1 &&
+           ctx->state == PicopassEmulatorStateActive) { // PICOPASS_CMD_IDENTIFY
+            // ASNB(8) CRC16(2)
+            picopass_anticoll_csn(response, nfc_data->uid);
+            picopass_append_crc(response, RFAL_PICOPASS_BLOCK_LEN);
+            response_length = RFAL_PICOPASS_BLOCK_LEN + 2;
+            break;
+        } else if(
+            nfcv_data->frame_length == 4 &&
+            ctx->state == PicopassEmulatorStateSelected) { // PICOPASS_CMD_READ ADDRESS(1) CRC16(2)
+            if(nfcv_data->frame[1] >= PICOPASS_MAX_APP_LIMIT) {
+                return;
+            }
+
+            // TODO: Check CRC?
+            // TODO: Check auth?
+
+            // DATA(8) CRC16(2)
+            if(nfcv_data->frame[1] == PICOPASS_SECURE_KD_BLOCK_INDEX ||
+               nfcv_data->frame[1] == PICOPASS_SECURE_KC_BLOCK_INDEX) {
+                // Reading Kd or Kc blocks always returns FF's
+                memcpy(response, block_ff, RFAL_PICOPASS_BLOCK_LEN);
+            } else {
+                picopass_emu_read_blocks(nfcv_data, response, nfcv_data->frame[1], 1);
+            }
+            picopass_append_crc(response, RFAL_PICOPASS_BLOCK_LEN);
+            response_length = RFAL_PICOPASS_BLOCK_LEN + 2;
+            break;
+        }
+
+        return;
+    case RFAL_PICOPASS_CMD_READ4: // ADDRESS(1) CRC16(2)
+        if(nfcv_data->frame_length != 4 || ctx->state != PicopassEmulatorStateSelected ||
+           nfcv_data->frame[1] + 4 >= PICOPASS_MAX_APP_LIMIT) {
+            return;
+        }
+
+        // TODO: Check CRC?
+        // TODO: Check auth?
+
+        uint8_t blockNum = nfcv_data->frame[1];
+
+        // DATA(32) CRC16(2)
+        picopass_emu_read_blocks(nfcv_data, response, blockNum, 4);
+        if(blockNum == 4) {
+            // Kc is block 4, so just redact first block of response
+            memcpy(response, block_ff, RFAL_PICOPASS_BLOCK_LEN);
+        } else if(blockNum < 4) {
+            // Kd is block 3
+            uint8_t* kdOffset = response + ((3 - blockNum) * RFAL_PICOPASS_BLOCK_LEN);
+            memcpy(kdOffset, block_ff, RFAL_PICOPASS_BLOCK_LEN);
+            if(blockNum != 0) {
+                // Redact Kc
+                memcpy(kdOffset + RFAL_PICOPASS_BLOCK_LEN, block_ff, RFAL_PICOPASS_BLOCK_LEN);
+            }
+        }
+        picopass_append_crc(response, RFAL_PICOPASS_BLOCK_LEN * 4);
+        response_length = (RFAL_PICOPASS_BLOCK_LEN * 4) + 2;
+        break;
+    case RFAL_PICOPASS_CMD_SELECT: // ASNB(8)|SERIALNB(8)
+        if(nfcv_data->frame_length != 9) {
+            return;
+        }
+
+        uint8_t select_csn[RFAL_PICOPASS_BLOCK_LEN];
+        if(ctx->state == PicopassEmulatorStateHalt || ctx->state == PicopassEmulatorStateIdle) {
+            memcpy(select_csn, nfc_data->uid, RFAL_PICOPASS_BLOCK_LEN);
+        } else {
+            picopass_anticoll_csn(select_csn, nfc_data->uid);
+        }
+
+        if(memcmp(nfcv_data->frame + 1, select_csn, RFAL_PICOPASS_BLOCK_LEN)) {
+            if(ctx->state == PicopassEmulatorStateActive) {
+                ctx->state = PicopassEmulatorStateIdle;
+            } else if(ctx->state == PicopassEmulatorStateSelected) {
+                // Technically we should go to StateHalt, but since we can't detect the field dropping we drop to idle instead
+                ctx->state = PicopassEmulatorStateIdle;
+            }
+
+            return;
+        }
+
+        ctx->state = PicopassEmulatorStateSelected;
+
+        // SERIALNB(8) CRC16(2)
+        memcpy(response, nfc_data->uid, RFAL_PICOPASS_BLOCK_LEN);
+        picopass_append_crc(response, RFAL_PICOPASS_BLOCK_LEN);
+
+        response_length = RFAL_PICOPASS_BLOCK_LEN + 2;
+        break;
+    case RFAL_PICOPASS_CMD_READCHECK_KC: // ADDRESS(1)
+        key_block_num = PICOPASS_SECURE_KC_BLOCK_INDEX;
+        // fallthrough
+    case RFAL_PICOPASS_CMD_READCHECK_KD: // ADDRESS(1)
+        if(nfcv_data->frame_length != 2 ||
+           nfcv_data->frame[1] != PICOPASS_SECURE_EPURSE_BLOCK_INDEX ||
+           ctx->state != PicopassEmulatorStateSelected) {
+            return;
+        }
+
+        // loclass mode doesn't do any card side crypto, just logs the readers crypto, so no-op in this mode
+        // we can also no-op if the key block is the same, CHECK re-inits if it failed already
+        if(ctx->key_block_num != key_block_num && !ctx->loclass_mode) {
+            ctx->key_block_num = key_block_num;
+            picopass_init_cipher_state(nfcv_data, ctx);
+        }
+
+        // DATA(8)
+        picopass_emu_read_blocks(nfcv_data, response, nfcv_data->frame[1], 1);
+        response_length = RFAL_PICOPASS_BLOCK_LEN;
+        break;
+    case RFAL_PICOPASS_CMD_CHECK: // CHALLENGE(4) READERSIGNATURE(4)
+        if(nfcv_data->frame_length != 9 || ctx->state != PicopassEmulatorStateSelected) {
+            return;
+        }
+
+        if(ctx->loclass_mode) {
+            // LOCLASS Reader attack mode
+
+            // Copy EPURSE
+            uint8_t cc[RFAL_PICOPASS_BLOCK_LEN];
+            picopass_emu_read_blocks(nfcv_data, cc, PICOPASS_SECURE_EPURSE_BLOCK_INDEX, 1);
+
+#ifndef PICOPASS_DEBUG_IGNORE_LOCLASS_STD_KEY
+            uint8_t key[RFAL_PICOPASS_BLOCK_LEN];
+            // loclass mode stores the derived standard debit key in Kd to check
+            picopass_emu_read_blocks(nfcv_data, key, PICOPASS_SECURE_KD_BLOCK_INDEX, 1);
+
+            uint8_t rmac[4];
+            loclass_opt_doReaderMAC_2(ctx->cipher_state, nfcv_data->frame + 1, rmac, key);
+
+            if(!memcmp(nfcv_data->frame + 5, rmac, 4)) {
+                // MAC from reader matches Standard Key, keyroll mode or non-elite keyed reader.
+                // Either way no point logging it.
+
+                FURI_LOG_W(TAG, "loclass: standard key detected during collection");
+                ctx->loclass_got_std_key = true;
+
+                // Don't reset the state as the reader may try a different key next without going through anticoll
+                // The reader is always free to redo the anticoll if it wants to anyway
+
+                return;
+            }
+#endif
+
+            // Save to buffer to defer flushing when we rotate CSN
+            memcpy(
+                ctx->loclass_mac_buffer + ((ctx->key_block_num % LOCLASS_NUM_PER_CSN) * 8),
+                nfcv_data->frame + 1,
+                8);
+
+            // Rotate to the next CSN/attempt
+            ctx->key_block_num++;
+
+            // CSN changed
+            if(ctx->key_block_num % LOCLASS_NUM_PER_CSN == 0) {
+                // Flush NR-MACs for this CSN to SD card
+                uint8_t cc[RFAL_PICOPASS_BLOCK_LEN];
+                picopass_emu_read_blocks(nfcv_data, cc, PICOPASS_SECURE_EPURSE_BLOCK_INDEX, 1);
+
+                for(int i = 0; i < LOCLASS_NUM_PER_CSN; i++) {
+                    loclass_writer_write_params(
+                        ctx->loclass_writer,
+                        ctx->key_block_num + i - LOCLASS_NUM_PER_CSN,
+                        nfc_data->uid,
+                        cc,
+                        ctx->loclass_mac_buffer + (i * 8),
+                        ctx->loclass_mac_buffer + (i * 8) + 4);
+                }
+
+                if(ctx->key_block_num < LOCLASS_NUM_CSNS * LOCLASS_NUM_PER_CSN) {
+                    loclass_update_csn(nfc_data, nfcv_data, ctx);
+                    // Only reset the state when we change to a new CSN for the same reason as when we get a standard key
+                    ctx->state = PicopassEmulatorStateIdle;
+                } else {
+                    ctx->state = PicopassEmulatorStateStopEmulation;
+                }
+            }
+
+            return;
+        }
+
+        uint8_t key[RFAL_PICOPASS_BLOCK_LEN];
+        picopass_emu_read_blocks(nfcv_data, key, ctx->key_block_num, 1);
+
+        uint8_t rmac[4];
+        loclass_opt_doBothMAC_2(ctx->cipher_state, nfcv_data->frame + 1, rmac, response, key);
+
+#ifndef PICOPASS_DEBUG_IGNORE_BAD_RMAC
+        if(memcmp(nfcv_data->frame + 5, rmac, 4)) {
+            // Bad MAC from reader, do not send a response.
+            FURI_LOG_I(TAG, "Got bad MAC from reader");
+            // Reset the cipher state since we don't do it in READCHECK
+            picopass_init_cipher_state(nfcv_data, ctx);
+            return;
+        }
+#endif
+
+        // CHIPRESPONSE(4)
+        response_length = 4;
+        break;
+    case RFAL_PICOPASS_CMD_UPDATE: // ADDRESS(1) DATA(8) SIGN(4)|CRC16(2)
+        if((nfcv_data->frame_length != 12 && nfcv_data->frame_length != 14) ||
+           ctx->state != PicopassEmulatorStateSelected || ctx->loclass_mode) {
+            return;
+        }
+
+        if(nfcv_data->frame[1] >= PICOPASS_MAX_APP_LIMIT) {
+            return;
+        }
+
+        uint8_t cfgBlock[RFAL_PICOPASS_BLOCK_LEN];
+        picopass_emu_read_blocks(nfcv_data, cfgBlock, PICOPASS_CONFIG_BLOCK_INDEX, 1);
+        bool persMode = HAS_MASK(cfgBlock[7], PICOPASS_FUSE_PERS);
+
+        if((nfcv_data->frame[1] == PICOPASS_CSN_BLOCK_INDEX) // CSN is always read only
+           ||
+           (!persMode &&
+            !HAS_MASK(cfgBlock[3], 0x80)) // Chip is in RO mode, no updated possible (even ePurse)
+           || (!persMode &&
+               nfcv_data->frame[1] ==
+                   PICOPASS_SECURE_AIA_BLOCK_INDEX) // AIA can only be set in personalisation mode
+           || (!persMode &&
+               (nfcv_data->frame[1] == PICOPASS_SECURE_KD_BLOCK_INDEX ||
+                nfcv_data->frame[1] == PICOPASS_SECURE_KC_BLOCK_INDEX) &&
+               (!HAS_MASK(cfgBlock[7], PICOPASS_FUSE_CRYPT10)))) {
+            return; // TODO: Is this the right response?
+        }
+
+        if(nfcv_data->frame[1] >= 6 && nfcv_data->frame[1] <= 12) {
+            if(!HAS_MASK(
+                   cfgBlock[3],
+                   1 << (nfcv_data->frame[1] - 6))) { // bit0 is block6, up to bit6 being block12
+                // Block is marked as read-only, deny writing
+                return; // TODO: Is this the right response?
+            }
+        }
+
+        // TODO: Check CRC/SIGN depending on if in secure mode
+        // Check correct key
+        // -> Kd only allows decrementing e-Purse
+        // -> per-app controlled by key access config
+        //bool keyAccess = HAS_MASK(cfgBlock[5], 0x01);
+        // -> must auth with that key to change it
+
+        uint8_t blockOffset = nfcv_data->frame[1];
+        uint8_t block[RFAL_PICOPASS_BLOCK_LEN];
+        switch(nfcv_data->frame[1]) {
+        case PICOPASS_CONFIG_BLOCK_INDEX:
+            block[0] = cfgBlock[0]; // Applications Limit
+            block[1] = cfgBlock[1] & nfcv_data->frame[3]; // OTP
+            block[2] = cfgBlock[2] & nfcv_data->frame[4]; // OTP
+            block[3] = cfgBlock[3] & nfcv_data->frame[5]; // Block Write Lock
+            block[4] = cfgBlock[4]; // Chip Config
+            block[5] = cfgBlock[5]; // Memory Config
+            block[6] = nfcv_data->frame[8]; // EAS
+            block[7] = cfgBlock[7]; // Fuses
+
+            // Some parts allow w (but not e) if in persMode
+            if(persMode) {
+                block[0] &= nfcv_data->frame[2]; // Applications Limit
+                block[4] &= nfcv_data->frame[6]; // Chip Config
+                block[5] &= nfcv_data->frame[7]; // Memory Config
+                block[7] &= nfcv_data->frame[9]; // Fuses
+            } else {
+                // Fuses allows setting Crypt1/0 from 1 to 0 only during application mode
+                block[7] &= nfcv_data->frame[9] | ~PICOPASS_FUSE_CRYPT10;
+            }
+            break;
+        case PICOPASS_SECURE_EPURSE_BLOCK_INDEX:
+            // ePurse updates swap first and second half of the block each update
+            memcpy(block + 4, nfcv_data->frame + 2, 4);
+            memcpy(block, nfcv_data->frame + 6, 4);
+            break;
+        case PICOPASS_SECURE_KD_BLOCK_INDEX:
+            // fallthrough
+        case PICOPASS_SECURE_KC_BLOCK_INDEX:
+            if(!persMode) {
+                picopass_emu_read_blocks(nfcv_data, block, blockOffset, 1);
+                for(uint8_t i = 0; i < sizeof(RFAL_PICOPASS_BLOCK_LEN); i++)
+                    block[i] ^= nfcv_data->frame[i + 2];
+                break;
+            }
+            // Use default case when in personalisation mode
+            // fallthrough
+        default:
+            memcpy(block, nfcv_data->frame + 2, RFAL_PICOPASS_BLOCK_LEN);
+            break;
+        }
+
+        picopass_emu_write_blocks(nfcv_data, block, blockOffset, 1);
+
+        if((nfcv_data->frame[1] == ctx->key_block_num ||
+            nfcv_data->frame[1] == PICOPASS_SECURE_EPURSE_BLOCK_INDEX) &&
+           !ctx->loclass_mode)
+            picopass_init_cipher_state(nfcv_data, ctx);
+
+        // DATA(8) CRC16(2)
+        if(nfcv_data->frame[1] == PICOPASS_SECURE_KD_BLOCK_INDEX ||
+           nfcv_data->frame[1] == PICOPASS_SECURE_KD_BLOCK_INDEX) {
+            // Key updates always return FF's
+            memcpy(response, block_ff, RFAL_PICOPASS_BLOCK_LEN);
+        } else {
+            memcpy(response, block, RFAL_PICOPASS_BLOCK_LEN);
+        }
+        picopass_append_crc(response, RFAL_PICOPASS_BLOCK_LEN);
+        response_length = RFAL_PICOPASS_BLOCK_LEN + 2;
+        break;
+    case RFAL_PICOPASS_CMD_PAGESEL: // PAGE(1) CRC16(2)
+        // Chips with a single page do not answer to this command
+        // BLOCK1(8) CRC16(2)
+        return;
+    case RFAL_PICOPASS_CMD_DETECT:
+        // TODO - not used by iClass though
+        return;
+    default:
+        return;
+    }
+
+    NfcVSendFlags flags = NfcVSendFlagsSof | NfcVSendFlagsOneSubcarrier | NfcVSendFlagsHighRate;
+    if(response_length > 0) {
+        flags |= NfcVSendFlagsEof;
+    }
+
+    nfcv_emu_send(
+        tx_rx,
+        nfcv_data,
+        response,
+        response_length,
+        flags,
+        nfcv_data->eof_timestamp + NFCV_FDT_FC(4000)); // 3650 is ~254uS 4000 is ~283uS
+}
+
+void picopass_worker_emulate(PicopassWorker* picopass_worker, bool loclass_mode) {
+    furi_hal_nfc_exit_sleep();
+
+    FuriHalNfcTxRxContext tx_rx = {};
+    PicopassEmulatorCtx emu_ctx = {
+        .state = PicopassEmulatorStateIdle,
+        .key_block_num = PICOPASS_SECURE_KD_BLOCK_INDEX,
+        .loclass_mode = loclass_mode,
+        .loclass_got_std_key = false,
+        .loclass_writer = NULL,
+    };
+    FuriHalNfcDevData nfc_data = {
+        .uid_len = RFAL_PICOPASS_UID_LEN,
+    };
+    NfcVData* nfcv_data = malloc(sizeof(NfcVData));
+    nfcv_data->block_size = RFAL_PICOPASS_BLOCK_LEN;
+    nfcv_data->emu_protocol_ctx = &emu_ctx;
+    nfcv_data->emu_protocol_handler = &picopass_emu_handle_packet;
+
+    PicopassDeviceData* dev_data = picopass_worker->dev_data;
+    PicopassBlock* blocks = dev_data->AA1;
+
+    if(loclass_mode) {
+        emu_ctx.loclass_writer = loclass_writer_alloc();
+        if(emu_ctx.loclass_writer == NULL) {
+            picopass_worker->callback(
+                PicopassWorkerEventLoclassFileError, picopass_worker->context);
+
+            while(picopass_worker->state == PicopassWorkerStateEmulate ||
+                  picopass_worker->state == PicopassWorkerStateLoclass) {
+                furi_delay_ms(1);
+            }
+
+            free(nfcv_data);
+
+            return;
+        }
+
+        // Setup blocks for loclass attack
+        uint8_t conf[8] = {0x12, 0xFF, 0xFF, 0xFF, 0x7F, 0x1F, 0xFF, 0x3C};
+        picopass_emu_write_blocks(nfcv_data, conf, PICOPASS_CONFIG_BLOCK_INDEX, 1);
+
+        uint8_t epurse[8] = {0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+        picopass_emu_write_blocks(nfcv_data, epurse, PICOPASS_SECURE_EPURSE_BLOCK_INDEX, 1);
+
+        uint8_t aia[8] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+        picopass_emu_write_blocks(nfcv_data, aia, PICOPASS_SECURE_AIA_BLOCK_INDEX, 1);
+
+        emu_ctx.key_block_num = 0;
+        loclass_update_csn(&nfc_data, nfcv_data, &emu_ctx);
+
+        loclass_writer_write_start_stop(emu_ctx.loclass_writer, true);
+    } else {
+        memcpy(nfc_data.uid, blocks[PICOPASS_CSN_BLOCK_INDEX].data, RFAL_PICOPASS_BLOCK_LEN);
+        memcpy(nfcv_data->data, blocks, sizeof(dev_data->AA1));
+        picopass_init_cipher_state(nfcv_data, &emu_ctx);
+    }
+
+    uint8_t last_loclass_csn_num = 0;
+    bool loclass_got_std_key = false;
+
+    nfcv_emu_init(&nfc_data, nfcv_data);
+    while(picopass_worker->state == PicopassWorkerStateEmulate ||
+          picopass_worker->state == PicopassWorkerStateLoclass) {
+        if(nfcv_emu_loop(&tx_rx, &nfc_data, nfcv_data, 500)) {
+            if(picopass_worker->callback) {
+                if((loclass_mode) && (last_loclass_csn_num != emu_ctx.key_block_num)) {
+                    last_loclass_csn_num = emu_ctx.key_block_num;
+                    picopass_worker->callback(
+                        PicopassWorkerEventLoclassGotMac, picopass_worker->context);
+                } else if((loclass_mode) && !loclass_got_std_key && emu_ctx.loclass_got_std_key) {
+                    loclass_got_std_key = true;
+                    picopass_worker->callback(
+                        PicopassWorkerEventLoclassGotStandardKey, picopass_worker->context);
+                } else {
+                    picopass_worker->callback(
+                        PicopassWorkerEventSuccess, picopass_worker->context);
+                }
+            }
+        }
+        furi_delay_us(1);
+    }
+
+    if(emu_ctx.loclass_writer) {
+        loclass_writer_write_start_stop(emu_ctx.loclass_writer, false);
+        loclass_writer_free(emu_ctx.loclass_writer);
+    }
+
+    nfcv_emu_deinit(nfcv_data);
+    free(nfcv_data);
+}

+ 57 - 0
picopass/picopass_worker.h

@@ -0,0 +1,57 @@
+#pragma once
+
+#include "picopass_device.h"
+#include "picopass_keys.h"
+
+typedef struct PicopassWorker PicopassWorker;
+
+typedef enum {
+    // Init states
+    PicopassWorkerStateNone,
+    PicopassWorkerStateBroken,
+    PicopassWorkerStateReady,
+    // Main worker states
+    PicopassWorkerStateDetect,
+    PicopassWorkerStateWrite,
+    PicopassWorkerStateWriteKey,
+    PicopassWorkerStateEliteDictAttack,
+    PicopassWorkerStateEmulate,
+    PicopassWorkerStateLoclass,
+    // Transition
+    PicopassWorkerStateStop,
+} PicopassWorkerState;
+
+typedef enum {
+    // Reserve first 50 events for application events
+    PicopassWorkerEventReserved = 50,
+
+    // Picopass worker common events
+    PicopassWorkerEventSuccess,
+    PicopassWorkerEventFail,
+    PicopassWorkerEventNoCardDetected,
+    PicopassWorkerEventSeEnabled,
+    PicopassWorkerEventAborted,
+    PicopassWorkerEventCardDetected,
+    PicopassWorkerEventNewDictKeyBatch,
+    PicopassWorkerEventNoDictFound,
+    PicopassWorkerEventLoclassGotMac,
+    PicopassWorkerEventLoclassGotStandardKey,
+    PicopassWorkerEventLoclassFileError,
+} PicopassWorkerEvent;
+
+typedef void (*PicopassWorkerCallback)(PicopassWorkerEvent event, void* context);
+
+PicopassWorker* picopass_worker_alloc();
+
+PicopassWorkerState picopass_worker_get_state(PicopassWorker* picopass_worker);
+
+void picopass_worker_free(PicopassWorker* picopass_worker);
+
+void picopass_worker_start(
+    PicopassWorker* picopass_worker,
+    PicopassWorkerState state,
+    PicopassDeviceData* dev_data,
+    PicopassWorkerCallback callback,
+    void* context);
+
+void picopass_worker_stop(PicopassWorker* picopass_worker);

+ 36 - 0
picopass/picopass_worker_i.h

@@ -0,0 +1,36 @@
+#pragma once
+
+#include "picopass_worker.h"
+#include "loclass_writer.h"
+#include "picopass_i.h"
+
+#include <furi.h>
+#include <lib/toolbox/stream/file_stream.h>
+
+#include <furi_hal.h>
+
+#include <stdlib.h>
+#include <rfal_rf.h>
+
+#include <platform.h>
+
+struct PicopassWorker {
+    FuriThread* thread;
+    Storage* storage;
+    Stream* dict_stream;
+
+    PicopassDeviceData* dev_data;
+    PicopassWorkerCallback callback;
+    void* context;
+
+    PicopassWorkerState state;
+};
+
+void picopass_worker_change_state(PicopassWorker* picopass_worker, PicopassWorkerState state);
+
+int32_t picopass_worker_task(void* context);
+
+void picopass_worker_detect(PicopassWorker* picopass_worker);
+void picopass_worker_write(PicopassWorker* picopass_worker);
+void picopass_worker_write_key(PicopassWorker* picopass_worker);
+void picopass_worker_emulate(PicopassWorker* picopass_worker, bool loclass_mode);

+ 213 - 0
picopass/rfal_picopass.c

@@ -0,0 +1,213 @@
+#include "rfal_picopass.h"
+
+#define RFAL_PICOPASS_TXRX_FLAGS                                                    \
+    (FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_TX_MANUAL | FURI_HAL_NFC_LL_TXRX_FLAGS_AGC_ON | \
+     FURI_HAL_NFC_LL_TXRX_FLAGS_PAR_RX_REMV | FURI_HAL_NFC_LL_TXRX_FLAGS_CRC_RX_KEEP)
+
+#define TAG "RFAL_PICOPASS"
+
+typedef struct {
+    uint8_t CMD;
+    uint8_t CSN[RFAL_PICOPASS_UID_LEN];
+} rfalPicoPassSelectReq;
+
+typedef struct {
+    uint8_t CMD;
+    uint8_t null[4];
+    uint8_t mac[4];
+} rfalPicoPassCheckReq;
+
+static uint16_t rfalPicoPassUpdateCcitt(uint16_t crcSeed, uint8_t dataByte) {
+    uint16_t crc = crcSeed;
+    uint8_t dat = dataByte;
+
+    dat ^= (uint8_t)(crc & 0xFFU);
+    dat ^= (dat << 4);
+
+    crc = (crc >> 8) ^ (((uint16_t)dat) << 8) ^ (((uint16_t)dat) << 3) ^ (((uint16_t)dat) >> 4);
+
+    return crc;
+}
+
+uint16_t rfalPicoPassCalculateCcitt(uint16_t preloadValue, const uint8_t* buf, uint16_t length) {
+    uint16_t crc = preloadValue;
+    uint16_t index;
+
+    for(index = 0; index < length; index++) {
+        crc = rfalPicoPassUpdateCcitt(crc, buf[index]);
+    }
+
+    return crc;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerInitialize(void) {
+    FuriHalNfcReturn ret;
+
+    ret = furi_hal_nfc_ll_set_mode(
+        FuriHalNfcModePollPicopass, FuriHalNfcBitrate26p48, FuriHalNfcBitrate26p48);
+    if(ret != FuriHalNfcReturnOk) {
+        return ret;
+    };
+
+    furi_hal_nfc_ll_set_error_handling(FuriHalNfcErrorHandlingNfc);
+    furi_hal_nfc_ll_set_guard_time(FURI_HAL_NFC_LL_GT_PICOPASS);
+    furi_hal_nfc_ll_set_fdt_listen(FURI_HAL_NFC_LL_FDT_LISTEN_PICOPASS_POLLER);
+    furi_hal_nfc_ll_set_fdt_poll(FURI_HAL_NFC_LL_FDT_POLL_PICOPASS_POLLER);
+
+    return FuriHalNfcReturnOk;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerCheckPresence(void) {
+    FuriHalNfcReturn ret;
+    uint8_t txBuf[1] = {RFAL_PICOPASS_CMD_ACTALL};
+    uint8_t rxBuf[32] = {0};
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    ret = furi_hal_nfc_ll_txrx(txBuf, 1, rxBuf, 32, &recvLen, flags, fwt);
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerIdentify(rfalPicoPassIdentifyRes* idRes) {
+    FuriHalNfcReturn ret;
+
+    uint8_t txBuf[1] = {RFAL_PICOPASS_CMD_READ_OR_IDENTIFY};
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    ret = furi_hal_nfc_ll_txrx(
+        txBuf,
+        sizeof(txBuf),
+        (uint8_t*)idRes,
+        sizeof(rfalPicoPassIdentifyRes),
+        &recvLen,
+        flags,
+        fwt);
+    // printf("identify rx: %d %s\n", recvLen, hex2Str(idRes->CSN, RFAL_PICOPASS_UID_LEN));
+
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerSelect(uint8_t* csn, rfalPicoPassSelectRes* selRes) {
+    FuriHalNfcReturn ret;
+
+    rfalPicoPassSelectReq selReq;
+    selReq.CMD = RFAL_PICOPASS_CMD_SELECT;
+    memcpy(selReq.CSN, csn, RFAL_PICOPASS_UID_LEN);
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    ret = furi_hal_nfc_ll_txrx(
+        (uint8_t*)&selReq,
+        sizeof(rfalPicoPassSelectReq),
+        (uint8_t*)selRes,
+        sizeof(rfalPicoPassSelectRes),
+        &recvLen,
+        flags,
+        fwt);
+    // printf("select rx: %d %s\n", recvLen, hex2Str(selRes->CSN, RFAL_PICOPASS_UID_LEN));
+    if(ret == FuriHalNfcReturnTimeout) {
+        return FuriHalNfcReturnOk;
+    }
+
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerReadCheck(rfalPicoPassReadCheckRes* rcRes) {
+    FuriHalNfcReturn ret;
+    uint8_t txBuf[2] = {RFAL_PICOPASS_CMD_READCHECK_KD, 0x02};
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    ret = furi_hal_nfc_ll_txrx(
+        txBuf,
+        sizeof(txBuf),
+        (uint8_t*)rcRes,
+        sizeof(rfalPicoPassReadCheckRes),
+        &recvLen,
+        flags,
+        fwt);
+    // printf("readcheck rx: %d %s\n", recvLen, hex2Str(rcRes->CCNR, 8));
+
+    if(ret == FuriHalNfcReturnCrc) {
+        return FuriHalNfcReturnOk;
+    }
+
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerCheck(uint8_t* mac, rfalPicoPassCheckRes* chkRes) {
+    FuriHalNfcReturn ret;
+    rfalPicoPassCheckReq chkReq;
+    chkReq.CMD = RFAL_PICOPASS_CMD_CHECK;
+    memcpy(chkReq.mac, mac, 4);
+    memset(chkReq.null, 0, 4);
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    // printf("check tx: %s\n", hex2Str((uint8_t *)&chkReq, sizeof(rfalPicoPassCheckReq)));
+    ret = furi_hal_nfc_ll_txrx(
+        (uint8_t*)&chkReq,
+        sizeof(rfalPicoPassCheckReq),
+        (uint8_t*)chkRes,
+        sizeof(rfalPicoPassCheckRes),
+        &recvLen,
+        flags,
+        fwt);
+    // printf("check rx: %d %s\n", recvLen, hex2Str(chkRes->mac, 4));
+    if(ret == FuriHalNfcReturnCrc) {
+        return FuriHalNfcReturnOk;
+    }
+
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerReadBlock(uint8_t blockNum, rfalPicoPassReadBlockRes* readRes) {
+    FuriHalNfcReturn ret;
+
+    uint8_t txBuf[4] = {RFAL_PICOPASS_CMD_READ_OR_IDENTIFY, 0, 0, 0};
+    txBuf[1] = blockNum;
+    uint16_t crc = rfalPicoPassCalculateCcitt(0xE012, txBuf + 1, 1);
+    memcpy(txBuf + 2, &crc, sizeof(uint16_t));
+
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+
+    ret = furi_hal_nfc_ll_txrx(
+        txBuf,
+        sizeof(txBuf),
+        (uint8_t*)readRes,
+        sizeof(rfalPicoPassReadBlockRes),
+        &recvLen,
+        flags,
+        fwt);
+    return ret;
+}
+
+FuriHalNfcReturn rfalPicoPassPollerWriteBlock(uint8_t blockNum, uint8_t data[8], uint8_t mac[4]) {
+    FuriHalNfcReturn ret;
+
+    uint8_t txBuf[14] = {RFAL_PICOPASS_CMD_UPDATE, blockNum, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+    memcpy(txBuf + 2, data, RFAL_PICOPASS_BLOCK_LEN);
+    memcpy(txBuf + 10, mac, 4);
+
+    uint16_t recvLen = 0;
+    uint32_t flags = RFAL_PICOPASS_TXRX_FLAGS;
+    uint32_t fwt = furi_hal_nfc_ll_ms2fc(20);
+    rfalPicoPassReadBlockRes block;
+
+    ret = furi_hal_nfc_ll_txrx(
+        txBuf, sizeof(txBuf), (uint8_t*)&block, sizeof(block), &recvLen, flags, fwt);
+
+    if(ret == FuriHalNfcReturnOk) {
+        // TODO: compare response
+    }
+
+    return ret;
+}

+ 75 - 0
picopass/rfal_picopass.h

@@ -0,0 +1,75 @@
+#pragma once
+
+#include <furi_hal_nfc.h>
+
+#define RFAL_PICOPASS_UID_LEN 8
+#define RFAL_PICOPASS_BLOCK_LEN 8
+
+enum {
+    // PicoPass command bytes:
+    // Low nibble used for command
+    // High nibble used for options and checksum (MSB)
+    // The only option we care about in 15693 mode is the key
+    // which is only used by READCHECK, so for simplicity we
+    // don't bother breaking down the command and flags into parts
+
+    // READ: ADDRESS(1) CRC16(2) -> DATA(8) CRC16(2)
+    // IDENTIFY: No args -> ASNB(8) CRC16(2)
+    RFAL_PICOPASS_CMD_READ_OR_IDENTIFY = 0x0C,
+    // ADDRESS(1) CRC16(2) -> DATA(32) CRC16(2)
+    RFAL_PICOPASS_CMD_READ4 = 0x06,
+    // ADDRESS(1) DATA(8) SIGN(4)|CRC16(2) -> DATA(8) CRC16(2)
+    RFAL_PICOPASS_CMD_UPDATE = 0x87,
+    // ADDRESS(1) -> DATA(8)
+    RFAL_PICOPASS_CMD_READCHECK_KD = 0x88,
+    // ADDRESS(1) -> DATA(8)
+    RFAL_PICOPASS_CMD_READCHECK_KC = 0x18,
+    // CHALLENGE(4) READERSIGNATURE(4) -> CHIPRESPONSE(4)
+    RFAL_PICOPASS_CMD_CHECK = 0x05,
+    // No args -> SOF
+    RFAL_PICOPASS_CMD_ACTALL = 0x0A,
+    // No args -> SOF
+    RFAL_PICOPASS_CMD_ACT = 0x8E,
+    // ASNB(8)|SERIALNB(8) -> SERIALNB(8) CRC16(2)
+    RFAL_PICOPASS_CMD_SELECT = 0x81,
+    // No args -> SERIALNB(8) CRC16(2)
+    RFAL_PICOPASS_CMD_DETECT = 0x0F,
+    // No args -> SOF
+    RFAL_PICOPASS_CMD_HALT = 0x00,
+    // PAGE(1) CRC16(2) -> BLOCK1(8) CRC16(2)
+    RFAL_PICOPASS_CMD_PAGESEL = 0x84,
+};
+
+typedef struct {
+    uint8_t CSN[RFAL_PICOPASS_UID_LEN]; // Anti-collision CSN
+    uint8_t crc[2];
+} rfalPicoPassIdentifyRes;
+
+typedef struct {
+    uint8_t CSN[RFAL_PICOPASS_UID_LEN]; // Real CSN
+    uint8_t crc[2];
+} rfalPicoPassSelectRes;
+
+typedef struct {
+    uint8_t CCNR[8];
+} rfalPicoPassReadCheckRes;
+
+typedef struct {
+    uint8_t mac[4];
+} rfalPicoPassCheckRes;
+
+typedef struct {
+    uint8_t data[RFAL_PICOPASS_BLOCK_LEN];
+    uint8_t crc[2];
+} rfalPicoPassReadBlockRes;
+
+uint16_t rfalPicoPassCalculateCcitt(uint16_t preloadValue, const uint8_t* buf, uint16_t length);
+
+FuriHalNfcReturn rfalPicoPassPollerInitialize(void);
+FuriHalNfcReturn rfalPicoPassPollerCheckPresence(void);
+FuriHalNfcReturn rfalPicoPassPollerIdentify(rfalPicoPassIdentifyRes* idRes);
+FuriHalNfcReturn rfalPicoPassPollerSelect(uint8_t* csn, rfalPicoPassSelectRes* selRes);
+FuriHalNfcReturn rfalPicoPassPollerReadCheck(rfalPicoPassReadCheckRes* rcRes);
+FuriHalNfcReturn rfalPicoPassPollerCheck(uint8_t* mac, rfalPicoPassCheckRes* chkRes);
+FuriHalNfcReturn rfalPicoPassPollerReadBlock(uint8_t blockNum, rfalPicoPassReadBlockRes* readRes);
+FuriHalNfcReturn rfalPicoPassPollerWriteBlock(uint8_t blockNum, uint8_t data[8], uint8_t mac[4]);

+ 30 - 0
picopass/scenes/picopass_scene.c

@@ -0,0 +1,30 @@
+#include "picopass_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const picopass_on_enter_handlers[])(void*) = {
+#include "picopass_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_event handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_event,
+bool (*const picopass_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "picopass_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_exit,
+void (*const picopass_on_exit_handlers[])(void* context) = {
+#include "picopass_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers picopass_scene_handlers = {
+    .on_enter_handlers = picopass_on_enter_handlers,
+    .on_event_handlers = picopass_on_event_handlers,
+    .on_exit_handlers = picopass_on_exit_handlers,
+    .scene_num = PicopassSceneNum,
+};

+ 29 - 0
picopass/scenes/picopass_scene.h

@@ -0,0 +1,29 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// Generate scene id and total number
+#define ADD_SCENE(prefix, name, id) PicopassScene##id,
+typedef enum {
+#include "picopass_scene_config.h"
+    PicopassSceneNum,
+} PicopassScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers picopass_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "picopass_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_event handlers declaration
+#define ADD_SCENE(prefix, name, id) \
+    bool prefix##_scene_##name##_on_event(void* context, SceneManagerEvent event);
+#include "picopass_scene_config.h"
+#undef ADD_SCENE
+
+// Generate scene on_exit handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_exit(void* context);
+#include "picopass_scene_config.h"
+#undef ADD_SCENE

+ 92 - 0
picopass/scenes/picopass_scene_card_menu.c

@@ -0,0 +1,92 @@
+#include "../picopass_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexSave,
+    SubmenuIndexSaveAsLF,
+    SubmenuIndexChangeKey,
+    SubmenuIndexWrite,
+    SubmenuIndexEmulate,
+};
+
+void picopass_scene_card_menu_submenu_callback(void* context, uint32_t index) {
+    Picopass* picopass = context;
+
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
+}
+
+void picopass_scene_card_menu_on_enter(void* context) {
+    Picopass* picopass = context;
+    Submenu* submenu = picopass->submenu;
+
+    submenu_add_item(
+        submenu, "Save", SubmenuIndexSave, picopass_scene_card_menu_submenu_callback, picopass);
+    submenu_add_item(
+        submenu,
+        "Save as LFRFID",
+        SubmenuIndexSaveAsLF,
+        picopass_scene_card_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu, "Write", SubmenuIndexWrite, picopass_scene_card_menu_submenu_callback, picopass);
+    submenu_add_item(
+        submenu,
+        "Emulate",
+        SubmenuIndexEmulate,
+        picopass_scene_card_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Change Key",
+        SubmenuIndexChangeKey,
+        picopass_scene_card_menu_submenu_callback,
+        picopass);
+
+    submenu_set_selected_item(
+        picopass->submenu,
+        scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneCardMenu));
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
+}
+
+bool picopass_scene_card_menu_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexSave) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSave);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            picopass->dev->format = PicopassDeviceSaveFormatHF;
+            consumed = true;
+        } else if(event.event == SubmenuIndexSaveAsLF) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexSaveAsLF);
+            picopass->dev->format = PicopassDeviceSaveFormatLF;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWrite) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCard);
+            consumed = true;
+        } else if(event.event == SubmenuIndexEmulate) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneEmulate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexChangeKey) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneCardMenu, SubmenuIndexChangeKey);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneKeyMenu);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            picopass->scene_manager, PicopassSceneStart);
+    }
+
+    return consumed;
+}
+
+void picopass_scene_card_menu_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    submenu_reset(picopass->submenu);
+}

+ 21 - 0
picopass/scenes/picopass_scene_config.h

@@ -0,0 +1,21 @@
+ADD_SCENE(picopass, start, Start)
+ADD_SCENE(picopass, read_card, ReadCard)
+ADD_SCENE(picopass, read_card_success, ReadCardSuccess)
+ADD_SCENE(picopass, card_menu, CardMenu)
+ADD_SCENE(picopass, save_name, SaveName)
+ADD_SCENE(picopass, save_success, SaveSuccess)
+ADD_SCENE(picopass, saved_menu, SavedMenu)
+ADD_SCENE(picopass, file_select, FileSelect)
+ADD_SCENE(picopass, device_info, DeviceInfo)
+ADD_SCENE(picopass, delete, Delete)
+ADD_SCENE(picopass, delete_success, DeleteSuccess)
+ADD_SCENE(picopass, write_card, WriteCard)
+ADD_SCENE(picopass, write_card_success, WriteCardSuccess)
+ADD_SCENE(picopass, write_card_failure, WriteCardFailure)
+ADD_SCENE(picopass, read_factory_success, ReadFactorySuccess)
+ADD_SCENE(picopass, write_key, WriteKey)
+ADD_SCENE(picopass, key_menu, KeyMenu)
+ADD_SCENE(picopass, elite_dict_attack, EliteDictAttack)
+ADD_SCENE(picopass, emulate, Emulate)
+ADD_SCENE(picopass, loclass, Loclass)
+ADD_SCENE(picopass, key_input, KeyInput)

+ 58 - 0
picopass/scenes/picopass_scene_delete.c

@@ -0,0 +1,58 @@
+#include "../picopass_i.h"
+
+void picopass_scene_delete_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Picopass* picopass = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_delete_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    // Setup Custom Widget view
+    char temp_str[64];
+    snprintf(temp_str, sizeof(temp_str), "\e#Delete %s?\e#", picopass->dev->dev_name);
+    widget_add_text_box_element(
+        picopass->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, temp_str, false);
+    widget_add_button_element(
+        picopass->widget,
+        GuiButtonTypeLeft,
+        "Back",
+        picopass_scene_delete_widget_callback,
+        picopass);
+    widget_add_button_element(
+        picopass->widget,
+        GuiButtonTypeRight,
+        "Delete",
+        picopass_scene_delete_widget_callback,
+        picopass);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_delete_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            return scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            if(picopass_device_delete(picopass->dev, true)) {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneDeleteSuccess);
+            } else {
+                scene_manager_search_and_switch_to_previous_scene(
+                    picopass->scene_manager, PicopassSceneStart);
+            }
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_delete_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    widget_reset(picopass->widget);
+}

+ 40 - 0
picopass/scenes/picopass_scene_delete_success.c

@@ -0,0 +1,40 @@
+#include "../picopass_i.h"
+
+void picopass_scene_delete_success_popup_callback(void* context) {
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
+}
+
+void picopass_scene_delete_success_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_icon(popup, 0, 2, &I_DolphinMafia_115x62);
+    popup_set_header(popup, "Deleted", 83, 19, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, picopass);
+    popup_set_callback(popup, picopass_scene_delete_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+}
+
+bool picopass_scene_delete_success_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                picopass->scene_manager, PicopassSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_delete_success_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    popup_reset(picopass->popup);
+}

+ 104 - 0
picopass/scenes/picopass_scene_device_info.c

@@ -0,0 +1,104 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_device_info_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    Picopass* picopass = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_device_info_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    FuriString* csn_str = furi_string_alloc_set("CSN:");
+    FuriString* credential_str = furi_string_alloc();
+    FuriString* wiegand_str = furi_string_alloc();
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    // Setup view
+    PicopassBlock* AA1 = picopass->dev->dev_data.AA1;
+    PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
+    Widget* widget = picopass->widget;
+
+    uint8_t csn[RFAL_PICOPASS_BLOCK_LEN] = {0};
+    memcpy(csn, AA1[PICOPASS_CSN_BLOCK_INDEX].data, RFAL_PICOPASS_BLOCK_LEN);
+    for(uint8_t i = 0; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(csn_str, "%02X ", csn[i]);
+    }
+
+    if(pacs->bitLength == 0 || pacs->bitLength == 255) {
+        // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
+        furi_string_cat_printf(wiegand_str, "Invalid PACS");
+    } else {
+        size_t bytesLength = pacs->bitLength / 8;
+        if(pacs->bitLength % 8 > 0) {
+            // Add extra byte if there are bits remaining
+            bytesLength++;
+        }
+        furi_string_set(credential_str, "");
+        for(uint8_t i = RFAL_PICOPASS_BLOCK_LEN - bytesLength; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+            furi_string_cat_printf(credential_str, "%02X", pacs->credential[i]);
+        }
+        furi_string_cat_printf(wiegand_str, "%d bits", pacs->bitLength);
+
+        if(pacs->sio) {
+            furi_string_cat_printf(credential_str, " +SIO");
+        }
+    }
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(csn_str));
+    widget_add_string_element(
+        widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(wiegand_str));
+    widget_add_string_element(
+        widget,
+        64,
+        36,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(credential_str));
+
+    furi_string_free(csn_str);
+    furi_string_free(credential_str);
+    furi_string_free(wiegand_str);
+
+    widget_add_button_element(
+        picopass->widget,
+        GuiButtonTypeLeft,
+        "Back",
+        picopass_scene_device_info_widget_callback,
+        picopass);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_device_info_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == PicopassCustomEventViewExit) {
+            view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void picopass_scene_device_info_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear views
+    widget_reset(picopass->widget);
+}

+ 172 - 0
picopass/scenes/picopass_scene_elite_dict_attack.c

@@ -0,0 +1,172 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "IclassEliteDictAttack"
+
+typedef enum {
+    DictAttackStateIdle,
+    DictAttackStateUserDictInProgress,
+    DictAttackStateFlipperDictInProgress,
+    DictAttackStateStandardDictInProgress,
+} DictAttackState;
+
+void picopass_dict_attack_worker_callback(PicopassWorkerEvent event, void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, event);
+}
+
+void picopass_dict_attack_result_callback(void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(
+        picopass->view_dispatcher, PicopassCustomEventDictAttackSkip);
+}
+
+static void
+    picopass_scene_elite_dict_attack_prepare_view(Picopass* picopass, DictAttackState state) {
+    IclassEliteDictAttackData* dict_attack_data =
+        &picopass->dev->dev_data.iclass_elite_dict_attack_data;
+    PicopassWorkerState worker_state = PicopassWorkerStateReady;
+    IclassEliteDict* dict = NULL;
+
+    // Identify scene state
+    if(state == DictAttackStateIdle) {
+        if(iclass_elite_dict_check_presence(IclassEliteDictTypeUser)) {
+            FURI_LOG_D(TAG, "Starting with user dictionary");
+            state = DictAttackStateUserDictInProgress;
+        } else {
+            FURI_LOG_D(TAG, "Starting with standard dictionary");
+            state = DictAttackStateStandardDictInProgress;
+        }
+    } else if(state == DictAttackStateUserDictInProgress) {
+        FURI_LOG_D(TAG, "Moving from user dictionary to standard dictionary");
+        state = DictAttackStateStandardDictInProgress;
+    } else if(state == DictAttackStateStandardDictInProgress) {
+        FURI_LOG_D(TAG, "Moving from standard dictionary to elite dictionary");
+        state = DictAttackStateFlipperDictInProgress;
+    }
+
+    // Setup view
+    if(state == DictAttackStateUserDictInProgress) {
+        worker_state = PicopassWorkerStateEliteDictAttack;
+        dict_attack_set_header(picopass->dict_attack, "Elite User Dictionary");
+        dict_attack_data->type = IclassEliteDictTypeUser;
+        dict = iclass_elite_dict_alloc(IclassEliteDictTypeUser);
+
+        // If failed to load user dictionary - try the system dictionary
+        if(!dict) {
+            FURI_LOG_E(TAG, "User dictionary not found");
+            state = DictAttackStateStandardDictInProgress;
+        }
+    }
+    if(state == DictAttackStateStandardDictInProgress) {
+        worker_state = PicopassWorkerStateEliteDictAttack;
+        dict_attack_set_header(picopass->dict_attack, "Standard System Dictionary");
+        dict_attack_data->type = IclassStandardDictTypeFlipper;
+        dict = iclass_elite_dict_alloc(IclassStandardDictTypeFlipper);
+
+        if(!dict) {
+            FURI_LOG_E(TAG, "Flipper standard dictionary not found");
+            state = DictAttackStateFlipperDictInProgress;
+        }
+    }
+    if(state == DictAttackStateFlipperDictInProgress) {
+        worker_state = PicopassWorkerStateEliteDictAttack;
+        dict_attack_set_header(picopass->dict_attack, "Elite System Dictionary");
+        dict_attack_data->type = IclassEliteDictTypeFlipper;
+        dict = iclass_elite_dict_alloc(IclassEliteDictTypeFlipper);
+        if(!dict) {
+            FURI_LOG_E(TAG, "Flipper Elite dictionary not found");
+            // Pass through to let the worker handle the failure
+        }
+    }
+    // Free previous dictionary
+    if(dict_attack_data->dict) {
+        iclass_elite_dict_free(dict_attack_data->dict);
+    }
+    dict_attack_data->dict = dict;
+    scene_manager_set_scene_state(picopass->scene_manager, PicopassSceneEliteDictAttack, state);
+    dict_attack_set_callback(
+        picopass->dict_attack, picopass_dict_attack_result_callback, picopass);
+    dict_attack_set_current_sector(picopass->dict_attack, 0);
+    dict_attack_set_card_detected(picopass->dict_attack);
+    dict_attack_set_total_dict_keys(
+        picopass->dict_attack, dict ? iclass_elite_dict_get_total_keys(dict) : 0);
+    picopass_worker_start(
+        picopass->worker,
+        worker_state,
+        &picopass->dev->dev_data,
+        picopass_dict_attack_worker_callback,
+        picopass);
+}
+
+void picopass_scene_elite_dict_attack_on_enter(void* context) {
+    Picopass* picopass = context;
+    picopass_scene_elite_dict_attack_prepare_view(picopass, DictAttackStateIdle);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewDictAttack);
+    picopass_blink_start(picopass);
+    notification_message(picopass->notifications, &sequence_display_backlight_enforce_on);
+}
+
+bool picopass_scene_elite_dict_attack_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    uint32_t state =
+        scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneEliteDictAttack);
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassWorkerEventSuccess) {
+            if(state == DictAttackStateUserDictInProgress ||
+               state == DictAttackStateStandardDictInProgress) {
+                picopass_worker_stop(picopass->worker);
+                picopass_scene_elite_dict_attack_prepare_view(picopass, state);
+                consumed = true;
+            } else {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess);
+                consumed = true;
+            }
+        } else if(event.event == PicopassWorkerEventAborted) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess);
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventCardDetected) {
+            dict_attack_set_card_detected(picopass->dict_attack);
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventNoCardDetected) {
+            dict_attack_set_card_removed(picopass->dict_attack);
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventNewDictKeyBatch) {
+            dict_attack_inc_current_dict_key(picopass->dict_attack, PICOPASS_DICT_KEY_BATCH_SIZE);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventDictAttackSkip) {
+            if(state == DictAttackStateUserDictInProgress) {
+                picopass_worker_stop(picopass->worker);
+                consumed = true;
+            } else if(state == DictAttackStateFlipperDictInProgress) {
+                picopass_worker_stop(picopass->worker);
+                consumed = true;
+            } else if(state == DictAttackStateStandardDictInProgress) {
+                picopass_worker_stop(picopass->worker);
+                consumed = true;
+            }
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(picopass->scene_manager);
+    }
+    return consumed;
+}
+
+void picopass_scene_elite_dict_attack_on_exit(void* context) {
+    Picopass* picopass = context;
+    IclassEliteDictAttackData* dict_attack_data =
+        &picopass->dev->dev_data.iclass_elite_dict_attack_data;
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+    if(dict_attack_data->dict) {
+        iclass_elite_dict_free(dict_attack_data->dict);
+        dict_attack_data->dict = NULL;
+    }
+    dict_attack_reset(picopass->dict_attack);
+    picopass_blink_stop(picopass);
+    notification_message(picopass->notifications, &sequence_display_backlight_enforce_auto);
+}

+ 58 - 0
picopass/scenes/picopass_scene_emulate.c

@@ -0,0 +1,58 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_emulate_worker_callback(PicopassWorkerEvent event, void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, event);
+}
+
+void picopass_scene_emulate_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcEmulate);
+
+    Widget* widget = picopass->widget;
+    widget_reset(widget);
+    widget_add_icon_element(widget, 0, 3, &I_RFIDDolphinSend_97x61);
+    widget_add_string_element(widget, 89, 32, AlignCenter, AlignTop, FontPrimary, "Emulating");
+    widget_add_string_element(widget, 89, 42, AlignCenter, AlignTop, FontPrimary, "PicoPass");
+
+    // Setup view
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+
+    // Start worker
+    picopass_worker_start(
+        picopass->worker,
+        PicopassWorkerStateEmulate,
+        &picopass->dev->dev_data,
+        picopass_emulate_worker_callback,
+        picopass);
+
+    picopass_blink_emulate_start(picopass);
+}
+
+bool picopass_scene_emulate_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventWorkerExit) {
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(picopass->scene_manager);
+    }
+    return consumed;
+}
+
+void picopass_scene_emulate_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    picopass_blink_stop(picopass);
+
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 25 - 0
picopass/scenes/picopass_scene_file_select.c

@@ -0,0 +1,25 @@
+#include "../picopass_i.h"
+#include "../picopass_device.h"
+
+void picopass_scene_file_select_on_enter(void* context) {
+    Picopass* picopass = context;
+    // Process file_select return
+    picopass_device_set_loading_callback(picopass->dev, picopass_show_loading_popup, picopass);
+    if(picopass_file_select(picopass->dev)) {
+        scene_manager_next_scene(picopass->scene_manager, PicopassSceneSavedMenu);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(
+            picopass->scene_manager, PicopassSceneStart);
+    }
+    picopass_device_set_loading_callback(picopass->dev, NULL, picopass);
+}
+
+bool picopass_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void picopass_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 48 - 0
picopass/scenes/picopass_scene_key_input.c

@@ -0,0 +1,48 @@
+#include "../picopass_i.h"
+#include <lib/toolbox/name_generator.h>
+#include <gui/modules/validators.h>
+#include <toolbox/path.h>
+
+void picopass_scene_key_input_text_input_callback(void* context) {
+    Picopass* picopass = context;
+
+    picopass->dev->dev_data.pacs.elite_kdf = true;
+    memcpy(picopass->dev->dev_data.pacs.key, picopass->byte_input_store, RFAL_PICOPASS_BLOCK_LEN);
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventByteInputDone);
+}
+
+void picopass_scene_key_input_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    ByteInput* byte_input = picopass->byte_input;
+    byte_input_set_header_text(byte_input, "Enter The Key In Hex");
+    byte_input_set_result_callback(
+        byte_input,
+        picopass_scene_key_input_text_input_callback,
+        NULL,
+        picopass,
+        picopass->byte_input_store,
+        RFAL_PICOPASS_BLOCK_LEN);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewByteInput);
+}
+
+bool picopass_scene_key_input_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventByteInputDone) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_key_input_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    byte_input_set_result_callback(picopass->byte_input, NULL, NULL, NULL, NULL, 0);
+    byte_input_set_header_text(picopass->byte_input, "");
+}

+ 119 - 0
picopass/scenes/picopass_scene_key_menu.c

@@ -0,0 +1,119 @@
+#include "../picopass_i.h"
+#include "../picopass_keys.h"
+
+enum SubmenuIndex {
+    SubmenuIndexWriteStandard,
+    SubmenuIndexWriteiCE,
+    SubmenuIndexWriteiCL,
+    SubmenuIndexWriteiCS,
+    SubmenuIndexWriteCustom,
+};
+
+void picopass_scene_key_menu_submenu_callback(void* context, uint32_t index) {
+    Picopass* picopass = context;
+
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
+}
+
+void picopass_scene_key_menu_on_enter(void* context) {
+    Picopass* picopass = context;
+    Submenu* submenu = picopass->submenu;
+
+    submenu_add_item(
+        submenu,
+        "Write Standard",
+        SubmenuIndexWriteStandard,
+        picopass_scene_key_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Write iCE",
+        SubmenuIndexWriteiCE,
+        picopass_scene_key_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Write iCL",
+        SubmenuIndexWriteiCL,
+        picopass_scene_key_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Write iCS",
+        SubmenuIndexWriteiCS,
+        picopass_scene_key_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Write Elite",
+        SubmenuIndexWriteCustom,
+        picopass_scene_key_menu_submenu_callback,
+        picopass);
+
+    submenu_set_selected_item(
+        picopass->submenu,
+        scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneKeyMenu));
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
+}
+
+bool picopass_scene_key_menu_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexWriteStandard) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneKeyMenu, SubmenuIndexWriteStandard);
+            memcpy(picopass->dev->dev_data.pacs.key, picopass_iclass_key, RFAL_PICOPASS_BLOCK_LEN);
+            picopass->dev->dev_data.pacs.elite_kdf = false;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWriteiCE) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneKeyMenu, SubmenuIndexWriteiCE);
+            memcpy(picopass->dev->dev_data.pacs.key, picopass_xice_key, RFAL_PICOPASS_BLOCK_LEN);
+            picopass->dev->dev_data.pacs.elite_kdf = true;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWriteiCL) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneKeyMenu, SubmenuIndexWriteiCL);
+            memcpy(picopass->dev->dev_data.pacs.key, picopass_xicl_key, RFAL_PICOPASS_BLOCK_LEN);
+            picopass->dev->dev_data.pacs.elite_kdf = false;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWriteiCS) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneKeyMenu, SubmenuIndexWriteiCS);
+            memcpy(picopass->dev->dev_data.pacs.key, picopass_xics_key, RFAL_PICOPASS_BLOCK_LEN);
+            picopass->dev->dev_data.pacs.elite_kdf = false;
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWriteCustom) {
+            // If user dictionary, prepopulate with the first key
+            if(iclass_elite_dict_check_presence(IclassEliteDictTypeUser)) {
+                IclassEliteDict* dict = iclass_elite_dict_alloc(IclassEliteDictTypeUser);
+                iclass_elite_dict_get_next_key(dict, picopass->byte_input_store);
+                iclass_elite_dict_free(dict);
+            }
+
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneKeyMenu, SubmenuIndexWriteCustom);
+            // Key and elite_kdf = true are both set in key_input scene after the value is input
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneKeyInput);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_search_and_switch_to_previous_scene(
+            picopass->scene_manager, PicopassSceneStart);
+    }
+
+    return consumed;
+}
+
+void picopass_scene_key_menu_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    submenu_reset(picopass->submenu);
+}

+ 88 - 0
picopass/scenes/picopass_scene_loclass.c

@@ -0,0 +1,88 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_loclass_worker_callback(PicopassWorkerEvent event, void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, event);
+}
+
+void picopass_loclass_result_callback(void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
+}
+
+void picopass_scene_loclass_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcEmulate);
+
+    scene_manager_set_scene_state(picopass->scene_manager, PicopassSceneLoclass, 0);
+
+    loclass_set_callback(picopass->loclass, picopass_loclass_result_callback, picopass);
+
+    // Start worker
+    picopass_worker_start(
+        picopass->worker,
+        PicopassWorkerStateLoclass,
+        &picopass->dev->dev_data,
+        picopass_loclass_worker_callback,
+        picopass);
+
+    picopass_blink_emulate_start(picopass);
+
+    loclass_set_header(picopass->loclass, "Loclass");
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoclass);
+}
+
+bool picopass_scene_loclass_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassWorkerEventLoclassGotMac) {
+            uint32_t loclass_macs_collected =
+                scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneLoclass);
+            loclass_macs_collected++;
+            notification_message(picopass->notifications, &sequence_single_vibro);
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneLoclass, loclass_macs_collected);
+            loclass_set_num_macs(picopass->loclass, loclass_macs_collected);
+            if(loclass_macs_collected >= LOCLASS_MACS_TO_COLLECT) {
+                notification_message(picopass->notifications, &sequence_double_vibro);
+                scene_manager_previous_scene(picopass->scene_manager);
+            }
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventLoclassGotStandardKey) {
+            loclass_set_header(picopass->loclass, "Loclass (Got Std Key)");
+            notification_message(picopass->notifications, &sequence_error);
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventLoclassFileError) {
+            scene_manager_set_scene_state(picopass->scene_manager, PicopassSceneLoclass, 255);
+            loclass_set_num_macs(picopass->loclass, 255);
+            loclass_set_header(picopass->loclass, "Error Opening Log File");
+            picopass_blink_stop(picopass);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventViewExit) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(picopass->scene_manager);
+    }
+    return consumed;
+}
+
+void picopass_scene_loclass_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    picopass_blink_stop(picopass);
+
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+
+    loclass_reset(picopass->loclass);
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 61 - 0
picopass/scenes/picopass_scene_read_card.c

@@ -0,0 +1,61 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include "../picopass_keys.h"
+
+void picopass_read_card_worker_callback(PicopassWorkerEvent event, void* context) {
+    UNUSED(event);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventWorkerExit);
+}
+
+void picopass_scene_read_card_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_header(popup, "Detecting\npicopass\ncard", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    // Start worker
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+    picopass_worker_start(
+        picopass->worker,
+        PicopassWorkerStateDetect,
+        &picopass->dev->dev_data,
+        picopass_read_card_worker_callback,
+        picopass);
+
+    picopass_blink_start(picopass);
+}
+
+bool picopass_scene_read_card_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventWorkerExit) {
+            if(memcmp(
+                   picopass->dev->dev_data.pacs.key,
+                   picopass_factory_debit_key,
+                   RFAL_PICOPASS_BLOCK_LEN) == 0) {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadFactorySuccess);
+            } else {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess);
+            }
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_read_card_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+    // Clear view
+    popup_reset(picopass->popup);
+
+    picopass_blink_stop(picopass);
+}

+ 178 - 0
picopass/scenes/picopass_scene_read_card_success.c

@@ -0,0 +1,178 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_read_card_success_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_read_card_success_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    FuriString* csn_str = furi_string_alloc_set("CSN:");
+    FuriString* credential_str = furi_string_alloc();
+    FuriString* wiegand_str = furi_string_alloc();
+    FuriString* key_str = furi_string_alloc();
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    // Send notification
+    notification_message(picopass->notifications, &sequence_success);
+
+    // Setup view
+    PicopassBlock* AA1 = picopass->dev->dev_data.AA1;
+    PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
+    Widget* widget = picopass->widget;
+
+    uint8_t csn[RFAL_PICOPASS_BLOCK_LEN] = {0};
+    memcpy(csn, AA1[PICOPASS_CSN_BLOCK_INDEX].data, RFAL_PICOPASS_BLOCK_LEN);
+    for(uint8_t i = 0; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+        furi_string_cat_printf(csn_str, "%02X", csn[i]);
+    }
+
+    // We can't test the pacs->key in case it is intentionally all 0's and we can't test the key block since it is populated with the diversified key before each key test, so we approximate with the PACS config block being blank.
+    bool no_key = picopass_is_memset(
+        AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0x00, RFAL_PICOPASS_BLOCK_LEN);
+    bool empty = picopass_is_memset(
+        AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0xFF, RFAL_PICOPASS_BLOCK_LEN);
+
+    if(no_key) {
+        furi_string_cat_printf(wiegand_str, "Read Failed");
+
+        if(pacs->se_enabled) {
+            furi_string_cat_printf(credential_str, "SE enabled");
+        }
+
+        widget_add_button_element(
+            widget,
+            GuiButtonTypeCenter,
+            "Menu",
+            picopass_scene_read_card_success_widget_callback,
+            picopass);
+    } else if(empty) {
+        furi_string_cat_printf(wiegand_str, "Empty");
+        widget_add_button_element(
+            widget,
+            GuiButtonTypeCenter,
+            "Menu",
+            picopass_scene_read_card_success_widget_callback,
+            picopass);
+    } else if(pacs->bitLength == 0 || pacs->bitLength == 255) {
+        // Neither of these are valid.  Indicates the block was all 0x00 or all 0xff
+        furi_string_cat_printf(wiegand_str, "Invalid PACS");
+
+        if(pacs->se_enabled) {
+            furi_string_cat_printf(credential_str, "SE enabled");
+        }
+        widget_add_button_element(
+            widget,
+            GuiButtonTypeCenter,
+            "Menu",
+            picopass_scene_read_card_success_widget_callback,
+            picopass);
+    } else {
+        size_t bytesLength = 1 + pacs->bitLength / 8;
+        furi_string_set(credential_str, "");
+        for(uint8_t i = RFAL_PICOPASS_BLOCK_LEN - bytesLength; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+            furi_string_cat_printf(credential_str, "%02X", pacs->credential[i]);
+        }
+        furi_string_cat_printf(wiegand_str, "%d bits", pacs->bitLength);
+
+        if(pacs->sio) {
+            furi_string_cat_printf(credential_str, " +SIO");
+        }
+
+        if(pacs->key) {
+            furi_string_cat_printf(key_str, "Key: ");
+            uint8_t key[RFAL_PICOPASS_BLOCK_LEN];
+            memcpy(key, &pacs->key, RFAL_PICOPASS_BLOCK_LEN);
+
+            bool standard_key = true;
+            // Handle DES key being 56bits with parity in LSB
+            for(uint8_t i = 0; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+                if((key[i] & 0xFE) != (picopass_iclass_key[i] & 0xFE)) {
+                    standard_key = false;
+                    break;
+                }
+            }
+
+            if(standard_key) {
+                furi_string_cat_printf(key_str, "Standard");
+            } else {
+                for(uint8_t i = 0; i < RFAL_PICOPASS_BLOCK_LEN; i++) {
+                    furi_string_cat_printf(key_str, "%02X", key[i]);
+                }
+            }
+        }
+
+        widget_add_button_element(
+            widget,
+            GuiButtonTypeRight,
+            "More",
+            picopass_scene_read_card_success_widget_callback,
+            picopass);
+    }
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeLeft,
+        "Retry",
+        picopass_scene_read_card_success_widget_callback,
+        picopass);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(csn_str));
+    widget_add_string_element(
+        widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(wiegand_str));
+    widget_add_string_element(
+        widget,
+        64,
+        36,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(credential_str));
+    widget_add_string_element(
+        widget, 64, 46, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(key_str));
+
+    furi_string_free(csn_str);
+    furi_string_free(credential_str);
+    furi_string_free(wiegand_str);
+    furi_string_free(key_str);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_read_card_success_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            // Clear device name
+            picopass_device_set_name(picopass->dev, "");
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneCardMenu);
+            consumed = true;
+        } else if(event.event == GuiButtonTypeCenter) {
+            consumed = scene_manager_search_and_switch_to_another_scene(
+                picopass->scene_manager, PicopassSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_read_card_success_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 80 - 0
picopass/scenes/picopass_scene_read_factory_success.c

@@ -0,0 +1,80 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include "../picopass_keys.h"
+
+void picopass_scene_read_factory_success_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_read_factory_success_on_enter(void* context) {
+    Picopass* picopass = context;
+    FuriString* title = furi_string_alloc_set("Factory Default");
+    FuriString* subtitle = furi_string_alloc_set("");
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    // Send notification
+    notification_message(picopass->notifications, &sequence_success);
+
+    // Setup view
+    Widget* widget = picopass->widget;
+    //PicopassPacs* pacs = &picopass->dev->dev_data.pacs;
+    PicopassBlock* AA1 = picopass->dev->dev_data.AA1;
+
+    uint8_t* configBlock = AA1[PICOPASS_CONFIG_BLOCK_INDEX].data;
+    uint8_t fuses = configBlock[7];
+
+    if((fuses & 0x80) == 0x80) {
+        furi_string_cat_printf(subtitle, "Personalization mode");
+    } else {
+        furi_string_cat_printf(subtitle, "Application mode");
+    }
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeCenter,
+        "Write Standard iClass Key",
+        picopass_scene_read_factory_success_widget_callback,
+        picopass);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(title));
+    widget_add_string_element(
+        widget, 64, 20, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(subtitle));
+
+    furi_string_free(title);
+    furi_string_free(subtitle);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_read_factory_success_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == GuiButtonTypeCenter) {
+            memcpy(picopass->dev->dev_data.pacs.key, picopass_iclass_key, RFAL_PICOPASS_BLOCK_LEN);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteKey);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_read_factory_success_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 83 - 0
picopass/scenes/picopass_scene_save_name.c

@@ -0,0 +1,83 @@
+#include "../picopass_i.h"
+#include <lib/toolbox/name_generator.h>
+#include <gui/modules/validators.h>
+#include <toolbox/path.h>
+
+void picopass_scene_save_name_text_input_callback(void* context) {
+    Picopass* picopass = context;
+
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventTextInputDone);
+}
+
+void picopass_scene_save_name_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    // Setup view
+    TextInput* text_input = picopass->text_input;
+    bool dev_name_empty = false;
+    if(!strcmp(picopass->dev->dev_name, "")) {
+        name_generator_make_auto(
+            picopass->text_store, sizeof(picopass->text_store), PICOPASS_APP_FILE_PREFIX);
+        dev_name_empty = true;
+    } else {
+        picopass_text_store_set(picopass, picopass->dev->dev_name);
+    }
+    text_input_set_header_text(text_input, "Name the card");
+    text_input_set_result_callback(
+        text_input,
+        picopass_scene_save_name_text_input_callback,
+        picopass,
+        picopass->text_store,
+        PICOPASS_DEV_NAME_MAX_LEN,
+        dev_name_empty);
+
+    FuriString* folder_path;
+    folder_path = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    if(furi_string_end_with(picopass->dev->load_path, PICOPASS_APP_EXTENSION)) {
+        path_extract_dirname(furi_string_get_cstr(picopass->dev->load_path), folder_path);
+    }
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(folder_path), PICOPASS_APP_EXTENSION, picopass->dev->dev_name);
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewTextInput);
+
+    furi_string_free(folder_path);
+}
+
+bool picopass_scene_save_name_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventTextInputDone) {
+            // Delete old file if renaming
+            if(strcmp(picopass->dev->dev_name, "") != 0) {
+                picopass_device_delete(picopass->dev, true);
+            }
+            strlcpy(
+                picopass->dev->dev_name, picopass->text_store, strlen(picopass->text_store) + 1);
+            if(picopass_device_save(picopass->dev, picopass->text_store)) {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveSuccess);
+                consumed = true;
+            } else {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    picopass->scene_manager, PicopassSceneStart);
+            }
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_save_name_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    void* validator_context = text_input_get_validator_callback_context(picopass->text_input);
+    text_input_set_validator(picopass->text_input, NULL, NULL);
+    validator_is_file_free(validator_context);
+
+    text_input_reset(picopass->text_input);
+}

+ 47 - 0
picopass/scenes/picopass_scene_save_success.c

@@ -0,0 +1,47 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_save_success_popup_callback(void* context) {
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventViewExit);
+}
+
+void picopass_scene_save_success_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_icon(popup, 32, 5, &I_DolphinNice_96x59);
+    popup_set_header(popup, "Saved!", 13, 22, AlignLeft, AlignBottom);
+    popup_set_timeout(popup, 1500);
+    popup_set_context(popup, picopass);
+    popup_set_callback(popup, picopass_scene_save_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+}
+
+bool picopass_scene_save_success_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventViewExit) {
+            if(scene_manager_has_previous_scene(picopass->scene_manager, PicopassSceneCardMenu)) {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    picopass->scene_manager, PicopassSceneCardMenu);
+            } else {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    picopass->scene_manager, PicopassSceneStart);
+            }
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_save_success_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    popup_reset(picopass->popup);
+}

+ 84 - 0
picopass/scenes/picopass_scene_saved_menu.c

@@ -0,0 +1,84 @@
+#include "../picopass_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexInfo,
+    SubmenuIndexWrite,
+    SubmenuIndexEmulate,
+    SubmenuIndexRename,
+    SubmenuIndexDelete,
+};
+
+void picopass_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
+    Picopass* picopass = context;
+
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
+}
+
+void picopass_scene_saved_menu_on_enter(void* context) {
+    Picopass* picopass = context;
+    Submenu* submenu = picopass->submenu;
+
+    submenu_add_item(
+        submenu, "Info", SubmenuIndexInfo, picopass_scene_saved_menu_submenu_callback, picopass);
+    submenu_add_item(
+        submenu, "Write", SubmenuIndexWrite, picopass_scene_saved_menu_submenu_callback, picopass);
+    submenu_add_item(
+        submenu,
+        "Emulate",
+        SubmenuIndexEmulate,
+        picopass_scene_saved_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Rename",
+        SubmenuIndexRename,
+        picopass_scene_saved_menu_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu,
+        "Delete",
+        SubmenuIndexDelete,
+        picopass_scene_saved_menu_submenu_callback,
+        picopass);
+
+    submenu_set_selected_item(
+        picopass->submenu,
+        scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneSavedMenu));
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
+}
+
+bool picopass_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(
+            picopass->scene_manager, PicopassSceneSavedMenu, event.event);
+
+        if(event.event == SubmenuIndexDelete) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneDelete);
+            consumed = true;
+        } else if(event.event == SubmenuIndexInfo) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneDeviceInfo);
+            consumed = true;
+        } else if(event.event == SubmenuIndexWrite) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCard);
+            consumed = true;
+        } else if(event.event == SubmenuIndexEmulate) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneEmulate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexRename) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneSaveName);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void picopass_scene_saved_menu_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    submenu_reset(picopass->submenu);
+}

+ 73 - 0
picopass/scenes/picopass_scene_start.c

@@ -0,0 +1,73 @@
+#include "../picopass_i.h"
+enum SubmenuIndex {
+    SubmenuIndexRead,
+    SubmenuIndexEliteDictAttack,
+    SubmenuIndexSaved,
+    SubmenuIndexLoclass,
+};
+
+void picopass_scene_start_submenu_callback(void* context, uint32_t index) {
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, index);
+}
+void picopass_scene_start_on_enter(void* context) {
+    Picopass* picopass = context;
+
+    Submenu* submenu = picopass->submenu;
+    submenu_add_item(
+        submenu, "Read Card", SubmenuIndexRead, picopass_scene_start_submenu_callback, picopass);
+    submenu_add_item(
+        submenu,
+        "Elite Dict. Attack",
+        SubmenuIndexEliteDictAttack,
+        picopass_scene_start_submenu_callback,
+        picopass);
+    submenu_add_item(
+        submenu, "Saved", SubmenuIndexSaved, picopass_scene_start_submenu_callback, picopass);
+
+    submenu_add_item(
+        submenu, "Loclass", SubmenuIndexLoclass, picopass_scene_start_submenu_callback, picopass);
+
+    submenu_set_selected_item(
+        submenu, scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneStart));
+    picopass_device_clear(picopass->dev);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewMenu);
+}
+
+bool picopass_scene_start_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexRead) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneStart, SubmenuIndexRead);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCard);
+            consumed = true;
+        } else if(event.event == SubmenuIndexSaved) {
+            // Explicitly save state so that the correct item is
+            // reselected if the user cancels loading a file.
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneStart, SubmenuIndexSaved);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexEliteDictAttack) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneStart, SubmenuIndexEliteDictAttack);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneEliteDictAttack);
+            consumed = true;
+        } else if(event.event == SubmenuIndexLoclass) {
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneStart, PicopassSceneLoclass);
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneLoclass);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void picopass_scene_start_on_exit(void* context) {
+    Picopass* picopass = context;
+    submenu_reset(picopass->submenu);
+}

+ 56 - 0
picopass/scenes/picopass_scene_write_card.c

@@ -0,0 +1,56 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_write_card_worker_callback(PicopassWorkerEvent event, void* context) {
+    UNUSED(event);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, event);
+}
+
+void picopass_scene_write_card_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_header(popup, "Writing\npicopass\ncard", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
+
+    // Start worker
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+    picopass_worker_start(
+        picopass->worker,
+        PicopassWorkerStateWrite,
+        &picopass->dev->dev_data,
+        picopass_write_card_worker_callback,
+        picopass);
+
+    picopass_blink_start(picopass);
+}
+
+bool picopass_scene_write_card_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassWorkerEventFail) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardFailure);
+            consumed = true;
+        } else if(event.event == PicopassWorkerEventSuccess) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardSuccess);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_write_card_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+    // Clear view
+    popup_reset(picopass->popup);
+
+    picopass_blink_stop(picopass);
+}

+ 65 - 0
picopass/scenes/picopass_scene_write_card_failure.c

@@ -0,0 +1,65 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_write_card_failure_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_write_card_failure_on_enter(void* context) {
+    Picopass* picopass = context;
+    Widget* widget = picopass->widget;
+    FuriString* str = furi_string_alloc_set("Write Failure!");
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeLeft,
+        "Retry",
+        picopass_scene_write_card_failure_widget_callback,
+        picopass);
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeRight,
+        "Menu",
+        picopass_scene_write_card_failure_widget_callback,
+        picopass);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(str));
+
+    furi_string_free(str);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_write_card_failure_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            // Clear device name
+            picopass_device_set_name(picopass->dev, "");
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                picopass->scene_manager, PicopassSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_write_card_failure_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 70 - 0
picopass/scenes/picopass_scene_write_card_success.c

@@ -0,0 +1,70 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_scene_write_card_success_widget_callback(
+    GuiButtonType result,
+    InputType type,
+    void* context) {
+    furi_assert(context);
+    Picopass* picopass = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(picopass->view_dispatcher, result);
+    }
+}
+
+void picopass_scene_write_card_success_on_enter(void* context) {
+    Picopass* picopass = context;
+    Widget* widget = picopass->widget;
+    FuriString* str = furi_string_alloc_set("Write Success!");
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    // Send notification
+    notification_message(picopass->notifications, &sequence_success);
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeLeft,
+        "Retry",
+        picopass_scene_write_card_success_widget_callback,
+        picopass);
+
+    widget_add_button_element(
+        widget,
+        GuiButtonTypeRight,
+        "Menu",
+        picopass_scene_write_card_success_widget_callback,
+        picopass);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(str));
+
+    furi_string_free(str);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+}
+
+bool picopass_scene_write_card_success_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            // Clear device name
+            picopass_device_set_name(picopass->dev, "");
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                picopass->scene_manager, PicopassSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_write_card_success_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Clear view
+    widget_reset(picopass->widget);
+}

+ 53 - 0
picopass/scenes/picopass_scene_write_key.c

@@ -0,0 +1,53 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+void picopass_write_key_worker_callback(PicopassWorkerEvent event, void* context) {
+    UNUSED(event);
+    Picopass* picopass = context;
+    view_dispatcher_send_custom_event(picopass->view_dispatcher, PicopassCustomEventWorkerExit);
+}
+
+void picopass_scene_write_key_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = picopass->popup;
+    popup_set_header(popup, "Writing\niClass\nkey", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
+
+    // Start worker
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+    picopass_worker_start(
+        picopass->worker,
+        PicopassWorkerStateWriteKey,
+        &picopass->dev->dev_data,
+        picopass_write_key_worker_callback,
+        picopass);
+
+    picopass_blink_start(picopass);
+}
+
+bool picopass_scene_write_key_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventWorkerExit) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardSuccess);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_write_key_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Stop worker
+    picopass_worker_stop(picopass->worker);
+    // Clear view
+    popup_reset(picopass->popup);
+
+    picopass_blink_stop(picopass);
+}

+ 281 - 0
picopass/views/dict_attack.c

@@ -0,0 +1,281 @@
+#include "dict_attack.h"
+
+#include <gui/elements.h>
+
+typedef enum {
+    DictAttackStateRead,
+    DictAttackStateCardRemoved,
+} DictAttackState;
+
+struct DictAttack {
+    View* view;
+    DictAttackCallback callback;
+    void* context;
+};
+
+typedef struct {
+    DictAttackState state;
+    MfClassicType type;
+    FuriString* header;
+    uint8_t sectors_total;
+    uint8_t sectors_read;
+    uint8_t sector_current;
+    uint8_t keys_total;
+    uint8_t keys_found;
+    uint16_t dict_keys_total;
+    uint16_t dict_keys_current;
+    bool is_key_attack;
+    uint8_t key_attack_current_sector;
+} DictAttackViewModel;
+
+static void dict_attack_draw_callback(Canvas* canvas, void* model) {
+    DictAttackViewModel* m = model;
+    if(m->state == DictAttackStateCardRemoved) {
+        canvas_set_font(canvas, FontPrimary);
+        canvas_draw_str_aligned(canvas, 64, 4, AlignCenter, AlignTop, "Lost the tag!");
+        canvas_set_font(canvas, FontSecondary);
+        elements_multiline_text_aligned(
+            canvas, 64, 23, AlignCenter, AlignTop, "Make sure the tag is\npositioned correctly.");
+    } else if(m->state == DictAttackStateRead) {
+        char draw_str[32] = {};
+        canvas_set_font(canvas, FontSecondary);
+        canvas_draw_str_aligned(
+            canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(m->header));
+        if(m->is_key_attack) {
+            snprintf(
+                draw_str,
+                sizeof(draw_str),
+                "Reuse key check for sector: %d",
+                m->key_attack_current_sector);
+        } else {
+            snprintf(draw_str, sizeof(draw_str), "Unlocking sector: %d", m->sector_current);
+        }
+        canvas_draw_str_aligned(canvas, 0, 10, AlignLeft, AlignTop, draw_str);
+        float dict_progress = m->dict_keys_total == 0 ?
+                                  0 :
+                                  (float)(m->dict_keys_current) / (float)(m->dict_keys_total);
+        float progress = m->sectors_total == 0 ? 0 :
+                                                 ((float)(m->sector_current) + dict_progress) /
+                                                     (float)(m->sectors_total);
+        if(progress > 1.0) {
+            progress = 1.0;
+        }
+        if(m->dict_keys_current == 0) {
+            // Cause when people see 0 they think it's broken
+            snprintf(draw_str, sizeof(draw_str), "%d/%d", 1, m->dict_keys_total);
+        } else {
+            snprintf(
+                draw_str, sizeof(draw_str), "%d/%d", m->dict_keys_current, m->dict_keys_total);
+        }
+        elements_progress_bar_with_text(canvas, 0, 20, 128, dict_progress, draw_str);
+        canvas_set_font(canvas, FontSecondary);
+        snprintf(draw_str, sizeof(draw_str), "Keys found: %d/%d", m->keys_found, m->keys_total);
+        canvas_draw_str_aligned(canvas, 0, 33, AlignLeft, AlignTop, draw_str);
+        snprintf(
+            draw_str, sizeof(draw_str), "Sectors Read: %d/%d", m->sectors_read, m->sectors_total);
+        canvas_draw_str_aligned(canvas, 0, 43, AlignLeft, AlignTop, draw_str);
+    }
+    elements_button_center(canvas, "Skip");
+}
+
+static bool dict_attack_input_callback(InputEvent* event, void* context) {
+    DictAttack* dict_attack = context;
+    bool consumed = false;
+    if(event->type == InputTypeShort && event->key == InputKeyOk) {
+        if(dict_attack->callback) {
+            dict_attack->callback(dict_attack->context);
+        }
+        consumed = true;
+    }
+    return consumed;
+}
+
+DictAttack* dict_attack_alloc() {
+    DictAttack* dict_attack = malloc(sizeof(DictAttack));
+    dict_attack->view = view_alloc();
+    view_allocate_model(dict_attack->view, ViewModelTypeLocking, sizeof(DictAttackViewModel));
+    view_set_draw_callback(dict_attack->view, dict_attack_draw_callback);
+    view_set_input_callback(dict_attack->view, dict_attack_input_callback);
+    view_set_context(dict_attack->view, dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { model->header = furi_string_alloc(); },
+        false);
+    return dict_attack;
+}
+
+void dict_attack_free(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { furi_string_free(model->header); },
+        false);
+    view_free(dict_attack->view);
+    free(dict_attack);
+}
+
+void dict_attack_reset(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            model->state = DictAttackStateRead;
+            model->type = MfClassicType1k;
+            model->sectors_total = 1;
+            model->sectors_read = 0;
+            model->sector_current = 0;
+            model->keys_total = 0;
+            model->keys_found = 0;
+            model->dict_keys_total = 0;
+            model->dict_keys_current = 0;
+            model->is_key_attack = false;
+            furi_string_reset(model->header);
+        },
+        false);
+}
+
+View* dict_attack_get_view(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    return dict_attack->view;
+}
+
+void dict_attack_set_callback(DictAttack* dict_attack, DictAttackCallback callback, void* context) {
+    furi_assert(dict_attack);
+    furi_assert(callback);
+    dict_attack->callback = callback;
+    dict_attack->context = context;
+}
+
+void dict_attack_set_header(DictAttack* dict_attack, const char* header) {
+    furi_assert(dict_attack);
+    furi_assert(header);
+
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { furi_string_set(model->header, header); },
+        true);
+}
+
+void dict_attack_set_card_detected(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            model->state = DictAttackStateRead;
+            model->sectors_total = 1;
+            model->keys_total = model->sectors_total;
+        },
+        true);
+}
+
+void dict_attack_set_card_removed(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { model->state = DictAttackStateCardRemoved; },
+        true);
+}
+
+void dict_attack_set_sector_read(DictAttack* dict_attack, uint8_t sec_read) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view, DictAttackViewModel * model, { model->sectors_read = sec_read; }, true);
+}
+
+void dict_attack_set_keys_found(DictAttack* dict_attack, uint8_t keys_found) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view, DictAttackViewModel * model, { model->keys_found = keys_found; }, true);
+}
+
+void dict_attack_set_current_sector(DictAttack* dict_attack, uint8_t curr_sec) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            model->sector_current = curr_sec;
+            model->dict_keys_current = 0;
+        },
+        true);
+}
+
+void dict_attack_inc_current_sector(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            if(model->sector_current < model->sectors_total) {
+                model->sector_current++;
+                model->dict_keys_current = 0;
+            }
+        },
+        true);
+}
+
+void dict_attack_inc_keys_found(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            if(model->keys_found < model->keys_total) {
+                model->keys_found++;
+            }
+        },
+        true);
+}
+
+void dict_attack_set_total_dict_keys(DictAttack* dict_attack, uint16_t dict_keys_total) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { model->dict_keys_total = dict_keys_total; },
+        true);
+}
+
+void dict_attack_inc_current_dict_key(DictAttack* dict_attack, uint16_t keys_tried) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            if(model->dict_keys_current + keys_tried < model->dict_keys_total) {
+                model->dict_keys_current += keys_tried;
+            }
+        },
+        true);
+}
+
+void dict_attack_set_key_attack(DictAttack* dict_attack, bool is_key_attack, uint8_t sector) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            model->is_key_attack = is_key_attack;
+            model->key_attack_current_sector = sector;
+        },
+        true);
+}
+
+void dict_attack_inc_key_attack_current_sector(DictAttack* dict_attack) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        {
+            if(model->key_attack_current_sector < model->sectors_total) {
+                model->key_attack_current_sector++;
+            }
+        },
+        true);
+}

+ 44 - 0
picopass/views/dict_attack.h

@@ -0,0 +1,44 @@
+#pragma once
+#include <stdint.h>
+#include <gui/view.h>
+#include <gui/modules/widget.h>
+
+#include <lib/nfc/protocols/mifare_classic.h>
+
+typedef struct DictAttack DictAttack;
+
+typedef void (*DictAttackCallback)(void* context);
+
+DictAttack* dict_attack_alloc();
+
+void dict_attack_free(DictAttack* dict_attack);
+
+void dict_attack_reset(DictAttack* dict_attack);
+
+View* dict_attack_get_view(DictAttack* dict_attack);
+
+void dict_attack_set_callback(DictAttack* dict_attack, DictAttackCallback callback, void* context);
+
+void dict_attack_set_header(DictAttack* dict_attack, const char* header);
+
+void dict_attack_set_card_detected(DictAttack* dict_attack);
+
+void dict_attack_set_card_removed(DictAttack* dict_attack);
+
+void dict_attack_set_sector_read(DictAttack* dict_attack, uint8_t sec_read);
+
+void dict_attack_set_keys_found(DictAttack* dict_attack, uint8_t keys_found);
+
+void dict_attack_set_current_sector(DictAttack* dict_attack, uint8_t curr_sec);
+
+void dict_attack_inc_current_sector(DictAttack* dict_attack);
+
+void dict_attack_inc_keys_found(DictAttack* dict_attack);
+
+void dict_attack_set_total_dict_keys(DictAttack* dict_attack, uint16_t dict_keys_total);
+
+void dict_attack_inc_current_dict_key(DictAttack* dict_attack, uint16_t keys_tried);
+
+void dict_attack_set_key_attack(DictAttack* dict_attack, bool is_key_attack, uint8_t sector);
+
+void dict_attack_inc_key_attack_current_sector(DictAttack* dict_attack);

+ 110 - 0
picopass/views/loclass.c

@@ -0,0 +1,110 @@
+#include "loclass.h"
+#include "../picopass_worker_i.h"
+
+#include <gui/elements.h>
+
+struct Loclass {
+    View* view;
+    LoclassCallback callback;
+    void* context;
+};
+
+typedef struct {
+    FuriString* header;
+    uint8_t num_macs;
+} LoclassViewModel;
+
+static void loclass_draw_callback(Canvas* canvas, void* model) {
+    LoclassViewModel* m = model;
+
+    char draw_str[32] = {};
+    canvas_set_font(canvas, FontSecondary);
+    canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, furi_string_get_cstr(m->header));
+
+    if(m->num_macs == 255) {
+        return;
+    }
+
+    float progress = m->num_macs == 0 ? 0 :
+                                        (float)(m->num_macs) / (float)(LOCLASS_MACS_TO_COLLECT);
+
+    if(progress > 1.0) {
+        progress = 1.0;
+    }
+
+    snprintf(draw_str, sizeof(draw_str), "%d/%d", m->num_macs, LOCLASS_MACS_TO_COLLECT);
+
+    elements_progress_bar_with_text(canvas, 0, 20, 128, progress, draw_str);
+
+    elements_button_center(canvas, "Skip");
+}
+
+static bool loclass_input_callback(InputEvent* event, void* context) {
+    Loclass* loclass = context;
+    bool consumed = false;
+    if(event->type == InputTypeShort && event->key == InputKeyOk) {
+        if(loclass->callback) {
+            loclass->callback(loclass->context);
+        }
+        consumed = true;
+    }
+    return consumed;
+}
+
+Loclass* loclass_alloc() {
+    Loclass* loclass = malloc(sizeof(Loclass));
+    loclass->view = view_alloc();
+    view_allocate_model(loclass->view, ViewModelTypeLocking, sizeof(LoclassViewModel));
+    view_set_draw_callback(loclass->view, loclass_draw_callback);
+    view_set_input_callback(loclass->view, loclass_input_callback);
+    view_set_context(loclass->view, loclass);
+    with_view_model(
+        loclass->view, LoclassViewModel * model, { model->header = furi_string_alloc(); }, false);
+    return loclass;
+}
+
+void loclass_free(Loclass* loclass) {
+    furi_assert(loclass);
+    with_view_model(
+        loclass->view, LoclassViewModel * model, { furi_string_free(model->header); }, false);
+    view_free(loclass->view);
+    free(loclass);
+}
+
+void loclass_reset(Loclass* loclass) {
+    furi_assert(loclass);
+    with_view_model(
+        loclass->view,
+        LoclassViewModel * model,
+        {
+            model->num_macs = 0;
+            furi_string_reset(model->header);
+        },
+        false);
+}
+
+View* loclass_get_view(Loclass* loclass) {
+    furi_assert(loclass);
+    return loclass->view;
+}
+
+void loclass_set_callback(Loclass* loclass, LoclassCallback callback, void* context) {
+    furi_assert(loclass);
+    furi_assert(callback);
+    loclass->callback = callback;
+    loclass->context = context;
+}
+
+void loclass_set_header(Loclass* loclass, const char* header) {
+    furi_assert(loclass);
+    furi_assert(header);
+
+    with_view_model(
+        loclass->view, LoclassViewModel * model, { furi_string_set(model->header, header); }, true);
+}
+
+void loclass_set_num_macs(Loclass* loclass, uint16_t num_macs) {
+    furi_assert(loclass);
+    with_view_model(
+        loclass->view, LoclassViewModel * model, { model->num_macs = num_macs; }, true);
+}

+ 22 - 0
picopass/views/loclass.h

@@ -0,0 +1,22 @@
+#pragma once
+#include <stdint.h>
+#include <gui/view.h>
+#include <gui/modules/widget.h>
+
+typedef struct Loclass Loclass;
+
+typedef void (*LoclassCallback)(void* context);
+
+Loclass* loclass_alloc();
+
+void loclass_free(Loclass* loclass);
+
+void loclass_reset(Loclass* loclass);
+
+View* loclass_get_view(Loclass* loclass);
+
+void loclass_set_callback(Loclass* loclass, LoclassCallback callback, void* context);
+
+void loclass_set_header(Loclass* loclass, const char* header);
+
+void loclass_set_num_macs(Loclass* loclass, uint16_t num_macs);