Переглянути джерело

Add picopass from https://github.com/xMasterX/all-the-plugins

git-subtree-dir: picopass
git-subtree-mainline: ac876749a3a616fc4c47209fe4c08961408bc34f
git-subtree-split: 2d51bb296351ba0d3ca68e5e0edcc6887b0aeb29
Willy-JL 2 роки тому
батько
коміт
1ad06cacbe
67 змінених файлів з 6955 додано та 0 видалено
  1. 1 0
      picopass/.gitsubtree
  2. BIN
      picopass/125_10px.png
  3. 25 0
      picopass/application.fam
  4. 41 0
      picopass/files/iclass_elite_dict.txt
  5. 47 0
      picopass/files/iclass_standard_dict.txt
  6. 164 0
      picopass/helpers/iclass_elite_dict.c
  7. 29 0
      picopass/helpers/iclass_elite_dict.h
  8. BIN
      picopass/icons/DolphinMafia_115x62.png
  9. BIN
      picopass/icons/DolphinNice_96x59.png
  10. BIN
      picopass/icons/Nfc_10px.png
  11. BIN
      picopass/icons/RFIDDolphinReceive_97x61.png
  12. BIN
      picopass/icons/RFIDDolphinSend_97x61.png
  13. 319 0
      picopass/lib/loclass/optimized_cipher.c
  14. 117 0
      picopass/lib/loclass/optimized_cipher.h
  15. 136 0
      picopass/lib/loclass/optimized_cipherutils.c
  16. 64 0
      picopass/lib/loclass/optimized_cipherutils.h
  17. 232 0
      picopass/lib/loclass/optimized_elite.c
  18. 58 0
      picopass/lib/loclass/optimized_elite.h
  19. 320 0
      picopass/lib/loclass/optimized_ikeys.c
  20. 66 0
      picopass/lib/loclass/optimized_ikeys.h
  21. 102 0
      picopass/loclass_writer.c
  22. 20 0
      picopass/loclass_writer.h
  23. 239 0
      picopass/picopass.c
  24. 3 0
      picopass/picopass.h
  25. 409 0
      picopass/picopass_device.c
  26. 151 0
      picopass/picopass_device.h
  27. 146 0
      picopass/picopass_i.h
  28. 8 0
      picopass/picopass_keys.c
  29. 10 0
      picopass/picopass_keys.h
  30. 667 0
      picopass/protocol/picopass_listener.c
  31. 45 0
      picopass/protocol/picopass_listener.h
  32. 52 0
      picopass/protocol/picopass_listener_i.c
  33. 47 0
      picopass/protocol/picopass_listener_i.h
  34. 543 0
      picopass/protocol/picopass_poller.c
  35. 80 0
      picopass/protocol/picopass_poller.h
  36. 217 0
      picopass/protocol/picopass_poller_i.c
  37. 85 0
      picopass/protocol/picopass_poller_i.h
  38. 48 0
      picopass/protocol/picopass_protocol.h
  39. 64 0
      picopass/rfal_picopass.h
  40. 30 0
      picopass/scenes/picopass_scene.c
  41. 29 0
      picopass/scenes/picopass_scene.h
  42. 92 0
      picopass/scenes/picopass_scene_card_menu.c
  43. 21 0
      picopass/scenes/picopass_scene_config.h
  44. 58 0
      picopass/scenes/picopass_scene_delete.c
  45. 40 0
      picopass/scenes/picopass_scene_delete_success.c
  46. 104 0
      picopass/scenes/picopass_scene_device_info.c
  47. 238 0
      picopass/scenes/picopass_scene_elite_dict_attack.c
  48. 51 0
      picopass/scenes/picopass_scene_emulate.c
  49. 25 0
      picopass/scenes/picopass_scene_file_select.c
  50. 48 0
      picopass/scenes/picopass_scene_key_input.c
  51. 120 0
      picopass/scenes/picopass_scene_key_menu.c
  52. 103 0
      picopass/scenes/picopass_scene_loclass.c
  53. 126 0
      picopass/scenes/picopass_scene_read_card.c
  54. 182 0
      picopass/scenes/picopass_scene_read_card_success.c
  55. 82 0
      picopass/scenes/picopass_scene_read_factory_success.c
  56. 83 0
      picopass/scenes/picopass_scene_save_name.c
  57. 47 0
      picopass/scenes/picopass_scene_save_success.c
  58. 84 0
      picopass/scenes/picopass_scene_saved_menu.c
  59. 73 0
      picopass/scenes/picopass_scene_start.c
  60. 88 0
      picopass/scenes/picopass_scene_write_card.c
  61. 65 0
      picopass/scenes/picopass_scene_write_card_failure.c
  62. 70 0
      picopass/scenes/picopass_scene_write_card_success.c
  63. 76 0
      picopass/scenes/picopass_scene_write_key.c
  64. 288 0
      picopass/views/dict_attack.c
  65. 44 0
      picopass/views/dict_attack.h
  66. 111 0
      picopass/views/loclass.c
  67. 22 0
      picopass/views/loclass.h

+ 1 - 0
picopass/.gitsubtree

@@ -0,0 +1 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/picopass

BIN
picopass/125_10px.png


+ 25 - 0
picopass/application.fam

@@ -0,0 +1,25 @@
+App(
+    appid="picopass",
+    name="[iClass] 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.7",
+    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]);

+ 239 - 0
picopass/picopass.c

@@ -0,0 +1,239 @@
+#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->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->nfc = nfc_alloc();
+
+    // 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;
+
+    nfc_free(picopass->nfc);
+
+    // 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);
+
+    // 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;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+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;

+ 409 - 0
picopass/picopass_device.c

@@ -0,0 +1,409 @@
+#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, 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, 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, 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, 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, PICOPASS_BLOCK_LEN)) {
+                block_read = false;
+                break;
+            }
+        }
+        if(!block_read) break;
+
+        picopass_device_parse_credential(AA1, pacs);
+        picopass_device_parse_wiegand(pacs->credential, pacs);
+
+        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;
+}
+
+void 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);
+}
+
+void picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs) {
+    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");
+        picopass_device_decrypt(AA1[7].data, pacs->credential);
+
+        picopass_device_decrypt(AA1[8].data, pacs->pin0);
+
+        picopass_device_decrypt(AA1[9].data, pacs->pin1);
+    } else if(pacs->encryption == PicopassDeviceEncryptionNone) {
+        FURI_LOG_D(TAG, "No Encryption");
+        memcpy(pacs->credential, AA1[7].data, PICOPASS_BLOCK_LEN);
+        memcpy(pacs->pin0, AA1[8].data, PICOPASS_BLOCK_LEN);
+        memcpy(pacs->pin1, AA1[9].data, 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
+}
+
+void 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);
+}
+
+bool picopass_device_hid_csn(PicopassDevice* dev) {
+    furi_assert(dev);
+    PicopassBlock* AA1 = dev->dev_data.AA1;
+    uint8_t* csn = AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    // From Proxmark3 RRG sourcecode
+    bool isHidRange = (memcmp(csn + 5, "\xFF\x12\xE0", 3) == 0) && ((csn[4] & 0xF0) == 0xF0);
+
+    return isHidRange;
+}

+ 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[PICOPASS_BLOCK_LEN];
+} PicopassBlock;
+
+typedef struct {
+    PicopassBlock AA1[PICOPASS_MAX_APP_LIMIT];
+    PicopassPacs pacs;
+} 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);
+
+void picopass_device_parse_credential(PicopassBlock* AA1, PicopassPacs* pacs);
+void picopass_device_parse_wiegand(uint8_t* credential, PicopassPacs* pacs);
+bool picopass_device_hid_csn(PicopassDevice* dev);

+ 146 - 0
picopass/picopass_i.h

