Eric Betts 10 месяцев назад
Сommit
1b7cc79438
79 измененных файлов с 7499 добавлено и 0 удалено
  1. BIN
      .catalog/screenshots/menu.png
  2. BIN
      .catalog/screenshots/read_success.png
  3. 246 0
      .clang-format
  4. 2 0
      .gitattributes
  5. 41 0
      .github/workflows/build.yml
  6. 4 0
      .gitignore
  7. 2 0
      CHANGELOG.md
  8. 57 0
      README.md
  9. 110 0
      aes_cmac.c
  10. 11 0
      aes_cmac.h
  11. 19 0
      application.fam
  12. 17 0
      boards/nrf52840dongle_nrf52840.overlay
  13. 108 0
      des_cmac.c
  14. 11 0
      des_cmac.h
  15. 14 0
      files/seos.nfc
  16. 0 0
      images/.gitkeep
  17. BIN
      images/DolphinMafia_115x62.png
  18. BIN
      images/DolphinNice_96x59.png
  19. BIN
      images/Nfc_10px.png
  20. BIN
      images/RFIDDolphinReceive_97x61.png
  21. BIN
      images/RFIDDolphinSend_97x61.png
  22. 7 0
      keys-example.txt
  23. 28 0
      keys.c
  24. 7 0
      keys.h
  25. 60 0
      memmem.c
  26. 30 0
      scenes/seos_scene.c
  27. 29 0
      scenes/seos_scene.h
  28. 45 0
      scenes/seos_scene_about.c
  29. 85 0
      scenes/seos_scene_ble_device.c
  30. 78 0
      scenes/seos_scene_ble_peripheral.c
  31. 16 0
      scenes/seos_scene_config.h
  32. 52 0
      scenes/seos_scene_delete.c
  33. 40 0
      scenes/seos_scene_delete_success.c
  34. 70 0
      scenes/seos_scene_emulate.c
  35. 25 0
      scenes/seos_scene_file_select.c
  36. 109 0
      scenes/seos_scene_info.c
  37. 56 0
      scenes/seos_scene_read.c
  38. 64 0
      scenes/seos_scene_read_error.c
  39. 109 0
      scenes/seos_scene_read_success.c
  40. 79 0
      scenes/seos_scene_save_name.c
  41. 42 0
      scenes/seos_scene_save_success.c
  42. 97 0
      scenes/seos_scene_saved_menu.c
  43. 61 0
      scenes/seos_scene_scanner_menu.c
  44. 137 0
      scenes/seos_scene_start.c
  45. 349 0
      secure_messaging.c
  46. 42 0
      secure_messaging.h
  47. 301 0
      seos.c
  48. 5 0
      seos.h
  49. BIN
      seos.png
  50. 492 0
      seos_att.c
  51. 100 0
      seos_att.h
  52. 4 0
      seos_att_i.h
  53. 176 0
      seos_central.c
  54. 33 0
      seos_central.h
  55. 4 0
      seos_central_i.h
  56. 348 0
      seos_characteristic.c
  57. 36 0
      seos_characteristic.h
  58. 4 0
      seos_characteristic_i.h
  59. 145 0
      seos_common.c
  60. 110 0
      seos_common.h
  61. 652 0
      seos_emulator.c
  62. 61 0
      seos_emulator.h
  63. 5 0
      seos_emulator_i.h
  64. 778 0
      seos_hci.c
  65. 68 0
      seos_hci.h
  66. 484 0
      seos_hci_h5.c
  67. 82 0
      seos_hci_h5.h
  68. 4 0
      seos_hci_h5_i.h
  69. 4 0
      seos_hci_i.h
  70. 123 0
      seos_i.h
  71. 235 0
      seos_l2cap.c
  72. 44 0
      seos_l2cap.h
  73. 4 0
      seos_l2cap_i.h
  74. 475 0
      seos_reader.c
  75. 45 0
      seos_reader.h
  76. 5 0
      seos_reader_i.h
  77. 234 0
      uart.c
  78. 24 0
      uart.h
  79. 55 0
      uart_i.h

BIN
.catalog/screenshots/menu.png


BIN
.catalog/screenshots/read_success.png


+ 246 - 0
.clang-format

@@ -0,0 +1,246 @@
+---
+Language:        Cpp
+AccessModifierOffset: -4
+AlignAfterOpenBracket: AlwaysBreak
+AlignArrayOfStructures: None
+AlignConsecutiveAssignments:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveBitFields:
+  Enabled:         true
+  AcrossEmptyLines: true
+  AcrossComments:  true
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveDeclarations:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCompound:   false
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveMacros:
+  Enabled:         true
+  AcrossEmptyLines: false
+  AcrossComments:  true
+  AlignCompound:   true
+  AlignFunctionPointers: false
+  PadOperators:    true
+AlignConsecutiveShortCaseStatements:
+  Enabled:         false
+  AcrossEmptyLines: false
+  AcrossComments:  false
+  AlignCaseColons: false
+AlignEscapedNewlines: Left
+AlignOperands:   Align
+AlignTrailingComments:
+  Kind:            Never
+  OverEmptyLines:  0
+AllowAllArgumentsOnNextLine: true
+AllowAllParametersOfDeclarationOnNextLine: false
+AllowBreakBeforeNoexceptSpecifier: Never
+AllowShortBlocksOnASingleLine: Never
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortCompoundRequirementOnASingleLine: true
+AllowShortEnumsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: None
+AllowShortIfStatementsOnASingleLine: WithoutElse
+AllowShortLambdasOnASingleLine: All
+AllowShortLoopsOnASingleLine: false
+AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: None
+AlwaysBreakBeforeMultilineStrings: false
+AlwaysBreakTemplateDeclarations: Yes
+AttributeMacros:
+  - __capability
+BinPackArguments: false
+BinPackParameters: false
+BitFieldColonSpacing: Both
+BraceWrapping:
+  AfterCaseLabel:  false
+  AfterClass:      false
+  AfterControlStatement: Never
+  AfterEnum:       false
+  AfterExternBlock: false
+  AfterFunction:   false
+  AfterNamespace:  false
+  AfterObjCDeclaration: false
+  AfterStruct:     false
+  AfterUnion:      false
+  BeforeCatch:     false
+  BeforeElse:      false
+  BeforeLambdaBody: false
+  BeforeWhile:     false
+  IndentBraces:    false
+  SplitEmptyFunction: true
+  SplitEmptyRecord: true
+  SplitEmptyNamespace: true
+BreakAdjacentStringLiterals: true
+BreakAfterAttributes: Leave
+BreakAfterJavaFieldAnnotations: false
+BreakArrays:     true
+BreakBeforeBinaryOperators: None
+BreakBeforeConceptDeclarations: Always
+BreakBeforeBraces: Attach
+BreakBeforeInlineASMColon: OnlyMultiline
+BreakBeforeTernaryOperators: false
+BreakConstructorInitializers: BeforeComma
+BreakInheritanceList: BeforeColon
+BreakStringLiterals: false
+ColumnLimit:     99
+CommentPragmas:  '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DerivePointerAlignment: false
+DisableFormat:   false
+EmptyLineAfterAccessModifier: Never
+EmptyLineBeforeAccessModifier: LogicalBlock
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: false
+ForEachMacros:
+  - foreach
+  - Q_FOREACH
+  - BOOST_FOREACH
+  - M_EACH
+IfMacros:
+  - KJ_IF_MAYBE
+IncludeBlocks:   Preserve
+IncludeCategories:
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '^(<|"(gtest|gmock|isl|json)/)'
+    Priority:        3
+    SortPriority:    0
+    CaseSensitive:   false
+  - Regex:           '.*'
+    Priority:        1
+    SortPriority:    0
+    CaseSensitive:   false
+IncludeIsMainRegex: '(Test)?$'
+IncludeIsMainSourceRegex: ''
+IndentAccessModifiers: false
+IndentCaseBlocks: false
+IndentCaseLabels: false
+IndentExternBlock: AfterExternBlock
+IndentGotoLabels: true
+IndentPPDirectives: None
+IndentRequiresClause: false
+IndentWidth:     4
+IndentWrappedFunctionNames: true
+InsertBraces:    false
+InsertNewlineAtEOF: true
+InsertTrailingCommas: None
+IntegerLiteralSeparator:
+  Binary:          0
+  BinaryMinDigits: 0
+  Decimal:         0
+  DecimalMinDigits: 0
+  Hex:             0
+  HexMinDigits:    0
+JavaScriptQuotes: Leave
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+KeepEmptyLinesAtEOF: false
+LambdaBodyIndentation: Signature
+LineEnding:      DeriveLF
+MacroBlockBegin: ''
+MacroBlockEnd:   ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Auto
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: true
+ObjCSpaceBeforeProtocolList: true
+PackConstructorInitializers: BinPack
+PenaltyBreakAssignment: 10
+PenaltyBreakBeforeFirstCallParameter: 30
+PenaltyBreakComment: 10
+PenaltyBreakFirstLessLess: 0
+PenaltyBreakOpenParenthesis: 0
+PenaltyBreakScopeResolution: 500
+PenaltyBreakString: 10
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 100
+PenaltyIndentedWhitespace: 0
+PenaltyReturnTypeOnItsOwnLine: 60
+PointerAlignment: Left
+PPIndentWidth:   -1
+QualifierAlignment: Leave
+ReferenceAlignment: Pointer
+ReflowComments:  false
+RemoveBracesLLVM: false
+RemoveParentheses: Leave
+RemoveSemicolon: true
+RequiresClausePosition: OwnLine
+RequiresExpressionIndentation: OuterScope
+SeparateDefinitionBlocks: Leave
+ShortNamespaceLines: 1
+SkipMacroDefinitionBody: false
+SortIncludes:    Never
+SortJavaStaticImport: Before
+SortUsingDeclarations: Never
+SpaceAfterCStyleCast: false
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: true
+SpaceAroundPointerQualifiers: Default
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCaseColon: false
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeJsonColon: false
+SpaceBeforeParens: Never
+SpaceBeforeParensOptions:
+  AfterControlStatements: false
+  AfterForeachMacros: false
+  AfterFunctionDefinitionName: false
+  AfterFunctionDeclarationName: false
+  AfterIfMacros:   false
+  AfterOverloadedOperator: false
+  AfterPlacementOperator: true
+  AfterRequiresInClause: false
+  AfterRequiresInExpression: false
+  BeforeNonEmptyParentheses: false
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceBeforeSquareBrackets: false
+SpaceInEmptyBlock: false
+SpacesBeforeTrailingComments: 1
+SpacesInAngles:  Never
+SpacesInContainerLiterals: false
+SpacesInLineCommentPrefix:
+  Minimum:         1
+  Maximum:         -1
+SpacesInParens:  Never
+SpacesInParensOptions:
+  InCStyleCasts:   false
+  InConditionalStatements: false
+  InEmptyParentheses: false
+  Other:           false
+SpacesInSquareBrackets: false
+Standard:        c++20
+StatementAttributeLikeMacros:
+  - Q_EMIT
+StatementMacros:
+  - Q_UNUSED
+  - QT_REQUIRE_VERSION
+TabWidth:        4
+UseTab:          Never
+VerilogBreakBetweenInstancePorts: true
+WhitespaceSensitiveMacros:
+  - STRINGIZE
+  - PP_STRINGIZE
+  - BOOST_PP_STRINGIZE
+  - NS_SWIFT_NAME
+  - CF_SWIFT_NAME
+...
+

+ 2 - 0
.gitattributes

@@ -0,0 +1,2 @@
+*.mkv filter=lfs diff=lfs merge=lfs -text
+*.mp4 filter=lfs diff=lfs merge=lfs -text

+ 41 - 0
.github/workflows/build.yml

@@ -0,0 +1,41 @@
+name: "FAP: Build for multiple SDK sources"
+# This will build your app for dev and release channels on GitHub. 
+# It will also build your app every day to make sure it's up to date with the latest SDK changes.
+# See https://github.com/marketplace/actions/build-flipper-application-package-fap for more information
+
+on:
+  push:
+    ## put your main branch name under "branches"
+    #branches: 
+    #  - master 
+  pull_request:
+  schedule: 
+    # do a build every day
+    - cron: "1 1 * * *"
+
+jobs:
+  ufbt-build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        include:
+          - name: dev channel
+            sdk-channel: dev
+          - name: release channel
+            sdk-channel: release
+          # You can add unofficial channels here. See ufbt action docs for more info.
+    name: 'ufbt: Build for ${{ matrix.name }}'
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v4
+      - name: Build with ufbt
+        uses: flipperdevices/flipperzero-ufbt-action@v0.1
+        id: build-app
+        with:
+          sdk-channel: ${{ matrix.sdk-channel }}
+      - name: Upload app artifacts
+        uses: actions/upload-artifact@v3
+        with:
+          # See ufbt action docs for other output variables
+          name: ${{ github.event.repository.name }}-${{ steps.build-app.outputs.suffix }}
+          path: ${{ steps.build-app.outputs.fap-artifacts }}

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+dist
+.vscode
+keys.txt
+*_keys.txt

+ 2 - 0
CHANGELOG.md

@@ -0,0 +1,2 @@
+## 1.0
+ - Public release

+ 57 - 0
README.md

@@ -0,0 +1,57 @@
+# Flipper Seos
+
+Flipper app for reading and emulating Seos-compatible cards/fobs/mobile credentials.
+
+![Demo Video](demo.mp4)
+
+## To do:
+
+- Fix iso14443a-4 framing
+- ASN.1 for serializing/deserializing
+- Support for larger message wrapping/unwrapping
+- CMAC checking where I missed it
+
+## Keys
+
+The app uses all 00 keys by default. If you'd like to use your own keys, use the format of the `keys-example.txt` to specify them, and place into `SD Card/apps_data/seos/keys.txt`
+
+## Hardware for BLE support
+
+1. Install/setup nordic SDK
+1. Install Toolchain manager
+1. Launch Toolchain manager
+1. Next to SDK version click down arroy and "open terminal"
+1. navigate to `samples/bluetooth/hci_uart_3wire`
+
+### nRF52840
+
+1. Edit `boards/nrf52840dk_nrf52840.overlay` and change current-speed to 460800 to match Flipper app.
+
+1. `west build -b nrf52840dk_nrf52840 -p auto`
+1. `west flash`
+
+### nRF52840 dongle
+
+1. Copy `boards/nrf52840dongle_nrf52840.overlay` to `hci_uart_3wire`
+1. Might need to: `nrfutil install nrf5sdk-tools`
+1. `west build -b nrf52840dongle_nrf52840 -p auto`
+1. `nrfutil nrf5sdk-tools pkg generate --hw-version 52 --sd-req=0x00  --application ./build/hci_uart_3wire/zephyr/zephyr.hex --application-version 1 app.zip`
+1. Put dongle into DFU by pressing 'reset' button
+1. `nrfutil nrf5sdk-tools dfu usb-serial -pkg app.zip -p /dev/cu.usbmodemD39BF26162261`
+
+### Connection
+
+| flipper purpose | pin | color  | nRF52840 dk pin | nRF52840 dongle pin |
+| --------------- | --- | ------ | --------------- | ------------------- |
+| rx              | 16  | yellow | P0.06           | P0.20               |
+| tx              | 15  | orange | P0.08           | P0.24               |
+| gnd             | 11  | black  | any ground      | GND                 |
+| power           | 5v  | red    | VIN 3-5v        | VBUS                |
+
+## Note
+
+This software incorporates a third-party implementation of Seos™ technology. It is not developed, authorized, licensed, or endorsed by HID Global, ASSA ABLOY, or any of their affiliates. References to Seos™ are solely for descriptive and compatibility purposes.
+
+No guarantee of compatibility or functionality is made. This implementation may not work with all Seos™-enabled systems, and its performance, security, and reliability are not assured. Users assume all risks associated with its use.
+
+Seos™, HID Global, and ASSA ABLOY are trademarks or registered trademarks of their respective owners. This software is not associated with or sponsored by them in any way.

+ 110 - 0
aes_cmac.c

@@ -0,0 +1,110 @@
+#include "aes_cmac.h"
+
+#define BLOCK_SIZE 16
+
+#define TAG "AESCMAC"
+
+static uint8_t zeroes[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static uint8_t Rb[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x87};
+
+void aes_cmac_padBlock(uint8_t* block, size_t len) {
+    block[len] = 0x80;
+}
+
+bool aes_cmac_aes(uint8_t* key, uint8_t* plain, size_t plain_len, uint8_t* enc) {
+    uint8_t iv[BLOCK_SIZE];
+    memset(iv, 0, BLOCK_SIZE);
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, key, BLOCK_SIZE * 8);
+    int rtn = mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, plain_len, iv, plain, enc);
+    mbedtls_aes_free(&ctx);
+
+    return rtn == 0;
+}
+
+void aes_cmac_bitShiftLeft(uint8_t* input, uint8_t* output, size_t len) {
+    size_t last = len - 1;
+    for(size_t i = 0; i < last; i++) {
+        output[i] = input[i] << 1;
+        if(input[i + 1] & 0x80) {
+            output[i] += 0x01;
+        }
+    }
+    output[last] = input[last] << 1;
+}
+
+// x = a ^ b
+void aes_cmac_xor(uint8_t* a, uint8_t* b, uint8_t* x, size_t len) {
+    for(size_t i = 0; i < len; i++) {
+        x[i] = a[i] ^ b[i];
+    }
+}
+
+bool aes_cmac_generateSubkeys(uint8_t* key, uint8_t* subkey1, uint8_t* subkey2) {
+    uint8_t l[BLOCK_SIZE] = {0};
+    aes_cmac_aes(key, zeroes, BLOCK_SIZE, l);
+
+    aes_cmac_bitShiftLeft(l, subkey1, BLOCK_SIZE);
+    if(l[0] & 0x80) {
+        aes_cmac_xor(subkey1, Rb, subkey1, BLOCK_SIZE);
+    }
+
+    aes_cmac_bitShiftLeft(subkey1, subkey2, BLOCK_SIZE);
+    if(subkey1[0] & 0x80) {
+        aes_cmac_xor(subkey2, Rb, subkey2, BLOCK_SIZE);
+    }
+
+    return true;
+}
+
+bool aes_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac) {
+    uint8_t subkey1[BLOCK_SIZE] = {0};
+    uint8_t subkey2[BLOCK_SIZE] = {0};
+    uint8_t blockCount = (message_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
+    bool lastBlockCompleteFlag;
+    uint8_t lastBlockIndex;
+    uint8_t lastBlock[BLOCK_SIZE] = {0};
+
+    // Only support key length of 16 bytes
+    if(key_len != BLOCK_SIZE) {
+        return false;
+    }
+
+    aes_cmac_generateSubkeys(key, subkey1, subkey2);
+
+    if(blockCount == 0) {
+        blockCount = 1;
+        lastBlockCompleteFlag = false;
+    } else {
+        lastBlockCompleteFlag = (message_len % BLOCK_SIZE == 0);
+    }
+    lastBlockIndex = blockCount - 1;
+
+    if(lastBlockCompleteFlag) {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), BLOCK_SIZE);
+        aes_cmac_xor(lastBlock, subkey1, lastBlock, BLOCK_SIZE);
+    } else {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), message_len % BLOCK_SIZE);
+        aes_cmac_padBlock(lastBlock, message_len % BLOCK_SIZE);
+        aes_cmac_xor(lastBlock, subkey2, lastBlock, BLOCK_SIZE);
+    }
+
+    uint8_t x[BLOCK_SIZE];
+    uint8_t y[BLOCK_SIZE];
+    memset(x, 0, sizeof(x));
+    memset(y, 0, sizeof(y));
+
+    for(size_t i = 0; i < lastBlockIndex; i++) {
+        aes_cmac_xor(x, message + (i * BLOCK_SIZE), y, BLOCK_SIZE);
+        aes_cmac_aes(key, y, BLOCK_SIZE, x);
+    }
+
+    aes_cmac_xor(x, lastBlock, y, BLOCK_SIZE);
+
+    bool success = aes_cmac_aes(key, y, BLOCK_SIZE, cmac);
+
+    return success;
+}

+ 11 - 0
aes_cmac.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <mbedtls/aes.h>
+
+#include <furi.h>
+
+bool aes_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac);

+ 19 - 0
application.fam

@@ -0,0 +1,19 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="seos",  # Must be unique
+    name="Seos compatible",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="seos_app",
+    stack_size=10 * 1024,
+    fap_category="NFC",
+    # Optional values
+    fap_version="1.0",
+    fap_icon="seos.png",  # 10x10 1-bit PNG
+    fap_description="Seos compatible reader/emulator",
+    fap_author="bettse",
+    fap_weburl="https://gitlab.com/bettse/flipper_seos",
+    fap_icon_assets="images",  # Image assets to compile for this application
+    fap_file_assets="files",
+    fap_libs=["mbedtls"],
+)

+ 17 - 0
boards/nrf52840dongle_nrf52840.overlay

@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: Apache-2.0 */
+
+&uart0 {
+	compatible = "nordic,nrf-uart";
+	current-speed = <460800>;
+	status = "okay";
+};
+
+/ {
+	chosen {
+                zephyr,console = &uart0;
+                zephyr,shell-uart = &uart0;
+                zephyr,uart-mcumgr = &uart0;
+                zephyr,bt-mon-uart = &uart0;
+                zephyr,bt-c2h-uart = &uart0;
+	};
+};

+ 108 - 0
des_cmac.c

@@ -0,0 +1,108 @@
+#include "des_cmac.h"
+
+#define BLOCK_SIZE 8
+
+#define TAG "DESCMAC"
+
+static uint8_t zeroes[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+static uint8_t Rb[] = {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1b};
+
+void des_cmac_padBlock(uint8_t* block, size_t len) {
+    block[len] = 0x80;
+}
+
+bool des_cmac_des3(uint8_t* key, uint8_t* plain, size_t plain_len, uint8_t* enc) {
+    uint8_t iv[BLOCK_SIZE];
+    memset(iv, 0, BLOCK_SIZE);
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, key);
+    int rtn = mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, plain_len, iv, plain, enc);
+    mbedtls_des3_free(&ctx);
+
+    return rtn == 0;
+}
+
+void des_cmac_bitShiftLeft(uint8_t* input, uint8_t* output, size_t len) {
+    size_t last = len - 1;
+    for(size_t i = 0; i < last; i++) {
+        output[i] = input[i] << 1;
+        if(input[i + 1] & 0x80) {
+            output[i] += 0x01;
+        }
+    }
+    output[last] = input[last] << 1;
+}
+
+// x = a ^ b
+void des_cmac_xor(uint8_t* a, uint8_t* b, uint8_t* x, size_t len) {
+    for(size_t i = 0; i < len; i++) {
+        x[i] = a[i] ^ b[i];
+    }
+}
+
+bool des_cmac_generateSubkeys(uint8_t* key, uint8_t* subkey1, uint8_t* subkey2) {
+    uint8_t l[BLOCK_SIZE] = {0};
+    des_cmac_des3(key, zeroes, BLOCK_SIZE, l);
+
+    des_cmac_bitShiftLeft(l, subkey1, BLOCK_SIZE);
+    if(l[0] & 0x80) {
+        des_cmac_xor(subkey1, Rb, subkey1, BLOCK_SIZE);
+    }
+
+    des_cmac_bitShiftLeft(subkey1, subkey2, BLOCK_SIZE);
+    if(subkey1[0] & 0x80) {
+        des_cmac_xor(subkey2, Rb, subkey2, BLOCK_SIZE);
+    }
+
+    return true;
+}
+
+bool des_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac) {
+    uint8_t subkey1[BLOCK_SIZE] = {0};
+    uint8_t subkey2[BLOCK_SIZE] = {0};
+    uint8_t blockCount = (message_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
+    bool lastBlockCompleteFlag;
+    uint8_t lastBlockIndex;
+    uint8_t lastBlock[BLOCK_SIZE] = {0};
+
+    // Only support key length of 16 bytes
+    if(key_len != 16) {
+        return false;
+    }
+
+    des_cmac_generateSubkeys(key, subkey1, subkey2);
+
+    if(blockCount == 0) {
+        blockCount = 1;
+        lastBlockCompleteFlag = false;
+    } else {
+        lastBlockCompleteFlag = (message_len % BLOCK_SIZE == 0);
+    }
+    lastBlockIndex = blockCount - 1;
+
+    if(lastBlockCompleteFlag) {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), BLOCK_SIZE);
+        des_cmac_xor(lastBlock, subkey1, lastBlock, BLOCK_SIZE);
+    } else {
+        memcpy(lastBlock, message + (lastBlockIndex * BLOCK_SIZE), message_len % BLOCK_SIZE);
+        des_cmac_padBlock(lastBlock, message_len % BLOCK_SIZE);
+        des_cmac_xor(lastBlock, subkey2, lastBlock, BLOCK_SIZE);
+    }
+
+    uint8_t x[BLOCK_SIZE];
+    uint8_t y[BLOCK_SIZE];
+    memset(x, 0, sizeof(x));
+    memset(y, 0, sizeof(y));
+
+    for(size_t i = 0; i < lastBlockIndex; i++) {
+        des_cmac_xor(x, message + (i * BLOCK_SIZE), y, BLOCK_SIZE);
+        des_cmac_des3(key, y, BLOCK_SIZE, x);
+    }
+
+    des_cmac_xor(x, lastBlock, y, BLOCK_SIZE);
+
+    bool success = des_cmac_des3(key, y, BLOCK_SIZE, cmac);
+
+    return success;
+}

+ 11 - 0
des_cmac.h

@@ -0,0 +1,11 @@
+#pragma once
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <mbedtls/des.h>
+
+#include <furi.h>
+
+bool des_cmac(uint8_t* key, size_t key_len, uint8_t* message, size_t message_len, uint8_t* cmac);

+ 14 - 0
files/seos.nfc

@@ -0,0 +1,14 @@
+Filetype: Flipper NFC device
+Version: 4
+# Device type can be ISO14443-3A, ISO14443-3B, ISO14443-4A, ISO14443-4B, ISO15693-3, FeliCa, NTAG/Ultralight, Mifare Classic, Mifare DESFire, SLIX, ST25TB
+Device type: ISO14443-4A
+# UID is common for all formats
+UID: 08 22 F3 5A
+# ISO14443-3A specific data
+ATQA: 00 04
+SAK: 20
+# ISO14443-4A specific data
+T0: 78
+TA(1): 77
+TB(1): 94
+TC(1): 02

+ 0 - 0
images/.gitkeep


BIN
images/DolphinMafia_115x62.png


BIN
images/DolphinNice_96x59.png


BIN
images/Nfc_10px.png


BIN
images/RFIDDolphinReceive_97x61.png


BIN
images/RFIDDolphinSend_97x61.png


+ 7 - 0
keys-example.txt

@@ -0,0 +1,7 @@
+Filetype: Seos keys
+Version: 1
+SEOS_ADF_OID_LEN: 17
+SEOS_ADF_OID: 2b 06 01 04 01 81 e4 38 01 01 02 01 18 01 01 02 02
+SEOS_ADF1_PRIV_ENC: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+SEOS_ADF1_PRIV_MAC: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
+SEOS_ADF1_READ: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

+ 28 - 0
keys.c

@@ -0,0 +1,28 @@
+#include <stdint.h>
+#include <stddef.h>
+
+size_t SEOS_ADF_OID_LEN = 17;
+uint8_t SEOS_ADF_OID[] = {
+    0x2b,
+    0x06,
+    0x01,
+    0x04,
+    0x01,
+    0x81,
+    0xe4,
+    0x38,
+    0x01,
+    0x01,
+    0x02,
+    0x01,
+    0x18,
+    0x01,
+    0x01,
+    0x02,
+    0x02};
+uint8_t SEOS_ADF1_PRIV_ENC[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+uint8_t SEOS_ADF1_PRIV_MAC[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+uint8_t SEOS_ADF1_READ[] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};

+ 7 - 0
keys.h

@@ -0,0 +1,7 @@
+#pragma once
+
+extern size_t SEOS_ADF_OID_LEN;
+extern uint8_t SEOS_ADF_OID[32];
+extern uint8_t SEOS_ADF1_PRIV_ENC[16];
+extern uint8_t SEOS_ADF1_PRIV_MAC[16];
+extern uint8_t SEOS_ADF1_READ[16];

+ 60 - 0
memmem.c

@@ -0,0 +1,60 @@
+/* Copyright (C) 1991-2025 Free Software Foundation, Inc.
+   This file is part of the GNU C Library.
+
+   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 2, or (at your option)
+   any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License along
+   with this program; if not, write to the Free Software Foundation,
+   Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.  */
+
+/*
+
+@deftypefn Supplemental void* memmem (const void *@var{haystack}, @
+  size_t @var{haystack_len} const void *@var{needle}, size_t @var{needle_len})
+
+Returns a pointer to the first occurrence of @var{needle} (length
+@var{needle_len}) in @var{haystack} (length @var{haystack_len}).
+Returns @code{NULL} if not found.
+
+@end deftypefn
+
+*/
+
+#include <stddef.h>
+#include <string.h>
+
+#ifndef _LIBC
+#define __builtin_expect(expr, val) (expr)
+#endif
+
+#undef memmem
+
+/* Return the first occurrence of NEEDLE in HAYSTACK.  */
+void* memmem(const void* haystack, size_t haystack_len, const void* needle, size_t needle_len) {
+    const char* begin;
+    const char* const last_possible = (const char*)haystack + haystack_len - needle_len;
+
+    if(needle_len == 0)
+        /* The first occurrence of the empty string is deemed to occur at
+       the beginning of the string.  */
+        return (void*)haystack;
+
+    /* Sanity check, otherwise the loop might search through the whole
+     memory.  */
+    if(__builtin_expect(haystack_len < needle_len, 0)) return NULL;
+
+    for(begin = (const char*)haystack; begin <= last_possible; ++begin)
+        if(begin[0] == ((const char*)needle)[0] &&
+           !memcmp((const void*)&begin[1], (const void*)((const char*)needle + 1), needle_len - 1))
+            return (void*)begin;
+
+    return NULL;
+}

+ 30 - 0
scenes/seos_scene.c

@@ -0,0 +1,30 @@
+#include "seos_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const seos_on_enter_handlers[])(void*) = {
+#include "seos_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 seos_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "seos_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 seos_on_exit_handlers[])(void* context) = {
+#include "seos_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers seos_scene_handlers = {
+    .on_enter_handlers = seos_on_enter_handlers,
+    .on_event_handlers = seos_on_event_handlers,
+    .on_exit_handlers = seos_on_exit_handlers,
+    .scene_num = SeosSceneNum,
+};

+ 29 - 0
scenes/seos_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) SeosScene##id,
+typedef enum {
+#include "seos_scene_config.h"
+    SeosSceneNum,
+} SeosScene;
+#undef ADD_SCENE
+
+extern const SceneManagerHandlers seos_scene_handlers;
+
+// Generate scene on_enter handlers declaration
+#define ADD_SCENE(prefix, name, id) void prefix##_scene_##name##_on_enter(void*);
+#include "seos_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 "seos_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 "seos_scene_config.h"
+#undef ADD_SCENE

+ 45 - 0
scenes/seos_scene_about.c

@@ -0,0 +1,45 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+static char* about =
+    "This software incorporates a third-party implementation of Seos™ technology. It is not developed, authorized, licensed, or endorsed by HID Global, ASSA ABLOY, or any of their affiliates. References to Seos™ are solely for descriptive and compatibility purposes.\nNo guarantee of compatibility or functionality is made. This implementation may not work with all Seos™-enabled systems, and its performance, security, and reliability are not assured. Users assume all risks associated with its use.\nSeos™, HID Global, and ASSA ABLOY are trademarks or registered trademarks of their respective owners. This software is not associated with or sponsored by them in any way.";
+
+void seos_scene_about_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Seos* seos = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_about_on_enter(void* context) {
+    Seos* seos = context;
+
+    furi_string_reset(seos->text_box_store);
+    FuriString* str = seos->text_box_store;
+    furi_string_cat_printf(str, "%s\n", about);
+
+    text_box_set_font(seos->text_box, TextBoxFontText);
+    text_box_set_text(seos->text_box, furi_string_get_cstr(seos->text_box_store));
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewTextBox);
+}
+
+bool seos_scene_about_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(seos->scene_manager);
+    }
+    return consumed;
+}
+
+void seos_scene_about_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear views
+    text_box_reset(seos->text_box);
+}

+ 85 - 0
scenes/seos_scene_ble_device.c

