---
title: 'eDO App 1.7.1 — audyt bezpieczeństwa i reverse engineering na WSA'
url: 'http://kvc.pl/analizy-techniczne-pl/edo_app-audyt-bezpieczenstwa'
markdown: 'http://kvc.pl/analizy-techniczne-pl/edo_app-audyt-bezpieczenstwa.md'
date: '2026-09-19'
description: 'Dwudniowa, niezależna analiza eDO App 1.7.1: Xamarin/.NET 9, ARM64 pod Houdini, XALZ/LZ4, ICU 70/72, obfuskacja, detekcja roota, SearchValues i HTTPS PWPW. Od FailFast ICU do stabilnego StartView na WSA z aktywnym rootem Magisk.'
---

# eDO App 1.7.1 — audyt bezpieczeństwa i reverse engineering na WSA

**Autor:** Marek Wesołowski (WESMAR) · [github.com/wesmar](https://github.com/wesmar) · [kvc.pl](https://kvc.pl)

> **W skrócie.** Artykuł obejmuje dwie części. Pierwsza to rozwiązanie problemu uruchomienia produkcyjnej wersji eDO App 1.7.1 (Xamarin/.NET 9, ARM) na WSA x86-64 przez translator Houdini: przyczyną awarii był splot biblioteki ICU, mostu native bridge, kodu wyciętego przez trimmer oraz optymalizacji wektorowych SIMD w bibliotece standardowej .NET. Druga część stanowi niezależny audyt bezpieczeństwa klienta: analiza statyczna assembly oraz weryfikacja dynamiczna na Xiaomi 13T Pro. Ustalenia potwierdzają brak lokalnego kontenera tożsamości, powiązanie sekretów z Android Keystore, implementację PACE i Chip Authentication na bibliotece Bouncy Castle oraz poprawną konfigurację certyfikatów. Główne uchybienie to brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego oraz brak filtracji wejścia z klawiatury sprzętowej.

Werdykt: ocena 7,3/10 — dobra (górny przedział)

**eDO App 1.7.1 działa na WSA 2407 z aktywnym MagiskSU, a sam audyt pokazuje klienta o dojrzałej architekturze bezpieczeństwa, wyraźnie dojrzalszej niż mObywatel.** Badanie objęło analizę statyczną trzech głównych assembly oraz częściową weryfikację dynamiczną na fizycznym Xiaomi 13T Pro. Mocne strony: brak lokalnego kontenera tożsamości, dane pobierane z karty, sekrety w Android Keystore wspieranym przez TEE, standardowe protokoły PACE i Chip Authentication na Bouncy Castle, aktywna flaga `FLAG_SECURE`, pinning certyfikatów oraz walidacja wejść QR i deep-link. Zidentyfikowane wady: brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego EDO-A-02 oraz brak filtrowania klawiatury sprzętowej w polach PIN i PUK EDO-Q-02. Ocena ważona architektury klienta wynosi **≈ 7,3/10 (ocena dobra, w górnym przedziale skali)** zgodnie z rozbiciem w sekcji 22. Weryfikacja ruchu telemetrii, atestacji klucza oraz integralności po stronie serwera wymaga uprawnień root i stanowi zakres kolejnego etapu. 

 **Status z 17–18 września 2026 r.** Środowisko WSA: proces `pl.pwpw.edohub` stabilny po zimnym starcie, przejście `RegulationsView → StartView` bez błędów krytycznych. Weryfikacja dynamiczna na fizycznym Xiaomi 13T Pro z systemem Android 16: wersja produkcyjna `1.7.1` z Google Play bez debugowania; blokada `FLAG_SECURE` potwierdzona czarnym zrzutem ekranu; magazyn kluczy wspierany w TEE bez układu StrongBox; uprawnienia standardowe. Badanie ruchu telemetrii, ramek APDU oraz atestacji klucza wymaga uprawnień root. 

 [ Pobierz narzędzie — patch-eDO-App.zip](http://kvc.pl/analizy-techniczne-pl/edo_app-audyt-bezpieczenstwa/patch-eDO-App.zip) [ Zobacz nagranie na YouTube](https://youtu.be/9MTmyHogsxw)Zawiera skrypty weryfikacyjne PowerShell, `apktool.jar`, definicje profili oraz `payload-manifest.json`. Skrypt `patch-eDO-App-1.7.1-WSA.ps1` sprawdza obecność podkatalogu `payload/` zdefiniowanego w manifeście; ze względów licencyjnych celowo go tam nie ma, a skrypt zatrzyma się z błędem braku plików wsadowych (zob. sekcja 21).

## 1. Zakres badania i granice wniosków

Badam własną instalację, własne WSA, własne konto i własny dowód. Celem jest interoperacyjność oraz niezależna ocena bezpieczeństwa klienta. Rozdzielam trzy poziomy pewności:

1. **Dowód statyczny**: obecny w APK, bibliotekach ELF i IL.
2. **Dowód wykonaniowy**: zaobserwowany w procesie, `logcat`, stosach, aktywnościach i ruchu TLS.
3. **Hipoteza audytowa**: trop wymagający jeszcze kontrolowanego testu.

APK badawczy podpisałem oddzielnym certyfikatem. Opisuję każdą modyfikację, aby późniejsze wyniki nie mieszały zachowania producenta z zachowaniem laboratorium. Podczas akceptacji regulaminu aplikacja wykonała ruch do serwera. Nie eksportowałem danych dokumentu, PIN-u ani innych danych osobowych.

## 2. Materiał i powtarzalność

| Element | Wartość |
|---|---|
| Pakiet | `pl.pwpw.edohub` |
| Wersja | `1.7.1` |
| Stos | Xamarin / Mono / .NET 9 |
| ABI | `arm64-v8a`, `armeabi-v7a`; brak x86/x86-64 |
| Oryginalny APK | 89 869 095 B |
| SHA-256 oryginału | `07768A1F0A07493D6A89A981E9603863787D04A750E04536FBB2E3CA8105B84B` |
| Działający APK badawczy | 112 204 867 B |
| SHA-256 APK badawczego | `B642FF8F5D8D3003F3B4585FCD7B73EA4BBA66AE1274B5F11EE2D072084D294A` |
| Assembly store ARM64 | 11 915 712 B |
| SHA-256 końcowego store | `1B546954849CE1943744242A9B108043F37F4653878AE3A955AC4D1ADD0CCBF3` |

Całość trzymam w `C:\Temp\eDO_App`, osobno od mObywatela. Nie odinstalowałem, nie nadpisałem ani nie podpisałem ponownie działającego mObywatela.

## 3. Stanowisko WSA

| Warstwa | Stan podczas testu |
|---|---|
| WSA | `2407.40000.4.0`, Android 13, host x86-64 |
| Translacja ARM | `libhoudini.so` |
| ADB | `127.0.0.1:58526` |
| Root | MagiskSU: `uid=0`, `u:r:magisk:s0` |
| SELinux | `Enforcing` |
| Zygisk | aktywny; eDO App działa z nim po końcowej konfiguracji |
| mObywatel | zainstalowany i nienaruszony |

W jednej z pośrednich konfiguracji problem z native bridge zbiegł się z włączonym Zygiskiem i dał komunikat `EM_AARCH64/EM_X86_64`. Była to błędna korelacja, nie warunek działania eDO App. Po końcowej konfiguracji WSA aplikacja działa z aktywnym Zygiskiem i aktywnym MagiskSU. Stan ten traktuję jako potwierdzony funkcjonalnie; gdy WSA zostanie ponownie uruchomiony, uzupełnię go jeszcze o bieżący zrzut właściwości systemowych.

## 4. Xamarin kontra Kotlin: dlaczego eDO to trudny klient dla WSA

Zanim wejdę w szczegóły, muszę wyjaśnić różnicę, która przesądza o całej trudności, a której w audycie mObywatela w ogóle nie było.

**mObywatel napisano w Kotlinie.** Kod kompiluje się do bajtkodu Dalvik i wykonuje go natywnie wirtualna maszyna Androida (ART). Na WSA ART działa jako proces x86-64, więc logika aplikacji chodzi natywnie; przez Houdini przechodzi jedynie kilka drobnych bibliotek natywnych. Dlatego dla WSA mObywatel jest „lekki".

**eDO App napisano w C# na .NET.** To rodowód Xamarina, dziś kontynuowany jako .NET MAUI (co widać w APK po obecności `Microsoft.Maui.Essentials`). Logika aplikacji to kod pośredni IL, który wykonuje **runtime Mono**, a Mono to duża, **natywna biblioteka ARM** (`libmonosgen-2.0.so`). W efekcie translator Houdini na WSA wykonuje **pełny silnik uruchomieniowy Mono wraz z biblioteką standardową .NET, warstwą ICU i kodem natywnym aplikacji**, a nie jedynie pojedyncze wywołania JNI. Stanowi to przyczynę problemów opisanych poniżej.

Kontekst technologiczny: **Microsoft wygasił „klasycznego" Xamarina 1 maja 2024 r.** i scalił go z ujednoliconym .NET pod postacią .NET MAUI. Powodem był koszt utrzymywania osobnego stosu mobilnego obok głównego .NET oraz chęć jednego modelu dla wszystkich platform. eDO App wykorzystuje platformę .NET 9 oraz MAUI, zachowując tę samą architekturę: kod zarządzany wykonywany przez natywne środowisko Mono. Na fizycznym procesorze ARM działa to bez narzutu, natomiast w procesie translacji ARM→x86-64 pod Houdini stanowi przypadek o najwyższym stopniu złożoności.

Aplikacja zawiera własne środowisko Mono oraz biblioteki natywne ARM, co wymusza translację pełnego stosu .NET pod emulatorem Houdini.

flowchart LR APK[eDO App ARM64] --> MONO[Mono / .NET 9] MONO --> BCL[System.Private.CoreLib] MONO --> GLOB[libSystem.Globalization.Native.so] BCL --> H[Houdini ARM64 → x86-64] GLOB --> H H --> WSA[Android 13 / WSA] 

Na tej granicy kolejno ujawniły się: ICU 70/72, host bridging, Scudo, bezwarunkowy `LoadICU`, reguły porządkowania tekstu, inicjalizacja MvvmCross, `SearchValues` i parser HTTP.

## 5. ICU 70 kontra ICU 72

WSA ma ICU 70, a eDO App oczekuje ICU 72. Houdini mostkuje wybrane biblioteki do hosta. Gdy ARM64 żądał symboli ICU 72, bridge szukał ich w hostowym ICU 70.

Próba dostarczenia czystych bibliotek gościa ARM64 ICU 72 zakończyła się niepowodzeniem: pliki danych były otwierane poprawnie, jednak funkcja `IcuRegistration::registerIcu()` powodowała błąd w alokatorze Scudo podczas zwalniania obiektów tymczasowych. Czterobajtowy skok w `libicuuc.so` omijał ten `SIGABRT`, ale wywołanie `ucol_open_72+56` nadal kończyło się błędem `SIGSEGV`.

To był potrzebny eksperyment z negatywnym zakończeniem: dowiódł, że problem nie leży już w ścieżce, uprawnieniach ani braku danych. Ścianą były reguły sortowania ICU wykonywane pod Houdini. Finalna metoda nie modyfikuje WSA i nie podmontowuje ICU do systemu.

## 6. Trimmer usunął strażnik globalizacji

Aplikację zbudowano z `InvariantGlobalization=false`. ILLink usunął gałąź `if (!Invariant)`. W `GlobalizationMode.Settings..cctor` wynik gettera był wyrzucany:

```text
call Settings.get_Invariant
pop
ldloca.s 0
call TryGetAppLocalIcuSwitchValue
...
call LoadICU
...
call Environment.FailFast
```

Przestawienie gettera na `true` nie pomagało, bo nikt nie używał wyniku. Skuteczny patch zatrzymał konstruktor przed `LoadICU`: w CoreLib offset `0x82100` zmieniłem z `0x12` (`ldloca.s`) na `0x2A` (`ret`). W LZ4 był to literał pod `0x9D4DEB`.

Po tej zmianie zniknął `Environment.FailFast: Unable to load required ICU Globalization data`.

## 7. Globalizacja bez niezaładowanego ICU

Po pominięciu `LoadICU` część wskaźników była pusta. `GlobalizationNative_GetSortHandle` wykonywało `blr` przez `ucol_open == 0`, więc proces skakał pod `pc=0`.

Najpierw zastąpiłem trzy punkty wejścia: `GetSortHandle`, `CompareString` i `GetSortKey`. Następnie objąłem poprawką wszystkie 36 eksportowanych funkcji globalizacji używanych przez runtime. Wyszukiwanie i porównania otrzymały bezpieczne procedury ARM64, a funkcje lokalizacyjne deterministyczne wartości. Dotyczy to m.in. `IndexOf`, `LastIndexOf`, `StartsWith`, `EndsWith`, `CompareString`, `GetLocaleName` i `IsPredefinedLocale`.

Końcowa `libSystem.Globalization.Native.so` ma 71 960 B:

```text
E14B0710A9BD310FA26F7B83426E71D4AC0AE06688FB574807B4A2EB800B99BF
```

To warstwa zgodności laboratorium, a nie pełny zamiennik językowy ICU.

## 8. Blokady w kodzie zarządzanym

| Miejsce | Objaw | Zmiana laboratoryjna |
|---|---|---|
| `_b3` / `ISmartAppService` | przedwczesne subskrypcje MvvmCross | pominięcie wadliwej części konstruktora |
| `AppStart.<NavigateToFirstViewModel>d__10` | `Root detected` | `IsRootedOrDangerous = false` |
| `RegulationsView.<ViewAppeared>d__17` | brak bezpiecznego keyguarda WSA | `IsDeviceSecurityHighEnough = true` |

Zmiany weszły do `Pwpw.Mobile.EdoHub.Core.dll`. Wpis #67 ponownie skompresowałem do dokładnie 742 032 B.

**Granica wniosku.** Lokalna możliwość wyłączenia kontroli roota i blokady ekranu dowodzi, że te decyzje są podejmowane po stronie klienta. Nie dowodzi póki co obejścia NFC, PIN-u, uwierzytelnienia ani podpisu.

## 9. Obfuskacja, która nie ukryła protokołu

Nazwy prywatnych klas i metod skrócono do `_bz`, `_b6`, `_b3`, `ac.g`, `s.e` i `b.d`. Zwiększyło to koszt ręcznej analizy, ale nie usunęło informacji potrzebnych do odtworzenia przepływu.

Pozostały czytelne:

- interfejsy, DTO, view-modele i część metod publicznych;
- tokeny metod i typów;
- maszyny stanów `IAsyncStateMachine` i `MoveNext`;
- zależności MvvmCross i System.Net.Http;
- literały endpointów, nagłówków i błędów;
- stosy łączące kod obfuskowany z `RegulationsViewModel`.

| Ślad | Wniosek |
|---|---|
| `_bz` rejestrowany jako `IEdoHubService` | rola klasy mimo zmienionej nazwy |
| `_bz/ac::g()`, token `0x060001F4` | maszyna stanu żądania HTTP |
| `_bz::j(...)`, token `0x060001B9` | miejsce pochłaniające wyjątek |
| `RegulationsViewModel.<AcceptRegulations>d__20.MoveNext` | wejście z przycisku regulaminu |
| `edoservices_012/acceptTerms` | względny endpoint |

Obfuskowany łańcuch `_bz+s.e → _bz+ac.g → _bz+b.d` prowadził wprost do maszyny stanu `AcceptRegulations`. Literały `X-Device`, `text/plain` oraz komunikat `probably no internet connection` nie były jawne: odzyskano je przez zdeszyfrowanie zaciemnionych łańcuchów (§9.2).

**EDO-P-01: obfuskacja nie ukrywa przepływu protokołu.** Zastosowano zmianę nazw, spłaszczenie przepływu sterowania oraz szyfrowanie literałów, jednak zachowane metadane .NET i odwracalny offline schemat szyfrowania łańcuchów pozwoliły na pełne odtworzenie logiki klienta, adresów endpointów oraz parametrów kryptograficznych. Fakt ten rzutuje na ocenę odporności aplikacji na inżynierię wsteczną.

XALZ/LZ4 nie jest obfuskacją. Po odczytaniu tabeli deskryptorów assembly można wyodrębnić, zanalizować po tokenach i wstawić ponownie.

### 9.1. Czym jest ta obfuskacja i na ile jest skuteczna

Obfuskatorem jest **PreEmptive Dotfuscator** (ślady w metadanych `Core.dll`). Zastosowano trzy techniki. Po pierwsze **przemianowanie symboli**: prywatne klasy i metody dostały krótkie nazwy `_bz`, `_b6`, `ac.g`, `s.e`, `b.d`. Po drugie **zaciemnienie przepływu sterowania**: właściwa maszyna stanów HTTP (`_bz/ac::g()`, token `0x060001F4`) jest rozbita na przełączniki i **fałszywe stany**, które utrudniają odtworzenie kolejności wykonania. Po trzecie **szyfrowanie literałów łańcuchowych**: łańcuchy przechowywane są jako zaszyfrowane tablice bajtów i odszyfrowywane w czasie wykonania (§9.2).

Charakterystyczne dla tego profilu jest zachowanie metadanych .NET przy jednoczesnym szyfrowaniu literałów. Jawne pozostały wszystkie interfejsy publiczne, DTO i view-modele, tokeny metod i typów oraz nazwy maszyn stanu async (`<AcceptRegulations>d__20`), a także zależności MvvmCross i `System.Net.Http`, które bezpośrednio ujawniają architekturę rozwiązania. Literały protokołu (endpoint `edoservices_012/acceptTerms`, nagłówek `X-Device`, `text/plain` oraz komunikaty błędów) były natomiast zaszyfrowane i wymagały odwrócenia szyfru (§9.2). Odtworzenie logiki sprowadziło się zatem do mapowania tokenów oraz zdeszyfrowania łańcuchów.

Deobfuskacja sprowadziła się więc do prześledzenia stosów i tokenów. Odzyskana mapa jest jednoznaczna:

| Zaciemniona nazwa | Token | Rzeczywista rola |
|---|---|---|
| `_bz/b::d()` | `0x060001BE` | `AcceptTermsOfService` |
| `_b6::r()` | `0x06000294` | zwraca `edoservices_012/acceptTerms` |
| `_bz::j(string, RequestBase, HttpMethod)` | `0x060001AC` | wspólny wrapper klienta HTTP |
| `_bz/ac::g()` | `0x060001F4` | maszyna stanów żądania HTTP |
| `_bz::j(Exception, string, HttpResponseMessage)` | `0x060001B9` | logger Sentry połykający szczegóły błędu |

W aplikacji wykorzystano komercyjny pakiet Dotfuscator w konfiguracji łączącej zmianę nazw symboli, spłaszczenie przepływu sterowania oraz szyfrowanie literałów łańcuchowych, przy zachowaniu metadanych .NET. Jest to konfiguracja istotnie mocniejsza od samej zmiany nazw, jednak odwracalny offline schemat szyfrowania łańcuchów oraz nienaruszone metadane sprawiają, że nie chroni ona architektury protokołu, adresów endpointów ani operacji kryptograficznych. Stanowi to rozwinięcie ustalenia EDO-P-01: zastosowany poziom zaciemnienia kodu podnosi próg analizy, lecz pozostaje nieadekwatny do wartości chronionego zasobu tożsamościowego.

### 9.2. Szyfrowanie literałów i jego odwrócenie

Wbrew pierwotnemu założeniu Dotfuscator **zaszyfrował literały łańcuchowe**. W `Core.dll` na 734 łańcuchy jawne przypada **2230 zaszyfrowanych**, w `Droid.dll` kolejne kilkaset. Zaszyfrowany łańcuch ma postać tablicy bajtów odszyfrowywanej w czasie wykonania przez wspólną metodę `_a3::b(string, int32, string)`.

Sam deszyfrator jest podręcznikowym przykładem zaciemnienia przepływu. Jego konstruktor statyczny to festiwal śmieciowej arytmetyki: mnożenie przez jeden, dodawanie i natychmiastowe odejmowanie tej samej stałej (`add 0x900000` / `sub 0x900000`), dzielenie przez jeden oraz `switch` na wartości zawsze prawdziwej. Instrukcje te nie zmieniają wyniku; służą wyłącznie zmyleniu dekompilatora i rozbiciu liniowego odczytu.

Sam algorytm szyfru jest natomiast słaby. Dla każdego znaku UTF-16 o bajtach (młodszy, starszy) tekst jawny odtwarza się jako `jawny = starszy XOR ((młodszy + 1) mod 256)`. Młodszy bajt każdego znaku pełni rolę strumienia klucza, rosnącego o dwa na znak, przez co **każdy szyfrogram niesie własny klucz**. W praktyce oznacza to, że argument `A_1` przekazywany do deszyfratora jest zbędny, a wszystkie łańcuchy da się odzyskać w pełni **offline, bez uruchamiania aplikacji**. Napisany na tej podstawie deszyfrator odtworzył komplet literałów.

Odzyskana zawartość pokazuje kluczowe parametry architektury, które mechanizm obfuskacji miał ukryć:

| Kategoria | Kluczowe zdeszyfrowane literały (`Core.dll` i `Droid.dll`) |
|---|---|
| **Usługi i nagłówki HTTP** | `https://www.edoapp.pl`, `https://edopost.pwpw.pl`, sklep Sigillum, `edoservices_012/acceptTerms`, `X-Device`, `text/plain`, `X-Token`, `X-CSRF-Token` |
| **Kryptografia i protokoły** | `PACE`, `ChipAuthentication`, `PassiveAuthentication`, `ActiveAuthentication`, `ECDSA`, `SHA384WITHECDSA`, `NONEwithECDSA`, `AES/CBC/PKCS7Padding` |
| **Klucze magazynu danych** | `EDOAPP_CAN`, `EDOAPP_MRZ`, `EDOAPP_SESSION_*`, `EDOAPP_AUTHENTICATOR_ID`, `EDOAPP_AUTHENTICATOR_PRIVATE_KEY_BASE64`, `CERT_ID` |
| **Wbudowany materiał krypto** | Osadzony klucz publiczny RSA (`-----BEGIN PUBLIC KEY-----`), blok licencyjny (`appId=pl.pwpw.*`, `expiryDate=2023-06-10`, podpis ECDSA) |
| **Detekcja środowiska i root** | `/sbin/su`, `/system/bin/su`, `/data/local/su`, `magisk`, `test-keys`, `keyguard`, `de.robv.android.xposed.installer`, `com.topjohnwu.magisk`, `RootCloak` |
| **Obsługa interfejsu NFC/APDU** | `Extended Length APDU not supported`, `Error (IsoDep is null!)`, `Tag not supported`, `nfc_dowod`, `nfc_passport`, `PinBlockedException` |
| **Przepływ podpisu i PDF** | `Plik do podpisu`, `Plik podpisany e-dowodem`, `Podpis Sigillum`, `Podpis osobisty e-dowodem`, `application/pdf` |

Rozwiń pełny zrzut zdeszyfrowanych literałów Droid.dll (348 ciągów w responsywnym układzie wielokolumnowym)

1. `This device doesn't support Camera2 API.`
2. `Camera2VideoFragment`
3. `Couldn't find any suitable video size`
4. `No suitable video size`
5. `No suitable preview size`
6. `No suitable preview size - even rotated!`
7. `Couldn't find any suitable preview size`
8. `CameraBackground`
9. `Time out waiting to lock camera opening.`
10. `Cannot access the camera.`
11. `dialog`
12. `Interrupted while trying to lock camera opening.`
13. `camera`
14. `Interrupted while trying to lock camera closing.`
15. `CameraPreview`
16. `video-`
17. `yyMMdd-hhmmss`
18. `.mp4`
19. `layout_inflater`
20. `/3`
21. `android.resource://`
22. `drawable`
23. `string`
24. `message`
25. `id`
26. `android`
27. `_description`
28. `pin_prompt`
29. `sign_success`
30. `info_dialog`
31. `#9697A1`
32. `#E42552`
33. `Resource not found (id = 0)`
34. `Resource name was empty or null`
35. `Resource container type was empty or null`
36. `Resource id cannot be null`
37. `navigated_to_govpl_getedo`
38. `card_data`
39. `data_display_get_edo_text`
40. `data_display_get_edo_linked_text`
41. `dialog_open`
42. `dialog_description`
43. `title`
44. `_display_name`
45. `mime_type`
46. `_size`
47. `Plik do podpisu`
48. `Plik podpisany e-dowodem`
49. `download`
50. `application/pdf`
51. `android.intent.action.SEND`
52. `android.intent.extra.STREAM`
53. `android.intent.action.VIEW_DOWNLOADS`
54. `android.resource`
55. `.pdf`
56. `- podpisano eDO App`
57. `_display_name=?`
58. `FACETEC INITIALIZATION PROBLEM!`
59. `Invalid processor initialization`
60. `keys are empty!`
61. `null`
62. `dDWqln4osRAo8vr1a8OzfPAldmI3Gwki`
63. `-----BEGIN PUBLIC KEY-----MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv2M11lmIlLullW795/a8d9dcXT7U/ia+qHVsQGvzSDKmctwtMrNPrwCqSkqx23tQqnjKL5TxZxDcl82uBUAwNtfF/R9JdTPOrnw45j314u0VsoWKJRI9IniRfOMu2rDHILdafbuOwylr7th21vz76MQVDd+CJC5uBGSxm5OxZNNwnXkKvfbLqGBXdVbpIiCkj/H0dm85JPnQtZGCFNYNTJKmHNuYddJnN32hPFd0Ec/HTik3D23gwhw6RWvcjhEcgDPjdI1X1Xg6nYzbe78gy5BLe+z6zzhBBIeAn10zLRjxYRP9I7FBHD9DkKoYAY52//YFc1kPnvubdTEDGUHy2wIDAQAB-----END PUBLIC KEY-----`
64. `appId      = pl.pwpw.*expiryDate = 2023-06-10key        = 003044022038003c16420f54489095409a4a96bf303dc11a6cc17069427bd5d7fe3f637af302202d1df9521ecb7e556ebc7a0e1eab5e50b9d9f504b47d3aaab16ebc7128e47868`
65. `authorizationMissingHelp_title`
66. `authorizationMissingHelp_message`
67. `inactiveCertificateHelp_title`
68. `inactiveAuthenticationHelp_message`
69. `inactiveAuthorizationHelp_message`
70. `android.nfc.extra.ADAPTER_STATE`
71. `NFCAdapterStateChanged:`
72. `Tag not supported`
73. `Error (IsoDep is null!)`
74. `Not connected, cannot transmit/receive`
75. `nfc`
76. `NFCAdapter was null`
77. `android.permission.NFC`
78. `Android API below KitKAt is too low!`
79. `NFC not available`
80. `presence`
81. `rate_dialog_opened`
82. `rate_dialog_navigated_to_store`
83. `market://details?id=`
84. `com.android.vending`
85. `android.intent.action.VIEW`
86. `https://play.google.com/store/apps/details?id=com.pl.pwpw.edohub`
87. `/system/app/Superuser.apk`
88. `/sbin/su`
89. `/system/bin/su`
90. `/system/bin/.ext/su`
91. `/system/xbin/su`
92. `/data/local/xbin/su`
93. `/data/local/bin/su`
94. `/system/usr/we-need-root/su`
95. `/system/sd/xbin/su`
96. `/system/bin/failsafe/su`
97. `/data/local/su`
98. `/data/su`
99. `/dev/su`
100. `/cache/su`
101. `/su/bin/su`
102. `PATH`
103. `supersu`
104. `superuser`
105. `root_detected_processname`
106. `magisk_detected_fullpath`
107. `su`
108. `root_detected_fullpath`
109. `root_detected_path`
110. `magisk`
111. `test-keys`
112. `root_detected_emulator`
113. `root_detected_debuggable`
114. `root_detected_debugger`
115. `root_detected_dangerous`
116. `de.robv.android.xposed.installer`
117. `com.saurik.substrate`
118. `com.android.vending.billing.InAppBillingService.LUCK`
119. `com.android.vending.billing.InAppBillingService.CLON`
120. `com.android.vending.billing.InAppBillingService.COIN`
121. `com.devadvance.rootcloak`
122. `com.devadvance.rootcloakplus`
123. `com.zachspong.temprootremovejb`
124. `com.amphoras.hidemyroot`
125. `com.amphoras.hidemyrootadfree`
126. `com.formyhm.hiderootPremium`
127. `com.formyhm.hideroot`
128. `com.noshufou.android.su`
129. `com.noshufou.android.su.elite`
130. `eu.chainfire.supersu`
131. `com.koushikdutta.superuser`
132. `com.thirdparty.superuser`
133. `com.yellowes.su`
134. `com.topjohnwu.magisk`
135. `com.kingroot.kinguser`
136. `com.kingo.root`
137. `com.smedialink.oneclickroot`
138. `com.zhiqupk.root.global`
139. `com.alephzain.framaroot`
140. `input_method`
141. `android.permission.POST_NOTIFICATIONS`
142. `connectivity`
143. `android.settings.NFC_SETTINGS`
144. `EXIT`
145. `minimizeApp`
146. `android_id`
147. `keyguard`
148. `notification`
149. `android.settings.APP_NOTIFICATION_SETTINGS`
150. `android.provider.extra.APP_PACKAGE`
151. `Version`
152. `Device`
153. `MainLog`
154. `Extended Length APDU not supported`
155. `ID read successfully`
156. `ID_read_successfully`
157. `Cancel`
158. `OK`
159. `device`
160. `android.hardware.camera`
161. `off`
162. `torch`
163. `rate`
164. `Brak danych`
165. `[on|off]`
166. `Prints usage information. If specified all other arguments (except 'v') are ignored`
167. `Prints version and copyright information`
168. `verbose`
169. `Prints information about the decoded codestream`
170. `on`
171. `pfile`
172. `<filename>`
173. `Loads the arguments from the specified file. Arguments that are specified on the command line override the ones from the file.The arguments file is a simple text file with one argument per line of the following form:  <argument name>=<argument value>If the argument is of boolean type (i.e. its presence turns a feature on), then the 'on' value turns it on, while the 'off' value turns it off. The argument name does not include the '-' or '+' character. Long lines can be broken into several lines by terminating them with '\'. Lines starting with '#' are considered as comments. This option is not recursive: any 'pfile' argument appearing in the file is ignored.`
174. `res`
175. `<resolution level index>`
176. `The resolution level at which to reconstruct the image  (0 means the lowest available resolution whereas the maximum resolution level corresponds to the original image resolution). If the given index is greater than the number of available resolution levels of the compressed image, the image is reconstructed at its highest resolution (among all tile-components). Note that this option affects only the inverse wavelet transform and not the number  of bytes read by the codestream parser: this number of bytes depends only on options '-nbytes' or '-rate'.`
177. `<filename or url>`
178. `The file containing the JPEG 2000 compressed data. This can be either a JPEG 2000 codestream or a JP2 file containing a JPEG 2000 codestream. In the latter case the first codestream in the file will be decoded. If an URL is specified (e.g., http://...) the data will be downloaded and cached in memory before decoding. This is intended for easy use in applets, but it is not a very efficient way of decoding network served data.`
179. `This is the name of the file to which the decompressed image is written. If no output filename is given, the image is displayed on the screen. Output file format is PGX by default. If the extension is '.pgm' then a PGM file is written as output, however this is only permitted if the component bitdepth does not exceed 8. If the extension is '.ppm' then a PPM file is written, however this is only permitted if there are 3 components and none of them has a bitdepth of more than 8. If there is more than 1 component, suffices '-1', '-2', '-3', ... are added to the file name, just before the extension, except for PPM files where all three components are written to the same file.`
180. `<decoding rate in bpp>`
181. `Specifies the decoding rate in bits per pixel (bpp) where the number of pixels is related to the image's original size (Note: this number is not affected by the '-res' option). If it is equalto -1, the whole codestream is decoded. The codestream is either parsed (default) or truncated depending the command line option '-parsing'. To specify the decoding rate in bytes, use '-nbytes' options instead.`
182. `-1`
183. `nbytes`
184. `<decoding rate in bytes>`
185. `Specifies the decoding rate in bytes. The codestream is either parsed (default) or truncated depending the command line option '-parsing'. To specify the decoding rate in bits per pixel, use '-rate' options instead.`
186. `parsing`
187. `Enable or not the parsing mode when decoding rate is specified ('-nbytes' or '-rate' options). If it is false, the codestream is decoded as if it were truncated to the given rate. If it is true, the decoder creates, truncates and decodes a virtual layer progressive codestream with the same truncation points in each code-block.`
188. `ncb_quit`
189. `<max number of code blocks>`
190. `Use the ncb and lbody quit conditions. If state information is found for more code blocks than is indicated with this option, the decoder will decode using only information found before that point. Using this otion implies that the 'rate' or 'nbyte' parameter is used to indicate the lbody parameter which is the number of packet body bytes the decoder will decode.`
191. `l_quit`
192. `<max number of layers>`
193. `Specifies the maximum number of layers to decode for any code-block`
194. `m_quit`
195. `<max number of bit planes>`
196. `Specifies the maximum number of bit planes to decode for any code-block`
197. `poc_quit`
198. `Specifies the whether the decoder should only decode code-blocks included in the first progression order.`
199. `one_tp`
200. `Specifies whether the decoder should only decode the first tile part of each tile.`
201. `comp_transf`
202. `Specifies whether the component transform indicated in the codestream should be used.`
203. `debug`
204. `Print debugging messages when an error is encountered.`
205. `cdstr_info`
206. `Display information about the codestream. This information is: - Marker segments value in main and tile-part headers,- Tile-part length and position within the code-stream.`
207. `nocolorspace`
208. `Ignore any colorspace information in the image.`
209. `colorspace_debug`
210. `Print debugging messages when an error is encountered in the colorspace module.`
211. `MSWiA`
212. `Polska Wytwórnia Papierów Wartościowych S.A.`
213. `Podpis Sigillum`
214. `Podpis osobisty e-dowodem`
215. `Inny podpis`
216. `Error stopping dialog`
217. `#4C5358`
218. `Error getting session token`
219. `edoapp_session_`
220. `biometry_match`
221. `Biometry result`
222. `Session interrupted`
223. `EdoHubService session expired:`
224. `or request details are null:`
225. `EdoHubService biometry request details are null`
226. `VerifyIdWithEnrollmentAndMatch response`
227. `Failed`
228. `OnAppOpenAttribution:`
229. `OnAttributionFailure =`
230. `OnInstallConversionFailure =`
231. `OnConversionDataSuccess:`
232. `Timestamp:`
233. `https://e7d89e5a50a63199c58202068150b7a3@diagnostics.edoapp.pl/2`
234. `[{Level}] {Message:l{NewLine:l}{Exception:l}`
235. `screen_view`
236. `firebase_screen_class`
237. `TEST`
238. `gplay_services_availibility`
239. `reason`
240. `unavailable`
241. `ok`
242. `<a href="`
243. `">`
244. `</a>`
245. `BuildNumber`
246. `ID`
247. `RequestDetails`
248. `Data`
249. `edo`
250. `error_message`
251. `(...)`
252. `base64,`
253. `serialNumber`
254. `certSerialNumber`
255. `dataRequest_`
256. `ChangingSignatureTypeMode`
257. `android.nfc.action.ADAPTER_STATE_CHANGED`
258. `{0}`
259. `{1}`
260. `#DD3345`
261. `_scroll`
262. `_info_label`
263. `#1E1E1E`
264. `Operations`
265. `ShowBuyCertificateButton`
266. `Certificates`
267. `ShouldCanBeRemembered`
268. `Can`
269. `TimeLeft`
270. `nfc_passport`
271. `nfc_dowod`
272. `nfc_new`
273. `POL`
274. `error on SetElementsVisibility:`
275. `<a href="https://www.edoapp.pl">www.edoapp.pl</a>`
276. `#F0F8FE`
277. `#FFFFFF`
278. `#DDE3EF`
279. `#282E6F`
280. `#000000`
281. `#ffffff`
282. `ac_esc`
283. `eb71b779-1756-49e8-ab9c-3dcca09b87b5`
284. `RecordingCounter`
285. `BottomText`
286. `IsFaceDetected`
287. `vm`
288. `android.permission.CAMERA`
289. `android.permission.RECORD_AUDIO`
290. `FaceDetector gms is not operational!`
291. `https://www.gov.pl/web/gov/uzyskaj-dowod-osobisty?utm_source=edoapp&utm_medium=app&utm_campaign=reflink`
292. `<a href=`
293. `<a href="http://e-dowod.gov.pl">e-dowod.gov.pl</a>.`
294. `<a href="https://www.edoapp.pl/centrum-pomocy-i-kontaktu">https://www.edoapp.pl/centrum-pomocy-i-kontaktu</a>.`
295. `MrzInfo`
296. `pl`
297. `Akceptuję <a href="`
298. `">regulamin</a>. Administratorem danych osobowych przetwarzanych w związku z korzystaniem z eDO App jest Polska Wytwórnia Papierów Wartościowych S.A. z siedzibą w Warszawie. Więcej informacji dostępnych jest <a href="`
299. `">tutaj</a>.`
300. `I accept <a href="`
301. `">terms of service</a>. The administrator of personal data processed in connection with the use of the eDO App is Polska Wytwórnia Papierów Wartościowych S.A. which its registered office in Warsaw. More information is available <a href="`
302. `">here</a>.`
303. `binding_error`
304. `exception`
305. `camera_not_found`
306. `camera_init_error`
307. `permission_missing`
308. `process_camera_provider_instance_null`
309. `Error starting camera`
310. `INTENT_EXTRA_RESULT_KEY`
311. `<a href = "https://www.gov.pl/web/gov/uzyskaj-dowod-osobisty?utm_source=edoapp&utm_medium=app&utm_campaign=reflink">`
312. `</a> !`
313. `change_environment`
314. `change_sigillum_environment`
315. `change_authenticator_environment`
316. `Obecnie wybrane:`
317. `Włączone`
318. `Wyłączone`
319. `MenuPositions`
320. `UseExtendedApdu`
321. `_validation`
322. `_error`
323. `_info`
324. `FileName`
325. `Splash EXIT`
326. `sign`
327. `android.intent.category.DEFAULT`
328. `shortcut`
329. `sign_document`
330. `#E6E6E6`
331. `Skaner kodu QR wymaga aparatu do działania.`
332. `en`
333. `Code received from intent`
334. `code`
335. `operation_id`
336. `AppUpdateRequired`
337. `https://play.google.com/store/apps/details?id=`
338. `<a href="https://www.gov.pl/web/gov/uslugi-dla-obywatela/">`
339. `</a>.`
340. `Opublikowaliśmy nowy regulamin. Aktualną wersję regulaminu zawsze znajdziesz na naszej stronie <a href="`
341. `We have published new terms of service. You can always find the current version of the regulations on our website <a href="`
342. `pl.pwpw.edowod.AppSettings`
343. `news`
344. `content`
345. `PESEL`
346. `Gest`
347. `Kalibracja`
348. `Size cannot be negative.`

Wniosek jest taki, że szyfrowanie literałów podnosi próg analizy pobieżnej, lecz przy samoopisującym się kluczu i zachowanych metadanych nie chroni ani architektury protokołu, ani parametrów kryptograficznych, ani osadzonego materiału klucza.

## 10. Pierwszy skrót w `SearchValues` i jego koszt

`CompareInfo..cctor` tworzył `SearchValues<char>` z prawidłowego, stałego zbioru ASCII. Pod Houdini `TryGetSingleRange<char>` rzucało `IndexOutOfRangeException`.

Pierwsze obejście zastąpiło fabryki `SearchValues.Create` obiektem `EmptySearchValues<T>`. Aplikacja przeszła do ekranu regulaminu, co jednak naruszyło semantykę parserów w bibliotece standardowej BCL. Po kliknięciu przycisku walidator nagłówka wywoływał:

```csharp
int invalidIndex = input.IndexOfAnyExcept(s_tokenChars);
```

Pusty zbiór zwracał `0`, więc `X-Device` był uznawany za nieprawidłowy. Doraźne `EmptySearchValues<T>.IndexOfAnyExcept => -1` przepuściło nazwę, ale parser `text/plain` nadal nie znajdował ukośnika. Użytkownik widział „brak Internetu”, mimo że Internet działał.

## 11. Jak zdjąłem fałszywy komunikat sieciowy

Warstwa aplikacji przechwytywała właściwy wyjątek i zastępowała go `probably no internet connection`. Zrobiłem dwie tymczasowe kompilacje diagnostyczne.

W `_bz::j(Exception,string,HttpResponseMessage)`, token `0x060001B9`, IL `0x1B9`, offset `0x24F91`, zmieniłem:

```text
02 28 55 01 00 0A 26 2A
```

na `ldarg.0; throw`:

```text
02 7A 00 00 00 00 00 00
```

To odsłoniło wrapper. Następnie w `_bz/ac::g()`, token `0x060001F4`, IL `0x1DA`, offset `0x2FDC2`, rzuciłem zachowany wyjątek wewnętrzny. `logcat` podał dokładną przyczynę:

```text
System.FormatException: net_http_headers_invalid_value, text/plain
 at System.Net.Http.Headers.MediaTypeHeaderValue.CheckMediaTypeFormat
 at System.Net.Http.StringContent..ctor
 at Xamarin.Android.Net.AndroidMessageHandler.<DoProcessRequest>d__140.MoveNext
 at _bz+s.e
 at _bz+ac.g
 at RegulationsViewModel.<AcceptRegulations>d__20.MoveNext
```

Obie sondy zostały wycofane po diagnozie i nie występują w finalnym APK.

## 12. Sieć i TLS działały

Proces łączył się z `185.31.88.82:8443`. TLS 1.2 dochodził do skutku. Serwer przedstawiał certyfikat `*.edoapp.pl`, PWPW S.A., ważny 24.10.2025–24.10.2026. Względny endpoint to `edoservices_012/acceptTerms`.

Awaria następowała wcześniej, przy tworzeniu `StringContent(..., "text/plain")`. Komunikat o braku Internetu był błędnym mapowaniem wyjątku parsera.

Potwierdziłem transport TLS i certyfikat serwera. Certificate pinning, odporność na kontrolowany MitM i pełna polityka sieciowa pozostają do osobnego testu.

## 13. Końcowy fix `SearchValues`

Zamiast omijać kolejne walidacje, przywróciłem prawdziwe zbiory znaków i wyłączyłem tylko dwie wadliwe optymalizacje.

| Token | Metoda | Zmiana |
|---|---|---|
| `0x060027FA` | `SearchValues.Create(ReadOnlySpan<byte>)` | oryginalny IL, 157 B |
| `0x060027FB` | `SearchValues.Create(ReadOnlySpan<char>)` | oryginalny IL, 299 B |
| `0x0600280E` | `EmptySearchValues<T>.IndexOfAnyExcept` | oryginalne zachowanie, 13 B |
| `0x060027A1` | `get_IsVectorizationSupported` | `ldc.i4.0; ret` pod `0x8D631` |
| `0x060027FD` | `TryGetSingleRange<T>` | `false; ret` po zapisaniu `min/max`, `0x8EA89` |

`TryGetSingleRange` prawidłowo obliczał `min` i `max`, a padał dopiero w fazie mapy `Span<bool>` z `localloc`. Po wcześniejszym zwrocie fabryka wybiera skalarną implementację bitmapową lub probabilistyczną z prawdziwym zbiorem.

flowchart TD A[SearchValues.Create: prawdziwy zbiór] --> B[oblicz min/max] B --> C[return false przed localloc] C --> D[AdvSimd = false] D --> E[skalarna implementacja BCL] E --> F[X-Device poprawny] E --> G[text/plain poprawny] 

Nieosiągalny kod po `ret` wypełniłem `nop`, dzięki czemu CoreLib skompresował się do 1 066 866 B. Rekord XALZ miał 1 066 878 B i zmieścił się w pierwotnym slocie 1 066 975 B. Round-trip dał SHA-256:

```text
5A1B8E868BC219C864E66995C498FBC6FAA29E444D9184050F365266E0DF01B7
```

## 14. Odrzucone hipotezy i ślepe odnogi

W dwudniowym reverse engineeringu ważne są również próby, które nie weszły do rozwiązania. Każda z nich rozstrzygnęła konkretną hipotezę.

| Próba | Co rzeczywiście wykazała | Dlaczego nie jest rozwiązaniem końcowym |
|---|---|---|
| `Settings.get_Invariant = true` | getter w assembly da się zmienić | jego wynik jest `pop`-owany; trimmer usunął warunek |
| `System.Globalization.Invariant=true` w `libarc.bin.so` | runtimeconfig można zmodyfikować bez zmiany rozmiaru payloadu | wycięty kod nie odczytuje ustawienia w krytycznym miejscu |
| `DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1` / `debug.mono.env` | ścieżka konfiguracji Mono istnieje | dla niedebugowalnej aplikacji nie odtwarza usuniętej gałęzi |
| prywatne nazwy `libicuux.so`, `libicuixxn.so` | można ominąć listę host bridging Houdini | po dołączeniu prywatnych `libcxx.so` i `libbasx.so` proces dochodził do `ucol_open_72+56`, gdzie nadal padał |
| bezpośredni loader `icudt72l.dat` | dane ICU 72 były mapowane poprawnie | błąd przesunął się do reguł porządkowania tekstu; nie był już błędem odnalezienia pliku |
| APEX z ICU 70 i 72 + bind mount biblioteki vendor ARM | proces widział oba zestawy danych | ten sam `ucol_open_72+56` kończył się `SIGSEGV` |
| czterobajtowy skok `b 0x1b1af8` w `libicuuc.so` | omijał destruktory wywołujące Scudo na błędnym ABI | usuwał jeden `SIGABRT`, ale nie naprawiał reguł porządkowania tekstu ICU |
| wcześniejsza interpretacja problemu z Zygiskiem | komunikat `EM_AARCH64/EM_X86_64` pojawił się w konfiguracji pośredniej | obalona: końcowa konfiguracja działa z aktywnym Zygiskiem |
| `SearchValues.Create -> EmptySearchValues` | aplikacja przechodziła inicjalizację `CompareInfo` i dochodziła do regulaminu | globalnie fałszował znaczenie zbiorów znaków |
| `EmptySearchValues.IndexOfAnyExcept -> -1` | przepuszczał nazwę `X-Device` | następny parser odrzucał `text/plain` |
| patch kontrolki „Internet jest dostępny” | mógł zmienić komunikat w UI | transport działał; przyczyna leżała w lokalnym parserze BCL |

Najważniejszy zwrot nastąpił po porzuceniu łatania kolejnych objawów. Skoro `X-Device` i `text/plain` psuły się w dwóch różnych walidatorach, wspólną przyczyną musiał być mechanizm wyszukiwania znaków. Przywrócenie prawdziwych `SearchValues` potwierdziło tę diagnozę.

Nie zostawiłem bind mountów ICU ani eksperymentalnych zmian w WSA jako wymogu uruchomienia aplikacji. Finalne dostosowanie znajduje się w kopii APK; WSA pozostaje wspólnym, stabilnym środowiskiem również dla mObywatela.

## 15. Weryfikacja końcowa po dwóch dniach pracy

APK zbudowałem przez `apktool 3.0.3`, wyrównałem `zipalign` i podpisałem niezależnym certyfikatem badawczym. `apksigner` potwierdził v2 i v3.

| Test | Wynik |
|---|---|
| `adb install -r` | `Success` |
| Zimny start | `Status: ok`, `LaunchState: COLD` |
| Czas startu | 2444 ms |
| Pierwsza aktywność | `RegulationsView` |
| Proces po 12 s | żywy, PID 17679 |
| Akceptacja regulaminu | żądanie przeszło |
| Aktywność po akceptacji | `StartView`, `topResumedActivity` |
| Krytyczne wyjątki | brak w końcowym logu |
| Root Magisk | aktywny |
| mObywatel | nienaruszony |

**Dowód wykonaniowy.** Ten sam przepływ, który wcześniej odrzucał `X-Device`, a potem `text/plain`, po naprawie semantyki zbudował żądanie, przyjął odpowiedź i otworzył `StartView` bez restartu procesu.

## 16. Wstępne ustalenia bezpieczeństwa

### EDO-A-01: kontrole roota i blokady ekranu są po stronie klienta

Wynik detekcji roota i `KeyguardManager.isDeviceSecure()` sterują lokalną nawigacją i można je zmienić w przepakowanym kliencie. Potwierdziłem to do ekranu startowego. Nie potwierdziłem jeszcze obejścia operacji kryptograficznej, NFC ani podpisu.

### EDO-P-01: obfuskacja nie chroni przepływu protokołu

Skrócone nazwy i szyfrowanie literałów utrudniają analizę, ale zachowane metadane oraz odwracalny offline szyfr łańcuchów wystarczyły do odtworzenia usług, endpointów, parametrów kryptograficznych i obsługi wyjątków (§9.2).

### EDO-Q-01: mylące mapowanie wyjątków

Lokalny `FormatException` został pokazany jako brak Internetu. Tak szerokie mapowanie utrudnia diagnostykę i ukrywa klasę faktycznej awarii.

### EDO-Q-02: pole PIN/PUK bez wymuszenia cyfr na klawiaturze sprzętowej

Ekranowy keypad ogranicza wejście tylko wizualnie; pole ma jedynie filtr długości, a w kodzie brak `DigitsKeyListener` i numerycznego `inputType`. Klawiaturą sprzętową (USB, Bluetooth, host WSA) można wpisać znaki niecyfrowe, w moim przypadku `WESMAR` zamiast sześciu cyfr. Sytuacja ta nie stanowi obejścia uwierzytelnienia, ponieważ PIN weryfikuje procesor karty, generuje jednak ryzyko wyczerpania limitu prób PIN i PUK oraz niespójności walidacji. Szczegóły i test w 19.3.2.

### EDO-C-01: silne związanie z ARM64

Brak x86-64 nie jest podatnością. W połączeniu z natywną globalizacją i AdvSimd ogranicza jednak interoperacyjność i ujawnia założenia niewidoczne na fizycznym ARM.

## 17. Co pozostaje do zbadania

- rejestracja dokumentu przez NFC, CAN/MRZ i PIN;
- przechowywanie sekretów i danych dokumentu;
- Android Keystore, TEE i StrongBox;
- certificate pinning i kontrolowany MitM;
- reakcja API na przepakowany, ponownie podpisany klient;
- integralność żądań, nonce, replay i powiązanie z urządzeniem;
- podpis PDF i walidacja dokumentu wynikowego;
- zachowanie po restarcie i długiej sesji.

Samo sforsowanie uruchomienia na WSA nie stanowiło jeszcze podstawy do wystawienia oceny bezpieczeństwa aplikacji. Odblokowanie środowiska badawczego i odzyskanie spójnych binariów otworzyło dopiero drogę do właściwego badania kodu i protokołów. Dlatego ocenę punktową odnoszę wyłącznie do wyników deasemblacji i weryfikacji wykonawczej mechanizmów klienta, opisanych w kolejnych rozdziałach.

## 18. Narzędzia i ślad dowodowy

- `apktool`, `zipalign`, `apksigner`, `adb`, `logcat`, `dumpsys`;
- `dnfile` i `ildasm` do metadanych oraz IL;
- własny parser assembly-store i dekompresor XALZ/LZ4;
- `K4os.Compression.LZ4` w trybie `L12_MAX`;
- analiza ELF, symboli i kodu ARM64;
- IDA Pro 9.4;
- SHA-256 oraz round-trip każdego przebudowanego assembly.

Powtarzalny patcher ma wersjoniezależny punkt wejścia i profile rozpoznawane po SHA-256 całego APK. Profil 1.7.1 został dwukrotnie sprawdzony od czystego pakietu. Nieznana przyszła wersja nie dostanie automatycznie starych offsetów: narzędzie zapisuje raport ABI oraz hashy krytycznych bibliotek i zatrzymuje się do czasu przeglądu nowego profilu.

## 19. Analiza statyczna: pierwszy przekrój

Ten etap dotyczy wyłącznie oryginalnego APK 1.7.1 o SHA-256 `07768A1F0A07493D6A89A981E9603863787D04A750E04536FBB2E3CA8105B84B`. Nie przenoszę wniosków z przebudowanej kopii badawczej na wydanie producenta. Zdekodowałem manifest i zasoby, a z oryginalnego assembly store ARM64 wyciągnąłem `Pwpw.Mobile.EdoHub.Core.dll`, `Pwpw.Mobile.EdoHub.Droid.dll`, `Pwpw.Mobile.EdoHub.Interfaces.dll` oraz `Microsoft.Maui.Essentials.dll`.

### 19.1. Szyfrowany magazyn dla danych lokalnych

Mimo zaciemnienia nazw pól, definicje typów oraz wywołania metod pozostają jawne w metadanych IL. `SecureStorageKeys` obejmuje `PIN1`, `PIN2`, `CAN`, `MRZ`, identyfikator urządzenia i certyfikatu, sól oraz prywatny klucz uwierzytelniający zapisany jako Base64. Kod biznesowy odczytuje i zapisuje je przez `Microsoft.Maui.Storage.SecureStorage`; lista certyfikatów jest przed zapisem serializowana do JSON.

Wyciągnięta z tego samego APK implementacja Maui nie jest zwykłym `SharedPreferences`. Tworzy `AndroidX.Security.Crypto.EncryptedSharedPreferences` z `MasterKey` w schemacie AES-256-GCM, nazwami kluczy szyfrowanymi AES-256-SIV i wartościami AES-256-GCM.

flowchart LR A[PIN1 / PIN2 / CAN / MRZ] --> B[Maui SecureStorage] C[identyfikator, sól, prywatny klucz] --> B B --> D[EncryptedSharedPreferences] D --> E[MasterKey AES-256-GCM] D --> F[nazwy AES-256-SIV] D --> G[wartości AES-256-GCM] 

To jest potwierdzona ochrona zapisów aplikacji przed prostym odczytem pliku preferencji. Nie oznacza jeszcze, że klucz jest sprzętowo związany z TEE lub StrongBox: sam ten fakt wymaga odczytu właściwości Android Keystore na fizycznym urządzeniu i osobnego testu.

Jest też ważny szczegół dostępności. Gdy utworzenie zaszyfrowanych preferencji rzuca `KeyStoreException`, `Java.Security.KeyStoreException` albo `BadPaddingException`, implementacja Maui czyści cały magazyn i tworzy go ponownie. Kod nie pozwala jeszcze rozstrzygnąć, czy eDO App wcześniej synchronizuje wszystkie dane potrzebne do odtworzenia stanu. To będzie osobny test po kontrolowanej zmianie klucza lub przywróceniu kopii aplikacji.

### 19.1.1. Brak lokalnego kontenera tożsamości

To podstawowa różnica architektoniczna względem mObywatela. Zawartość bezpiecznego magazynu `SecureStorageKeys` obejmuje wyłącznie zdefiniowaną listę kluczy:

| Klucz | Rola |
|---|---|
| `PIN1`, `PIN2` | sekrety dostępowe |
| `CAN`, `MRZ` | dane dostępu do karty (PACE) |
| `DEVICE_ID`, `CERT_ID` | identyfikatory urządzenia i certyfikatu |
| `AUTHENTICATOR_ID`, `AUTHENTICATOR_SALT` | tożsamość i sól authenticatora |
| `AUTHENTICATOR_PRIVATE_KEY_BASE64` | prywatny klucz uwierzytelniający, zapisany jako Base64 |
| `AUTHENTICATOR_CERTIFICATE_LIST` | lista certyfikatów |

Magazyn nie zawiera danych osobowych: numeru PESEL, imienia, nazwiska, wizerunku ani grup danych. Modele `PersonalData`, `Photo` oraz `DataGroup`, gdzie DG1 oznacza MRZ, a DG2 wizerunek twarzy, istnieją wyłącznie jako obiekty w pamięci operacyjnej. Powstają podczas odczytu karty NFC i są niszczone komunikatem `EndSessionMessage`. Kod aplikacji nie zawiera żadnej warstwy trwałego zapisu danych dokumentu w SQLite, Realm ani w plikach płaskich.

Funkcja zapamiętywania danych nie narusza tej zasady. Metoda `MrzInfo.RememberData` zapisuje wyłącznie sumy kontrolne MRZ do bezpiecznego magazynu pod kluczem `MRZ`. Z kolei `DocumentChoiceViewModel.RememberDocumentChoice` to flaga interfejsu w jawnym rejestrze `StorageKeys` pod kluczem `IdDocumentChoiceKey`. Aplikacja utrwala jedynie parametry dostępowe do chipa NFC oraz preferencje widoku, bez danych tożsamościowych.

Architektura mObywatela opierała się na trwałym kontenerze mDowodu zapisanym w pamięci masowej, chronionym programowo funkcją PBKDF2 bez powiązania z Android Keystore, co umożliwiało atak offline na kopię danych. Aplikacja eDO App nie posiada takiego kontenera. Źródłem danych pozostaje fizyczny chip dowodu osobistego odczytywany przez interfejs NFC. W pamięci urządzenia pozostają jedynie tokeny sesyjne chronione przez `EncryptedSharedPreferences` z kluczem głównym w Android Keystore. W warstwie ochrony danych w spoczynku eDO App reprezentuje bezpieczniejszy model.

Dwa elementy techniczne wymagają odnotowania. Po pierwsze, klucz `AUTHENTICATOR_PRIVATE_KEY_BASE64` zapisano w formacie Base64 zaszyfrowanym kluczem głównym, zamiast użyć nieeksportowalnego uchwytu w Keystore. Po odszyfrowaniu magazynu klucz prywatny można wyodrębnić. Po drugie, sprzętowe wiązanie klucza zależy od konkretnego telefonu. Test na fizycznym Xiaomi 13T Pro z systemem Android 16 wykazał obecność flagi `android.hardware.hardware_keystore`, czyli wsparcie dla środowiska TEE, przy braku flagi `android.hardware.strongbox_keystore`. Domyślny MasterKey biblioteki AndroidX jest na tym urządzeniu chroniony sprzętowo w TEE, bez dedykowanego układu StrongBox. Sama zdolność sprzętowa jest potwierdzona i odróżnia eDO od czysto programowego kontenera mObywatela.

### 19.1.2. Weryfikacja magazynu na żywym urządzeniu (WSA z rootem)

Ustalenia statyczne z §19.1 i §19.1.1 potwierdziłem empirycznie, odczytując prywatny katalog aplikacji na zrootowanym WSA (`/data/data/pl.pwpw.edohub/`). Wartości poniżej częściowo zamaskowano.

Plik sekretów `…maui.essentials.preferences.xml` ma postać AndroidX `EncryptedSharedPreferences`. Nazwy kluczy są szyfrogramem deterministycznym AES-SIV (wspólny prefiks Base64 `AfnyGfQ…`), a wartości szyfrogramem uwierzytelnionym AES-GCM w formacie Tink (prefiks wersji i identyfikatora klucza `AUqzru…`). Obok nich występują dwie pozycje `__androidx_security_crypto_encrypted_prefs_*_keyset__`, przechowujące zestawy kluczy opakowane kluczem głównym z Android Keystore. W chwili badania magazyn zawierał dokładnie dwa wpisy danych o długości wartości 105 oraz 47 bajtów.

Zawartość obu wpisów zidentyfikowałem bez odszyfrowywania, na podstawie znanego formatu zapisu. Wartość w `EncryptedSharedPreferences` jest ramką `4 bajty (typ) + 4 bajty (długość) + N bajtów łańcucha UTF-8`, a szyfrowanie Tink AES-GCM dokłada stały narzut 33 bajtów (5 bajtów wersji i identyfikatora klucza, 12 bajtów wektora inicjującego, 16 bajtów znacznika uwierzytelnienia). Po odjęciu narzutu i ramki wpis 47-bajtowy odpowiada łańcuchowi o długości sześciu znaków, a wpis 105-bajtowy łańcuchowi o długości sześćdziesięciu czterech znaków. Sześcioznakowy łańcuch odpowiada numerowi CAN, utrwalonemu po zaznaczeniu opcji „Zapamiętaj numer CAN" i prezentowanemu w interfejsie w postaci zamaskowanej `…886`, natomiast łańcuch sześćdziesięcioczteroznakowy odpowiada identyfikatorowi urządzenia `DEVICE_ID`. Wniosek jest zgodny z deterministyczną naturą AES-SIV: ponowny zapis tego samego numeru CAN odtwarza tę samą nazwę klucza przy nowym, losowym szyfrogramie wartości.

Numer CAN jest zatem utrwalany, lecz wyłącznie w postaci zaszyfrowanej; w pliku ustawień jawnych `…AppSettings.xml` nie występuje. Plik ten zawiera jedynie flagi przepływu, między innymi `IS_REGULATIONS_ACCEPTED_KEY`, `FIRST_RUN`, `SIGN_DOCUMENT_VIEW` i `CAN_CHANGE_SHOW_HELP_DIALOG`, oraz token powiadomień. Nie stwierdzono w nim numeru CAN, ciągu MRZ ani numeru PESEL w postaci jawnej.

Wymagane jest zdefiniowanie granicy zaufania dla tego mechanizmu ochronnego. Prezentacja zamaskowanego numeru CAN w interfejsie dowodzi, że aplikacja odszyfrowuje wartość na żądanie kluczem głównym z Android Keystore. Na urządzeniu z uprawnieniami root klucz ten pozostaje używalny w kontekście aplikacji, wobec czego zapamiętany CAN jest tam odtwarzalny. Jest to wspólna właściwość każdego szyfrowania danych w spoczynku opartego o Keystore: chroni ono przed wydobyciem danych z samego pliku, na przykład z kopii zapasowej lub skradzionego nośnika, a nie przed żywym, skompromitowanym systemem, w którym klucz jest dostępny. Waga tego ograniczenia jest przy tym niska. CAN to sześciocyfrowy numer nadrukowany na awersie karty, w standardzie ICAO 9303 pełniący rolę słabego sekretu dostępowego do zestawienia kanału PACE; jego znajomość, bez fizycznego kontaktu NFC z kartą i bez kodu PIN, nie umożliwia żadnej operacji.

Deszyfracja literałów (§9.2) pozwoliła odtworzyć rzeczywiste nazwy kluczy magazynu i sklasyfikować je według trwałości. Trwale, na życzenie użytkownika, pamiętane są `EDOAPP_CAN` oraz `EDOAPP_MRZ`; obu odpowiadają pozycje menu „zapomnij CAN" i „zapomnij MRZ". Charakter trwały operacyjny mają `DEVICE_ID` oraz komplet kluczy Authenticatora (`EDOAPP_AUTHENTICATOR_ID`, `_SALT`, `_PRIVATE_KEY_BASE64`, `_CERTIFICATE_LIST`).

Postępowanie z kodami PIN wymaga sprostowania wcześniejszego uproszczenia. PIN tożsamości (`PIN1`) oraz PIN podpisu (`PIN2`) nie są pamiętane na stałe, lecz przechodzą przez zaszyfrowany magazyn przejściowo, na czas operacji, po czym są z niego jawnie usuwane. Kod zawiera dedykowaną procedurę czyszczącą wywoływaną w cyklu życia widoku, która kasuje `PIN1`, `PIN2` oraz `CERT_ID` (`SecureStorage.Remove`), a wylogowanie wykonuje pełne `SecureStorage.RemoveAll`. Zapis `PIN1` realizowany jest w warstwie `Droid` (`SecureStorage.SetAsync`), a wartość podlega następnie opisanemu skasowaniu. Jest to poprawna higiena: sekret uwierzytelniający nie pozostaje w spoczynku po zakończeniu operacji.

W środowisku WSA, pozbawionym czytnika NFC, nie doszło do rejestracji Authenticatora ani do operacji na karcie, wobec czego zaobserwowany magazyn zawierał wyłącznie zapamiętany CAN oraz `DEVICE_ID`, bez klucza prywatnego Authenticatora, sekretu TOTP i bez pozostałości kodów PIN, co jest zgodne z powyższą procedurą czyszczącą.

Katalogi `databases/` oraz `files/` zawierają wyłącznie komponenty Google, Firebase i Sentry. Nie odnaleziono bazy dokumentu, danych karty ani wizerunku twarzy. Empirycznie potwierdza to model bez trwałego kontenera tożsamości (§19.1.1) oraz brak zapisu dokumentów na nośnik (§19.3.5). Pełny zestaw sekretów operacyjnych można zweryfikować dopiero na urządzeniu z rzeczywistym odczytem karty.

### 19.2. Transport i piny certyfikatów

Manifest wskazuje `network_security_config.xml`. Dla `edoapp.pl` zapisano 11 pinów SHA-256, dla `sigillum.pl` kolejnych 6; dla obu domen i subdomen wyłączono ruch jawny. Zaufanie ograniczono do magazynu systemowego, bez magazynu certyfikatów użytkownika. W pliku nie ma dat ważności pinów.

Warstwa .NET nie podmienia tego transportu dowolnym klientem. `IHttpMessageHandlerProvider` zwraca `Xamarin.Android.Net.AndroidMessageHandler`, a kod tworzy z niego `HttpClient`. W kodzie własnym aplikacji nie znalazłem callbacku walidacji certyfikatu ani ścieżki typu „zaakceptuj każdy certyfikat”. To spina konfigurację Androida z faktycznym klientem HTTP; wcześniejszy test regulaminu dodatkowo potwierdził działające połączenie HTTPS.

flowchart LR A[kod usługi eDO] --> B[HttpClient] B --> C[Xamarin.Android.Net.AndroidMessageHandler] C --> D[Android network security config] D --> E[edoapp.pl: HTTPS + 11 pinów] D --> F[sigillum.pl: HTTPS + 6 pinów] 

Konfiguracja bazowa dopuszcza ruch cleartext dla pozostałych domen, w zasobach zidentyfikowano jednak wyłącznie archiwalny odnośnik ODR do `http://ec.europa.eu/consumers/odr`. Brak przesłanek wskazujących na transmisję danych wrażliwych otwartym kanałem HTTP.

### 19.3. Granice procesu Android

Wydanie produkcyjne ma `android:debuggable="false"` oraz `android:allowBackup="false"`. Dostawca treści Maui deklaruje szeroki zestaw ścieżek plikowych obejmujący pamięć zewnętrzną oraz pamięć podręczną, jednak sam provider `pl.pwpw.edohub.fileProvider` ma atrybut `exported="false"`. Dostęp z zewnętrznych procesów jest zablokowany, a pliki są udostępniane wyłącznie przez jawne nadanie uprawnień do URI.

Aplikacja udostępnia funkcję pobierania i przekazywania dokumentów. Weryfikacja zakresu uprawnień URI oraz czasu ich ważności wymaga testu z kontrolowaną aplikacją odbiorczą. Sama definicja ścieżek przy braku eksportu komponentu nie stanowi podatności.

Brak literału `FLAG_SECURE` w assembly biznesowych wynika z nakładania flagi w bazowej aktywności Androida. Weryfikacja dynamiczna na telefonie Xiaomi 13T Pro z systemem Android 16 potwierdziła obecność flagi `SECURE` w `dumpsys window` na wszystkich ekranach: `CardActivityView`, `CanEntryView`, `AcceptSignDataView`, `SignDocumentView`, `RegulationsView` oraz `StartView`. Próbny zrzut ekranu wykonany podczas procedury podpisu wygenerował czarny obraz, co potwierdza skuteczne działanie blokady przechwytywania widoku.

Eksportowana jest tylko jedna własna aktywność: `SplashView`. Manifest przypisuje jej cztery wejścia: start z ikony, `edohub://application`, `edoapp://application` i `ACTION_SEND` dla `application/pdf`. To jest zamierzona powierzchnia integracji z innymi aplikacjami.

Ścieżkę wejściową prześledziłem dalej niż manifest. `IntentExtension.ToStartupAction` bierze URI z intencji, otwiera je przez `ContentResolver.OpenInputStream`, kopiuje do `MemoryStream`, a następnie tworzy `SignFileAction` z nazwą i tablicą bajtów. Gdy odczyt się nie uda, adapter przekazuje pustą wartość zamiast kończyć proces. Ten adapter nie odczytuje `Intent.Type` i nie parsuje zawartości PDF; jest tylko mostem z Androida do kodu aplikacji.

Właściwa kontrola pojawia się w `SignDocumentViewModel`. Przed przejściem do dalszego kroku obowiązuje limit `0x500000`, czyli 5 MiB. Następnie osobny walidator otrzymuje całą tablicę bajtów, odrzuca `null`, dane puste i dokument krótszy niż 290 B, a błędy walidacji zamienia na komunikat w interfejsie. To jest realna kontrola wejścia, choć sama obecność walidatora nie jest jeszcze dowodem odporności na uszkodzony lub celowo zbudowany PDF. Sprawdzę ją osobno dokumentami granicznymi i mutacjami struktury.

flowchart LR A[Inna aplikacja
ACTION_SEND / URI] --> B[SplashView] B --> C[ContentResolver
OpenInputStream] C --> D[SignFileAction
nazwa + bajty] D --> E[SignDocumentViewModel] E --> F{limit 5 MiB
i walidator} F -->|poprawny| G[przepływ podpisu] F -->|odrzucony| H[komunikat UI] 

Te same wejścia URI prowadzą do dwóch odrębnych akcji startowych: parametr z URI może utworzyć `CodeAction`, a osobny parametr operacji jest najpierw parsowany jako `Guid` i dopiero wtedy tworzy `NewOperationAction`. Dynamicznie podam niepoprawne URI, pusty `ClipData`, wielokrotny `ACTION_SEND` i PDF o błędnej strukturze. Celem jest sprawdzenie walidacji całego łańcucha, nie tylko wywołanie ekranu.

Pozostałe eksportowane elementy nie są własną ścieżką biznesową eDO: odbiornik Firebase wymaga `com.google.android.c2dm.permission.SEND`, a `androidx.profileinstaller.ProfileInstallReceiver` wymaga `android.permission.DUMP`. Niezabezpieczony deklaracją permission odbiornik AppsFlyer przyjmuje tylko `com.android.vending.INSTALL_REFERRER`.

Własny `AppsFlyerConversionDelegate` odbiera dane konwersji i atrybucji, przechodzi po parach klucz–wartość i składa tekst diagnostyczny. W przejrzanej implementacji wynik nie jest zapisywany, przekazywany do logiki eDO ani używany do nawigacji. Ogranicza to wpływ spreparowanego referrera na aplikację, nie wyklucza jednak telemetrii instalacyjnej prowadzonej wewnętrznie przez pakiet SDK AppsFlyer. Zagadnienie to rozstrzygnie rejestracja ruchu sieciowego podczas pierwszego uruchomienia.

### 19.3.1. Sygnały integralności środowiska

`IRootDetectionService` nie sprowadza się do jednego testu `su`. Metoda `IsRootedOrDangerous()` łączy wynik rozpoznania roota, listy niepożądanych pakietów, wykrywania emulatora, flagi debugowalności i podłączonego debugera. Ten ostatni sygnał pochodzi bezpośrednio z `Android.OS.Debug.IsDebuggerConnected`. Są to lokalne wskaźniki stanu urządzenia.

Klasa `IRootDetectionService` nie wykonuje kryptograficznej atestacji stanu systemu. Weryfikacja ewentualnego odrzucania żądań przez serwer na podstawie parametrów nagłówka wymaga analizy ruchu sieciowego.

### 19.3.2. Pole PIN i PUK nie wymusza cyfr na klawiaturze sprzętowej

To ustalenie wyszło z obserwacji wykonaniowej, którą potem potwierdziłem w kodzie. Na ekranie PIN-u, mimo własnej ekranowej klawiatury numerycznej `PinKeyboard`, dało się wpisać znaki niecyfrowe z klawiatury sprzętowej: zamiast sześciu cyfr wpisałem słowo `WESMAR`. Na WSA jest to klawiatura hosta; na fizycznym Androidzie ten sam efekt da klawiatura USB albo Bluetooth.

Mechanizm jest znany: `android:inputType="number"` ogranicza jedynie miękką klawiaturę ekranową. Bez jawnego `DigitsKeyListener` albo `InputFilter` przepuszczającego tylko cyfry, klawiatura sprzętowa nie jest filtrowana. Kontrola kodu to potwierdza: w warstwie widoku `PinView` oraz `PinKeyboard` pole posiada wyłącznie filtr długości `LengthFilter`. W kodzie nie występuje `DigitsKeyListener`, numeryczny `inputType` ani filtr cyfr. Ograniczenie do cyfr wynika wyłącznie z układu graficznego klawiatury ekranowej, a nie z ograniczenia nałożonego na pole tekstowe. Ten sam wzorzec obejmuje ekrany zmiany PIN-u oraz PUK.

Wpływ na bezpieczeństwo: zjawisko to nie stanowi obejścia uwierzytelnienia, ponieważ PIN weryfikuje chip karty poleceniem APDU, które odrzuci ciąg znaków alfanumerycznych. Ryzyko dotyczy braku spójności walidacji oraz potencjalnego zużywania limitu prób. Jeżeli niepoprawny ciąg zostanie przekazany bezpośrednio do karty, każda próba dekrementuje licznik PIN, a w przypadku kodu PUK grozi trwałym zablokowaniem dowodu osobistego. Kwestię tego, czy `PinValidatorBase.IsValid` filtruje znaki niecyfrowe przed transmisją APDU, rozstrzygnie test z monitorowaniem licznika fizycznej karty. Ustalenie oznaczono jako **EDO-Q-02**.

### 19.3.3. Podpisywanie PDF: płytka walidacja klienta i pytanie WYSIWYS

Procedura składania podpisu kwalifikowanego została poddana deasemblacji oraz weryfikacji wykonawczej.

Po pierwsze, aplikacja kliencka nie zawiera biblioteki do parsowania plików PDF, takich jak PdfBox, iText czy PdfSharp. Walidacja w `SignDocumentViewModel` sprawdza jedynie nagłówek `%PDF`, rozmiar w granicach od 290 bajtów do 5 MiB oraz oblicza skrót SHA-256. Klient nie posiada powierzchni ataku parsera PDF, eliminując podatności typu XXE lub bomby dekompresyjne. Sam format PAdES generowany jest po stronie serwera Sigillum.

Po drugie, w kodzie aplikacji brak komponentu renderującego treść PDF. Widok `StampPlacementView` powiązany ze `StampPlacementViewModel` służy wyłącznie do określenia położenia pieczęci graficznej na siatce pól. W teście wykonawczym ekran ten wyświetlał jedynie schemat rozmieszczenia bez podglądu tekstu dokumentu.

Obecnie nie posiadam aktywnego certyfikatu kwalifikowanego; z podpisu mSzafir korzystałem wcześniej jako prezes Atlogika Sp. z o.o., dlatego test wykonawczy objął procedurę do etapu pozycjonowania pieczęci. Kluczowy fakt wynika bezpośrednio z kodu: aplikacja nie wyświetla podpisywanej treści. Przekazanie pliku do zewnętrznej przeglądarki przez akcję `ACTION_VIEW` jest opcjonalne i realizowane poza procesem eDO App. Narusza to zasadę WYSIWYS, czyli What You See Is What You Sign. Użytkownik autoryzuje operację kryptograficzną na danych binarnych, których integralności wizualnej klient nie wymusza.

Dochodzi do tego subtelniejsza warstwa, spójna z powyższym. Dokument i bajty podpisywane odczytywane są niezależnie: `IntentExtension.ToStartupAction` czyta URI przez `ContentResolver.OpenInputStream` do `MemoryStream`, osobno od jakiegokolwiek podglądu. Dla identyfikatora `content://` przekazanego przez obcą aplikację zewnętrzny provider może zwrócić inną treść podczas podglądu, a inną w chwili podpisu, co otwiera drogę do podatności typu TOCTOU. Weryfikacja tego scenariusza wymaga osobnego testu z kontrolowanym dostawcą treści. Niezależnie jednak od podatności na wyścig, **brak wymuszonego, czytelnego podglądu dokumentu przed złożeniem podpisu** pozostaje najpoważniejszą wadą całej tej procedury.

Sprawdzenie 4 bajtów nagłówka `%PDF` i limitu rozmiaru to kontrola czysto formatowa. Aplikacja nie weryfikuje struktury dokumentu ani nie wyświetla jego stron. Użytkownik autoryzuje e-dowodem niezweryfikowane dane binarne, co przy braku podglądu pozwala na podrzucenie do podpisu zmanipulowanego pliku. Stanowi to podstawę do oznaczenia podatności jako **EDO-A-02**.

### 19.3.4. Walidacja wejścia QR i deep-link: potwierdzona dynamicznie

Jedyna eksportowana aktywność `SplashView` przyjmuje deep-linki `edohub://application` i `edoapp://application`; kod z QR mapuje się wewnętrznie na ten sam URI i przez `IntentExtension.ToStartupAction` na `CodeAction` albo `NewOperationAction` (parametry `code`, `operation`). To jest właściwa powierzchnia wejścia, więc sprawdziłem ją wykonaniowo.

Test był kontrolowany: wygenerowałem kod QR z treścią spoza formatu operacji (zwykłym tekstem) i zeskanowałem go w aplikacji. Reakcja jest wzorcowa: eDO **odrzuciło kod** komunikatem, że aplikacja odczytuje wyłącznie kody ze stron partnerów. `dumpsys` potwierdza, że proces nie padł i wrócił na `StartView`, bez crasha i bez przejścia w nieoczekiwany stan. Skaner QR waliduje więc treść względem oczekiwanego źródła, a nie przetwarza bezkrytycznie dowolnego ciągu.

Odfiltrowanie nieprawidłowych danych QR stanowi skuteczną warstwę ochronną klienta. W celu weryfikacji odporności na spreparowane parametry przeprowadzono test dynamiczny przez adb. Przekazano zestaw spreparowanych intencji: parametr `operation` ze zniekształconym formatem Guid, poprawny składniowo, niewystępujący w systemie Guid zerowy, parametr `code` z ciągiem wstrzyknięcia skryptu oraz pusty URI `edoapp://application` przy zimnym starcie procesu. W każdym przypadku parser `ToStartupAction` odrzucił wejście bez błędu krytycznego, a aplikacja powróciła do widoku `StartView`. Obsługa wejść zewnętrznych po stronie klienta zachowuje odporność na zniekształcone dane.

### 19.3.5. Brak zapisu dokumentów do pamięci współdzielonej

Zweryfikowałem odrębnie, czy aplikacja pozostawia pobrane lub podpisane dokumenty w lokalizacji dostępnej dla innych aplikacji, ponieważ była to realna słabość analizowana w przypadku mObywatela. W eDO nie stwierdzono ku temu podstaw. W kodzie nie występują typy odpowiedzialne za zapis ani udostępnianie plików na dysku, nie odnaleziono również literałów ścieżek do katalogu `Downloads`, pamięci zewnętrznej ani plików `*.pdf`. Liczne wystąpienia terminu „Download" odnoszą się do pobierania sieciowego, nie zaś do zapisu na nośniku. Dokument przeznaczony do podpisu jest wczytywany do pamięci (`MemoryStream`) i w niej pozostaje. Jest to ustalenie korzystne, spójne z modelem bez trwałego kontenera z §19.1.1: eDO nie utrwala treści dokumentów w pamięci urządzenia poza czasem operacji.

### 19.4. Diagnostyka i telemetria

W APK są Sentry, Firebase i AppsFlyer. Manifest ma `io.sentry.auto-init=false`, ale `Setup` inicjalizuje Sentry ręcznie, z własnym DSN ukrytym przez obfuskację. `tracesSampleRate` i `profilesSampleRate` ustawiono na `1.0`. Kod wywołuje `SentrySdk.CaptureException` w blokach obsługi wyjątków, m.in. przy inicjalizacji aparatu oraz podczas odczytu i przetwarzania plików PDF. W warstwie Androida zidentyfikowałem ponadto dwa wywołania `Android.Util.Log.Error` dotyczące doboru rozdzielczości kamery, sparowane z wysyłką raportu do Sentry. Z kolei kod biznesowy implementuje interfejs `IAnalyticsService` z metodami `TrackEvent` i `SendLog`; zdarzenia telemetryczne rejestrowane są m.in. przy przejściu użytkownika do ekranu wprowadzania CAN.

W kodzie brak jawnego wywołania `setSendDefaultPii(true)`, callbacku przypisującego tożsamość użytkownika oraz funkcji dołączania zrzutu ekranu. Nie wyklucza to jednak obecności danych wrażliwych w treści samych komunikatów wyjątków, a pełne próbkowanie śladów i profili (`1.0`) zwiększa ryzyko wycieku. Faktyczny zakres telemetrii zweryfikuje analiza ruchu sieciowego i bufora Sentry podczas wywołania kontrolowanego błędu testowego.

### 19.5. Biometria: FaceTec, przepływ danych twarzy i retencja

Weryfikacja tożsamości wykorzystuje silnik **FaceTec**, certyfikowane, komercyjne rozwiązanie do detekcji żywotności (Liveness 3D) oraz biometrycznego dopasowania twarzy. Kontrakty pozostają jawne w metadanych IL: interfejs `IFaceTecClientService` udostępnia operacje `GetSessionToken`, `Enrollment3d`, `MatchFacePhoto`, `VerifyIdWithEnrollmentAndMatch` oraz `ConvertJP2ToJPEG`, natomiast `IFaceTecPlatformService` odpowiada za sterowanie sesją. W warstwie prezentacji model `FaceBiometryRecordingViewModel` zarządza akwizycją klatek obrazu (`OnNewFrame`, `StartRecording`, `HandleVideoSentMessage`).

Obecność metody `ConvertJP2ToJPEG` jednoznacznie wyznacza schemat weryfikacji: skan trójwymiarowy z kamery jest zestawiany z wizerunkiem referencyjnym odczytanym z chipu dowodu osobistego, gdzie zgodnie ze specyfikacją ICAO 9303 zdjęcie twarzy (grupa DG2) kodowane jest w formacie JPEG 2000. Schemat ten zakłada zdalną autoryzację tożsamości, co wiąże się z transmisją danych biometrycznych poza urządzenie.

Zastosowanie renomowanego silnika biometrycznego, implementacja procedur czyszczenia buforów pamięci (`wipe`) oraz brak trwałego zapisu wizerunku na nośniku flash stanowią mocne punkty tej architektury. Kwestią wymagającą weryfikacji w teście dynamicznym pozostaje docelowy endpoint sieciowy, struktura przesyłanych pakietów oraz retencja danych po stronie serwera.

### 19.6. NFC i kryptografia karty

#### 19.6.1. Zagrożenie skimmingiem i rola numeru CAN

Bezkontaktowy interfejs radiowy (ISO/IEC 14443) umożliwia transmisję danych drogą elektromagnetyczną bez bezpośredniego styku galwanicznego. W architekturze systemów RFID cecha ta rodzi specyficzne ryzyko: komunikacja może zostać zainicjowana przez czytnik przez materiał kieszeni, torebki czy portfela bez świadomości i woli posiadacza dokumentu. Zagadnienie to stanowiło realną podatność wczesnych generacji paszportów biometrycznych, w których brak wstępnego uwierzytelnienia pozwalał na pasywne odpytanie chipu z odległości i pozyskanie danych tożsamości (skimming).

W strukturze e-dowodu ochronę przed niekontrolowaną transmisją realizuje sześciocyfrowy numer **CAN (Card Access Number)** nadrukowany na awersie karty. Pełni on trzy kluczowe funkcje architektoniczne:

1. **Optyczny dowód fizycznego dostępu i zgody (line-of-sight consent token).** Odczyt logiczny danych wymaga uprzedniego odczytania numeru widocznego na dokumencie. Wiąże to nawiązanie sesji radiowej z bezpośrednią kontrolą wzrokową i fizycznym dostępem posiadacza do dokumentu. Zdalny odczyt przez kieszeń lub portfel w przestrzeni publicznej zostaje wykluczony, ponieważ bez znajomości CAN mikrokontroler karty odrzuca żądanie nawiązania sesji.
2. **Materiał wejściowy dla protokołu PACE.** Sześciocyfrowy ciąg stanowi ziarno (seed) dla funkcji wyprowadzania kluczy (KDF) w protokole PACE (Password Authenticated Connection Establishment, ICAO Doc 9303). W oparciu o ten parametr aplikacja i karta uzgadniają klucze sesyjne do szyfrowania i uwierzytelniania kanału (secure messaging). Chociaż przestrzeń 6 cyfr ($10^6$ kombinacji) jest niska w ujęciu czysto obliczeniowym, mikrokontroler karty egzekwuje sprzętowy licznik błędnych prób. Po przekroczeniu określonego limitu nieudanych autoryzacji procesor blokuje dalsze próby do momentu podania procedury odblokowującej, co uniemożliwia skuteczny atak siłowy (brute-force) na interfejsie radiowym.
3. **Ergonomiczny ekwiwalent strefy MRZ.** W paszportach biometrycznych rolę sekretu wejściowego do procedur BAC lub PACE pełni odczyt strefy maszynowej (Machine Readable Zone, MRZ), co wymaga fizycznego otwarcia dokumentu i optycznego skanowania kodów. W dowodzie osobistym CAN stanowi zminiaturyzowany, numeryczny odpowiednik MRZ, możliwy do szybkiego wprowadzenia ręcznego lub za pomocą mechanizmu OCR kamery.

**Granice ochrony i model bezpieczeństwa CAN:**
CAN jest celowo uproszczonym sekretem chronionym wyłącznie barierą optyczną, a nie poufnością kryptograficzną. Każdy podmiot mający wgląd w awers dowodu (np. pracownik instytucji weryfikującej tożsamość, obsługa hotelowa czy odbiorca kserokopii) zna numer CAN. Numer ten nie chroni dokumentu przed osobą posiadającą fizyczny dostęp do karty lub jej odwzorowania cyfrowego. Z tego względu architektura bezpieczeństwa e-dowodu rozdziela role na trzy odrębne mechanizmy:

- **CAN:** zabezpieczenie warstwy radiowej przed skimmingiem z dystansu (mechanizm zapobiegający nieautoryzowanemu wzbudzeniu interfejsu na odległość),
- **PIN:** poufne uwierzytelnienie intencji posiadacza dla operacji wrażliwych (nie jest nadrukowany na karcie i pozostaje niezbędny do złożenia podpisu osobistego lub kwalifikowanego),
- **Chip Authentication:** asymetryczna weryfikacja autentyczności mikrokontrolera (potwierdzenie, że fizyczny nośnik nie jest programową repliką lub emulatorem).

#### 19.6.2. Wymiana APDU, protokół PACE i prymitywy kryptograficzne

NFC nie jest wyłącznie wywołaniem gotowego SDK. Interfejs `ICardCommunicator` ma surowe `TransreceiveCommandRaw`, wymianę APDU z limitem czasu, restart i anulowanie secure messaging oraz `SetupChipAuthentication`. Warstwa PACE tworzy własny klucz z sekretu, odszyfrowuje nonce, wylicza wspólny sekret i token uwierzytelnienia. W tym samym przepływie `IAuthenticatorService` otrzymuje PIN, identyfikator certyfikatu, klucz publiczny serwera, zakodowany sekret TOTP i klucz rejestracyjny.

Implementacja wyprowadzania kluczy jest czytelna mimo obfuskacji: dla `KSenc` dokleja do sekretu czterobajtowy licznik kończący się `01`, dla `KSmac` analogiczny licznik z `02`, po czym liczy skrót. Dostępne profile obejmują AES-128/192/256 i 3DES; dla chip authentication są DH i ECDH z 3DES-CBC-CBC lub AES-CBC-CMAC. Używana jest biblioteka Bouncy Castle.

flowchart LR A[CAN / MRZ / PIN] --> B[PACE: sekret i nonce] B --> C[KSenc oraz KSmac] C --> D[APDU i secure messaging] D --> E[chip authentication] E --> F[dane i certyfikaty karty] F --> G[operacja uwierzytelniająca] 

W funkcji skrótu znajduje się także gałąź SHA-1. Fakt ten nie stanowi wprost luki bezpieczeństwa: standardy kartowe definiują profile wstecznej zgodności, a analiza statyczna kodu nie przesądza, czy procesor dowodu osobistego i jego OID wybierają tę ścieżkę. Test z fizycznym dokumentem zweryfikuje negocjowany algorytm, OID i długość kluczy.

Weryfikacja komponentów potwierdza zgodność implementacji ze standardami. W bibliotece `Core.dll` obecne są klasy `Sha1Digest`, `Sha256Digest`, `AesEngine`, `DesEdeEngine` (3DES), `CMac` z dopełnieniem `ISO9797` oraz struktury `PaceInfo`, `ChipAuthenticationInfo` i `SecurityInfos`. Odpowiada to specyfikacji protokołów PACE oraz Chip Authentication według ICAO 9303, zrealizowanej w oparciu o bibliotekę Bouncy Castle. Zestaw szyfrów jest negocjowany na podstawie parametrów karty (`SecurityInfos` oraz identyfikatory OID), a nie narzucany statycznie przez aplikację. Gałąź SHA-1 oraz 3DES uaktywnia się wyłącznie w przypadku zadeklarowania jej przez chip dokumentu, co wynika z wymogów wstecznej zgodności ze specyfikacją ICAO, a nie z implementacji eDO App. Stanowi to różnicę względem mObywatela, w którym zastosowano własny kontener CBC ze stałą solą bez mechanizmów uwierzytelnienia; eDO App wykorzystuje standardowy protokół oraz audytowaną bibliotekę Bouncy Castle. Wybór algorytmu dla polskiego dowodu osobistego zostanie zweryfikowany podczas rejestracji sesji NFC w części dynamicznej.

### 19.7. Protokół Authenticatora: autoryzacja podpisu kwalifikowanego

Kluczowym modułem eDO App jest mechanizm Authenticatora, odpowiedzialny za autoryzację operacji podpisu kwalifikowanego. Protokół odtwarza się bezpośrednio z metadanych i interfejsu `IAuthenticatorClient`, struktur DTO oraz maszyn stanu w bibliotece `Core.dll`.

Procedura obejmuje cztery etapy:

1. **Rejestracja (`SetupAuthenticator`).** Klient generuje parę kluczy asymetrycznych i przekazuje klucz publiczny w żądaniu `SetupAuthenticatorRequest{PublicKey, PublicKeyType, AuthenticatorId, OperationId}`. W odpowiedzi serwer zwraca `SetupAuthenticatorResponse{TotpSecret, PublicKey, CertificateId}`, provisionując sekret TOTP oraz identyfikator certyfikatu. Wygenerowany klucz prywatny oraz sekret TOTP trafiają do lokalnego magazynu `SecureStorage`.
2. **Pobranie operacji (`LoadOperation`).** Odpowiedź serwera zawiera parametry `Counter`, `CreatedAt` oraz `ExpiresAt`. Numeryczna sekwencja operacji i ograniczenie czasu ważności zapewniają ochronę przed atakami typu replay oraz przetwarzaniem przeterminowanych żądań.
3. **Autoryzacja (`ConfirmOperation{Totp, OperationId}`).** Potwierdzenie operacji bazuje na jednorazowym kodzie TOTP wyliczanym z lokalnego sekretu i powiązanym z identyfikatorem zadania.
4. **Złożenie podpisu.** Obiekt `SignaturePayloadDto` przekazuje parametry `Challenge`, `Timestamp`, `Data` i `Certificate`. Z kolei struktura `SignatureDataDto` zawiera skróty grup danych dokumentu (`Sod`), flagi `Authentication` i `OperationSecurity` oraz numer seryjny karty (`CardSerialNumber`). Procedura implementuje mechanizm challenge-response z kryptograficznym wiązaniem z fizycznym chipem dowodu osobistego.

flowchart TD R[Rejestracja: para kluczy do serwera] --> T[Odpowiedz: TotpSecret + CertificateId] T --> L[LoadOperation: Counter, ExpiresAt] L --> C[ConfirmOperation: TOTP + OperationId] C --> S[Podpis: Challenge + Timestamp + Data] S --> B[Wiazanie karty: SOD + numer seryjny] 

**Wnioski z analizy architektury:**
Sam protokół zaprojektowano prawidłowo. Wykorzystuje standardowe mechanizmy challenge-response, TOTP, sekwencyjne liczniki operacji, okna czasowe ważności oraz powiązanie podpisu z danymi fizycznej karty.

**Identyfikowane ryzyko architektoniczne:**
Prywatny klucz Authenticatora oraz sekret TOTP są serializowane do magazynu `SecureStorage` jako ciągi Base64. Mimo szyfrowania bazy kluczem z Android Keystore, sekrety te po odszyfrowaniu mają postać jawną w pamięci procesu, a nie nieeksportowalnych uchwytów sprzętowych chronionych w TEE. Ekstrakcja danych z odblokowanego lub zrootowanego urządzenia umożliwia odtworzenie generatora autoryzacji poza fizycznym telefonem.

Weryfikacja tego, czy serwer bezwzględnie egzekwuje parametry wyzwania, sekwencję licznika i limity czasu, stanowi przedmiot badania dynamicznego w kolejnym etapie prac.

## 20. Dygresja: brak czytnika NFC w WSA i przekaźnik APDU

WSA nie obsługuje kontrolera NFC, co uniemożliwia bezpośredni odczyt dowodu osobistego w maszynie wirtualnej. Podpis i odczyt zrealizowano na fizycznym telefonie Xiaomi 13T Pro. Poniższa architektura przedstawia koncepcję programowego przekaźnika ramek APDU przez sieć lokalną, pozwalającego na odczyt karty w środowisku WSA przy użyciu telefonu jako zdalnego czytnika.

Rozwiązaniem technicznym jest przekaźnik ramek APDU. Aplikacja eDO App komunikuje się z chipem dowodu poleceniami `IsoDep.transceive`, a protokoły PACE oraz bezpieczne kanały wymiany danych działają bezpośrednio między aplikacją a procesorem karty. Warstwa transmisyjna przesyła zaszyfrowane ramki bez konieczności ich modyfikacji.

Koncepcja wdrożenia:

1. **Moduł LSPosed w WSA.** Przechwycenie wywołań `NfcAdapter.getDefaultAdapter` oraz `isEnabled` w celu emulacji dostępności kontrolera NFC. Metoda `IsoDep.transceive` w warstwie Java jest przechwytywana przez LSPosed pomimo wywołania ze środowiska Xamarin.
2. **Wstrzyknięcie tagu.** Sfabrykowanie zdarzenia wykrycia karty w `enableReaderMode` oraz podstawienie instancji `IsoDep` przy użyciu mechanizmów refleksji.
3. **Warstwa transportowa.** Przekazywanie przechwyconych ramek APDU przez protokół TCP z wykorzystaniem przekierowania `adb forward`.
4. **Demon na telefonie fizycznym.** Aplikacja z aktywnym `enableReaderMode` wykonująca polecenie `realIsoDep.transceive` na przyłożonym dowodzie i odsyłająca odpowiedź do gniazda sieciowego w WSA. Wymaga obsługi rozszerzonych ramek APDU zadeklarowanych przez `ExtendedLengthAPDUSupportedKey` oraz zachowania limitów czasowych transmisji.

flowchart LR subgraph WSA["WSA (bez NFC)"] EDO[eDO App] --> ISO[IsoDep.transceive] ISO --> HOOK[Hook LSPosed] end HOOK -->|APDU po TCP / adb forward| DEMON subgraph XIAOMI["Xiaomi 13T Pro (czytnik)"] DEMON[Demon enableReaderMode] --> CARD[Fizyczny dowód] end CARD -->|odpowiedz APDU| DEMON DEMON -->|po TCP| HOOK 

Architektura ta bazuje na mechanizmie przekaźnika APDU tożsamym z technikami typu relay attack na interfejsy zbliżeniowe. Z punktu widzenia inżynierskiego brak kontrolera NFC w WSA stanowi ograniczenie sprzętowe, które można rozwiązać programowo.

## 21. Reprodukowalność, dystrybucja i licencja

Procedurę modyfikacji zautomatyzowano skryptem weryfikującym sumę kontrolną SHA-256 oryginalnego pakietu APK oraz instalującym zmodyfikowane pliki poleceniem `adb install -r` z zachowaniem danych aplikacji.

Kod źródłowy mObywatela opublikowano na licencji MIT, co zezwalało na redystrybucję. Aplikacja eDO App stanowi oprogramowanie własnościowe PWPW S.A. Modyfikacja lokalnej kopii w celu zapewnienia interoperacyjności mieści się w granicach dozwolonego użytku, natomiast dystrybucja zmodyfikowanych binariów PWPW naruszałaby prawa autorskie producenta. Z tego względu repozytorium nie zawiera plików binarnych.

Paczka zawiera wyłącznie metodykę i skrypty weryfikacyjne: pliki automatyzujące, definicje profili oraz `payload-manifest.json` z sumami kontrolnymi SHA-256 wymaganych komponentów. Skrypt `patch-eDO-App-1.7.1-WSA.ps1` sprawdza obecność podkatalogu `payload/` zdefiniowanego w manifeście; skoro ze względów licencyjnych celowo go tam nie ma, skrypt zatrzyma się z błędem braku plików wsadowych. Dzięki temu repozytorium stanowi ścisły ślad dowodowy i weryfikator integralności, a nie kanał redystrybucji chronionego kodu producenta.

Potwierdzeniem działania procedury uruchomieniowej jest [materiał wideo na YouTube](https://youtu.be/9MTmyHogsxw) rejestrujący start aplikacji na WSA z aktywnym MagiskSU, przejście przez akceptację regulaminu oraz załadowanie widoku `StartView`.

## 22. Ocena architektury bezpieczeństwa klienta

Ocena dotyczy warstwy aplikacji klienckiej Android (`pl.pwpw.edohub`) i opiera się na analizie statycznej trzech głównych assembly .NET, deasemblacji zasobów oraz testach dynamicznych wykonanych na telefonie bez uprawnień root. Nie stanowi audytu infrastruktury serwerowej PWPW ani fizycznego procesora karty kryptograficznej. Wagi poszczególnych osi odzwierciedlają model zagrożeń aplikacji tożsamościowej: decydujące znaczenie ma ochrona danych w spoczynku (25%) oraz integralność procedury podpisu kwalifikowanego (20%), podczas gdy podatność kodu na inżynierię wsteczną traktowana jest jako warstwa pomocnicza (5%). Weryfikacja ruchu telemetrii, atestacji klucza w Keystore, integralności po stronie serwera oraz odporności pinningu na inspekcję TLS wymaga środowiska z rootem i stanowi przedmiot kolejnego etapu badań.

| Oś architektury | Waga | Ocena | Uzasadnienie inżynierskie |
|---|---|---|---|
| Dane w spoczynku | 25% | **8,5/10** | brak lokalnego kontenera tożsamości, dane odczytywane z karty, sekrety w Keystore ze wsparciem TEE; potwierdzenie dynamiczne na WSA (poprawne AES-SIV/AES-GCM); kody PIN przejściowe i jawnie usuwane (`Remove`/`RemoveAll`); zastrzeżenia: trwale pamiętane CAN i MRZ oraz eksportowalny klucz Authenticatora w Base64 |
| Kryptografia NFC/PACE | 15% | **8,5/10** | standardowe protokoły PACE i Chip Authentication wg ICAO oparte o Bouncy Castle, zestaw szyfrów negocjowany z karty |
| Transport i pinning | 15% | **8,0/10** | deklaracja 11 i 6 pinów w konfiguracji sieciowej, powiązanie z AndroidMessageHandler, brak akceptacji niezweryfikowanych certyfikatów w kodzie klienta |
| Integralność podpisu (WYSIWYS) | 20% | **4,5/10** | brak wymuszonego podglądu dokumentu w kroku podpisu kwalifikowanego, brak parsera i renderera PDF w kliencie, podatność EDO-A-02 |
| Ochrona ekranu i granice procesu | 10% | **8,0/10** | flaga `FLAG_SECURE` aktywna na wszystkich ekranach, brak flagi debuggowania, zamknięty fileProvider |
| Walidacja wejścia (QR, deep-link, PIN) | 10% | **6,5/10** | filtry wejść QR i deep-link odporne na niepoprawne dane; brak filtrowania klawiatury sprzętowej w polach PIN i PUK, podatność EDO-Q-02 |
| Odporność na reverse engineering | 5% | **7,0/10** | pełna konfiguracja Dotfuscatora: zmiana nazw, spłaszczenie przepływu i szyfrowanie literałów; skuteczność ograniczona samoopisującym się strumieniem klucza (odwracalnym offline) oraz zachowanymi metadanymi .NET, podatność EDO-P-01 |

**Wynik ważony: ≈ 7,3/10: ocena dobra (górny przedział skali).**

**Podsumowanie wyników.** Architektura eDO App wykazuje wyższy poziom bezpieczeństwa danych w spoczynku niż mObywatel. Brak lokalnego kontenera danych tożsamościowych eliminuje ryzyko ich ekstrakcji offline z kopii zapasowej. Dane uwierzytelniające chroni Android Keystore powiązany z TEE, a operacje kryptograficzne bazują na standardzie ICAO 9303 oraz zweryfikowanej bibliotece Bouncy Castle. Główną wadą rozwiązania pozostaje brak wymuszonego podglądu dokumentu przed złożeniem podpisu kwalifikowanego (fakt weryfikowalny bezpośrednio w kodzie widoku i view-modelu), uzupełniony o uchybienia w walidacji wejścia klawiatury sprzętowej oraz o obfuskację, która mimo szyfrowania literałów pozostaje odwracalna offline.

**Porównanie z architekturą mObywatela.** Przyjęty przez PWPW model bezpieczeństwa cechuje większy rygor inżynierski. Oparcie tożsamości na fizycznej karcie kryptograficznej, brak programowych magazynów tożsamości podatnych na ekstrakcję offline oraz implementacja standardowych protokołów kryptograficznych stanowią rozwiązania dojrzalsze od pierwotnej architektury mObywatela.

**Nota autorska.** Piszę to jako inżynier, który od lat dziewięćdziesiątych rozkłada oprogramowanie na części pierwsze i buduje własne, ulepszone wersje, w tym natywnego, desktopowego klienta mObywatela. Moje publiczne projekty (github.com/wesmar) sięgają warstw HVCI, EFI, DSE i PPL, dlatego klienta operującego wyłącznie w przestrzeni użytkownika, przez PC/SC i publiczne biblioteki kryptograficzne, traktuję jako zadanie inżyniersko proste. Przy dzisiejszym warsztacie i narzędziach realistycznie szacuję około trzech tygodni na działający szkielet i kolejne trzy na dopracowanie całości klienta do wersji w pełni dojrzałej i wydajnej. Znając strukturę klienta, zestaw protokołów i format magazynu, stawiam tezę praktyczną: warstwę odczytu e-dowodu da się odtworzyć jako natywną aplikację desktopową dla Windows w nowoczesnym C++23 na WinAPI. Odczyt tożsamości opiera się na otwartym standardzie ICAO 9303 (PACE, Chip Authentication, Passive Authentication, DG1/DG2/SOD), dostępnym przez interfejs PC/SC (WinSCard) i publiczne biblioteki kryptograficzne, bez żadnego zastrzeżonego komponentu PWPW. Wiele laptopów ma dziś czytnik NFC, a tam gdzie go brak, wystarcza tani czytnik PC/SC na USB; niezależny klient rozwiązałby realny problem osób, które eDO App na telefonie mieć nie mogą lub nie chcą.

Zachowanie transparentności systemowej wymaga dokładnego zdefiniowania granic między obiema warstwami, zamiast jedynie deklaratywnego ujęcia problemu. Warstwa odczytu charakteryzuje się bezpośrednią odtwarzalnością. Warstwa usługowa (internetowy Authenticator eDO oraz komunikacja z Sigillum) jest odtwarzalna co do zasady, lecz wyższym kosztem: wymaga przechwycenia i odtworzenia protokołu sieciowego (zdjęcie przypinania certyfikatu na własnym urządzeniu, analiza ruchu), ewentualnie sprzętowego podsłuchu wymiany APDU z kartą. Jest to wykonalne metodą *logika + ruch + sniffing*, a znajomość kodu daje tu istotną przewagę. Nie przesądzam wykonalności końcowej, ponieważ nie zbadałem jeszcze, czy serwery PWPW nie wymuszają atestacji integralności klienta, co stanowi właściwe pytanie etapu dynamicznego. Odrębną barierą, tym razem nie techniczną, lecz prawną, pozostaje status podpisu kwalifikowanego: jego moc wynika z certyfikowanego procesu dostawcy usług zaufania (QTSP) i ważnego certyfikatu w rozumieniu eIDAS, a nie z samego klienta. Taka implementacja byłaby moim autorskim kodem, dystrybuowanym bez binariów PWPW, i pozostawałaby w domenie interoperacyjności, nie zaś obchodzenia zabezpieczeń.

## 23. Puenta

Etap uruchomieniowy na WSA potwierdził konieczność wyeliminowania niezgodności w pięciu obszarach: native bridge, biblioteka ICU, zachowanie trimmera kodu, obfuskacja klienta oraz błędy translatora Houdini przy obsłudze wektorowych optymalizacji SIMD w bibliotece standardowej .NET 9. Moduł Zygisk działa poprawnie w konfiguracji końcowej.

Przywrócenie prawidłowej semantyki parsera HTTP oraz precyzyjne wyłączenie wadliwych optymalizacji umożliwiło przeprowadzenie audytu bezpieczeństwa klienta: interfejsu NFC, stosowanych algorytmów kryptograficznych, protokołu komunikacji oraz magazynu danych.

Badania dynamiczne wymagające uprawnień root, obejmujące inspekcję ruchu TLS, analizę atestacji kluczy w Keystore oraz weryfikację reakcji serwera na zmodyfikowanego klienta, zostaną zrealizowane w dedykowanym środowisku laboratoryjnym.

---

*Badanie prowadzę na własnym urządzeniu wirtualnym, własnym koncie i własnym dokumencie. Opis dotyczy interoperacyjności i oceny bezpieczeństwa klienta. Nie zawiera danych osobowych ani PIN-u.*

### Add a comment

---

## Navigation

- Parent: [Analizy techniczne (PL)](http://kvc.pl/analizy-techniczne-pl.md)
- Previous: [mObywatel 4.87.2 — niezależny audyt bezpieczeństwa (analiza statyczna i dynamiczna)](http://kvc.pl/analizy-techniczne-pl/mobywatel-audyt-bezpieczenstwa.md)