@@ -0,0 +1,146 @@
+#pragma once
+
+#include "picopass.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>
+
+#include <nfc/nfc.h>
+#include <nfc/helpers/nfc_dict.h>
+#include "protocol/picopass_poller.h"
+#include "protocol/picopass_listener.h"
+
+#define PICOPASS_TEXT_STORE_SIZE 128
+
+#define PICOPASS_ICLASS_ELITE_DICT_FLIPPER_NAME APP_ASSETS_PATH("iclass_elite_dict.txt")
+#define PICOPASS_ICLASS_STANDARD_DICT_FLIPPER_NAME APP_ASSETS_PATH("iclass_standard_dict.txt")
+#define PICOPASS_ICLASS_ELITE_DICT_USER_NAME APP_DATA_PATH("assets/iclass_elite_dict_user.txt")
+
+enum PicopassCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    PicopassCustomEventReserved = 100,
+
+    PicopassCustomEventViewExit,
+    PicopassCustomEventWorkerExit,
+    PicopassCustomEventByteInputDone,
+    PicopassCustomEventTextInputDone,
+    PicopassCustomEventDictAttackSkip,
+    PicopassCustomEventDictAttackUpdateView,
+    PicopassCustomEventLoclassGotMac,
+    PicopassCustomEventLoclassGotStandardKey,
+
+    PicopassCustomEventPollerSuccess,
+    PicopassCustomEventPollerFail,
+};
+
+typedef enum {
+    EventTypeTick,
+    EventTypeKey,
+} EventType;
+
+typedef struct {
+    const char* name;
+    uint16_t total_keys;
+    uint16_t current_key;
+    bool card_detected;
+} PicopassDictAttackContext;
+
+typedef struct {
+    uint8_t key_to_write[PICOPASS_BLOCK_LEN];
+    bool is_elite;
+} PicopassWriteKeyContext;
+
+typedef struct {
+    size_t macs_collected;
+} PicopassLoclassContext;
+
+struct Picopass {
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    PicopassDevice* dev;
+
+    Nfc* nfc;
+    PicopassPoller* poller;
+    PicopassListener* listener;
+    NfcDict* dict;
+
+    char text_store[PICOPASS_TEXT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+    uint8_t byte_input_store[PICOPASS_BLOCK_LEN];
+
+    // Common Views
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    ByteInput* byte_input;
+    Widget* widget;
+    DictAttack* dict_attack;
+    Loclass* loclass;
+
+    PicopassDictAttackContext dict_attack_ctx;
+    PicopassWriteKeyContext write_key_context;
+    PicopassLoclassContext loclass_context;
+};
+
+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[PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_factory_credit_key[PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_factory_debit_key[PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xice_key[PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xicl_key[PICOPASS_BLOCK_LEN];
+extern const uint8_t picopass_xics_key[PICOPASS_BLOCK_LEN];

+ 667 - 0
picopass/protocol/picopass_listener.c

@@ -0,0 +1,667 @@
+#include "picopass_listener_i.h"
+#include "picopass_keys.h"
+
+#include <furi/furi.h>
+
+#define PICOPASS_LISTENER_HAS_MASK(x, b) ((x & b) == b)
+
+typedef enum {
+    PicopassListenerCommandProcessed,
+    PicopassListenerCommandSilent,
+    PicopassListenerCommandSendSoF,
+    PicopassListenerCommandStop,
+} PicopassListenerCommand;
+
+typedef PicopassListenerCommand (
+    *PicopassListenerCommandHandler)(PicopassListener* instance, BitBuffer* buf);
+
+typedef struct {
+    uint8_t start_byte_cmd;
+    size_t cmd_len_bits;
+    PicopassListenerCommandHandler handler;
+} PicopassListenerCmd;
+
+// CSNs from Proxmark3 repo
+static const uint8_t loclass_csns[LOCLASS_NUM_CSNS][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_listener_reset(PicopassListener* instance) {
+    instance->state = PicopassListenerStateIdle;
+}
+
+static void picopass_listener_loclass_update_csn(PicopassListener* instance) {
+    // collect LOCLASS_NUM_PER_CSN nonces in a row for each CSN
+    const uint8_t* csn =
+        loclass_csns[(instance->key_block_num / LOCLASS_NUM_PER_CSN) % LOCLASS_NUM_CSNS];
+    memcpy(instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data, csn, sizeof(PicopassBlock));
+
+    uint8_t key[PICOPASS_BLOCK_LEN] = {};
+    loclass_iclass_calc_div_key(csn, picopass_iclass_key, key, false);
+    memcpy(instance->data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data, key, sizeof(PicopassBlock));
+
+    picopass_listener_init_cipher_state_key(instance, key);
+}
+
+PicopassListenerCommand
+    picopass_listener_actall_handler(PicopassListener* instance, BitBuffer* buf) {
+    UNUSED(buf);
+
+    if(instance->state != PicopassListenerStateHalt) {
+        instance->state = PicopassListenerStateActive;
+    }
+    // nfc_set_fdt_listen_fc(instance->nfc, 1000);
+
+    return PicopassListenerCommandSendSoF;
+}
+
+PicopassListenerCommand picopass_listener_act_handler(PicopassListener* instance, BitBuffer* buf) {
+    UNUSED(buf);
+
+    PicopassListenerCommand command = PicopassListenerCommandSendSoF;
+
+    if(instance->state != PicopassListenerStateActive) {
+        command = PicopassListenerCommandSilent;
+    }
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_halt_handler(PicopassListener* instance, BitBuffer* buf) {
+    UNUSED(buf);
+
+    PicopassListenerCommand command = PicopassListenerCommandSendSoF;
+
+    // Technically we should go to StateHalt, but since we can't detect the field dropping we drop to idle instead
+    instance->state = PicopassListenerStateIdle;
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_identify_handler(PicopassListener* instance, BitBuffer* buf) {
+    UNUSED(buf);
+
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if(instance->state != PicopassListenerStateActive) break;
+        picopass_listener_write_anticoll_csn(instance, instance->tx_buffer);
+        PicopassError error = picopass_listener_send_frame(instance, instance->tx_buffer);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_D(TAG, "Error sending CSN: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_select_handler(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if((instance->state == PicopassListenerStateHalt) ||
+           (instance->state == PicopassListenerStateIdle)) {
+            bit_buffer_copy_bytes(
+                instance->tmp_buffer,
+                instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data,
+                sizeof(PicopassBlock));
+        } else {
+            picopass_listener_write_anticoll_csn(instance, instance->tmp_buffer);
+        }
+        const uint8_t* listener_uid = bit_buffer_get_data(instance->tmp_buffer);
+        const uint8_t* received_data = bit_buffer_get_data(buf);
+
+        if(memcmp(listener_uid, &received_data[1], PICOPASS_BLOCK_LEN) != 0) {
+            if(instance->state == PicopassListenerStateActive) {
+                instance->state = PicopassListenerStateIdle;
+            } else if(instance->state == PicopassListenerStateSelected) {
+                // Technically we should go to StateHalt, but since we can't detect the field dropping we drop to idle instead
+                instance->state = PicopassListenerStateIdle;
+            }
+            break;
+        }
+
+        instance->state = PicopassListenerStateSelected;
+        bit_buffer_copy_bytes(
+            instance->tx_buffer,
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data,
+            sizeof(PicopassBlock));
+
+        PicopassError error = picopass_listener_send_frame(instance, instance->tx_buffer);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_D(TAG, "Error sending select response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_read_handler(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        uint8_t block_num = bit_buffer_get_byte(buf, 1);
+        if(block_num > PICOPASS_MAX_APP_LIMIT) break;
+
+        bit_buffer_reset(instance->tx_buffer);
+        if((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
+           (block_num == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
+            for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+                bit_buffer_append_byte(instance->tx_buffer, 0xff);
+            }
+        } else {
+            bit_buffer_copy_bytes(
+                instance->tx_buffer, instance->data->AA1[block_num].data, sizeof(PicopassBlock));
+        }
+        PicopassError error = picopass_listener_send_frame(instance, instance->tx_buffer);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_D(TAG, "Failed to tx read block response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+static PicopassListenerCommand
+    picopass_listener_readcheck(PicopassListener* instance, BitBuffer* buf, uint8_t key_block_num) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if(instance->state != PicopassListenerStateSelected) break;
+        uint8_t block_num = bit_buffer_get_byte(buf, 1);
+        if(block_num != PICOPASS_SECURE_EPURSE_BLOCK_INDEX) break;
+
+        // 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((instance->key_block_num != key_block_num) &&
+           (instance->mode != PicopassListenerModeLoclass)) {
+            instance->key_block_num = key_block_num;
+            picopass_listener_init_cipher_state(instance);
+        }
+
+        // DATA(8)
+        bit_buffer_copy_bytes(
+            instance->tx_buffer, instance->data->AA1[block_num].data, sizeof(PicopassBlock));
+        NfcError error = nfc_listener_tx(instance->nfc, instance->tx_buffer);
+        if(error != NfcErrorNone) {
+            FURI_LOG_D(TAG, "Failed to tx read check response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_readcheck_kd_handler(PicopassListener* instance, BitBuffer* buf) {
+    return picopass_listener_readcheck(instance, buf, PICOPASS_SECURE_KD_BLOCK_INDEX);
+}
+
+PicopassListenerCommand
+    picopass_listener_readcheck_kc_handler(PicopassListener* instance, BitBuffer* buf) {
+    return picopass_listener_readcheck(instance, buf, PICOPASS_SECURE_KC_BLOCK_INDEX);
+}
+
+PicopassListenerCommand
+    picopass_listener_check_handler_loclass(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+    NfcCommand callback_command = NfcCommandContinue;
+
+    // LOCLASS Reader attack mode
+    do {
+#ifndef PICOPASS_DEBUG_IGNORE_LOCLASS_STD_KEY
+        // loclass mode stores the derived standard debit key in Kd to check
+
+        PicopassBlock key = instance->data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX];
+        uint8_t rmac[4];
+        uint8_t rx_data[9] = {};
+        bit_buffer_write_bytes(buf, rx_data, sizeof(rx_data));
+        loclass_opt_doReaderMAC_2(instance->cipher_state, &rx_data[1], rmac, key.data);
+
+        if(!memcmp(&rx_data[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");
+            if(instance->callback) {
+                instance->event.type = PicopassListenerEventTypeLoclassGotStandardKey;
+                callback_command = instance->callback(instance->event, instance->context);
+                if(callback_command == NfcCommandStop) {
+                    command = PicopassListenerCommandStop;
+                }
+            }
+
+            // 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
+
+            break;
+        }
+#endif
+
+        // Save to buffer to defer flushing when we rotate CSN
+        memcpy(
+            instance->loclass_mac_buffer + ((instance->key_block_num % LOCLASS_NUM_PER_CSN) * 8),
+            &rx_data[1],
+            8);
+
+        // Rotate to the next CSN/attempt
+        instance->key_block_num++;
+
+        // CSN changed
+        if(instance->key_block_num % LOCLASS_NUM_PER_CSN == 0) {
+            // Flush NR-MACs for this CSN to SD card
+            for(int i = 0; i < LOCLASS_NUM_PER_CSN; i++) {
+                loclass_writer_write_params(
+                    instance->writer,
+                    instance->key_block_num + i - LOCLASS_NUM_PER_CSN,
+                    instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data,
+                    instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data,
+                    instance->loclass_mac_buffer + (i * 8),
+                    instance->loclass_mac_buffer + (i * 8) + 4);
+            }
+
+            if(instance->key_block_num < LOCLASS_NUM_CSNS * LOCLASS_NUM_PER_CSN) {
+                picopass_listener_loclass_update_csn(instance);
+                // Only reset the state when we change to a new CSN for the same reason as when we get a standard key
+                instance->state = PicopassListenerStateIdle;
+            }
+        }
+        if(instance->callback) {
+            instance->event.type = PicopassListenerEventTypeLoclassGotMac;
+            instance->callback(instance->event, instance->context);
+        }
+
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_check_handler_emulation(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        uint8_t rmac[4] = {};
+        uint8_t tmac[4] = {};
+        const uint8_t* key = instance->data->AA1[instance->key_block_num].data;
+        // Since nr isn't const in loclass_opt_doBothMAC_2() copy buffer
+        uint8_t rx_data[9] = {};
+        bit_buffer_write_bytes(buf, rx_data, sizeof(rx_data));
+        loclass_opt_doBothMAC_2(instance->cipher_state, &rx_data[1], rmac, tmac, key);
+
+#ifndef PICOPASS_DEBUG_IGNORE_BAD_RMAC
+        if(memcmp(&rx_data[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_listener_init_cipher_state(instance);
+            break;
+        }
+#endif
+
+        bit_buffer_copy_bytes(instance->tx_buffer, tmac, sizeof(tmac));
+        NfcError error = nfc_listener_tx(instance->nfc, instance->tx_buffer);
+        if(error != NfcErrorNone) {
+            FURI_LOG_D(TAG, "Failed tx update response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+PicopassListenerCommand
+    picopass_listener_check_handler(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if(instance->state != PicopassListenerStateSelected) break;
+        if(instance->mode == PicopassListenerModeLoclass) {
+            command = picopass_listener_check_handler_loclass(instance, buf);
+        } else {
+            command = picopass_listener_check_handler_emulation(instance, buf);
+        }
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_update_handler(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if(instance->mode == PicopassListenerModeLoclass) break;
+        if(instance->state != PicopassListenerStateSelected) break;
+
+        PicopassBlock config_block = instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX];
+        bool pers_mode = PICOPASS_LISTENER_HAS_MASK(config_block.data[7], PICOPASS_FUSE_PERS);
+
+        const uint8_t* rx_data = bit_buffer_get_data(buf);
+        uint8_t block_num = rx_data[1];
+        if(block_num == PICOPASS_CSN_BLOCK_INDEX) break; // CSN is always read only
+        if(!pers_mode && PICOPASS_LISTENER_HAS_MASK(config_block.data[3], 0x80))
+            break; // Chip is in RO mode, no updated possible (even ePurse)
+        if(!pers_mode && (block_num == PICOPASS_SECURE_AIA_BLOCK_INDEX))
+            break; // AIA can only be set in personalisation mode
+        if(!pers_mode &&
+           ((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX ||
+             block_num == PICOPASS_SECURE_KC_BLOCK_INDEX) &&
+            (!PICOPASS_LISTENER_HAS_MASK(config_block.data[7], PICOPASS_FUSE_CRYPT10))))
+            break;
+
+        if(block_num >= 6 && block_num <= 12) {
+            // bit0 is block6, up to bit6 being block12
+            if(!PICOPASS_LISTENER_HAS_MASK(config_block.data[3], (1 << (block_num - 6)))) {
+                // Block is marked as read-only, deny writing
+                break;
+            }
+        }
+        // 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 = PICOPASS_LISTENER_HAS_MASK(config_block.data[5], 0x01);
+        // -> must auth with that key to change it
+
+        PicopassBlock new_block = {};
+        switch(block_num) {
+        case PICOPASS_CONFIG_BLOCK_INDEX:
+            new_block.data[0] = config_block.data[0]; // Applications Limit
+            new_block.data[1] = config_block.data[1] & rx_data[3]; // OTP
+            new_block.data[2] = config_block.data[2] & rx_data[4]; // OTP
+            new_block.data[3] = config_block.data[3] & rx_data[5]; // Block Write Lock
+            new_block.data[4] = config_block.data[4]; // Chip Config
+            new_block.data[5] = config_block.data[5]; // Memory Config
+            new_block.data[6] = rx_data[8]; // EAS
+            new_block.data[7] = config_block.data[7]; // Fuses
+
+            // Some parts allow w (but not e) if in persMode
+            if(pers_mode) {
+                new_block.data[0] &= rx_data[2]; // Applications Limit
+                new_block.data[4] &= rx_data[6]; // Chip Config
+                new_block.data[5] &= rx_data[7]; // Memory Config
+                new_block.data[7] &= rx_data[9]; // Fuses
+            } else {
+                // Fuses allows setting Crypt1/0 from 1 to 0 only during application mode
+                new_block.data[7] &= rx_data[9] | ~PICOPASS_FUSE_CRYPT10;
+            }
+            break;
+
+        case PICOPASS_SECURE_EPURSE_BLOCK_INDEX:
+            // ePurse updates swap first and second half of the block each update
+            memcpy(&new_block.data[4], &rx_data[2], 4);
+            memcpy(&new_block.data[0], &rx_data[6], 4);
+            break;
+
+        case PICOPASS_SECURE_KD_BLOCK_INDEX:
+            // fallthrough
+        case PICOPASS_SECURE_KC_BLOCK_INDEX:
+            if(!pers_mode) {
+                new_block = instance->data->AA1[block_num];
+                for(size_t i = 0; i < sizeof(PicopassBlock); i++) {
+                    new_block.data[i] ^= rx_data[i + 2];
+                }
+                break;
+            }
+            // Use default case when in personalisation mode
+            // fallthrough
+        default:
+            memcpy(new_block.data, &rx_data[2], sizeof(PicopassBlock));
+            break;
+        }
+
+        instance->data->AA1[block_num] = new_block;
+        if((block_num == instance->key_block_num) ||
+           (block_num == PICOPASS_SECURE_EPURSE_BLOCK_INDEX)) {
+            picopass_listener_init_cipher_state(instance);
+        }
+
+        bit_buffer_reset(instance->tx_buffer);
+        if((block_num == PICOPASS_SECURE_KD_BLOCK_INDEX) ||
+           (block_num == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
+            // Key updates always return FF's
+            for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+                bit_buffer_append_byte(instance->tx_buffer, 0xff);
+            }
+        } else {
+            bit_buffer_copy_bytes(
+                instance->tx_buffer, instance->data->AA1[block_num].data, sizeof(PicopassBlock));
+        }
+
+        PicopassError error = picopass_listener_send_frame(instance, instance->tx_buffer);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_D(TAG, "Failed to tx update response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+PicopassListenerCommand
+    picopass_listener_read4_handler(PicopassListener* instance, BitBuffer* buf) {
+    PicopassListenerCommand command = PicopassListenerCommandSilent;
+
+    do {
+        if(instance->state != PicopassListenerStateSelected) break;
+
+        uint8_t block_start = bit_buffer_get_byte(buf, 1);
+        if(block_start + 4 >= PICOPASS_MAX_APP_LIMIT) break;
+
+        // TODO: Check CRC?
+        // TODO: Check auth?
+
+        bit_buffer_reset(instance->tx_buffer);
+        for(uint8_t i = block_start; i < block_start + 4; i++) {
+            if((i == PICOPASS_SECURE_KD_BLOCK_INDEX) || (i == PICOPASS_SECURE_KC_BLOCK_INDEX)) {
+                for(size_t j = 0; j < sizeof(PicopassBlock); j++) {
+                    bit_buffer_append_byte(instance->tx_buffer, 0xff);
+                }
+            } else {
+                bit_buffer_append_bytes(
+                    instance->tx_buffer, instance->data->AA1[i].data, sizeof(PicopassBlock));
+            }
+        }
+
+        PicopassError error = picopass_listener_send_frame(instance, instance->tx_buffer);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_D(TAG, "Failed to tx read4 response: %d", error);
+            break;
+        }
+
+        command = PicopassListenerCommandProcessed;
+    } while(false);
+
+    return command;
+}
+
+static const PicopassListenerCmd picopass_listener_cmd_handlers[] = {
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_ACTALL,
+        .cmd_len_bits = 8,
+        .handler = picopass_listener_actall_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_ACT,
+        .cmd_len_bits = 8,
+        .handler = picopass_listener_act_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_HALT,
+        .cmd_len_bits = 8,
+        .handler = picopass_listener_halt_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_READ_OR_IDENTIFY,
+        .cmd_len_bits = 8,
+        .handler = picopass_listener_identify_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_SELECT,
+        .cmd_len_bits = 8 * 9,
+        .handler = picopass_listener_select_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_READ_OR_IDENTIFY,
+        .cmd_len_bits = 8 * 4,
+        .handler = picopass_listener_read_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_READCHECK_KD,
+        .cmd_len_bits = 8 * 2,
+        .handler = picopass_listener_readcheck_kd_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_READCHECK_KC,
+        .cmd_len_bits = 8 * 2,
+        .handler = picopass_listener_readcheck_kc_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_CHECK,
+        .cmd_len_bits = 8 * 9,
+        .handler = picopass_listener_check_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_UPDATE,
+        .cmd_len_bits = 8 * 14,
+        .handler = picopass_listener_update_handler,
+    },
+    {
+        .start_byte_cmd = RFAL_PICOPASS_CMD_READ4,
+        .cmd_len_bits = 8 * 4,
+        .handler = picopass_listener_read4_handler,
+    },
+};
+
+PicopassListener* picopass_listener_alloc(Nfc* nfc, const PicopassDeviceData* data) {
+    furi_assert(nfc);
+    furi_assert(data);
+
+    PicopassListener* instance = malloc(sizeof(PicopassListener));
+    instance->nfc = nfc;
+    instance->data = malloc(sizeof(PicopassDeviceData));
+    mempcpy(instance->data, data, sizeof(PicopassDeviceData));
+
+    instance->tx_buffer = bit_buffer_alloc(PICOPASS_LISTENER_BUFFER_SIZE_MAX);
+    instance->tmp_buffer = bit_buffer_alloc(PICOPASS_LISTENER_BUFFER_SIZE_MAX);
+
+    nfc_set_fdt_listen_fc(instance->nfc, PICOPASS_FDT_LISTEN_FC);
+    nfc_config(instance->nfc, NfcModeListener, NfcTechIso15693);
+
+    return instance;
+}
+
+void picopass_listener_free(PicopassListener* instance) {
+    furi_assert(instance);
+
+    bit_buffer_free(instance->tx_buffer);
+    bit_buffer_free(instance->tmp_buffer);
+    free(instance->data);
+    if(instance->writer) {
+        loclass_writer_write_start_stop(instance->writer, false);
+        loclass_writer_free(instance->writer);
+    }
+    free(instance);
+}
+
+bool picopass_listener_set_mode(PicopassListener* instance, PicopassListenerMode mode) {
+    furi_assert(instance);
+    bool success = true;
+
+    instance->mode = mode;
+    if(instance->mode == PicopassListenerModeLoclass) {
+        instance->key_block_num = 0;
+        picopass_listener_loclass_update_csn(instance);
+        instance->writer = loclass_writer_alloc();
+        if(instance->writer) {
+            loclass_writer_write_start_stop(instance->writer, true);
+        } else {
+            success = false;
+        }
+    }
+
+    return success;
+}
+
+NfcCommand picopass_listener_start_callback(NfcEvent event, void* context) {
+    furi_assert(context);
+
+    NfcCommand command = NfcCommandContinue;
+    PicopassListener* instance = context;
+    BitBuffer* rx_buf = event.data.buffer;
+
+    PicopassListenerCommand picopass_cmd = PicopassListenerCommandSilent;
+    if(event.type == NfcEventTypeRxEnd) {
+        for(size_t i = 0; i < COUNT_OF(picopass_listener_cmd_handlers); i++) {
+            if(bit_buffer_get_size(rx_buf) != picopass_listener_cmd_handlers[i].cmd_len_bits) {
+                continue;
+            }
+            if(bit_buffer_get_byte(rx_buf, 0) !=
+               picopass_listener_cmd_handlers[i].start_byte_cmd) {
+                continue;
+            }
+            picopass_cmd = picopass_listener_cmd_handlers[i].handler(instance, rx_buf);
+            break;
+        }
+        if(picopass_cmd == PicopassListenerCommandSendSoF) {
+            nfc_iso15693_listener_tx_sof(instance->nfc);
+        } else if(picopass_cmd == PicopassListenerCommandStop) {
+            command = NfcCommandStop;
+        }
+    }
+
+    return command;
+}
+
+void picopass_listener_start(
+    PicopassListener* instance,
+    PicopassListenerCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(callback);
+
+    instance->callback = callback;
+    instance->context = context;
+
+    picopass_listener_reset(instance);
+    nfc_start(instance->nfc, picopass_listener_start_callback, instance);
+}
+
+void picopass_listener_stop(PicopassListener* instance) {
+    furi_assert(instance);
+
+    nfc_stop(instance->nfc);
+}
+
+const PicopassDeviceData* picopass_listener_get_data(PicopassListener* instance) {
+    furi_assert(instance);
+
+    return instance->data;
+}

+ 45 - 0
picopass/protocol/picopass_listener.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <nfc/nfc.h>
+#include "picopass_protocol.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    PicopassListenerModeEmulation,
+    PicopassListenerModeLoclass,
+} PicopassListenerMode;
+
+typedef enum {
+    PicopassListenerEventTypeLoclassGotStandardKey,
+    PicopassListenerEventTypeLoclassGotMac,
+} PicopassListenerEventType;
+
+typedef struct {
+    PicopassListenerEventType type;
+} PicopassListenerEvent;
+
+typedef NfcCommand (*PicopassListenerCallback)(PicopassListenerEvent event, void* context);
+
+typedef struct PicopassListener PicopassListener;
+
+PicopassListener* picopass_listener_alloc(Nfc* nfc, const PicopassDeviceData* data);
+
+void picopass_listener_free(PicopassListener* instance);
+
+bool picopass_listener_set_mode(PicopassListener* instance, PicopassListenerMode mode);
+
+void picopass_listener_start(
+    PicopassListener* instance,
+    PicopassListenerCallback callback,
+    void* context);
+
+void picopass_listener_stop(PicopassListener* instance);
+
+const PicopassDeviceData* picopass_listener_get_data(PicopassListener* instance);
+
+#ifdef __cplusplus
+}
+#endif

+ 52 - 0
picopass/protocol/picopass_listener_i.c

@@ -0,0 +1,52 @@
+#include "picopass_listener_i.h"
+
+#include <furi/furi.h>
+
+static PicopassError picopass_listener_process_error(NfcError error) {
+    PicopassError ret = PicopassErrorNone;
+
+    switch(error) {
+    case NfcErrorNone:
+        ret = PicopassErrorNone;
+        break;
+
+    default:
+        ret = PicopassErrorTimeout;
+        break;
+    }
+
+    return ret;
+}
+
+void picopass_listener_init_cipher_state_key(PicopassListener* instance, const uint8_t* key) {
+    uint8_t cc[PICOPASS_BLOCK_LEN] = {};
+    memcpy(
+        cc, instance->data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data, sizeof(PicopassBlock));
+
+    instance->cipher_state = loclass_opt_doTagMAC_1(cc, key);
+}
+
+void picopass_listener_init_cipher_state(PicopassListener* instance) {
+    uint8_t key[PICOPASS_BLOCK_LEN] = {};
+    memcpy(key, instance->data->AA1[instance->key_block_num].data, sizeof(PicopassBlock));
+
+    picopass_listener_init_cipher_state_key(instance, key);
+}
+
+PicopassError picopass_listener_send_frame(PicopassListener* instance, BitBuffer* tx_buffer) {
+    iso13239_crc_append(Iso13239CrcTypePicopass, tx_buffer);
+    NfcError error = nfc_listener_tx(instance->nfc, tx_buffer);
+
+    return picopass_listener_process_error(error);
+}
+
+// from proxmark3 armsrc/iclass.c rotateCSN
+PicopassError picopass_listener_write_anticoll_csn(PicopassListener* instance, BitBuffer* buffer) {
+    const uint8_t* uid = instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+    bit_buffer_reset(buffer);
+    for(size_t i = 0; i < PICOPASS_BLOCK_LEN; i++) {
+        bit_buffer_append_byte(buffer, (uid[i] >> 3) | (uid[(i + 1) % 8] << 5));
+    }
+
+    return PicopassErrorNone;
+}

+ 47 - 0
picopass/protocol/picopass_listener_i.h

@@ -0,0 +1,47 @@
+#pragma once
+
+#include "picopass_listener.h"
+#include <nfc/helpers/iso13239_crc.h>
+
+#include <optimized_ikeys.h>
+#include <optimized_cipher.h>
+#include <loclass_writer.h>
+
+#define TAG "PicopassListener"
+
+#define PICOPASS_LISTENER_BUFFER_SIZE_MAX (255)
+
+typedef enum {
+    PicopassListenerStateIdle,
+    PicopassListenerStateHalt,
+    PicopassListenerStateActive,
+    PicopassListenerStateSelected,
+} PicopassListenerState;
+
+struct PicopassListener {
+    Nfc* nfc;
+    PicopassDeviceData* data;
+    PicopassListenerState state;
+
+    LoclassState_t cipher_state;
+    PicopassListenerMode mode;
+
+    BitBuffer* tx_buffer;
+    BitBuffer* tmp_buffer;
+    uint8_t key_block_num;
+
+    LoclassWriter* writer;
+    uint8_t loclass_mac_buffer[8 * LOCLASS_NUM_PER_CSN];
+
+    PicopassListenerEvent event;
+    PicopassListenerCallback callback;
+    void* context;
+};
+
+void picopass_listener_init_cipher_state_key(PicopassListener* instance, const uint8_t* key);
+
+void picopass_listener_init_cipher_state(PicopassListener* instance);
+
+PicopassError picopass_listener_send_frame(PicopassListener* instance, BitBuffer* tx_buffer);
+
+PicopassError picopass_listener_write_anticoll_csn(PicopassListener* instance, BitBuffer* buffer);

+ 543 - 0
picopass/protocol/picopass_poller.c

@@ -0,0 +1,543 @@
+#include "picopass_poller_i.h"
+
+#include "../loclass/optimized_cipher.h"
+
+#include <furi/furi.h>
+
+#define TAG "Picopass"
+
+typedef NfcCommand (*PicopassPollerStateHandler)(PicopassPoller* instance);
+
+static void picopass_poller_reset(PicopassPoller* instance) {
+    instance->current_block = 0;
+}
+
+static void picopass_poller_prepare_read(PicopassPoller* instance) {
+    instance->app_limit = instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] <
+                                  PICOPASS_MAX_APP_LIMIT ?
+                              instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0] :
+                              PICOPASS_MAX_APP_LIMIT;
+    instance->current_block = 2;
+}
+
+NfcCommand picopass_poller_request_mode_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    instance->event.type = PicopassPollerEventTypeRequestMode;
+    command = instance->callback(instance->event, instance->context);
+    instance->mode = instance->event_data.req_mode.mode;
+    instance->state = PicopassPollerStateDetect;
+
+    return command;
+}
+
+NfcCommand picopass_poller_detect_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    PicopassError error = picopass_poller_actall(instance);
+
+    if(error == PicopassErrorNone) {
+        instance->state = PicopassPollerStateSelect;
+        instance->event.type = PicopassPollerEventTypeCardDetected;
+        command = instance->callback(instance->event, instance->context);
+    } else {
+        furi_delay_ms(100);
+    }
+
+    return command;
+}
+
+NfcCommand picopass_poller_select_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    do {
+        PicopassError error = picopass_poller_identify(instance, &instance->col_res_serial_num);
+        if(error != PicopassErrorNone) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+
+        error =
+            picopass_poller_select(instance, &instance->col_res_serial_num, &instance->serial_num);
+        if(error != PicopassErrorNone) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+
+        if(instance->mode == PicopassPollerModeRead) {
+            instance->state = PicopassPollerStatePreAuth;
+        } else {
+            instance->state = PicopassPollerStateAuth;
+        }
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_pre_auth_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    PicopassError error = PicopassErrorNone;
+
+    do {
+        memcpy(
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data,
+            instance->serial_num.data,
+            sizeof(PicopassSerialNum));
+        FURI_LOG_D(
+            TAG,
+            "csn %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[0],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[1],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[2],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[3],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[4],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[5],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[6],
+            instance->data->AA1[PICOPASS_CSN_BLOCK_INDEX].data[7]);
+
+        PicopassBlock block = {};
+        error = picopass_poller_read_block(instance, 1, &block);
+        if(error != PicopassErrorNone) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+        memcpy(
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data,
+            block.data,
+            sizeof(PicopassBlock));
+        FURI_LOG_D(
+            TAG,
+            "config %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[0],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[1],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[2],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[3],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[4],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[5],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[6],
+            instance->data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data[7]);
+
+        error = picopass_poller_read_block(instance, 5, &block);
+        if(error != PicopassErrorNone) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+        memcpy(
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data,
+            block.data,
+            sizeof(PicopassBlock));
+        FURI_LOG_D(
+            TAG,
+            "aia %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[0],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[1],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[2],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[3],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[4],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[5],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[6],
+            instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data[7]);
+
+        instance->state = PicopassPollerStateCheckSecurity;
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_check_security(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    // Thank you proxmark!
+    PicopassBlock temp_block = {};
+    memset(temp_block.data, 0xff, sizeof(PicopassBlock));
+    instance->data->pacs.legacy =
+        (memcmp(
+             instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data,
+             temp_block.data,
+             sizeof(PicopassBlock)) == 0);
+
+    temp_block.data[3] = 0x00;
+    temp_block.data[4] = 0x06;
+    instance->data->pacs.se_enabled =
+        (memcmp(
+             instance->data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data,
+             temp_block.data,
+             sizeof(PicopassBlock)) == 0);
+
+    if(instance->data->pacs.se_enabled) {
+        FURI_LOG_D(TAG, "SE enabled");
+        instance->state = PicopassPollerStateFail;
+    } else {
+        instance->state = PicopassPollerStateAuth;
+    }
+
+    return command;
+}
+
+NfcCommand picopass_poller_auth_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    do {
+        // Request key
+        instance->event.type = PicopassPollerEventTypeRequestKey;
+        command = instance->callback(instance->event, instance->context);
+        if(command != NfcCommandContinue) break;
+
+        if(!instance->event_data.req_key.is_key_provided) {
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+
+        FURI_LOG_D(
+            TAG,
+            "Try to %s auth with key %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->event_data.req_key.is_elite_key ? "elite" : "standard",
+            instance->event_data.req_key.key[0],
+            instance->event_data.req_key.key[1],
+            instance->event_data.req_key.key[2],
+            instance->event_data.req_key.key[3],
+            instance->event_data.req_key.key[4],
+            instance->event_data.req_key.key[5],
+            instance->event_data.req_key.key[6],
+            instance->event_data.req_key.key[7]);
+
+        PicopassReadCheckResp read_check_resp = {};
+        uint8_t* csn = instance->serial_num.data;
+        memset(instance->div_key, 0, sizeof(instance->div_key));
+        uint8_t* div_key = NULL;
+
+        if(instance->mode == PicopassPollerModeRead) {
+            div_key = instance->data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data;
+        } else {
+            div_key = instance->div_key;
+        }
+
+        uint8_t ccnr[12] = {};
+        PicopassMac mac = {};
+
+        PicopassError error = picopass_poller_read_check(instance, &read_check_resp);
+        if(error == PicopassErrorTimeout) {
+            instance->event.type = PicopassPollerEventTypeCardLost;
+            command = instance->callback(instance->event, instance->context);
+            instance->state = PicopassPollerStateDetect;
+            break;
+        } else if(error != PicopassErrorNone) {
+            FURI_LOG_E(TAG, "Read check failed: %d", error);
+            break;
+        }
+        memcpy(ccnr, read_check_resp.data, sizeof(PicopassReadCheckResp)); // last 4 bytes left 0
+
+        loclass_iclass_calc_div_key(
+            csn,
+            instance->event_data.req_key.key,
+            div_key,
+            instance->event_data.req_key.is_elite_key);
+        loclass_opt_doReaderMAC(ccnr, div_key, mac.data);
+
+        PicopassCheckResp check_resp = {};
+        error = picopass_poller_check(instance, &mac, &check_resp);
+        if(error == PicopassErrorNone) {
+            FURI_LOG_I(TAG, "Found key");
+            memcpy(instance->mac.data, mac.data, sizeof(PicopassMac));
+            if(instance->mode == PicopassPollerModeRead) {
+                memcpy(
+                    instance->data->pacs.key, instance->event_data.req_key.key, PICOPASS_KEY_LEN);
+                instance->data->pacs.elite_kdf = instance->event_data.req_key.is_elite_key;
+                picopass_poller_prepare_read(instance);
+                instance->state = PicopassPollerStateReadBlock;
+            } else if(instance->mode == PicopassPollerModeWrite) {
+                instance->state = PicopassPollerStateWriteBlock;
+            } else {
+                instance->state = PicopassPollerStateWriteKey;
+            }
+        }
+
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_read_block_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    do {
+        if(instance->current_block == instance->app_limit) {
+            instance->state = PicopassPollerStateParseCredential;
+            break;
+        }
+
+        if(instance->current_block == PICOPASS_SECURE_KD_BLOCK_INDEX) {
+            // Skip over Kd block which is populated earlier (READ of Kd returns all FF's)
+            instance->current_block++;
+        }
+
+        PicopassBlock block = {};
+        PicopassError error =
+            picopass_poller_read_block(instance, instance->current_block, &block);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_E(TAG, "Failed to read block %d: %d", instance->current_block, error);
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+        FURI_LOG_D(
+            TAG,
+            "Block %d: %02x%02x%02x%02x%02x%02x%02x%02x",
+            instance->current_block,
+            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(
+            instance->data->AA1[instance->current_block].data, block.data, sizeof(PicopassBlock));
+        instance->current_block++;
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_parse_credential_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    picopass_device_parse_credential(instance->data->AA1, &instance->data->pacs);
+    instance->state = PicopassPollerStateParseWiegand;
+    return command;
+}
+
+NfcCommand picopass_poller_parse_wiegand_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    picopass_device_parse_wiegand(instance->data->pacs.credential, &instance->data->pacs);
+    instance->state = PicopassPollerStateSuccess;
+    return command;
+}
+
+NfcCommand picopass_poller_write_block_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    PicopassError error = PicopassErrorNone;
+
+    do {
+        instance->event.type = PicopassPollerEventTypeRequestWriteBlock;
+        command = instance->callback(instance->event, instance->context);
+        if(command != NfcCommandContinue) break;
+
+        PicopassPollerEventDataRequestWriteBlock* write_block = &instance->event_data.req_write;
+        if(!write_block->perform_write) {
+            instance->state = PicopassPollerStateSuccess;
+            break;
+        }
+
+        FURI_LOG_D(TAG, "Writing %d block", write_block->block_num);
+        uint8_t data[9] = {};
+        data[0] = write_block->block_num;
+        memcpy(&data[1], write_block->block->data, PICOPASS_BLOCK_LEN);
+        loclass_doMAC_N(data, sizeof(data), instance->div_key, instance->mac.data);
+        FURI_LOG_D(
+            TAG,
+            "loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x",
+            write_block->block_num,
+            data[1],
+            data[2],
+            data[3],
+            data[4],
+            data[5],
+            data[6],
+            data[7],
+            data[8],
+            instance->mac.data[0],
+            instance->mac.data[1],
+            instance->mac.data[2],
+            instance->mac.data[3]);
+        error = picopass_poller_write_block(
+            instance, write_block->block_num, write_block->block, &instance->mac);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_E(TAG, "Failed to write block %d. Error %d", write_block->block_num, error);
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_write_key_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+    PicopassError error = PicopassErrorNone;
+
+    do {
+        instance->event.type = PicopassPollerEventTypeRequestWriteKey;
+        command = instance->callback(instance->event, instance->context);
+        if(command != NfcCommandContinue) break;
+
+        const PicopassDeviceData* picopass_data = instance->event_data.req_write_key.data;
+        const uint8_t* new_key = instance->event_data.req_write_key.key;
+        bool is_elite_key = instance->event_data.req_write_key.is_elite_key;
+
+        const uint8_t* csn = picopass_data->AA1[PICOPASS_CSN_BLOCK_INDEX].data;
+        const uint8_t* config_block = picopass_data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data;
+        uint8_t fuses = config_block[7];
+        const uint8_t* old_key = picopass_data->AA1[PICOPASS_SECURE_KD_BLOCK_INDEX].data;
+
+        PicopassBlock new_block = {};
+        loclass_iclass_calc_div_key(csn, new_key, new_block.data, is_elite_key);
+
+        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 < PICOPASS_BLOCK_LEN; i++) {
+                new_block.data[i] ^= old_key[i];
+            }
+        }
+
+        FURI_LOG_D(TAG, "Writing key to %d block", PICOPASS_SECURE_KD_BLOCK_INDEX);
+        uint8_t data[9] = {};
+        data[0] = PICOPASS_SECURE_KD_BLOCK_INDEX;
+        memcpy(&data[1], new_block.data, PICOPASS_BLOCK_LEN);
+        loclass_doMAC_N(data, sizeof(data), instance->div_key, instance->mac.data);
+        FURI_LOG_D(
+            TAG,
+            "loclass_doMAC_N %d %02x%02x%02x%02x%02x%02x%02x%02x %02x%02x%02x%02x",
+            PICOPASS_SECURE_KD_BLOCK_INDEX,
+            data[1],
+            data[2],
+            data[3],
+            data[4],
+            data[5],
+            data[6],
+            data[7],
+            data[8],
+            instance->mac.data[0],
+            instance->mac.data[1],
+            instance->mac.data[2],
+            instance->mac.data[3]);
+        error = picopass_poller_write_block(
+            instance, PICOPASS_SECURE_KD_BLOCK_INDEX, &new_block, &instance->mac);
+        if(error != PicopassErrorNone) {
+            FURI_LOG_E(
+                TAG, "Failed to write block %d. Error %d", PICOPASS_SECURE_KD_BLOCK_INDEX, error);
+            instance->state = PicopassPollerStateFail;
+            break;
+        }
+        instance->state = PicopassPollerStateSuccess;
+
+    } while(false);
+
+    return command;
+}
+
+NfcCommand picopass_poller_success_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandContinue;
+
+    instance->event.type = PicopassPollerEventTypeSuccess;
+    command = instance->callback(instance->event, instance->context);
+    furi_delay_ms(100);
+
+    return command;
+}
+
+NfcCommand picopass_poller_fail_handler(PicopassPoller* instance) {
+    NfcCommand command = NfcCommandReset;
+
+    instance->event.type = PicopassPollerEventTypeFail;
+    command = instance->callback(instance->event, instance->context);
+    picopass_poller_reset(instance);
+    instance->state = PicopassPollerStateDetect;
+
+    return command;
+}
+
+static const PicopassPollerStateHandler picopass_poller_state_handler[PicopassPollerStateNum] = {
+    [PicopassPollerStateRequestMode] = picopass_poller_request_mode_handler,
+    [PicopassPollerStateDetect] = picopass_poller_detect_handler,
+    [PicopassPollerStateSelect] = picopass_poller_select_handler,
+    [PicopassPollerStatePreAuth] = picopass_poller_pre_auth_handler,
+    [PicopassPollerStateCheckSecurity] = picopass_poller_check_security,
+    [PicopassPollerStateAuth] = picopass_poller_auth_handler,
+    [PicopassPollerStateReadBlock] = picopass_poller_read_block_handler,
+    [PicopassPollerStateWriteBlock] = picopass_poller_write_block_handler,
+    [PicopassPollerStateWriteKey] = picopass_poller_write_key_handler,
+    [PicopassPollerStateParseCredential] = picopass_poller_parse_credential_handler,
+    [PicopassPollerStateParseWiegand] = picopass_poller_parse_wiegand_handler,
+    [PicopassPollerStateSuccess] = picopass_poller_success_handler,
+    [PicopassPollerStateFail] = picopass_poller_fail_handler,
+};
+
+static NfcCommand picopass_poller_callback(NfcEvent event, void* context) {
+    furi_assert(context);
+
+    PicopassPoller* instance = context;
+    NfcCommand command = NfcCommandContinue;
+
+    if(event.type == NfcEventTypePollerReady) {
+        command = picopass_poller_state_handler[instance->state](instance);
+    }
+
+    if(instance->session_state == PicopassPollerSessionStateStopRequest) {
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+void picopass_poller_start(
+    PicopassPoller* instance,
+    PicopassPollerCallback callback,
+    void* context) {
+    furi_assert(instance);
+    furi_assert(instance->session_state == PicopassPollerSessionStateIdle);
+
+    instance->callback = callback;
+    instance->context = context;
+
+    instance->session_state = PicopassPollerSessionStateActive;
+    nfc_start(instance->nfc, picopass_poller_callback, instance);
+}
+
+void picopass_poller_stop(PicopassPoller* instance) {
+    furi_assert(instance);
+
+    instance->session_state = PicopassPollerSessionStateStopRequest;
+    nfc_stop(instance->nfc);
+    instance->session_state = PicopassPollerSessionStateIdle;
+}
+
+PicopassPoller* picopass_poller_alloc(Nfc* nfc) {
+    furi_assert(nfc);
+
+    PicopassPoller* instance = malloc(sizeof(PicopassPoller));
+    instance->nfc = nfc;
+    nfc_config(instance->nfc, NfcModePoller, NfcTechIso15693);
+    nfc_set_guard_time_us(instance->nfc, 10000);
+    nfc_set_fdt_poll_fc(instance->nfc, 5000);
+    nfc_set_fdt_poll_poll_us(instance->nfc, 1000);
+
+    instance->event.data = &instance->event_data;
+    instance->data = malloc(sizeof(PicopassDeviceData));
+
+    instance->tx_buffer = bit_buffer_alloc(PICOPASS_POLLER_BUFFER_SIZE);
+    instance->rx_buffer = bit_buffer_alloc(PICOPASS_POLLER_BUFFER_SIZE);
+    instance->tmp_buffer = bit_buffer_alloc(PICOPASS_POLLER_BUFFER_SIZE);
+
+    return instance;
+}
+
+void picopass_poller_free(PicopassPoller* instance) {
+    furi_assert(instance);
+
+    free(instance->data);
+    bit_buffer_free(instance->tx_buffer);
+    bit_buffer_free(instance->rx_buffer);
+    bit_buffer_free(instance->tmp_buffer);
+    free(instance);
+}
+
+const PicopassDeviceData* picopass_poller_get_data(PicopassPoller* instance) {
+    furi_assert(instance);
+
+    return instance->data;
+}

+ 80 - 0
picopass/protocol/picopass_poller.h

@@ -0,0 +1,80 @@
+#pragma once
+
+#include <nfc/nfc.h>
+#include "picopass_protocol.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    PicopassPollerEventTypeRequestMode,
+    PicopassPollerEventTypeCardDetected,
+    PicopassPollerEventTypeCardLost,
+    PicopassPollerEventTypeRequestKey,
+    PicopassPollerEventTypeRequestWriteBlock,
+    PicopassPollerEventTypeRequestWriteKey,
+    PicopassPollerEventTypeSuccess,
+    PicopassPollerEventTypeFail,
+} PicopassPollerEventType;
+
+typedef enum {
+    PicopassPollerModeRead,
+    PicopassPollerModeWrite,
+    PicopassPollerModeWriteKey,
+} PicopassPollerMode;
+
+typedef struct {
+    PicopassPollerMode mode;
+} PicopassPollerEventDataRequestMode;
+
+typedef struct {
+    uint8_t key[PICOPASS_KEY_LEN];
+    bool is_key_provided;
+    bool is_elite_key;
+} PicopassPollerEventDataRequestKey;
+
+typedef struct {
+    bool perform_write;
+    uint8_t block_num;
+    const PicopassBlock* block;
+} PicopassPollerEventDataRequestWriteBlock;
+
+typedef struct {
+    const PicopassDeviceData* data;
+    uint8_t key[PICOPASS_KEY_LEN];
+    bool is_elite_key;
+} PicopassPollerEventDataRequestWriteKey;
+
+typedef union {
+    PicopassPollerEventDataRequestMode req_mode;
+    PicopassPollerEventDataRequestKey req_key;
+    PicopassPollerEventDataRequestWriteBlock req_write;
+    PicopassPollerEventDataRequestWriteKey req_write_key;
+} PicopassPollerEventData;
+
+typedef struct {
+    PicopassPollerEventType type;
+    PicopassPollerEventData* data;
+} PicopassPollerEvent;
+
+typedef NfcCommand (*PicopassPollerCallback)(PicopassPollerEvent event, void* context);
+
+typedef struct PicopassPoller PicopassPoller;
+
+PicopassPoller* picopass_poller_alloc(Nfc* nfc);
+
+void picopass_poller_free(PicopassPoller* instance);
+
+void picopass_poller_start(
+    PicopassPoller* instance,
+    PicopassPollerCallback callback,
+    void* context);
+
+void picopass_poller_stop(PicopassPoller* instance);
+
+const PicopassDeviceData* picopass_poller_get_data(PicopassPoller* instance);
+
+#ifdef __cplusplus
+}
+#endif

+ 217 - 0
picopass/protocol/picopass_poller_i.c

@@ -0,0 +1,217 @@
+#include "picopass_poller_i.h"
+
+#include <nfc/helpers/iso14443_crc.h>
+
+#define PICOPASS_POLLER_FWT_FC (100000)
+
+#define TAG "Picopass"
+
+static PicopassError picopass_poller_process_error(NfcError error) {
+    PicopassError ret = PicopassErrorNone;
+
+    switch(error) {
+    case NfcErrorNone:
+        ret = PicopassErrorNone;
+        break;
+
+    default:
+        ret = PicopassErrorTimeout;
+        break;
+    }
+
+    return ret;
+}
+
+static PicopassError picopass_poller_send_frame(
+    PicopassPoller* instance,
+    BitBuffer* tx_buffer,
+    BitBuffer* rx_buffer,
+    uint32_t fwt_fc) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        NfcError error = nfc_poller_trx(instance->nfc, tx_buffer, rx_buffer, fwt_fc);
+        if(error != NfcErrorNone) {
+            ret = picopass_poller_process_error(error);
+            break;
+        }
+        if(!iso13239_crc_check(Iso13239CrcTypePicopass, rx_buffer)) {
+            ret = PicopassErrorIncorrectCrc;
+            break;
+        }
+        iso13239_crc_trim(instance->rx_buffer);
+    } while(false);
+
+    return ret;
+}
+
+PicopassError picopass_poller_actall(PicopassPoller* instance) {
+    PicopassError ret = PicopassErrorNone;
+
+    bit_buffer_reset(instance->tx_buffer);
+    bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_ACTALL);
+
+    NfcError error = nfc_poller_trx(
+        instance->nfc, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+    if(error != NfcErrorIncompleteFrame) {
+        ret = picopass_poller_process_error(error);
+    }
+
+    return ret;
+}
+
+PicopassError picopass_poller_identify(
+    PicopassPoller* instance,
+    PicopassColResSerialNum* col_res_serial_num) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_READ_OR_IDENTIFY);
+        ret = picopass_poller_send_frame(
+            instance, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(ret != PicopassErrorNone) break;
+        if(bit_buffer_get_size_bytes(instance->rx_buffer) != sizeof(PicopassColResSerialNum)) {
+            ret = PicopassErrorProtocol;
+            break;
+        }
+        bit_buffer_write_bytes(
+            instance->rx_buffer, col_res_serial_num->data, sizeof(PicopassColResSerialNum));
+    } while(false);
+
+    return ret;
+}
+
+PicopassError picopass_poller_select(
+    PicopassPoller* instance,
+    PicopassColResSerialNum* col_res_serial_num,
+    PicopassSerialNum* serial_num) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_SELECT);
+        bit_buffer_append_bytes(
+            instance->tx_buffer, col_res_serial_num->data, sizeof(PicopassColResSerialNum));
+        ret = picopass_poller_send_frame(
+            instance, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(ret != PicopassErrorNone) break;
+        if(bit_buffer_get_size_bytes(instance->rx_buffer) != sizeof(PicopassSerialNum)) {
+            ret = PicopassErrorProtocol;
+            break;
+        }
+        bit_buffer_write_bytes(instance->rx_buffer, serial_num->data, sizeof(PicopassSerialNum));
+    } while(false);
+
+    return ret;
+}
+
+PicopassError
+    picopass_poller_read_block(PicopassPoller* instance, uint8_t block_num, PicopassBlock* block) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tmp_buffer);
+        bit_buffer_append_byte(instance->tmp_buffer, block_num);
+        iso13239_crc_append(Iso13239CrcTypePicopass, instance->tmp_buffer);
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_READ_OR_IDENTIFY);
+        bit_buffer_append(instance->tx_buffer, instance->tmp_buffer);
+
+        ret = picopass_poller_send_frame(
+            instance, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(ret != PicopassErrorNone) break;
+
+        if(bit_buffer_get_size_bytes(instance->rx_buffer) != sizeof(PicopassBlock)) {
+            ret = PicopassErrorProtocol;
+            break;
+        }
+        bit_buffer_write_bytes(instance->rx_buffer, block->data, sizeof(PicopassBlock));
+    } while(false);
+
+    return ret;
+}
+
+PicopassError
+    picopass_poller_read_check(PicopassPoller* instance, PicopassReadCheckResp* read_check_resp) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_READCHECK_KD);
+        bit_buffer_append_byte(instance->tx_buffer, 0x02);
+
+        NfcError error = nfc_poller_trx(
+            instance->nfc, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(error != NfcErrorNone) {
+            ret = picopass_poller_process_error(error);
+            break;
+        }
+
+        if(bit_buffer_get_size_bytes(instance->rx_buffer) != sizeof(PicopassReadCheckResp)) {
+            ret = PicopassErrorProtocol;
+            break;
+        }
+        bit_buffer_write_bytes(
+            instance->rx_buffer, read_check_resp->data, sizeof(PicopassReadCheckResp));
+    } while(false);
+
+    return ret;
+}
+
+PicopassError picopass_poller_check(
+    PicopassPoller* instance,
+    PicopassMac* mac,
+    PicopassCheckResp* check_resp) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_CHECK);
+        uint8_t null_arr[4] = {};
+        bit_buffer_append_bytes(instance->tx_buffer, null_arr, sizeof(null_arr));
+        bit_buffer_append_bytes(instance->tx_buffer, mac->data, sizeof(PicopassMac));
+
+        NfcError error = nfc_poller_trx(
+            instance->nfc, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(error != NfcErrorNone) {
+            ret = picopass_poller_process_error(error);
+            break;
+        }
+
+        if(bit_buffer_get_size_bytes(instance->rx_buffer) != sizeof(PicopassCheckResp)) {
+            ret = PicopassErrorProtocol;
+            break;
+        }
+        bit_buffer_write_bytes(instance->rx_buffer, check_resp->data, sizeof(PicopassCheckResp));
+
+    } while(false);
+
+    return ret;
+}
+
+PicopassError picopass_poller_write_block(
+    PicopassPoller* instance,
+    uint8_t block_num,
+    const PicopassBlock* block,
+    const PicopassMac* mac) {
+    PicopassError ret = PicopassErrorNone;
+
+    do {
+        bit_buffer_reset(instance->tx_buffer);
+        bit_buffer_append_byte(instance->tx_buffer, RFAL_PICOPASS_CMD_UPDATE);
+        bit_buffer_append_byte(instance->tx_buffer, block_num);
+        bit_buffer_append_bytes(instance->tx_buffer, block->data, sizeof(PicopassBlock));
+        bit_buffer_append_bytes(instance->tx_buffer, mac->data, sizeof(PicopassMac));
+
+        NfcError error = nfc_poller_trx(
+            instance->nfc, instance->tx_buffer, instance->rx_buffer, PICOPASS_POLLER_FWT_FC);
+        if(error != NfcErrorNone) {
+            ret = picopass_poller_process_error(error);
+            break;
+        }
+
+    } while(false);
+
+    return ret;
+}

+ 85 - 0
picopass/protocol/picopass_poller_i.h

@@ -0,0 +1,85 @@
+#pragma once
+
+#include "picopass_poller.h"
+#include "picopass_protocol.h"
+
+#include <nfc/helpers/iso13239_crc.h>
+
+#define PICOPASS_POLLER_BUFFER_SIZE (255)
+#define PICOPASS_CRC_SIZE (2)
+
+typedef enum {
+    PicopassPollerSessionStateIdle,
+    PicopassPollerSessionStateActive,
+    PicopassPollerSessionStateStopRequest,
+} PicopassPollerSessionState;
+
+typedef enum {
+    PicopassPollerStateRequestMode,
+    PicopassPollerStateDetect,
+    PicopassPollerStateSelect,
+    PicopassPollerStatePreAuth,
+    PicopassPollerStateCheckSecurity,
+    PicopassPollerStateAuth,
+    PicopassPollerStateReadBlock,
+    PicopassPollerStateWriteBlock,
+    PicopassPollerStateWriteKey,
+    PicopassPollerStateParseCredential,
+    PicopassPollerStateParseWiegand,
+    PicopassPollerStateSuccess,
+    PicopassPollerStateFail,
+
+    PicopassPollerStateNum,
+} PicopassPollerState;
+
+struct PicopassPoller {
+    Nfc* nfc;
+    PicopassPollerSessionState session_state;
+    PicopassPollerState state;
+    PicopassPollerMode mode;
+
+    PicopassColResSerialNum col_res_serial_num;
+    PicopassSerialNum serial_num;
+    PicopassMac mac;
+    uint8_t div_key[8];
+    uint8_t current_block;
+    uint8_t app_limit;
+
+    PicopassDeviceData* data;
+
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+    BitBuffer* tmp_buffer;
+
+    PicopassPollerEvent event;
+    PicopassPollerEventData event_data;
+    PicopassPollerCallback callback;
+    void* context;
+};
+
+PicopassError picopass_poller_actall(PicopassPoller* instance);
+
+PicopassError
+    picopass_poller_identify(PicopassPoller* instance, PicopassColResSerialNum* col_res_serial_num);
+
+PicopassError picopass_poller_select(
+    PicopassPoller* instance,
+    PicopassColResSerialNum* col_res_serial_num,
+    PicopassSerialNum* serial_num);
+
+PicopassError
+    picopass_poller_read_block(PicopassPoller* instance, uint8_t block_num, PicopassBlock* block);
+
+PicopassError
+    picopass_poller_read_check(PicopassPoller* instance, PicopassReadCheckResp* read_check_resp);
+
+PicopassError picopass_poller_check(
+    PicopassPoller* instance,
+    PicopassMac* mac,
+    PicopassCheckResp* check_resp);
+
+PicopassError picopass_poller_write_block(
+    PicopassPoller* instance,
+    uint8_t block_num,
+    const PicopassBlock* block,
+    const PicopassMac* mac);

+ 48 - 0
picopass/protocol/picopass_protocol.h

@@ -0,0 +1,48 @@
+#pragma once
+
+#include "../picopass_device.h"
+
+#define PICOPASS_BLOCK_LEN 8
+#define PICOPASS_MAX_APP_LIMIT 32
+#define PICOPASS_UID_LEN 8
+#define PICOPASS_READ_CHECK_RESP_LEN 8
+#define PICOPASS_CHECK_RESP_LEN 4
+#define PICOPASS_MAC_LEN 4
+#define PICOPASS_KEY_LEN 8
+
+#define PICOPASS_FDT_LISTEN_FC (1000)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef enum {
+    PicopassErrorNone,
+    PicopassErrorTimeout,
+    PicopassErrorIncorrectCrc,
+    PicopassErrorProtocol,
+} PicopassError;
+
+typedef struct {
+    uint8_t data[RFAL_PICOPASS_UID_LEN];
+} PicopassColResSerialNum;
+
+typedef struct {
+    uint8_t data[RFAL_PICOPASS_UID_LEN];
+} PicopassSerialNum;
+
+typedef struct {
+    uint8_t data[PICOPASS_READ_CHECK_RESP_LEN];
+} PicopassReadCheckResp;
+
+typedef struct {
+    uint8_t data[PICOPASS_CHECK_RESP_LEN];
+} PicopassCheckResp;
+
+typedef struct {
+    uint8_t data[PICOPASS_MAC_LEN];
+} PicopassMac;
+
+#ifdef __cplusplus
+}
+#endif