@@ -0,0 +1,85 @@
+#include "../seos_i.h"
+#include "../seos_central.h"
+#include "../seos_common.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneBleDevice"
+
+void seos_scene_ble_device_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Starting...", 68, 20, AlignLeft, AlignTop);
+    // popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    seos->seos_central = seos_central_alloc(seos);
+    seos_central_start(seos->seos_central, seos->flow_mode);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_ble_device_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            notification_message(seos->notifications, &sequence_success);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            notification_message(seos->notifications, &sequence_error);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        } else if(event.event == SeosCustomEventHCIInit) {
+            popup_set_header(popup, "Init", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventScan) {
+            popup_set_header(popup, "Scanning...", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventFound) {
+            popup_set_header(popup, "Device\nfound", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventConnected) {
+            popup_set_header(popup, "Connected", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        if(seos->credential.sio_len > 0) {
+            scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneSavedMenu);
+        } else {
+            scene_manager_previous_scene(seos->scene_manager);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_ble_device_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->seos_central) {
+        FURI_LOG_D(TAG, "Cleanup");
+        seos_central_stop(seos->seos_central);
+        seos_central_free(seos->seos_central);
+        seos->seos_central = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 78 - 0
scenes/seos_scene_ble_peripheral.c

@@ -0,0 +1,78 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneBleReader"
+
+void seos_scene_ble_peripheral_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Starting", 68, 20, AlignLeft, AlignTop);
+    // popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    seos->seos_characteristic = seos_characteristic_alloc(seos);
+    seos_characteristic_start(seos->seos_characteristic, seos->flow_mode);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_ble_peripheral_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            notification_message(seos->notifications, &sequence_success);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        } else if(event.event == SeosCustomEventHCIInit) {
+            popup_set_header(popup, "Init", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAdvertising) {
+            popup_set_header(popup, "Advertising", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventConnected) {
+            popup_set_header(popup, "Connected", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 20, AlignLeft, AlignTop);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        if(seos->credential.sio_len > 0) {
+            scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneSavedMenu);
+        } else {
+            scene_manager_previous_scene(seos->scene_manager);
+        }
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_ble_peripheral_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->seos_characteristic) {
+        seos_characteristic_stop(seos->seos_characteristic);
+        seos_characteristic_free(seos->seos_characteristic);
+        seos->seos_characteristic = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 16 - 0
scenes/seos_scene_config.h

@@ -0,0 +1,16 @@
+ADD_SCENE(seos, start, Start)
+ADD_SCENE(seos, emulate, Emulate)
+ADD_SCENE(seos, read, Read)
+ADD_SCENE(seos, file_select, FileSelect)
+ADD_SCENE(seos, saved_menu, SavedMenu)
+ADD_SCENE(seos, read_success, ReadSuccess)
+ADD_SCENE(seos, save_name, SaveName)
+ADD_SCENE(seos, save_success, SaveSuccess)
+ADD_SCENE(seos, about, About)
+ADD_SCENE(seos, delete, Delete)
+ADD_SCENE(seos, delete_success, DeleteSuccess)
+ADD_SCENE(seos, read_error, ReadError)
+ADD_SCENE(seos, info, Info)
+ADD_SCENE(seos, ble_peripheral, BlePeripheral)
+ADD_SCENE(seos, ble_device, BleDevice)
+ADD_SCENE(seos, scanner_menu, ScannerMenu)

+ 52 - 0
scenes/seos_scene_delete.c

@@ -0,0 +1,52 @@
+#include "../seos_i.h"
+
+void seos_scene_delete_widget_callback(GuiButtonType result, InputType type, void* context) {
+    Seos* seos = context;
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_delete_on_enter(void* context) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+
+    // Setup Custom Widget view
+    char temp_str[141];
+    snprintf(temp_str, sizeof(temp_str), "\e#Delete %s?\e#", seos_emulator->name);
+    widget_add_text_box_element(
+        seos->widget, 0, 0, 128, 23, AlignCenter, AlignCenter, temp_str, false);
+    widget_add_button_element(
+        seos->widget, GuiButtonTypeLeft, "Back", seos_scene_delete_widget_callback, seos);
+    widget_add_button_element(
+        seos->widget, GuiButtonTypeRight, "Delete", seos_scene_delete_widget_callback, seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_delete_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            return scene_manager_previous_scene(seos->scene_manager);
+        } else if(event.event == GuiButtonTypeRight) {
+            if(seos_emulator_delete(seos_emulator, true)) {
+                scene_manager_next_scene(seos->scene_manager, SeosSceneDeleteSuccess);
+            } else {
+                scene_manager_search_and_switch_to_previous_scene(
+                    seos->scene_manager, SeosSceneStart);
+            }
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_delete_on_exit(void* context) {
+    Seos* seos = context;
+
+    widget_reset(seos->widget);
+}

+ 40 - 0
scenes/seos_scene_delete_success.c

@@ -0,0 +1,40 @@
+#include "../seos_i.h"
+
+void seos_scene_delete_success_popup_callback(void* context) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventViewExit);
+}
+
+void seos_scene_delete_success_on_enter(void* context) {
+    Seos* seos = context;
+
+    // Setup view
+    Popup* popup = seos->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, seos);
+    popup_set_callback(popup, seos_scene_delete_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_delete_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_delete_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    popup_reset(seos->popup);
+}

+ 70 - 0
scenes/seos_scene_emulate.c

@@ -0,0 +1,70 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneEmulate"
+
+void seos_scene_emulate_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcEmulate);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Emulating", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinSend_97x61);
+
+    nfc_device_load(seos->nfc_device, APP_ASSETS_PATH("seos.nfc"));
+    FURI_LOG_I(TAG, "file loaded");
+
+    const Iso14443_4aData* data = nfc_device_get_data(seos->nfc_device, NfcProtocolIso14443_4a);
+    seos->listener = nfc_listener_alloc(seos->nfc, NfcProtocolIso14443_4a, data);
+    nfc_listener_start(seos->listener, seos_worker_listener_callback, seos);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_emulate_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    Popup* popup = seos->popup;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventEmulate) {
+            popup_set_header(popup, "Emulating", 68, 30, AlignLeft, AlignTop);
+        } else if(event.event == SeosCustomEventAIDSelected) {
+            popup_set_header(popup, "AID\nSelected", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventADFMatched) {
+            popup_set_header(popup, "ADF\nMatched", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventAuthenticated) {
+            popup_set_header(popup, "Auth'd", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        } else if(event.event == SeosCustomEventSIORequested) {
+            popup_set_header(popup, "SIO\nRequested", 68, 30, AlignLeft, AlignTop);
+            consumed = true;
+        }
+
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneSavedMenu);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_emulate_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->listener) {
+        nfc_listener_stop(seos->listener);
+        nfc_listener_free(seos->listener);
+        seos->listener = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 25 - 0
scenes/seos_scene_file_select.c

@@ -0,0 +1,25 @@
+#include "../seos_i.h"
+#include "../seos_emulator.h"
+
+void seos_scene_file_select_on_enter(void* context) {
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+    // Process file_select return
+    seos_emulator_set_loading_callback(seos_emulator, seos_show_loading_popup, seos);
+    if(seos_emulator_file_select(seos_emulator)) {
+        scene_manager_next_scene(seos->scene_manager, SeosSceneSavedMenu);
+    } else {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneStart);
+    }
+    seos_emulator_set_loading_callback(seos_emulator, NULL, seos);
+}
+
+bool seos_scene_file_select_on_event(void* context, SceneManagerEvent event) {
+    UNUSED(context);
+    UNUSED(event);
+    return false;
+}
+
+void seos_scene_file_select_on_exit(void* context) {
+    UNUSED(context);
+}

+ 109 - 0
scenes/seos_scene_info.c

@@ -0,0 +1,109 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneInfo"
+
+static uint8_t empty[16] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+void seos_scene_info_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_info_on_enter(void* context) {
+    Seos* seos = context;
+    Widget* widget = seos->widget;
+    SeosCredential credential = seos->credential;
+
+    FuriString* primary_str = furi_string_alloc_set("Info");
+    FuriString* secondary_str_label = furi_string_alloc();
+    FuriString* secondary_str_value = furi_string_alloc();
+    FuriString* details_str = furi_string_alloc();
+    FuriString* keys_str = furi_string_alloc();
+
+    furi_string_set(secondary_str_label, "Diversifier:");
+    for(size_t i = 0; i < credential.diversifier_len; i++) {
+        furi_string_cat_printf(secondary_str_value, "%02X", credential.diversifier[i]);
+    }
+
+    // RID
+    if(credential.sio_len > 3 && credential.sio[2] == 0x81) {
+        size_t len = credential.sio[3];
+        furi_string_set(details_str, "RID:");
+        for(size_t i = 0; i < len; i++) {
+            furi_string_cat_printf(details_str, "%02X", credential.sio[4 + i]);
+        }
+        if(len >= 4 && credential.sio[3 + 1] == 0x01) {
+            furi_string_cat_printf(details_str, "(retail)");
+        } else if(len == 2) {
+            furi_string_cat_printf(details_str, "(ER)");
+        } else {
+            furi_string_cat_printf(details_str, "(other)");
+        }
+    }
+
+    // keys
+    if(memcmp(credential.priv_key, empty, sizeof(empty)) != 0) {
+        furi_string_cat_printf(keys_str, "+keys");
+    }
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_label));
+
+    widget_add_string_element(
+        widget,
+        64,
+        30,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_value));
+
+    widget_add_string_element(
+        widget, 64, 40, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(details_str));
+
+    widget_add_string_element(
+        widget, 64, 50, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(keys_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str_label);
+    furi_string_free(secondary_str_value);
+    furi_string_free(details_str);
+    furi_string_free(keys_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_info_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        consumed = scene_manager_previous_scene(seos->scene_manager);
+    }
+    return consumed;
+}
+
+void seos_scene_info_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 56 - 0
scenes/seos_scene_read.c

@@ -0,0 +1,56 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneRead"
+
+void seos_scene_read_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = seos->popup;
+    popup_set_header(popup, "Reading", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    seos->poller = nfc_poller_alloc(seos->nfc, NfcProtocolIso14443_4a);
+    nfc_poller_start(seos->poller, seos_worker_poller_callback, seos);
+
+    seos_blink_start(seos);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_read_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventReaderSuccess) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadSuccess);
+            consumed = true;
+        } else if(event.event == SeosCustomEventReaderError) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneReadError);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneStart);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_read_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos->poller) {
+        nfc_poller_stop(seos->poller);
+        nfc_poller_free(seos->poller);
+        seos->poller = NULL;
+    }
+
+    // Clear view
+    popup_reset(seos->popup);
+
+    seos_blink_stop(seos);
+}

+ 64 - 0
scenes/seos_scene_read_error.c

@@ -0,0 +1,64 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneReadCardSuccess"
+
+void seos_scene_read_error_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_read_error_on_enter(void* context) {
+    Seos* seos = context;
+    Widget* widget = seos->widget;
+
+    // Send notification
+    notification_message(seos->notifications, &sequence_success);
+    FuriString* primary_str = furi_string_alloc_set("Read Errror");
+    FuriString* secondary_str = furi_string_alloc_set("Try again?");
+
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "Retry", seos_scene_read_error_widget_callback, seos);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_read_error_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(seos->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneStart);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void seos_scene_read_error_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 109 - 0
scenes/seos_scene_read_success.c

@@ -0,0 +1,109 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "SeosSceneReadCardSuccess"
+
+void seos_scene_read_success_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, result);
+    }
+}
+
+void seos_scene_read_success_on_enter(void* context) {
+    Seos* seos = context;
+    SeosCredential credential = seos->credential;
+    Widget* widget = seos->widget;
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+
+    FuriString* primary_str = furi_string_alloc_set("SIO Captured");
+    FuriString* secondary_str_label = furi_string_alloc();
+    FuriString* secondary_str_value = furi_string_alloc();
+    FuriString* details_str = furi_string_alloc();
+
+    furi_string_set(secondary_str_label, "Diversifier:");
+    for(size_t i = 0; i < credential.diversifier_len; i++) {
+        furi_string_cat_printf(secondary_str_value, "%02X", credential.diversifier[i]);
+    }
+
+    // RID
+    if(credential.sio_len > 3 && credential.sio[2] == 0x81) {
+        size_t len = credential.sio[3];
+        furi_string_set(details_str, "RID:");
+        for(size_t i = 0; i < len; i++) {
+            furi_string_cat_printf(details_str, "%02X", credential.sio[4 + i]);
+        }
+        if(len >= 4 && credential.sio[3 + 1] == 0x01) {
+            furi_string_cat_printf(details_str, "(retail)");
+        } else if(len == 2) {
+            furi_string_cat_printf(details_str, "(ER)");
+        } else {
+            furi_string_cat_printf(details_str, "(other)");
+        }
+    }
+
+    widget_add_button_element(
+        widget, GuiButtonTypeLeft, "Save", seos_scene_read_success_widget_callback, seos);
+
+    widget_add_button_element(
+        widget, GuiButtonTypeRight, "Emulate", seos_scene_read_success_widget_callback, seos);
+
+    widget_add_string_element(
+        widget, 64, 5, AlignCenter, AlignCenter, FontPrimary, furi_string_get_cstr(primary_str));
+
+    widget_add_string_element(
+        widget,
+        64,
+        20,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_label));
+
+    widget_add_string_element(
+        widget,
+        64,
+        30,
+        AlignCenter,
+        AlignCenter,
+        FontSecondary,
+        furi_string_get_cstr(secondary_str_value));
+
+    widget_add_string_element(
+        widget, 64, 45, AlignCenter, AlignCenter, FontSecondary, furi_string_get_cstr(details_str));
+
+    furi_string_free(primary_str);
+    furi_string_free(secondary_str_label);
+    furi_string_free(secondary_str_value);
+    furi_string_free(details_str);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewWidget);
+}
+
+bool seos_scene_read_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneSaveName);
+            consumed = true;
+        } else if(event.event == GuiButtonTypeRight) {
+            scene_manager_next_scene(seos->scene_manager, SeosSceneSavedMenu);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneStart);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void seos_scene_read_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    widget_reset(seos->widget);
+}

+ 79 - 0
scenes/seos_scene_save_name.c

@@ -0,0 +1,79 @@
+#include "../seos_i.h"
+#include <lib/toolbox/name_generator.h>
+#include <gui/modules/validators.h>
+#include <toolbox/path.h>
+
+#define SEOS_APP_FILE_PREFIX "Seos"
+
+void seos_scene_save_name_text_input_callback(void* context) {
+    Seos* seos = context;
+
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventTextInputDone);
+}
+
+void seos_scene_save_name_on_enter(void* context) {
+    Seos* seos = context;
+
+    // Setup view
+    TextInput* text_input = seos->text_input;
+    bool dev_name_empty = false;
+    if(!strcmp(seos->dev_name, "")) {
+        name_generator_make_auto(seos->text_store, sizeof(seos->text_store), SEOS_APP_FILE_PREFIX);
+        dev_name_empty = true;
+    } else {
+        seos_text_store_set(seos, seos->dev_name);
+    }
+    text_input_set_header_text(text_input, "Name the card");
+    text_input_set_result_callback(
+        text_input,
+        seos_scene_save_name_text_input_callback,
+        seos,
+        seos->text_store,
+        sizeof(seos->text_store),
+        dev_name_empty);
+
+    FuriString* folder_path;
+    folder_path = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    if(furi_string_end_with(seos->load_path, SEOS_APP_EXTENSION)) {
+        path_extract_dirname(furi_string_get_cstr(seos->load_path), folder_path);
+    }
+
+    ValidatorIsFile* validator_is_file = validator_is_file_alloc_init(
+        furi_string_get_cstr(folder_path), SEOS_APP_EXTENSION, seos->dev_name);
+    text_input_set_validator(text_input, validator_is_file_callback, validator_is_file);
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewTextInput);
+
+    furi_string_free(folder_path);
+}
+
+bool seos_scene_save_name_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventTextInputDone) {
+            strlcpy(seos->dev_name, seos->text_store, strlen(seos->text_store) + 1);
+            if(seos_credential_save(seos, seos->text_store)) {
+                scene_manager_next_scene(seos->scene_manager, SeosSceneSaveSuccess);
+                consumed = true;
+            } else {
+                consumed = scene_manager_search_and_switch_to_previous_scene(
+                    seos->scene_manager, SeosSceneStart);
+            }
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_save_name_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    void* validator_context = text_input_get_validator_callback_context(seos->text_input);
+    text_input_set_validator(seos->text_input, NULL, NULL);
+    validator_is_file_free(validator_context);
+
+    text_input_reset(seos->text_input);
+}

+ 42 - 0
scenes/seos_scene_save_success.c

@@ -0,0 +1,42 @@
+#include "../seos_i.h"
+#include <dolphin/dolphin.h>
+
+void seos_scene_save_success_popup_callback(void* context) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventViewExit);
+}
+
+void seos_scene_save_success_on_enter(void* context) {
+    Seos* seos = context;
+    dolphin_deed(DolphinDeedNfcSave);
+
+    // Setup view
+    Popup* popup = seos->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, seos);
+    popup_set_callback(popup, seos_scene_save_success_popup_callback);
+    popup_enable_timeout(popup);
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewPopup);
+}
+
+bool seos_scene_save_success_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SeosCustomEventViewExit) {
+            consumed = scene_manager_search_and_switch_to_previous_scene(
+                seos->scene_manager, SeosSceneStart);
+        }
+    }
+    return consumed;
+}
+
+void seos_scene_save_success_on_exit(void* context) {
+    Seos* seos = context;
+
+    // Clear view
+    popup_reset(seos->popup);
+}

+ 97 - 0
scenes/seos_scene_saved_menu.c

@@ -0,0 +1,97 @@
+#include "../seos_i.h"
+
+enum SubmenuIndex {
+    SubmenuIndexEmulate,
+    SubmenuIndexBLEEmulateCentral,
+    SubmenuIndexBLEEmulatePeripheral,
+    SubmenuIndexDelete,
+    SubmenuIndexInfo,
+};
+
+void seos_scene_saved_menu_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_saved_menu_on_enter(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+
+    submenu_add_item(
+        submenu, "NFC Emulate", SubmenuIndexEmulate, seos_scene_saved_menu_submenu_callback, seos);
+
+    if(seos->has_ble) {
+        submenu_add_item(
+            submenu,
+            "BLE Emulate Central",
+            SubmenuIndexBLEEmulateCentral,
+            seos_scene_saved_menu_submenu_callback,
+            seos);
+        submenu_add_item(
+            submenu,
+            "BLE Emulate Peripheral",
+            SubmenuIndexBLEEmulatePeripheral,
+            seos_scene_saved_menu_submenu_callback,
+            seos);
+    }
+    submenu_add_item(
+        submenu, "Info", SubmenuIndexInfo, seos_scene_saved_menu_submenu_callback, seos);
+    submenu_add_item(
+        submenu, "Delete", SubmenuIndexDelete, seos_scene_saved_menu_submenu_callback, seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneSavedMenu));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+bool seos_scene_saved_menu_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        scene_manager_set_scene_state(seos->scene_manager, SeosSceneSavedMenu, event.event);
+
+        if(event.event == SubmenuIndexEmulate) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexEmulate);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneEmulate);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEEmulateCentral) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexBLEEmulateCentral);
+            seos->flow_mode = FLOW_CRED;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleDevice);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEEmulatePeripheral) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexBLEEmulatePeripheral);
+            seos->flow_mode = FLOW_CRED;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBlePeripheral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexInfo) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexInfo);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneInfo);
+            consumed = true;
+        } else if(event.event == SubmenuIndexDelete) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneSavedMenu, SubmenuIndexDelete);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneDelete);
+            consumed = true;
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        memset((void*)&seos->credential, 0, sizeof(seos->credential));
+        scene_manager_search_and_switch_to_previous_scene(seos->scene_manager, SeosSceneStart);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void seos_scene_saved_menu_on_exit(void* context) {
+    Seos* seos = context;
+
+    submenu_reset(seos->submenu);
+}

+ 61 - 0
scenes/seos_scene_scanner_menu.c

@@ -0,0 +1,61 @@
+#include "../seos_i.h"
+enum SubmenuIndex {
+    SubmenuIndexBLEReaderScanner,
+    SubmenuIndexBLECredScanner,
+};
+
+void seos_scene_scanner_menu_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_scanner_menu_on_enter(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+
+    submenu_add_item(
+        submenu,
+        "Start BLE Reader Scanner",
+        SubmenuIndexBLEReaderScanner,
+        seos_scene_scanner_menu_submenu_callback,
+        seos);
+    submenu_add_item(
+        submenu,
+        "Start BLE Cred Scanner",
+        SubmenuIndexBLECredScanner,
+        seos_scene_scanner_menu_submenu_callback,
+        seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneStart));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+bool seos_scene_scanner_menu_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexBLEReaderScanner) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneStart, SubmenuIndexBLEReaderScanner);
+            seos->flow_mode = FLOW_READER_SCANNER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleDevice);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLECredScanner) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneStart, SubmenuIndexBLECredScanner);
+            seos->flow_mode = FLOW_CRED_SCANNER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleDevice);
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void seos_scene_scanner_menu_on_exit(void* context) {
+    Seos* seos = context;
+    submenu_reset(seos->submenu);
+}

+ 137 - 0
scenes/seos_scene_start.c

@@ -0,0 +1,137 @@
+#include "../seos_i.h"
+
+#define TAG "SceneStart"
+
+enum SubmenuIndex {
+    SubmenuIndexSaved,
+    SubmenuIndexRead,
+    SubmenuIndexBLEReader,
+    SubmenuIndexScannerMenu,
+    SubmenuIndexBLECredInterrogate,
+    SubmenuIndexAbout,
+};
+
+static SeosHci* seos_hci = NULL;
+static int8_t ble_checks;
+
+void seos_scene_start_submenu_callback(void* context, uint32_t index) {
+    Seos* seos = context;
+    view_dispatcher_send_custom_event(seos->view_dispatcher, index);
+}
+
+void seos_scene_start_on_update(void* context) {
+    Seos* seos = context;
+    Submenu* submenu = seos->submenu;
+    submenu_reset(submenu);
+
+    submenu_add_item(submenu, "Saved", SubmenuIndexSaved, seos_scene_start_submenu_callback, seos);
+    submenu_add_item(
+        submenu, "Read NFC", SubmenuIndexRead, seos_scene_start_submenu_callback, seos);
+    if(seos->has_ble) {
+        submenu_add_item(
+            submenu,
+            "Start BLE Reader",
+            SubmenuIndexBLEReader,
+            seos_scene_start_submenu_callback,
+            seos);
+        submenu_add_item(
+            submenu,
+            "Scanners >",
+            SubmenuIndexScannerMenu,
+            seos_scene_start_submenu_callback,
+            seos);
+        submenu_add_item(
+            submenu,
+            "BLE Cred Interrogate",
+            SubmenuIndexBLECredInterrogate,
+            seos_scene_start_submenu_callback,
+            seos);
+    }
+    submenu_add_item(submenu, "About", SubmenuIndexAbout, seos_scene_start_submenu_callback, seos);
+
+    submenu_set_selected_item(
+        seos->submenu, scene_manager_get_scene_state(seos->scene_manager, SeosSceneStart));
+
+    view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewMenu);
+}
+
+void seos_scene_start_on_enter(void* context) {
+    Seos* seos = context;
+    // Dont' check if we've checked before
+    if(seos->has_ble == false) {
+        ble_checks = 3;
+        seos_hci = seos_hci_alloc(seos);
+        // mode/flow doesn't really matter, but these at least don't cause traffic
+        seos_hci_start(seos_hci, BLE_PERIPHERAL, FLOW_READER);
+    }
+
+    seos_scene_start_on_update(context);
+}
+
+bool seos_scene_start_on_event(void* context, SceneManagerEvent event) {
+    Seos* seos = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexRead) {
+            scene_manager_set_scene_state(seos->scene_manager, SeosSceneStart, SubmenuIndexRead);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneRead);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLEReader) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneStart, SubmenuIndexBLEReader);
+            seos->flow_mode = FLOW_READER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBlePeripheral);
+            consumed = true;
+        } else if(event.event == SubmenuIndexScannerMenu) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneStart, SubmenuIndexScannerMenu);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneScannerMenu);
+            consumed = true;
+        } else if(event.event == SubmenuIndexBLECredInterrogate) {
+            scene_manager_set_scene_state(
+                seos->scene_manager, SeosSceneStart, SubmenuIndexBLECredInterrogate);
+            seos->flow_mode = FLOW_READER;
+            scene_manager_next_scene(seos->scene_manager, SeosSceneBleDevice);
+            consumed = true;
+        } else if(event.event == SubmenuIndexSaved) {
+            scene_manager_set_scene_state(seos->scene_manager, SeosSceneStart, SubmenuIndexSaved);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneFileSelect);
+            consumed = true;
+        } else if(event.event == SubmenuIndexAbout) {
+            scene_manager_set_scene_state(seos->scene_manager, SeosSceneStart, SubmenuIndexAbout);
+            scene_manager_next_scene(seos->scene_manager, SeosSceneAbout);
+            consumed = true;
+        } else if(event.event == SeosCustomEventHCIInit) {
+            seos->has_ble = true;
+            FURI_LOG_I(TAG, "HCI Init");
+            if(seos_hci) {
+                seos_hci_stop(seos_hci);
+                seos_hci_free(seos_hci);
+                seos_hci = NULL;
+            }
+        }
+    } else if(event.type == SceneManagerEventTypeTick) {
+        if(ble_checks >= 0) {
+            FURI_LOG_D(TAG, "ble check %d", ble_checks);
+            ble_checks--;
+            if(seos->has_ble) {
+                seos_scene_start_on_update(context);
+            }
+        }
+    }
+
+    return consumed;
+}
+
+void seos_scene_start_on_exit(void* context) {
+    Seos* seos = context;
+
+    if(seos_hci) {
+        seos_hci_stop(seos_hci);
+        seos_hci_free(seos_hci);
+        seos_hci = NULL;
+    }
+
+    submenu_reset(seos->submenu);
+}

+ 349 - 0
secure_messaging.c

@@ -0,0 +1,349 @@
+#include "secure_messaging.h"
+
+#define TAG "SecureMessaging"
+
+uint8_t padding[16] =
+    {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+SecureMessaging* secure_messaging_alloc(AuthParameters* params) {
+    SecureMessaging* secure_messaging = malloc(sizeof(SecureMessaging));
+    memset(secure_messaging, 0, sizeof(SecureMessaging));
+
+    secure_messaging->cipher = params->cipher;
+    if(params->cipher == AES_128_CBC) {
+        memcpy(secure_messaging->aesContext, params->rndICC, 8);
+        memcpy(secure_messaging->aesContext + 8, params->UID, 8);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        memcpy(secure_messaging->desContext, params->rndICC, 4);
+        memcpy(secure_messaging->desContext + 4, params->UID, 4);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t index = 0;
+    uint8_t buffer[38];
+    memset(buffer, 0, sizeof(buffer));
+    index += 4; // skip 4 bytes where iteration will be put
+    memcpy(buffer + index, params->cNonce, 8);
+    index += 8;
+    memcpy(buffer + index, params->rNonce, 8);
+    index += 8;
+    buffer[index++] = params->cipher;
+    buffer[index++] = params->cipher;
+    memcpy(buffer + index, params->rndICC, 8);
+    index += 8;
+    memcpy(buffer + index, params->UID, 8);
+    index += 8;
+
+    size_t iterations = 1;
+    size_t unit = 0;
+    if(params->hash == SHA1) {
+        unit = 160 / 8;
+    } else if(params->hash == SHA256) {
+        unit = 256 / 8;
+    }
+    // FURI_LOG_D(TAG, "secure_messaging_alloc hash %d unit %d", hash, unit);
+
+    // More than enough space for the hash
+    uint8_t accumulator[64];
+    memset(accumulator, 0, sizeof(accumulator));
+    for(size_t i = 0; i < 32; i += unit) {
+        buffer[3] = iterations++;
+        if(params->hash == SHA1) {
+            mbedtls_sha1_context ctx;
+            mbedtls_sha1_init(&ctx);
+            mbedtls_sha1_starts(&ctx);
+            mbedtls_sha1_update(&ctx, buffer, index);
+            mbedtls_sha1_finish(&ctx, accumulator + i);
+            mbedtls_sha1_free(&ctx);
+        } else if(params->hash == SHA256) {
+            mbedtls_sha256_context ctx;
+            mbedtls_sha256_init(&ctx);
+            mbedtls_sha256_starts(&ctx, 0);
+            mbedtls_sha256_update(&ctx, buffer, index);
+            mbedtls_sha256_finish(&ctx, accumulator + i);
+            mbedtls_sha256_free(&ctx);
+        } else {
+            FURI_LOG_W(TAG, "Could not match hash algorithm");
+        }
+    }
+
+    memcpy(secure_messaging->PrivacyKey, accumulator, 16);
+    memcpy(secure_messaging->CMACKey, accumulator + 16, 16);
+
+    return secure_messaging;
+}
+
+void secure_messaging_free(SecureMessaging* secure_messaging) {
+    furi_assert(secure_messaging);
+    // Nothing to free;
+    free(secure_messaging);
+}
+
+void secure_messaging_increment_context(SecureMessaging* secure_messaging) {
+    uint8_t* context = NULL;
+    size_t context_len = 0;
+    if(secure_messaging->cipher == AES_128_CBC) {
+        context = secure_messaging->aesContext;
+        context_len = sizeof(secure_messaging->aesContext);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        context = secure_messaging->desContext;
+        context_len = sizeof(secure_messaging->desContext);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+        return;
+    }
+    do {
+    } while(++context[--context_len] == 0 && context_len > 0);
+}
+
+void secure_messaging_wrap_apdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    uint8_t cipher = secure_messaging->cipher;
+
+    if(message_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "Message too long to wrap");
+        return;
+    }
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    memcpy(clear, message, message_len);
+    clear[message_len] = 0x80;
+    uint8_t block_size = cipher == AES_128_CBC ? 16 : 8;
+    uint8_t block_count = (message_len + block_size - 1) / block_size;
+    size_t clear_len = block_count * block_size;
+
+    uint8_t encrypted[SECURE_MESSAGING_MAX_SIZE];
+    if(cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    uint8_t header[] = {0x0c, 0xcb, 0x3f, 0xff};
+    uint8_t encrypted_prefix[] = {0x85, clear_len};
+    uint8_t no_something[] = {0x97, 0x00};
+
+    uint8_t cmac[16];
+    BitBuffer* cmacInput = bit_buffer_alloc(256);
+    bit_buffer_reset(cmacInput);
+    if(cipher == AES_128_CBC) {
+        uint8_t* context = secure_messaging->aesContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, header, sizeof(header));
+        bit_buffer_append_bytes(cmacInput, padding, block_size - sizeof(header));
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, no_something, sizeof(no_something));
+        // Need to pad [encrypted_prefix, encrypted, no_something], and encrypted is by definition block_size units,so we just need to pad to align the other 4.
+        bit_buffer_append_bytes(cmacInput, padding, block_size - 4);
+
+        aes_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t* context = secure_messaging->desContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, header, sizeof(header));
+        bit_buffer_append_bytes(cmacInput, padding, block_size - sizeof(header));
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, no_something, sizeof(no_something));
+        // Need to pad [encrypted_prefix, encrypted, no_something], and encrypted is by definition block_size units,so we just need to pad to align the other 4.
+        bit_buffer_append_bytes(cmacInput, padding, block_size - 4);
+
+        des_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    bit_buffer_free(cmacInput);
+
+    uint8_t apdu_len[] = {2 + clear_len + 2 + 2 + 8};
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    uint8_t Le[] = {0x00};
+
+    bit_buffer_reset(tx_buffer);
+    bit_buffer_append_bytes(tx_buffer, header, sizeof(header));
+    bit_buffer_append_bytes(tx_buffer, apdu_len, sizeof(apdu_len));
+    bit_buffer_append_bytes(tx_buffer, encrypted_prefix, sizeof(encrypted_prefix));
+    bit_buffer_append_bytes(tx_buffer, encrypted, clear_len);
+    bit_buffer_append_bytes(tx_buffer, no_something, sizeof(no_something));
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, 8);
+    bit_buffer_append_bytes(tx_buffer, Le, sizeof(Le));
+}
+
+void secure_messaging_unwrap_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    // 8540 <encrypted> 99029000 8e08 <cmac>
+
+    size_t encrypted_len = bit_buffer_get_byte(rx_buffer, 1);
+    const uint8_t* encrypted = bit_buffer_get_data(rx_buffer) + 2;
+    // const uint8_t *cmac = bit_buffer_get_data(rx_buffer) + 2 + encrypted_len + 6;
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+
+    // TODO: check cmac
+    if(secure_messaging->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t clear_len = encrypted_len;
+    do {
+    } while(clear[--clear_len] == 0 && clear_len > 0);
+    bit_buffer_reset(rx_buffer);
+    bit_buffer_append_bytes(rx_buffer, clear, clear_len);
+}
+
+// Assumes it is an iso14443a-4 and doesn't have framing bytes
+/*
+0ccb3fff
+16
+  8508
+    4088b37ca72bc7ae
+  9700
+  8e08
+    85345f0f5c44b980
+00
+*/
+void secure_messaging_unwrap_apdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+
+    // TODO: check cmac
+    const uint8_t* encrypted = bit_buffer_get_data(rx_buffer) + 7;
+    size_t encrypted_len = bit_buffer_get_byte(rx_buffer, 6);
+
+    if(encrypted_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "message too large (%d) to unwrap", encrypted_len);
+        return;
+    }
+
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    if(secure_messaging->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else if(secure_messaging->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(secure_messaging->PrivacyKey, encrypted_len, encrypted, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    do {
+    } while(clear[--encrypted_len] == 0 && encrypted_len > 0);
+    bit_buffer_reset(rx_buffer);
+    bit_buffer_append_bytes(rx_buffer, clear, encrypted_len);
+}
+
+void secure_messaging_wrap_rapdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+    uint8_t cipher = secure_messaging->cipher;
+
+    if(message_len > SECURE_MESSAGING_MAX_SIZE) {
+        FURI_LOG_W(TAG, "Message too long to wrap");
+        return;
+    }
+
+    // Copy into clear so we can pad it.
+    uint8_t clear[SECURE_MESSAGING_MAX_SIZE];
+    memset(clear, 0, sizeof(clear));
+    memcpy(clear, message, message_len);
+    clear[message_len] = 0x80;
+    uint8_t block_size = cipher == AES_128_CBC ? 16 : 8;
+    uint8_t block_count = (message_len + block_size - 1) / block_size;
+    size_t clear_len = block_count * block_size;
+
+    uint8_t encrypted[SECURE_MESSAGING_MAX_SIZE];
+    if(cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(secure_messaging->PrivacyKey, clear_len, clear, encrypted);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    uint8_t encrypted_prefix[] = {0x85, clear_len};
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    uint8_t something[] = {0x99, 0x02, 0x90, 0x00};
+
+    uint8_t cmac[16];
+    BitBuffer* cmacInput = bit_buffer_alloc(256);
+    bit_buffer_reset(cmacInput);
+    if(cipher == AES_128_CBC) {
+        uint8_t* context = secure_messaging->aesContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, something, sizeof(something));
+        // Need to pad to multiple of block size, but context and encrypted are already block size.
+        bit_buffer_append_bytes(
+            cmacInput, padding, block_size - sizeof(encrypted_prefix) - sizeof(something));
+
+        aes_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else if(cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t* context = secure_messaging->desContext;
+        bit_buffer_append_bytes(cmacInput, context, block_size);
+        bit_buffer_append_bytes(cmacInput, encrypted_prefix, sizeof(encrypted_prefix));
+        bit_buffer_append_bytes(cmacInput, encrypted, clear_len);
+        bit_buffer_append_bytes(cmacInput, something, sizeof(something));
+        // Need to pad to multiple of block size, but context and encrypted are already block size.
+        bit_buffer_append_bytes(
+            cmacInput, padding, block_size - sizeof(encrypted_prefix) - sizeof(something));
+
+        des_cmac(
+            secure_messaging->CMACKey,
+            sizeof(secure_messaging->CMACKey),
+            (uint8_t*)bit_buffer_get_data(cmacInput),
+            bit_buffer_get_size_bytes(cmacInput),
+            cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+    bit_buffer_free(cmacInput);
+
+    bit_buffer_append_bytes(tx_buffer, encrypted_prefix, sizeof(encrypted_prefix));
+    bit_buffer_append_bytes(tx_buffer, encrypted, clear_len);
+    bit_buffer_append_bytes(tx_buffer, something, sizeof(something));
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, 8);
+    // Success (9000) is appended by common code before transmission
+
+    /*
+8540
+  2b4f4e5598193e71cc94ff6dc2b24d1e9feae4182d7315b8b11a8a034670fe8c369f2a98c256dd6f4decf28277a180ea1c4ce515812abfa683e1e004bc66d757
+9902
+  9000
+8e08
+  a860944446f6d53d
+9000
+*/
+}

+ 42 - 0
secure_messaging.h

@@ -0,0 +1,42 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+#include <mbedtls/sha1.h>
+#include <mbedtls/sha256.h>
+
+#include "seos_common.h"
+#include "aes_cmac.h"
+#include "des_cmac.h"
+
+#define SECURE_MESSAGING_MAX_SIZE 128
+
+typedef struct {
+    uint8_t cipher;
+    uint8_t PrivacyKey[16];
+    uint8_t CMACKey[16];
+    uint8_t aesContext[16];
+    uint8_t desContext[8];
+} SecureMessaging;
+
+SecureMessaging* secure_messaging_alloc(AuthParameters* params);
+
+void secure_messaging_free(SecureMessaging* secure_messaging);
+
+void secure_messaging_wrap_apdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer);
+
+void secure_messaging_unwrap_apdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer);
+
+void secure_messaging_unwrap_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer);
+void secure_messaging_wrap_rapdu(
+    SecureMessaging* secure_messaging,
+    uint8_t* message,
+    size_t message_len,
+    BitBuffer* tx_buffer);

