Eric Betts 9 месяцев назад
Сommit
9fe02b0a3e

+ 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
+...
+

+ 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@v4
+        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 }}

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+dist
+.vscode

+ 18 - 0
application.fam

@@ -0,0 +1,18 @@
+# For details & more options, see documentation/AppManifests.md in firmware repo
+
+App(
+    appid="passy",  # Must be unique
+    name="Passport",  # Displayed in menus
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="passy_app",
+    stack_size=2 * 1024,
+    fap_category="NFC",
+    # Optional values
+    fap_version="0.1",
+    fap_icon="passy.png",  # 10x10 1-bit PNG
+    fap_description="eMRTD Reader",
+    fap_author="bettse",
+    # fap_weburl="https://github.com/user/passy",
+    fap_icon_assets="images",  # Image assets to compile for this application
+    fap_libs=["mbedtls"],
+)

+ 0 - 0
images/.gitkeep


BIN
images/RFIDDolphinReceive_97x61.png


+ 292 - 0
passy.c

@@ -0,0 +1,292 @@
+#include "passy_i.h"
+
+#define TAG "Passy"
+
+#define PASSY_MRZ_INFO_FILENAME "mrz_info"
+
+bool passy_load_mrz_info(Passy* passy) {
+    const char* file_header = "MRZ Info";
+    const uint32_t file_version = 1;
+    bool parsed = false;
+    FlipperFormat* file = flipper_format_file_alloc(passy->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, PASSY_MRZ_INFO_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;
+        }
+        // passport number
+        // dob
+        // doe
+
+        if(!flipper_format_read_string(file, "Passport Number", temp_str)) break;
+        strncpy(
+            passy->passport_number,
+            furi_string_get_cstr(temp_str),
+            PASSY_PASSPORT_NUMBER_MAX_LENGTH);
+        if(!flipper_format_read_string(file, "Date of Birth", temp_str)) break;
+        strncpy(passy->date_of_birth, furi_string_get_cstr(temp_str), PASSY_DOB_MAX_LENGTH);
+        if(!flipper_format_read_string(file, "Date of Expiry", temp_str)) break;
+        strncpy(passy->date_of_expiry, furi_string_get_cstr(temp_str), PASSY_DOE_MAX_LENGTH);
+
+        parsed = true;
+    } while(false);
+
+    if(parsed) {
+        FURI_LOG_I(TAG, "MRZ Info loaded");
+    }
+
+    furi_string_free(path);
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+
+    return parsed;
+}
+
+bool passy_save_mrz_info(Passy* passy) {
+    bool saved = false;
+    const char* file_header = "MRZ Info";
+    const uint32_t file_version = 1;
+    FlipperFormat* file = flipper_format_file_alloc(passy->storage);
+    FuriString* temp_str = furi_string_alloc();
+
+    do {
+        furi_string_printf(
+            temp_str, "%s/%s%s", STORAGE_APP_DATA_PATH_PREFIX, PASSY_MRZ_INFO_FILENAME, ".txt");
+
+        // 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, file_header, file_version)) break;
+
+        furi_string_set_str(temp_str, passy->passport_number);
+        if(!flipper_format_write_string(file, "Passport Number", temp_str)) break;
+        furi_string_set_str(temp_str, passy->date_of_birth);
+        if(!flipper_format_write_string(file, "Date of Birth", temp_str)) break;
+        furi_string_set_str(temp_str, passy->date_of_expiry);
+        if(!flipper_format_write_string(file, "Date of Expiry", temp_str)) break;
+
+        saved = true;
+    } while(false);
+
+    furi_string_free(temp_str);
+    flipper_format_free(file);
+    return saved;
+}
+
+bool passy_custom_event_callback(void* context, uint32_t event) {
+    furi_assert(context);
+    Passy* passy = context;
+    return scene_manager_handle_custom_event(passy->scene_manager, event);
+}
+
+bool passy_back_event_callback(void* context) {
+    furi_assert(context);
+    Passy* passy = context;
+    return scene_manager_handle_back_event(passy->scene_manager);
+}
+
+void passy_tick_event_callback(void* context) {
+    furi_assert(context);
+    Passy* passy = context;
+    scene_manager_handle_tick_event(passy->scene_manager);
+}
+
+Passy* passy_alloc() {
+    Passy* passy = malloc(sizeof(Passy));
+
+    passy->view_dispatcher = view_dispatcher_alloc();
+    passy->scene_manager = scene_manager_alloc(&passy_scene_handlers, passy);
+    view_dispatcher_set_event_callback_context(passy->view_dispatcher, passy);
+    view_dispatcher_set_custom_event_callback(passy->view_dispatcher, passy_custom_event_callback);
+    view_dispatcher_set_navigation_event_callback(
+        passy->view_dispatcher, passy_back_event_callback);
+    view_dispatcher_set_tick_event_callback(
+        passy->view_dispatcher, passy_tick_event_callback, 100);
+
+    passy->nfc = nfc_alloc();
+
+    // Nfc device
+    passy->nfc_device = nfc_device_alloc();
+    nfc_device_set_loading_callback(passy->nfc_device, passy_show_loading_popup, passy);
+
+    // Open GUI record
+    passy->gui = furi_record_open(RECORD_GUI);
+    view_dispatcher_attach_to_gui(
+        passy->view_dispatcher, passy->gui, ViewDispatcherTypeFullscreen);
+
+    // Open Notification record
+    passy->notifications = furi_record_open(RECORD_NOTIFICATION);
+
+    // Submenu
+    passy->submenu = submenu_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewMenu, submenu_get_view(passy->submenu));
+
+    // Popup
+    passy->popup = popup_alloc();
+    view_dispatcher_add_view(passy->view_dispatcher, PassyViewPopup, popup_get_view(passy->popup));
+
+    // Loading
+    passy->loading = loading_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewLoading, loading_get_view(passy->loading));
+
+    // Text Input
+    passy->text_input = text_input_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewTextInput, text_input_get_view(passy->text_input));
+
+    // Number Input
+    passy->number_input = number_input_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewNumberInput, number_input_get_view(passy->number_input));
+
+    // TextBox
+    passy->text_box = text_box_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewTextBox, text_box_get_view(passy->text_box));
+    passy->text_box_store = furi_string_alloc();
+
+    // Custom Widget
+    passy->widget = widget_alloc();
+    view_dispatcher_add_view(
+        passy->view_dispatcher, PassyViewWidget, widget_get_view(passy->widget));
+
+    passy->storage = furi_record_open(RECORD_STORAGE);
+    passy->dialogs = furi_record_open(RECORD_DIALOGS);
+    passy->load_path = furi_string_alloc();
+
+    passy->DG1 = bit_buffer_alloc(PASSY_DG1_MAX_LENGTH);
+
+    return passy;
+}
+
+void passy_free(Passy* passy) {
+    furi_assert(passy);
+
+    nfc_free(passy->nfc);
+
+    // Nfc device
+    nfc_device_free(passy->nfc_device);
+
+    // Submenu
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewMenu);
+    submenu_free(passy->submenu);
+
+    // Popup
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewPopup);
+    popup_free(passy->popup);
+
+    // Loading
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewLoading);
+    loading_free(passy->loading);
+
+    // TextInput
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewTextInput);
+    text_input_free(passy->text_input);
+
+    // NumberInput
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewNumberInput);
+    number_input_free(passy->number_input);
+
+    // TextBox
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewTextBox);
+    text_box_free(passy->text_box);
+    furi_string_free(passy->text_box_store);
+
+    // Custom Widget
+    view_dispatcher_remove_view(passy->view_dispatcher, PassyViewWidget);
+    widget_free(passy->widget);
+
+    // View Dispatcher
+    view_dispatcher_free(passy->view_dispatcher);
+
+    // Scene Manager
+    scene_manager_free(passy->scene_manager);
+
+    // GUI
+    furi_record_close(RECORD_GUI);
+    passy->gui = NULL;
+
+    // Notifications
+    furi_record_close(RECORD_NOTIFICATION);
+    passy->notifications = NULL;
+
+    furi_string_free(passy->load_path);
+    furi_record_close(RECORD_STORAGE);
+    furi_record_close(RECORD_DIALOGS);
+
+    bit_buffer_free(passy->DG1);
+
+    free(passy);
+}
+
+void passy_text_store_set(Passy* passy, const char* text, ...) {
+    va_list args;
+    va_start(args, text);
+
+    vsnprintf(passy->text_store, sizeof(passy->text_store), text, args);
+
+    va_end(args);
+}
+
+void passy_text_store_clear(Passy* passy) {
+    memset(passy->text_store, 0, sizeof(passy->text_store));
+}
+
+static const NotificationSequence passy_sequence_blink_start_blue = {
+    &message_blink_start_10,
+    &message_blink_set_color_blue,
+    &message_do_not_reset,
+    NULL,
+};
+
+static const NotificationSequence passy_sequence_blink_stop = {
+    &message_blink_stop,
+    NULL,
+};
+
+void passy_blink_start(Passy* passy) {
+    notification_message(passy->notifications, &passy_sequence_blink_start_blue);
+}
+
+void passy_blink_stop(Passy* passy) {
+    notification_message(passy->notifications, &passy_sequence_blink_stop);
+}
+
+void passy_show_loading_popup(void* context, bool show) {
+    Passy* passy = context;
+
+    if(show) {
+        // Raise timer priority so that animations can play
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityElevated);
+        view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewLoading);
+    } else {
+        // Restore default timer priority
+        furi_timer_set_thread_priority(FuriTimerThreadPriorityNormal);
+    }
+}
+
+int32_t passy_app(void* p) {
+    UNUSED(p);
+    Passy* passy = passy_alloc();
+
+    passy_load_mrz_info(passy);
+
+    scene_manager_next_scene(passy->scene_manager, PassySceneMainMenu);
+
+    view_dispatcher_run(passy->view_dispatcher);
+
+    passy_free(passy);
+
+    return 0;
+}