+ 64 - 0
picopass/rfal_picopass.h

@@ -0,0 +1,64 @@
+#pragma once
+
+#include <furi_hal_nfc.h>
+
+#define RFAL_PICOPASS_UID_LEN 8
+#define 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[PICOPASS_BLOCK_LEN];
+    uint8_t crc[2];
+} rfalPicoPassReadBlockRes;

+ 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[PICOPASS_BLOCK_LEN] = {0};
+    memcpy(csn, AA1[PICOPASS_CSN_BLOCK_INDEX].data, PICOPASS_BLOCK_LEN);
+    for(uint8_t i = 0; i < 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 = PICOPASS_BLOCK_LEN - bytesLength; i < 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);
+}

+ 238 - 0
picopass/scenes/picopass_scene_elite_dict_attack.c

@@ -0,0 +1,238 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include "../picopass_keys.h"
+
+#define PICOPASS_SCENE_DICT_ATTACK_KEYS_BATCH_UPDATE (10)
+
+enum {
+    PicopassSceneEliteDictAttackDictEliteUser,
+    PicopassSceneEliteDictAttackDictStandart,
+    PicopassSceneEliteDictAttackDictElite,
+};
+
+const char* picopass_dict_name[] = {
+    [PicopassSceneEliteDictAttackDictEliteUser] = "Elite User Dictionary",
+    [PicopassSceneEliteDictAttackDictStandart] = "Standard System Dictionary",
+    [PicopassSceneEliteDictAttackDictElite] = "Elite System Dictionary",
+};
+
+static bool picopass_elite_dict_attack_change_dict(Picopass* picopass) {
+    bool success = false;
+
+    do {
+        uint32_t scene_state =
+            scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneEliteDictAttack);
+        nfc_dict_free(picopass->dict);
+        picopass->dict = NULL;
+        if(scene_state == PicopassSceneEliteDictAttackDictElite) break;
+        if(scene_state == PicopassSceneEliteDictAttackDictEliteUser) {
+            if(!nfc_dict_check_presence(PICOPASS_ICLASS_STANDARD_DICT_FLIPPER_NAME)) break;
+            picopass->dict = nfc_dict_alloc(
+                PICOPASS_ICLASS_STANDARD_DICT_FLIPPER_NAME,
+                NfcDictModeOpenExisting,
+                PICOPASS_KEY_LEN);
+            scene_state = PicopassSceneEliteDictAttackDictStandart;
+        } else if(scene_state == PicopassSceneEliteDictAttackDictStandart) {
+            if(!nfc_dict_check_presence(PICOPASS_ICLASS_ELITE_DICT_FLIPPER_NAME)) break;
+            picopass->dict = nfc_dict_alloc(
+                PICOPASS_ICLASS_ELITE_DICT_FLIPPER_NAME,
+                NfcDictModeOpenExisting,
+                PICOPASS_KEY_LEN);
+            scene_state = PicopassSceneEliteDictAttackDictElite;
+        }
+        picopass->dict_attack_ctx.card_detected = true;
+        picopass->dict_attack_ctx.total_keys = nfc_dict_get_total_keys(picopass->dict);
+        picopass->dict_attack_ctx.current_key = 0;
+        picopass->dict_attack_ctx.name = picopass_dict_name[scene_state];
+        scene_manager_set_scene_state(
+            picopass->scene_manager, PicopassSceneEliteDictAttack, scene_state);
+        success = true;
+    } while(false);
+
+    return success;
+}
+
+NfcCommand picopass_elite_dict_attack_worker_callback(PicopassPollerEvent event, void* context) {
+    furi_assert(context);
+    NfcCommand command = NfcCommandContinue;
+
+    Picopass* picopass = context;
+
+    if(event.type == PicopassPollerEventTypeRequestMode) {
+        event.data->req_mode.mode = PicopassPollerModeRead;
+    } else if(event.type == PicopassPollerEventTypeRequestKey) {
+        uint8_t key[PICOPASS_KEY_LEN] = {};
+        bool is_key_provided = true;
+        if(!nfc_dict_get_next_key(picopass->dict, key, PICOPASS_KEY_LEN)) {
+            if(picopass_elite_dict_attack_change_dict(picopass)) {
+                is_key_provided = nfc_dict_get_next_key(picopass->dict, key, PICOPASS_KEY_LEN);
+                view_dispatcher_send_custom_event(
+                    picopass->view_dispatcher, PicopassCustomEventDictAttackUpdateView);
+            } else {
+                is_key_provided = false;
+            }
+        }
+        uint32_t scene_state =
+            scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneEliteDictAttack);
+        memcpy(event.data->req_key.key, key, PICOPASS_KEY_LEN);
+        event.data->req_key.is_elite_key =
+            (scene_state != PicopassSceneEliteDictAttackDictStandart);
+        event.data->req_key.is_key_provided = is_key_provided;
+        if(is_key_provided) {
+            picopass->dict_attack_ctx.current_key++;
+            if(picopass->dict_attack_ctx.current_key %
+                   PICOPASS_SCENE_DICT_ATTACK_KEYS_BATCH_UPDATE ==
+               0) {
+                view_dispatcher_send_custom_event(
+                    picopass->view_dispatcher, PicopassCustomEventDictAttackUpdateView);
+            }
+        }
+    } else if(event.type == PicopassPollerEventTypeSuccess) {
+        const PicopassDeviceData* data = picopass_poller_get_data(picopass->poller);
+        memcpy(&picopass->dev->dev_data, data, sizeof(PicopassDeviceData));
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    } else if(event.type == PicopassPollerEventTypeFail) {
+        const PicopassDeviceData* data = picopass_poller_get_data(picopass->poller);
+        memcpy(&picopass->dev->dev_data, data, sizeof(PicopassDeviceData));
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    } else if(event.type == PicopassPollerEventTypeCardLost) {
+        picopass->dict_attack_ctx.card_detected = false;
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventDictAttackUpdateView);
+    } else if(event.type == PicopassPollerEventTypeCardDetected) {
+        picopass->dict_attack_ctx.card_detected = true;
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventDictAttackUpdateView);
+    }
+
+    return command;
+}
+
+static void picopass_scene_elite_dict_attack_update_view(Picopass* instance) {
+    if(instance->dict_attack_ctx.card_detected) {
+        dict_attack_set_card_detected(instance->dict_attack);
+        dict_attack_set_header(instance->dict_attack, instance->dict_attack_ctx.name);
+        dict_attack_set_total_dict_keys(
+            instance->dict_attack, instance->dict_attack_ctx.total_keys);
+        dict_attack_set_current_dict_key(
+            instance->dict_attack, instance->dict_attack_ctx.current_key);
+    } else {
+        dict_attack_set_card_removed(instance->dict_attack);
+    }
+}
+
+static void picopass_scene_elite_dict_attack_callback(void* context) {
+    Picopass* instance = context;
+
+    view_dispatcher_send_custom_event(
+        instance->view_dispatcher, PicopassCustomEventDictAttackSkip);
+}
+
+void picopass_scene_elite_dict_attack_on_enter(void* context) {
+    Picopass* picopass = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup dict attack context
+    uint32_t state = PicopassSceneEliteDictAttackDictEliteUser;
+
+    bool use_user_dict = nfc_dict_check_presence(PICOPASS_ICLASS_ELITE_DICT_USER_NAME);
+    if(use_user_dict) {
+        picopass->dict = nfc_dict_alloc(
+            PICOPASS_ICLASS_ELITE_DICT_USER_NAME, NfcDictModeOpenExisting, PICOPASS_KEY_LEN);
+        if(nfc_dict_get_total_keys(picopass->dict) == 0) {
+            nfc_dict_free(picopass->dict);
+            use_user_dict = false;
+        }
+    }
+    if(use_user_dict) {
+        state = PicopassSceneEliteDictAttackDictEliteUser;
+    } else {
+        picopass->dict = nfc_dict_alloc(
+            PICOPASS_ICLASS_STANDARD_DICT_FLIPPER_NAME, NfcDictModeOpenExisting, PICOPASS_KEY_LEN);
+        state = PicopassSceneEliteDictAttackDictStandart;
+    }
+    picopass->dict_attack_ctx.card_detected = true;
+    picopass->dict_attack_ctx.total_keys = nfc_dict_get_total_keys(picopass->dict);
+    picopass->dict_attack_ctx.current_key = 0;
+    picopass->dict_attack_ctx.name = picopass_dict_name[state];
+    scene_manager_set_scene_state(picopass->scene_manager, PicopassSceneEliteDictAttack, state);
+
+    // Setup view
+    picopass_scene_elite_dict_attack_update_view(picopass);
+    dict_attack_set_callback(
+        picopass->dict_attack, picopass_scene_elite_dict_attack_callback, picopass);
+
+    // Start worker
+    picopass->poller = picopass_poller_alloc(picopass->nfc);
+    picopass_poller_start(picopass->poller, picopass_elite_dict_attack_worker_callback, picopass);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewDictAttack);
+    picopass_blink_start(picopass);
+}
+
+bool picopass_scene_elite_dict_attack_on_event(void* context, SceneManagerEvent event) {
+    Picopass* picopass = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventPollerSuccess) {
+            if(memcmp(
+                   picopass->dev->dev_data.pacs.key,
+                   picopass_factory_debit_key,
+                   PICOPASS_BLOCK_LEN) == 0) {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadFactorySuccess);
+            } else {
+                scene_manager_next_scene(picopass->scene_manager, PicopassSceneReadCardSuccess);
+            }
+            consumed = true;
+        } else if(event.event == PicopassCustomEventDictAttackUpdateView) {
+            picopass_scene_elite_dict_attack_update_view(picopass);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventDictAttackSkip) {
+            uint32_t scene_state = scene_manager_get_scene_state(
+                picopass->scene_manager, PicopassSceneEliteDictAttack);
+            if(scene_state != PicopassSceneEliteDictAttackDictElite) {
+                picopass_elite_dict_attack_change_dict(picopass);
+                picopass_scene_elite_dict_attack_update_view(picopass);
+            } else {
+                if(memcmp(
+                       picopass->dev->dev_data.pacs.key,
+                       picopass_factory_debit_key,
+                       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_elite_dict_attack_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    if(picopass->dict) {
+        nfc_dict_free(picopass->dict);
+        picopass->dict = NULL;
+    }
+    picopass->dict_attack_ctx.current_key = 0;
+    picopass->dict_attack_ctx.total_keys = 0;
+
+    picopass_poller_stop(picopass->poller);
+    picopass_poller_free(picopass->poller);
+
+    // Clear view
+    popup_reset(picopass->popup);
+    scene_manager_set_scene_state(
+        picopass->scene_manager,
+        PicopassSceneEliteDictAttack,
+        PicopassSceneEliteDictAttackDictEliteUser);
+
+    picopass_blink_stop(picopass);
+}

+ 51 - 0
picopass/scenes/picopass_scene_emulate.c

@@ -0,0 +1,51 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+NfcCommand picopass_scene_listener_callback(PicopassListenerEvent event, void* context) {
+    UNUSED(event);
+    UNUSED(context);
+
+    return NfcCommandContinue;
+}
+
+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");
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewWidget);
+    picopass_blink_emulate_start(picopass);
+
+    picopass->listener = picopass_listener_alloc(picopass->nfc, &picopass->dev->dev_data);
+    picopass_listener_start(picopass->listener, picopass_scene_listener_callback, 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);
+    picopass_listener_stop(picopass->listener);
+    picopass_listener_free(picopass->listener);
+
+    // 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;
+
+    memcpy(picopass->write_key_context.key_to_write, picopass->byte_input_store, PICOPASS_KEY_LEN);
+    picopass->write_key_context.is_elite = true;
+    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,
+        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, "");
+}