+ 301 - 0
seos.c

@@ -0,0 +1,301 @@
+#include "seos_i.h"
+
+#define TAG "Seos"
+
+#define SEOS_KEYS_FILENAME "keys"
+
+bool seos_load_keys(Seos* seos) {
+    const char* file_header = "Seos keys";
+    const uint32_t file_version = 1;
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos->storage);
+    FuriString* path = furi_string_alloc();
+    FuriString* temp_str = furi_string_alloc();
+    uint32_t version = 0;
+
+    do {
+        furi_string_printf(
+            path, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, SEOS_KEYS_FILENAME, ".txt");
+        // Open file
+        if(!flipper_format_file_open_existing(file, furi_string_get_cstr(path))) break;
+        if(!flipper_format_read_header(file, temp_str, &version)) break;
+        if(!furi_string_equal_str(temp_str, file_header) || (version != file_version)) {
+            break;
+        }
+
+        if(!flipper_format_read_uint32(file, "SEOS_ADF_OID_LEN", (uint32_t*)&SEOS_ADF_OID_LEN, 1))
+            break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF_OID", SEOS_ADF_OID, SEOS_ADF_OID_LEN)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_PRIV_ENC", SEOS_ADF1_PRIV_ENC, 16)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_PRIV_MAC", SEOS_ADF1_PRIV_MAC, 16)) break;
+        if(!flipper_format_read_hex(file, "SEOS_ADF1_READ", SEOS_ADF1_READ, 16)) break;
+
+        parsed = true;
+    } while(false);
+
+    if(parsed) {
+        FURI_LOG_I(TAG, "Keys loaded");
+        BitBuffer* tmp = bit_buffer_alloc(SEOS_ADF_OID_LEN);
+        bit_buffer_append_bytes(tmp, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+        seos_log_bitbuffer(TAG, "Keys for ADF OID loaded", tmp);
+        bit_buffer_free(tmp);
+    } else {
+        FURI_LOG_I(TAG, "Using default keys");
+    }
+
+    furi_string_free(path);
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool seos_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Seos* seos = context;
+    return scene_manager_handle_custom_event(seos->scene_manager, event);
+}
+
+bool seos_back_event_callback(void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+    return scene_manager_handle_back_event(seos->scene_manager);
+}
+
+void seos_tick_event_callback(void* context) {
+    furi_assert(context);
+    Seos* seos = context;
+    scene_manager_handle_tick_event(seos->scene_manager);
+}
+
+Seos* seos_alloc() {
+    Seos* seos = malloc(sizeof(Seos));
+
+    seos->has_ble = false;
+    furi_hal_power_enable_otg();
+
+    seos->view_dispatcher = view_dispatcher_alloc();
+    seos->scene_manager = scene_manager_alloc(&seos_scene_handlers, seos);
+    view_dispatcher_set_event_callback_context(seos->view_dispatcher, seos);
+    view_dispatcher_set_custom_event_callback(seos->view_dispatcher, seos_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(seos->view_dispatcher, seos_back_event_callback);
+    view_dispatcher_set_tick_event_callback(seos->view_dispatcher, seos_tick_event_callback, 100);
+
+    seos->nfc = nfc_alloc();
+
+    // Nfc device
+    seos->nfc_device = nfc_device_alloc();
+    nfc_device_set_loading_callback(seos->nfc_device, seos_show_loading_popup, seos);
+
+    // Open GUI record
+    seos->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(seos->view_dispatcher, seos->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    seos->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    seos->submenu = submenu_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewMenu, submenu_get_view(seos->submenu));
+
+    // Popup
+    seos->popup = popup_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewPopup, popup_get_view(seos->popup));
+
+    // Loading
+    seos->loading = loading_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewLoading, loading_get_view(seos->loading));
+
+    // Text Input
+    seos->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewTextInput, text_input_get_view(seos->text_input));
+
+    // TextBox
+    seos->text_box = text_box_alloc();
+    view_dispatcher_add_view(
+        seos->view_dispatcher, SeosViewTextBox, text_box_get_view(seos->text_box));
+    seos->text_box_store = furi_string_alloc();
+
+    // Custom Widget
+    seos->widget = widget_alloc();
+    view_dispatcher_add_view(seos->view_dispatcher, SeosViewWidget, widget_get_view(seos->widget));
+
+    seos->storage = furi_record_open(RECORD_STORAGE);
+    seos->dialogs = furi_record_open(RECORD_DIALOGS);
+    seos->load_path = furi_string_alloc();
+
+    seos->seos_emulator = seos_emulator_alloc(&seos->credential);
+
+    seos_load_keys(seos);
+
+    return seos;
+}
+
+void seos_free(Seos* seos) {
+    furi_assert(seos);
+
+    furi_hal_power_disable_otg();
+
+    nfc_free(seos->nfc);
+
+    // Nfc device
+    nfc_device_free(seos->nfc_device);
+
+    // Submenu
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewMenu);
+    submenu_free(seos->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewPopup);
+    popup_free(seos->popup);
+
+    // Loading
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewLoading);
+    loading_free(seos->loading);
+
+    // TextInput
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewTextInput);
+    text_input_free(seos->text_input);
+
+    // TextBox
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewTextBox);
+    text_box_free(seos->text_box);
+    furi_string_free(seos->text_box_store);
+
+    // Custom Widget
+    view_dispatcher_remove_view(seos->view_dispatcher, SeosViewWidget);
+    widget_free(seos->widget);
+
+    // View Dispatcher
+    view_dispatcher_free(seos->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(seos->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    seos->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    seos->notifications = NULL;
+
+    furi_string_free(seos->load_path);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+
+    if(seos->seos_emulator) {
+        seos_emulator_free(seos->seos_emulator);
+        seos->seos_emulator = NULL;
+    }
+
+    free(seos);
+}
+
+void seos_text_store_set(Seos* seos, const char* text, ...) {
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(seos->text_store, sizeof(seos->text_store), text, args);
+
+    va_end(args);
+}
+
+void seos_text_store_clear(Seos* seos) {
+    memset(seos->text_store, 0, sizeof(seos->text_store));
+}
+
+bool seos_credential_save(Seos* seos, const char* dev_name) {
+    bool saved = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos->storage);
+    FuriString* temp_str = furi_string_alloc();
+    bool use_load_path = true;
+
+    do {
+        if(use_load_path && !furi_string_empty(seos->load_path)) {
+            // Get directory name
+            path_extract_dirname(furi_string_get_cstr(seos->load_path), temp_str);
+            // Make path to file to save
+            furi_string_cat_printf(temp_str, "/%s%s", dev_name, SEOS_APP_EXTENSION);
+        } else {
+            // First remove file if it was saved
+            furi_string_printf(
+                temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, dev_name, SEOS_APP_EXTENSION);
+        }
+
+        // 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, seos_file_header, seos_file_version)) break;
+
+        if(!flipper_format_write_uint32(
+               file, "Diversifier Length", (uint32_t*)&(seos->credential.diversifier_len), 1))
+            break;
+        if(!flipper_format_write_hex(
+               file, "Diversifier", seos->credential.diversifier, seos->credential.diversifier_len))
+            break;
+        if(!flipper_format_write_uint32(
+               file, "SIO Length", (uint32_t*)&(seos->credential.sio_len), 1))
+            break;
+        if(!flipper_format_write_hex(file, "SIO", seos->credential.sio, seos->credential.sio_len))
+            break;
+
+        saved = true;
+    } while(false);
+
+    if(!saved) {
+        dialog_message_show_storage_error(seos->dialogs, "Can not save\nfile");
+    }
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+    return saved;
+}
+
+static const NotificationSequence seos_sequence_blink_start_blue = {
+    &message_blink_start_10,
+    &message_blink_set_color_blue,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence seos_sequence_blink_stop = {
+    &message_blink_stop,
+    NULL,
+};
+
+void seos_blink_start(Seos* seos) {
+    notification_message(seos->notifications, &seos_sequence_blink_start_blue);
+}
+
+void seos_blink_stop(Seos* seos) {
+    notification_message(seos->notifications, &seos_sequence_blink_stop);
+}
+
+void seos_show_loading_popup(void* context, bool show) {
+    Seos* seos = context;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(seos->view_dispatcher, SeosViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+int32_t seos_app(void* p) {
+    UNUSED(p);
+    Seos* seos = seos_alloc();
+
+    scene_manager_next_scene(seos->scene_manager, SeosSceneStart);
+
+    view_dispatcher_run(seos->view_dispatcher);
+
+    seos_free(seos);
+
+    return 0;
+}

+ 5 - 0
seos.h

@@ -0,0 +1,5 @@
+#pragma once
+
+typedef struct Seos Seos;
+
+bool seos_credential_save(Seos* seos, const char* dev_name);


+ 492 - 0
seos_att.c

@@ -0,0 +1,492 @@
+#include "seos_att_i.h"
+
+#define TAG "SeosAtt"
+
+struct att_read_by_type_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+    union {
+        uint16_t short_uuid;
+        uint8_t long_uuid[16];
+    } attribute_type;
+} __packed;
+
+struct att_find_info_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+} __packed;
+
+struct att_find_type_value_req {
+    uint8_t opcode;
+    uint16_t start_handle;
+    uint16_t end_handle;
+    uint16_t att_type;
+    uint8_t att_value[0];
+} __packed;
+
+struct att_write_req {
+    uint8_t opcode;
+    uint16_t handle;
+} __packed;
+
+static uint8_t seos_reader_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00};
+static uint8_t seos_cred_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00};
+
+SeosAtt* seos_att_alloc(Seos* seos) {
+    SeosAtt* seos_att = malloc(sizeof(SeosAtt));
+    memset(seos_att, 0, sizeof(SeosAtt));
+
+    seos_att->seos = seos;
+    seos_att->seos_l2cap = seos_l2cap_alloc(seos);
+    seos_l2cap_set_receive_callback(seos_att->seos_l2cap, seos_att_process_payload, seos_att);
+    seos_l2cap_set_central_connection_callback(
+        seos_att->seos_l2cap, seos_att_central_connection_start, seos_att);
+
+    seos_att->tx_buf = bit_buffer_alloc(128);
+
+    seos_att->tx_mtu = 0;
+    seos_att->rx_mtu = 0x0200;
+
+    return seos_att;
+}
+
+void seos_att_free(SeosAtt* seos_att) {
+    furi_assert(seos_att);
+
+    seos_l2cap_free(seos_att->seos_l2cap);
+
+    bit_buffer_free(seos_att->tx_buf);
+    free(seos_att);
+}
+
+void seos_att_start(SeosAtt* seos_att, BleMode mode, FlowMode flow_mode) {
+    seos_att->ble_mode = mode;
+    seos_att->flow_mode = flow_mode;
+    seos_l2cap_start(seos_att->seos_l2cap, mode, flow_mode);
+}
+
+void seos_att_stop(SeosAtt* seos_att) {
+    seos_l2cap_stop(seos_att->seos_l2cap);
+}
+
+void seos_att_central_connection_start(void* context) {
+    SeosAtt* seos_att = (SeosAtt*)context;
+    FURI_LOG_D(TAG, "central connnection start");
+    bit_buffer_reset(seos_att->tx_buf);
+
+    if(seos_att->flow_mode == FLOW_READER) {
+        uint16_t start = 0x0001;
+        uint16_t end = 0xffff;
+        uint16_t attribute_type = PRIMARY_SERVICE;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&start, sizeof(start));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&end, sizeof(end));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, seos_cred_service_backwards, sizeof(seos_cred_service_backwards));
+    } else if(seos_att->flow_mode == FLOW_CRED) {
+        // First thing to do with any new connection is the MTU
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_EXCHANGE_MTU_REQ);
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->rx_mtu, sizeof(seos_att->rx_mtu));
+    }
+
+    seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+}
+
+void seos_att_notify(SeosAtt* seos_att, uint16_t handle, BitBuffer* content) {
+    seos_log_bitbuffer(TAG, "notify", content);
+    size_t content_len = bit_buffer_get_size_bytes(content);
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(handle) + content_len);
+
+    bit_buffer_append_byte(tx, ATT_HANDLE_VALUE_NTF);
+    bit_buffer_append_bytes(tx, (uint8_t*)&handle, sizeof(handle));
+    bit_buffer_append_bytes(tx, bit_buffer_get_data(content), content_len);
+
+    seos_l2cap_send(seos_att->seos_l2cap, tx);
+    bit_buffer_free(tx);
+}
+
+void seos_att_write_request(SeosAtt* seos_att, BitBuffer* content) {
+    seos_log_bitbuffer(TAG, "write_request", content);
+    size_t content_len = bit_buffer_get_size_bytes(content);
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(seos_att->value_handle) + content_len);
+
+    bit_buffer_append_byte(tx, ATT_WRITE_CMD);
+    bit_buffer_append_bytes(
+        tx, (uint8_t*)&(seos_att->value_handle), sizeof(seos_att->value_handle));
+    bit_buffer_append_bytes(tx, bit_buffer_get_data(content), content_len);
+
+    seos_l2cap_send(seos_att->seos_l2cap, tx);
+    bit_buffer_free(tx);
+}
+
+// TODO: figure out the proper name for data that comes in
+void seos_att_process_payload(void* context, BitBuffer* message) {
+    SeosAtt* seos_att = (SeosAtt*)context;
+    // seos_log_bitbuffer(TAG, "process payload", message);
+
+    bit_buffer_reset(seos_att->tx_buf);
+    const uint8_t* data = bit_buffer_get_data(message);
+    uint8_t opcode = data[0];
+    struct att_read_by_type_req* s;
+    uint16_t* start_handle;
+    uint16_t* end_handle;
+    uint16_t attribute_type;
+    size_t length = 0;
+
+    if(seos_att->ble_mode == BLE_CENTRAL && ((opcode & 0x01) == 0x00)) {
+        bool reject = true;
+        FURI_LOG_I(
+            TAG,
+            "%s request(0x%02x) when operating as Central",
+            reject ? "Rejecting" : "Ignoring",
+            opcode);
+        if(reject) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(seos_att->tx_buf, data + 1, sizeof(uint16_t));
+            bit_buffer_append_byte(seos_att->tx_buf, 0x0a);
+            seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+        }
+        return;
+    }
+
+    switch(opcode) {
+    case ATT_ERROR_RSP:
+        uint8_t error_with_opcode = data[1];
+        FURI_LOG_W(TAG, "Error with command %02x", error_with_opcode);
+        break;
+    case ATT_EXCHANGE_MTU_REQ: // MTU request
+
+        // Trying a new way to copy the uint16_t
+        seos_att->tx_mtu = *(uint16_t*)(data + 1);
+        FURI_LOG_D(TAG, "MTU REQ = %04x", seos_att->tx_mtu);
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_EXCHANGE_MTU_RSP);
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->rx_mtu, sizeof(seos_att->rx_mtu));
+        break;
+    case ATT_EXCHANGE_MTU_RSP:
+        seos_att->tx_mtu = *(uint16_t*)(data + 1);
+        FURI_LOG_D(TAG, "MTU RSP = %04x", seos_att->tx_mtu);
+
+        uint16_t start = 0x0001;
+        uint16_t end = 0xffff;
+        attribute_type = PRIMARY_SERVICE;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&start, sizeof(start));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&end, sizeof(end));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf,
+            seos_reader_service_backwards,
+            sizeof(seos_reader_service_backwards));
+        break;
+    case ATT_READ_BY_TYPE_REQ:
+        s = (struct att_read_by_type_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT read by type %04x - %04x, type %04x",
+            s->start_handle,
+            s->end_handle,
+            s->attribute_type.short_uuid);
+
+        if(s->attribute_type.short_uuid == CHARACTERISTIC) {
+            if(s->start_handle == 0x0006) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_RSP);
+                uint8_t response[] = {0x07, 0x07, 0x00, 0x20, 0x08, 0x00, 0x05, 0x2a};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+
+            } else if(s->start_handle == 0x000a) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_RSP);
+                uint8_t flow_mode_byte = seos_att->flow_mode == FLOW_READER ? 0x00 : 0x01;
+                uint8_t response[] = {0x15, 0x0b, 0x00,           0x14, 0x0c, 0x00, 0x02, 0x00,
+                                      0x00, 0x7a, 0x17,           0x00, 0x00, 0x80, 0x00, 0x10,
+                                      0x00, 0x00, flow_mode_byte, 0xaa, 0x00, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        } else if(s->attribute_type.short_uuid == DEVICE_NAME) {
+            if(s->start_handle == 0x0001) {
+                uint8_t response[] = {0x09, 0x03, 0x00, 0x46, 0x6c, 0x69, 0x70, 0x70, 0x65, 0x72};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+
+        if(bit_buffer_get_size_bytes(seos_att->tx_buf) == 0) {
+            FURI_LOG_W(TAG, "Return error");
+            bit_buffer_reset(seos_att->tx_buf);
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(
+                seos_att->tx_buf, (uint8_t*)&s->start_handle, sizeof(s->start_handle));
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND);
+        }
+        break;
+    case ATT_READ_BY_TYPE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_READ_BY_TYPE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+
+        uint16_t* handle;
+        handle = (uint16_t*)(data + 2);
+        seos_att->characteristic_handle = *handle;
+        // skip properties byte
+        handle = (uint16_t*)(data + 5);
+        seos_att->value_handle = *handle;
+
+        *handle = *handle + 1;
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_INFORMATION_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&seos_att->service_end_handle, sizeof(uint16_t));
+        break;
+    case ATT_READ_BY_GROUP_TYPE_REQ:
+        s = (struct att_read_by_type_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT read by group type %04x - %04x, type %04x",
+            s->start_handle,
+            s->end_handle,
+            s->attribute_type.short_uuid);
+        if(s->attribute_type.short_uuid == PRIMARY_SERVICE) {
+            if(s->start_handle == 0x0001) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_RSP);
+                uint8_t response[] = {
+                    0x06, 0x01, 0x00, 0x05, 0x00, 0x00, 0x18, 0x06, 0x00, 0x09, 0x00, 0x01, 0x18};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            } else if(s->start_handle == 0x000a) {
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_RSP);
+                uint8_t flow_mode_byte = seos_att->flow_mode == FLOW_READER ? 0x00 : 0x01;
+                uint8_t response[] = {0x14, 0x0a, 0x00, 0x0d,           0x00, 0x02, 0x00,
+                                      0x00, 0x7a, 0x17, 0x00,           0x00, 0x80, 0x00,
+                                      0x10, 0x00, 0x00, flow_mode_byte, 0x98, 0x00, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+
+        // Didn't match either attribute_type or start_handle
+        if(bit_buffer_get_size_bytes(seos_att->tx_buf) == 0) {
+            FURI_LOG_W(TAG, "Return error");
+            bit_buffer_reset(seos_att->tx_buf);
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_RSP);
+            bit_buffer_append_byte(seos_att->tx_buf, opcode);
+            bit_buffer_append_bytes(
+                seos_att->tx_buf, (uint8_t*)&s->start_handle, sizeof(s->start_handle));
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND);
+        }
+        break;
+    case ATT_READ_BY_GROUP_TYPE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_READ_BY_GROUP_TYPE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+        // NOTE: this might not be actively used
+
+        uint8_t size = data[1];
+        size_t i = 2;
+        do {
+            start_handle = (uint16_t*)(data + i);
+            end_handle = (uint16_t*)(data + sizeof(uint16_t) + i);
+
+            // +4 for 2 uint16_t
+            if(size == (sizeof(seos_cred_service_backwards) + 4)) {
+                if(memcmp(
+                       seos_cred_service_backwards,
+                       data + i + 4,
+                       sizeof(seos_cred_service_backwards)) == 0) {
+                    seos_att->service_start_handle = *start_handle;
+                    seos_att->service_end_handle = *end_handle;
+                }
+            }
+            i += size;
+        } while(i < bit_buffer_get_size_bytes(message));
+        *end_handle = *end_handle + 1;
+
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_GROUP_TYPE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)end_handle, sizeof(uint16_t));
+        uint8_t suffix[] = {0xff, 0xff, 0x00, 0x28};
+        bit_buffer_append_bytes(seos_att->tx_buf, suffix, sizeof(suffix));
+
+        break;
+    case ATT_FIND_INFORMATION_REQ:
+        struct att_find_info_req* e = (struct att_find_info_req*)(data);
+        FURI_LOG_D(TAG, "ATT find information %04x - %04x", e->start_handle, e->end_handle);
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_INFORMATION_RSP);
+
+        if(e->start_handle == 0x0009) {
+            uint8_t response[] = {0x01, 0x09, 0x00, 0x02, 0x29};
+            bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+        } else if(e->start_handle == 0x000d) {
+            uint8_t response[] = {0x01, 0x0d, 0x00, 0x02, 0x29};
+            bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+        } else {
+            FURI_LOG_W(TAG, "unhandled handle in ATT_FIND_INFORMATION_REQ");
+        }
+        break;
+    case ATT_FIND_INFORMATION_RSP:
+        seos_log_buffer(
+            TAG, "ATT_FIND_INFORMATION_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+
+        // 05 01 3c00 0029 3d00 0229
+        uint8_t format = data[1];
+        if(format == 1) { // short UUID
+            uint16_t* handle;
+            uint16_t* uuid;
+            size_t i = 2;
+            do {
+                handle = (uint16_t*)(data + i);
+                uuid = (uint16_t*)(data + i + 2);
+                i += 4;
+                if(*uuid == CCCD) {
+                    seos_att->cccd_handle = *handle;
+                }
+            } while(i < bit_buffer_get_size_bytes(message));
+
+            if(seos_att->cccd_handle > 0) {
+                FURI_LOG_I(TAG, "Subscribe to phone/device");
+                uint16_t value = ENABLE_NOTIFICATION_VALUE;
+                bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_CMD);
+                bit_buffer_append_bytes(
+                    seos_att->tx_buf, (uint8_t*)&seos_att->cccd_handle, sizeof(uint16_t));
+                bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)&value, sizeof(value));
+            }
+        }
+
+        break;
+    case ATT_FIND_BY_TYPE_VALUE_REQ:
+        struct att_find_type_value_req* t = (struct att_find_type_value_req*)(data);
+        FURI_LOG_D(
+            TAG,
+            "ATT_FIND_BY_TYPE_VALUE_REQ %04x - %04x for %04x",
+            t->start_handle,
+            t->end_handle,
+            t->att_type);
+        if(t->att_type == PRIMARY_SERVICE) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_FIND_BY_TYPE_VALUE_RSP);
+            if(seos_att->flow_mode == FLOW_CRED) {
+                uint8_t response[] = {0x0a, 0x00, 0x0e, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            } else if(seos_att->flow_mode == FLOW_READER) {
+                uint8_t response[] = {0x0c, 0x00, 0x0e, 0x00};
+                bit_buffer_append_bytes(seos_att->tx_buf, response, sizeof(response));
+            }
+        }
+        break;
+    case ATT_FIND_BY_TYPE_VALUE_RSP:
+        seos_log_buffer(
+            TAG, "ATT_FIND_BY_TYPE_VALUE_RSP", (uint8_t*)data, bit_buffer_get_size_bytes(message));
+        start_handle = (uint16_t*)(data + 1);
+        end_handle = (uint16_t*)(data + 3);
+
+        seos_att->service_start_handle = *start_handle;
+        seos_att->service_end_handle = *end_handle;
+
+        attribute_type = CHARACTERISTIC;
+        bit_buffer_append_byte(seos_att->tx_buf, ATT_READ_BY_TYPE_REQ);
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)start_handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(seos_att->tx_buf, (uint8_t*)end_handle, sizeof(uint16_t));
+        bit_buffer_append_bytes(
+            seos_att->tx_buf, (uint8_t*)&attribute_type, sizeof(attribute_type));
+        break;
+    case ATT_WRITE_REQ:
+        struct att_write_req* w = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT Write Req %d bytes to %04x", length, w->handle);
+        if(w->handle == 0x0009) {
+            bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_RSP);
+        } else if(w->handle == 0x000d) {
+            uint16_t* value = (uint16_t*)(data + sizeof(struct att_write_req));
+            if(*value == DISABLE_NOTIFICATION_VALUE) {
+                FURI_LOG_I(TAG, "Unsubscribe");
+            } else if(*value == ENABLE_NOTIFICATION_VALUE) {
+                FURI_LOG_I(TAG, "Subscribe");
+                if(seos_att->on_subscribe) {
+                    // comes in as 0x000d, but we need to use 0x000c: I'm sure there is a reason for this that I'm just not aware of
+                    seos_att->on_subscribe(seos_att->on_subscribe_context, w->handle - 1);
+                    bit_buffer_append_byte(seos_att->tx_buf, ATT_WRITE_RSP);
+                } else {
+                    FURI_LOG_W(TAG, "No on_subscribe callback defined");
+                }
+            }
+        }
+        break;
+    case ATT_WRITE_RSP:
+        FURI_LOG_D(TAG, "ATT_WRITE_RSP");
+        break;
+    case ATT_WRITE_CMD:
+        struct att_write_req* c = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT Write CMD %d bytes to %04x", length, c->handle);
+
+        if(c->handle == 0x000c) {
+            BitBuffer* attribute_value = bit_buffer_alloc(length);
+            bit_buffer_append_bytes(
+                attribute_value,
+                bit_buffer_get_data(message) + sizeof(struct att_write_req),
+                length);
+            if(seos_att->write_request) {
+                seos_att->write_request(seos_att->write_request_context, attribute_value);
+            }
+            bit_buffer_free(attribute_value);
+        } else {
+            seos_log_bitbuffer(TAG, "write to unsupported handle", message);
+        }
+        // No response to CMD expected
+        break;
+    case ATT_HANDLE_VALUE_NTF:
+        struct att_write_req* n = (struct att_write_req*)(data);
+        length = bit_buffer_get_size_bytes(message) - sizeof(struct att_write_req);
+        FURI_LOG_D(TAG, "ATT handle value notify %d bytes to %04x", length, n->handle);
+        if(n->handle == 0x000d) {
+            if(seos_att->notify) {
+                seos_att->notify(
+                    seos_att->notify_context,
+                    bit_buffer_get_data(message) + sizeof(struct att_write_req),
+                    length);
+            }
+        } else {
+            FURI_LOG_W(TAG, "Notify with unhandled handle");
+        }
+        break;
+    case ATT_HANDLE_VALUE_CFM:
+        FURI_LOG_I(TAG, "Indication confirmation");
+        break;
+    default:
+        FURI_LOG_W(TAG, "seos_att_process_message no handler for 0x%02x", opcode);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(seos_att->tx_buf) > 0) {
+        seos_log_bitbuffer(TAG, "sending", seos_att->tx_buf);
+        seos_l2cap_send(seos_att->seos_l2cap, seos_att->tx_buf);
+    }
+}
+
+void seos_att_set_on_subscribe_callback(
+    SeosAtt* seos_att,
+    SeosAttOnSubscribeCallback callback,
+    void* context) {
+    seos_att->on_subscribe = callback;
+    seos_att->on_subscribe_context = context;
+}
+
+void seos_att_set_write_request_callback(
+    SeosAtt* seos_att,
+    SeosAttWriteRequestCallback callback,
+    void* context) {
+    seos_att->write_request = callback;
+    seos_att->write_request_context = context;
+}
+
+void seos_att_set_notify_callback(SeosAtt* seos_att, SeosAttNotifyCallback callback, void* context) {
+    seos_att->notify = callback;
+    seos_att->notify_context = context;
+}

+ 100 - 0
seos_att.h