+ 5 - 0
passy.h

@@ -0,0 +1,5 @@
+#pragma once
+
+typedef struct Passy Passy;
+
+bool passy_save_mrz_info(Passy* passy);


+ 141 - 0
passy_common.c

@@ -0,0 +1,141 @@
+#include "passy_common.h"
+
+#define PASSY_WORKER_MAX_BUFFER_SIZE 128
+
+#define TAG "PassyCommon"
+
+void passy_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[PASSY_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)PASSY_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 passy_log_buffer(char* tag, char* prefix, uint8_t* buffer, size_t buffer_len) {
+    char display[PASSY_WORKER_MAX_BUFFER_SIZE * 2 + 1];
+
+    size_t limit = MIN((size_t)PASSY_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);
+    }
+}
+
+// ISO/IEC 9797-1 MAC Algorithm 3
+void passy_mac(uint8_t* key, uint8_t* data, size_t data_length, uint8_t* mac, bool prepadded) {
+    size_t block_count = data_length / 8;
+    uint8_t y[8];
+    memset(y, 0, sizeof(y));
+
+    for(size_t i = 0; i < block_count; i++) {
+        uint8_t* x = data + (i * 8);
+        //passy_log_buffer(TAG, "x", x, 8);
+
+        uint8_t iv[8];
+        memcpy(iv, y, sizeof(y));
+        mbedtls_des_context ctx;
+        mbedtls_des_init(&ctx);
+        mbedtls_des_setkey_enc(&ctx, key); // uses 8 bytes
+        mbedtls_des_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, 8, iv, x, y);
+        mbedtls_des_free(&ctx);
+        //passy_log_buffer(TAG, "y", y, 8);
+    }
+
+    mbedtls_des_context ctx;
+
+    if(!prepadded) {
+        // last block
+        uint8_t last_block[8] = {0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
+        uint8_t iv[8];
+        memcpy(iv, y, sizeof(y));
+        mbedtls_des_init(&ctx);
+        mbedtls_des_setkey_enc(&ctx, key); // uses 8 bytes
+        mbedtls_des_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, 8, iv, last_block, y);
+        //passy_log_buffer(TAG, "y", y, 8);
+    }
+
+    uint8_t b[8];
+    mbedtls_des_init(&ctx);
+    mbedtls_des_setkey_dec(&ctx, key + 8); // uses 8 bytes
+    mbedtls_des_crypt_ecb(&ctx, y, b);
+    //passy_log_buffer(TAG, "b", b, 8);
+
+    mbedtls_des_init(&ctx);
+    mbedtls_des_setkey_enc(&ctx, key); // uses 8 bytes
+    mbedtls_des_crypt_ecb(&ctx, b, mac);
+    //passy_log_buffer(TAG, "mac", mac, 8);
+
+    mbedtls_des_free(&ctx);
+}
+
+// https://en.wikipedia.org/wiki/Machine-readable_passport
+char passy_checksum(char* str) {
+    uint8_t values[] = {
+        0,  1,  2,  3,  4,  5,  6,  7,  8,  9,  10, 11, 12,
+        13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
+        // < = filler
+        // A = 10
+        // B = 11
+        // C = 12
+        // D = 13
+        // E = 14
+        // F = 15
+        // G = 16
+        // H = 17
+        // I = 18
+        // J = 19
+        // K = 20
+        // L = 21
+        // M = 22
+        // N = 23
+        // O = 24
+        // P = 25
+        // Q = 26
+        // R = 27
+        // S = 28
+        // T = 29
+        // U = 30
+        // V = 31
+        // W = 32
+        // X = 33
+        // Y = 34
+        // Z = 35
+    };
+
+    size_t sum = 0;
+    uint8_t weight_map[] = {7, 3, 1};
+    for(uint8_t i = 0; i < strlen(str); i++) {
+        uint8_t value;
+        uint8_t weight = weight_map[(i % 3)];
+        if(str[i] >= '0' && str[i] <= '9') {
+            value = values[str[i] - '0'];
+        } else if(str[i] >= 'A' && str[i] <= 'Z') {
+            value = values[str[i] - 'A' + 10];
+        } else if(str[i] == '<') {
+            value = 0;
+        } else {
+            FURI_LOG_E(TAG, "Invalid character %02x", str[i]);
+            return -1;
+        }
+        sum += value * weight;
+    }
+    return 0x30 + (sum % 10);
+}

+ 10 - 0
passy_common.h

@@ -0,0 +1,10 @@
+#pragma once
+
+#include <mbedtls/des.h>
+#include <furi.h>
+#include <toolbox/bit_buffer.h>
+
+void passy_log_bitbuffer(char* tag, char* prefix, BitBuffer* buffer);
+void passy_log_buffer(char* tag, char* prefix, uint8_t* buffer, size_t buffer_len);
+void passy_mac(uint8_t* key, uint8_t* data, size_t data_length, uint8_t* mac, bool prepadded);
+char passy_checksum(char* str);

+ 111 - 0
passy_i.h