+ 120 - 0
picopass/scenes/picopass_scene_key_menu.c

@@ -0,0 +1,120 @@
+#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->write_key_context.key_to_write, picopass_iclass_key, PICOPASS_KEY_LEN);
+            picopass->write_key_context.is_elite = 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->write_key_context.key_to_write, picopass_xice_key, PICOPASS_KEY_LEN);
+            picopass->write_key_context.is_elite = 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->write_key_context.key_to_write, picopass_xicl_key, PICOPASS_KEY_LEN);
+            picopass->write_key_context.is_elite = 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->write_key_context.key_to_write, picopass_xics_key, PICOPASS_KEY_LEN);
+            picopass->write_key_context.is_elite = 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);
+}

+ 103 - 0
picopass/scenes/picopass_scene_loclass.c

@@ -0,0 +1,103 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+static NfcCommand
+    picopass_scene_loclass_listener_callback(PicopassListenerEvent event, void* context) {
+    NfcCommand command = NfcCommandContinue;
+
+    Picopass* picopass = context;
+
+    if(event.type == PicopassListenerEventTypeLoclassGotMac) {
+        picopass->loclass_context.macs_collected++;
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventLoclassGotMac);
+    } else if(event.type == PicopassListenerEventTypeLoclassGotStandardKey) {
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventLoclassGotStandardKey);
+        command = NfcCommandStop;
+    }
+
+    return command;
+}
+
+static 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);
+    loclass_set_header(picopass->loclass, "Loclass");
+
+    picopass_blink_emulate_start(picopass);
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewLoclass);
+
+    PicopassDeviceData* data = malloc(sizeof(PicopassDeviceData));
+    const uint8_t config_block[PICOPASS_BLOCK_LEN] = {
+        0x12, 0xFF, 0xFF, 0xFF, 0x7F, 0x1F, 0xFF, 0x3C};
+    memcpy(data->AA1[PICOPASS_CONFIG_BLOCK_INDEX].data, config_block, sizeof(config_block));
+
+    const uint8_t epurse[PICOPASS_BLOCK_LEN] = {0xFE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+    memcpy(data->AA1[PICOPASS_SECURE_EPURSE_BLOCK_INDEX].data, epurse, sizeof(epurse));
+
+    const uint8_t aia[PICOPASS_BLOCK_LEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
+    memcpy(data->AA1[PICOPASS_SECURE_AIA_BLOCK_INDEX].data, aia, sizeof(aia));
+
+    picopass->listener = picopass_listener_alloc(picopass->nfc, data);
+    free(data);
+    if(picopass_listener_set_mode(picopass->listener, PicopassListenerModeLoclass)) {
+        picopass_listener_start(
+            picopass->listener, picopass_scene_loclass_listener_callback, picopass);
+    } else {
+        loclass_set_num_macs(picopass->loclass, 255);
+        loclass_set_header(picopass->loclass, "Error Opening Log File");
+        picopass_listener_free(picopass->listener);
+        picopass->listener = NULL;
+    }
+}
+
+bool picopass_scene_loclass_on_event(void* context, SceneManagerEvent event) {
+    bool consumed = false;
+    Picopass* picopass = context;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PicopassCustomEventLoclassGotMac) {
+            notification_message(picopass->notifications, &sequence_single_vibro);
+            loclass_set_num_macs(picopass->loclass, picopass->loclass_context.macs_collected);
+            if(picopass->loclass_context.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 == PicopassCustomEventLoclassGotStandardKey) {
+            loclass_set_header(picopass->loclass, "Loclass (Got Std Key)");
+            notification_message(picopass->notifications, &sequence_error);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventViewExit) {
+            consumed = scene_manager_previous_scene(picopass->scene_manager);
+        }
+    }
+
+    return consumed;
+}
+
+void picopass_scene_loclass_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    if(picopass->listener) {
+        picopass_listener_stop(picopass->listener);
+        picopass_listener_free(picopass->listener);
+    }
+    picopass->loclass_context.macs_collected = 0;
+    picopass_blink_stop(picopass);
+
+    loclass_reset(picopass->loclass);
+}