@@ -0,0 +1,100 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include "seos_l2cap.h"
+#include "seos_common.h"
+
+#define ATT_ERROR_RSP              0x01
+#define ATT_EXCHANGE_MTU_REQ       0x02
+#define ATT_EXCHANGE_MTU_RSP       0x03
+#define ATT_FIND_INFORMATION_REQ   0x04
+#define ATT_FIND_INFORMATION_RSP   0x05
+#define ATT_FIND_BY_TYPE_VALUE_REQ 0x06
+#define ATT_FIND_BY_TYPE_VALUE_RSP 0x07
+#define ATT_READ_BY_TYPE_REQ       0x08
+#define ATT_READ_BY_TYPE_RSP       0x09
+#define ATT_READ_REQ               0x0a
+#define ATT_READ_RSP               0x0b
+#define ATT_READ_BLOB_REQ          0x0c
+#define ATT_READ_BLOB_RSP          0x0d
+#define ATT_READ_MULTIPLE_REQ      0x0e
+#define ATT_READ_MULTIPLE_RSP      0x0f
+#define ATT_READ_BY_GROUP_TYPE_REQ 0x10
+#define ATT_READ_BY_GROUP_TYPE_RSP 0x11
+#define ATT_WRITE_REQ              0x12
+#define ATT_WRITE_RSP              0x13
+#define ATT_HANDLE_VALUE_NTF       0x1b
+#define ATT_HANDLE_VALUE_IND       0x1d
+#define ATT_HANDLE_VALUE_CFM       0x1e
+
+#define ATT_READ_MULTIPLE_VARIABLE_REQ 0x20
+#define ATT_READ_MULTIPLE_VARIABLE_RSP 0x21
+
+#define ATT_WRITE_CMD 0x52
+
+#define ATT_ERROR_CODE_ATTRIBUTE_NOT_FOUND   0x0a
+#define ATT_ERROR_CODE_INVALID_HANDLE        0x01
+#define ATT_ERROR_CODE_REQUEST_NOT_SUPPORTED 0x06
+
+#define PRIMARY_SERVICE 0x2800
+#define CHARACTERISTIC  0x2803
+#define CCCD            0x2902
+#define DEVICE_NAME     0x2a00
+// service_changed = 2a05
+
+#define DISABLE_NOTIFICATION_VALUE 0x0000
+#define ENABLE_NOTIFICATION_VALUE  0x0001
+#define ENABLE_INDICATION_VALUE    0x0002
+
+typedef void (*SeosAttOnSubscribeCallback)(void* context, uint16_t handle);
+typedef void (*SeosAttWriteRequestCallback)(void* context, BitBuffer* attribute_value);
+typedef void (*SeosAttNotifyCallback)(void* context, const uint8_t* buffer, size_t buffer_len);
+
+typedef struct {
+    Seos* seos;
+    SeosL2Cap* seos_l2cap;
+    BitBuffer* tx_buf;
+    uint16_t tx_mtu;
+    uint16_t rx_mtu;
+
+    SeosAttOnSubscribeCallback on_subscribe;
+    void* on_subscribe_context;
+
+    SeosAttWriteRequestCallback write_request;
+    void* write_request_context;
+
+    SeosAttNotifyCallback notify;
+    void* notify_context;
+
+    BleMode ble_mode;
+    FlowMode flow_mode;
+
+    uint16_t service_start_handle;
+    uint16_t service_end_handle;
+    uint16_t characteristic_handle;
+    uint16_t value_handle;
+    uint16_t cccd_handle;
+} SeosAtt;
+
+SeosAtt* seos_att_alloc(Seos* seos);
+void seos_att_free(SeosAtt* seos_att);
+void seos_att_start(SeosAtt* seos_att, BleMode mode, FlowMode flow_mode);
+void seos_att_stop(SeosAtt* seos_att);
+void seos_att_notify(SeosAtt* seos_att, uint16_t handle, BitBuffer* content);
+void seos_att_write_request(SeosAtt* seos_att, BitBuffer* content);
+void seos_att_process_payload(void* context, BitBuffer* message);
+void seos_att_central_connection_start(void* context);
+
+void seos_att_set_on_subscribe_callback(
+    SeosAtt* seos_att,
+    SeosAttOnSubscribeCallback callback,
+    void* context);
+
+void seos_att_set_write_request_callback(
+    SeosAtt* seos_att,
+    SeosAttWriteRequestCallback callback,
+    void* context);
+
+void seos_att_set_notify_callback(SeosAtt* seos_att, SeosAttNotifyCallback callback, void* context);

+ 4 - 0
seos_att_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_att.h"

+ 176 - 0
seos_central.c

@@ -0,0 +1,176 @@
+#include "seos_central_i.h"
+
+#define TAG "SeosCentral"
+
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t file_not_found[] = {0x6A, 0x82};
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+
+SeosCentral* seos_central_alloc(Seos* seos) {
+    SeosCentral* seos_central = malloc(sizeof(SeosCentral));
+    memset(seos_central, 0, sizeof(SeosCentral));
+    seos_central->seos = seos;
+    seos_central->credential = &seos->credential;
+
+    seos_central->phase = SELECT_AID;
+    // Using DES for greater compatibilty
+    seos_central->params.cipher = TWO_KEY_3DES_CBC_MODE;
+    seos_central->params.hash = SHA1;
+
+    memset(seos_central->params.rndICC, 0x0d, sizeof(seos_central->params.rndICC));
+    memset(seos_central->params.rNonce, 0x0c, sizeof(seos_central->params.rNonce));
+
+    seos_central->secure_messaging = NULL;
+
+    seos_central->seos_att = seos_att_alloc(seos);
+    seos_att_set_notify_callback(seos_central->seos_att, seos_central_notify, seos_central);
+
+    return seos_central;
+}
+
+void seos_central_free(SeosCentral* seos_central) {
+    furi_assert(seos_central);
+    seos_att_free(seos_central->seos_att);
+    free(seos_central);
+}
+
+void seos_central_start(SeosCentral* seos_central, FlowMode mode) {
+    seos_att_start(seos_central->seos_att, BLE_CENTRAL, mode);
+}
+
+void seos_central_stop(SeosCentral* seos_central) {
+    seos_att_stop(seos_central->seos_att);
+}
+
+void seos_central_notify(void* context, const uint8_t* buffer, size_t buffer_len) {
+    SeosCentral* seos_central = (SeosCentral*)context;
+    seos_log_buffer(TAG, "notify", (uint8_t*)buffer, buffer_len);
+    const uint8_t* data = buffer;
+    if(data[0] == 0xe1) {
+        FURI_LOG_W(TAG, "Reader BLE Error code: %02x", data[1]);
+        return;
+    }
+
+    BitBuffer* response = bit_buffer_alloc(128);
+
+    if(data[0] != BLE_START) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+    const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            bit_buffer_append_byte(response, BLE_START);
+            seos_emulator_select_aid(response);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+            seos_central->phase = SELECT_ADF;
+        } else {
+            bit_buffer_append_byte(response, BLE_START);
+            bit_buffer_append_bytes(response, (uint8_t*)file_not_found, sizeof(file_not_found));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
+
+            bit_buffer_append_byte(response, BLE_START);
+
+            seos_emulator_select_adf(&seos_central->params, seos_central->credential, response);
+
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+            seos_central->phase = GENERAL_AUTHENTICATION_1;
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        bit_buffer_append_byte(response, BLE_START);
+
+        seos_emulator_general_authenticate_1(response, seos_central->params);
+
+        bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        seos_central->phase = GENERAL_AUTHENTICATION_2;
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        bit_buffer_append_byte(response, BLE_START);
+
+        if(seos_emulator_general_authenticate_2(
+               apdu, buffer_len - 1, seos_central->credential, &seos_central->params, response)) {
+            FURI_LOG_I(TAG, "Authenticated");
+
+            view_dispatcher_send_custom_event(
+                seos_central->seos->view_dispatcher, SeosCustomEventAuthenticated);
+            seos_central->secure_messaging = secure_messaging_alloc(&seos_central->params);
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+        } else {
+            bit_buffer_reset(response);
+        }
+        seos_central->phase = REQUEST_SIO;
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        bit_buffer_append_byte(response, BLE_START);
+
+        if(seos_central->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 1 BLE_START byte
+            size_t bytes_to_ignore = 1;
+            BitBuffer* tmp = bit_buffer_alloc(buffer_len);
+            bit_buffer_append_bytes(tmp, buffer + bytes_to_ignore, buffer_len - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "NFC received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_central->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "NFC received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos_central->seos->view_dispatcher, SeosCustomEventSIORequested);
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_central->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file, seos_central->credential->sio, seos_central->credential->sio_len);
+
+                seos_log_bitbuffer(TAG, "sio_file", sio_file);
+                secure_messaging_wrap_rapdu(
+                    seos_central->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    response);
+
+                bit_buffer_free(sio_file);
+            } else {
+                FURI_LOG_W(TAG, "Did not match the cleartext request");
+            }
+            bit_buffer_append_bytes(response, (uint8_t*)success, sizeof(success));
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(response, no_sm, sizeof(no_sm));
+        }
+
+    } else {
+        FURI_LOG_W(TAG, "no match for attribute_value");
+    }
+
+    if(bit_buffer_get_size_bytes(response) > 0) {
+        seos_att_write_request(seos_central->seos_att, response);
+    }
+    bit_buffer_free(response);
+}

+ 33 - 0
seos_central.h

@@ -0,0 +1,33 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+#include "seos_common.h"
+#include "seos.h"
+#include "seos_att.h"
+#include "keys.h"
+
+#define BLE_START 0xc0
+
+typedef struct {
+    Seos* seos;
+    SeosAtt* seos_att;
+
+    SeosPhase phase;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+} SeosCentral;
+
+SeosCentral* seos_central_alloc(Seos* seos);
+void seos_central_free(SeosCentral* seos_central);
+void seos_central_start(SeosCentral* seos_central, FlowMode mode);
+void seos_central_stop(SeosCentral* seos_central);
+void seos_central_notify(void* context, const uint8_t* buffer, size_t buffer_len);

+ 4 - 0
seos_central_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_central.h"
+#include "seos_i.h"

+ 348 - 0
seos_characteristic.c

@@ -0,0 +1,348 @@
+#include "seos_characteristic_i.h"
+
+#define TAG "SeosCharacteristic"
+
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t cd02[] = {0xcd, 0x02};
+
+static uint8_t ga1_response[] = {0x7c, 0x0a, 0x81, 0x08};
+
+// Emulation
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t file_not_found[] = {0x6A, 0x82};
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+
+SeosCharacteristic* seos_characteristic_alloc(Seos* seos) {
+    SeosCharacteristic* seos_characteristic = malloc(sizeof(SeosCharacteristic));
+    memset(seos_characteristic, 0, sizeof(SeosCharacteristic));
+    seos_characteristic->seos = seos;
+    seos_characteristic->credential = &seos->credential;
+
+    seos_characteristic->phase = SELECT_AID;
+    seos_characteristic->secure_messaging = NULL;
+
+    seos_characteristic->params.key_no = 1;
+    memset(seos_characteristic->params.cNonce, 0x0c, sizeof(seos_characteristic->params.cNonce));
+    memset(seos_characteristic->params.UID, 0x0d, sizeof(seos_characteristic->params.UID));
+
+    seos_characteristic->seos_att = seos_att_alloc(seos);
+
+    seos_att_set_on_subscribe_callback(
+        seos_characteristic->seos_att, seos_characteristic_on_subscribe, seos_characteristic);
+
+    seos_att_set_write_request_callback(
+        seos_characteristic->seos_att, seos_characteristic_write_request, seos_characteristic);
+
+    return seos_characteristic;
+}
+
+void seos_characteristic_free(SeosCharacteristic* seos_characteristic) {
+    furi_assert(seos_characteristic);
+    seos_att_free(seos_characteristic->seos_att);
+    free(seos_characteristic);
+}
+
+void seos_characteristic_start(SeosCharacteristic* seos_characteristic, FlowMode mode) {
+    seos_characteristic->flow_mode = mode;
+    if(seos_characteristic->flow_mode == FLOW_CRED) {
+        seos_characteristic->params.key_no = 0;
+        seos_characteristic->params.cipher = TWO_KEY_3DES_CBC_MODE;
+        seos_characteristic->params.hash = SHA1;
+
+        memset(
+            seos_characteristic->params.rndICC, 0x0d, sizeof(seos_characteristic->params.rndICC));
+        memset(
+            seos_characteristic->params.rNonce, 0x0c, sizeof(seos_characteristic->params.rNonce));
+        memset(seos_characteristic->params.UID, 0x00, sizeof(seos_characteristic->params.UID));
+        memset(
+            seos_characteristic->params.cNonce, 0x00, sizeof(seos_characteristic->params.cNonce));
+    }
+    seos_att_start(seos_characteristic->seos_att, BLE_PERIPHERAL, mode);
+}
+
+void seos_characteristic_stop(SeosCharacteristic* seos_characteristic) {
+    seos_att_stop(seos_characteristic->seos_att);
+}
+
+void seos_characteristic_reader_flow(
+    SeosCharacteristic* seos_characteristic,
+    BitBuffer* attribute_value,
+    BitBuffer* payload) {
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+    const uint8_t* rx_data = data + 1; // Match name to nfc version for easier copying
+
+    // 022f20180014000400
+    // 520c00
+    // c0 6f0c840a a0000004400001010001
+    // 9000
+    if(memcmp(data + 5, standard_seos_aid, sizeof(standard_seos_aid)) == 0) { // response to select
+        FURI_LOG_I(TAG, "Select ADF");
+        uint8_t select_adf_header[] = {
+            0x80, 0xa5, 0x04, 0x00, (uint8_t)SEOS_ADF_OID_LEN + 2, 0x06, (uint8_t)SEOS_ADF_OID_LEN};
+
+        bit_buffer_append_bytes(payload, select_adf_header, sizeof(select_adf_header));
+        bit_buffer_append_bytes(payload, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+        seos_characteristic->phase = SELECT_ADF;
+    } else if(memcmp(data + 1, cd02, sizeof(cd02)) == 0) {
+        if(seos_reader_select_adf_response(
+               attribute_value, 1, seos_characteristic->credential, &seos_characteristic->params)) {
+            // Craft response
+            general_authenticate_1[3] = seos_characteristic->params.key_no;
+            bit_buffer_append_bytes(
+                payload, general_authenticate_1, sizeof(general_authenticate_1));
+            seos_characteristic->phase = GENERAL_AUTHENTICATION_1;
+        }
+    } else if(memcmp(data + 1, ga1_response, sizeof(ga1_response)) == 0) {
+        memcpy(seos_characteristic->params.rndICC, data + 5, 8);
+
+        // Craft response
+        uint8_t cryptogram[32 + 8];
+        memset(cryptogram, 0, sizeof(cryptogram));
+        seos_reader_generate_cryptogram(
+            seos_characteristic->credential, &seos_characteristic->params, cryptogram);
+
+        uint8_t ga_header[] = {
+            0x00,
+            0x87,
+            0x00,
+            seos_characteristic->params.key_no,
+            sizeof(cryptogram) + 4,
+            0x7c,
+            sizeof(cryptogram) + 2,
+            0x82,
+            sizeof(cryptogram)};
+
+        bit_buffer_append_bytes(payload, ga_header, sizeof(ga_header));
+        bit_buffer_append_bytes(payload, cryptogram, sizeof(cryptogram));
+
+        seos_characteristic->phase = GENERAL_AUTHENTICATION_2;
+    } else if(rx_data[0] == 0x7C && rx_data[2] == 0x82) { // ga2 response
+        if(rx_data[3] == 40) {
+            if(!seos_reader_verify_cryptogram(&seos_characteristic->params, rx_data + 4)) {
+                FURI_LOG_W(TAG, "Card cryptogram failed verification");
+                return;
+            }
+            FURI_LOG_I(TAG, "Authenticated");
+            view_dispatcher_send_custom_event(
+                seos_characteristic->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        } else {
+            FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
+        }
+
+        seos_characteristic->secure_messaging =
+            secure_messaging_alloc(&seos_characteristic->params);
+
+        SecureMessaging* secure_messaging = seos_characteristic->secure_messaging;
+
+        uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
+        secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), payload);
+        seos_characteristic->phase = REQUEST_SIO;
+        view_dispatcher_send_custom_event(
+            seos_characteristic->seos->view_dispatcher, SeosCustomEventSIORequested);
+    } else if(seos_characteristic->phase == REQUEST_SIO) {
+        SecureMessaging* secure_messaging = seos_characteristic->secure_messaging;
+
+        BitBuffer* rx_buffer = bit_buffer_alloc(bit_buffer_get_size_bytes(attribute_value) - 1);
+        bit_buffer_append_bytes(
+            rx_buffer, rx_data, bit_buffer_get_size_bytes(attribute_value) - 1);
+        seos_log_bitbuffer(TAG, "BLE response(wrapped)", rx_buffer);
+        secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
+        seos_log_bitbuffer(TAG, "BLE response(clear)", rx_buffer);
+
+        // Skip fileId
+        seos_characteristic->credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
+        if(seos_characteristic->credential->sio_len >
+           sizeof(seos_characteristic->credential->sio)) {
+            FURI_LOG_W(TAG, "SIO too long to save");
+            return;
+        }
+        memcpy(
+            seos_characteristic->credential->sio,
+            bit_buffer_get_data(rx_buffer) + 3,
+            seos_characteristic->credential->sio_len);
+        FURI_LOG_I(TAG, "SIO Captured, %d bytes", seos_characteristic->credential->sio_len);
+
+        Seos* seos = seos_characteristic->seos;
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
+        bit_buffer_free(rx_buffer);
+
+        seos_characteristic->phase = SELECT_AID;
+    } else if(data[0] == 0xe1) {
+        //ignore
+    } else {
+        FURI_LOG_W(TAG, "No match for write request");
+    }
+}
+
+void seos_characteristic_cred_flow(
+    SeosCharacteristic* seos_characteristic,
+    BitBuffer* attribute_value,
+    BitBuffer* payload) {
+    UNUSED(seos_characteristic);
+
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+    const uint8_t* apdu = data + 1; // Match name to nfc version for easier copying
+
+    if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+        if(memcmp(apdu + sizeof(select_header) + 1, standard_seos_aid, sizeof(standard_seos_aid)) ==
+           0) {
+            seos_emulator_select_aid(payload);
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        } else {
+            bit_buffer_append_bytes(payload, (uint8_t*)file_not_found, sizeof(file_not_found));
+        }
+    } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+        // is our adf in the list?
+        // +1 to skip APDU length byte
+        void* p = memmem(
+            apdu + sizeof(select_adf_header) + 1,
+            apdu[sizeof(select_adf_header)],
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN);
+        if(p) {
+            seos_log_buffer(TAG, "Matched ADF", p, SEOS_ADF_OID_LEN);
+
+            seos_emulator_select_adf(
+                &seos_characteristic->params, seos_characteristic->credential, payload);
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        } else {
+            FURI_LOG_W(TAG, "Failed to match any ADF OID");
+        }
+
+    } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+        seos_emulator_general_authenticate_1(payload, seos_characteristic->params);
+        bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+    } else if(memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) == 0) {
+        if(!seos_emulator_general_authenticate_2(
+               apdu,
+               bit_buffer_get_size_bytes(attribute_value),
+               seos_characteristic->credential,
+               &seos_characteristic->params,
+               payload)) {
+            FURI_LOG_W(TAG, "Failure in General Authenticate 2");
+        } else {
+            bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+        }
+
+        view_dispatcher_send_custom_event(
+            seos_characteristic->seos->view_dispatcher, SeosCustomEventAuthenticated);
+        // Prepare for future communication
+        seos_characteristic->secure_messaging =
+            secure_messaging_alloc(&seos_characteristic->params);
+    } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+        uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+        if(seos_characteristic->secure_messaging) {
+            FURI_LOG_D(TAG, "Unwrap secure message");
+
+            // c0 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+            // Ignore 1 BLE_START byte
+            size_t bytes_to_ignore = 1;
+            BitBuffer* tmp = bit_buffer_alloc(bit_buffer_get_size_bytes(attribute_value));
+            bit_buffer_append_bytes(
+                tmp,
+                bit_buffer_get_data(attribute_value) + bytes_to_ignore,
+                bit_buffer_get_size_bytes(attribute_value) - bytes_to_ignore);
+
+            seos_log_bitbuffer(TAG, "received(wrapped)", tmp);
+            secure_messaging_unwrap_apdu(seos_characteristic->secure_messaging, tmp);
+            seos_log_bitbuffer(TAG, "received(clear)", tmp);
+
+            const uint8_t* message = bit_buffer_get_data(tmp);
+            if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                view_dispatcher_send_custom_event(
+                    seos_characteristic->seos->view_dispatcher, SeosCustomEventSIORequested);
+
+                BitBuffer* sio_file = bit_buffer_alloc(128);
+                bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                bit_buffer_append_byte(sio_file, seos_characteristic->credential->sio_len);
+                bit_buffer_append_bytes(
+                    sio_file,
+                    seos_characteristic->credential->sio,
+                    seos_characteristic->credential->sio_len);
+
+                secure_messaging_wrap_rapdu(
+                    seos_characteristic->secure_messaging,
+                    (uint8_t*)bit_buffer_get_data(sio_file),
+                    bit_buffer_get_size_bytes(sio_file),
+                    payload);
+                bit_buffer_append_bytes(payload, (uint8_t*)success, sizeof(success));
+
+                bit_buffer_free(sio_file);
+            }
+
+            bit_buffer_free(tmp);
+        } else {
+            uint8_t no_sm[] = {0x69, 0x88};
+            bit_buffer_append_bytes(payload, no_sm, sizeof(no_sm));
+        }
+    } else if(data[0] == 0xe1) {
+        // ignore
+    } else {
+        FURI_LOG_W(TAG, "no match for attribute_value");
+    }
+}
+
+void seos_characteristic_write_request(void* context, BitBuffer* attribute_value) {
+    SeosCharacteristic* seos_characteristic = (SeosCharacteristic*)context;
+    seos_log_bitbuffer(TAG, "write request", attribute_value);
+
+    BitBuffer* payload = bit_buffer_alloc(128); // TODO: MTU
+    const uint8_t* data = bit_buffer_get_data(attribute_value);
+
+    if(data[0] != BLE_START && data[0] != 0xe1) {
+        FURI_LOG_W(TAG, "Unexpected start of BLE packet");
+    }
+
+    if(seos_characteristic->flow_mode == FLOW_READER) {
+        seos_characteristic_reader_flow(seos_characteristic, attribute_value, payload);
+    } else if(seos_characteristic->flow_mode == FLOW_CRED) {
+        seos_characteristic_cred_flow(seos_characteristic, attribute_value, payload);
+    }
+
+    if(bit_buffer_get_size_bytes(payload) > 0) {
+        BitBuffer* tx = bit_buffer_alloc(1 + 2 + 1 + bit_buffer_get_size_bytes(payload));
+
+        bit_buffer_append_byte(tx, BLE_START);
+        bit_buffer_append_bytes(
+            tx, bit_buffer_get_data(payload), bit_buffer_get_size_bytes(payload));
+
+        seos_att_notify(seos_characteristic->seos_att, seos_characteristic->handle, tx);
+        bit_buffer_free(tx);
+    }
+
+    bit_buffer_free(payload);
+}
+
+void seos_characteristic_on_subscribe(void* context, uint16_t handle) {
+    SeosCharacteristic* seos_characteristic = (SeosCharacteristic*)context;
+    FURI_LOG_D(TAG, "seos_characteristic_on_subscribe %04x", handle);
+    /*
+    if(seos_characteristic->handle != 0) {
+        FURI_LOG_W(TAG, "Ignoring subscribe; already subscribed");
+        return;
+    }
+    */
+
+    seos_characteristic->handle = handle;
+
+    // Send initial select
+    uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00, (uint8_t)sizeof(standard_seos_aid)};
+
+    BitBuffer* tx = bit_buffer_alloc(1 + sizeof(select_header) + sizeof(standard_seos_aid));
+
+    bit_buffer_append_byte(tx, BLE_START);
+    bit_buffer_append_bytes(tx, select_header, sizeof(select_header));
+    bit_buffer_append_bytes(tx, standard_seos_aid, sizeof(standard_seos_aid));
+    seos_log_bitbuffer(TAG, "initial select", tx);
+
+    seos_att_notify(seos_characteristic->seos_att, seos_characteristic->handle, tx);
+    seos_characteristic->phase = SELECT_AID;
+    bit_buffer_free(tx);
+}

+ 36 - 0
seos_characteristic.h

@@ -0,0 +1,36 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+#include "seos_common.h"
+#include "seos.h"
+#include "seos_att.h"
+#include "keys.h"
+
+#define BLE_START 0xc0
+
+typedef struct {
+    Seos* seos;
+    SeosAtt* seos_att;
+    uint16_t handle;
+
+    SeosPhase phase;
+
+    FlowMode flow_mode;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+    SeosCredential* credential;
+} SeosCharacteristic;
+
+SeosCharacteristic* seos_characteristic_alloc(Seos* seos);
+void seos_characteristic_free(SeosCharacteristic* seos_characteristic);
+void seos_characteristic_start(SeosCharacteristic* seos_characteristic, FlowMode mode);
+void seos_characteristic_stop(SeosCharacteristic* seos_characteristic);
+void seos_characteristic_write_request(void* context, BitBuffer* attribute_value);
+void seos_characteristic_on_subscribe(void* context, uint16_t handle);

+ 4 - 0
seos_characteristic_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_characteristic.h"
+#include "seos_i.h"

+ 145 - 0
seos_common.c

@@ -0,0 +1,145 @@
+#include "seos_common.h"
+
+char* seos_file_header = "Flipper Seos Credential";
+uint32_t seos_file_version = 1;
+
+void seos_log_buffer(char* TAG, char* prefix, uint8_t* buffer, size_t buffer_len) {
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)SEOS_WORKER_MAX_BUFFER_SIZE, buffer_len);
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < limit; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", buffer[i]);
+    }
+    if(prefix) {
+        FURI_LOG_D(TAG, "%s %d: %s", prefix, limit, display);
+    } else {
+        FURI_LOG_D(TAG, "Buffer %d: %s", limit, display);
+    }
+}
+
+void seos_log_bitbuffer(char* TAG, char* prefix, BitBuffer* buffer) {
+    furi_assert(buffer);
+
+    size_t length = bit_buffer_get_size_bytes(buffer);
+    const uint8_t* data = bit_buffer_get_data(buffer);
+
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)SEOS_WORKER_MAX_BUFFER_SIZE, length);
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < limit; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", data[i]);
+    }
+    if(prefix) {
+        FURI_LOG_D(TAG, "%s %d: %s", prefix, length, display);
+    } else {
+        FURI_LOG_D(TAG, "Buffer %d: %s", length, display);
+    }
+}
+
+void seos_common_copy_credential(const SeosCredential* src, SeosCredential* dst) {
+    furi_assert(src);
+    furi_assert(dst);
+
+    dst->diversifier_len = src->diversifier_len;
+    memcpy(dst->diversifier, src->diversifier, dst->diversifier_len);
+    dst->sio_len = src->sio_len;
+    memcpy(dst->sio, src->sio, dst->sio_len);
+}
+
+void seos_worker_diversify_key(
+    uint8_t master_key_value[16],
+    uint8_t* diversifier,
+    size_t diversifier_len,
+    uint8_t* adf_oid,
+    size_t adf_oid_len,
+    uint8_t algo_id1,
+    uint8_t algo_id2,
+    uint8_t keyId,
+    bool is_encryption,
+    uint8_t* div_key) {
+    char* TAG = "SeosCommon";
+    // 0000000000000000000000 04 00 0080 01 0907 01 2B0601040181E4380101020118010102 3D50AD518CD820
+    size_t index = 0;
+    uint8_t buffer[128];
+    memset(buffer, 0, sizeof(buffer));
+    index += 11;
+    buffer[index++] = is_encryption ? 0x04 : 0x06;
+    index++; // separation
+    index++; // 0x00 that goes with 0x80 to indicate 128bit key
+    buffer[index++] = 0x80;
+    buffer[index++] = 0x01; // i
+    buffer[index++] = algo_id1;
+    buffer[index++] = algo_id2;
+    buffer[index++] = keyId;
+    memcpy(buffer + index, adf_oid, adf_oid_len);
+    index += adf_oid_len;
+    memcpy(buffer + index, diversifier, diversifier_len);
+    index += diversifier_len;
+
+    aes_cmac(master_key_value, 16, buffer, index, div_key);
+
+    char display[33];
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < 16; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", div_key[i]);
+    }
+    FURI_LOG_I(TAG, "Diversified %s key: %s", is_encryption ? "Encrypt" : "Mac", display);
+}
+
+void seos_worker_aes_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear) {
+    uint8_t iv[16];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_dec(&ctx, key, 128);
+    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT, length, iv, encrypted, clear);
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_worker_des_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear) {
+    uint8_t iv[8];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_dec(&ctx, key);
+    mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, length, iv, encrypted, clear);
+    mbedtls_des3_free(&ctx);
+}
+
+void seos_worker_aes_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted) {
+    uint8_t iv[16];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, key, 128);
+    mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_ENCRYPT, length, iv, clear, encrypted);
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_worker_des_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted) {
+    uint8_t iv[8];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, key);
+    mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, length, iv, clear, encrypted);
+    mbedtls_des3_free(&ctx);
+}

+ 110 - 0
seos_common.h

@@ -0,0 +1,110 @@
+#pragma once
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "aes_cmac.h"
+#include "des_cmac.h"
+
+#define TWO_KEY_3DES_CBC_MODE   2
+#define THREE_KEY_3DES_CBC_MODE 4
+#define SHA1                    6
+#define SHA256                  7
+#define AES_128_CBC             9
+
+#define SEOS_WORKER_MAX_BUFFER_SIZE 128
+#define SEOS_WORKER_CMAC_SIZE       8
+
+#define SEOS_APP_EXTENSION        ".seos"
+#define SEOS_FILE_NAME_MAX_LENGTH 32
+
+extern char* seos_file_header;
+extern uint32_t seos_file_version;
+
+typedef enum {
+    BLE_PERIPHERAL,
+    BLE_CENTRAL,
+} BleMode;
+
+typedef enum {
+    FLOW_READER,
+    FLOW_CRED,
+    FLOW_READER_SCANNER,
+    FLOW_CRED_SCANNER,
+} FlowMode;
+
+typedef enum {
+    SELECT_AID,
+    SELECT_ADF,
+    GENERAL_AUTHENTICATION_1,
+    GENERAL_AUTHENTICATION_2,
+    REQUEST_SIO,
+} SeosPhase;
+
+typedef struct {
+    uint8_t diversifier[16];
+    size_t diversifier_len;
+    uint8_t sio[128];
+    size_t sio_len;
+    uint8_t priv_key[16];
+    uint8_t auth_key[16];
+    uint8_t adf_response[72];
+} SeosCredential;
+
+typedef struct {
+    uint8_t rndICC[8];
+    uint8_t UID[8];
+    uint8_t cNonce[16];
+    uint8_t rNonce[16];
+    uint8_t priv_key[16];
+    uint8_t auth_key[16];
+    uint8_t key_no;
+    uint8_t cipher;
+    uint8_t hash;
+} AuthParameters;
+
+void seos_log_bitbuffer(char* TAG, char* prefix, BitBuffer* buffer);
+void seos_log_buffer(char* TAG, char* prefix, uint8_t* buffer, size_t buffer_len);
+
+void seos_common_copy_credential(const SeosCredential* src, SeosCredential* dst);
+
+void seos_worker_diversify_key(
+    uint8_t master_key_value[16],
+    uint8_t* diversifier,
+    size_t diversifier_len,
+    uint8_t* adf_oid,
+    size_t adf_oid_len,
+    uint8_t algo_id1,
+    uint8_t algo_id2,
+    uint8_t reference_qualifier,
+    bool is_encryption,
+    uint8_t* div_key);
+
+void seos_worker_aes_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear);
+void seos_worker_des_decrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* encrypted,
+    uint8_t* clear);
+
+void seos_worker_aes_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted);
+void seos_worker_des_encrypt(
+    uint8_t key[16],
+    size_t length,
+    const uint8_t* clear,
+    uint8_t* encrypted);

+ 652 - 0
seos_emulator.c