@@ -0,0 +1,111 @@
+#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 <dialogs/dialogs.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/number_input.h>
+#include <gui/modules/text_box.h>
+#include <gui/modules/widget.h>
+
+#include <input/input.h>
+#include <lib/flipper_format/flipper_format.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 <passy_icons.h>
+
+#include "passy.h"
+#include "passy_common.h"
+#include "scenes/passy_scene.h"
+
+#define PASSY_TEXT_STORE_SIZE            128
+#define PASSY_FILE_NAME_MAX_LENGTH       32
+#define PASSY_PASSPORT_NUMBER_MAX_LENGTH 32
+#define PASSY_DOB_MAX_LENGTH             8
+#define PASSY_DOE_MAX_LENGTH             8
+
+#define PASSY_DG1_MAX_LENGTH 256
+
+enum PassyCustomEvent {
+    // Reserve first 100 events for button types and indexes, starting from 0
+    PassyCustomEventReserved = 100,
+
+    PassyCustomEventViewExit,
+    PassyCustomEventTextInputDone,
+    PassyCustomEventNumberInputDone,
+    // Read card events
+    PassyCustomEventReaderError,
+    PassyCustomEventReaderSuccess,
+    PassyCustomEventReaderDetected,
+    PassyCustomEventReaderAuthenticated,
+    PassyCustomEventReaderReading,
+};
+
+struct Passy {
+    ViewDispatcher* view_dispatcher;
+    Gui* gui;
+    NotificationApp* notifications;
+    SceneManager* scene_manager;
+    Storage* storage;
+
+    char text_store[PASSY_TEXT_STORE_SIZE + 1];
+    FuriString* text_box_store;
+
+    // Common Views
+    Submenu* submenu;
+    Popup* popup;
+    Loading* loading;
+    TextInput* text_input;
+    NumberInput* number_input;
+    TextBox* text_box;
+    Widget* widget;
+    DialogsApp* dialogs;
+
+    Nfc* nfc;
+    NfcListener* listener;
+    NfcPoller* poller;
+    NfcDevice* nfc_device;
+
+    FuriString* load_path;
+    char file_name[PASSY_FILE_NAME_MAX_LENGTH + 1];
+
+    char passport_number[PASSY_PASSPORT_NUMBER_MAX_LENGTH + 1];
+    char date_of_birth[PASSY_DOB_MAX_LENGTH + 1];
+    char date_of_expiry[PASSY_DOE_MAX_LENGTH + 1];
+
+    BitBuffer* DG1;
+};
+
+typedef enum {
+    PassyViewMenu,
+    PassyViewPopup,
+    PassyViewLoading,
+    PassyViewTextInput,
+    PassyViewNumberInput,
+    PassyViewTextBox,
+    PassyViewWidget,
+} PassyView;
+
+void passy_text_store_set(Passy* passy, const char* text, ...);
+
+void passy_text_store_clear(Passy* passy);
+
+void passy_blink_start(Passy* passy);
+
+void passy_blink_stop(Passy* passy);
+
+void passy_show_loading_popup(void* context, bool show);

+ 435 - 0
passy_reader.c