+ 126 - 0
picopass/scenes/picopass_scene_read_card.c

@@ -0,0 +1,126 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include "../picopass_keys.h"
+
+enum {
+    PicopassSceneReadCardDictStandart,
+    PicopassSceneReadCardDictElite,
+};
+
+static bool picopass_read_card_change_dict(Picopass* picopass) {
+    bool success = false;
+
+    do {
+        uint32_t scene_state =
+            scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneReadCard);
+        nfc_dict_free(picopass->dict);
+        picopass->dict = NULL;
+        if(scene_state == PicopassSceneReadCardDictElite) break;
+        if(!nfc_dict_check_presence(PICOPASS_ICLASS_ELITE_DICT_FLIPPER_NAME)) break;
+
+        picopass->dict = nfc_dict_alloc(
+            PICOPASS_ICLASS_ELITE_DICT_FLIPPER_NAME, NfcDictModeOpenExisting, PICOPASS_KEY_LEN);
+        scene_manager_set_scene_state(
+            picopass->scene_manager, PicopassSceneReadCard, PicopassSceneReadCardDictElite);
+        success = true;
+    } while(false);
+
+    return success;
+}
+
+NfcCommand picopass_read_card_worker_callback(PicopassPollerEvent event, void* context) {
+    furi_assert(context);
+    NfcCommand command = NfcCommandContinue;
+
+    Picopass* picopass = context;
+
+    if(event.type == PicopassPollerEventTypeRequestMode) {
+        event.data->req_mode.mode = PicopassPollerModeRead;
+    } else if(event.type == PicopassPollerEventTypeRequestKey) {
+        uint8_t key[PICOPASS_KEY_LEN] = {};
+        bool is_key_provided = true;
+        if(!nfc_dict_get_next_key(picopass->dict, key, PICOPASS_KEY_LEN)) {
+            if(picopass_read_card_change_dict(picopass)) {
+                is_key_provided = nfc_dict_get_next_key(picopass->dict, key, PICOPASS_KEY_LEN);
+            } else {
+                is_key_provided = false;
+            }
+        }
+        uint32_t scene_state =
+            scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneReadCard);
+        memcpy(event.data->req_key.key, key, PICOPASS_KEY_LEN);
+        event.data->req_key.is_elite_key = (scene_state == PicopassSceneReadCardDictElite);
+        event.data->req_key.is_key_provided = is_key_provided;
+    } else if(event.type == PicopassPollerEventTypeSuccess) {
+        const PicopassDeviceData* data = picopass_poller_get_data(picopass->poller);
+        memcpy(&picopass->dev->dev_data, data, sizeof(PicopassDeviceData));
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    } else if(event.type == PicopassPollerEventTypeFail) {
+        const PicopassDeviceData* data = picopass_poller_get_data(picopass->poller);
+        memcpy(&picopass->dev->dev_data, data, sizeof(PicopassDeviceData));
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    }
+
+    return command;
+}
+
+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);
+
+    picopass->dict = nfc_dict_alloc(
+        PICOPASS_ICLASS_STANDARD_DICT_FLIPPER_NAME, NfcDictModeOpenExisting, PICOPASS_KEY_LEN);
+    scene_manager_set_scene_state(
+        picopass->scene_manager, PicopassSceneReadCard, PicopassSceneReadCardDictStandart);
+    // Start worker
+    picopass->poller = picopass_poller_alloc(picopass->nfc);
+    picopass_poller_start(picopass->poller, picopass_read_card_worker_callback, picopass);
+
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+    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 == PicopassCustomEventPollerSuccess) {
+            if(memcmp(
+                   picopass->dev->dev_data.pacs.key,
+                   picopass_factory_debit_key,
+                   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;
+
+    if(picopass->dict) {
+        nfc_dict_free(picopass->dict);
+        picopass->dict = NULL;
+    }
+    picopass_poller_stop(picopass->poller);
+    picopass_poller_free(picopass->poller);
+
+    // Clear view
+    popup_reset(picopass->popup);
+    scene_manager_set_scene_state(
+        picopass->scene_manager, PicopassSceneReadCard, PicopassSceneReadCardDictStandart);
+
+    picopass_blink_stop(picopass);
+}

+ 182 - 0
picopass/scenes/picopass_scene_read_card_success.c

@@ -0,0 +1,182 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include <picopass_keys.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[PICOPASS_BLOCK_LEN] = {0};
+    memcpy(csn, AA1[PICOPASS_CSN_BLOCK_INDEX].data, PICOPASS_BLOCK_LEN);
+    for(uint8_t i = 0; i < 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, PICOPASS_BLOCK_LEN);
+    bool empty = picopass_is_memset(
+        AA1[PICOPASS_ICLASS_PACS_CFG_BLOCK_INDEX].data, 0xFF, PICOPASS_BLOCK_LEN);
+
+    if(no_key) {
+        furi_string_cat_printf(wiegand_str, "Read Failed");
+        bool hid_csn = picopass_device_hid_csn(picopass->dev);
+
+        if(pacs->se_enabled) {
+            furi_string_cat_printf(credential_str, "SE enabled");
+        } else if(!hid_csn) {
+            furi_string_cat_printf(credential_str, "Non-HID CSN");
+        }
+
+        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 = PICOPASS_BLOCK_LEN - bytesLength; i < 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[PICOPASS_BLOCK_LEN];
+            memcpy(key, &pacs->key, PICOPASS_BLOCK_LEN);
+
+            bool standard_key = true;
+            // Handle DES key being 56bits with parity in LSB
+            for(uint8_t i = 0; i < 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 < 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);
+}

+ 82 - 0
picopass/scenes/picopass_scene_read_factory_success.c

@@ -0,0 +1,82 @@
+#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->write_key_context.key_to_write, picopass_iclass_key, PICOPASS_BLOCK_LEN);
+            picopass->write_key_context.is_elite = false;
+            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);
+}