@@ -0,0 +1,652 @@
+#include "seos_emulator_i.h"
+
+#define TAG "SeosEmulator"
+
+#define NAD_MASK 0x08
+
+static uint8_t select_header[] = {0x00, 0xa4, 0x04, 0x00};
+static uint8_t standard_seos_aid[] = {0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+
+static uint8_t SEOS_APPLET_FCI[] =
+    {0x6F, 0x0C, 0x84, 0x0A, 0xA0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+static uint8_t FILE_NOT_FOUND[] = {0x6A, 0x82};
+static uint8_t success[] = {0x90, 0x00};
+
+static uint8_t select_adf_header[] = {0x80, 0xa5, 0x04, 0x00};
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+static uint8_t general_authenticate_1_response_header[] = {0x7c, 0x0a, 0x81, 0x08};
+static uint8_t general_authenticate_2_header[] = {0x00, 0x87, 0x00, 0x01};
+static uint8_t secure_messaging_header[] = {0x0c, 0xcb, 0x3f, 0xff};
+static uint8_t empty[16] =
+    {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+
+SeosEmulator* seos_emulator_alloc(SeosCredential* credential) {
+    SeosEmulator* seos_emulator = malloc(sizeof(SeosEmulator));
+    memset(seos_emulator, 0, sizeof(SeosEmulator));
+
+    // Using DES for greater compatibilty
+    seos_emulator->params.cipher = TWO_KEY_3DES_CBC_MODE;
+    seos_emulator->params.hash = SHA1;
+
+    memset(seos_emulator->params.rndICC, 0x0d, sizeof(seos_emulator->params.rndICC));
+    memset(seos_emulator->params.rNonce, 0x0c, sizeof(seos_emulator->params.rNonce));
+    seos_emulator->credential = credential;
+
+    seos_emulator->secure_messaging = NULL;
+
+    seos_emulator->storage = furi_record_open(RECORD_STORAGE);
+    seos_emulator->dialogs = furi_record_open(RECORD_DIALOGS);
+    seos_emulator->load_path = furi_string_alloc();
+    seos_emulator->tx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+
+    return seos_emulator;
+}
+
+void seos_emulator_free(SeosEmulator* seos_emulator) {
+    furi_assert(seos_emulator);
+
+    if(seos_emulator->secure_messaging) {
+        secure_messaging_free(seos_emulator->secure_messaging);
+    }
+
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+    furi_string_free(seos_emulator->load_path);
+    bit_buffer_free(seos_emulator->tx_buffer);
+    free(seos_emulator);
+}
+
+void seos_emulator_set_loading_callback(
+    SeosEmulator* seos_emulator,
+    SeosLoadingCallback callback,
+    void* context) {
+    furi_assert(seos_emulator);
+
+    seos_emulator->loading_cb = callback;
+    seos_emulator->loading_cb_ctx = context;
+}
+
+static bool
+    seos_emulator_file_load(SeosEmulator* seos_emulator, FuriString* path, bool show_dialog) {
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(seos_emulator->storage);
+    FuriString* temp_str;
+    temp_str = furi_string_alloc();
+    bool deprecated_version = false;
+
+    if(seos_emulator->loading_cb) {
+        seos_emulator->loading_cb(seos_emulator->loading_cb_ctx, true);
+    }
+
+    memset(
+        seos_emulator->credential->diversifier, 0, sizeof(seos_emulator->credential->diversifier));
+    memset(seos_emulator->credential->sio, 0, sizeof(seos_emulator->credential->sio));
+    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, seos_file_header) || (version != seos_file_version)) {
+            deprecated_version = true;
+            break;
+        }
+
+        if(!flipper_format_read_uint32(
+               file,
+               "Diversifier Length",
+               (uint32_t*)&(seos_emulator->credential->diversifier_len),
+               1))
+            break;
+        if(!flipper_format_read_hex(
+               file,
+               "Diversifier",
+               seos_emulator->credential->diversifier,
+               seos_emulator->credential->diversifier_len))
+            break;
+
+        if(!flipper_format_read_uint32(
+               file, "SIO Length", (uint32_t*)&(seos_emulator->credential->sio_len), 1))
+            break;
+        if(!flipper_format_read_hex(
+               file, "SIO", seos_emulator->credential->sio, seos_emulator->credential->sio_len))
+            break;
+
+        // optional
+        memset(
+            seos_emulator->credential->priv_key, 0, sizeof(seos_emulator->credential->priv_key));
+        memset(
+            seos_emulator->credential->auth_key, 0, sizeof(seos_emulator->credential->auth_key));
+        memset(
+            seos_emulator->credential->adf_response,
+            0,
+            sizeof(seos_emulator->credential->adf_response));
+        flipper_format_read_hex(
+            file,
+            "Priv Key",
+            seos_emulator->credential->priv_key,
+            sizeof(seos_emulator->credential->priv_key));
+        flipper_format_read_hex(
+            file,
+            "Auth Key",
+            seos_emulator->credential->auth_key,
+            sizeof(seos_emulator->credential->auth_key));
+        if(memcmp(seos_emulator->credential->priv_key, empty, sizeof(empty)) != 0) {
+            FURI_LOG_I(TAG, "+ Priv Key");
+        }
+        if(memcmp(seos_emulator->credential->priv_key, empty, sizeof(empty)) != 0) {
+            FURI_LOG_I(TAG, "+ Auth Key");
+        }
+        flipper_format_read_hex(
+            file,
+            "ADF Response",
+            seos_emulator->credential->adf_response,
+            sizeof(seos_emulator->credential->adf_response));
+        parsed = true;
+    } while(false);
+
+    if(seos_emulator->loading_cb) {
+        seos_emulator->loading_cb(seos_emulator->loading_cb_ctx, false);
+    }
+
+    if((!parsed) && (show_dialog)) {
+        if(deprecated_version) {
+            dialog_message_show_storage_error(seos_emulator->dialogs, "File format deprecated");
+        } else {
+            dialog_message_show_storage_error(seos_emulator->dialogs, "Can not parse\nfile");
+        }
+    }
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool seos_emulator_file_select(SeosEmulator* seos_emulator) {
+    furi_assert(seos_emulator);
+    bool res = false;
+
+    FuriString* seos_app_folder = furi_string_alloc_set(STORAGE_APP_DATA_PATH_PREFIX);
+
+    DialogsFileBrowserOptions browser_options;
+    dialog_file_browser_set_basic_options(&browser_options, SEOS_APP_EXTENSION, &I_Nfc_10px);
+    browser_options.base_path = STORAGE_APP_DATA_PATH_PREFIX;
+
+    res = dialog_file_browser_show(
+        seos_emulator->dialogs, seos_emulator->load_path, seos_app_folder, &browser_options);
+
+    furi_string_free(seos_app_folder);
+    if(res) {
+        FuriString* filename;
+        filename = furi_string_alloc();
+        path_extract_filename(seos_emulator->load_path, filename, true);
+        strncpy(seos_emulator->name, furi_string_get_cstr(filename), SEOS_FILE_NAME_MAX_LENGTH);
+        res = seos_emulator_file_load(seos_emulator, seos_emulator->load_path, true);
+        furi_string_free(filename);
+    }
+
+    return res;
+}
+
+bool seos_emulator_delete(SeosEmulator* seos_emulator, bool use_load_path) {
+    furi_assert(seos_emulator);
+    bool deleted = false;
+    FuriString* file_path;
+    file_path = furi_string_alloc();
+
+    do {
+        // Delete original file
+        if(use_load_path && !furi_string_empty(seos_emulator->load_path)) {
+            furi_string_set(file_path, seos_emulator->load_path);
+        } else {
+            furi_string_printf(
+                file_path, APP_DATA_PATH("%s%s"), seos_emulator->name, SEOS_APP_EXTENSION);
+        }
+        if(!storage_simply_remove(seos_emulator->storage, furi_string_get_cstr(file_path))) break;
+        deleted = true;
+    } while(0);
+
+    if(!deleted) {
+        dialog_message_show_storage_error(seos_emulator->dialogs, "Can not remove file");
+    }
+
+    furi_string_free(file_path);
+    return deleted;
+}
+
+void seos_emulator_select_aid(BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "Select AID");
+    bit_buffer_append_bytes(tx_buffer, SEOS_APPLET_FCI, sizeof(SEOS_APPLET_FCI));
+}
+
+void seos_emulator_general_authenticate_1(BitBuffer* tx_buffer, AuthParameters params) {
+    bit_buffer_append_bytes(
+        tx_buffer,
+        general_authenticate_1_response_header,
+        sizeof(general_authenticate_1_response_header));
+    bit_buffer_append_bytes(tx_buffer, params.rndICC, sizeof(params.rndICC));
+}
+
+// 0a00
+// 00870001 2c7c 2a82 28 bbb4e9156136f27f687e2967865dfe812e33c95ddcf9294a4340d26da3e76db0220d1163c591e5b8 00
+bool seos_emulator_general_authenticate_2(
+    const uint8_t* buffer,
+    size_t buffer_len,
+    SeosCredential* credential,
+    AuthParameters* params,
+    BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "seos_emulator_general_authenticate_2");
+    UNUSED(buffer_len);
+
+    uint8_t* rx_data = (uint8_t*)buffer;
+    uint8_t* cryptogram = rx_data + sizeof(general_authenticate_2_header) + 5;
+    size_t encrypted_len = 32;
+    uint8_t* mac = cryptogram + encrypted_len;
+
+    params->key_no = rx_data[3];
+
+    if(memcmp(credential->priv_key, empty, sizeof(empty)) == 0) {
+        seos_worker_diversify_key(
+            SEOS_ADF1_READ,
+            credential->diversifier,
+            credential->diversifier_len,
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN,
+            params->cipher,
+            params->hash,
+            params->key_no,
+            true,
+            params->priv_key);
+    } else {
+        memcpy(params->priv_key, credential->priv_key, sizeof(params->priv_key));
+    }
+    if(memcmp(credential->auth_key, empty, sizeof(empty)) == 0) {
+        seos_worker_diversify_key(
+            SEOS_ADF1_READ,
+            credential->diversifier,
+            credential->diversifier_len,
+            SEOS_ADF_OID,
+            SEOS_ADF_OID_LEN,
+            params->cipher,
+            params->hash,
+            params->key_no,
+            false,
+            params->auth_key);
+    } else {
+        memcpy(params->auth_key, credential->auth_key, sizeof(params->auth_key));
+    }
+
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        aes_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, encrypted_len, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        des_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, encrypted_len, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+        return false;
+    }
+
+    if(memcmp(cmac, mac, SEOS_WORKER_CMAC_SIZE) != 0) {
+        FURI_LOG_W(TAG, "Incorrect cryptogram mac %02x... vs %02x...", cmac[0], mac[0]);
+        return false;
+    }
+
+    uint8_t clear[32];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    size_t index = 0;
+    memcpy(params->UID, clear + index, sizeof(params->UID));
+    index += sizeof(params->UID);
+    if(memcmp(clear + index, params->rndICC, sizeof(params->rndICC)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect rndICC returned");
+        return false;
+    }
+    index += sizeof(params->rndICC);
+    memcpy(params->cNonce, clear + index, sizeof(params->cNonce));
+    index += sizeof(params->cNonce);
+
+    // Construct response
+    uint8_t response_header[] = {0x7c, 0x2a, 0x82, 0x28};
+    memset(clear, 0, sizeof(clear));
+    memset(cmac, 0, sizeof(cmac));
+    index = 0;
+    memcpy(clear + index, params->rndICC, sizeof(params->rndICC));
+    index += sizeof(params->rndICC);
+    memcpy(clear + index, params->UID, sizeof(params->UID));
+    index += sizeof(params->UID);
+    memcpy(clear + index, params->rNonce, sizeof(params->rNonce));
+    index += sizeof(params->rNonce);
+
+    uint8_t encrypted[32];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(params->priv_key, sizeof(clear), clear, encrypted);
+
+        aes_cmac(params->auth_key, sizeof(params->auth_key), encrypted, sizeof(encrypted), cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(params->priv_key, sizeof(clear), clear, encrypted);
+        des_cmac(params->auth_key, sizeof(params->auth_key), encrypted, sizeof(encrypted), cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    bit_buffer_append_bytes(tx_buffer, response_header, sizeof(response_header));
+    bit_buffer_append_bytes(tx_buffer, encrypted, sizeof(encrypted));
+    bit_buffer_append_bytes(tx_buffer, cmac, SEOS_WORKER_CMAC_SIZE);
+
+    return true;
+}
+
+void seos_emulator_des_adf_payload(SeosCredential* credential, uint8_t* buffer) {
+    // Synethic IV
+    /// random bytes
+    uint8_t rnd[4] = {0, 0, 0, 0};
+    uint8_t cmac[8] = {0};
+    /// cmac
+    des_cmac(SEOS_ADF1_PRIV_MAC, sizeof(SEOS_ADF1_PRIV_MAC), rnd, sizeof(rnd), cmac);
+    uint8_t iv[8];
+    memcpy(iv + 0, rnd, sizeof(rnd));
+    memcpy(iv + sizeof(rnd), cmac, sizeof(iv) - sizeof(rnd));
+
+    // Copy IV to buffer because mbedtls_des3_crypt_cbc mutates it
+    memcpy(buffer + 0, iv, sizeof(iv));
+
+    uint8_t clear[0x30];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+
+    // OID
+    clear[index++] = 0x06;
+    clear[index++] = SEOS_ADF_OID_LEN, memcpy(clear + index, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+    index += SEOS_ADF_OID_LEN;
+    // diversifier
+    clear[index++] = 0xcf;
+    clear[index++] = credential->diversifier_len;
+    memcpy(clear + index, credential->diversifier, credential->diversifier_len);
+    index += credential->diversifier_len;
+
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, SEOS_ADF1_PRIV_ENC);
+    mbedtls_des3_crypt_cbc(
+        &ctx, MBEDTLS_DES_ENCRYPT, sizeof(clear), iv, clear, buffer + sizeof(iv));
+    mbedtls_des3_free(&ctx);
+}
+
+void seos_emulator_aes_adf_payload(SeosCredential* credential, uint8_t* buffer) {
+    // Synethic IV
+    /// random bytes
+    uint8_t rnd[8] = {0, 0, 0, 0, 0, 0, 0, 0};
+    uint8_t cmac[16] = {0};
+    /// cmac
+    aes_cmac(SEOS_ADF1_PRIV_MAC, sizeof(SEOS_ADF1_PRIV_MAC), rnd, sizeof(rnd), cmac);
+    uint8_t iv[16];
+    memcpy(iv + 0, rnd, sizeof(rnd));
+    memcpy(iv + sizeof(rnd), cmac, sizeof(iv) - sizeof(rnd));
+
+    // Copy IV to buffer because mbedtls_aes_crypt_cbc mutates it
+    memcpy(buffer + 0, iv, sizeof(iv));
+
+    uint8_t clear[0x30];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+
+    // OID
+    clear[index++] = 0x06;
+    clear[index++] = SEOS_ADF_OID_LEN;
+    memcpy(clear + index, SEOS_ADF_OID, SEOS_ADF_OID_LEN);
+    index += SEOS_ADF_OID_LEN;
+    // diversifier
+    clear[index++] = 0xcf;
+    clear[index++] = credential->diversifier_len;
+    memcpy(clear + index, credential->diversifier, credential->diversifier_len);
+    index += credential->diversifier_len;
+
+    mbedtls_aes_context ctx;
+    mbedtls_aes_init(&ctx);
+    mbedtls_aes_setkey_enc(&ctx, SEOS_ADF1_PRIV_ENC, sizeof(SEOS_ADF1_PRIV_ENC) * 8);
+    mbedtls_aes_crypt_cbc(
+        &ctx, MBEDTLS_AES_ENCRYPT, sizeof(clear), iv, clear, buffer + sizeof(iv));
+    mbedtls_aes_free(&ctx);
+}
+
+void seos_emulator_select_adf(
+    AuthParameters* params,
+    SeosCredential* credential,
+    BitBuffer* tx_buffer) {
+    FURI_LOG_D(TAG, "Select ADF");
+    // Shortcut if the credential file contained the hardcoded response
+    if(credential->adf_response[2] != 0x00 && credential->adf_response[2] == params->cipher) {
+        FURI_LOG_I(TAG, "Using hardcoded ADF Response");
+        bit_buffer_append_bytes(
+            tx_buffer, credential->adf_response, sizeof(credential->adf_response));
+        seos_log_bitbuffer(TAG, "Select ADF (0xcd02...)", tx_buffer);
+        return;
+    }
+
+    size_t prefix_len = bit_buffer_get_size_bytes(tx_buffer);
+    size_t des_cryptogram_length = 56;
+    size_t aes_cryptogram_length = 64;
+    uint8_t header[] = {0xcd, 0x02, params->cipher, params->hash};
+    bit_buffer_append_bytes(tx_buffer, header, sizeof(header));
+
+    // cryptogram
+    // 06112b0601040181e438010102011801010202 cf 07 3d4c010c71cfa7 e2d0b41a00cc5e494c8d52b6e562592399fe614a
+    uint8_t buffer[64];
+    uint8_t cmac[16];
+    memset(buffer, 0, sizeof(buffer));
+    if(params->cipher == AES_128_CBC) {
+        uint8_t cryptogram_prefix[] = {0x85, aes_cryptogram_length};
+        bit_buffer_append_bytes(tx_buffer, cryptogram_prefix, sizeof(cryptogram_prefix));
+
+        seos_emulator_aes_adf_payload(credential, buffer);
+        bit_buffer_append_bytes(tx_buffer, buffer, aes_cryptogram_length);
+
+        aes_cmac(
+            SEOS_ADF1_PRIV_MAC,
+            sizeof(SEOS_ADF1_PRIV_MAC),
+            (uint8_t*)bit_buffer_get_data(tx_buffer) + prefix_len,
+            bit_buffer_get_size_bytes(tx_buffer) - prefix_len,
+            cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        uint8_t cryptogram_prefix[] = {0x85, des_cryptogram_length};
+        bit_buffer_append_bytes(tx_buffer, cryptogram_prefix, sizeof(cryptogram_prefix));
+
+        seos_emulator_des_adf_payload(credential, buffer);
+        bit_buffer_append_bytes(tx_buffer, buffer, des_cryptogram_length);
+
+        // +2 / -2 is to ignore iso14a framing
+        des_cmac(
+            SEOS_ADF1_PRIV_MAC,
+            sizeof(SEOS_ADF1_PRIV_MAC),
+            (uint8_t*)bit_buffer_get_data(tx_buffer) + prefix_len,
+            bit_buffer_get_size_bytes(tx_buffer) - prefix_len,
+            cmac);
+    }
+
+    uint8_t cmac_prefix[] = {0x8e, 0x08};
+    bit_buffer_append_bytes(tx_buffer, cmac_prefix, sizeof(cmac_prefix));
+    bit_buffer_append_bytes(tx_buffer, cmac, SEOS_WORKER_CMAC_SIZE);
+
+    seos_log_bitbuffer(TAG, "Select ADF (0xcd02...)", tx_buffer);
+}
+
+NfcCommand seos_worker_listener_callback(NfcGenericEvent event, void* context) {
+    furi_assert(context);
+    furi_assert(event.protocol == NfcProtocolIso14443_4a);
+    furi_assert(event.event_data);
+    Seos* seos = context;
+    SeosEmulator* seos_emulator = seos->seos_emulator;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aListenerEvent* iso14443_4a_event = event.event_data;
+    Iso14443_3aListener* iso14443_listener = event.instance;
+    seos_emulator->iso14443_listener = iso14443_listener;
+
+    BitBuffer* tx_buffer = seos_emulator->tx_buffer;
+    bit_buffer_reset(tx_buffer);
+
+    switch(iso14443_4a_event->type) {
+    case Iso14443_4aListenerEventTypeReceivedData:
+        seos_emulator->rx_buffer = iso14443_4a_event->data->buffer;
+        const uint8_t* rx_data = bit_buffer_get_data(seos_emulator->rx_buffer);
+        bool NAD = (rx_data[0] & NAD_MASK) == NAD_MASK;
+        uint8_t offset = NAD ? 2 : 1;
+
+        if(bit_buffer_get_size_bytes(iso14443_4a_event->data->buffer) == offset) {
+            FURI_LOG_I(TAG, "No contents in frame");
+            break;
+        }
+
+        // + x to skip stuff before APDU
+        const uint8_t* apdu = rx_data + offset;
+
+        seos_log_bitbuffer(TAG, "NFC received", seos_emulator->rx_buffer);
+
+        // Some ISO14443a framing I need to figure out
+        bit_buffer_append_bytes(tx_buffer, rx_data, offset);
+
+        if(memcmp(apdu, select_header, sizeof(select_header)) == 0) {
+            if(memcmp(
+                   apdu + sizeof(select_header) + 1,
+                   standard_seos_aid,
+                   sizeof(standard_seos_aid)) == 0) {
+                seos_emulator_select_aid(seos_emulator->tx_buffer);
+                view_dispatcher_send_custom_event(
+                    seos->view_dispatcher, SeosCustomEventAIDSelected);
+            } else {
+                bit_buffer_append_bytes(
+                    seos_emulator->tx_buffer, (uint8_t*)FILE_NOT_FOUND, sizeof(FILE_NOT_FOUND));
+            }
+        } else if(memcmp(apdu, select_adf_header, sizeof(select_adf_header)) == 0) {
+            // is our adf in the list?
+            // +1 to skip APDU length byte
+            void* p = memmem(
+                apdu + sizeof(select_adf_header) + 1,
+                apdu[sizeof(select_adf_header)],
+                SEOS_ADF_OID,
+                SEOS_ADF_OID_LEN);
+            if(p) {
+                BitBuffer* tmp = bit_buffer_alloc(SEOS_ADF_OID_LEN);
+                bit_buffer_append_bytes(tmp, p, SEOS_ADF_OID_LEN);
+                seos_log_bitbuffer(TAG, "Matched ADF", tmp);
+                bit_buffer_free(tmp);
+                view_dispatcher_send_custom_event(
+                    seos->view_dispatcher, SeosCustomEventADFMatched);
+
+                seos_emulator_select_adf(
+                    &seos_emulator->params, seos_emulator->credential, seos_emulator->tx_buffer);
+            } else {
+                FURI_LOG_W(TAG, "Failed to match any ADF OID");
+            }
+        } else if(memcmp(apdu, general_authenticate_1, sizeof(general_authenticate_1)) == 0) {
+            seos_emulator_general_authenticate_1(seos_emulator->tx_buffer, seos_emulator->params);
+        } else if(
+            memcmp(apdu, general_authenticate_2_header, sizeof(general_authenticate_2_header)) ==
+            0) {
+            if(!seos_emulator_general_authenticate_2(
+                   apdu,
+                   bit_buffer_get_size_bytes(seos_emulator->rx_buffer),
+                   seos_emulator->credential,
+                   &seos_emulator->params,
+                   seos_emulator->tx_buffer)) {
+                FURI_LOG_W(TAG, "Failure in General Authenticate 2");
+                ret = NfcCommandStop;
+                return ret;
+            }
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventAuthenticated);
+            // Prepare for future communication
+            seos_emulator->secure_messaging = secure_messaging_alloc(&seos_emulator->params);
+        } else if(memcmp(apdu, secure_messaging_header, sizeof(secure_messaging_header)) == 0) {
+            uint8_t request_sio[] = {0x5c, 0x02, 0xff, 0x00};
+
+            if(seos_emulator->secure_messaging) {
+                FURI_LOG_D(TAG, "Unwrap secure message");
+
+                // 0b00 0ccb3fff 16 8508fa8395d30de4e8e097008e085da7edbd833b002d00
+                // Ignore 2 iso frame bytes
+                size_t bytes_to_ignore = offset;
+                BitBuffer* tmp =
+                    bit_buffer_alloc(bit_buffer_get_size_bytes(seos_emulator->rx_buffer));
+                bit_buffer_append_bytes(
+                    tmp,
+                    bit_buffer_get_data(seos_emulator->rx_buffer) + bytes_to_ignore,
+                    bit_buffer_get_size_bytes(seos_emulator->rx_buffer) - bytes_to_ignore);
+
+                seos_log_bitbuffer(TAG, "NFC received(wrapped)", tmp);
+                secure_messaging_unwrap_apdu(seos_emulator->secure_messaging, tmp);
+                seos_log_bitbuffer(TAG, "NFC received(clear)", tmp);
+
+                const uint8_t* message = bit_buffer_get_data(tmp);
+                if(memcmp(message, request_sio, sizeof(request_sio)) == 0) {
+                    view_dispatcher_send_custom_event(
+                        seos->view_dispatcher, SeosCustomEventSIORequested);
+                    BitBuffer* sio_file = bit_buffer_alloc(128);
+                    bit_buffer_append_bytes(sio_file, message + 2, 2); // fileId
+                    bit_buffer_append_byte(sio_file, seos_emulator->credential->sio_len);
+                    bit_buffer_append_bytes(
+                        sio_file,
+                        seos_emulator->credential->sio,
+                        seos_emulator->credential->sio_len);
+
+                    secure_messaging_wrap_rapdu(
+                        seos_emulator->secure_messaging,
+                        (uint8_t*)bit_buffer_get_data(sio_file),
+                        bit_buffer_get_size_bytes(sio_file),
+                        tx_buffer);
+
+                    bit_buffer_free(sio_file);
+                }
+
+                bit_buffer_free(tmp);
+            } else {
+                uint8_t no_sm[] = {0x69, 0x88};
+                bit_buffer_append_bytes(tx_buffer, no_sm, sizeof(no_sm));
+            }
+        } else {
+            // I'm trying to find a good place to re-assert that we're emulating so we don't get stuck on a previous UI screen when we emulate repeatedly
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventEmulate);
+        }
+
+        if(bit_buffer_get_size_bytes(seos_emulator->tx_buffer) >
+           offset) { // contents belong iso framing
+            bit_buffer_append_bytes(tx_buffer, success, sizeof(success));
+
+            iso14443_crc_append(Iso14443CrcTypeA, tx_buffer);
+
+            seos_log_bitbuffer(TAG, "NFC transmit", seos_emulator->tx_buffer);
+
+            NfcError error = nfc_listener_tx((Nfc*)iso14443_listener, tx_buffer);
+            if(error != NfcErrorNone) {
+                FURI_LOG_W(TAG, "Tx error: %d", error);
+                break;
+            }
+        } else {
+            iso14443_crc_append(Iso14443CrcTypeA, tx_buffer);
+
+            seos_log_bitbuffer(TAG, "NFC transmit", seos_emulator->tx_buffer);
+
+            NfcError error = nfc_listener_tx((Nfc*)iso14443_listener, tx_buffer);
+            if(error != NfcErrorNone) {
+                FURI_LOG_W(TAG, "Tx error: %d", error);
+                break;
+            }
+        }
+        break;
+    case Iso14443_4aListenerEventTypeHalted:
+        FURI_LOG_I(TAG, "Halted");
+        break;
+    case Iso14443_4aListenerEventTypeFieldOff:
+        FURI_LOG_I(TAG, "Field Off");
+        break;
+    }
+
+    return ret;
+}

+ 61 - 0
seos_emulator.h

@@ -0,0 +1,61 @@
+#pragma once
+
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <lib/toolbox/path.h>
+#include <lib/nfc/protocols/nfc_generic_event.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a_listener.h>
+#include <lib/nfc/helpers/iso14443_crc.h>
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+
+typedef void (*SeosLoadingCallback)(void* context, bool state);
+
+typedef struct {
+    Iso14443_3aListener* iso14443_listener;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+
+    AuthParameters params;
+
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+
+    char name[SEOS_FILE_NAME_MAX_LENGTH + 1];
+    FuriString* load_path;
+    SeosLoadingCallback loading_cb;
+    void* loading_cb_ctx;
+    Storage* storage;
+    DialogsApp* dialogs;
+} SeosEmulator;
+
+NfcCommand seos_worker_listener_callback(NfcGenericEvent event, void* context);
+
+SeosEmulator* seos_emulator_alloc(SeosCredential* credential);
+
+void seos_emulator_free(SeosEmulator* seos_emulator);
+
+void seos_emulator_set_loading_callback(
+    SeosEmulator* seos_emulator,
+    SeosLoadingCallback callback,
+    void* context);
+
+bool seos_emulator_file_select(SeosEmulator* seos_emulator);
+bool seos_emulator_delete(SeosEmulator* seos_emulator, bool use_load_path);
+
+void seos_emulator_general_authenticate_1(BitBuffer* tx_buffer, AuthParameters params);
+bool seos_emulator_general_authenticate_2(
+    const uint8_t* buffer,
+    size_t buffer_len,
+    SeosCredential* credential,
+    AuthParameters* params,
+    BitBuffer* tx_buffer);
+
+void seos_emulator_select_aid(BitBuffer* tx_buffer);
+void seos_emulator_select_adf(
+    AuthParameters* params,
+    SeosCredential* credential,
+    BitBuffer* tx_buffer);

+ 5 - 0
seos_emulator_i.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_emulator.h"
+#include "keys.h"

+ 778 - 0
seos_hci.c

@@ -0,0 +1,778 @@
+#include "seos_hci_i.h"
+
+#define TAG "SeosHci"
+
+#define OGF_LINK_CTL   0x01
+#define OCF_DISCONNECT 0x0006
+
+#define OGF_HOST_CTL                0x03
+#define OCF_SET_EVENT_MASK          0x0001
+#define OCF_RESET                   0x0003
+#define OCF_READ_LE_HOST_SUPPORTED  0x006c
+#define OCF_WRITE_LE_HOST_SUPPORTED 0x006d
+
+#define OGF_INFO_PARAM         0x04
+#define OCF_READ_LOCAL_VERSION 0x0001
+#define OCF_READ_BUFFER_SIZE   0x0005
+#define OCF_READ_BD_ADDR       0x0009
+
+#define OGF_STATUS_PARAM 0x05
+#define OCF_READ_RSSI    0x0005
+
+#define OGF_LE_CTL                           0x08
+#define OCF_LE_SET_EVENT_MASK                0x0001
+#define OCF_LE_READ_BUFFER_SIZE              0x0002
+#define OCF_LE_READ_LOCAL_SUPPORTED_FEATURES 0x0003
+#define OCF_LE_SET_RANDOM_ADDRESS            0x0005
+#define OCF_LE_SET_ADVERTISING_PARAMETERS    0x0006
+#define OCF_LE_SET_ADVERTISING_DATA          0x0008
+#define OCF_LE_SET_SCAN_RESPONSE_DATA        0x0009
+#define OCF_LE_SET_ADVERTISE_ENABLE          0x000a
+#define OCF_LE_SET_SCAN_PARAMETERS           0x000b
+#define OCF_LE_SET_SCAN_ENABLE               0x000c
+#define OCF_LE_CREATE_CONNECTION             0x000d
+
+#define OGF_VENDOR_CTL       0x3F
+#define OCF_LE_LTK_NEG_REPLY 0x001B
+
+/* Obtain OGF from OpCode */
+#define BT_OGF(opcode) (((opcode) >> 10) & 0x3f)
+/* Obtain OCF from OpCode */
+#define BT_OCF(opcode) ((opcode) & 0x3FF)
+
+#define BT_OP(ogf, ocf) ((ocf) | ((ogf) << 10))
+
+#define BT_HCI_EVT_DISCONN_COMPLETE      0x05 // HCI_Disconnection_Complete
+#define BT_HCI_EVT_QOS_SETUP_COMPLETE    0x0d
+#define BT_HCI_EVT_CMD_COMPLETE          0x0e
+#define BT_HCI_EVT_CMD_STATUS            0x0f
+#define BT_HCI_EVT_NUM_COMPLETED_PACKETS 0x13
+#define BT_HCI_EVT_LE_META               0x3e // HCI_LE_Connection_Complete
+
+#define HCI_LE_CONNECTION_COMPLETE 0x01
+#define HCI_LE_ADVERTISING_REPORT  0x02
+
+// Consider making this an enum that shifts a bit in the apropriate amount
+#define CAP_TWIST_AND_GO 0x02
+#define CAP_ALLOW_TAP    0x04
+#define CAP_APP_SPECIFIC 0x08
+#define CAP_ENHANCED_TAP 0x40
+
+static uint8_t seos_reader_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x00, 0x98, 0x00, 0x00};
+static uint8_t seos_cred_service_backwards[] =
+    {0x02, 0x00, 0x00, 0x7a, 0x17, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x01, 0x98, 0x00, 0x00};
+
+// Occationally scan stop's completion doesn't get caught.
+// Use the timer callback to call it again
+void seos_hci_timer(void* context) {
+    FURI_LOG_I(TAG, "RUN TIMER");
+    SeosHci* seos_hci = (SeosHci*)context;
+    if(seos_hci->mode == BLE_PERIPHERAL) {
+        seos_hci_enable_advertising(seos_hci, seos_hci->adv_status);
+    } else if(seos_hci->mode == BLE_CENTRAL) {
+        seos_hci_set_scan(seos_hci, seos_hci->scan_status);
+    }
+}
+
+SeosHci* seos_hci_alloc(Seos* seos) {
+    SeosHci* seos_hci = malloc(sizeof(SeosHci));
+    memset(seos_hci, 0, sizeof(SeosHci));
+
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+
+    seos_hci->seos = seos;
+    seos_hci->seos_hci_h5 = seos_hci_h5_alloc();
+    seos_hci->timer = furi_timer_alloc(seos_hci_timer, FuriTimerTypeOnce, seos_hci);
+    seos_hci_h5_set_init_callback(seos_hci->seos_hci_h5, seos_hci_init, seos_hci);
+    seos_hci_h5_set_receive_callback(seos_hci->seos_hci_h5, seos_hci_recv, seos_hci);
+
+    return seos_hci;
+}
+
+void seos_hci_free(SeosHci* seos_hci) {
+    furi_assert(seos_hci);
+
+    furi_timer_free(seos_hci->timer);
+    seos_hci_h5_free(seos_hci->seos_hci_h5);
+    free(seos_hci);
+}
+
+void seos_hci_start(SeosHci* seos_hci, BleMode mode, FlowMode flow_mode) {
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+    seos_hci->flow_mode = flow_mode;
+    seos_hci->mode = mode;
+    seos_hci_h5_start(seos_hci->seos_hci_h5);
+}
+
+void seos_hci_stop(SeosHci* seos_hci) {
+    if(seos_hci->connection_handle > 0) {
+        uint16_t opcode = BT_OP(OGF_LINK_CTL, OCF_DISCONNECT);
+        BitBuffer* disconnect = bit_buffer_alloc(5);
+        bit_buffer_append_bytes(disconnect, (uint8_t*)&opcode, sizeof(opcode));
+        bit_buffer_append_bytes(
+            disconnect,
+            (uint8_t*)&seos_hci->connection_handle,
+            sizeof(seos_hci->connection_handle));
+        bit_buffer_append_byte(disconnect, 0x00);
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, disconnect);
+    }
+
+    seos_hci->device_found = false;
+    seos_hci->connection_handle = 0;
+    seos_hci_h5_stop(seos_hci->seos_hci_h5);
+    if(seos_hci->mode == BLE_PERIPHERAL) {
+        seos_hci_enable_advertising(seos_hci, false);
+    } else if(seos_hci->mode == BLE_CENTRAL) {
+        seos_hci_set_scan(seos_hci, false);
+    }
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_host(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_RESET:
+        uint8_t le_read_local_supported_features[] = {0x03, 0x20, 0x00};
+        bit_buffer_append_bytes(
+            message, le_read_local_supported_features, sizeof(le_read_local_supported_features));
+        break;
+    case OCF_SET_EVENT_MASK:
+        uint8_t set_le_event_mask[] = {
+            0x01, 0x20, 0x08, 0x1f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+        bit_buffer_append_bytes(message, set_le_event_mask, sizeof(set_le_event_mask));
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_info(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_READ_LOCAL_VERSION:
+        // uint8_t write_LE_host_supported[] = {   0x01, 0x6d, 0x0c, 0x02, 0x01, 0x00};
+        uint8_t read_bd_addr[] = {0x09, 0x10, 0x00};
+        bit_buffer_append_bytes(message, read_bd_addr, sizeof(read_bd_addr));
+        break;
+    case OCF_READ_BD_ADDR:
+        uint8_t le_read_buffer_size[] = {0x02, 0x20, 0x00};
+        bit_buffer_append_bytes(message, le_read_buffer_size, sizeof(le_read_buffer_size));
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_le(SeosHci* seos_hci, uint16_t OCF, BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case OCF_LE_SET_EVENT_MASK:
+        uint8_t read_local_version[] = {0x01, 0x10, 0x00};
+        bit_buffer_append_bytes(message, read_local_version, sizeof(read_local_version));
+        break;
+    case OCF_LE_READ_BUFFER_SIZE:
+        uint8_t le_set_random_address[] = {0x05, 0x20, 0x06, 0xCA, 0xFE, 0x00, 0x00, 0x00, 0x03};
+        bit_buffer_append_bytes(message, le_set_random_address, sizeof(le_set_random_address));
+        break;
+    case OCF_LE_SET_ADVERTISING_DATA:
+        seos_hci_enable_advertising(seos_hci, true);
+        break;
+    case OCF_LE_SET_SCAN_RESPONSE_DATA:
+        uint8_t flow_mode_byte = seos_hci->flow_mode == FLOW_READER ? 0x00 : 0x01;
+        // TODO: Use seos_reader_service_backwards
+        uint8_t adv_data[] = {0x08, 0x20, 0x20, 0x15,           0x02, 0x01, 0x06, 0x11, 0x07,
+                              0x02, 0x00, 0x00, 0x7a,           0x17, 0x00, 0x00, 0x80, 0x00,
+                              0x10, 0x00, 0x00, flow_mode_byte, 0x98, 0x00, 0x00, 0x00, 0x00,
+                              0x00, 0x00, 0x00, 0x00,           0x00, 0x00, 0x00, 0x00};
+        bit_buffer_append_bytes(message, adv_data, sizeof(adv_data));
+        break;
+    case OCF_LE_SET_ADVERTISE_ENABLE:
+        if(furi_timer_is_running(seos_hci->timer)) {
+            furi_timer_stop(seos_hci->timer);
+        }
+        uint8_t status = bit_buffer_get_byte(frame, 6);
+        if(status == 0) {
+            if(seos_hci->adv_status) {
+                FURI_LOG_I(TAG, "*** Advertising enabled ***");
+                view_dispatcher_send_custom_event(
+                    seos_hci->seos->view_dispatcher, SeosCustomEventAdvertising);
+
+            } else {
+                FURI_LOG_I(TAG, "*** Advertising disabled ***");
+            }
+        } else {
+            FURI_LOG_W(TAG, "Advertising enabled FAILED");
+        }
+        break;
+    case OCF_LE_SET_SCAN_PARAMETERS:
+        seos_hci_set_scan(seos_hci, true);
+        break;
+    case OCF_LE_SET_SCAN_ENABLE:
+        if(furi_timer_is_running(seos_hci->timer)) {
+            furi_timer_stop(seos_hci->timer);
+        }
+        if(seos_hci->scan_status) { // enabled
+            FURI_LOG_I(TAG, "Scan enable complete. new state: %d", seos_hci->scan_status);
+            view_dispatcher_send_custom_event(
+                seos_hci->seos->view_dispatcher, SeosCustomEventScan);
+        } else if(seos_hci->device_found) {
+            // Scanning stopped, try to connect
+            seos_hci_connect(seos_hci);
+        }
+
+        break;
+    case OCF_LE_READ_LOCAL_SUPPORTED_FEATURES:
+        // FURI_LOG_D(TAG, "Local Supported Features");
+        uint8_t set_event_mask[] = {
+            0x01, 0x0c, 0x08, 0xff, 0xff, 0xfb, 0xff, 0x07, 0xf8, 0xbf, 0x3d};
+        bit_buffer_append_bytes(message, set_event_mask, sizeof(set_event_mask));
+        break;
+    case OCF_LE_SET_RANDOM_ADDRESS:
+        // FURI_LOG_D(TAG, "opcode = %04x", BT_OP(0x3f, 0x0006)); <--- reverse this in byte array
+        uint8_t vendor_set_addr[] = {0x06, 0xfc, 0x06, 0x0, 0x0, 0x1, 0x2, 0x21, 0xAD};
+        bit_buffer_append_bytes(message, vendor_set_addr, sizeof(vendor_set_addr));
+        break;
+    case OCF_LE_SET_ADVERTISING_PARAMETERS:
+        // TODO: make this more dynamic
+        uint8_t capabilities = CAP_TWIST_AND_GO | CAP_ALLOW_TAP | CAP_APP_SPECIFIC |
+                               CAP_ENHANCED_TAP;
+        int8_t tap_rssi = -75;
+        int8_t twist_rssi = -75;
+        int8_t seamless_rssi = -75;
+        int8_t app_rssi = -75;
+        uint8_t mfg_data[] = {0x14,     0xff,       0x2e,          0x01,     0x15, capabilities,
+                              tap_rssi, twist_rssi, seamless_rssi, app_rssi, 0x2a, 0x46,
+                              0x4c,     0x30,       0x4b,          0x37,     0x5a, 0x30,
+                              0x31,     0x55,       0x31,          0x00,     0x00};
+        uint8_t device_name[] = {
+            0x09, 0x20, 0x20, 0x1e, 0x08, 0x09, 0x46, 0x6c, 0x69, 0x70, 0x70, 0x65, 0x72};
+        bit_buffer_append_bytes(message, device_name, sizeof(device_name));
+        bit_buffer_append_bytes(message, mfg_data, sizeof(mfg_data));
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete_ogf_vendor(
+    SeosHci* seos_hci,
+    uint16_t OCF,
+    BitBuffer* frame) {
+    UNUSED(frame);
+
+    BitBuffer* message = bit_buffer_alloc(128);
+    switch(OCF) {
+    case 0x0006:
+        if(seos_hci->mode == BLE_PERIPHERAL) {
+            // Flipper as Reader
+            uint8_t adv_param[] = {
+                0x06,
+                0x20,
+                0x0f,
+                0xa0,
+                0x00,
+                0xa0,
+                0x00,
+                0x00,
+                0x00,
+                0x01,
+                0xDE,
+                0xAF,
+                0xBE,
+                0xEF,
+                0xCA,
+                0xFE,
+                0x07,
+                0x00};
+            bit_buffer_append_bytes(message, adv_param, sizeof(adv_param));
+        } else if(seos_hci->mode == BLE_CENTRAL) {
+            // Flipper as device/credential
+            seos_hci_send_scan_params(seos_hci);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OCF %04x", OCF);
+        break;
+    }
+
+    if(bit_buffer_get_size_bytes(message) > 0) {
+        seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    }
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_cmd_complete(SeosHci* seos_hci, BitBuffer* frame) {
+    BitBuffer* message = bit_buffer_alloc(128);
+    const uint8_t* data = bit_buffer_get_data(frame);
+
+    uint8_t event_type = data[0];
+    uint8_t sub_event_type = data[1];
+    uint8_t ncmd = data[3];
+    uint16_t cmd = data[5] << 8 | data[4];
+    uint8_t status = data[6];
+    if(status == 0) {
+        /*
+        FURI_LOG_D(
+            TAG,
+            "event %d sub event %d ncmd %d cmd %d status %d",
+            event_type,
+            sub_event_type,
+            ncmd,
+            cmd,
+            status);
+            */
+    } else {
+        FURI_LOG_W(
+            TAG,
+            "event %d sub event %d ncmd %d cmd %d status %d",
+            event_type,
+            sub_event_type,
+            ncmd,
+            cmd,
+            status);
+        bit_buffer_free(message);
+        return;
+    }
+
+    uint16_t OGF = BT_OGF(cmd);
+    uint16_t OCF = BT_OCF(cmd);
+    // FURI_LOG_D(TAG, "OGF = %04x OCF = %04x", OGF, OCF);
+
+    switch(OGF) {
+    case OGF_HOST_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_host(seos_hci, OCF, frame);
+        break;
+    case OGF_INFO_PARAM:
+        seos_hci_handle_event_cmd_complete_ogf_info(seos_hci, OCF, frame);
+        break;
+    case OGF_LE_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_le(seos_hci, OCF, frame);
+        break;
+    case OGF_VENDOR_CTL:
+        seos_hci_handle_event_cmd_complete_ogf_vendor(seos_hci, OCF, frame);
+        break;
+    default:
+        FURI_LOG_W(TAG, "Unhandled OGF %04x", OGF);
+        break;
+    }
+
+    bit_buffer_free(message);
+}
+
+void seos_hci_enable_advertising(SeosHci* seos_hci, bool enable) {
+    seos_hci->adv_status = enable;
+    FURI_LOG_I(TAG, "Enable Advertising: %s", enable ? "true" : "false");
+    uint8_t adv_enable[] = {0x0a, 0x20, 0x01, enable ? 0x01 : 0x00};
+    BitBuffer* message = bit_buffer_alloc(sizeof(adv_enable));
+    bit_buffer_append_bytes(message, adv_enable, sizeof(adv_enable));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+
+    FURI_LOG_I(TAG, "Start timer to make sure adv change ran");
+    size_t delay = 100 /*ms*/ / (1000.0f / furi_kernel_get_tick_frequency());
+    furi_check(furi_timer_start(seos_hci->timer, delay) == FuriStatusOk);
+}
+
+void seos_hci_send_scan_params(SeosHci* seos_hci) {
+    uint8_t LE_Scan_Type = 0x00;
+    uint8_t Scanning_Filter_Policy = 0x00;
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_SET_SCAN_PARAMETERS);
+    uint8_t scan_param[] = {
+        0xff, 0xff, 0x07, LE_Scan_Type, 0x10, 0x00, 0x10, 0x00, 0x00, Scanning_Filter_Policy};
+    BitBuffer* message = bit_buffer_alloc(sizeof(scan_param));
+    memcpy(scan_param, (uint8_t*)&opcode, sizeof(opcode));
+    bit_buffer_append_bytes(message, scan_param, sizeof(scan_param));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_set_scan(SeosHci* seos_hci, bool enable) {
+    FURI_LOG_I(TAG, "Start Scan: %s", enable ? "true" : "false");
+    seos_hci->scan_status = enable;
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE);
+    uint8_t set_scan[] = {0xff, 0xff, 0x02, enable ? 0x01 : 0x00, 0x00};
+    memcpy(set_scan, (uint8_t*)&opcode, sizeof(opcode));
+    BitBuffer* message = bit_buffer_alloc(sizeof(set_scan));
+    bit_buffer_append_bytes(message, set_scan, sizeof(set_scan));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+
+    FURI_LOG_I(TAG, "Start timer to make sure set scan ran");
+    size_t delay = 100 /*ms*/ / (1000.0f / furi_kernel_get_tick_frequency());
+    furi_check(furi_timer_start(seos_hci->timer, delay) == FuriStatusOk);
+}
+
+// TODO: test this: hci create le conn - writing: 010d 2019 6000 3000 00 01 2db88ee137c3 000600120000002a0004000600
+void seos_hci_connect(SeosHci* seos_hci) {
+    FURI_LOG_I(TAG, "seos_hci_connect");
+    uint16_t opcode = BT_OP(OGF_LE_CTL, OCF_LE_CREATE_CONNECTION);
+    // Values arbitrarily copied from https://stackoverflow.com/questions/71250571/how-to-send-le-extended-create-connection-in-ble-with-raspberry-pi
+    uint8_t connect[] = {
+        0xff,
+        0xff, //opcode
+        0x19, // length
+        0x60,
+        0x00, // LE_Scan_Interval
+        0x60,
+        0x00, // LE_Scan_Window
+        0x00, // Initiator_Filter_Policy
+        seos_hci->address_type, // Peer_Address_Type
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF,
+        0xFF, // Peer_Address
+        0x01, // Own_Address_Type
+        0x18,
+        0x00, // Connection_Interval_Min
+        0x28,
+        0x00, // Connection_Interval_Max
+        0x00,
+        0x00, // Max_Latency
+        0x90,
+        0x00, // Supervision_Timeout
+        0x00,
+        0x00, // Min_CE_Length
+        0x00,
+        0x00, // Max_CE_Length
+    };
+    memcpy(connect, (uint8_t*)&opcode, sizeof(opcode));
+    memcpy(connect + 9, seos_hci->address, 6);
+    BitBuffer* message = bit_buffer_alloc(sizeof(connect));
+    bit_buffer_append_bytes(message, connect, sizeof(connect));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_handle_event_le_meta(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    // uint8_t length = data[2];
+    uint8_t subevent_code = data[3];
+
+    switch(subevent_code) {
+    case HCI_LE_CONNECTION_COMPLETE:
+        uint8_t status = data[4];
+        if(status != 0x00) {
+            FURI_LOG_W(TAG, "Connection complete with non-zero status");
+            return;
+        }
+
+        seos_hci->connection_handle = data[6] << 8 | data[5];
+        uint8_t role = data[7];
+        uint8_t peer_address_type = data[8];
+        // and more...
+
+        FURI_LOG_D(
+            TAG,
+            "connection complete: handle %04x role %d peer_address_type %d",
+            seos_hci->connection_handle,
+            role,
+            peer_address_type);
+        view_dispatcher_send_custom_event(
+            seos_hci->seos->view_dispatcher, SeosCustomEventConnected);
+
+        if(role == 0x00) { // I'm a central!
+            if(seos_hci->central_connection_callback) {
+                seos_hci->central_connection_callback(seos_hci->central_connection_context);
+            } else {
+                FURI_LOG_W(TAG, "No central_connection_callback defined");
+            }
+        } else if(role == 0x01) { // I'm a peripheral!
+        }
+
+        break;
+    case HCI_LE_ADVERTISING_REPORT:
+        // Prevent interruptions to handling a device by a second advertisement
+        if(seos_hci->device_found) {
+            break;
+        }
+        // TODO: Support single packet with multiple reports
+        uint8_t num_reports = data[4];
+        uint8_t Event_Type = data[5];
+        uint8_t Address_Type = data[6];
+        const uint8_t* Address = data + 7;
+        uint8_t Data_Length = data[13];
+        const uint8_t* adv_data = data + 14;
+        char name[20];
+        memset(name, 0, sizeof(name));
+        if(Event_Type != 0 || Data_Length < sizeof(seos_reader_service_backwards)) {
+            break;
+        }
+
+        FURI_LOG_D(
+            TAG,
+            "Adv %d reports: event type %d address type %d data len %d",
+            num_reports,
+            Event_Type,
+            Address_Type,
+            Data_Length);
+        // seos_log_buffer(TAG, "ADV_IND", (uint8_t*)adv_data, Data_Length);
+
+        uint8_t i = 0;
+        do {
+            uint8_t l = adv_data[i++];
+            uint8_t t = adv_data[i++];
+            const uint8_t* val = adv_data + i;
+            i += l - 1; // subtract one so we don't overcount the type byte
+            switch(t) {
+            case 0x07:
+                if(seos_hci->flow_mode == FLOW_CRED) {
+                    // You're acting like a credential, looking for readers to connect and send to
+                    if(memcmp(
+                           val,
+                           seos_reader_service_backwards,
+                           sizeof(seos_reader_service_backwards)) == 0) {
+                        seos_hci->device_found = true;
+                    }
+                } else if(seos_hci->flow_mode == FLOW_READER) {
+                    if(memcmp(
+                           val,
+                           seos_cred_service_backwards,
+                           sizeof(seos_cred_service_backwards)) == 0) {
+                        seos_hci->device_found = true;
+                    }
+                } else if(seos_hci->flow_mode == FLOW_READER_SCANNER) {
+                    // Reader scanner looks for readers, it doesn't act like a reader (as in FLOW_READER)
+                    if(memcmp(
+                           val,
+                           seos_reader_service_backwards,
+                           sizeof(seos_reader_service_backwards)) == 0) {
+                        // TODO: handle duplicates
+                        notification_message(
+                            seos_hci->seos->notifications, &sequence_single_vibro);
+                    }
+                } else if(seos_hci->flow_mode == FLOW_CRED_SCANNER) {
+                    // Cred scanner looks for devices advertising credential service, it doesn't act like a credential (as in FLOW_CRED)
+                    if(memcmp(
+                           val,
+                           seos_cred_service_backwards,
+                           sizeof(seos_cred_service_backwards)) == 0) {
+                        // TODO: handle duplicates
+                        notification_message(
+                            seos_hci->seos->notifications, &sequence_single_vibro);
+                    }
+                }
+                break;
+            case 0x08: // Short device name
+            case 0x09: // full device name
+                memcpy(name, val, l - 1);
+                break;
+            }
+        } while(i < Data_Length - 1);
+
+        seos_hci->adv_report_count += num_reports;
+
+        if(seos_hci->device_found) {
+            FURI_LOG_I(TAG, "Matched Seos Reader Service: %s", name);
+            seos_hci->address_type = Address_Type;
+            memcpy(seos_hci->address, Address, sizeof(seos_hci->address));
+            seos_hci_set_scan(seos_hci, false);
+            view_dispatcher_send_custom_event(
+                seos_hci->seos->view_dispatcher, SeosCustomEventFound);
+        }
+        break;
+    default:
+        FURI_LOG_W(TAG, "LE Meta event with unknown subevent code");
+        break;
+    }
+}
+
+void seos_hci_event_handler(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    uint8_t sub_event_type = data[1];
+    // uint8_t length = data[2];
+
+    if(sub_event_type == BT_HCI_EVT_CMD_STATUS) {
+        struct bt_hci_evt_cmd_status {
+            uint8_t status;
+            uint8_t ncmd;
+            uint16_t opcode;
+        } __packed;
+        struct bt_hci_evt_cmd_status* status = (struct bt_hci_evt_cmd_status*)(data + 3);
+        if(status->status == 0) {
+            /*
+            FURI_LOG_D(
+                TAG,
+                "Status: status %d ncmd 0x%02x opcode %04x",
+                status->status,
+                status->ncmd,
+                status->opcode);
+                */
+        } else {
+            // Unknown HCI command (0x01)
+            FURI_LOG_W(
+                TAG,
+                "Status: status %d ncmd 0x%02x opcode %04x",
+                status->status,
+                status->ncmd,
+                status->opcode);
+        }
+    } else if(sub_event_type == BT_HCI_EVT_CMD_COMPLETE) {
+        seos_hci_handle_event_cmd_complete(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_LE_META) {
+        seos_hci_handle_event_le_meta(seos_hci, frame);
+    } else if(sub_event_type == BT_HCI_EVT_DISCONN_COMPLETE) {
+        seos_hci->connection_handle = 0;
+
+        if(seos_hci->mode == BLE_PERIPHERAL) {
+            FURI_LOG_W(TAG, "Disconnect. Restart Advertising");
+            seos_hci_enable_advertising(seos_hci, true);
+        } else if(seos_hci->mode == BLE_CENTRAL) {
+            FURI_LOG_W(TAG, "Disconnect. Scan again");
+            seos_hci->device_found = false;
+            seos_hci_set_scan(seos_hci, true);
+        }
+    } else if(sub_event_type == BT_HCI_EVT_NUM_COMPLETED_PACKETS) {
+        struct bt_hci_evt_num_completed_packets {
+            uint8_t num_handles;
+            uint16_t handle;
+            uint16_t count;
+        } __attribute__((packed));
+        struct bt_hci_evt_num_completed_packets* evt =
+            (struct bt_hci_evt_num_completed_packets*)(data + 3);
+        if(evt->num_handles == 1) {
+            // FURI_LOG_D(TAG, "Number of completed packets for %04x: %d", evt->handle, evt->count);
+        } else {
+            FURI_LOG_D(TAG, "Number of completed packets for multiple handles");
+        }
+        if(seos_hci->completed_packets_callback) {
+            seos_hci->completed_packets_callback(seos_hci->completed_packets_context);
+        }
+    } else {
+        FURI_LOG_W(TAG, "Unhandled event subtype %02x", sub_event_type);
+    }
+}
+
+void seos_hci_acldata_send(SeosHci* seos_hci, uint8_t flags, BitBuffer* tx) {
+    // seos_log_buffer("seos_hci_acldata_send", tx);
+    uint16_t tx_len = bit_buffer_get_size_bytes(tx);
+
+    uint16_t handle = seos_hci->connection_handle | (flags << 12);
+
+    BitBuffer* response = bit_buffer_alloc(tx_len + sizeof(handle) + sizeof(tx_len));
+    bit_buffer_append_bytes(response, (uint8_t*)&handle, sizeof(handle));
+    bit_buffer_append_bytes(response, (uint8_t*)&tx_len, sizeof(tx_len));
+    // tx
+    bit_buffer_append_bytes(response, bit_buffer_get_data(tx), tx_len);
+
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_ACLDATA_PKT, response);
+
+    bit_buffer_free(response);
+}
+
+void seos_hci_acldata_handler(SeosHci* seos_hci, BitBuffer* frame) {
+    const uint8_t* data = bit_buffer_get_data(frame);
+    // 0 is 0x02 for ACL DATA
+
+    uint16_t handle = (data[2] << 8 | data[1]) & 0x0FFF;
+    uint8_t flags = data[2] >> 4;
+    uint16_t length = data[4] << 8 | data[3];
+
+    /*
+    uint8_t Broadcast_Flag = flags >> 2;
+    uint8_t Packet_Boundary_Flag = flags & 0x03;
+
+    FURI_LOG_D(
+        TAG,
+        "ACLDATA handle %04x Broadcast_Flag %02x Packet_Boundary_Flag %02x length %d",
+        handle,
+        Broadcast_Flag,
+        Packet_Boundary_Flag,
+        length);
+        */
+    if(handle != seos_hci->connection_handle) {
+        FURI_LOG_W(TAG, "Mismatched handle values");
+    }
+
+    BitBuffer* pdu = bit_buffer_alloc(length);
+    bit_buffer_append_bytes(pdu, data + 1 /*ACL DATA */ + sizeof(handle) + sizeof(length), length);
+    if(seos_hci->receive_callback) {
+        seos_hci->receive_callback(seos_hci->receive_callback_context, handle, flags, pdu);
+    }
+    bit_buffer_free(pdu);
+}
+
+size_t seos_hci_recv(void* context, BitBuffer* frame) {
+    SeosHci* seos_hci = (SeosHci*)context;
+    // seos_log_buffer("HCI Frame", frame);
+
+    const uint8_t* data = bit_buffer_get_data(frame);
+    uint8_t event_type = data[0];
+    // TODO: consider `bit_buffer_starts_with_byte`
+    switch(event_type) {
+    case HCI_EVENT_PKT:
+        seos_hci_event_handler(seos_hci, frame);
+        break;
+    case HCI_ACLDATA_PKT:
+        seos_hci_acldata_handler(seos_hci, frame);
+        break;
+    default:
+        FURI_LOG_W(TAG, "Haven't added support for other HCI commands yet");
+        break;
+    }
+
+    return 0;
+}
+
+// TODO: Consider making this a general "when the state changes" callback which would check if H5 is active (or needs to be reset)
+void seos_hci_init(void* context) {
+    SeosHci* seos_hci = (SeosHci*)context;
+    BitBuffer* message = bit_buffer_alloc(128);
+
+    uint8_t reset[] = {0x03, 0x0c, 0x00};
+    bit_buffer_append_bytes(message, reset, sizeof(reset));
+    seos_hci_h5_send(seos_hci->seos_hci_h5, HCI_COMMAND_PKT, message);
+    view_dispatcher_send_custom_event(seos_hci->seos->view_dispatcher, SeosCustomEventHCIInit);
+
+    bit_buffer_free(message);
+}
+
+void seos_hci_set_receive_callback(
+    SeosHci* seos_hci,
+    SeosHciReceiveCallback callback,
+    void* context) {
+    seos_hci->receive_callback = callback;
+    seos_hci->receive_callback_context = context;
+}
+
+void seos_hci_set_completed_packets_callback(
+    SeosHci* seos_hci,
+    SeosHciCompletedPacketsCallback callback,
+    void* context) {
+    seos_hci->completed_packets_callback = callback;
+    seos_hci->completed_packets_context = context;
+}
+
+void seos_hci_set_central_connection_callback(
+    SeosHci* seos_hci,
+    SeosHciCentralConnectionCallback callback,
+    void* context) {
+    seos_hci->central_connection_callback = callback;
+    seos_hci->central_connection_context = context;
+}

+ 68 - 0
seos_hci.h

@@ -0,0 +1,68 @@
+#pragma once
+
+#include <furi.h>
+#include "seos_hci_h5.h"
+#include "seos_common.h"
+
+typedef void (
+    *SeosHciReceiveCallback)(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu);
+
+typedef void (*SeosHciCompletedPacketsCallback)(void* context);
+typedef void (*SeosHciCentralConnectionCallback)(void* context);
+
+typedef struct {
+    Seos* seos;
+    SeosHciH5* seos_hci_h5;
+    uint16_t connection_handle;
+
+    SeosHciReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosHciCompletedPacketsCallback completed_packets_callback;
+    void* completed_packets_context;
+
+    SeosHciCentralConnectionCallback central_connection_callback;
+    void* central_connection_context;
+
+    BleMode mode;
+
+    FlowMode flow_mode;
+
+    bool scan_status;
+    bool adv_status;
+    uint8_t address[6];
+    uint8_t address_type;
+
+    size_t adv_report_count;
+    bool device_found;
+
+    FuriTimer* timer;
+} SeosHci;
+
+SeosHci* seos_hci_alloc(Seos* seos);
+void seos_hci_free(SeosHci* seos_hci);
+void seos_hci_start(SeosHci* seos_hci, BleMode mode, FlowMode flow_mode);
+void seos_hci_stop(SeosHci* seos_hci);
+size_t seos_hci_recv(void* context, BitBuffer* frame);
+void seos_hci_acldata_send(SeosHci* seos_hci, uint8_t flags, BitBuffer* tx);
+void seos_hci_init(void* context);
+
+void seos_hci_set_receive_callback(
+    SeosHci* seos_hci,
+    SeosHciReceiveCallback callback,
+    void* context);
+
+void seos_hci_set_completed_packets_callback(
+    SeosHci* seos_hci,
+    SeosHciCompletedPacketsCallback callback,
+    void* context);
+
+void seos_hci_set_central_connection_callback(
+    SeosHci* seos_hci,
+    SeosHciCentralConnectionCallback callback,
+    void* context);
+
+void seos_hci_set_scan(SeosHci* seos_hci, bool enable);
+void seos_hci_enable_advertising(SeosHci* seos_hci, bool enable);
+void seos_hci_send_scan_params(SeosHci* seos_hci);
+void seos_hci_connect(SeosHci* seos_hci);

+ 484 - 0
seos_hci_h5.c

@@ -0,0 +1,484 @@
+#include "seos_hci_h5_i.h"
+
+#define TAG "SeosHciH5"
+
+#define MAX_OUT_OF_ORDER 3
+
+/* Convenience macros for reading Three-wire header values */
+#define H5_HDR_SEQ(hdr)      ((hdr)[0] & 0x07)
+#define H5_HDR_ACK(hdr)      (((hdr)[0] >> 3) & 0x07)
+#define H5_HDR_CRC(hdr)      (((hdr)[0] >> 6) & 0x01)
+#define H5_HDR_RELIABLE(hdr) (((hdr)[0] >> 7) & 0x01)
+#define H5_HDR_PKT_TYPE(hdr) ((hdr)[1] & 0x0f)
+#define H5_HDR_LEN(hdr)      ((((hdr)[1] >> 4) & 0x0f) + ((hdr)[2] << 4))
+
+#define SLIP_DELIMITER 0xc0
+#define SLIP_ESC       0xdb
+#define SLIP_ESC_DELIM 0xdc
+#define SLIP_ESC_ESC   0xdd
+
+#define MESSAGE_QUEUE_SIZE 10
+
+/* H5 state flags */
+enum {
+    H5_RX_ESC, /* SLIP escape mode */
+    H5_TX_ACK_REQ, /* Pending ack to send */
+    H5_WAKEUP_DISABLE, /* Device cannot wake host */
+    H5_HW_FLOW_CONTROL, /* Use HW flow control */
+};
+
+typedef struct {
+    size_t len;
+    uint8_t buf[SEOS_UART_RX_BUF_SIZE];
+} HCI_MESSAGE;
+
+int32_t seos_hci_h5_task(void* context);
+void seos_hci_h5_link_control(SeosHciH5* seos_hci_h5, uint8_t* data, size_t len);
+
+void seos_hci_h5_sync(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->out_of_order_count = 0;
+    uint8_t sync[] = {0x01, 0x7e};
+    seos_hci_h5_link_control(seos_hci_h5, sync, sizeof(sync));
+}
+
+SeosHciH5* seos_hci_h5_alloc() {
+    SeosHciH5* seos_hci_h5 = malloc(sizeof(SeosHciH5));
+    memset(seos_hci_h5, 0, sizeof(SeosHciH5));
+    seos_hci_h5->tx_win = H5_TX_WIN_MAX;
+    seos_hci_h5->stage = STOPPED;
+
+    seos_hci_h5->uart = seos_uart_alloc();
+    seos_uart_set_receive_callback(seos_hci_h5->uart, seos_hci_h5_recv, seos_hci_h5);
+
+    seos_hci_h5->thread =
+        furi_thread_alloc_ex("SeosHciH5Worker", 5 * 1024, seos_hci_h5_task, seos_hci_h5);
+    seos_hci_h5->messages = furi_message_queue_alloc(MESSAGE_QUEUE_SIZE, sizeof(HCI_MESSAGE));
+    seos_hci_h5->mq_mutex = furi_mutex_alloc(FuriMutexTypeNormal);
+    furi_thread_start(seos_hci_h5->thread);
+
+    // Give the UART threads a little time to get started
+    furi_delay_ms(3);
+
+    return seos_hci_h5;
+}
+
+void seos_hci_h5_free(SeosHciH5* seos_hci_h5) {
+    furi_assert(seos_hci_h5);
+
+    furi_message_queue_free(seos_hci_h5->messages);
+    furi_mutex_free(seos_hci_h5->mq_mutex);
+    seos_uart_free(seos_hci_h5->uart);
+    free(seos_hci_h5);
+}
+
+void seos_hci_h5_start(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->stage = STARTED;
+    seos_hci_h5->out_of_order_count = 0;
+
+    /* Send initial sync request */
+    seos_hci_h5_sync(seos_hci_h5);
+}
+void seos_hci_h5_stop(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->stage = STOPPED;
+    furi_thread_flags_set(furi_thread_get_id(seos_hci_h5->thread), WorkerEvtStop);
+    furi_thread_join(seos_hci_h5->thread);
+    furi_thread_free(seos_hci_h5->thread);
+}
+
+void seos_hci_h5_peer_reset(SeosHciH5* seos_hci_h5) {
+    seos_hci_h5->state = H5_UNINITIALIZED;
+    seos_hci_h5->tx_seq = 0;
+    seos_hci_h5->tx_ack = 0;
+}
+
+void seos_hci_h5_reset_rx(SeosHciH5* seos_hci_h5) {
+    bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, false);
+}
+
+void seos_hci_h5_link_control(SeosHciH5* seos_hci_h5, uint8_t* data, size_t len) {
+    BitBuffer* message = bit_buffer_alloc(len);
+    bit_buffer_append_bytes(message, data, len);
+
+    seos_hci_h5_send(seos_hci_h5, HCI_3WIRE_LINK_PKT, message);
+    bit_buffer_free(message);
+}
+
+void seos_hci_h5_handle_internal_rx(SeosHciH5* seos_hci_h5, BitBuffer* rx_skb) {
+    uint8_t sync_req[] = {0x01, 0x7e};
+    uint8_t sync_rsp[] = {0x02, 0x7d};
+    uint8_t conf_req[3] = {0x03, 0xfc};
+    uint8_t conf_rsp[] = {0x04, 0x7b};
+    uint8_t wakeup_req[] = {0x05, 0xfa};
+    uint8_t woken_req[] = {0x06, 0xf9};
+    uint8_t sleep_req[] = {0x07, 0x78};
+    const uint8_t* header = bit_buffer_get_data(rx_skb);
+    const uint8_t* data = bit_buffer_get_data(rx_skb) + 4;
+
+    if(H5_HDR_PKT_TYPE(header) != HCI_3WIRE_LINK_PKT) return;
+
+    if(H5_HDR_LEN(header) < 2) return;
+
+    conf_req[2] = seos_hci_h5->tx_win & 0x07;
+
+    if(memcmp(data, sync_req, 2) == 0) {
+        if(seos_hci_h5->state == H5_ACTIVE) seos_hci_h5_peer_reset(seos_hci_h5);
+        seos_hci_h5_link_control(seos_hci_h5, sync_rsp, 2);
+    } else if(memcmp(data, sync_rsp, 2) == 0) {
+        if(seos_hci_h5->state == H5_ACTIVE) seos_hci_h5_peer_reset(seos_hci_h5);
+        seos_hci_h5->state = H5_INITIALIZED;
+        seos_hci_h5_link_control(seos_hci_h5, conf_req, 3);
+    } else if(memcmp(data, conf_req, 2) == 0) {
+        seos_hci_h5_link_control(seos_hci_h5, conf_rsp, 2);
+        seos_hci_h5_link_control(seos_hci_h5, conf_req, 3);
+    } else if(memcmp(data, conf_rsp, 2) == 0) {
+        if(H5_HDR_LEN(header) > 2) seos_hci_h5->tx_win = (data[2] & 0x07);
+        FURI_LOG_D(TAG, "---- Three-wire init complete. tx_win %u ----", seos_hci_h5->tx_win);
+        seos_hci_h5->state = H5_ACTIVE;
+        if(seos_hci_h5->init_callback) {
+            seos_hci_h5->init_callback(seos_hci_h5->init_callback_context);
+        }
+        return;
+    } else if(memcmp(data, sleep_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer went to sleep");
+        seos_hci_h5->sleep = H5_SLEEPING;
+        return;
+    } else if(memcmp(data, woken_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer woke up");
+        seos_hci_h5->sleep = H5_AWAKE;
+    } else if(memcmp(data, wakeup_req, 2) == 0) {
+        FURI_LOG_D(TAG, "Peer requested wakeup");
+        seos_hci_h5_link_control(seos_hci_h5, woken_req, 2);
+        seos_hci_h5->sleep = H5_AWAKE;
+    } else {
+        FURI_LOG_D(TAG, "Link Control: 0x%02hhx 0x%02hhx", data[0], data[1]);
+        return;
+    }
+
+    // hci_uart_tx_wakeup(hu);
+}
+
+static void seos_hci_h5_slip_delim(BitBuffer* skb) {
+    const char delim = SLIP_DELIMITER;
+
+    bit_buffer_append_byte(skb, delim);
+}
+
+static void seos_hci_h5_slip_one_byte(BitBuffer* skb, uint8_t c) {
+    const char esc_delim[2] = {SLIP_ESC, SLIP_ESC_DELIM};
+    const char esc_esc[2] = {SLIP_ESC, SLIP_ESC_ESC};
+
+    switch(c) {
+    case SLIP_DELIMITER:
+        bit_buffer_append_byte(skb, esc_delim[0]);
+        bit_buffer_append_byte(skb, esc_delim[1]);
+        break;
+    case SLIP_ESC:
+        bit_buffer_append_byte(skb, esc_esc[0]);
+        bit_buffer_append_byte(skb, esc_esc[1]);
+
+        break;
+    default:
+        bit_buffer_append_byte(skb, c);
+    }
+}
+
+static void seos_hci_h5_unslip_one_byte(SeosHciH5* seos_hci_h5, BitBuffer* skb, uint8_t c) {
+    const uint8_t delim = SLIP_DELIMITER, esc = SLIP_ESC;
+    uint8_t byte = c;
+
+    bool test = bit_lib_get_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC);
+    if(!test && c == SLIP_ESC) {
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, true);
+        return;
+    }
+
+    if(test) {
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_RX_ESC, false);
+        switch(c) {
+        case SLIP_ESC_DELIM:
+            byte = delim;
+            break;
+        case SLIP_ESC_ESC:
+            byte = esc;
+            break;
+        default:
+            FURI_LOG_W(TAG, "Invalid esc byte 0x%02hhx", c);
+            seos_hci_h5_reset_rx(seos_hci_h5);
+            return;
+        }
+    }
+
+    bit_buffer_append_byte(skb, byte);
+}
+
+void seos_hci_h5_send(SeosHciH5* seos_hci_h5, uint8_t pkt_type, BitBuffer* message) {
+    SeosUart* seos_uart = seos_hci_h5->uart;
+
+    const uint8_t* message_buf = message ? bit_buffer_get_data(message) : NULL;
+    size_t message_len = message ? bit_buffer_get_size_bytes(message) : 0;
+
+    if(message_len > 0) {
+        // seos_log_buffer("seos_hci_h5_send", message);
+    }
+    uint8_t header[4];
+
+    /*
+   * Max len of packet: (original len + 4 (H5 hdr) + 2 (crc)) * 2
+   * (because bytes 0xc0 and 0xdb are escaped, worst case is when
+   * the packet is all made of 0xc0 and 0xdb) + 2 (0xc0
+   * delimiters at start and end).
+   */
+    size_t max_packet_len = (message_len + 6) * 2 + 2;
+
+    BitBuffer* nskb = bit_buffer_alloc(max_packet_len);
+    seos_hci_h5_slip_delim(nskb);
+
+    bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, false);
+    header[0] = seos_hci_h5->tx_ack << 3;
+
+    /* Reliable packet? */
+    if(pkt_type == HCI_ACLDATA_PKT || pkt_type == HCI_COMMAND_PKT) {
+        header[0] |= 1 << 7;
+        header[0] |= seos_hci_h5->tx_seq;
+        seos_hci_h5->tx_seq = (seos_hci_h5->tx_seq + 1) % 8;
+    }
+
+    header[1] = pkt_type | ((message_len & 0x0f) << 4);
+    header[2] = message_len >> 4;
+    header[3] = ~((header[0] + header[1] + header[2]) & 0xff);
+
+    /*
+    FURI_LOG_D(
+        TAG,
+        "tx: seq %u ack %u crc %u rel %u type %u len %u",
+        H5_HDR_SEQ(header),
+        H5_HDR_ACK(header),
+        H5_HDR_CRC(header),
+        H5_HDR_RELIABLE(header),
+        H5_HDR_PKT_TYPE(header),
+        H5_HDR_LEN(header));
+    */
+
+    for(size_t i = 0; i < sizeof(header); i++) {
+        seos_hci_h5_slip_one_byte(nskb, header[i]);
+    }
+    for(size_t i = 0; i < message_len; i++) {
+        seos_hci_h5_slip_one_byte(nskb, message_buf[i]);
+    }
+
+    seos_hci_h5_slip_delim(nskb);
+
+    seos_uart_send(
+        seos_uart, (uint8_t*)bit_buffer_get_data(nskb), bit_buffer_get_size_bytes(nskb));
+    bit_buffer_free(nskb);
+}
+
+size_t seos_hci_h5_recv(void* context, uint8_t* buffer, size_t len) {
+    SeosHciH5* seos_hci_h5 = (SeosHciH5*)context;
+
+    // Must get start byte + 4 byte header + end byte to even give a shit
+    if(len < 6) {
+        return 0;
+    }
+
+    if(buffer[0] != SLIP_DELIMITER) {
+        // FURI_LOG_E(TAG, "UART didn't start with SLIP_DELIMITER (%02x)", buffer[0]);
+        return 1;
+    }
+
+    BitBuffer* rx_skb = bit_buffer_alloc(len);
+
+    // i = 1 -> Skip first c0 byte
+    for(size_t i = 1; i < len; i++) {
+        uint8_t c = buffer[i];
+        seos_hci_h5_unslip_one_byte(seos_hci_h5, rx_skb, c);
+    }
+    // seos_log_buffer("HCI H5 Recv", rx_skb);
+
+    // From h5_rx_3wire_hdr
+    const uint8_t* header = bit_buffer_get_data(rx_skb);
+
+    /*
+    FURI_LOG_D(
+        TAG,
+        "rx: seq %u ack %u crc %u rel %u type %u len %u",
+        H5_HDR_SEQ(header),
+        H5_HDR_ACK(header),
+        H5_HDR_CRC(header),
+        H5_HDR_RELIABLE(header),
+        H5_HDR_PKT_TYPE(header),
+        H5_HDR_LEN(header));
+        */
+
+    if(((header[0] + header[1] + header[2] + header[3]) & 0xff) != 0xff) {
+        FURI_LOG_W(TAG, "Invalid header checksum");
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+
+    if(len < (size_t)(1 + 4 + H5_HDR_LEN(header) + 1)) {
+        // FURI_LOG_W(TAG, "Incomplete packet (%d), wait for more data", len);
+        bit_buffer_free(rx_skb);
+        return 0;
+    }
+
+    if(H5_HDR_RELIABLE(header) && H5_HDR_SEQ(header) != seos_hci_h5->tx_ack) {
+        FURI_LOG_W(
+            TAG, "Out-of-order packet arrived (%u != %u)", H5_HDR_SEQ(header), seos_hci_h5->tx_ack);
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, true);
+        seos_hci_h5_reset_rx(seos_hci_h5);
+
+        seos_hci_h5->out_of_order_count++;
+        if(seos_hci_h5->out_of_order_count >= MAX_OUT_OF_ORDER) {
+            seos_hci_h5_sync(seos_hci_h5);
+        }
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+    // When a packet isn't out of order, reset the count
+    seos_hci_h5->out_of_order_count = 0;
+
+    if(seos_hci_h5->state != H5_ACTIVE && H5_HDR_PKT_TYPE(header) != HCI_3WIRE_LINK_PKT) {
+        FURI_LOG_W(TAG, "Non-link packet received in non-active state");
+        seos_hci_h5_reset_rx(seos_hci_h5);
+        bit_buffer_free(rx_skb);
+        return len;
+    }
+
+    if(H5_HDR_CRC(header)) {
+        // TODO: Check header
+    }
+
+    // From h5_complete_rx_pkt
+    if(H5_HDR_RELIABLE(header)) {
+        seos_hci_h5->tx_ack = (seos_hci_h5->tx_ack + 1) % 8;
+        bit_lib_set_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ, true);
+        //hci_uart_tx_wakeup(hu);
+    }
+
+    seos_hci_h5->rx_ack = H5_HDR_ACK(header);
+    size_t payload_len = H5_HDR_LEN(header);
+
+    // Determine amount of data we consumed
+    BitBuffer* tmp = bit_buffer_alloc(len);
+    for(size_t i = 0; i < 4 + payload_len; i++) {
+        seos_hci_h5_slip_one_byte(tmp, bit_buffer_get_byte(rx_skb, i));
+    }
+    size_t consumed = 1 + bit_buffer_get_size_bytes(tmp) + 1;
+    if(consumed > len) {
+        // At one point I was double processing the header, and not processing all the paylaod, and ended up with consumed > len.
+        // This is to track if there are edge cases I missed.
+        FURI_LOG_W(TAG, "Consumed %d > len %d", consumed, len);
+        consumed = len;
+    }
+    bit_buffer_free(tmp);
+
+    // Remove from send queue?
+    // h5_pkt_cull(h5);
+
+    switch(H5_HDR_PKT_TYPE(header)) {
+    case HCI_EVENT_PKT:
+    case HCI_ACLDATA_PKT:
+    case HCI_SCODATA_PKT:
+    case HCI_ISODATA_PKT:
+        uint32_t space = furi_message_queue_get_space(seos_hci_h5->messages);
+        if(space > 0) {
+            HCI_MESSAGE message = {};
+            message.len = 4 + payload_len;
+            if(message.len > sizeof(message.buf)) {
+                FURI_LOG_W(TAG, "Too big to queue");
+                return len;
+            }
+            memcpy(message.buf, bit_buffer_get_data(rx_skb), message.len);
+
+            if(furi_mutex_acquire(seos_hci_h5->mq_mutex, FuriWaitForever) == FuriStatusOk) {
+                furi_message_queue_put(seos_hci_h5->messages, &message, FuriWaitForever);
+                furi_mutex_release(seos_hci_h5->mq_mutex);
+            }
+            if(space < MESSAGE_QUEUE_SIZE / 2) {
+                FURI_LOG_D(TAG, "Queue message.  %ld remaining", space);
+            }
+        } else {
+            if(seos_hci_h5->stage != STOPPED) {
+                FURI_LOG_E(TAG, "No space in message queue");
+            }
+        }
+        break;
+    default:
+        seos_hci_h5_handle_internal_rx(seos_hci_h5, rx_skb);
+        break;
+    }
+
+    if(bit_lib_get_bit((uint8_t*)&seos_hci_h5->flags, H5_TX_ACK_REQ)) {
+        seos_hci_h5_send(seos_hci_h5, HCI_3WIRE_ACK_PKT, NULL);
+    }
+
+    seos_hci_h5_reset_rx(seos_hci_h5);
+
+    bit_buffer_free(rx_skb);
+
+    // FURI_LOG_D(TAG, "%d consumed", consumed);
+    return consumed;
+}
+
+int32_t seos_hci_h5_task(void* context) {
+    SeosHciH5* seos_hci_h5 = (SeosHciH5*)context;
+    bool running = true;
+
+    while(running) {
+        uint32_t events = furi_thread_flags_get();
+        if(events & WorkerEvtStop) {
+            running = false;
+            break;
+        }
+
+        if(furi_mutex_acquire(seos_hci_h5->mq_mutex, 1) == FuriStatusOk) {
+            uint32_t count = furi_message_queue_get_count(seos_hci_h5->messages);
+            if(count > 0) {
+                if(count > MESSAGE_QUEUE_SIZE / 2) {
+                    FURI_LOG_I(TAG, "Dequeue message [%ld messages]", count);
+                }
+
+                HCI_MESSAGE message = {};
+                FuriStatus status =
+                    furi_message_queue_get(seos_hci_h5->messages, &message, FuriWaitForever);
+                if(status != FuriStatusOk) {
+                    FURI_LOG_W(TAG, "furi_message_queue_get fail %d", status);
+                }
+
+                if(seos_hci_h5->receive_callback) {
+                    size_t payload_len = H5_HDR_LEN(message.buf);
+                    BitBuffer* frame = bit_buffer_alloc(payload_len + 1);
+                    bit_buffer_append_byte(frame, H5_HDR_PKT_TYPE(message.buf));
+                    bit_buffer_append_bytes(frame, message.buf + 4, payload_len);
+                    seos_hci_h5->receive_callback(seos_hci_h5->receive_callback_context, frame);
+                    bit_buffer_free(frame);
+                }
+            }
+            furi_mutex_release(seos_hci_h5->mq_mutex);
+        } else {
+            FURI_LOG_W(TAG, "Failed to acquire mutex");
+        }
+
+        // A beat for event flags
+        // furi_delay_ms(1);
+    }
+
+    return 0;
+}
+
+void seos_hci_h5_set_receive_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5ReceiveCallback callback,
+    void* context) {
+    seos_hci_h5->receive_callback = callback;
+    seos_hci_h5->receive_callback_context = context;
+}
+
+void seos_hci_h5_set_init_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5InitCallback callback,
+    void* context) {
+    seos_hci_h5->init_callback = callback;
+    seos_hci_h5->init_callback_context = context;
+}

+ 82 - 0
seos_hci_h5.h

@@ -0,0 +1,82 @@
+#pragma once
+
+#include <string.h>
+#include <stdlib.h>
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+#include <lib/bit_lib/bit_lib.h>
+
+#include "uart.h"
+
+// include/net/bluetooth/hci.h
+/* HCI data types */
+#define HCI_COMMAND_PKT 0x01
+#define HCI_ACLDATA_PKT 0x02
+#define HCI_SCODATA_PKT 0x03
+#define HCI_EVENT_PKT   0x04
+#define HCI_ISODATA_PKT 0x05
+#define HCI_DIAG_PKT    0xf0
+#define HCI_VENDOR_PKT  0xff
+
+#define HCI_3WIRE_ACK_PKT  0
+#define HCI_3WIRE_LINK_PKT 0x0F
+
+#define H5_TX_WIN_MAX 4
+
+typedef size_t (*SeosHciH5ReceiveCallback)(void* context, BitBuffer* frame);
+typedef void (*SeosHciH5InitCallback)(void* context);
+
+typedef struct {
+    SeosUart* uart;
+
+    uint8_t tx_seq; /* Next seq number to send */
+    uint8_t tx_ack; /* Next ack number to send */
+    uint8_t tx_win; /* Sliding window size */
+    uint8_t rx_ack; /* Last ack number received */
+
+    unsigned long flags;
+
+    enum {
+        H5_UNINITIALIZED,
+        H5_INITIALIZED,
+        H5_ACTIVE,
+    } state;
+    enum {
+        H5_AWAKE,
+        H5_SLEEPING,
+        H5_WAKING_UP,
+    } sleep;
+
+    enum {
+        STOPPED,
+        STARTED
+    } stage;
+
+    SeosHciH5ReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosHciH5InitCallback init_callback;
+    void* init_callback_context;
+
+    size_t out_of_order_count;
+
+    FuriMessageQueue* messages;
+    FuriMutex* mq_mutex;
+    FuriThread* thread;
+} SeosHciH5;
+
+SeosHciH5* seos_hci_h5_alloc();
+void seos_hci_h5_free(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_start(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_stop(SeosHciH5* seos_hci_h5);
+void seos_hci_h5_send(SeosHciH5* seos_hci_h5, uint8_t pkt_type, BitBuffer* message);
+size_t seos_hci_h5_recv(void* context, uint8_t* buffer, size_t len);
+void seos_hci_h5_set_receive_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5ReceiveCallback callback,
+    void* context);
+void seos_hci_h5_set_init_callback(
+    SeosHciH5* seos_hci_h5,
+    SeosHciH5InitCallback callback,
+    void* context);

+ 4 - 0
seos_hci_h5_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_hci_h5.h"

+ 4 - 0
seos_hci_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_hci.h"

+ 123 - 0
seos_i.h

@@ -0,0 +1,123 @@
+#pragma once
+
+#include <furi.h>
+#include <furi_hal.h>
+#include <gui/gui.h>
+#include <gui/view_dispatcher.h>
+#include <gui/scene_manager.h>
+#include <notification/notification_messages.h>
+#include <storage/storage.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/text_box.h>
+#include <gui/modules/widget.h>
+
+#include <input/input.h>
+
+#include <lib/nfc/nfc.h>
+#include <nfc/nfc_listener.h>
+#include <nfc/nfc_poller.h>
+#include <nfc/nfc_device.h>
+
+/* generated by fbt from .png files in images folder */
+#include <seos_icons.h>
+
+#include "seos.h"
+#include "keys.h"
+#include "seos_hci.h"
+#include "seos_characteristic.h"
+#include "seos_central.h"
+#include "seos_common.h"
+#include "seos_reader.h"
+#include "seos_emulator.h"
+#include "scenes/seos_scene.h"
+#include "des_cmac.h"
+#include "aes_cmac.h"
+
+#define SEOS_TEXT_STORE_SIZE 128
+
+enum SeosCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    SeosCustomEventReserved = 100,
+
+    SeosCustomEventViewExit,
+    SeosCustomEventTextInputDone,
+    // Read card events
+    SeosCustomEventReaderError,
+    SeosCustomEventReaderSuccess,
+
+    SeosCustomEventHCIInit,
+    // Events during emulating or reading
+    SeosCustomEventScan,
+    SeosCustomEventFound,
+    SeosCustomEventEmulate,
+    SeosCustomEventADFMatched,
+    SeosCustomEventAIDSelected,
+    SeosCustomEventConnected,
+    SeosCustomEventAuthenticated,
+    SeosCustomEventSIORequested,
+    SeosCustomEventAdvertising,
+};
+
+struct Seos {
+    bool is_debug_enabled;
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    Storage* storage;
+
+    char text_store[SEOS_TEXT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+
+    // Common Views
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    TextBox* text_box;
+    Widget* widget;
+
+    Nfc* nfc;
+    NfcListener* listener;
+    NfcPoller* poller;
+    NfcDevice* nfc_device;
+
+    SeosCredential credential;
+
+    // NFC
+    SeosEmulator* seos_emulator;
+    SeosReader* seos_reader;
+
+    // BLE
+    bool has_ble;
+    SeosCharacteristic* seos_characteristic;
+    SeosCentral* seos_central;
+    FlowMode flow_mode;
+
+    char dev_name[SEOS_FILE_NAME_MAX_LENGTH + 1];
+    FuriString* load_path;
+    DialogsApp* dialogs;
+};
+
+typedef enum {
+    SeosViewMenu,
+    SeosViewPopup,
+    SeosViewLoading,
+    SeosViewTextInput,
+    SeosViewTextBox,
+    SeosViewWidget,
+} SeosView;
+
+void seos_text_store_set(Seos* seos, const char* text, ...);
+
+void seos_text_store_clear(Seos* seos);
+
+void seos_blink_start(Seos* seos);
+
+void seos_blink_stop(Seos* seos);
+
+void seos_show_loading_popup(void* context, bool show);

+ 235 - 0
seos_l2cap.c

@@ -0,0 +1,235 @@
+#include "seos_l2cap_i.h"
+
+#define TAG "SeosL2Cap"
+
+#define ACL_START_NO_FLUSH 0x00
+#define ACL_CONT           0x01
+#define ACL_START          0x02
+
+#define CID_ATT       0x0004
+#define CID_LE_SIGNAL 0x0005
+
+// 5.4.2. HCI ACL Data packets
+#define SEOS_MAX_ACLDATA_SEND_LENGTH 23
+
+struct l2cap_header {
+    uint16_t payload_len;
+    uint16_t cid;
+} __packed;
+
+SeosL2Cap* seos_l2cap_alloc(Seos* seos) {
+    SeosL2Cap* seos_l2cap = malloc(sizeof(SeosL2Cap));
+    memset(seos_l2cap, 0, sizeof(SeosL2Cap));
+
+    // TODO: match MTU
+    seos_l2cap->tx_accumulator = bit_buffer_alloc(256);
+    seos_l2cap->rx_accumulator = bit_buffer_alloc(256);
+
+    seos_l2cap->seos = seos;
+    seos_l2cap->seos_hci = seos_hci_alloc(seos);
+
+    // Callback to lower level services to call this one
+    seos_hci_set_receive_callback(seos_l2cap->seos_hci, seos_l2cap_recv, seos_l2cap);
+    seos_hci_set_completed_packets_callback(
+        seos_l2cap->seos_hci, seos_l2cap_send_next_chunk, seos_l2cap);
+    seos_hci_set_central_connection_callback(
+        seos_l2cap->seos_hci, seos_l2cap_central_connection, seos_l2cap);
+
+    return seos_l2cap;
+}
+
+void seos_l2cap_free(SeosL2Cap* seos_l2cap) {
+    furi_assert(seos_l2cap);
+
+    bit_buffer_free(seos_l2cap->tx_accumulator);
+    bit_buffer_free(seos_l2cap->rx_accumulator);
+
+    seos_hci_free(seos_l2cap->seos_hci);
+    free(seos_l2cap);
+}
+
+void seos_l2cap_start(SeosL2Cap* seos_l2cap, BleMode mode, FlowMode flow_mode) {
+    seos_hci_start(seos_l2cap->seos_hci, mode, flow_mode);
+}
+
+void seos_l2cap_stop(SeosL2Cap* seos_l2cap) {
+    seos_hci_stop(seos_l2cap->seos_hci);
+}
+
+void seos_l2cap_recv(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    seos_l2cap->handle = handle;
+
+    // seos_log_bitbuffer(TAG, "recv", pdu);
+    // uint8_t Broadcast_Flag = flags >> 2;
+    uint8_t Packet_Boundary_Flag = flags & 0x03;
+
+    const uint8_t* data = bit_buffer_get_data(pdu);
+    struct l2cap_header* header = (struct l2cap_header*)(data);
+
+    switch(Packet_Boundary_Flag) {
+    case ACL_CONT:
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) == 0) {
+            FURI_LOG_I(TAG, "Request to att continue when no previous data received");
+            seos_log_bitbuffer(TAG, "cont", pdu);
+            return;
+        }
+        // No header this time
+        bit_buffer_append_bytes(seos_l2cap->rx_accumulator, data, bit_buffer_get_size_bytes(pdu));
+
+        // Check for overage
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) > seos_l2cap->pdu_len) {
+            FURI_LOG_W(
+                TAG,
+                "Oh shit, too much data: %d > %d",
+                bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator),
+                seos_l2cap->pdu_len);
+            seos_log_bitbuffer(TAG, "cont", seos_l2cap->rx_accumulator);
+        }
+        // Full PDU
+        if(bit_buffer_get_size_bytes(seos_l2cap->rx_accumulator) == seos_l2cap->pdu_len) {
+            // FURI_LOG_I(TAG, "Complete reassembled PDU");
+            // When we've accumulated, we've skipped copying the header, so we can pass the accumulator directly in.
+            if(seos_l2cap->receive_callback) {
+                seos_l2cap->receive_callback(
+                    seos_l2cap->receive_callback_context, seos_l2cap->rx_accumulator);
+            }
+        }
+        break;
+    case ACL_START:
+    case ACL_START_NO_FLUSH:
+        uint16_t payload_len = header->payload_len;
+        uint16_t cid = header->cid;
+
+        if(bit_buffer_get_size_bytes(pdu) < header->payload_len) {
+            // FURI_LOG_W(TAG, "Incomplete PDU");
+            seos_l2cap->pdu_len = payload_len;
+            bit_buffer_reset(seos_l2cap->rx_accumulator);
+            bit_buffer_append_bytes(
+                seos_l2cap->rx_accumulator,
+                data + sizeof(struct l2cap_header),
+                bit_buffer_get_size_bytes(pdu) - sizeof(struct l2cap_header));
+            return;
+        }
+
+        if(cid == CID_ATT) {
+            BitBuffer* payload = bit_buffer_alloc(payload_len);
+            bit_buffer_append_bytes(payload, data + sizeof(struct l2cap_header), payload_len);
+            if(seos_l2cap->receive_callback) {
+                seos_l2cap->receive_callback(seos_l2cap->receive_callback_context, payload);
+            }
+            bit_buffer_free(payload);
+        } else if(cid == CID_LE_SIGNAL) {
+            seos_log_bitbuffer(TAG, "LE Signal", pdu);
+        } else {
+            FURI_LOG_W(TAG, "Unhandled CID: %d", cid);
+        }
+
+        break;
+    default:
+        FURI_LOG_W(TAG, "unhandled Packet_Boundary_Flag %d", Packet_Boundary_Flag);
+        break;
+    }
+}
+
+void seos_l2cap_central_connection(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    if(seos_l2cap->central_connection_callback) {
+        seos_l2cap->central_connection_callback(seos_l2cap->central_connection_context);
+    }
+}
+
+void seos_l2cap_send_chunk(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    // FURI_LOG_D(TAG, "seos_l2cap_send_chunk");
+    uint16_t cid = CID_ATT;
+
+    uint8_t Packet_Boundary_Flag = 0x00;
+
+    bool continuation = bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) <
+                        seos_l2cap->pdu_len;
+    if(continuation) {
+        Packet_Boundary_Flag = 0x01;
+    }
+
+    uint16_t tx_len =
+        MIN((size_t)SEOS_MAX_ACLDATA_SEND_LENGTH,
+            bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator));
+    if(tx_len == 0) {
+        seos_l2cap->pdu_len = 0;
+        return;
+    }
+
+    BitBuffer* response = bit_buffer_alloc(tx_len + sizeof(cid) + sizeof(tx_len));
+
+    if(!continuation) {
+        //pdu length
+        bit_buffer_append_bytes(
+            response, (uint8_t*)&seos_l2cap->pdu_len, sizeof(seos_l2cap->pdu_len));
+        // cid
+        bit_buffer_append_bytes(response, (uint8_t*)&cid, sizeof(cid));
+    }
+    // tx
+    bit_buffer_append_bytes(response, bit_buffer_get_data(seos_l2cap->tx_accumulator), tx_len);
+    FURI_LOG_D(TAG, "send_chunk: %d/%d bytes", tx_len, seos_l2cap->pdu_len);
+
+    seos_hci_acldata_send(seos_l2cap->seos_hci, Packet_Boundary_Flag, response);
+    bit_buffer_free(response);
+
+    // trim off send bytes
+    int new_len = bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) - tx_len;
+    if(new_len <= 0) {
+        bit_buffer_reset(seos_l2cap->tx_accumulator);
+        seos_l2cap->pdu_len = 0;
+        // FURI_LOG_D(TAG, "TX accumulator empty");
+        return;
+    }
+
+    BitBuffer* tmp = bit_buffer_alloc(new_len);
+    bit_buffer_append_bytes(
+        tmp, bit_buffer_get_data(seos_l2cap->tx_accumulator) + tx_len, new_len);
+    bit_buffer_reset(seos_l2cap->tx_accumulator);
+    bit_buffer_append_bytes(
+        seos_l2cap->tx_accumulator, bit_buffer_get_data(tmp), bit_buffer_get_size_bytes(tmp));
+    bit_buffer_free(tmp);
+    // FURI_LOG_D(TAG, "tx accumulator length = %d", bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator));
+}
+
+void seos_l2cap_send(SeosL2Cap* seos_l2cap, BitBuffer* content) {
+    // seos_log_buffer("seos_l2cap_send", content);
+
+    if(bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) > 0) {
+        FURI_LOG_W(TAG, "Failed to add message to L2CAP accumulator: it isn't empty");
+        return;
+    }
+    bit_buffer_append_bytes(
+        seos_l2cap->tx_accumulator,
+        bit_buffer_get_data(content),
+        bit_buffer_get_size_bytes(content));
+    seos_l2cap->pdu_len = bit_buffer_get_size_bytes(content);
+
+    seos_l2cap_send_chunk(seos_l2cap);
+}
+
+void seos_l2cap_send_next_chunk(void* context) {
+    SeosL2Cap* seos_l2cap = (SeosL2Cap*)context;
+    if(bit_buffer_get_size_bytes(seos_l2cap->tx_accumulator) > 0) {
+        seos_l2cap_send_chunk(seos_l2cap);
+    }
+}
+
+void seos_l2cap_set_receive_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapReceiveCallback callback,
+    void* context) {
+    seos_l2cap->receive_callback = callback;
+    seos_l2cap->receive_callback_context = context;
+}
+
+void seos_l2cap_set_central_connection_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapCentralConnectionCallback callback,
+    void* context) {
+    seos_l2cap->central_connection_callback = callback;
+    seos_l2cap->central_connection_context = context;
+}