@@ -0,0 +1,435 @@
+#include "passy_reader.h"
+
+#define TAG "PassyReader"
+
+static uint8_t passport_aid[] = {0xA0, 0x00, 0x00, 0x02, 0x47, 0x10, 0x01};
+static uint8_t select_header[] = {0x00, 0xA4, 0x04, 0x0C};
+
+static uint8_t get_challenge[] = {0x00, 0x84, 0x00, 0x00, 0x08};
+
+static uint8_t SW_success[] = {0x90, 0x00};
+
+PassyReader* passy_reader_alloc(Passy* passy, Iso14443_4bPoller* iso14443_4b_poller) {
+    PassyReader* passy_reader = malloc(sizeof(PassyReader));
+    memset(passy_reader, 0, sizeof(PassyReader));
+
+    passy_reader->iso14443_4b_poller = iso14443_4b_poller;
+
+    passy_reader->DG1 = passy->DG1;
+    passy_reader->tx_buffer = bit_buffer_alloc(PASSY_READER_MAX_BUFFER_SIZE);
+    passy_reader->rx_buffer = bit_buffer_alloc(PASSY_READER_MAX_BUFFER_SIZE);
+
+    char passport_number[11];
+    memset(passport_number, 0, sizeof(passport_number));
+    memcpy(passport_number, passy->passport_number, strlen(passy->passport_number));
+    passport_number[strlen(passy->passport_number)] = passy_checksum(passy->passport_number);
+    FURI_LOG_I(TAG, "Passport number: %s", passport_number);
+
+    char date_of_birth[8];
+    memset(date_of_birth, 0, sizeof(date_of_birth));
+    memcpy(date_of_birth, passy->date_of_birth, strlen(passy->date_of_birth));
+    date_of_birth[strlen(passy->date_of_birth)] = passy_checksum(passy->date_of_birth);
+    FURI_LOG_I(TAG, "Date of birth: %s", date_of_birth);
+
+    char date_of_expiry[8];
+    memset(date_of_expiry, 0, sizeof(date_of_expiry));
+    memcpy(date_of_expiry, passy->date_of_expiry, strlen(passy->date_of_expiry));
+    date_of_expiry[strlen(passy->date_of_expiry)] = passy_checksum(passy->date_of_expiry);
+    FURI_LOG_I(TAG, "Date of expiry: %s", date_of_expiry);
+
+    passy_save_mrz_info(passy);
+
+    passy_reader->secure_messaging = secure_messaging_alloc(
+        (uint8_t*)passport_number, (uint8_t*)date_of_birth, (uint8_t*)date_of_expiry);
+
+    return passy_reader;
+}
+
+void passy_reader_free(PassyReader* passy_reader) {
+    furi_assert(passy_reader);
+    bit_buffer_free(passy_reader->tx_buffer);
+    bit_buffer_free(passy_reader->rx_buffer);
+    if(passy_reader->secure_messaging) {
+        secure_messaging_free(passy_reader->secure_messaging);
+    }
+    free(passy_reader);
+}
+
+NfcCommand passy_reader_select_application(PassyReader* passy_reader) {
+    NfcCommand ret = NfcCommandContinue;
+
+    BitBuffer* tx_buffer = passy_reader->tx_buffer;
+    BitBuffer* rx_buffer = passy_reader->rx_buffer;
+    Iso14443_4bPoller* iso14443_4b_poller = passy_reader->iso14443_4b_poller;
+    Iso14443_4bError error;
+
+    bit_buffer_append_bytes(tx_buffer, select_header, sizeof(select_header));
+    bit_buffer_append_byte(tx_buffer, sizeof(passport_aid));
+    bit_buffer_append_bytes(tx_buffer, passport_aid, sizeof(passport_aid));
+    bit_buffer_append_byte(tx_buffer, 0x00); // Le
+
+    error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4b_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // Check SW
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    if(length < 2) {
+        FURI_LOG_W(TAG, "Invalid response length %d", length);
+        return NfcCommandStop;
+    }
+    if(memcmp(data + length - 2, SW_success, sizeof(SW_success)) != 0) {
+        FURI_LOG_W(TAG, "Invalid SW %02x %02x", data[length - 2], data[length - 1]);
+        return NfcCommandStop;
+    }
+
+    return ret;
+}
+
+NfcCommand passy_reader_get_challenge(PassyReader* passy_reader) {
+    NfcCommand ret = NfcCommandContinue;
+
+    BitBuffer* tx_buffer = passy_reader->tx_buffer;
+    BitBuffer* rx_buffer = passy_reader->rx_buffer;
+    Iso14443_4bPoller* iso14443_4b_poller = passy_reader->iso14443_4b_poller;
+    Iso14443_4bError error;
+
+    bit_buffer_append_bytes(tx_buffer, get_challenge, sizeof(get_challenge));
+
+    error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4b_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // Check SW
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    if(length < 2) {
+        FURI_LOG_W(TAG, "Invalid response length %d", length);
+        return NfcCommandStop;
+    }
+    if(memcmp(data + length - 2, SW_success, sizeof(SW_success)) != 0) {
+        FURI_LOG_W(TAG, "Invalid SW %02x %02x", data[length - 2], data[length - 1]);
+        return NfcCommandStop;
+    }
+
+    SecureMessaging* secure_messaging = passy_reader->secure_messaging;
+    const uint8_t* rnd_icc = data;
+    memcpy(secure_messaging->rndICC, rnd_icc, 8);
+
+    return ret;
+}
+
+NfcCommand passy_reader_authenticate(PassyReader* passy_reader) {
+    NfcCommand ret = NfcCommandContinue;
+    BitBuffer* tx_buffer = passy_reader->tx_buffer;
+    BitBuffer* rx_buffer = passy_reader->rx_buffer;
+    Iso14443_4bPoller* iso14443_4b_poller = passy_reader->iso14443_4b_poller;
+    Iso14443_4bError error;
+
+    // TODO: move into secure_messaging
+    SecureMessaging* secure_messaging = passy_reader->secure_messaging;
+    uint8_t S[32];
+    memset(S, 0, sizeof(S));
+    uint8_t eifd[32];
+    memcpy(S, secure_messaging->rndIFD, sizeof(secure_messaging->rndIFD));
+    memcpy(
+        S + sizeof(secure_messaging->rndIFD),
+        secure_messaging->rndICC,
+        sizeof(secure_messaging->rndICC));
+    memcpy(
+        S + sizeof(secure_messaging->rndIFD) + sizeof(secure_messaging->rndICC),
+        secure_messaging->Kifd,
+        sizeof(secure_messaging->Kifd));
+
+    uint8_t iv[8];
+    memset(iv, 0, sizeof(iv));
+    mbedtls_des3_context ctx;
+    mbedtls_des3_init(&ctx);
+    mbedtls_des3_set2key_enc(&ctx, secure_messaging->KENC);
+    mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_ENCRYPT, sizeof(S), iv, S, eifd);
+    mbedtls_des3_free(&ctx);
+
+    passy_log_buffer(TAG, "S", S, sizeof(S));
+    passy_log_buffer(TAG, "eifd", eifd, sizeof(eifd));
+
+    uint8_t mifd[8];
+    passy_mac(secure_messaging->KMAC, eifd, sizeof(eifd), mifd, false);
+    passy_log_buffer(TAG, "mifd", mifd, sizeof(mifd));
+
+    uint8_t authenticate_header[] = {0x00, 0x82, 0x00, 0x00};
+
+    bit_buffer_append_bytes(tx_buffer, authenticate_header, sizeof(authenticate_header));
+    bit_buffer_append_byte(tx_buffer, sizeof(eifd) + sizeof(mifd));
+    bit_buffer_append_bytes(tx_buffer, eifd, sizeof(eifd));
+    bit_buffer_append_bytes(tx_buffer, mifd, sizeof(mifd));
+    bit_buffer_append_byte(tx_buffer, 0x28); // Le
+
+    passy_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+
+    error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4b_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // Check SW
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    if(length < 2) {
+        FURI_LOG_W(TAG, "Invalid response length %d", length);
+        return NfcCommandStop;
+    }
+    if(memcmp(data + length - 2, SW_success, sizeof(SW_success)) != 0) {
+        FURI_LOG_W(TAG, "Invalid SW %02x %02x", data[length - 2], data[length - 1]);
+        return NfcCommandStop;
+    }
+
+    const uint8_t* mac = data + length - 2 - 8;
+    uint8_t calculated_mac[8];
+    passy_mac(secure_messaging->KMAC, (uint8_t*)data, length - 8 - 2, calculated_mac, false);
+    if(memcmp(mac, calculated_mac, sizeof(calculated_mac)) != 0) {
+        FURI_LOG_W(TAG, "Invalid MAC");
+        return NfcCommandStop;
+    }
+
+    uint8_t decrypted[32];
+    do {
+        uint8_t iv[8];
+        memset(iv, 0, sizeof(iv));
+
+        mbedtls_des3_context ctx;
+        mbedtls_des3_init(&ctx);
+        mbedtls_des3_set2key_dec(&ctx, secure_messaging->KENC);
+        mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, length - 2 - 8, iv, data, decrypted);
+        mbedtls_des3_free(&ctx);
+    } while(false);
+    passy_log_buffer(TAG, "decrypted", decrypted, sizeof(decrypted));
+
+    uint8_t* rnd_icc = decrypted;
+    uint8_t* rnd_ifd = decrypted + 8;
+    uint8_t* Kicc = decrypted + 16;
+
+    if(memcmp(rnd_icc, secure_messaging->rndICC, sizeof(secure_messaging->rndICC)) != 0) {
+        FURI_LOG_W(TAG, "Invalid rndICC");
+        return NfcCommandStop;
+    }
+
+    memcpy(secure_messaging->Kicc, Kicc, sizeof(secure_messaging->Kicc));
+    memcpy(secure_messaging->SSC + 0, rnd_icc + 4, 4);
+    memcpy(secure_messaging->SSC + 4, rnd_ifd + 4, 4);
+
+    return ret;
+}
+
+NfcCommand passy_reader_select_file(PassyReader* passy_reader, uint16_t file_id) {
+    NfcCommand ret = NfcCommandContinue;
+
+    BitBuffer* tx_buffer = passy_reader->tx_buffer;
+    BitBuffer* rx_buffer = passy_reader->rx_buffer;
+    Iso14443_4bPoller* iso14443_4b_poller = passy_reader->iso14443_4b_poller;
+    Iso14443_4bError error;
+
+    uint8_t select_0101[] = {0x00, 0xa4, 0x02, 0x0c, 0x02, 0x00, 0x00};
+    select_0101[5] = (file_id >> 8) & 0xFF;
+    select_0101[6] = file_id & 0xFF;
+
+    secure_messaging_wrap_apdu(
+        passy_reader->secure_messaging, select_0101, sizeof(select_0101), tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+    error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4b_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // Check SW
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    if(length < 2) {
+        FURI_LOG_W(TAG, "Invalid response length %d", length);
+        return NfcCommandStop;
+    }
+    if(memcmp(data + length - 2, SW_success, sizeof(SW_success)) != 0) {
+        FURI_LOG_W(TAG, "Invalid SW %02x %02x", data[length - 2], data[length - 1]);
+        return NfcCommandStop;
+    }
+
+    secure_messaging_unwrap_rapdu(passy_reader->secure_messaging, rx_buffer);
+    passy_log_bitbuffer(TAG, "NFC response (decrypted)", rx_buffer);
+
+    return ret;
+}
+
+NfcCommand passy_reader_read_binary(
+    PassyReader* passy_reader,
+    uint8_t offset,
+    uint8_t Le,
+    uint8_t* output_buffer) {
+    NfcCommand ret = NfcCommandContinue;
+
+    BitBuffer* tx_buffer = passy_reader->tx_buffer;
+    BitBuffer* rx_buffer = passy_reader->rx_buffer;
+    Iso14443_4bPoller* iso14443_4b_poller = passy_reader->iso14443_4b_poller;
+    Iso14443_4bError error;
+
+    uint8_t read_binary[] = {0x00, 0xB0, 0x00, offset, Le};
+
+    secure_messaging_wrap_apdu(
+        passy_reader->secure_messaging, read_binary, sizeof(read_binary), tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC transmit", tx_buffer);
+    error = iso14443_4b_poller_send_block(iso14443_4b_poller, tx_buffer, rx_buffer);
+    if(error != Iso14443_4bErrorNone) {
+        FURI_LOG_W(TAG, "iso14443_4b_poller_send_block error %d", error);
+        return NfcCommandStop;
+    }
+    bit_buffer_reset(tx_buffer);
+
+    passy_log_bitbuffer(TAG, "NFC response", rx_buffer);
+
+    // Check SW
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    if(length < 2) {
+        FURI_LOG_W(TAG, "Invalid response length %d", length);
+        return NfcCommandStop;
+    }
+    if(memcmp(data + length - 2, SW_success, sizeof(SW_success)) != 0) {
+        FURI_LOG_W(TAG, "Invalid SW %02x %02x", data[length - 2], data[length - 1]);
+        return NfcCommandStop;
+    }
+
+    secure_messaging_unwrap_rapdu(passy_reader->secure_messaging, rx_buffer);
+    passy_log_bitbuffer(TAG, "NFC response (decrypted)", rx_buffer);
+
+    const uint8_t* decrypted_data = bit_buffer_get_data(rx_buffer);
+    memcpy(output_buffer, decrypted_data, Le);
+
+    return ret;
+}
+
+NfcCommand passy_reader_state_machine(Passy* passy, PassyReader* passy_reader) {
+    furi_assert(passy_reader);
+    NfcCommand ret = NfcCommandContinue;
+
+    do {
+        ret = passy_reader_select_application(passy_reader);
+        if(ret != NfcCommandContinue) {
+            view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderError);
+            break;
+        }
+        ret = passy_reader_get_challenge(passy_reader);
+        if(ret != NfcCommandContinue) {
+            view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderError);
+            break;
+        }
+        ret = passy_reader_authenticate(passy_reader);
+        if(ret != NfcCommandContinue) {
+            view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderError);
+            break;
+        }
+        FURI_LOG_I(TAG, "Mututal authentication success");
+        secure_messaging_calculate_session_keys(passy_reader->secure_messaging);
+        view_dispatcher_send_custom_event(
+            passy->view_dispatcher, PassyCustomEventReaderAuthenticated);
+
+        ret = passy_reader_select_file(passy_reader, 0x0101);
+        if(ret != NfcCommandContinue) {
+            view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderError);
+            break;
+        }
+
+        uint8_t header[4];
+        ret = passy_reader_read_binary(passy_reader, 0x00, 0x04, header);
+        if(ret != NfcCommandContinue) {
+            view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderError);
+
+            break;
+        }
+        uint8_t body_size = header[1];
+        uint8_t body_offset = 0x04;
+        do {
+            view_dispatcher_send_custom_event(
+                passy->view_dispatcher, PassyCustomEventReaderReading);
+            uint8_t chunk[0x20];
+            uint8_t Le = MIN(sizeof(chunk), (size_t)(body_size - body_offset));
+            FURI_LOG_I(TAG, "Reading %d bytes from offset %d", Le, body_offset);
+
+            ret = passy_reader_read_binary(passy_reader, body_offset, Le, chunk);
+            if(ret != NfcCommandContinue) {
+                view_dispatcher_send_custom_event(
+                    passy->view_dispatcher, PassyCustomEventReaderError);
+                break;
+            }
+            bit_buffer_append_bytes(passy_reader->DG1, chunk, sizeof(chunk));
+            body_offset += sizeof(chunk);
+        } while(body_offset < body_size);
+        const uint8_t* decrypted_data = bit_buffer_get_data(passy_reader->DG1) + 3;
+        FURI_LOG_I(TAG, "Decrypted data: %s", decrypted_data);
+
+        // Everything done
+        ret = NfcCommandStop;
+        view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderSuccess);
+    } while(false);
+
+    return ret;
+}
+
+NfcCommand passy_reader_poller_callback(NfcGenericEvent event, void* context) {
+    furi_assert(event.protocol == NfcProtocolIso14443_4b);
+    Passy* passy = context;
+    NfcCommand ret = NfcCommandContinue;
+
+    const Iso14443_4bPollerEvent* iso14443_4b_event = event.event_data;
+    Iso14443_4bPoller* iso14443_4b_poller = event.instance;
+
+    FURI_LOG_D(TAG, "iso14443_4b_event->type %i", iso14443_4b_event->type);
+
+    PassyReader* passy_reader = passy_reader_alloc(passy, iso14443_4b_poller);
+
+    if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeReady) {
+        view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventReaderDetected);
+        nfc_device_set_data(
+            passy->nfc_device, NfcProtocolIso14443_4b, nfc_poller_get_data(passy->poller));
+
+        ret = passy_reader_state_machine(passy, passy_reader);
+
+        furi_thread_set_current_priority(FuriThreadPriorityLowest);
+    } else if(iso14443_4b_event->type == Iso14443_4bPollerEventTypeError) {
+        Iso14443_4bPollerEventData* data = iso14443_4b_event->data;
+        Iso14443_4bError error = data->error;
+        FURI_LOG_W(TAG, "Iso14443_4bError %i", error);
+        switch(error) {
+        case Iso14443_4bErrorNone:
+            break;
+        case Iso14443_4bErrorNotPresent:
+            break;
+        case Iso14443_4bErrorProtocol:
+            ret = NfcCommandStop;
+            break;
+        case Iso14443_4bErrorTimeout:
+            break;
+        }
+    }
+
+    passy_reader_free(passy_reader);
+    return ret;
+}