+ 88 - 0
picopass/scenes/picopass_scene_write_card.c

@@ -0,0 +1,88 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+#include "../picopass_keys.h"
+
+#define PICOPASS_SCENE_WRITE_BLOCK_START 6
+#define PICOPASS_SCENE_WRITE_BLOCK_STOP 10
+
+NfcCommand picopass_scene_write_poller_callback(PicopassPollerEvent event, void* context) {
+    Picopass* picopass = context;
+    NfcCommand command = NfcCommandContinue;
+
+    if(event.type == PicopassPollerEventTypeRequestMode) {
+        event.data->req_mode.mode = PicopassPollerModeWrite;
+    } else if(event.type == PicopassPollerEventTypeRequestKey) {
+        memcpy(event.data->req_key.key, picopass_iclass_key, sizeof(picopass_iclass_key));
+        event.data->req_key.is_elite_key = false;
+        event.data->req_key.is_key_provided = true;
+    } else if(event.type == PicopassPollerEventTypeRequestWriteBlock) {
+        uint8_t block_num =
+            scene_manager_get_scene_state(picopass->scene_manager, PicopassSceneWriteCard);
+        if(block_num == PICOPASS_SCENE_WRITE_BLOCK_STOP) {
+            event.data->req_write.perform_write = false;
+        } else {
+            event.data->req_write.block_num = block_num;
+            event.data->req_write.block = &picopass->dev->dev_data.AA1[block_num];
+            event.data->req_write.perform_write = true;
+            block_num++;
+            scene_manager_set_scene_state(
+                picopass->scene_manager, PicopassSceneWriteCard, block_num);
+        }
+    } else if(event.type == PicopassPollerEventTypeSuccess) {
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    } else if(event.type == PicopassPollerEventTypeFail) {
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerFail);
+    }
+
+    return command;
+}
+
+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);
+    scene_manager_set_scene_state(
+        picopass->scene_manager, PicopassSceneWriteCard, PICOPASS_SCENE_WRITE_BLOCK_START);
+
+    // Start worker
+    view_dispatcher_switch_to_view(picopass->view_dispatcher, PicopassViewPopup);
+
+    picopass->poller = picopass_poller_alloc(picopass->nfc);
+    picopass_poller_start(picopass->poller, picopass_scene_write_poller_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 == PicopassCustomEventPollerFail) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardFailure);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventPollerSuccess) {
+            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_poller_stop(picopass->poller);
+    picopass_poller_free(picopass->poller);
+    // 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);
+}