+ 44 - 0
seos_l2cap.h

@@ -0,0 +1,44 @@
+#pragma once
+
+#include <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include "seos_hci.h"
+#include "seos_common.h"
+
+typedef void (*SeosL2CapReceiveCallback)(void* context, BitBuffer* payload);
+typedef void (*SeosL2CapCentralConnectionCallback)(void* context);
+
+typedef struct {
+    Seos* seos;
+    SeosHci* seos_hci;
+    // Where to collect up data
+    BitBuffer* tx_accumulator;
+    BitBuffer* rx_accumulator;
+    uint16_t pdu_len;
+    uint16_t handle;
+
+    SeosL2CapReceiveCallback receive_callback;
+    void* receive_callback_context;
+
+    SeosL2CapCentralConnectionCallback central_connection_callback;
+    void* central_connection_context;
+} SeosL2Cap;
+
+SeosL2Cap* seos_l2cap_alloc(Seos* seos);
+void seos_l2cap_free(SeosL2Cap* seos_l2cap);
+void seos_l2cap_start(SeosL2Cap* seos_l2cap, BleMode mode, FlowMode flow_mode);
+void seos_l2cap_stop(SeosL2Cap* seos_l2cap);
+void seos_l2cap_recv(void* context, uint16_t handle, uint8_t flags, BitBuffer* pdu);
+void seos_l2cap_send_next_chunk(void* context);
+void seos_l2cap_send(SeosL2Cap* seos_l2cap, BitBuffer* content);
+void seos_l2cap_central_connection(void* context);
+void seos_l2cap_set_receive_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapReceiveCallback callback,
+    void* context);
+
+void seos_l2cap_set_central_connection_callback(
+    SeosL2Cap* seos_l2cap,
+    SeosL2CapCentralConnectionCallback callback,
+    void* context);