+ 31 - 0
passy_reader.h

@@ -0,0 +1,31 @@
+#pragma once
+
+#include <lib/nfc/protocols/nfc_generic_event.h>
+#include <lib/nfc/protocols/iso14443_4b/iso14443_4b_poller.h>
+#include <lib/nfc/helpers/iso14443_crc.h>
+#include <mbedtls/des.h>
+
+#include "passy_i.h"
+#include "passy_common.h"
+#include "secure_messaging.h"
+
+#define PASSY_READER_MAX_BUFFER_SIZE 128
+
+NfcCommand passy_reader_poller_callback(NfcGenericEvent event, void* context);
+
+typedef struct {
+    Iso14443_4bPoller* iso14443_4b_poller;
+    BitBuffer* tx_buffer;
+    BitBuffer* rx_buffer;
+
+    BitBuffer* DG1;
+
+    SecureMessaging* secure_messaging;
+
+} PassyReader;
+
+PassyReader* passy_reader_alloc(Passy* passy, Iso14443_4bPoller* iso14443_4b_poller);
+
+void passy_reader_free(PassyReader* passy_reader);
+
+void passy_reader_mac(uint8_t* key, uint8_t* data, size_t data_length, uint8_t* mac);

+ 246 - 0
scenes/.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
+...
+

+ 30 - 0
scenes/passy_scene.c

@@ -0,0 +1,30 @@
+#include "passy_scene.h"
+
+// Generate scene on_enter handlers array
+#define ADD_SCENE(prefix, name, id) prefix##_scene_##name##_on_enter,
+void (*const passy_on_enter_handlers[])(void*) = {
+#include "passy_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 passy_on_event_handlers[])(void* context, SceneManagerEvent event) = {
+#include "passy_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 passy_on_exit_handlers[])(void* context) = {
+#include "passy_scene_config.h"
+};
+#undef ADD_SCENE
+
+// Initialize scene handlers configuration structure
+const SceneManagerHandlers passy_scene_handlers = {
+    .on_enter_handlers = passy_on_enter_handlers,
+    .on_event_handlers = passy_on_event_handlers,
+    .on_exit_handlers = passy_on_exit_handlers,
+    .scene_num = PassySceneNum,
+};

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

+ 7 - 0
scenes/passy_scene_config.h

@@ -0,0 +1,7 @@
+ADD_SCENE(passy, main_menu, MainMenu)
+ADD_SCENE(passy, read, Read)
+ADD_SCENE(passy, read_error, ReadError)
+ADD_SCENE(passy, read_success, ReadSuccess)
+ADD_SCENE(passy, passport_number_input, PassportNumberInput)
+ADD_SCENE(passy, dob_input, DoBInput)
+ADD_SCENE(passy, doe_input, DoEInput)

+ 52 - 0
scenes/passy_scene_dob_input.c

@@ -0,0 +1,52 @@
+#include "../passy_i.h"
+#include <gui/modules/validators.h>
+
+#define PASSY_APP_FILE_PREFIX "Passy"
+#define TAG                   "PassySceneDoBInput"
+
+void passy_scene_dob_input_text_input_callback(void* context) {
+    Passy* passy = context;
+
+    view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventTextInputDone);
+}
+
+void passy_scene_dob_input_on_enter(void* context) {
+    Passy* passy = context;
+
+    // Setup view
+    TextInput* text_input = passy->text_input;
+
+    // TODO: reload from saved data
+
+    text_input_set_header_text(text_input, "DoB: YYMMDD");
+    text_input_set_minimum_length(text_input, 6);
+    text_input_set_result_callback(
+        text_input,
+        passy_scene_dob_input_text_input_callback,
+        passy,
+        passy->text_store,
+        sizeof(passy->text_store),
+        false);
+
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewTextInput);
+}
+
+bool passy_scene_dob_input_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PassyCustomEventTextInputDone) {
+            strlcpy(passy->date_of_birth, passy->text_store, strlen(passy->text_store) + 1);
+            scene_manager_next_scene(passy->scene_manager, PassySceneDoEInput);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void passy_scene_dob_input_on_exit(void* context) {
+    Passy* passy = context;
+    memset(passy->text_store, 0, sizeof(passy->text_store));
+    text_input_reset(passy->text_input);
+}

+ 53 - 0
scenes/passy_scene_doe_input.c

@@ -0,0 +1,53 @@
+#include "../passy_i.h"
+#include <gui/modules/validators.h>
+
+#define PASSY_APP_FILE_PREFIX "Passy"
+
+void passy_scene_doe_input_text_input_callback(void* context) {
+    Passy* passy = context;
+
+    view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventTextInputDone);
+}
+
+void passy_scene_doe_input_on_enter(void* context) {
+    Passy* passy = context;
+
+    // Setup view
+    TextInput* text_input = passy->text_input;
+
+    // TODO: reload from saved data
+
+    text_input_set_header_text(text_input, "DoE: YYMMDD");
+    text_input_set_minimum_length(text_input, 6);
+    text_input_set_result_callback(
+        text_input,
+        passy_scene_doe_input_text_input_callback,
+        passy,
+        passy->text_store,
+        sizeof(passy->text_store),
+        false);
+
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewTextInput);
+}
+
+bool passy_scene_doe_input_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PassyCustomEventTextInputDone) {
+            strlcpy(passy->date_of_expiry, passy->text_store, strlen(passy->text_store) + 1);
+            scene_manager_next_scene(passy->scene_manager, PassySceneRead);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void passy_scene_doe_input_on_exit(void* context) {
+    Passy* passy = context;
+
+    // Clear view
+    memset(passy->text_store, 0, sizeof(passy->text_store));
+    text_input_reset(passy->text_input);
+}

+ 51 - 0
scenes/passy_scene_main_menu.c

@@ -0,0 +1,51 @@
+#include "../passy_i.h"
+
+#define TAG "SceneMainMenu"
+
+enum SubmenuIndex {
+    SubmenuIndexRead,
+};
+
+void passy_scene_main_menu_submenu_callback(void* context, uint32_t index) {
+    Passy* passy = context;
+    view_dispatcher_send_custom_event(passy->view_dispatcher, index);
+}
+
+void passy_scene_main_menu_on_enter(void* context) {
+    Passy* passy = context;
+    Submenu* submenu = passy->submenu;
+    submenu_reset(submenu);
+
+    submenu_add_item(
+        submenu, "Read", SubmenuIndexRead, passy_scene_main_menu_submenu_callback, passy);
+
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewMenu);
+}
+
+bool passy_scene_main_menu_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == SubmenuIndexRead) {
+            scene_manager_set_scene_state(
+                passy->scene_manager, PassySceneMainMenu, SubmenuIndexRead);
+
+            if(strlen(passy->passport_number) > 0 && strlen(passy->date_of_birth) > 0 &&
+               strlen(passy->date_of_expiry) > 0) {
+                scene_manager_next_scene(passy->scene_manager, PassySceneRead);
+            } else {
+                scene_manager_next_scene(passy->scene_manager, PassyScenePassportNumberInput);
+            }
+            consumed = true;
+        }
+    }
+
+    return consumed;
+}
+
+void passy_scene_main_menu_on_exit(void* context) {
+    Passy* passy = context;
+
+    submenu_reset(passy->submenu);
+}