+ 76 - 0
picopass/scenes/picopass_scene_write_key.c

@@ -0,0 +1,76 @@
+#include "../picopass_i.h"
+#include <dolphin/dolphin.h>
+
+NfcCommand picopass_scene_write_key_poller_callback(PicopassPollerEvent event, void* context) {
+    NfcCommand command = NfcCommandContinue;
+    Picopass* picopass = context;
+
+    if(event.type == PicopassPollerEventTypeRequestMode) {
+        event.data->req_mode.mode = PicopassPollerModeWriteKey;
+    } else if(event.type == PicopassPollerEventTypeRequestKey) {
+        event.data->req_key.is_key_provided = true;
+        memcpy(event.data->req_key.key, picopass->dev->dev_data.pacs.key, PICOPASS_KEY_LEN);
+        event.data->req_key.is_elite_key = picopass->dev->dev_data.pacs.elite_kdf;
+    } else if(event.type == PicopassPollerEventTypeRequestWriteKey) {
+        event.data->req_write_key.data = &picopass->dev->dev_data;
+        memcpy(
+            event.data->req_write_key.key,
+            picopass->write_key_context.key_to_write,
+            PICOPASS_KEY_LEN);
+        event.data->req_write_key.is_elite_key = picopass->write_key_context.is_elite;
+    } else if(event.type == PicopassPollerEventTypeSuccess) {
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerSuccess);
+    } else if(event.type == PicopassPollerEventTypeFail) {
+        view_dispatcher_send_custom_event(
+            picopass->view_dispatcher, PicopassCustomEventPollerFail);
+    }
+
+    return command;
+}
+
+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_blink_start(picopass);
+
+    picopass->poller = picopass_poller_alloc(picopass->nfc);
+    picopass_poller_start(picopass->poller, picopass_scene_write_key_poller_callback, 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 == PicopassCustomEventPollerSuccess) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardSuccess);
+            consumed = true;
+        } else if(event.event == PicopassCustomEventPollerFail) {
+            scene_manager_next_scene(picopass->scene_manager, PicopassSceneWriteCardFailure);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void picopass_scene_write_key_on_exit(void* context) {
+    Picopass* picopass = context;
+
+    // Stop worker
+    picopass_poller_stop(picopass->poller);
+    picopass_poller_free(picopass->poller);
+
+    // Clear view
+    popup_reset(picopass->popup);
+
+    picopass_blink_stop(picopass);
+}

+ 288 - 0
picopass/views/dict_attack.c

@@ -0,0 +1,288 @@
+#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;
+    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->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_set_current_dict_key(DictAttack* dict_attack, uint16_t current_key) {
+    furi_assert(dict_attack);
+    with_view_model(
+        dict_attack->view,
+        DictAttackViewModel * model,
+        { model->dict_keys_current = current_key; },
+        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>
+
+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_set_current_dict_key(DictAttack* dict_attack, uint16_t current_key);
+
+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);

+ 111 - 0
picopass/views/loclass.c

@@ -0,0 +1,111 @@
+#include "loclass.h"
+
+#include <picopass_device.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);