+ 4 - 0
seos_l2cap_i.h

@@ -0,0 +1,4 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_l2cap.h"

+ 475 - 0
seos_reader.c

@@ -0,0 +1,475 @@
+#include "seos_reader_i.h"
+
+#define TAG "SeosReader"
+
+static uint8_t success[] = {0x90, 0x00};
+static uint8_t select[] =
+    {0x00, 0xa4, 0x04, 0x00, 0x0a, 0xa0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01, 0x00};
+static uint8_t SEOS_APPLET_FCI[] =
+    {0x6F, 0x0C, 0x84, 0x0A, 0xA0, 0x00, 0x00, 0x04, 0x40, 0x00, 0x01, 0x01, 0x00, 0x01};
+
+// TODO: support value from keys file
+static uint8_t select_adf[] = {0x80, 0xa5, 0x04, 0x00, 0x13, 0x06, 0x11, 0x2b, 0x06,
+                               0x01, 0x04, 0x01, 0x81, 0xe4, 0x38, 0x01, 0x01, 0x02,
+                               0x01, 0x18, 0x01, 0x01, 0x02, 0x02, 0x00};
+
+static uint8_t general_authenticate_1[] =
+    {0x00, 0x87, 0x00, 0x01, 0x04, 0x7c, 0x02, 0x81, 0x00, 0x00};
+
+SeosReader* seos_reader_alloc(SeosCredential* credential, Iso14443_4aPoller* iso14443_4a_poller) {
+    SeosReader* seos_reader = malloc(sizeof(SeosReader));
+    memset(seos_reader, 0, sizeof(SeosReader));
+    seos_reader->params.key_no = 1;
+    seos_reader->secure_messaging = NULL;
+    memset(seos_reader->params.cNonce, 0x0c, sizeof(seos_reader->params.cNonce));
+    memset(seos_reader->params.UID, 0x0d, sizeof(seos_reader->params.UID));
+
+    seos_reader->credential = credential;
+    seos_reader->iso14443_4a_poller = iso14443_4a_poller;
+
+    seos_reader->tx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+    seos_reader->rx_buffer = bit_buffer_alloc(SEOS_WORKER_MAX_BUFFER_SIZE);
+
+    return seos_reader;
+}
+
+void seos_reader_free(SeosReader* seos_reader) {
+    furi_assert(seos_reader);
+    bit_buffer_free(seos_reader->tx_buffer);
+    bit_buffer_free(seos_reader->rx_buffer);
+    if(seos_reader->secure_messaging) {
+        secure_messaging_free(seos_reader->secure_messaging);
+    }
+    free(seos_reader);
+}
+
+bool seos_reader_request_sio(SeosReader* seos_reader) {
+    SecureMessaging* secure_messaging = seos_reader->secure_messaging;
+    furi_assert(secure_messaging);
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+    Iso14443_4aError error;
+
+    uint8_t message[] = {0x5c, 0x02, 0xff, 0x00};
+    secure_messaging_wrap_apdu(secure_messaging, message, sizeof(message), tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return false;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response(wrapped)", rx_buffer);
+    secure_messaging_unwrap_rapdu(secure_messaging, rx_buffer);
+    seos_log_bitbuffer(TAG, "NFC response(clear)", rx_buffer);
+
+    // Skip fileId
+    seos_reader->credential->sio_len = bit_buffer_get_byte(rx_buffer, 2);
+    if(seos_reader->credential->sio_len > sizeof(seos_reader->credential->sio)) {
+        FURI_LOG_W(TAG, "SIO too long to save");
+        return false;
+    }
+    memcpy(
+        seos_reader->credential->sio,
+        bit_buffer_get_data(rx_buffer) + 3,
+        seos_reader->credential->sio_len);
+
+    return true;
+}
+
+void seos_reader_generate_cryptogram(
+    SeosCredential* credential,
+    AuthParameters* params,
+    uint8_t* cryptogram) {
+    seos_worker_diversify_key(
+        SEOS_ADF1_READ,
+        credential->diversifier,
+        credential->diversifier_len,
+        SEOS_ADF_OID,
+        SEOS_ADF_OID_LEN,
+        params->cipher,
+        params->hash,
+        params->key_no,
+        true,
+        params->priv_key);
+    seos_worker_diversify_key(
+        SEOS_ADF1_READ,
+        credential->diversifier,
+        credential->diversifier_len,
+        SEOS_ADF_OID,
+        SEOS_ADF_OID_LEN,
+        params->cipher,
+        params->hash,
+        params->key_no,
+        false,
+        params->auth_key);
+
+    uint8_t clear[32];
+    memset(clear, 0, sizeof(clear));
+    size_t index = 0;
+    memcpy(clear + index, params->UID, sizeof(params->UID));
+    index += sizeof(params->UID);
+    memcpy(clear + index, params->rndICC, sizeof(params->rndICC));
+    index += sizeof(params->rndICC);
+    memcpy(clear + index, params->cNonce, sizeof(params->cNonce));
+    index += sizeof(params->cNonce);
+
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_encrypt(params->priv_key, sizeof(clear), clear, cryptogram);
+
+        aes_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, index, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_encrypt(params->priv_key, sizeof(clear), clear, cryptogram);
+
+        des_cmac(params->auth_key, sizeof(params->auth_key), cryptogram, index, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+    memcpy(cryptogram + sizeof(clear), cmac, SEOS_WORKER_CMAC_SIZE);
+}
+
+bool seos_reader_verify_cryptogram(AuthParameters* params, const uint8_t* cryptogram) {
+    // cryptogram is 40 bytes: 32 byte encrypted + 8 byte cmac
+    size_t encrypted_len = 32;
+    uint8_t* mac = (uint8_t*)cryptogram + encrypted_len;
+    uint8_t cmac[16];
+    if(params->cipher == AES_128_CBC) {
+        aes_cmac(
+            params->auth_key, sizeof(params->auth_key), (uint8_t*)cryptogram, encrypted_len, cmac);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        des_cmac(
+            params->auth_key, sizeof(params->auth_key), (uint8_t*)cryptogram, encrypted_len, cmac);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    if(memcmp(cmac, mac, SEOS_WORKER_CMAC_SIZE) != 0) {
+        FURI_LOG_W(TAG, "Incorrect cryptogram mac %02x... vs %02x...", cmac[0], mac[0]);
+        return false;
+    }
+
+    uint8_t clear[32];
+    memset(clear, 0, sizeof(clear));
+    if(params->cipher == AES_128_CBC) {
+        seos_worker_aes_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        seos_worker_des_decrypt(params->priv_key, encrypted_len, cryptogram, clear);
+    } else {
+        FURI_LOG_W(TAG, "Cipher not matched");
+    }
+
+    // rndICC[8], UID[8], rNonce[16]
+    uint8_t* rndICC = clear;
+    if(memcmp(rndICC, params->rndICC, sizeof(params->rndICC)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect rndICC returned");
+        return false;
+    }
+    uint8_t* UID = clear + 8;
+    if(memcmp(UID, params->UID, sizeof(params->UID)) != 0) {
+        FURI_LOG_W(TAG, "Incorrect UID returned");
+        return false;
+    }
+
+    memcpy(params->rNonce, clear + 8 + 8, sizeof(params->rNonce));
+    return true;
+}
+
+NfcCommand seos_reader_select_aid(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    bit_buffer_append_bytes(tx_buffer, select, sizeof(select));
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // TODO: validate response
+
+    if(memcmp(
+           bit_buffer_get_data(rx_buffer) + bit_buffer_get_size_bytes(rx_buffer) - sizeof(success),
+           success,
+           sizeof(success)) != 0) {
+        FURI_LOG_W(TAG, "Non-success response");
+        return NfcCommandStop;
+    }
+
+    if(memcmp(bit_buffer_get_data(rx_buffer), SEOS_APPLET_FCI, sizeof(SEOS_APPLET_FCI)) != 0) {
+        FURI_LOG_W(TAG, "Unexpected select AID response");
+        return NfcCommandStop;
+    }
+
+    return ret;
+}
+
+NfcCommand seos_reader_select_adf(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    bit_buffer_append_bytes(tx_buffer, select_adf, sizeof(select_adf));
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+    return ret;
+}
+
+bool seos_reader_select_adf_response(
+    BitBuffer* rx_buffer,
+    size_t offset,
+    SeosCredential* credential,
+    AuthParameters* params) {
+    seos_log_bitbuffer(TAG, "response", rx_buffer);
+
+    // cd 02 0206
+    // 85 38 41c01a89db89aecf 4b35b4f18dc4045b2a3d65cdd1c1944e8c8548f786e6c51128a5c8546a27120a7e44ba0f4cd7218a026ea1a73a9211a9
+    // 8e 08 20f830009042cb85
+
+    uint8_t expected_header[] = {0xcd, 0x02};
+    if(bit_buffer_get_size_bytes(rx_buffer) < sizeof(expected_header)) {
+        FURI_LOG_W(TAG, "Invalid response length");
+        return false;
+    }
+    // handle when the buffer starts with other stuff
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer) + offset;
+    if(memcmp(rx_data, expected_header, sizeof(expected_header)) != 0) {
+        FURI_LOG_W(TAG, "Invalid response");
+        return false;
+    }
+    params->cipher = rx_data[2];
+    params->hash = rx_data[3];
+
+    size_t bufLen = 0;
+    uint8_t clear[0x40];
+
+    if(params->cipher == AES_128_CBC) {
+        size_t ivLen = 16;
+        bufLen = rx_data[5] - ivLen;
+        uint8_t* iv = (uint8_t*)rx_data + 6;
+        uint8_t* enc = (uint8_t*)rx_data + 6 + ivLen;
+        mbedtls_aes_context ctx;
+        mbedtls_aes_init(&ctx);
+        mbedtls_aes_setkey_dec(&ctx, SEOS_ADF1_PRIV_ENC, sizeof(SEOS_ADF1_PRIV_ENC) * 8);
+        mbedtls_aes_crypt_cbc(&ctx, MBEDTLS_AES_DECRYPT, bufLen, iv, enc, clear);
+        mbedtls_aes_free(&ctx);
+    } else if(params->cipher == TWO_KEY_3DES_CBC_MODE) {
+        size_t ivLen = 8;
+        bufLen = rx_data[5] - ivLen;
+        uint8_t* iv = (uint8_t*)rx_data + 6;
+        uint8_t* enc = (uint8_t*)rx_data + 6 + ivLen;
+
+        mbedtls_des3_context ctx;
+        mbedtls_des3_init(&ctx);
+        mbedtls_des3_set2key_dec(&ctx, SEOS_ADF1_PRIV_ENC);
+        mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, bufLen, iv, enc, clear);
+        mbedtls_des3_free(&ctx);
+    }
+
+    // 06112b0601040181e438010102011801010202 cf 07 3d4c010c71cfa7 e2d0b41a00cc5e494c8d52b6e562592399fe614a
+    if(clear[0] != 0x06) {
+        FURI_LOG_W(TAG, "Missing expected 0x06 at start of clear");
+        return false;
+    }
+    size_t oidLen = clear[1];
+    if(clear[2 + oidLen] != 0xCF) {
+        FURI_LOG_W(TAG, "Missing expected 0xCF after OID");
+        return false;
+    }
+    credential->diversifier_len = clear[2 + oidLen + 1];
+    if(credential->diversifier_len > sizeof(credential->diversifier)) {
+        FURI_LOG_W(TAG, "diversifier too large");
+        return false;
+    }
+
+    uint8_t* diversifier = clear + 2 + oidLen + 2;
+    memcpy(credential->diversifier, diversifier, credential->diversifier_len);
+
+    char display[SEOS_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+    memset(display, 0, sizeof(display));
+    for(uint8_t i = 0; i < credential->diversifier_len; i++) {
+        snprintf(display + (i * 2), sizeof(display), "%02x", diversifier[i]);
+    }
+    FURI_LOG_D(TAG, "diversifier: %s", display);
+
+    return true;
+}
+
+NfcCommand seos_reader_general_authenticate_1(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    general_authenticate_1[3] = seos_reader->params.key_no;
+    bit_buffer_append_bytes(tx_buffer, general_authenticate_1, sizeof(general_authenticate_1));
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+    // 7c0a8108018cde7d6049edb09000
+
+    uint8_t expected_header[] = {0x7c, 0x0a, 0x81, 0x08};
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer);
+    if(memcmp(rx_data, expected_header, sizeof(expected_header)) != 0) {
+        FURI_LOG_W(TAG, "Invalid response");
+        return NfcCommandStop;
+    }
+
+    memcpy(seos_reader->params.rndICC, rx_data + 4, 8);
+
+    return ret;
+}
+
+NfcCommand seos_reader_general_authenticate_2(SeosReader* seos_reader) {
+    Iso14443_4aPoller* iso14443_4a_poller = seos_reader->iso14443_4a_poller;
+    BitBuffer* tx_buffer = seos_reader->tx_buffer;
+    BitBuffer* rx_buffer = seos_reader->rx_buffer;
+
+    NfcCommand ret = NfcCommandContinue;
+    Iso14443_4aError error;
+
+    uint8_t cryptogram[32 + 8];
+    memset(cryptogram, 0, sizeof(cryptogram));
+    seos_reader_generate_cryptogram(seos_reader->credential, &seos_reader->params, cryptogram);
+
+    uint8_t ga_header[] = {
+        0x00, 0x87, 0x00, seos_reader->params.key_no, 0x2c, 0x7c, 0x2a, 0x82, 0x28};
+
+    bit_buffer_append_bytes(tx_buffer, ga_header, sizeof(ga_header));
+    bit_buffer_append_bytes(tx_buffer, cryptogram, sizeof(cryptogram));
+    seos_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+
+    error = iso14443_4a_poller_send_block(iso14443_4a_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4aErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4a_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    seos_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    const uint8_t* rx_data = bit_buffer_get_data(rx_buffer);
+    if(rx_data[0] != 0x7C || rx_data[2] != 0x82) {
+        FURI_LOG_W(TAG, "Invalid rx_data");
+        return NfcCommandStop;
+    }
+
+    if(rx_data[3] == 40) {
+        if(!seos_reader_verify_cryptogram(&seos_reader->params, rx_data + 4)) {
+            FURI_LOG_W(TAG, "Card cryptogram failed verification");
+            return NfcCommandStop;
+        }
+        FURI_LOG_I(TAG, "Authenticated");
+    } else {
+        FURI_LOG_W(TAG, "Unhandled card cryptogram size %d", rx_data[3]);
+    }
+
+    seos_reader->secure_messaging = secure_messaging_alloc(&seos_reader->params);
+
+    return ret;
+}
+
+NfcCommand seos_state_machine(Seos* seos, Iso14443_4aPoller* iso14443_4a_poller) {
+    furi_assert(seos);
+    NfcCommand ret = NfcCommandContinue;
+
+    SeosReader* seos_reader = seos_reader_alloc(&seos->credential, iso14443_4a_poller);
+    seos->seos_reader = seos_reader;
+
+    do {
+        ret = seos_reader_select_aid(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        ret = seos_reader_select_adf(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        if(!seos_reader_select_adf_response(
+               seos_reader->rx_buffer, 0, seos_reader->credential, &seos_reader->params)) {
+            break;
+        }
+
+        ret = seos_reader_general_authenticate_1(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        ret = seos_reader_general_authenticate_2(seos_reader);
+        if(ret == NfcCommandStop) break;
+
+        if(seos_reader_request_sio(seos_reader)) {
+            view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderSuccess);
+        }
+
+    } while(false);
+
+    // An error occurred
+    if(ret == NfcCommandStop) {
+        view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventReaderError);
+    }
+    seos_reader_free(seos_reader);
+
+    return NfcCommandStop;
+}
+
+NfcCommand seos_worker_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(event.protocol == NfcProtocolIso14443_4a);
+    NfcCommand ret = NfcCommandContinue;
+
+    Seos* seos = context;
+
+    const Iso14443_4aPollerEvent* iso14443_4a_event = event.event_data;
+    Iso14443_4aPoller* iso14443_4a_poller = event.instance;
+
+    if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeReady) {
+        // view_dispatcher_send_custom_event(seos->view_dispatcher, SeosCustomEventPollerDetect);
+
+        nfc_device_set_data(
+            seos->nfc_device, NfcProtocolIso14443_4a, nfc_poller_get_data(seos->poller));
+
+        ret = seos_state_machine(seos, iso14443_4a_poller);
+
+        // furi_thread_set_current_priority(FuriThreadPriorityLowest);
+    } else if(iso14443_4a_event->type == Iso14443_4aPollerEventTypeError) {
+        Iso14443_4aPollerEventData* data = iso14443_4a_event->data;
+        Iso14443_4aError error = data->error;
+        FURI_LOG_W(TAG, "Iso14443_4aError %i", error);
+        // I was hoping to catch MFC here, but it seems to be treated the same (None) as no card being present.
+        switch(error) {
+        case Iso14443_4aErrorNone:
+            break;
+        case Iso14443_4aErrorNotPresent:
+            break;
+        case Iso14443_4aErrorProtocol:
+            ret = NfcCommandStop;
+            break;
+        case Iso14443_4aErrorTimeout:
+            break;
+        }
+    }
+
+    return ret;
+}

+ 45 - 0
seos_reader.h

@@ -0,0 +1,45 @@
+#pragma once
+
+#include <storage/storage.h>
+#include <dialogs/dialogs.h>
+#include <lib/toolbox/path.h>
+#include <lib/nfc/protocols/nfc_generic_event.h>
+#include <lib/nfc/protocols/iso14443_4a/iso14443_4a_poller.h>
+#include <lib/nfc/helpers/iso14443_crc.h>
+#include <mbedtls/des.h>
+#include <mbedtls/aes.h>
+
+#include "secure_messaging.h"
+
+NfcCommand seos_worker_poller_callback(NfcGenericEvent event, void* context);
+
+typedef struct {
+    Iso14443_4aPoller* iso14443_4a_poller;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+
+    AuthParameters params;
+    SecureMessaging* secure_messaging;
+
+    SeosCredential* credential;
+
+} SeosReader;
+
+SeosReader* seos_reader_alloc(SeosCredential* credential, Iso14443_4aPoller* iso14443_4a_poller);
+
+void seos_reader_free(SeosReader* seos_reader);
+
+bool seos_reader_save(SeosReader* seos_reader, const char* dev_name);
+
+bool seos_reader_select_adf_response(
+    BitBuffer* rx_buffer,
+    size_t offset,
+    SeosCredential* credential,
+    AuthParameters* params);
+
+void seos_reader_generate_cryptogram(
+    SeosCredential* credential,
+    AuthParameters* params,
+    uint8_t* cryptogram);
+
+bool seos_reader_verify_cryptogram(AuthParameters* params, const uint8_t* cryptogram);

+ 5 - 0
seos_reader_i.h

@@ -0,0 +1,5 @@
+#pragma once
+
+#include "seos_i.h"
+#include "seos_reader.h"
+#include "keys.h"

+ 234 - 0
uart.c

@@ -0,0 +1,234 @@
+#include "seos_i.h"
+#include "uart.h"
+
+#define TAG "SeosUART"
+
+#define SEOS_UART_BAUD 460800
+
+static void seos_uart_on_irq_rx_dma_cb(
+    FuriHalSerialHandle* handle,
+    FuriHalSerialRxEvent ev,
+    size_t size,
+    void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+    if(ev & (FuriHalSerialRxEventData | FuriHalSerialRxEventIdle)) {
+        uint8_t data[FURI_HAL_SERIAL_DMA_BUFFER_SIZE] = {0};
+        while(size) {
+            size_t ret = furi_hal_serial_dma_rx(
+                handle,
+                data,
+                (size > FURI_HAL_SERIAL_DMA_BUFFER_SIZE) ? FURI_HAL_SERIAL_DMA_BUFFER_SIZE : size);
+            furi_stream_buffer_send(seos_uart->rx_stream, data, ret, 0);
+            size -= ret;
+        };
+        furi_thread_flags_set(furi_thread_get_id(seos_uart->thread), WorkerEvtRxDone);
+    }
+}
+
+void seos_uart_disable(SeosUart* seos_uart) {
+    furi_assert(seos_uart);
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->thread), WorkerEvtStop);
+    furi_thread_join(seos_uart->thread);
+    furi_thread_free(seos_uart->thread);
+    free(seos_uart);
+}
+
+void seos_uart_serial_init(SeosUart* seos_uart, uint8_t uart_ch) {
+    furi_assert(!seos_uart->serial_handle);
+    SeosUartConfig cfg = seos_uart->cfg;
+
+    seos_uart->serial_handle = furi_hal_serial_control_acquire(uart_ch);
+    furi_assert(seos_uart->serial_handle);
+
+    furi_hal_serial_init(seos_uart->serial_handle, cfg.baudrate);
+    furi_hal_serial_dma_rx_start(
+        seos_uart->serial_handle, seos_uart_on_irq_rx_dma_cb, seos_uart, false);
+}
+
+void seos_uart_serial_deinit(SeosUart* seos_uart) {
+    furi_assert(seos_uart->serial_handle);
+    furi_hal_serial_deinit(seos_uart->serial_handle);
+    furi_hal_serial_control_release(seos_uart->serial_handle);
+    seos_uart->serial_handle = NULL;
+}
+
+void seos_uart_set_baudrate(SeosUart* seos_uart, uint32_t baudrate) {
+    if(baudrate != 0) {
+        furi_hal_serial_set_br(seos_uart->serial_handle, baudrate);
+    } else {
+        FURI_LOG_I(TAG, "No baudrate specified");
+    }
+}
+
+size_t seos_uart_process_buffer(SeosUart* seos_uart, uint8_t* cmd, size_t cmd_len) {
+    if(cmd_len < 2) {
+        return cmd_len;
+    }
+
+    size_t consumed = 0;
+    do {
+        if(seos_uart->receive_callback) {
+            consumed =
+                seos_uart->receive_callback(seos_uart->receive_callback_context, cmd, cmd_len);
+        }
+
+        if(consumed > 0) {
+            memset(cmd, 0, consumed);
+            cmd_len -= consumed;
+            if(cmd_len > 0) {
+                memmove(cmd, cmd + consumed, cmd_len);
+            }
+
+            /*
+            memset(display, 0, SEOS_UART_RX_BUF_SIZE);
+            for(uint8_t i = 0; i < cmd_len; i++) {
+                snprintf(display + (i * 2), sizeof(display), "%02x", cmd[i]);
+            }
+            FURI_LOG_I(TAG, "cmd is now %d bytes: %s", cmd_len, display);
+            */
+        }
+    } while(consumed > 0 && cmd_len > 0);
+    return cmd_len;
+}
+
+int32_t seos_uart_worker(void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+    furi_thread_set_current_priority(FuriThreadPriorityHighest);
+
+    memcpy(&seos_uart->cfg, &seos_uart->cfg_new, sizeof(SeosUartConfig));
+
+    seos_uart->rx_stream = furi_stream_buffer_alloc(SEOS_UART_RX_BUF_SIZE, 1);
+
+    seos_uart->tx_sem = furi_semaphore_alloc(1, 1);
+
+    seos_uart->tx_thread =
+        furi_thread_alloc_ex("SeosUartTxWorker", 1.5 * 1024, seos_uart_tx_thread, seos_uart);
+
+    seos_uart_serial_init(seos_uart, seos_uart->cfg.uart_ch);
+    seos_uart_set_baudrate(seos_uart, seos_uart->cfg.baudrate);
+
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtDevRx);
+
+    furi_thread_start(seos_uart->tx_thread);
+
+    uint8_t cmd[SEOS_UART_RX_BUF_SIZE];
+    size_t cmd_len = 0;
+
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_RX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check(!(events & FuriFlagError));
+        if(events & WorkerEvtStop) {
+            memset(cmd, 0, cmd_len);
+            cmd_len = 0;
+            break;
+        }
+        if(events & (WorkerEvtRxDone | WorkerEvtDevTxComplete)) {
+            size_t len = furi_stream_buffer_receive(
+                seos_uart->rx_stream, seos_uart->rx_buf, SEOS_UART_RX_BUF_SIZE, 0);
+            if(len > 0) {
+                furi_delay_ms(5); //WTF
+
+                /*
+                char display[SEOS_UART_RX_BUF_SIZE * 2 + 1] = {0};
+                for(uint8_t i = 0; i < len; i++) {
+                    snprintf(display + (i * 2), sizeof(display), "%02x", seos_uart->rx_buf[i]);
+                }
+                FURI_LOG_D(TAG, "RECV %d bytes: %s", len, display);
+                */
+
+                if(cmd_len + len > SEOS_UART_RX_BUF_SIZE) {
+                    FURI_LOG_I(TAG, "OVERFLOW: %d + %d", cmd_len, len);
+                    memset(cmd, 0, cmd_len);
+                    cmd_len = 0;
+                }
+
+                memcpy(cmd + cmd_len, seos_uart->rx_buf, len);
+                cmd_len += len;
+                cmd_len = seos_uart_process_buffer(seos_uart, cmd, cmd_len);
+            }
+        }
+    }
+    seos_uart_serial_deinit(seos_uart);
+
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtTxStop);
+    furi_thread_join(seos_uart->tx_thread);
+    furi_thread_free(seos_uart->tx_thread);
+
+    furi_stream_buffer_free(seos_uart->rx_stream);
+    furi_semaphore_free(seos_uart->tx_sem);
+    return 0;
+}
+
+SeosUart* seos_uart_enable(SeosUartConfig* cfg) {
+    SeosUart* seos_uart = malloc(sizeof(SeosUart));
+
+    memcpy(&(seos_uart->cfg_new), cfg, sizeof(SeosUartConfig));
+
+    seos_uart->thread =
+        furi_thread_alloc_ex("SeosUartWorker", 5 * 1024, seos_uart_worker, seos_uart);
+
+    furi_thread_start(seos_uart->thread);
+    return seos_uart;
+}
+
+int32_t seos_uart_tx_thread(void* context) {
+    SeosUart* seos_uart = (SeosUart*)context;
+
+    furi_thread_set_current_priority(FuriThreadPriorityHighest);
+    while(1) {
+        uint32_t events =
+            furi_thread_flags_wait(WORKER_ALL_TX_EVENTS, FuriFlagWaitAny, FuriWaitForever);
+        furi_check(!(events & FuriFlagError));
+        if(events & WorkerEvtTxStop) break;
+        if(events & WorkerEvtDevRx) {
+            if(seos_uart->tx_len > 0) {
+                /*
+                char display[SEOS_UART_RX_BUF_SIZE * 2 + 1] = {0};
+                for(uint8_t i = 0; i < seos_uart->tx_len; i++) {
+                    snprintf(display + (i * 2), sizeof(display), "%02x", seos_uart->tx_buf[i]);
+                }
+                FURI_LOG_D(TAG, "SEND %d bytes: %s", seos_uart->tx_len, display);
+                */
+                furi_hal_serial_tx(seos_uart->serial_handle, seos_uart->tx_buf, seos_uart->tx_len);
+            }
+        }
+    }
+    return 0;
+}
+
+void seos_uart_get_config(SeosUart* seos_uart, SeosUartConfig* cfg) {
+    furi_assert(seos_uart);
+    furi_assert(cfg);
+    memcpy(cfg, &(seos_uart->cfg_new), sizeof(SeosUartConfig));
+}
+
+SeosUart* seos_uart_alloc() {
+    SeosUartConfig cfg = {.uart_ch = FuriHalSerialIdLpuart, .baudrate = SEOS_UART_BAUD};
+    SeosUart* seos_uart;
+
+    FURI_LOG_I(TAG, "Enable UART");
+    seos_uart = seos_uart_enable(&cfg);
+
+    seos_uart_get_config(seos_uart, &cfg);
+    return seos_uart;
+}
+
+void seos_uart_free(SeosUart* seos_uart) {
+    seos_uart_disable(seos_uart);
+}
+
+void seos_uart_send(SeosUart* seos_uart, uint8_t* buffer, size_t len) {
+    memset(seos_uart->tx_buf, 0, sizeof(seos_uart->tx_buf));
+    memcpy(seos_uart->tx_buf, buffer, len);
+    seos_uart->tx_len = len;
+    furi_thread_flags_set(furi_thread_get_id(seos_uart->tx_thread), WorkerEvtDevRx);
+}
+
+void seos_uart_set_receive_callback(
+    SeosUart* seos_uart,
+    SeosUartReceiveCallback callback,
+    void* context) {
+    seos_uart->receive_callback = callback;
+    seos_uart->receive_callback_context = context;
+}