+ 52 - 0
scenes/passy_scene_passport_number_input.c

@@ -0,0 +1,52 @@
+#include "../passy_i.h"
+#include <gui/modules/validators.h>
+
+#define PASSY_APP_FILE_PREFIX "Passy"
+
+void passy_scene_passport_number_input_text_input_callback(void* context) {
+    Passy* passy = context;
+
+    view_dispatcher_send_custom_event(passy->view_dispatcher, PassyCustomEventTextInputDone);
+}
+
+void passy_scene_passport_number_input_on_enter(void* context) {
+    Passy* passy = context;
+
+    // Setup view
+    TextInput* text_input = passy->text_input;
+
+    // TODO: reload from saved data
+
+    text_input_set_header_text(text_input, "Passport Number");
+    text_input_set_result_callback(
+        text_input,
+        passy_scene_passport_number_input_text_input_callback,
+        passy,
+        passy->text_store,
+        sizeof(passy->text_store),
+        false);
+
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewTextInput);
+}
+
+bool passy_scene_passport_number_input_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PassyCustomEventTextInputDone) {
+            strlcpy(passy->passport_number, passy->text_store, strlen(passy->text_store) + 1);
+            scene_manager_next_scene(passy->scene_manager, PassySceneDoBInput);
+            consumed = true;
+        }
+    }
+    return consumed;
+}
+
+void passy_scene_passport_number_input_on_exit(void* context) {
+    Passy* passy = context;
+
+    // Clear view
+    memset(passy->text_store, 0, sizeof(passy->text_store));
+    text_input_reset(passy->text_input);
+}

+ 65 - 0
scenes/passy_scene_read.c

@@ -0,0 +1,65 @@
+#include "../passy_i.h"
+#include "../passy_reader.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "PassySceneRead"
+
+void passy_scene_read_on_enter(void* context) {
+    Passy* passy = context;
+    dolphin_deed(DolphinDeedNfcRead);
+
+    // Setup view
+    Popup* popup = passy->popup;
+    popup_set_header(popup, "Reading", 68, 30, AlignLeft, AlignTop);
+    popup_set_icon(popup, 0, 3, &I_RFIDDolphinReceive_97x61);
+
+    passy->poller = nfc_poller_alloc(passy->nfc, NfcProtocolIso14443_4b);
+    nfc_poller_start(passy->poller, passy_reader_poller_callback, passy);
+
+    passy_blink_start(passy);
+
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewPopup);
+}
+
+bool passy_scene_read_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+    Popup* popup = passy->popup;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == PassyCustomEventReaderSuccess) {
+            scene_manager_next_scene(passy->scene_manager, PassySceneReadSuccess);
+            consumed = true;
+        } else if(event.event == PassyCustomEventReaderError) {
+            scene_manager_next_scene(passy->scene_manager, PassySceneReadError);
+            consumed = true;
+        } else if(event.event == PassyCustomEventReaderDetected) {
+            popup_set_header(popup, "Detected", 68, 30, AlignLeft, AlignTop);
+        } else if(event.event == PassyCustomEventReaderAuthenticated) {
+            popup_set_header(popup, "Authenticated", 68, 30, AlignLeft, AlignTop);
+        } else if(event.event == PassyCustomEventReaderReading) {
+            popup_set_header(popup, "Reading", 68, 30, AlignLeft, AlignTop);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(
+            passy->scene_manager, PassySceneMainMenu);
+        consumed = true;
+    }
+
+    return consumed;
+}
+
+void passy_scene_read_on_exit(void* context) {
+    Passy* passy = context;
+
+    if(passy->poller) {
+        nfc_poller_stop(passy->poller);
+        nfc_poller_free(passy->poller);
+        passy->poller = NULL;
+    }
+
+    // Clear view
+    popup_reset(passy->popup);
+
+    passy_blink_stop(passy);
+}

+ 65 - 0
scenes/passy_scene_read_error.c

@@ -0,0 +1,65 @@
+#include "../passy_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "PassySceneReadCardSuccess"
+
+void passy_scene_read_error_widget_callback(GuiButtonType result, InputType type, void* context) {
+    furi_assert(context);
+    Passy* passy = context;
+
+    if(type == InputTypeShort) {
+        view_dispatcher_send_custom_event(passy->view_dispatcher, result);
+    }
+}
+
+void passy_scene_read_error_on_enter(void* context) {
+    Passy* passy = context;
+    Widget* widget = passy->widget;
+
+    // Send notification
+    notification_message(passy->notifications, &sequence_error);
+    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", passy_scene_read_error_widget_callback, passy);
+
+    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(passy->view_dispatcher, PassyViewWidget);
+}
+
+bool passy_scene_read_error_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+        if(event.event == GuiButtonTypeLeft) {
+            consumed = scene_manager_previous_scene(passy->scene_manager);
+        }
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(
+            passy->scene_manager, PassySceneMainMenu);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void passy_scene_read_error_on_exit(void* context) {
+    Passy* passy = context;
+
+    // Clear view
+    widget_reset(passy->widget);
+}

+ 39 - 0
scenes/passy_scene_read_success.c

@@ -0,0 +1,39 @@
+#include "../passy_i.h"
+#include <dolphin/dolphin.h>
+
+#define TAG "PassySceneReadCardSuccess"
+
+void passy_scene_read_success_on_enter(void* context) {
+    Passy* passy = context;
+
+    dolphin_deed(DolphinDeedNfcReadSuccess);
+    notification_message(passy->notifications, &sequence_success);
+
+    furi_string_reset(passy->text_box_store);
+    FuriString* str = passy->text_box_store;
+    furi_string_cat_printf(str, "%s\n", bit_buffer_get_data(passy->DG1) + 3);
+
+    text_box_set_font(passy->text_box, TextBoxFontText);
+    text_box_set_text(passy->text_box, furi_string_get_cstr(passy->text_box_store));
+    view_dispatcher_switch_to_view(passy->view_dispatcher, PassyViewTextBox);
+}
+
+bool passy_scene_read_success_on_event(void* context, SceneManagerEvent event) {
+    Passy* passy = context;
+    bool consumed = false;
+
+    if(event.type == SceneManagerEventTypeCustom) {
+    } else if(event.type == SceneManagerEventTypeBack) {
+        scene_manager_search_and_switch_to_previous_scene(
+            passy->scene_manager, PassySceneMainMenu);
+        consumed = true;
+    }
+    return consumed;
+}
+
+void passy_scene_read_success_on_exit(void* context) {
+    Passy* passy = context;
+
+    // Clear view
+    text_box_reset(passy->text_box);
+}

+ 389 - 0
secure_messaging.c

@@ -0,0 +1,389 @@
+#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};
+
+void secure_messaging_adjust_parity(uint8_t key[16]) {
+    for(size_t i = 0; i < 16; i++) {
+        // Set the parity bit to 1 if the number of 1 bits is even
+        for(size_t j = 0; j < 8; j++) {
+            if((key[i] >> j) & 0x01) {
+                key[i] ^= 0x01;
+            }
+        }
+        key[i] ^= 0x01;
+    }
+}
+
+void secure_messaging_key_diversification(uint8_t input[20], uint8_t* output) {
+    uint8_t sha[20];
+    mbedtls_sha1_context ctx;
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, input, 20);
+    mbedtls_sha1_finish(&ctx, sha);
+
+    memcpy(output, sha, 16);
+    secure_messaging_adjust_parity(output);
+}
+
+SecureMessaging* secure_messaging_alloc(
+    uint8_t* passport_number,
+    uint8_t* date_of_birth,
+    uint8_t* date_of_expiry) {
+    SecureMessaging* secure_messaging = malloc(sizeof(SecureMessaging));
+    memset(secure_messaging, 0, sizeof(SecureMessaging));
+    mbedtls_sha1_context ctx;
+
+    memset(secure_messaging->rndIFD, 0x00, sizeof(secure_messaging->rndIFD));
+    memset(secure_messaging->Kifd, 0x00, sizeof(secure_messaging->Kifd));
+
+    uint8_t mrz[10 + 7 + 7];
+    memcpy(mrz, passport_number, 10);
+    memcpy(mrz + 10, date_of_birth, 7);
+    memcpy(mrz + 10 + 7, date_of_expiry, 7);
+    FURI_LOG_D(TAG, "secure_messaging_alloc mrz %s", mrz);
+
+    uint8_t sha[20];
+
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, mrz, sizeof(mrz));
+    mbedtls_sha1_finish(&ctx, sha);
+
+    passy_log_buffer(TAG, "secure_messaging_alloc sha", sha, sizeof(sha));
+
+    uint8_t D[20];
+    memset(D, 0, sizeof(D));
+    memcpy(D, sha, 16);
+    D[19] = 0x01;
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, D, sizeof(D));
+    mbedtls_sha1_finish(&ctx, sha);
+
+    memcpy(secure_messaging->KENC, sha, 16);
+    passy_log_buffer(
+        TAG, "secure_messaging_alloc KENC", secure_messaging->KENC, sizeof(secure_messaging->KENC));
+
+    // Adjust the parity bits
+    secure_messaging_adjust_parity(secure_messaging->KENC);
+    passy_log_buffer(
+        TAG, "secure_messaging_alloc KENC", secure_messaging->KENC, sizeof(secure_messaging->KENC));
+
+    D[19] = 0x02;
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, D, sizeof(D));
+    mbedtls_sha1_finish(&ctx, sha);
+
+    memcpy(secure_messaging->KMAC, sha, 16);
+    passy_log_buffer(
+        TAG, "secure_messaging_alloc KMAC", secure_messaging->KMAC, sizeof(secure_messaging->KMAC));
+
+    secure_messaging_adjust_parity(secure_messaging->KMAC);
+    passy_log_buffer(
+        TAG, "secure_messaging_alloc KMAC", secure_messaging->KMAC, sizeof(secure_messaging->KMAC));
+
+    mbedtls_sha1_free(&ctx);
+    return secure_messaging;
+}
+
+void secure_messaging_free(SecureMessaging* secure_messaging) {
+    furi_assert(secure_messaging);
+    // Nothing to free;
+    free(secure_messaging);
+}
+
+void secure_messaging_calculate_session_keys(SecureMessaging* secure_messaging) {
+    uint8_t Kseed[16];
+    for(size_t i = 0; i < sizeof(Kseed); i++) {
+        Kseed[i] = secure_messaging->Kifd[i] ^ secure_messaging->Kicc[i];
+    }
+    passy_log_buffer(TAG, "secure_messaging_calculate_session_keys Kseed", Kseed, sizeof(Kseed));
+    mbedtls_sha1_context ctx;
+
+    uint8_t D[20];
+    uint8_t sha[20];
+
+    memset(D, 0, sizeof(D));
+    memcpy(D, Kseed, sizeof(Kseed));
+    D[19] = 0x01;
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, D, sizeof(D));
+    mbedtls_sha1_finish(&ctx, sha);
+
+    memcpy(secure_messaging->KSenc, sha, 16);
+    secure_messaging_adjust_parity(secure_messaging->KSenc);
+    passy_log_buffer(
+        TAG,
+        "secure_messaging_calculate_session_keys KSenc",
+        secure_messaging->KSenc,
+        sizeof(secure_messaging->KSenc));
+
+    memset(D, 0, sizeof(D));
+    memcpy(D, Kseed, sizeof(Kseed));
+    D[19] = 0x02;
+    mbedtls_sha1_init(&ctx);
+    mbedtls_sha1_starts(&ctx);
+    mbedtls_sha1_update(&ctx, D, sizeof(D));
+    mbedtls_sha1_finish(&ctx, sha);
+    memcpy(secure_messaging->KSmac, sha, 16);
+    secure_messaging_adjust_parity(secure_messaging->KSmac);
+    passy_log_buffer(
+        TAG,
+        "secure_messaging_calculate_session_keys KSmac",
+        secure_messaging->KSmac,
+        sizeof(secure_messaging->KSmac));
+}
+
+void secure_messaging_increment_context(SecureMessaging* secure_messaging) {
+    uint8_t* context = secure_messaging->SSC;
+    size_t context_len = sizeof(secure_messaging->SSC);
+    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) {
+    furi_assert(secure_messaging);
+    secure_messaging_increment_context(secure_messaging);
+
+    uint8_t payload_length = 0;
+    bool has_le = false;
+    if(message_len == 5) { // APDU with no payload and Le
+        has_le = true;
+    } else {
+        payload_length = message[4];
+    }
+    if(has_le) {
+        FURI_LOG_I(TAG, "secure_messaging_wrap_apdu has_le %d", message[message_len - 1]);
+    }
+
+    uint8_t cmd_header[8];
+    memset(cmd_header, 0, sizeof(cmd_header));
+    memcpy(cmd_header, message, 4);
+    cmd_header[0] |= 0x0c;
+    cmd_header[4] = 0x80;
+
+    uint8_t D087[3 + 8];
+    if(payload_length > 0) {
+        uint8_t* payload = message + 5;
+        if(payload_length > 7) {
+            FURI_LOG_W(TAG, "secure_messaging_wrap_apdu payload length too large to handle");
+            return;
+        }
+        uint8_t padded_payload[8];
+        memset(padded_payload, 0, sizeof(padded_payload));
+        memcpy(padded_payload, payload, payload_length);
+        padded_payload[payload_length] = 0x80;
+        passy_log_buffer(
+            TAG,
+            "secure_messaging_wrap_apdu padded_payload",
+            padded_payload,
+            sizeof(padded_payload));
+
+        uint8_t encrypted_payload[8];
+        uint8_t iv[8];
+        memset(iv, 0, sizeof(iv));
+        mbedtls_des3_context ctx;
+        mbedtls_des3_init(&ctx);
+        mbedtls_des3_set2key_enc(&ctx, secure_messaging->KSenc);
+        mbedtls_des3_crypt_cbc(
+            &ctx,
+            MBEDTLS_DES_ENCRYPT,
+            sizeof(padded_payload),
+            iv,
+            padded_payload,
+            encrypted_payload);
+        mbedtls_des3_free(&ctx);
+
+        memset(D087, 0, sizeof(D087));
+        D087[0] = 0x87;
+        D087[1] = 1 + sizeof(encrypted_payload);
+        D087[2] = 0x01; // TODO: look into the meaning of this
+        memcpy(D087 + 3, encrypted_payload, sizeof(encrypted_payload));
+    }
+
+    uint8_t D097[3];
+    memset(D097, 0, sizeof(D097));
+    if(has_le) {
+        D097[0] = 0x97;
+        D097[1] = 0x01;
+        D097[2] = message[message_len - 1];
+    }
+
+    uint8_t M[8 + 3 + 8 /* + 2*/];
+    uint8_t M_index = 0;
+    memset(M, 0, sizeof(M));
+    memcpy(M, cmd_header, sizeof(cmd_header));
+    M_index += sizeof(cmd_header);
+
+    if(payload_length > 0) {
+        memcpy(M + M_index, D087, sizeof(D087));
+        M_index += sizeof(D087);
+    }
+
+    if(has_le) {
+        memcpy(M + M_index, D097, sizeof(D097));
+        M_index += sizeof(D097);
+    }
+    passy_log_buffer(TAG, "secure_messaging_wrap_apdu M", M, M_index);
+
+    uint8_t N[32];
+    uint8_t N_index = 0;
+    memset(N, 0, sizeof(N));
+    memcpy(N, secure_messaging->SSC, sizeof(secure_messaging->SSC));
+    N_index += sizeof(secure_messaging->SSC);
+    memcpy(N + N_index, M, M_index);
+    N_index += M_index;
+    N[N_index++] = 0x80;
+    // Align to 8 bytes
+    uint8_t block_count = (N_index + 7) / 8;
+    N_index = block_count * 8;
+    passy_log_buffer(TAG, "secure_messaging_wrap_apdu N", N, N_index);
+
+    uint8_t mac[8];
+    passy_mac(secure_messaging->KSmac, N, N_index, mac, true);
+    passy_log_buffer(TAG, "secure_messaging_wrap_apdu mac", mac, sizeof(mac));
+
+    uint8_t D08E[2 + 8];
+    memset(D08E, 0, sizeof(D08E));
+    D08E[0] = 0x8E;
+    D08E[1] = sizeof(mac);
+    memcpy(D08E + 2, mac, sizeof(mac));
+
+    bit_buffer_append_bytes(tx_buffer, cmd_header, 4);
+
+    uint8_t protected_payload_length = 0;
+    protected_payload_length += sizeof(D08E);
+
+    if(payload_length > 0) {
+        protected_payload_length += sizeof(D087);
+    }
+    if(has_le) {
+        protected_payload_length += sizeof(D097);
+    }
+
+    // Lc
+    bit_buffer_append_byte(tx_buffer, protected_payload_length);
+
+    if(payload_length > 0) {
+        bit_buffer_append_bytes(tx_buffer, D087, sizeof(D087));
+    }
+
+    if(has_le) {
+        bit_buffer_append_bytes(tx_buffer, D097, sizeof(D097));
+    }
+    bit_buffer_append_bytes(tx_buffer, D08E, sizeof(D08E));
+    bit_buffer_append_byte(tx_buffer, 0x00); // Le
+}
+
+void secure_messaging_unwrap_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer) {
+    secure_messaging_increment_context(secure_messaging);
+
+    size_t length = bit_buffer_get_size_bytes(rx_buffer);
+    const uint8_t* data = bit_buffer_get_data(rx_buffer);
+    uint8_t status_word[2];
+    uint8_t* mac = NULL;
+    uint8_t* encrypted = NULL;
+    uint8_t encrypted_len = 0;
+
+    // Look for mac
+    uint8_t i = 0;
+    do {
+        uint8_t type = data[i++];
+        uint8_t len = data[i++];
+        switch(type) {
+        case 0x87:
+            // Encrypted data always starts with a 0x01
+            encrypted = (uint8_t*)data + i + 1;
+            encrypted_len = len - 1;
+            break;
+        case 0x8E:
+            mac = (uint8_t*)data + i;
+            break;
+        case 0x99:
+            status_word[0] = data[i + 0];
+            status_word[1] = data[i + 1];
+            break;
+        default:
+            FURI_LOG_W(TAG, "Unknown type %02x", type);
+            break;
+        }
+        i += len;
+    } while(i < length - 2);
+
+    if(mac) {
+        uint8_t K[SECURE_MESSAGING_MAX_SIZE];
+        memset(K, 0, sizeof(K));
+        uint8_t K_index = 0;
+        memcpy(K, secure_messaging->SSC, sizeof(secure_messaging->SSC));
+        K_index += sizeof(secure_messaging->SSC);
+        if(encrypted) {
+            K[K_index++] = 0x87;
+            K[K_index++] = encrypted_len + 1;
+            K[K_index++] = 0x01;
+            memcpy(K + K_index, encrypted, encrypted_len);
+            K_index += encrypted_len;
+        }
+
+        // Assume the status word is always present
+        K[K_index++] = 0x99;
+        K[K_index++] = 0x02;
+        memcpy(K + K_index, status_word, 2);
+        K_index += 2;
+        K[K_index++] = 0x80;
+        // Align to 8 bytes
+        uint8_t block_count = (K_index + 7) / 8;
+        K_index = block_count * 8;
+        passy_log_buffer(TAG, "secure_messaging_unwrap_rapdu K", K, K_index);
+        uint8_t calculated_mac[8];
+        passy_mac(secure_messaging->KSmac, K, K_index, calculated_mac, true);
+        passy_log_buffer(
+            TAG,
+            "secure_messaging_unwrap_rapdu calculated_mac",
+            calculated_mac,
+            sizeof(calculated_mac));
+        if(memcmp(mac, calculated_mac, sizeof(calculated_mac)) != 0) {
+            FURI_LOG_W(TAG, "Invalid MAC");
+            return;
+        }
+    }
+
+    uint8_t decrypted[SECURE_MESSAGING_MAX_SIZE];
+    uint8_t decrypted_len = encrypted_len;
+    if(encrypted) {
+        if(encrypted_len > sizeof(decrypted)) {
+            FURI_LOG_W(TAG, "secure_messaging_unwrap_rapdu encrypted length too large to handle");
+            return;
+        }
+        uint8_t iv[8];
+        memset(iv, 0, sizeof(iv));
+        mbedtls_des3_context ctx;
+        mbedtls_des3_init(&ctx);
+        mbedtls_des3_set2key_dec(&ctx, secure_messaging->KSenc);
+        mbedtls_des3_crypt_cbc(&ctx, MBEDTLS_DES_DECRYPT, encrypted_len, iv, encrypted, decrypted);
+        mbedtls_des3_free(&ctx);
+
+        // Remove padding
+        do {
+        } while(decrypted[--decrypted_len] == 0 && decrypted_len > 0);
+
+        passy_log_buffer(TAG, "secure_messaging_unwrap_rapdu decrypted", decrypted, decrypted_len);
+    }
+
+    // Don't reset until after data has been decrypted
+    bit_buffer_reset(rx_buffer);
+    if(encrypted) {
+        bit_buffer_append_bytes(rx_buffer, decrypted, decrypted_len);
+    }
+
+    bit_buffer_append_bytes(rx_buffer, status_word, 2);
+}

+ 53 - 0
secure_messaging.h

@@ -0,0 +1,53 @@
+#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 <furi.h>
+#include <lib/toolbox/bit_buffer.h>
+
+#include "passy_common.h"
+
+#define SECURE_MESSAGING_MAX_SIZE 128
+
+typedef struct {
+    uint8_t passport_number[10];
+    uint8_t date_of_birth[7];
+    uint8_t date_of_expiry[7];
+
+    uint8_t KENC[16];
+    uint8_t KMAC[16];
+
+    uint8_t rndICC[8];
+    uint8_t rndIFD[8];
+
+    uint8_t Kifd[16];
+    uint8_t Kicc[16];
+
+    uint8_t KSenc[16];
+    uint8_t KSmac[16];
+    uint8_t SSC[8];
+
+} SecureMessaging;
+
+SecureMessaging* secure_messaging_alloc(
+    uint8_t* passport_number,
+    uint8_t* date_of_birth,
+    uint8_t* date_of_expiry);
+
+void secure_messaging_free(SecureMessaging* secure_messaging);
+
+void secure_messaging_calculate_session_keys(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_rapdu(SecureMessaging* secure_messaging, BitBuffer* rx_buffer);