+ 24 - 0
uart.h

@@ -0,0 +1,24 @@
+#pragma once
+
+#include "uart_i.h"
+
+int32_t seos_uart_tx_thread(void* context);
+void seos_uart_on_irq_cb(uint8_t data, void* context);
+void seos_uart_serial_init(SeosUart* seos_uart, uint8_t uart_ch);
+void seos_uart_serial_deinit(SeosUart* seos_uart);
+void seos_uart_set_baudrate(SeosUart* seos_uart, uint32_t baudrate);
+int32_t seos_uart_worker(void* context);
+
+SeosUart* seos_uart_enable(SeosUartConfig* cfg);
+void seos_uart_disable(SeosUart* seos_uart);
+void seos_uart_set_config(SeosUart* seos_uart, SeosUartConfig* cfg);
+void seos_uart_get_config(SeosUart* seos_uart, SeosUartConfig* cfg);
+
+SeosUart* seos_uart_alloc();
+void seos_uart_free(SeosUart* seos_uart);
+
+void seos_uart_send(SeosUart* seos_uart, uint8_t* buffer, size_t len);
+void seos_uart_set_receive_callback(
+    SeosUart* seos_uart,
+    SeosUartReceiveCallback callback,
+    void* context);

+ 55 - 0
uart_i.h

@@ -0,0 +1,55 @@
+#pragma once
+
+#include <stdlib.h> // malloc
+#include <stdint.h> // uint32_t
+#include <stdarg.h> // __VA_ARGS__
+#include <string.h>
+#include <stdio.h>
+
+#include <furi.h>
+#include <furi_hal.h>
+
+#define SEOS_UART_RX_BUF_SIZE (256)
+
+#define WORKER_ALL_RX_EVENTS (WorkerEvtStop | WorkerEvtRxDone | WorkerEvtDevTxComplete)
+#define WORKER_ALL_TX_EVENTS (WorkerEvtTxStop | WorkerEvtDevRx)
+
+typedef size_t (*SeosUartReceiveCallback)(void* context, uint8_t* buffer, size_t len);
+
+typedef enum {
+    WorkerEvtStop = (1 << 0),
+    WorkerEvtRxDone = (1 << 1),
+
+    WorkerEvtTxStop = (1 << 2),
+    WorkerEvtDevRx = (1 << 3),
+    WorkerEvtDevTxComplete = (1 << 4),
+} WorkerEvtFlags;
+
+typedef struct {
+    uint8_t uart_ch;
+    uint8_t flow_pins;
+    uint8_t baudrate_mode;
+    uint32_t baudrate;
+} SeosUartConfig;
+
+struct SeosUart {
+    SeosUartConfig cfg;
+    SeosUartConfig cfg_new;
+
+    FuriThread* thread;
+    FuriThread* tx_thread;
+
+    FuriStreamBuffer* rx_stream;
+    FuriHalSerialHandle* serial_handle;
+
+    FuriSemaphore* tx_sem;
+
+    uint8_t rx_buf[SEOS_UART_RX_BUF_SIZE];
+    uint8_t tx_buf[SEOS_UART_RX_BUF_SIZE];
+    size_t tx_len;
+
+    SeosUartReceiveCallback receive_callback;
+    void* receive_callback_context;
+};
+
+typedef struct SeosUart SeosUart;