---
title: 'mObywatel 4.87.2 — niezależny audyt bezpieczeństwa (analiza statyczna i dynamiczna)'
url: 'https://kvc.pl/analizy-techniczne-pl/mobywatel-audyt-bezpieczenstwa'
markdown: 'https://kvc.pl/analizy-techniczne-pl/mobywatel-audyt-bezpieczenstwa.md'
date: '2026-09-16'
description: 'Niezależna analiza statyczna i dynamiczna produkcyjnego pakietu APK mObywatel 4.87.2 na fizycznym urządzeniu z Androidem 16. Weryfikacja podpisu, ochrony ekranu, odsyłaczy głębokich, CMS SignedData, PBKDF2/AES-CBC, PKCS#12, WebView TLS i bibliotek natywnych. Wyniki rozdzielają fakty statyczne, potwierdzenia wykonaniowe, obalone tezy i nierozstrzygnięte testy P0.'
---

> **W skrócie.** To niezależna, kontradyktoryjna analiza statyczna i dynamiczna dokładnie tej samej produkcyjnej wersji 4.87.2. Kod DEX i zasoby zestawiono z zachowaniem zalogowanej aplikacji na fizycznym urządzeniu Xiaomi z Androidem 16. Testy potwierdziły ochronę obrazu przez `FLAG_SECURE`, brak trybu `debuggable` oraz rzeczywistą lukę walidacji jawnego Intentu `institutions`: klient przyjął obcy schemat i host, po czym dopiero dalsza warstwa odrzuciła fikcyjny kod. Nie wykazano obejścia autoryzacji. Krytyczne hipotezy CMS, KDF, klucza UNIVERSITY, CT i starszego OAuth pozostają jawnie nierozstrzygnięte do chwili wykonania kontrolowanych testów wejściowych.

**NOWY DOWÓD DYNAMICZNY.** Produkcyjna aplikacja przyjęła jawny Intent `audit-invalid://untrusted.example/institutions` mimo nieprawidłowego schematu i hosta, a następnie przekazała fikcyjny `qrCode` do logiki weryfikacji. Serwerowa/domenowa warstwa odrzuciła wartość jako `WRONG_QR_CODE`. Potwierdza to błąd granicy wejściowej MOB-A-03, ale nie potwierdza przejęcia konta ani ominięcia autoryzacji.

# Niezależny audyt bezpieczeństwa mObywatel 4.87.2 dla Androida

4 / 10

 **Ocena ochrony tożsamości obywatela — 4/10 (dostateczny z minusem).** Aplikacja skutecznie chroni przed napastnikiem *zdalnym* (TLS 1.3, cert-pinning, Certificate Transparency), ale słabo chroni tożsamość przy *utracie fizycznej kontroli* nad urządzeniem: kontener mDowodu jest łamalny offline po zrzucie danych aplikacji — brak sprzętowego klucza, PBKDF2 poniżej zaleceń OWASP, a wszystkie składniki soli (w tym losowa `NSalt`) spoczywają jawnie na urządzeniu (24.7), a granica „zweryfikuj, zanim odczytasz" jest złamana po stronie klienta i serwera (MOB-A-05, 9.8). Ocena jest autorska i ważona; rozbicie poniżej, dowody w treści — plik po pliku. 

| Kategoria audytu | Waga | Ocena | Uzasadnienie inżynierskie |
|---|---|---|---|
| Bezpieczeństwo sieciowe (w locie) | 25% | **8,5/10** | TLS 1.3, cert-pinning, Certificate Transparency — dobra ochrona przed podsłuchem i MitM |
| Kryptografia tożsamości (w spoczynku) | 35% | **2,5/10** | Brak Keystore dla kontenera, wszystkie składniki soli jawne na urządzeniu, CBC bez AEAD, PIN łamalny offline w sekundy (24.7) |
| Higiena danych lokalnych i uprawnienia | 15% | **3,5/10** | Nieszyfrowany Room SQLite (dane aut i dokumentów), eksport do `Downloads/`, szeroki `FileProvider` |
| Architektura kluczy (Keystore / TEE) | 15% | **2,0/10** | Keystore obecny dla biometrii i Tink, lecz architektura klucza dla najcenniejszego zasobu — kontenera tożsamości — jest w pełni programowa; porażka tam, gdzie najważniejsze |
| Jakość kodu UI i dostępność (WCAG) | 10% | **4,0/10** | Wyłączony minimalny cel dotykowy WCAG, blokada TalkBacka, dług migracyjny M2/M3 |
| **Średnia ważona** | **100%** | **4,2/10** | **Ocena końcowa: 4/10 — dostateczny z minusem** |

**Werdykt.** Główny grzech to asymetria: nowoczesną ochronę sprzętową (Keystore) i biblioteki Google Tink zastosowano do błahych preferencji i odcisku palca, podczas gdy najcenniejszy zasób — cyfrowa tożsamość obywatela — spoczywa w przestarzałym formacie (`mDoki_0.10`) opartym o czysto programowe PBKDF2 bez sprzętowego klucza, z solą, której wszystkie składniki są odczytywalne na urządzeniu, podatnym na natychmiastowy atak offline po zrzucie danych. Ani 1/10 (bo transmisja i sesja są chronione poprawnie), ani 7/10 (bo serce systemu nie spełnia standardu, jakiego wymaga tożsamość) nie oddawałoby prawdy.

 Audyt w toku · 30 sierpnia – 12 września 2026

**Stan dokumentu:** raport z analizy statycznej i dynamicznej, wydanie 2 z 30 sierpnia 2026 r. — prace w toku. Sesje dynamiczne na fizycznym urządzeniu i w środowisku wirtualnym trwały po kilka godzin każda.

**Autor:** Marek Wesołowski (WESMAR), 2026

**Badany pakiet:** `pl.nask.mobywatel`, `4.87.2 (6426)`, versionCode `56416`

**Środowiska dynamiczne:** (1) Xiaomi 23078PND5G, Android 16 (API 36), produkcyjna instalacja zalogowanego użytkownika; (2) Windows Subsystem for Android `2407.40000.4.0` (Android 13, API 33; jądro `Linux 5.15.104-windows-subsystem-for-android`, x86_64) z Magiskiem/Zygisk, na Windows 11

**Cel:** niezależna weryfikacja techniczna artefaktu i jego zachowania wykonaniowego

**Zasada oceny:** ani domniemanie bezpieczeństwa, ani domniemanie winy; wnioski zależą od dowodów i jawnie określonego modelu zagrożeń.

 [ Pobierz narzędzie — mObywatel.zip](https://kvc.pl/analizy-techniczne-pl/mobywatel-audyt-bezpieczenstwa/mObywatel.zip) [ Zobacz nagranie na YouTube (3:29)](https://youtu.be/ois-NUd7Jyg)Zawiera `patch_mObywatel_universal.ps1`, `apktool.jar` oraz `audit.jks`. Do celów badawczych na własnym urządzeniu; neutralizuje wyłącznie klient-side detekcję środowiska (zob. 24.5). `audit.jks` to jednorazowy, samopodpisany klucz testowy (`CN=Audit`) — nic wrażliwego; służy tylko do spójnego podpisania paczki. Bez niego skrypt utworzy własny.

Trzy i pół minuty dowodu: państwowa aplikacja tożsamości uruchamia się na zwykłym komputerze (WSA, Windows 11) po usunięciu klient-side detekcji roota — jednym patchem, tym samym kluczem, bez utraty logowania. To nie „złamanie" serwera: nagranie pokazuje, że wykrywanie środowiska po stronie aplikacji nie jest granicą bezpieczeństwa. Pełny rozbiór techniczny w [sekcji 24.5](#nagranie-obejscie). 

---

## 1. Najpierw istotne ograniczenie materiału publicznego

Publicznie udostępniony materiał, który można przypisać projektowi Android, zawiera **192 pliki Kotlin**. Przegląd drzewa wykazał, że jest to przede wszystkim warstwa interfejsu i system komponentów wizualnych. W tym materiale nie znaleziono:

- plików Gradle ani `settings.gradle`;
- `AndroidManifest.xml`;
- implementacji warstwy sieciowej, walidacji TLS i mechanizmu przejrzystości certyfikatów (Certificate Transparency, CT);
- implementacji kryptografii, Android Keystore i przepływów uwierzytelniania;
- kompletnej konfiguracji produktu pozwalającej zbudować i odtworzyć oficjalny APK.

Oznacza to, że publiczny materiał nie wystarczał do przeprowadzenia audytu zachowania aplikacji produkcyjnej. Pozostałą część trzeba było **odtworzyć z kodu DEX i zasobów podpisanego APK**. Jest to uczciwe i użyteczne źródło informacji o wykonywanej binarce, ale nie jest odpowiednikiem pełnego kodu źródłowego: dekompilator traci nazwy, typy i część struktury sterowania, a niektóre funkcje Kotlin/coroutines rekonstruuje błędnie.

W konsekwencji:

- fakty z manifestu, zasobów, stałych i prostych ścieżek są zwykle wysoce pewne;
- złożone ścieżki sterowania były potwierdzane drugim trybem dekompilacji lub dowodem wykonaniowym;
- nie przypisano aplikacji podatności tylko dlatego, że niebezpieczna klasa występuje w pliku APK;
- nie uznano aplikacji za bezpieczną tylko dlatego, że ustawienie produkcyjne ma poprawną wartość domyślną.

Publikacja części interfejsu użytkownika może wspierać przejrzystość wzornictwa, lecz nie umożliwia niezależnej kontroli właściwości bezpieczeństwa produktu. Jest to ograniczenie audytowalności, a nie samo w sobie dowód podatności.

Zamknięcie pozostałej części implementacji nie jest kontrolą bezpieczeństwa. Uniemożliwia ono zewnętrznym badaczom sprawdzenie założeń, odtworzenie kompilacji i szybkie wykrywanie regresji. Równocześnie sama niejawność kodu nie stanowi dowodu, że znajduje się w nim konkretna podatność — taki zarzut nadal wymaga śladu technicznego. W niniejszej pracy brak przejrzystości traktowany jest więc jako **obniżenie poziomu niezależnego zapewnienia**, a nie jako zamiennik analizy.

---

## 2. Streszczenie wykonawcze

W obiegowej ocenie tego artefaktu pojawia się kilka realnie niebezpiecznych konstrukcji obecnych w APK; częściej jednak przypisuje się im wpływ i dostępność, których kod nie potwierdza. Najczęstsze nadinterpretacje to:

1. utożsamienie wyjątku HTTP dla `gstatic.com` z możliwością podmiany fontów, JavaScriptu i dowolnych zasobów;
2. uznanie prywatnej flagi aplikacji i nieosiągalnego w produkcji ekranu deweloperskiego za zdalnie dostępny mechanizm wyłączenia TLS;
3. błędna interpretacja `ksRandomizer` jako losowania aliasu klucza;
4. zalecenie `android:autoVerify` dla własnego schematu URI, mimo że weryfikacja Android App Links dotyczy adresów HTTP/HTTPS;
5. potraktowanie obecności bibliotek AndroidX/Tink jako dowodu, że chronią one konkretną bazę i krytyczne dane aplikacji.

Jednocześnie zwykle pomijana jest rzeczywista słabość mechanizmu przejrzystości certyfikatów (CT):

> Lista dzienników CT jest pobierana po HTTP i wprawdzie poprawnie weryfikowana podpisem Google, ale aplikacja nie egzekwuje jej świeżości, wersji, stanu dziennika ani przedziału czasowego. Aktywny napastnik sieciowy może więc ponownie podawać starszą, nadal poprawnie podpisaną listę.

Wpływ jest warunkowy. Samo ponowne podanie starszej listy nie przełamuje TLS i nie pozwala wstrzyknąć dowolnej treści. Może jednak osłabić dodatkową kontrolę CT w scenariuszu, w którym napastnik dysponuje już certyfikatem akceptowanym przez system oraz SCT pochodzącym z dziennika, któremu aplikacja nie powinna już ufać. Oceniono to jako **ryzyko średnie dotyczące obrony warstwowej**; nie jest to samodzielne przejęcie sesji.

Poza tym potwierdzono:

- oficjalny certyfikat podpisujący `base.apk`, zgodny z odciskiem opublikowanym przez domenę `mobywatel.gov.pl`;
- bezpieczne wartości domyślne TLS, przypinania certyfikatów, CT, obsługi SSL w WebView i `FLAG_SECURE` w wariancie produkcyjnym;
- niebezpieczne ścieżki `TrustAll`, permisywny `HostnameVerifier` oraz `SslErrorHandler.proceed()` pozostawione w produkcyjnym APK, lecz bez wykazanego przełącznika dostępnego zwykłej aplikacji trzeciej, użytkownikowi lub napastnikowi sieciowemu;
- starszy wariant autoryzacji OAuth e-Doręczeń, wybierany serwerową flagą funkcjonalną, który nie generuje ani nie porównuje `state` i wymienia kod z `code_verifier=null`; równolegle obecna nowsza implementacja stosuje losowy `state` oraz PKCE S256, co stanowi zarówno dobry wzorzec, jak i dowód realnej luki regresyjnej między wariantami;
- na Androidzie 8–9 nazwa załącznika z serwerowego `Content-Disposition` może zostać włączona bezpośrednio do ścieżki w pamięci współdzielonej, bez normalizacji i kontroli pozostania w katalogu `Downloads`; daje to warunkowy zapis nowego pliku poza katalogiem docelowym, ale kod nie nadpisuje pliku już istniejącego, a rzeczywistą kontrolę nazwy przez innego użytkownika należy potwierdzić testem;
- poprawne odrzucanie listy CT o błędnym podpisie i zachowanie ostatniej znanej poprawnej listy;
- rozsądne zabezpieczenia manifestu: wyłączone kopie danych, Safe Browsing, prywatny `FileProvider` i uprawnienia systemowe na eksportowanych komponentach bibliotek;
- produkcyjne włączenie `FLAG_SECURE`, potwierdzone statycznie i dynamicznie na Androidzie 16; regresja na Androidzie 8 i 14 pozostaje do wykonania;
- dynamiczne działanie `FLAG_SECURE` na Androidzie 16: czarny zrzut aktywnej aplikacji, pusty podgląd jej karty w ostatnich aplikacjach i brak treści w próbie nagrania ekranu;
- przyjęcie przez eksportowaną `MainActivity` jawnego Intentu `institutions` z obcym schematem i hostem; fikcyjny kod został przekazany do weryfikacji, lecz poprawnie odrzucony jako `WRONG_QR_CODE`;
- ściślejszą obsługę `eqsig`: obcy schemat nie uruchomił parsera, natomiast fikcyjny token w poprawnym `mobywatel://eqsig` został bezpiecznie odrzucony na dekodowaniu Base64.

Inwentaryzacja łańcucha dostaw ujawniła 324 zadeklarowane moduły Maven, w tym 28 artefaktów oznaczonych `alpha` lub `beta`. Trzy spakowane moduły Bouncy Castle 1.83 są objęte znanymi poprawkami bezpieczeństwa; automatyczne zapytanie OSV wskazało trzy komunikaty, a ręczna kontrola not projektu źródłowego ujawniła dalsze pozycje pominięte przez to źródło danych. Poprawiona wersja 1.84 była dostępna od 28 kwietnia 2026 r., czyli około trzy miesiące przed badanym wydaniem. Aplikacja rzeczywiście rejestruje dostawcę kryptograficznego BC i używa CMS, PKCS#12 oraz ASN.1, więc nie jest to martwa zależność.

Nie znaleziono wywołania konkretnych mechanizmów GOST, LDAP, Composite Signature ani FrodoKEM poza kodem biblioteki. Jest to **potwierdzony dług aktualizacyjny i potencjalna powierzchnia podatności, ale nie dowód osiągalnej eskalacji tymi czterema CVE**. Znaleziono natomiast odrębny wzorzec aplikacyjny: klient tworzy i zapisuje obiekty `CMSSignedData`, a następnie odczytuje `getSignedContent()` bez znalezionej lokalnej weryfikacji podpisujących. Brak obejmuje więc zidentyfikowane punkty przyjęcia i wykorzystania danych, a nie tylko ekran prezentacji. Wpływ zależy od tego, czy napastnik może dostarczyć lub zmienić kontener, ale hipoteza wymaga testu P0, ponieważ treść zasila modele dokumentów.

Ocena procesu inżynierskiego jest krytyczna. W produkcie przetwarzającym państwową tożsamość nie powinno być konieczne tłumaczenie, dlaczego kod `TrustAll` nie może trafić do wydania produkcyjnego, dlaczego aktywnie używany dostawca kryptograficzny z opublikowaną od kwietnia aktualizacją nie powinien pozostać w lipcowym wydaniu ani dlaczego `SignedData` musi być zweryfikowane przed użyciem treści. Każdy z tych problemów osobno może mieć warunkowy wpływ; razem świadczą o braku wystarczająco twardych bram jakości artefaktu. Publiczne udostępnienie wyłącznie warstwy interfejsu użytkownika dodatkowo uniemożliwia społeczną weryfikację, czy bramy takie istnieją i działają.

Nie ma podstaw do oceny, ile środków finansowych zostało zmarnowanych ani kto personalnie odpowiada za decyzje — badany materiał nie zawiera budżetów, umów, historii przeglądów ani wyjątków ryzyka. Można natomiast uczciwie stwierdzić, że rezultat techniczny i zakres publikacji nie odpowiadają poziomowi audytowalności oczekiwanemu od krytycznej aplikacji publicznej.

**Nie ma podstaw do ogólnego komunikatu „aplikacja jest bezpieczna”.** Wynik jest migawką analizy statycznej i rozpoczętej analizy dynamicznej wersji 4.87.2. Część tez została rozstrzygnięta na fizycznym urządzeniu, lecz najważniejsze testy CMS, KDF, klucza UNIVERSITY, CT i starszego OAuth nadal wymagają kontrolowanych danych lub warstwy sieciowej opisanych w sekcji 12.

---

## 3. Identyfikacja i pochodzenie badanego artefaktu

| Właściwość | Wynik |
|---|---|
| Archiwum | `mObywatel.apkm` |
| SHA-256 archiwum | `534df84d382f05a28a0f1fb6ff48950a1e182e3f126644e495ce945f873b1078` |
| Badana binarka | `decompiling/base.apk` |
| SHA-256 `base.apk` | `1ede2b031ffb7e5036308dda8ad08c98ccd0d30316c3336cbf7b93caf86ecb72` |
| Pakiet | `pl.nask.mobywatel` |
| Wersja | `4.87.2 (6426)` |
| VersionCode | `56416` |
| Min / target / compile API | 26 / 36 / 36 |
| Data wpisana w metadanych APKM | 29 lipca 2026 r. |

Weryfikator Android `apksig 9.3.2` potwierdził integralność `base.apk`:

```text
verified = true
v1 = false
v2 = true
v3 = true
v4 = false
```

Certyfikat podpisujący:

```text
Subject/Issuer: CN=Android, OU=Android, O=Google Inc.,
                L=Mountain View, ST=California, C=US
SHA-256: CB:4F:1E:A4:F0:BE:4A:91:EA:80:34:97:DD:A6:9F:31:
         84:5C:9A:98:F4:39:03:C5:60:17:CF:4A:B8:77:D8:9F
```

Sam ogólny podmiot `O=Google Inc.` nie dowodzi autentyczności aplikacji. Rozstrzygający jest fakt, że identyczny odcisk dla pakietu `pl.nask.mobywatel` publikuje oficjalny plik [Digital Asset Links](https://api.mobywatel.gov.pl/.well-known/assetlinks.json). Potwierdza to użycie klucza autoryzowanego przez domenę mObywatel. Nie dowodzi natomiast, że plik o podanym hashu jest obecnie identyczny z egzemplarzem pobieranym z każdego kanału dystrybucji; do takiego twierdzenia potrzebne byłoby bezpośrednie porównanie artefaktów.

---

## 4. Metoda, skala pewności i model zagrożeń

### 4.1. Zakres wykonany

- inwentaryzacja APKM i metadanych manifestu;
- kryptograficzna weryfikacja podpisu APK i powiązania z oficjalną domeną;
- dekompilacja DEX i zasobów za pomocą JADX;
- ponowna dekompilacja problematycznej funkcji CT w trybie zapasowym (`fallback`);
- analiza konfiguracji sieci, TLS, przypinania certyfikatów, CT, WebView, obsługi odnośników kierujących do aplikacji, magazynu kluczy, logowania, komponentów eksportowanych, kopii danych i ścieżek zapisu plików;
- pobranie bieżącej listy CT, weryfikacja jej podpisu kluczem osadzonym w aplikacji i analiza schematu danych;
- regresyjna kontrola publicznie opisanego CVE-2025-11598;
- testy na fizycznym urządzeniu z Androidem 16: identyfikacja zainstalowanego pakietu, pobranie split APK, ponowna weryfikacja podpisu i skrótów, `run-as`, zrzut ekranu, ostatnie aplikacje, `screenrecord`, logi procesu oraz statystyki sieciowe UID;
- kontradyktoryjne testy jawnych Intentów skierowanych do eksportowanej `MainActivity` dla ścieżek `institutions` i `eqsig`, z jednoznacznie fikcyjnymi znacznikami audytowymi;
- weryfikacja każdej materialnej tezy o bezpieczeństwie aplikacji.

### 4.2. Poza zakresem tego audytu

- testy na emulatorach Android 8, 9, 14 oraz na drugim urządzeniu;
- kontrolowane logowanie w dwóch równoległych sesjach i pełna ocena autoryzacji po stronie serwera;
- testowanie mutacyjne parserów i interfejsów Binder/JNI;
- analiza infrastruktury serwerowej i procedur operacyjnych;
- pełny przegląd bibliotek natywnych;
- porównanie ze źródłami, które nie zostały jeszcze udostępnione audytorowi.

### 4.3. Znaczenie ocen

| Oznaczenie | Znaczenie |
|---|---|
| **Potwierdzone** | Zachowanie wynika bezpośrednio z artefaktu lub zostało odtworzone eksperymentalnie. |
| **Częściowo potwierdzone** | Mechanizm istnieje, ale pełny wpływ wymaga warunku lub testu dynamicznego. |
| **Nieudowodnione** | Hipoteza jest możliwa, ale obecny materiał nie wykazuje ścieżki wykorzystania. |
| **Obalone** | Kod lub eksperyment przeczy tezie. |

Ocena zakłada przede wszystkim zwykłego napastnika sieciowego lub złośliwą aplikację bez uprawnień administratora systemu (`root`). Uzyskanie takich uprawnień, instrumentacja Frida, modyfikacja prywatnego katalogu aplikacji i przepakowanie APK są omawiane osobno, ponieważ napastnik o takich możliwościach może bezpośrednio przechwytywać dane procesu i omijać kontrolę TLS bez używania flag aplikacji.

### 4.4. Granice dowodu i zaufania

flowchart LR PUB[Publiczne 192 pliki Kotlin
głównie interfejs użytkownika]:::limited APK[Podpisany base.apk
kod DEX + zasoby]:::evidence DOMAIN[Oficjalny assetlinks.json
odcisk certyfikatu]:::evidence DECOMP[Dekompilacja JADX
tryb standardowy + zapasowy]:::method LIVE[Zasoby zdalne
CT / źródła publiczne]:::method REPORT[Wnioski audytu
z poziomem pewności]:::result MISSING[Brak pełnych źródeł,
zaplecza serwerowego i śladu wykonania]:::gap PUB --> REPORT APK --> DECOMP --> REPORT DOMAIN --> REPORT LIVE --> REPORT MISSING -. ogranicza .-> REPORT classDef evidence fill:#d9f2e6,stroke:#16784b,color:#102a20; classDef method fill:#e2ecff,stroke:#345da8,color:#12213d; classDef result fill:#fff1c7,stroke:#a66a00,color:#342300; classDef limited fill:#f2e8ff,stroke:#7651a8,color:#261b36; classDef gap fill:#ffe0e0,stroke:#ad3030,color:#3d1111; 

---

## 5. Zestawienie ustaleń

| ID | Ustalenie | Ocena | Pewność |
|---|---|---|---|
| MOB-A-01 | Możliwość podstawienia starszej, prawidłowo podpisanej listy dzienników CT pobieranej po HTTP | **Średnie** | Potwierdzone zachowanie; wpływ warunkowy |
| MOB-A-02 | Ścieżki akceptacji połączenia mimo błędu TLS lub WebView pozostawione w binarce produkcyjnej | **Niskie** | Potwierdzone istnienie; brak zwykłej ścieżki aktywacji |
| MOB-A-03 | Własne schematy URI przenoszące tokeny: brak pełnej walidacji jawnego Intentu `institutions` | **Niskie, wpływ warunkowy** | **Potwierdzone statycznie i dynamicznie; fikcyjny kod odrzucony przez dalszą warstwę** |
| MOB-A-04 | Aktywnie używany Bouncy Castle 1.83 mimo wcześniejszej dostępności 1.84; źródło danych OSV nie obejmuje pełnej listy projektu źródłowego | **Średnie operacyjne / P0** | Obecność i użycie potwierdzone; wpływ poszczególnych CVE wymaga potwierdzenia osiągalności kodu |
| MOB-A-05 | Przyjęcie, zapis i odczyt `CMSSignedData` bez znalezionej lokalnej weryfikacji podpisujących | **Średnie / P0 do testu** | Zachowanie potwierdzone; wektor dostarczenia warunkowy |
| MOB-A-06 | Globalna, stała ośmiobajtowa sól PBKDF2 dla pakietów aktywacyjnych legitymacji studenckiej | **Niskie–Średnie / P0 do charakterystyki** | Stała sól i parametry KDF potwierdzone; entropia kodu i wykonalność ataku poza aplikacją wymagają testu |
| MOB-A-08 | Eksportowalny klucz prywatny certyfikatu UNIVERSITY i hasło PKCS#12 współlokowane w pakiecie aktywacyjnym; zapis nieopakowanego PKCS#8 w SQLCipher | **Średnie, warunkowo Wysokie po P0** | Cykl życia klucza potwierdzony; odzyskanie poza aplikacją i skutki użycia klucza wymagają testu |
| MOB-A-09 | Starszy wariant OAuth e-Doręczeń bez `state` i PKCE, pozostawiony jako ścieżka wybierana flagą serwerową | **Średnie / warunkowe** | Brak obu zabezpieczeń potwierdzony; aktywny wariant i praktyczne wstrzyknięcie kodu wymagają P0 |
| MOB-A-10 | Nazwa załącznika z `Content-Disposition` używana bez kanonikalizacji w bezpośrednio budowanej ścieżce zapisu na API 26–28 | **Niskie–Średnie / P1 do testu** | Łańcuch klienta potwierdzony; kontrola nazwy przez atakującego i wpływ serwera warunkowe |
| MOB-O-01 | Zdalne logowanie pełnego URL dla części profili HTTP może ujawnić parametry zapytania | Obserwacja | Nieudowodniony wrażliwy przepływ |
| MOB-O-02 | `FileProvider` obejmuje całą pamięć zewnętrzną, choć nie jest eksportowany | Utwardzenie | Potwierdzone, bez wykazanego wycieku |
| MOB-O-03 | Rozbudowany zakres tagów urządzenia i zdarzeń bezpieczeństwa wysyłanych do Sentry | Prywatność / do testu | Zakres konfiguracji potwierdzony |
| MOB-O-04 | Brak znalezionej ochrony aplikacyjnej przed pełnym i częściowym zasłonięciem ekranów operacji wrażliwych | Utwardzenie / do testu | Brak mechanizmów potwierdzony statycznie; skuteczna akcja przez nakładkę nieudowodniona |
| MOB-O-05 | Wyprowadzanie ośmiobajtowej wartości wiążącej kontener ze starszego magazynu z numeru IMEI urządzenia | Dług techniczny / kod archiwalny | Implementacja potwierdzona; ścieżka zależna od flagi i niedostępna od Androida 10 |

Nie nadano osobnych identyfikatorów dla rzekomego wstrzykiwania czcionek/JavaScriptu, zdalnej zmiany flagi TLS, „losowania aliasu” przez `ksRandomizer` ani programowego przejścia na słabszy wariant bez StrongBox jako CWE-326, ponieważ te tezy są odpowiednio obalone lub nieudowodnione.

Identyfikator **MOB-A-07** obejmuje ścieżkę podpisu kwalifikowanego `mobywatel://eqsig`. Test dynamiczny (24.2) potwierdził, że klient przyjmuje ten deep link od dowolnej aplikacji bez walidacji nadawcy i — dla poprawnego Base64 — przekazuje token wprost do produkcyjnego API (`.../qualified-signature-process/start-authentication`), które odrzuca sfałszowaną wartość (`400 INVALID_DEEPLINK`). Potwierdza to tę samą klasę otwartej granicy wejściowej co MOB-A-03; nie wykazano obejścia autoryzacji — obrona spoczywa wyłącznie po stronie serwera.

Potencjalne ustalenie formułowane jako „WebView akceptuje połączenie mimo błędu TLS i zezwala na dostęp do plików” zostało scalone z wcześniejszym **MOB-A-02**, ponieważ opisuje tę samą klasę granicy WebView i nie powinno sztucznie zwiększać liczby wyników. Identyfikator MOB-A-09 w niniejszym raporcie oznacza niezależną wadę transakcyjnego wiązania OAuth opisaną w sekcji 8.2.

Ustalenia dotyczące jakości i architektury opublikowanego kodu interfejsu prowadzone są w odrębnym ciągu **MOB-Q-xx** i zebrane w sekcji 20.3. Rozdział ten jest celowy: tamte ustalenia opisują rzemiosło programistyczne warstwy pozbawionej mechanizmów bezpieczeństwa i pozostają poza klasyfikacją podatności.

---

## 6. MOB-A-01 — możliwość podstawienia starszej, prawidłowo podpisanej listy dzienników CT

**Ocena:** średnia, mechanizm obrony w głąb
**Atakujący:** aktywny napastnik na ścieżce sieciowej
**Warunki wpływu:** dodatkowo certyfikat akceptowany przez system oraz SCT z dziennika, który po wydaniu starszej listy utracił zaufanie albo zmienił dopuszczalny przedział
**Pewność:** potwierdzona akceptacja starszej listy; wpływ eksploatacyjny zależny od PKI i historii dzienników

### 6.1. Dowód

Konfiguracja zezwala na HTTP do `gstatic.com`:

```xml
<domain-config cleartextTrafficPermitted="true">
    <domain includeSubdomains="true">gstatic.com</domain>
</domain-config>
```

Dowód: `network_security_config.xml`.

Jedyny aktywny literał HTTP do tej domeny znaleziony w zasobach aplikacji to:

```text
http://www.gstatic.com/ct/log_list/v3/log_list.zip
```

Dowód: `strings.xml`.

Kod pobiera ZIP profilem `NonCTRaw`, wyodrębnia `log_list.json` i `log_list.sig`, ograniczając ich wielkość odpowiednio do 1 MiB i 512 B. Następnie weryfikuje podpis kluczem publicznym osadzonym w APK. Dowody: `iy3/c.java`, `iy3/c.java`, `iy3/c.java`.

Pierwsza standardowa dekompilacja funkcji weryfikującej była błędna i sugerowała akceptację nieważnego podpisu. Dekompilacja tej samej klasy w trybie zapasowym (`fallback`) wykazała przeciwnie:

- brak klucza publicznego → odrzucenie;
- błędny podpis → odrzucenie;
- poprawny podpis → akceptacja.

Bieżący ZIP pobrany podczas badania miał SHA-256:

```text
164e3b2368fcb84763bbb6fb4c3839d53d69c2ca469515365bd0e762613b50ce
```

Weryfikacja `log_list.sig` kluczem z APK dała `Verified OK`. To obala hipotezę, że napastnik może podmienić listę dowolną treścią.

Problem dotyczy świeżości. DTO zawiera pola `version` i `log_list_timestamp`, ale w badanej implementacji nie znaleziono ich użycia do kontroli monotoniczności ani maksymalnego wieku. Dowód: `LogListDto.java`. Model pojedynczego dziennika pomija ponadto pola bieżącego formatu, takie jak stan dziennika i przedział czasowy; przechowuje głównie identyfikator i klucz. Dowód: `LogDto.java`.

Po poprawnej weryfikacji aplikacja zapisuje lokalny czas pobrania jako `cacheTime`. W efekcie historyczny, lecz prawidłowo podpisany dokument dostarczony ponownie po HTTP wygląda lokalnie jak świeżo pobrany. Dowód: `iy3/c.java`.

### 6.2. Przepływ

sequenceDiagram autonumber participant G as Google / gstatic participant A as Archiwum napastnika participant N as Aktywny MITM HTTP participant M as mObywatel 4.87.2 participant C as Pamięć podręczna listy CT G->>A: Starszy log_list.zip
JSON + prawidłowy podpis A->>N: Zachowany historyczny artefakt N->>M: Ponowne podanie starszego ZIP przez HTTP M->>M: Weryfikacja podpisu = poprawna M->>M: Kontrola znacznika czasu = brak M->>M: Kontrola cofnięcia wersji = brak M->>M: Kontrola stanu / przedziału czasowego = brak M->>C: Zapis listy i lokalnego czasu pobrania Note over M,C: Integralność zachowana,
świeżość nieudowodniona 

### 6.3. Czego ten błąd nie oznacza

- Nie pozwala podpisać własnej listy dzienników CT.
- Nie pozwala podmieniać czcionek, JavaScriptu ani innych zasobów aplikacji; nie znaleziono takiego odbiorcy dla tego wyjątku HTTP.
- Nie omija samodzielnie systemowej walidacji łańcucha certyfikatu.
- Nie jest dowodem ujawnienia informacji wrażliwych przez HTTP — pobierana lista jest publiczna.

### 6.4. Zalecenie

1. Zmienić punkt końcowy na HTTPS i usunąć zezwolenie na ruch nieszyfrowany dla całej domeny `gstatic.com`.
2. Zachować podpis aplikacyjny jako niezależną warstwę ochrony.
3. Egzekwować maksymalny wiek `log_list_timestamp` oraz monotoniczny wzrost `version`.
4. Odczytywać i egzekwować stan dziennika oraz jego `temporal_interval` względem czasu SCT/certyfikatu.
5. Dopuszczać ostatnią znaną poprawną listę tylko przez jawnie ustalony okres awaryjny.
6. Dodać testy: ponowne podanie starszej podpisanej wersji, wersja cofnięta, znacznik czasu z przyszłości, dziennik wycofany, błędny podpis i uszkodzony ZIP.

---

## 7. MOB-A-02 — niebezpieczne ścieżki TLS/WebView w produkcyjnym APK

**Ocena:** niska jako higiena i odporność produktu
**Pewność:** istnienie kodu potwierdzone; dostępność dla zwykłego napastnika niepotwierdzona i w standardowej konfiguracji zaprzeczona

### 7.1. Co rzeczywiście znajduje się w APK

Konfiguracja klienta HTTP pobiera podczas działania flagę `HTTP_CLIENT_SSL`. Jeżeli jest fałszywa, fabryka ustawia permisywny `HostnameVerifier` i `TrustAllX509TrustManager`. Dowód: `pl/gov/coi/common/network/o.java`.

Wspólny `WebViewClient` wywołuje `SslErrorHandler.proceed()`, gdy odpowiadająca mu konfiguracja SSL jest wyłączona. Dowód: `t80/h.java`.

Ten klient jest wspólny dla wielu ekranów. Ustawia parametry dokładnie z obiektu konfiguracji: dostęp do plików, JavaScript, pamięć DOM, treść mieszaną i pamięć podręczną: `t80/l.java`. Domyślna klasa bazowa ma restrykcyjne wartości: JavaScript `false`, pamięć DOM `false`, `allowFileAccess=false` oraz `mixedContentMode=1`, czyli `MIXED_CONTENT_NEVER_ALLOW`: `t80/p.java`. Konkretne ekrany świadomie rozszerzają te uprawnienia.

| Rodzina WebView | SSL | JavaScript | Pamięć DOM | Dostęp do plików | Treść mieszana | Ocena statyczna |
|---|---|---|---|---|---|---|
| płatności i zapis karty (`e32`, `mt3`, `s32`) | na stałe `true` | `true` | domyślnie `false` | `false` | `NEVER_ALLOW` | poprawne odrzucenie przy błędzie |
| podglądy i proste treści (`qu3`) | na stałe `true` | `true` | `true` | `false` | `NEVER_ALLOW` | rozsądne, bez mostu Java |
| OAuth e-Doręczeń / Keycloak (`a22`, `ys3`) | lokalna flaga `WEB_VIEW_SSL` | `true` | `true` | `false` | `NEVER_ALLOW` | bezpieczne przy produkcyjnym `true`; krytyczny skutek błędnego wariantu |
| usługi online (`f43`) | lokalna flaga `WEB_VIEW_SSL` | `true` | `true` | **`true`** | `NEVER_ALLOW` | zbędne rozszerzenie powierzchni; nie wykazano samodzielnego odczytu pliku |

Ścieżka usług internetowych pobiera zdalny URL, obsługuje POST, pobieranie plików oraz systemowy wybór pliku, a konfiguracja ustawia `allowFileAccess=true`: `f43/b.java`, `e43/u.java`. Samo to ustawienie nie przełamuje zasady wspólnego pochodzenia i w tym materiale nie wykazuje odczytu lokalnego pliku przez zdalną stronę. Jest jednak niepotrzebnym uprawnieniem dla komponentu przetwarzającego treść z sieci. Należy je wyłączyć i testować nawigacje `file://`, `content://`, przekierowania oraz systemowy wybór pliku.

Wspólny klient buduje komunikaty diagnostyczne z pełnego URL i po `onPageFinished` pobiera całe `document.body.outerHTML`: `t80/h.java`. W badanym wariancie produkcyjnym nie jest to potwierdzony wyciek: wywołania trafiają na poziom diagnostyczny do `q00.a`, którego metody `debug`/`info` odrzucają treść, a dopiero błąd jest przekazywany do Sentry: `q00/a.java`. To pozytywny wynik dla badanego odbiornika logów, ale kod powinien przestać konstruować pełny DOM i URL, ponieważ podmiana implementacji mechanizmu logowania w innym wariancie natychmiast zmieni tę właściwość bezpieczeństwa.

Przypinanie certyfikatów jest także zależne od flag, a profil `NonCTRaw` ma dodatkową ścieżkę akceptacji mimo błędu: jeśli nie uda się utworzyć oczekiwanego menedżera zaufania, używa wariantu permisywnego. Dotyczy to danych podpisanych na poziomie aplikacji, m.in. listy CT; nie należy automatycznie przenosić wpływu na główne API.

### 7.2. Dlaczego to ryzyko bywa zawyżane

Wartości produkcyjne są bezpieczne:

```text
HTTP_CLIENT_SSL          = true
CERTIFICATE_TRANSPARENCY = true
HTTP_CLIENT_CERT_PINNING = true
WEB_VIEW_SSL             = true
WEB_VIEW_DEBUGGING       = false
```

Dowód wartości domyślnych: `a04/c.java`.

Lokalne nadpisania trafiają do prywatnych `SharedPreferences`. Manifest wyłącza backup, a reguły ekstrakcji wykluczają dane. Zwykła aplikacja trzecia i napastnik sieciowy nie mogą zapisać tego pliku.

Repozytorium flag składa wartości domyślne z lokalnymi wpisami i nie pobiera `WEB_VIEW_SSL` z listy flag serwerowych: `e04/b.java`. Rozgałęzienie `f04.e` ma osobne wyjątki dla `NURSE_CARD`, ale dla `WEB_VIEW_SSL` po sprawdzeniu profilu audytowego/testowego odczytuje właśnie lokalne repozytorium. Co istotne, profile `isSecurityAudit` i `isAutomaticTest` wymuszają dla tej flagi `false`; jest to celowe ułatwienie testów MITM, a nie zdalny przełącznik produkcji. Taki wyjątek nadal powinien istnieć wyłącznie w osobnym artefakcie testowym.

Kod ekranu deweloperskiego istotnie jest skompilowany, ale produkcyjny graf zależności wstrzykuje implementację modułu zarządzającego, której `isAvailable()` zwraca `false` i która nie wykonuje nawigacji. Główna aktywność nie eksportuje osobnego ekranu deweloperskiego. Sama obecność `oq1.m` nie dowodzi osiągalności interfejsu.

flowchart TD NET[Atakujący sieciowy]:::ordinary APP[Zwykła aplikacja trzecia]:::ordinary USER[Użytkownik interfejsu produkcyjnego]:::ordinary ROOT[Uprawnienia root / Frida
przepakowany APK]:::priv DEV[Moduł funkcji deweloperskich
produkcja: isAvailable = false]:::guard PREF[Prywatne SharedPreferences
HTTP_CLIENT_SSL]:::state DEC{sslEnabled?} SAFE[Systemowy TrustManager
przypinanie certyfikatów / CT]:::safe UNSAFE[TrustAll + permisywna nazwa hosta
przypinanie wyłączone]:::danger NET -. brak zapisu .-> PREF APP -. izolacja Androida .-> PREF USER -. DEV .-> DEV DEV -. niedostępny .-> PREF ROOT --> PREF PREF --> DEC DEC -->|true, domyślnie w produkcji| SAFE DEC -->|false| UNSAFE classDef ordinary fill:#e2ecff,stroke:#345da8,color:#12213d; classDef priv fill:#ffe0e0,stroke:#ad3030,color:#3d1111; classDef guard fill:#f0f0f0,stroke:#666,color:#222; classDef state fill:#fff1c7,stroke:#a66a00,color:#342300; classDef safe fill:#d9f2e6,stroke:#16784b,color:#102a20; classDef danger fill:#ffc7c7,stroke:#b00020,color:#3b0008; 

Stąd klasyfikacja **Wysokie/CWE-295 jako zdalnie sterowalnego obejścia zabezpieczeń** jest nieuzasadniona. Konstrukcja nadal jest zła z perspektywy obrony warstwowej: błąd wariantu, błędne wstrzyknięcie zależności lub przyszła regresja może aktywować kod akceptujący połączenie mimo błędu.

### 7.3. Zalecenie

- usunąć klasy `TrustAll`, permisywny `HostnameVerifier` i `SslErrorHandler.proceed()` z wariantu produkcyjnego przez rozdzielenie zestawów źródeł;
- dla hostów produkcyjnych bezwzględnie odrzucać połączenie przy błędzie: wyłączenie TLS powinno kończyć inicjalizację niepowodzeniem;
- nie utrzymywać lokalnie nadpisywalnych flag bezpieczeństwa w wariancie produkcyjnym;
- ustawić `allowFileAccess=false` we wszystkich WebView sieciowych oraz dodać jawne listy dozwolonych schematów i hostów dla nawigacji uprzywilejowanych;
- usunąć pobieranie pełnego DOM i usuwać parametry zapytania oraz fragment adresu przed utworzeniem komunikatu logowania, nawet jeżeli obecny produkcyjny odbiornik odrzuca poziom diagnostyczny;
- dodać test artefaktu wydania, który wykrywa obecność sygnatur klas TrustAll i wywołania `SslErrorHandler.proceed()`;
- przetestować końcowy podpisany pakiet, a nie tylko wariant pośredni z ciągłej integracji (CI).

---

## 8. MOB-A-03 — własne schematy URI i tokeny

**POTWIERDZONE STATYCZNIE I DYNAMICZNIE.** Eksportowaną `MainActivity` uruchomiono jawnym Intentem zawierającym `audit-invalid://untrusted.example/institutions?qrCode=AUDIT_INVALID_7fcb7d9c`. Aplikacja nie odrzuciła obcego schematu ani hosta i przekazała znacznik do przebiegu weryfikacji. Dalsza warstwa odrzuciła go komunikatem `WRONG_QR_CODE`. Luka walidacji klienta jest faktem; obejście autoryzacji, przejęcie sesji i akceptacja nieprawidłowego kodu nie zostały wykazane.

### 8.1. Eksportowana aktywność i walidacja URI

Manifest eksportuje `MainActivity` dla:

- `mobywatel://app/institutions`;
- `mobywatel://eqsig`;
- zweryfikowanego `https://api.mobywatel.gov.pl/application/diia_confirmed`.

Dowód: `AndroidManifest.xml`.

Procedura obsługi `institutions` odczytuje parametr `qrCode`. Wewnątrz aplikacji sprawdza akcję `VIEW`, obecność URI i wyłącznie to, czy ostatni segment ścieżki jest równy `institutions`; **nie porównuje ponownie schematu ani hosta**: `bk3/e.java`. Filtr manifestu ogranicza zwykłe rozpoznanie niejawnego Intentu do `mobywatel://app/institutions`, lecz dowolna aplikacja może skierować jawny Intent do eksportowanej `MainActivity` z innym schematem lub hostem i pasującym ostatnim segmentem. To nie dowodzi przejęcia procesu: `qrCode` uruchamia przebieg weryfikacji/logowania i nie znaleziono lokalnego pominięcia autoryzacji. Jest jednak konkretną luką walidacji granicy wejściowej, którą trzeba zamknąć przed analizą jednorazowości tokenu po stronie serwera.

Procedura obsługi podpisu kwalifikowanego jest pod tym względem lepsza: sprawdza oczekiwany schemat i host, odczytuje `token` i rozpoczyna proces potwierdzenia tożsamości. Nie wykazano, aby samo dostarczenie wartości omijało logowanie, zgodę użytkownika lub walidację po stronie serwera.

Ryzyka własnego schematu są dwa:

1. inna aplikacja może zarejestrować ten sam schemat i próbować przechwycić odnośnik wychodzący;
2. inna aplikacja może jawnie uruchomić eksportowaną `MainActivity` z przygotowanym URI.

`android:autoVerify` nie rozwiązuje tego dla `mobywatel://`, ponieważ mechanizm zweryfikowanych linków aplikacyjnych Android App Links obejmuje domeny HTTP/HTTPS. Bywa więc zalecany środek technicznie niedopasowany.

Zalecenie: przenieść przebiegi zawierające tokeny do zweryfikowanych linków HTTPS, stosować token jednorazowy z krótkim czasem ważności (TTL), wiązaniem z odbiorcą, stanem i sesją (`audience/state/session`) oraz walidacją po stronie serwera. Do czasu testu z aplikacją kolidującą nie należy przedstawiać tego jako potwierdzonego przejęcia konta lub podpisu.

### 8.2. MOB-A-09 — starszy wariant OAuth e-Doręczeń bez `state` i PKCE

**Ocena:** średnia, warunkowa
**Pewność:** brak mechanizmów po stronie klienta potwierdzony; aktywność wariantu i skuteczność ataku wymagają testu P0
**Klasa skutku:** CSRF podczas logowania, wstrzyknięcie kodu autoryzacyjnego lub pomylenie sesji, bez statycznie wykazanego przejęcia konta

APK zawiera starszą implementację autoryzacji e-Doręczeń oraz nowsze mechanizmy autoryzacyjne. Starsza ścieżka `y12.t` uruchamia URL `edorUrl` bez dodania transakcyjnego `state` ani `code_challenge`: `y12/t.java`. Wywołanie zwrotne akceptuje każdy URL o schemacie `mobywatel`, którego ścieżka zaczyna się od `/edelivery`, pobiera sam parametr `code` i przekazuje go do wymiany tokenów; host, `state` i powiązanie z rozpoczętą transakcją nie są sprawdzane: `y12/t.java`.

Łańcuch nie urywa się na warstwie interfejsu użytkownika. Stara ścieżka tworzy `sn0.x.Params(tokenUrl, code, null)`, więc `codeVerifier` jest jawnie pusty: `y12/t.java`. Repozytorium przekazuje tę wartość do pola formularza `code_verifier` punktu końcowego tokenów: `zn0/y.java`, `tn0/h.java`. Nie znaleziono alternatywnej wartości jednorazowej (`nonce`) ani lokalnego sekretu transakcyjnego w tym wariancie.

Nowszy moduł Keycloak obecny w tej samej aplikacji pokazuje wzorzec, który powinien obowiązywać wszędzie: generuje sekrety transakcji, dopisuje `state`, `code_challenge` i `code_challenge_method=S256`, porównuje zwrócony `state`, a do punktu końcowego tokenów przekazuje `codeVerifier`: `vs3/p.java`, `vs3/p.java`, `vs3/p.java`. Jest to dobra implementacja oraz silny dowód, że brak w `y12.t` nie wynika z ograniczenia platformy. Analiza statyczna nie pozwala utożsamić każdego wywołania `vs3.p` z następcą konkretnego ekranu `y12.t`, dlatego porównanie dotyczy własności zabezpieczeń, a nie deklaracji pełnej równoważności tras.

Wybór między starszym WebView a nowym modułem natywnym odbywa się przez serwerową flagę `NATIVE_LOGIN_EDOR_MODULE_ANDROID`: przypadek użycia zwraca `false`, jeżeli flagi brakuje albo jest nieaktywna, a interfejs użytkownika wybiera wtedy właśnie `y12.t`: `xy1/i0.java`, `i64/c6.java`. Analiza statyczna nie ujawnia bieżącej wartości flagi dla konkretnego użytkownika, regionu lub grupy wdrożeniowej. Dlatego nie wolno pisać, że wszyscy użytkownicy produkcyjni są obecnie podatni; równie nieuczciwe byłoby uznanie starego wariantu za martwy tylko dlatego, że istnieje nowszy.

Aktualne dobre praktyki OAuth (BCP) wymagają ochrony klienta przed CSRF i wstrzyknięciem kodu. Klient publiczny powinien używać PKCE, a gdy ochrona CSRF nie wynika z poprawnie związanego PKCE/OIDC `nonce`, powinien stosować jednorazowy `state` związany z przeglądarką lub innym programem pośredniczącym użytkownika. Źródło normatywne: [RFC 9700, sekcja 2.1 i 2.1.1](https://www.rfc-editor.org/rfc/rfc9700.html#section-2.1).

sequenceDiagram autonumber participant U as Użytkownik participant L as Starszy wariant y12.t participant AS as Serwer autoryzacyjny participant T as Punkt końcowy tokenów participant N as Nowszy vs3.p U->>L: Start autoryzacji L->>AS: edorUrl
bez state i code_challenge AS-->>L: mobywatel://.../edelivery?code=... L->>L: sprawdza schemat + początek ścieżki
nie sprawdza state/host L->>T: code + code_verifier=null Note over L,T: Brak transakcyjnego związania po stronie klienta U->>N: Start nowszego wariantu N->>AS: state + code_challenge S256 AS-->>N: code + state N->>N: sprawdza zgodność i jednorazowość state N->>T: code + code_verifier 

**Warunek podniesienia oceny:** w teście P0 należy wymusić starszy wariant flagą serwerową, rozpocząć dwie równoległe transakcje na kontrolowanych kontach i sprawdzić, czy kod z transakcji A można wstrzyknąć do wywołania zwrotnego B lub dostarczyć przez kontrolowaną nawigację WebView. Należy także potwierdzić dokładne dopasowanie zwrotnego URI po stronie serwera, jednorazowość kodu, wiązanie kodu z klientem oraz zachowanie przy braku lub powtórzeniu `state`. Do tego czasu raport stwierdza **brak wymaganych zabezpieczeń klienta**, a nie udane przejęcie sesji.

**Korekta:** usunąć starszy wariant albo nadać mu ten sam generator sekretów i walidator co nowej ścieżce; wymusić PKCE S256 bez możliwości obniżenia zabezpieczeń, jednorazowy `state`, dokładną kontrolę schematu, hosta i ścieżki, jedną aktywną transakcję na przebieg oraz atomowe zużycie wywołania zwrotnego.

---

## 9. Dalsze ustalenia i obserwacje wymagające rozstrzygnięcia

### 9.1. MOB-O-01 — pełne URL w logowaniu zdalnym

Przechwytywacz dla części profili zewnętrznych przekazuje do zdalnego mechanizmu logowania pełny URL, potencjalnie wraz z parametrami zapytania. Konfiguracja produkcyjna włącza zdalne logowanie HTTP. Nie znaleziono jednak jeszcze potwierdzonego wywołania, w którym sekret lub dane osobowe przechodzą tą konkretną ścieżką. Jest to hipoteza do testu przepływowego, a nie gotowa podatność.

Zalecenie: globalnie usuwać parametry zapytania, fragment adresu, nagłówki autoryzacyjne i identyfikatory z telemetrii; utrzymywać listę dozwolonych pól diagnostycznych.

### 9.2. MOB-O-02 — szeroki zakres `FileProvider`

Dostawca treści nie jest eksportowany i wymaga jawnego nadania uprawnienia do URI, co jest poprawną granicą Androida: `AndroidManifest.xml`. Jego konfiguracja deklaruje jednak `external-path path="."`, czyli korzeń współdzielonej pamięci zewnętrznej, zamiast wydzielonego podkatalogu: `provider_paths.xml`. Sam wpis nie udostępnia plików bez przyznania uprawnienia i nie jest dowodem wycieku. Powoduje natomiast, że błąd w miejscu budującym URI lub nadającym czasowe uprawnienie może mieć znacznie większy zasięg niż potrzebny.

Zalecenie: zawęzić ścieżki dostawcy do wydzielonych katalogów i typów plików oraz zinwentaryzować wszystkie miejsca wywołujące `getUriForFile`, `grantUriPermission` i `FLAG_GRANT_*_URI_PERMISSION`. Test negatywny powinien potwierdzić, że nie da się utworzyć ani skutecznie nadać URI dla pliku spoza katalogu eksportu.

### 9.3. MOB-O-03 — zakres telemetrii Sentry i zdarzeń bezpieczeństwa

Produkcja inicjalizuje Sentry własnym kodem; automatyczna inicjalizacja manifestowa jest wyłączona. Konfiguracja nadaje zdarzeniom między innymi tagi:

- wersja Google Play Services i poziom poprawek bezpieczeństwa;
- wersje OpenGL i Vulkan, program uruchamiający i język aplikacji;
- rozmiar czcionki, TalkBack, eksploracja dotykiem i animacje;
- stan sieci komórkowej, Wi-Fi, GPS, NFC i biometrii systemowej;
- `X_SESSION_ID`, poziom StrongBox, obciążenie CPU i wykryte zagrożenia.

Dowód: `q00/d.java`. Mechanizm logowania przyjmuje także komunikaty, wyjątki i ślady zdarzeń. Zestaw programistyczny (SDK) zawiera integracje pomiarowe dla uruchomienia aplikacji, Compose, baz danych, plików, Logcat i OkHttp. Sama obecność integracji w metadanych nie dowodzi, że każda jest włączona ani że przesyła treść danych. Biblioteka odtwarzania sesji (Session Replay) również znajduje się w pakiecie, lecz nie znaleziono konfiguracji aplikacyjnej potwierdzającej aktywne rejestrowanie sesji; nie przypisano go więc aplikacji.

Moduł wykrywania zagrożeń przekazuje do mechanizmu logowania zdarzenia dotyczące uprawnień `root`, emulatora i podatności BlueBorne oraz nazwę i identyfikator pakietu wykrytego złośliwego oprogramowania. Dowód: `h83/c.java`. Dane te mogą być uzasadnione ochroną przed nadużyciami, ale stanowią osobną granicę prywatności i powinny być objęte minimalizacją, retencją, kontrolą dostępu oraz jawną dokumentacją celu.

Nie znaleziono dowodu, że wrażliwa treść dokumentów jest wysyłana do Sentry. Jednocześnie przechwytywacz opisany w MOB-O-01 zapisuje pełny URL, dlatego brak wycieku musi zostać potwierdzony testem ruchu i zdarzeń, a nie założony na podstawie intencji implementacji.

### 9.4. MOB-O-04 — ochrona ekranów potwierdzenia przed atakiem przez zasłanianie interfejsu

Pełne przeszukanie źródeł i manifestu nie znalazło `filterTouchesWhenObscured`, `setFilterTouchesWhenObscured`, obsługi `FLAG_WINDOW_IS_PARTIALLY_OBSCURED`, `Window.setHideOverlayWindows()` ani deklaracji `android.permission.HIDE_OVERLAY_WINDOWS`. Produkcyjne `FLAG_SECURE` ogranicza przechwytywanie obrazu, ale nie jest kontrolą przyjmowania dotyku przez zasłonięte okno. Oficjalne zalecenie Androida rozróżnia pełne i częściowe zasłonięcie: nowszy system ogranicza część pełnych nakładek, lecz dla częściowego zasłonięcia nie ma ochrony domyślnej. [Android Developers — ataki przez zasłanianie interfejsu (Tapjacking)](https://developer.android.com/privacy-and-security/risks/tapjacking).

W aplikacji zawierającej potwierdzenia związane z tożsamością i podpisem jest to brak obrony warstwowej. Analiza statyczna nie dowodzi jednak, że nakładka potrafi uruchomić konkretną operację bez czytelnej zgody, biometrii, PIN-u lub walidacji serwera. Dlatego MOB-O-04 pozostaje obserwacją do testu, a nie stwierdzoną podatnością.

Zalecenie: dla całego okna czynności wrażliwych na Androidzie 12+ zadeklarować `HIDE_OVERLAY_WINDOWS` i użyć `setHideOverlayWindows(true)`; niezależnie od wersji odrzucać dotknięcia oznaczone pełnym lub częściowym zasłonięciem na przyciskach potwierdzenia. Testy muszą objąć pełną nakładkę, przezroczystą nakładkę poniżej progu systemowego, częściowe zasłonięcie i umieszczenie aktywności między oknami napastnika (`activity sandwich`), z kontrolą dostępności dla prawidłowych narzędzi asystujących.

### 9.5. MOB-A-10 — nienormalizowana nazwa zdalnego załącznika w ścieżce `Downloads` na API 26–28

**Ocena:** Niskie–Średnie, warunkowe
**Atakujący:** nadawca lub inny podmiot mogący wpłynąć na nazwę pobieranego załącznika; alternatywnie przejęte zaplecze serwerowe
**Warunki:** Android 8–9 (API 26–28), zgoda na zapis w pamięci zewnętrznej oraz zaakceptowanie przez usługę nazwy zawierającej segmenty ścieżki
**Pewność:** potwierdzony brak walidacji po stronie klienta; zdalna sterowalność nazwy i skuteczność wobec produkcyjnej usługi wymagają testu

Łańcuch zaczyna się w odpowiedzi HTTP. Parser `Content-Disposition` wybiera `filename=` albo dekoduje `filename*=`, lecz nie usuwa separatorów katalogów, `..` ani znaków sterujących: `q10/a.java`. Repozytorium e-Doręczeń przekazuje wynik bez zmiany do `FileDto`, a następnie `DomainFile`; w razie braku nagłówka używa bezpiecznej nazwy domyślnej: `wn0/d.java`, `un0/a.java`. Wspólny przypadek użycia rozdziela nazwę wyłącznie wokół ostatniej kropki i nie przeprowadza normalizacji ścieżki: `ww3/l.java`.

Standardowy przebieg JADX pominął główną metodę zapisu w `ww3/m.java`; ponowna dekompilacja tej samej klasy z `--show-bad-code` odtworzyła jawny warunek `Build.VERSION.SDK_INT >= 29`. Na API 29+ aplikacja używa `MediaStore` z `relative_path=Downloads`: `r20/e.java`. Na API 26–28 — które nadal mieszczą się w deklarowanym `minSdkVersion=26` — buduje natomiast ścieżkę tekstową i otwiera ją przez `FileOutputStream`: `ww3/m.java`, `AndroidManifest.xml`, `r20/e.java`. Bazą jest publiczny katalog `Downloads`: `r20/b.java`. Funkcja składa go operatorem tekstowym jako `Downloads + "/" + fileName + "." + extension`, bez `getCanonicalFile()` i sprawdzenia prefiksu: `ww3/c.java`.

Przykładowa nazwa `../Pictures/probe.pdf` prowadzi więc w starszej gałęzi do ścieżki równoważnej `Download/../Pictures/probe.pdf`. Manifest żąda `WRITE_EXTERNAL_STORAGE`, a przypadek użycia poprzedza zapis prośbą o to uprawnienie: `AndroidManifest.xml`, `ww3/l.java`. Kod sprawdza istnienie celu i dodaje przyrostek `(n)`, zatem w przeanalizowanym przebiegu **nie potwierdzono nadpisywania istniejącego pliku**. Potwierdzona własność ogranicza się do możliwości utworzenia nowego pliku w innym istniejącym katalogu pamięci współdzielonej, w granicach uprawnień starego Androida. Nie oznacza zapisu do prywatnego obszaru innej aplikacji ani wykonania kodu.

flowchart LR HTTP[Odpowiedź HTTP] --> CD[Content-Disposition
filename / filename*] CD --> NAME[Nazwa bez walidacji
separatorów i ..] NAME --> SPLIT[Podział przy ostatniej kropce] SPLIT --> SDK{API urządzenia} SDK -->|29+| MS[MediaStore
relative_path = Downloads] SDK -->|26-28| PATH[Downloads / nazwa . ext] PATH --> FOS[FileOutputStream] PATH -.-> OUT[Segmenty .. kierują nowy plik
poza Downloads w pamięci współdzielonej] style NAME fill:#ffe0e0,stroke:#ad3030 style PATH fill:#ffe0e0,stroke:#ad3030 style MS fill:#d9f2e6,stroke:#16784b style OUT fill:#fff1c7,stroke:#a66a00 

**Test P1:** na emulatorach API 26 i 28 wysłać w kontrolowanym środowisku załączniki o nazwach zwykłych oraz `../Pictures/probe.pdf`, `..%2FPictures%2Fprobe.pdf` w `filename*=`, z wieloma segmentami `../`, separatorem odwrotnym i nazwą już istniejącą. Należy zapisać zarówno odpowiedź serwera, jak i rzeczywistą ścieżkę pliku. Test rozstrzyga, czy serwer zachowuje nazwę nadawcy, normalizuje ją lub odrzuca przed zwróceniem klientowi. Na API 29+ trzeba osobno potwierdzić zachowanie `_display_name`; nie należy automatycznie przenosić wyniku ze starej gałęzi.

**Naprawa:** potraktować nazwę z sieci wyłącznie jako etykietę. Odrzucać `/`, `\\`, segmenty `.`/`..`, NUL, znaki sterujące i nieakceptowane rozszerzenia; ograniczyć długość i zastosować listę dozwolonych znaków. Dla gałęzi plikowej obliczyć kanoniczny katalog bazowy i cel, a następnie wymagać, by cel pozostawał jego potomkiem. Najlepiej usunąć starszą gałąź bezpośredniego budowania ścieżki wraz z końcem wsparcia API 26–28 albo użyć bezpiecznego API systemowego również tam. Walidację trzeba wykonać niezależnie po stronie serwera i klienta.

---

### 9.6. MOB-O-05 — numer IMEI jako materiał wiążący kontener starszego magazynu

Klasa `pl/gov/coi/mobywatel/feature/legacy/storage/j.java` zawiera metodę `c(String, Context)`, która przy ustawionej fladze `isContainerBuiltByImei` buduje wartość ośmiobajtową z numeru IMEI: łańcuch identyfikatora poprzedzany jest cyfrą `1`, zamieniany na `BigInteger`, a z jego reprezentacji bajtowej pobieranych jest osiem pierwszych bajtów, uzupełnianych od końca znakami samego identyfikatora (wiersze 105–125). Gałąź alternatywna kieruje do metody `b(String)`, niezwiązanej z identyfikatorem sprzętowym.

Numer IMEI nie jest sekretem: bywa nadrukowany na obudowie, odczytywany przez sieć operatora i widoczny w wielu procesach obsługi urządzenia. Wiązanie kontenera taką wartością nie wnosi entropii i nie zastępuje materiału klucza. Od Androida 10 dostęp do trwałych identyfikatorów sprzętowych jest dla zwykłych aplikacji zamknięty, wobec czego ścieżka ta nie może obsługiwać kontenerów zakładanych na bieżących wydaniach systemu.

Ustalenie zgłaszam jako dług techniczny w kodzie archiwalnym, a nie jako czynną podatność. Należy zweryfikować, czy kontenery zbudowane w tym trybie nadal występują u użytkowników po migracjach oraz czy wartość ta bierze udział w wyprowadzaniu klucza wspólnie z materiałem opisanym w MOB-A-06, czy wyłącznie w rozpoznaniu urządzenia. Do czasu takiego rozstrzygnięcia ośmiobajtowa zbieżność z solą opisaną w MOB-A-06 pozostaje obserwacją, a nie wykazanym powiązaniem.

**Zalecenie:** wycofać tryb wiązania po identyfikatorze sprzętowym, wymusić migrację istniejących kontenerów na materiał z generatora kryptograficznego przechowywany w magazynie kluczy oraz usunąć gałąź wraz z flagą po zakończeniu migracji.

---

### 9.7. Model zaufania do wyświetlanego dokumentu

Odrębnej uwagi wymaga to, na czym opiera się wiarygodność dokumentu prezentowanego na ekranie. Wizualne cechy „autentyczności" — animowana, falująca flaga oraz układ karty — nie są kontrolą kryptograficzną, lecz elementem interfejsu. Animacja flagi to sekwencja sześćdziesięciu statycznych klatek WebP (zob. 24.4.2), a cały system komponentów wizualnych opublikowano na licencji MIT (zob. 20.3). Wygląd karty można więc odtworzyć co do piksela z materiałów jawnych i legalnie dostępnych, bez inżynierii wstecznej. Ochrona `FLAG_SECURE` (zob. 24.2) zabezpiecza obraz okna prawdziwej aplikacji, lecz z zasady nie odróżnia jej od aplikacji-sobowtóra malującej identyczny ekran.

Płynie stąd wniosek o modelu zaufania, nie o kryptografii: **obejrzenie ekranu nie jest weryfikacją.** Zapewnienie autentyczności daje wyłącznie kontrola kryptograficzna — sprawdzenie podpisu dokumentu narzędziem weryfikującym (mWeryfikator lub kontrola online), nie zaś ocena wzrokowa. Wszędzie tam, gdzie w praktyce dokument bywa „sprawdzany" rzutem oka na telefon — od kontroli w terenie po sytuacje o wysokiej stawce, w których do potwierdzenia tożsamości dopuszcza się okazanie aplikacji, jak przy głosowaniu — warstwa wizualna nie daje żadnego zabezpieczenia, a ruchome, autorytatywnie wyglądające elementy mogą wręcz budować fałszywą pewność weryfikującego.

Szczegółów operacyjnych — jak konkretnie złożyć przekonującą imitację i ile „łatek" do tego potrzeba — świadomie nie ujawniam. Nie dlatego, że są trudne, lecz dlatego, że to jest problem do rozwiązania po stronie Państwa i wystawcy dokumentu, a nie gotowiec dla naciągacza. Zgodnie z linią całego raportu opisuję klasę słabości i jej naprawę; nie dostarczam narzędzia do podszywania się pod tożsamość drugiego człowieka.

**Zalecenie.** W każdym scenariuszu, w którym okazanie dokumentu ma wywoływać skutek prawny, weryfikacja musi przebiegać kryptograficznie po stronie sprawdzającego (mWeryfikator albo kontrola online), a procedury i szkolenia muszą wykluczać „uznanie na oko". Sama aplikacja nie powinna prezentować cech sugerujących poziom zapewnienia wyższy niż faktycznie dostarcza.

Na koniec obserwacja szersza niż ten jeden dokument, bo wzorzec się powtarza. Problem systemowy polskiego publicznego IT jest prosty i przewidywalny: zamówienie idzie do najtańszego oferenta, najtańszy oferent płaci najniższą krajową, i w aplikacji tożsamości państwowej dostajesz Bouncy Castle 1.83 trzy miesiące po tym, jak wyszła łatka (MOB-A-04), albo globalną, stałą sól PBKDF2 w pakiecie kryptograficznym legitymacji studenckiej (MOB-A-06). Gorzka pointa jest taka, że przy takich przetargach nawet oszczędność bywa pozorna — a jakość i tak wychodzi, jaka wychodzi.

---

### 9.8. Wspólny model błędu — strona kliencka i serwerowa

Publiczna analiza firmy SecuRing, opisująca współpracę przy zabezpieczaniu systemu tożsamości, ujawniła po stronie serwera błąd należący do tej samej klasy co MOB-A-05 po stronie klienta. Serwer weryfikujący sprawdzał podpis żądania użytkownika oraz państwowy podpis kontenera CMS, lecz **nie weryfikował wiązania tożsamości** (identity binding) między podmiotem certyfikatu użytkownika (`CN=MTMid…`) a danymi osobowymi wewnątrz kontenera. Umożliwiało to podstawienie cudzego, prawidłowo podpisanego kontenera i podszycie się pod dowolnego obywatela. Ustalenie zostało odpowiedzialnie zgłoszone i naprawione; przywołuję je za publicznym opracowaniem autorów — nie odtwarzam.

Zestawienie obu stron jest wymowne:

| Warstwa | Co jest sprawdzane | Czego brakuje |
|---|---|---|
| Klient (MOB-A-05; `y10/f0.java`, `d64/l.java`) | odczyt `getSignedContent().getContent()` | brak weryfikacji podpisu i łańcucha wystawcy CMS |
| Serwer (SecuRing, publicznie) | podpis żądania oraz państwowy podpis kontenera | brak wiązania tożsamości certyfikatu z zawartością kontenera |

Obie podatności wynikają z tego samego założenia: **PKCS#7 SignedData traktowany jest jak odseparowany blob danych, a nie jako kryptograficzne wiązanie tożsamości na twardej granicy „zweryfikuj, zanim odczytasz".** Klient ufa podpisanej treści, której nie zweryfikował; serwer ufa kontenerowi, którego nie związał z tożsamością okaziciela. To nie zbieg okoliczności, lecz systemowy wzorzec w modelu zaufania do dokumentu — i najsilniejszy argument za tym, że wiązanie tożsamości musi być egzekwowane po obu stronach naraz, nigdy tylko na jednej.

---

## 10. Kontrola regresji wcześniejszego incydentu

[CERT Polska opisał CVE-2025-11598](https://cert.pl/posts/2026/02/CVE-2025-11598/) jako błąd wersji **iOS starszych niż 4.71.0**, w których dane mogły pozostać widoczne w systemowym przełączniku aplikacji po zakończeniu sesji. [NVD](https://nvd.nist.gov/vuln/detail/CVE-2025-11598) klasyfikuje problem jako CWE-359 i niski poziom CVSS 4. Nie jest to CVE Androida i nie wolno przedstawiać statycznego sprawdzenia Androida jako potwierdzenia naprawy błędu iOS.

W Androidzie 4.87.2 znaleziono jednak właściwy wzorzec ochronny:

- `SECURE_WINDOW` ma wartość produkcyjną `true`;
- obserwator cyklu życia aktywności dodaje flagę o wartości `8192`, czyli `WindowManager.LayoutParams.FLAG_SECURE`;
- mechanizm jest dołączony do głównej aktywności.

Dowód: `a04/c.java`, `y10/x.java`.

**FLAG\_SECURE POPRAWNIE ZASTOSOWANE (ANDROID 16).** Przy aktywnej, zalogowanej aplikacji systemowy zrzut ekranu zwrócił czarną treść, karta mObywatela w widoku ostatnich aplikacji nie pokazała zawartości, a trzysekundowa próba `screenrecord` nie utrwaliła obrazu. To potwierdza flagę **wobec przechwytywania programowego** — spełnione minimum, którego się oczekuje od aplikacji tej klasy, nie zaś wyróżnik. Ochrona nie obejmuje napastnika z uprawnieniami root (zob. 24.8) i nie jest dowodem zachowania iOS ani wszystkich wspieranych wersji Androida.

Pozostaje rozszerzyć macierz o Androida 8 i 14 oraz pełny cykl: aktywna sesja → tło → wylogowanie → wymuszone zamknięcie → ponowne uruchomienie. Obecny wynik zamyka przypadek aktywnej sesji, zrzutu, nagrania i przełącznika aplikacji na Androidzie 16.

Podczas szybkiego minimalizowania i przywracania system utrzymywał równocześnie dwa odrębne zadania mObywatela (`task #4003` i `task #4001`), każde wskazujące tę samą `MainActivity`. Potwierdza to anomalię zarządzania zadaniami, ale bez wykazanego wpływu na poufność lub autoryzację nie otrzymuje ona obecnie odrębnej oceny podatności.

---

## 11. Magazyn kluczy — stan rzeczywisty

Potwierdzone dobre praktyki:

- użycie `AndroidKeyStore`;
- AES-GCM i operacje RSA w odpowiednich ścieżkach;
- możliwość związania klucza z uwierzytelnieniem urządzenia lub biometrią;
- możliwość unieważnienia po zmianie biometrii;
- żądanie StrongBox dla wybranych kluczy tożsamości;
- inspekcja faktycznego `KeyInfo.securityLevel` i raportowanie poziomu.

Korekty:

- pole `ksRandomizer` jest przekazywane do `setRandomizedEncryptionRequired()`. Nie losuje aliasu klucza;
- `StrongBox.PREFERRED` celowo dopuszcza ponowienie generacji bez StrongBox po `ProviderException`;
- odczyt rzeczywistego poziomu zabezpieczenia jest widoczny, ale nie znaleziono ogólnej blokady funkcji przy poziomie `SOFTWARE`;
- obecność klas AndroidX Security i Tink w APK nie dowodzi, że konkretna baza dokumentów jest nimi chroniona;
- programowe wykonanie zastępcze nie jest automatycznie CWE-326. To decyzja o poziomie zapewnienia, którą należy zestawić z wymaganiami protokołu i modelem urządzeń.

Zalecenie: formalnie określić, które klucze wymagają co najmniej TEE, które StrongBox, a które mogą działać programowo. Warunek powinien być egzekwowany w punkcie emisji/użycia klucza, jeżeli bezpieczeństwo protokołu faktycznie od tego zależy.

---

## 12. Plan testów dynamicznych przed zamknięciem audytu

flowchart LR FIX[Korekta kodu] --> BUILD[Podpisana kompilacja produkcyjna] BUILD --> STATIC{Brama statyczna} STATIC -->|brak TrustAll/proceed
HTTPS + świeżość CT| DYNAMIC{Brama dynamiczna} STATIC -->|nie| REJECT[Odrzucenie wydania] DYNAMIC -->|P0 zaliczone| TRACE[Pakiet dowodowy
skrót + logi + urządzenia] DYNAMIC -->|nie| REJECT TRACE --> REVIEW[Niezależna weryfikacja] REVIEW -->|ustalenia zamknięte| ACCEPT[Akceptacja konkretnego artefaktu] REVIEW -->|otwarte ryzyko| REJECT style ACCEPT fill:#d9f2e6,stroke:#16784b style REJECT fill:#ffe0e0,stroke:#ad3030 

| Priorytet | Test | Kryterium pozytywne |
|---|---|---|
| P0 | Ponowne podanie historycznej, prawidłowo podpisanej listy CT przez kontrolowany punkt dostępowy lub serwer pośredniczący | aplikacja odrzuca cofnięcie wersji lub oznacza listę jako przeterminowaną |
| P0 | Lista CT z błędnym podpisem i uszkodzony ZIP | brak zastąpienia ostatniej znanej poprawnej wersji; brak pętli awarii; alert telemetryczny bez danych użytkownika |
| P0 | Zrzut ekranu, nagrywanie obrazu i ekran ostatnich aplikacji na Androidzie 8, 14 i 16 — **Android 16: zaliczone dla aktywnej sesji; 8/14 i cykl wylogowania otwarte** | brak obrazu dokumentu i danych na każdym etapie sesji oraz po wylogowaniu |
| P0 | Kompilacja produkcyjna przy błędzie certyfikatu API/WebView | połączenie zawsze odrzucone; brak przejścia po błędzie SSL |
| P0 | CMS: zły podpis, brak podpisujących, obcy certyfikat, zły OID/typ, obca koperta z nieufnym CMS oraz wstrzyknięcie na wejściu | odrzucenie przed zapisem i przed utworzeniem modelu interfejsu użytkownika |
| P0 | KDF pakietu studenckiego: dwa kontrolowane pakiety, polityka kodu i pomiar słownika poza aplikacją | unikalna losowa sól każdego pakietu; udokumentowana entropia, TTL i koszt ataku; brak praktycznej walidacji zgadywanego kodu poza aplikacją |
| P0 | Pełny cykl klucza UNIVERSITY: kontrolowany pakiet, odzyskanie poza aplikacją, import PKCS#12, zapis bazy, użycie i unieważnienie | brak transportu eksportowalnego klucza; klucz nieeksportowalny w magazynie kluczy; serwer odrzuca klucz po unieważnieniu; brak sekretów w stercie i logach |
| P0 | OAuth e-Doręczeń w starszym `y12.t`: dwie równoległe sesje, obcy kod, brak lub powtórzenie `state`, obniżenie ochrony PKCE i wywołanie zwrotne o obcym hoście lub ścieżce | starszy wariant usunięty albo każde wywołanie zwrotne związane z właściwą transakcją przez PKCE S256 i jednorazowy `state`; obce wartości odrzucone przed punktem końcowym tokenów |
| P1 | Złośliwa aplikacja rejestrująca `mobywatel://` | brak ujawnienia użytecznego tokenu; przebieg odporny na przechwycenie i spreparowanie danych |
| P1 | Jawny Intent do `MainActivity` z błędnym hostem, ścieżką lub tokenem — **wykonane: kryterium niespełnione dla `institutions`, spełnione dla schematu `eqsig`** | odrzucenie przed zmianą stanu; brak awarii i wycieku diagnostycznego |
| P1 | WebView usług internetowych: `file://`, `content://`, przekierowania, systemowy wybór pliku i złośliwy HTML | `allowFileAccess=false`; nawigacja spoza listy dozwolonej zablokowana; brak odczytu plików i nadmiernych uprawnień do URI |
| P1 | Nazwy załączników e-Doręczeń z `../`, separatorami i zakodowanym `filename*=` na API 26, 28 i 29 | nazwa odrzucona lub sprowadzona do bezpiecznej etykiety; cel kanoniczny zawsze pozostaje w `Downloads` |
| P1 | Inspekcja Sentry przez kontrolowane punkty końcowe | URL i zdarzenia nie zawierają tokenów, numeru PESEL, danych dokumentu ani parametrów zapytania |
| P1 | Macierz urządzeń bez TEE / TEE / StrongBox | poziom klucza zgodny z wymaganiami i czytelna polityka przejścia na wariant zastępczy |
| P1 | Atak przez zasłanianie ekranów logowania, zgody i podpisu: pełna lub częściowa nakładka oraz aktywność między oknami napastnika | dotyk w zasłoniętym oknie odrzucony; brak wykonania operacji; poprawne narzędzie dostępności nadal działa zgodnie z polityką |
| P2 | Nadawanie URI przez `FileProvider` | uprawnienie obejmuje wyłącznie oczekiwany plik i wygasa po użyciu |

Testy powinny być wykonane na dokładnie tym samym certyfikowanym artefakcie wydania, dla którego zostaną opublikowane skrót kryptograficzny i `versionCode`. Wyniki negatywne także należy zachować w raporcie — brak powodzenia określonej hipotezy jest istotnym wynikiem audytu.

**WYNIK NEGATYWNY KRYTERIUM P1.** Dla `institutions` aplikacja nie odrzuciła jawnego Intentu z obcym schematem i hostem przed przekazaniem `qrCode` do dalszej logiki. Odrzucenie nastąpiło dopiero z powodu fikcyjnej wartości kodu. Oznacza to potwierdzony brak walidacji granicy klienta, bez potwierdzonego przejęcia autoryzacji.

---

## 13. Macierz rozstrzygnięcia tez

| Rozpatrywana teza | Werdykt | Uzasadnienie |
|---|---|---|
| APK jest podpisany oficjalnym kluczem | **Potwierdzone po uzupełnieniu dowodu** | Odcisk certyfikatu odpowiada oficjalnemu `assetlinks.json`; sam DN Google był niewystarczający. |
| HTTP dla gstatic pozwala wstrzykiwać czcionki/JS/zasoby | **Obalone** | Jedyny aktywny odbiorca to podpisana lista CT; błędny podpis jest odrzucany. |
| HTTP dla gstatic jest poprawne i bez konsekwencji | **Obalone** | Możliwe jest podstawienie starszej, poprawnie podpisanej listy; brak kontroli świeżości i stanu. |
| `sslEnabled` można przełączyć przez produkcyjny ekran deweloperski | **Obalone dla standardowej produkcji** | Kod interfejsu istnieje, ale produkcyjny moduł zarządzający jest niedostępny i brak eksportowanego wejścia. |
| Modyfikacja prywatnego XML przez `root` wyłącza TLS | **Technicznie prawdziwe, ale mylący model zagrożeń** | Uprawnienia `root` lub Frida oznaczają już kontrolę nad procesem; nie jest to zdalne ryzyko wysokie. |
| Obecność TrustAll w produkcji jest obojętna | **Obalone** | To niepotrzebna ścieżka akceptacji mimo błędu i ryzyko regresji, choć bez wykazanego zwykłego sposobu jej uruchomienia. |
| Przypinanie certyfikatów stanowi osobną średnią podatność | **Nieudowodnione / duplikat** | Wyłącza się w tej samej uprzywilejowanej ścieżce flagi co TLS. |
| `ksRandomizer` losuje alias | **Obalone** | Steruje `setRandomizedEncryptionRequired()`. |
| Aplikacja blokuje krytyczne funkcje bez StrongBox/TEE | **Nieudowodnione** | Znaleziono zamierzone przejście `PREFERRED` na wariant zastępczy i raportowanie poziomu, a nie ogólną blokadę. |
| AndroidX/Tink chroni krytyczną bazę dokumentów | **Nieudowodnione** | Obecność biblioteki nie dowodzi konkretnego przepływu danych. |
| `autoVerify` należy dodać do `mobywatel://` | **Technicznie błędne zalecenie** | Android App Links weryfikuje domenowe linki HTTP/HTTPS. |
| Odnośniki kierujące do aplikacji nie wymagają dalszych testów | **Obalone** | Własny schemat URI z tokenem wymaga testu kolizji, spreparowania danych i walidacji po stronie serwera. |
| Analiza była „pełna” | **Obalone opisem zakresu** | Wykonano pierwszą serię testów dynamicznych na Androidzie 16, ale nie wykonano całej macierzy urządzeń, kontrolowanych testów zaplecza ani pełnej analizy kodu natywnego; źródła publiczne pozostają niekompletne. |

---

## 14. Dobre wzorce, które należy odnotować

Uczciwy audyt obejmuje również kontrole działające poprawnie:

- `android:allowBackup="false"` i reguły tworzenia kopii oraz ekstrakcji wykluczające dane aplikacji;
- globalne `android:usesCleartextTraffic="false"`, z jednym jawnie opisanym wyjątkiem;
- ochrona Safe Browsing dla WebView włączona w manifeście;
- poprawna kryptograficzna weryfikacja listy CT i ograniczenia rozmiaru elementów ZIP;
- zachowanie ostatniej znanej poprawnej wersji zamiast przyjęcia danych o błędnym podpisie;
- pin podstawowy i zapasowy dla głównego API;
- `FLAG_SECURE` domyślnie aktywne w produkcji;
- brak aplikacyjnego `addJavascriptInterface` w przejrzanym kodzie;
- restrykcyjne ustawienia bazowe WebView (`allowFileAccess=false`, zablokowana treść mieszana) oraz twarde `sslEnabled=true` w ekranach płatności;
- produkcyjny odbiornik komunikatów `debug`/`info` nie utrwala treści pełnego URL ani DOM budowanego przez wspólny WebView; nie znaleziono zdalnej wysyłki tych komunikatów;
- nowszy moduł autoryzacyjny implementuje transakcyjny `state` oraz PKCE S256 i odrzuca wywołanie zwrotne z niezgodnym `state`;
- niemutowalne `PendingIntent` we własnych powiadomieniach i sprawdzonych przebiegach interfejsu; odziedziczona funkcja pomocnicza rejestracji Google tworzy osobny mutowalny token do fikcyjnego pakietu, dlatego nie zaliczono całej powierzchni bezwarunkowo do dobrych wzorców;
- komponenty bibliotek eksportowane tylko z właściwymi uprawnieniami systemowymi;
- `FileProvider` nieeksportowany;
- użycie Android Keystore, żądanie StrongBox dla wybranych kluczy i inspekcja realnego poziomu zabezpieczenia.

Te elementy zmniejszają ryzyko, ale nie unieważniają ustaleń MOB-A-01, MOB-A-02 ani rozbieżności między wariantami opisanej jako MOB-A-09.

---

## 15. Odtwarzalność kluczowych sprawdzeń

Przykładowe polecenia:

```bash
sha256sum mObywatel.apkm decompiling/base.apk

apksigner verify --verbose --print-certs decompiling/base.apk

curl --fail --silent --show-error \
  https://api.mobywatel.gov.pl/.well-known/assetlinks.json

rg -n 'http://[^" ]+' src/resources src/sources

rg -n 'getVersion\(|getLogListTimestamp\(' src/sources

rg -n 'filterTouchesWhenObscured|setFilterTouchesWhenObscured|\
FLAG_WINDOW_IS_PARTIALLY_OBSCURED|setHideOverlayWindows|HIDE_OVERLAY_WINDOWS' \
  src/sources src/resources/AndroidManifest.xml

jadx --single-class iy3.c \
  --single-class-output /tmp/iy3_c_fallback.java \
  --decompilation-mode fallback decompiling/base.apk

unzip -p log_list.zip log_list.json > log_list.json
unzip -p log_list.zip log_list.sig > log_list.sig
openssl dgst -sha256 -verify gstatic_public_key.pem \
  -signature log_list.sig log_list.json
```

Należy zapisać wersje narzędzi, pełne dane wyjściowe, czas UTC pobrania zasobu zdalnego oraz skróty kryptograficzne wszystkich plików dowodowych. Plik z kluczem publicznym w ostatnim poleceniu powinien być zbudowany bezpośrednio z wartości `gstatic_public_key` badanego APK.

---

## 16. Łańcuch dostaw i podatności zależności

### 16.1. Inwentaryzacja

Metadane Sentry w APK zawierają listę **324 dokładnych współrzędnych Maven**. Jest to użyteczny substytut wykazu składników oprogramowania (SBOM), lecz nie pełny dokument CycloneDX/SPDX: nie podaje relacji bezpośrednia/przechodnia, zakresu użycia, skrótów plików JAR ani modułów własnych. Dowód: `sentry-external-modules.txt`.

Wśród składników odnotowano między innymi:

| Składnik | Wersja w APK | Znaczenie audytowe |
|---|---|---|
| Bouncy Castle `bcprov` / `bcpkix` / `bcutil-jdk15to18` | 1.83 | aktywnie używany dostawca kryptograficzny, CMS, PKCS#12 i ASN.1; znane CVE |
| Tink Android | 1.8.0 | stara wersja nie dowodzi użycia w krytycznym przepływie |
| Conscrypt | 2.5.3 | Java/JNI TLS; wymaga osobnej kontroli kodu natywnego |
| SQLCipher Android | 4.10.0 | natywna baza; obecność nie dowodzi zakresu szyfrowanych danych |
| OkHttp | 4.12.0 oraz 2.7.2 | dwa pokolenia API zwiększają koszt utrzymania |
| Sentry Android | 8.22.0 | telemetria kodu Java i natywnego |
| RootBeer | 0.1.2 | wykrywanie uprawnień `root`/JNI; mechanizm ten nie stanowi granicy zaufania |
| Protobuf | 4.29.3 | parser danych binarnych |

Lista zawiera **28 artefaktów `alpha`/`beta`**, głównie Compose 1.11.0-beta02, Material3 1.5.0-alpha18, Navigation3 1.1.0-alpha01 i ODML Image beta. Wersja przedprodukcyjna nie jest podatnością sama w sobie. W aplikacji państwowej powinna jednak wymagać jawnego wyjątku ryzyka, testów regresji i terminu przejścia na wydanie stabilne.

### 16.2. MOB-A-04 — Bouncy Castle 1.83

Wersję biblioteki potwierdza sam artefakt: klasa dostawcy deklaruje łańcuch `"BouncyCastle Security Provider v1.83"` i rejestruje się z numerem `1.83d` (`org/bouncycastle/jce/provider/BouncyCastleProvider.java:60,106`). Ustalenie wersji opiera się zatem na samoopisie kodu w pakiecie, a nie wyłącznie na dopasowaniu współrzędnych Maven przez skaner składu.

Zapytania do OSV dla wszystkich 324 współrzędnych, wykonane 25 sierpnia 2026 r., zwróciły trzy wpisy dotyczące dwóch z trzech spakowanych modułów Bouncy Castle:

| Moduł | Komunikat / CVE | Mechanizm | Wersja naprawiona | Osiągalność kodu w aplikacji |
|---|---|---|---|---|
| `bcpkix-jdk15to18:1.83` | [GHSA-wg6q-6289-32hp / CVE-2026-5588](https://github.com/advisories/GHSA-wg6q-6289-32hp) | roboczy `CompositeVerifier` akceptuje pustą sekwencję podpisów | 1.84 | klasa obecna; brak wywołania spoza BC |
| `bcprov-jdk15to18:1.83` | [GHSA-574f-3g2m-x479 / CVE-2025-14813](https://github.com/advisories/GHSA-574f-3g2m-x479) | licznik GOST 28147/G3413 CTR zawija się i powtarza strumień klucza | 1.84 | klasa obecna; brak użycia GOST spoza BC |
| `bcprov-jdk15to18:1.83` | [GHSA-c3fc-8qff-9hwx / CVE-2026-0636](https://github.com/advisories/GHSA-c3fc-8qff-9hwx) | wstrzyknięcie danych w `LDAPStoreHelper` | 1.84 | klasa obecna; brak konfiguracji/wywołania LDAP spoza BC |

Ręczna kontrola oficjalnej noty 1.84 wykazała, że pojedyncze źródło danych było niepełne. Projekt źródłowy wymienia także CVE-2026-3505 oraz CVE-2026-5598:

- CVE-2026-3505 dotyczy OpenPGP AEAD; moduł `bcpg` i pakiet `org.bouncycastle.openpgp` nie występują w badanym APK, więc ten wpis nie jest przypisywany aplikacji. Odnotowuję dla ścisłości, że przeszukiwanie po samym łańcuchu „openpgp” trafia w `org/bouncycastle/crypto/modes/OpenPGPCFBBlockCipher.java` — klasę warstwy trybów szyfru w module `bcprov`, niezwiązaną z modułem OpenPGP i nieprzesądzającą o przypisaniu tego wpisu;
- CVE-2026-5598 dotyczy różnic czasowych przy dekapsulacji FrodoKEM. Kod Frodo znajduje się w `bcprov`, lecz nie znaleziono referencji aplikacyjnej do Frodo/FRODO/frodokem; osiągalność nie została wykazana.

Przeszukanie 45 452 zdekompilowanych plików Java wykazało symbole GOST, LDAP, Composite Signature i Frodo wyłącznie pod `org/bouncycastle/**`. Jest to **negatywny wynik osiągalności kodu na poziomie jawnych odwołań**, a nie matematyczny dowód nieosiągalności: dostawca JCA może być wybierany nazwą algorytmu, refleksją albo przez dane wejściowe.

Nie wolno natomiast uznać całej zależności za martwą. Aplikacja tworzy i rejestruje `BouncyCastleProvider`, generuje oraz odczytuje CMS, używa magazynu `PKCS12` dostawcy BC i bezpośrednio odczytuje ASN.1. Przykłady: `y10/d0.java`, `kh0/m.java`, `dh2/a.java`, `ez/e0.java`. Oznacza to, że dla błędów CMS/PKIX/PKCS#12/ASN.1 sama obecność aktywnego odbiorcy jest potwierdzona, a rozstrzygnięcie musi zejść do konkretnych metod i danych wejściowych.

Bazowa ocena CVE opisuje najgorsze użycie wadliwego komponentu; nie jest automatycznie oceną badanej aplikacji. W szczególności krytyczna ocena CVE-2025-14813 wymaga rzeczywistego użycia trybu GOST CTR z warunkami ponownego użycia strumienia; takiego przebiegu nie wykazano. Z drugiej strony pozostawienie znanego podatnego dostawcy kryptograficznego w aplikacji tożsamościowej jest nieakceptowalnym długiem, ponieważ przyszłe lub słabo widoczne miejsce użycia może uczynić kod osiągalnym.

Wersja 1.84, która naprawiała powyższe problemy, została wydana 28 kwietnia 2026 r. Badana aplikacja trafiła do dystrybucji około 23–29 lipca 2026 r. Pozostawienie 1.83 nie może więc zostać wyjaśnione tym, że poprawka pojawiła się dopiero po zamknięciu kompilacji. To co najmniej niewystarczająca dyscyplina aktualizacji komponentu kryptograficznego albo brak wykazanego procesu akceptacji ryzyka.

Projekt źródłowy opublikował następnie 28 lipca 2026 r. Bouncy Castle 1.85, którego nota wymienia 32 dalsze poprawki CVE w całym produkcie, a 7 sierpnia poprawki dostawcy 1.85.2. Nie oznacza to, że wszystkie wpisy dotyczą użytych modułów albo przebiegów mObywatela. Ponieważ aplikacja używa CMS, PKCS#12 i ASN.1, priorytetowego rozpoznania wymagają między innymi klasy błędów CMS, walidacji RSA/certyfikatów, limitów ASN.1 i kosztu KDF. [Nota wydania Bouncy Castle 1.85](https://www.bouncycastle.org/resources/new-release-bouncy-castle-java-1-85/), [bieżące pliki i noty wydania](https://www.bouncycastle.org/download/bouncy-castle-java/).

Osiągalność tych podatności nie sprowadza się jednak wyłącznie do poszukiwania jawnych wywołań w kodzie aplikacji. Ścieżka deszyfrowania CMS EnvelopedData nie ogranicza dozwolonego algorytmu treści: wywołuje `recipientInformation.getContent(...)` bez sprawdzenia identyfikatora algorytmu, a Bouncy Castle odczytuje ten identyfikator (OID) wprost z przetwarzanego, niezaufanego kontenera (wywołanie w `dh2/a.java:274`, wewnątrz metody rozpoczynającej się w wierszu 265). Ponieważ zarejestrowany jest pełny dostawca Bouncy Castle, a w pakiecie obecne są moduły `pqc` (617 klas, w tym 19 klas rodziny Frodo), `gost` (71 klas), `composite` (`jcajce/provider/asymmetric/CompositeSignatures.java` oraz `COMPOSITE.java`) i `ldap` (`jce/provider/X509LDAPCertStoreSpi.java` wraz z czterema magazynami `X509StoreLDAP*`), spreparowany kontener z identyfikatorem wskazującym choćby na algorytm rodziny GOST skieruje przetwarzanie do odpowiedniej implementacji biblioteki. Płynie stąd istotne rozróżnienie: nieobecność jawnego wywołania w kodzie aplikacji jest faktem, lecz nie dowodzi nieosiągalności podatnego kodu. Zwinność algorytmiczna tej ścieżki sprawia, że implementacje wrażliwych mechanizmów pozostają osiągalne przez dane, warunkowo i tą samą bramą dostarczenia, którą opisano dla MOB-A-05.

Rzetelność wymaga odnotowania zabezpieczenia obecnego na tej samej ścieżce. Odbiorca budowany w `dh2/a.java:232-244` ustawia `setKeySizeValidation(true)` (wiersz 242), przez co Bouncy Castle odrzuca kontener, w którym długość odzyskanego klucza treści rozmija się z długością wynikającą z zadeklarowanego algorytmu. Kontrola ta zawęża zbiór kontenerów możliwych do spreparowania. Listy algorytmów dozwolonych nie zastępuje: algorytm o zgodnej długości klucza nadal zostanie rozwiązany przez dostawcę i uruchomiony. Osobno odnotowuję, że odbiorca pozostaje typu `JceKeyTransRecipient`, co ogranicza warstwę transportu klucza do RSA. Ograniczenie to dotyczy wyłącznie sposobu dostarczenia klucza treści i pozostaje bez wpływu na algorytm szyfrowania samej treści, którego dotyczy niniejsze ustalenie; obie warstwy koperty CMS deklarowane są niezależnie.

Rozstrzygnięcie, czy dostarczony kontener spełni szczegółowe warunki konkretnej podatności — na przykład ponowne użycie strumienia w trybie GOST CTR z CVE-2025-14813 — pozostaje hipotezą wymagającą testu dynamicznego. Sam mechanizm osiągalności jest natomiast potwierdzony statycznie i przenosi ustalenie z kategorii długu aktualizacyjnego bez wykazanej osiągalności do kategorii ścieżki osiągalnej przez dane, warunkowej na dostarczeniu kontenera.

**Zalecenie:** wprowadzić na granicy deszyfrowania i weryfikacji CMS listę algorytmów dozwolonych, docelowo ograniczoną do AES-256, z odrzuceniem nieoczekiwanych identyfikatorów algorytmów; przejść na najnowszy zgodny zestaw stabilnych artefaktów po testach współdziałania; usunąć nieużywane moduły i algorytmy przez minimalizację; generować podpisany SBOM dla każdego wydania; uruchamiać analizę składu oprogramowania (SCA) przy budowie i ponownie codziennie dla już wydanych wersji; łączyć co najmniej OSV/GHSA, NVD i komunikaty producenta; dla każdego CVE zapisywać decyzję „osiągalny / nieosiągalny / nierozstrzygnięty” wraz z dowodem. Samo podbicie numeru wersji bez testów nie zamyka ustalenia.

### 16.3. MOB-A-05 — treść CMS przyjmowana i odczytywana bez lokalnej weryfikacji podpisu

W wielu ścieżkach aplikacja tworzy `CMSSignedData` z danych binarnych i natychmiast pobiera `getSignedContent().getContent()`. Globalne wyszukanie nie znalazło poza kodem biblioteki wywołań `getSignerInfos()`, `SignerInformation.verify()`, `verifySignatures()` ani budowy `SignerInformationVerifier`. Najprostszy dekoder domenowy wykonuje wyłącznie:

```java
return UTF_8.decode(ByteBuffer.wrap(
    (byte[]) new CMSSignedData(byteArray).getSignedContent().getContent()
)).toString();
```

Dowód: `y10/f0.java`. Ten dekoder zasila repozytoria dokumentów. Analogiczny wzorzec występuje w `ih2/a.java` oraz starszym magazynie `storage/g.java`. Wprost odczytywane w ten sposób są między innymi zakresy prawa jazdy i legitymacji studenckiej: `d64/l.java`, `d64/l.java`. Inna ścieżka najpierw odszyfrowuje `CMSEnvelopedData` kluczem użytkownika, a następnie tylko odczytuje wewnętrzne obiekty `CMSSignedData`: `d64/l.java`.

Brak nie ogranicza się do ścieżki odczytu. Publiczna metoda repozytorium `b0(...)` przekazuje otrzymany ciąg do `K1(...)`; zależnie od trybu `B0(...)` albo `A0(...)` odszyfrowuje CMS EnvelopedData, tworzy wewnętrzne `CMSSignedData`, a następnie zapisuje ich `getEncoded()` do kontenera. Sprawdzana jest liczba elementów albo dozwolona nazwa zakresu (`scope`), lecz nie podmiot podpisujący. Dowody: `d64/l.java`, `d64/l.java`, `d64/l.java`. Analiza statyczna oznacza tu brak weryfikacji zarówno na zidentyfikowanej granicy przyjęcia danych, jak i później przy budowaniu modeli. Nie przesądza natomiast, kto może wywołać tę granicę z kontrolowaną treścią.

Osobny, istotny wariant dotyczy aktywacji legitymacji studenckiej. Pakiet pochodzi z punktu końcowego `FrontSrv/rs/post/getPackage`: `uo0/b.java`, `yo0/b.java`. Flaga `MOB_DB_CONTAINERS`, której wartości domyślne w artefakcie wynoszą `false/false`, przełącza całą implementację aktywacji pomiędzy `ey3.a` i `wy3.a`: `a04/c.java`, `u54/y5.java`. W starszej ścieżce `packageData` jest dekodowane i odszyfrowywane hasłem, po czym `A1(...)` wyciąga `fullStudentData` jako `CMSSignedData` i porównuje PESEL bez weryfikacji podpisu: `d64/l.java`, `d64/l.java`. Alternatywna implementacja również tworzy obiekty `CMSSignedData` z mapy pakietu bez znalezionej kontroli podpisujących: `ey3/b.java`. Flaga zmienia więc zaplecze magazynu i wykonanie aktywacji, ale w zbadanych wariantach nie usuwa omawianego braku.

`CMSSignedData` jest parserem i kontenerem; konstruktor ani `getSignedContent()` nie ustanawiają zaufania do podpisu. Dokumentacja projektu źródłowego pokazuje osobne pobranie `getSignerInfos()` i wywołanie `signer.verify(...)`; zaznacza też, że nawet taki prosty przykład nie sprawdza jeszcze ważności certyfikatu. [Bouncy Castle API — `CMSSignedData`](https://downloads.bouncycastle.org/java/docs/bcpkix-jdk15to18-javadoc/org/bouncycastle/cms/CMSSignedData.html). Prawidłowa walidacja wymaga co najmniej obecności oczekiwanej liczby podpisujących, weryfikacji kryptograficznej, budowy i polityki łańcucha certyfikatów, EKU/OID polityki, czasu, unieważnienia oraz związania podpisującego z typem dokumentu i środowiskiem.

**Potwierdzone:** klient przyjmuje i zapisuje w zidentyfikowanych przebiegach oraz odczytuje treść oznaczoną jako CMS SignedData bez znalezionej lokalnej weryfikacji podpisujących. Globalny wynik negatywny obejmuje również brak `getSignerInfos()`, `SignerInformation.verify()`, `verifySignatures()` i budowy `SignerInformationVerifier` poza biblioteką.

**Niepotwierdzone:** zwykły napastnik może samodzielnie dostarczyć taki kontener do repozytorium. Dane są chronione przez izolację aplikacji, TLS i w części przebiegów CMS EnvelopedData. Ta koperta kryptograficzna używa `AES256_CBC` i certyfikatu odbiorcy: `bh2/a.java`, `dh2/a.java`. Zapewnia poufność, lecz nie uwierzytelnia nadawcy ani nie jest trybem AEAD. Podmiot, który pozyska publiczny certyfikat odbiorcy, może technicznie zbudować skierowaną do niego kopertę; APK nie dowodzi jednak, że podmiot nieufny może pozyskać właściwy certyfikat i dostarczyć wynik do `b0(...)`.

Nie należy mieszać tego z zewnętrzną warstwą pakietu aktywacyjnego legitymacji: w starszej implementacji `C0/G0` używa hasła i `AES/CBC/PKCS5Padding`, a kod dopina IV na końcu szyfrogramu i nie pokazuje znacznika uwierzytelniającego: `d64/l.java`, `d64/l.java`, `ch2/a.java`, `ch2/c.java`. Niezależny ślad w nowej implementacji prowadzi od `packageData` i hasła przez generator klucza PBE do odszyfrowania wariantem z `IvParameterSpec`, a następnie do utworzenia `CMSSignedData`: `ey3/b.java`, `ey3/b.java`, `y10/k.java`, `ey3/b.java`. Jest to drugi rodzaj mechanizmu poufności bez potwierdzonej integralności kryptograficznej, obecny w obu implementacjach aktywacji; praktyczna modyfikacja nadal wymaga spełnienia formatu, dopełnienia, znajomości hasła lub kontrolowanej zmiany szyfrogramu oraz warunków dostarczenia pakietu. Dlatego błąd może stać się istotny przy przejęciu kanału, błędzie serwera, podatnym imporcie lub zapisie lokalnym, lecz nie wykazano samodzielnego zdalnego wektora.

Parametry KDF zewnętrznego pakietu aktywacyjnego, w tym potwierdzoną stałą sól PBKDF2, wydzielono jako niezależne ustalenie MOB-A-06. Nie są one dowodem braku weryfikacji CMS i nie należy sztucznie sumować obu wad, choć w jednym przepływie mogą wpływać na ten sam pakiet danych.

Aplikacja ma ogólny weryfikator `Signature.initVerify()/verify()` w `y10/c.java`, ale znaleziony odbiorca używa go do podpisu listy dzienników CT: `iy3/c.java`. Nie jest to gotowa implementacja polityki CMS: nie buduje łańcucha ani nie sprawdza EKU/OID, czasu i unieważnienia. Pokazuje jednak niespójność architektoniczną: podpis zewnętrznego artefaktu CT jest jawnie weryfikowany, podczas gdy podpisane dane dokumentów są jedynie odczytywane.

flowchart LR DOC[Zaplecze dokumentów] --> TLS1[TLS i przypinanie certyfikatów] TLS1 --> ENV[Opcjonalny CMS EnvelopedData] ENV --> ING[Przyjęcie danych A0 / B0] STUD[FrontSrv getPackage] --> TLS2[TLS i przypinanie certyfikatów] TLS2 --> CBC[Pakiet hasłowy AES-CBC] CBC --> ING2[Aktywacja legitymacji] ING --> SD[CMSSignedData] ING2 --> SD SD --> STORE[Zapis getEncoded] STORE --> PARSE[getSignedContent] PARSE --> MODEL[Model dokumentu / interfejs] SIGNER[Weryfikacja podpisującego,
łańcucha, EKU, czasu] -. nieznaleziona na wejściu
ani odczycie .-> SD style SIGNER fill:#ffe0e0,stroke:#ad3030 style MODEL fill:#fff1c7,stroke:#a66a00 

**Test P0:** w kontrolowanym środowisku sprawdzić na granicy przyjęcia danych i ponownie przy odczycie: (a) SignedData z uszkodzonym podpisem, (b) SignedData bez podpisujących, (c) podpis poprawny, ale wykonany nieufnym certyfikatem, (d) poprawny podpis niewłaściwego typu dokumentu/OID, (e) kopertę zbudowaną poza oczekiwanym producentem do publicznego certyfikatu testowego użytkownika i zawierającą jeden z nieufnych wariantów a–d oraz (f) bezpośrednie wstrzyknięcie wariantów a–e do `b0(...)` i pakietu aktywacyjnego. Należy osobno zmierzyć zachowanie obu stanów `MOB_DB_CONTAINERS`. Warianty a–f muszą zostać odrzucone przed zapisem i przed zbudowaniem modelu interfejsu użytkownika, a log ma wskazać konkretną przyczynę bez ujawnienia danych dokumentu. Koperta utworzona przez inny podmiot, ale przenosząca autentyczny, zaufany i świeży CMS, jest odrębnym przypadkiem ponownego podania danych i ustalania ich pochodzenia — sama koperta nie zawiera dowodu nadawcy.

**Naprawa:** jedna, centralna funkcja `verifyThenDecode`, wywoływana na granicy przyjęcia danych przed `getEncoded()` i zapisem; ponowna walidacja przy odczycie jako obrona warstwowa; brak publicznego API `decode` odczytującego niezweryfikowaną treść; testy negatywne dla braku podpisujących, złej sygnatury, obcego certyfikatu, złego OID i czasu; oddzielne typy `UntrustedCmsBytes` oraz `VerifiedDocumentPayload`. Dla nowych formatów należy zastąpić nieuwierzytelnione AES-CBC trybem AEAD lub poprawną konstrukcją „najpierw szyfrowanie, potem MAC” (`encrypt-then-MAC`). Nie należy jednak traktować tego jako zamiennika podpisu wystawcy dokumentu.

### 16.4. MOB-A-06 — globalna stała sól PBKDF2 w pakiecie aktywacyjnym legitymacji

Pakiet aktywacyjny legitymacji studenckiej jest szyfrowany kluczem wyprowadzanym z kodu aktywacyjnego. Śledzenie danych rozdziela dwa elementy przekazywane użytkownikowi: `State.getActivationCode()` jest argumentem hasłowym operacji deszyfrowania `packageData`, natomiast `setupData.getQrCode()` trafia osobno do operacji oznaczenia pakietu jako pobranego: `z63/n.java`, `z63/n.java`. Zasoby interfejsu również mówią osobno o kodzie QR i kodzie aktywacyjnym oraz deklarują ich jednorazowe użycie: `strings.xml`. Nie ma więc podstaw, aby nazywać hasło „kodem QR”; z APK nie odtworzono natomiast alfabetu, długości ani procesu losowania kodu aktywacyjnego.

Parametr `0` widoczny przy tworzeniu `PBEKeySpec` jest fałszywym tropem dekompilacyjnym. Wywołanie korzysta z maski argumentu domyślnego Kotlina, a konstruktor syntetyczny podstawia `PKIFailureInfo.notAuthorized`, czyli **65 536**: `lz/d.java`, `PKIFailureInfo.java`. Generator przekazuje tę wartość do `javax.crypto.spec.PBEKeySpec`; algorytm to PBKDF2-HMAC-SHA256, a długość klucza wynosi 256 bitów: `e20/g.java`. Zarzut „zero iteracji” byłby nieprawdziwy.

Potwierdzonym problemem jest sól. `saltStudentID` to wpisana w kod, ośmiobajtowa stała `{-73, 116, 33, -84, 127, -116, -18, -103}` obecna w obu implementacjach: `d64/p.java`, `ey3/b.java`. Funkcja przedstawiona przez dekompilator jako łączenie trzech wartości wykonuje cykliczny XOR, a nie konkatenację ani KDF: `hh2/a.java`. Wszystkie trzy argumenty są identyczne, więc wynik algebraicznie pozostaje tą samą ośmiobajtową stałą: `salt XOR salt XOR salt = salt`. Nowa ścieżka przekazuje je jawnie w `ey3/b.java`; starsza ścieżka robi to przez funkcję pomocniczą w `bh2/a.java`, wywołaną z tym samym stałym argumentem trzykrotnie w `d64/l.java`. Nie należy zatem przedstawiać tej funkcji pomocniczej jako dowodu użycia losowej soli dla legitymacji studenckiej.

flowchart LR QR[Kod QR] --> CTRL[Oddzielna obsługa pobrania / statusu] CODE[Kod aktywacyjny użytkownika] --> KDF[PBKDF2-HMAC-SHA256
65 536 iteracji] CONST[Ta sama stała 8 B
w każdym pakiecie] --> XOR[XOR trzech
identycznych wartości] XOR --> KDF KDF --> KEY[Klucz AES-256] DATA[packageData] --> CBC[AES-CBC] KEY --> CBC CBC --> DOC[JSON / kontenery CMS] style CONST fill:#ffe0e0,stroke:#ad3030 style CODE fill:#fff1c7,stroke:#a66a00 style DOC fill:#e2ecff,stroke:#345da8 

Skutek kryptograficzny jest precyzyjny: sól nie zapewnia rozdzielenia użytkowników ani pakietów, a identyczny kod aktywacyjny i te same parametry PBKDF2 prowadzą do identycznego klucza AES. Napastnik posiadający wiele szyfrogramów może amortyzować przygotowanie słownika między celami. Jednorazowość kodu ogranicza jego ponowne użycie w legalnym protokole, lecz sama nie usuwa ataku słownikowego poza aplikacją po pozyskaniu pakietu. AES-CBC z dopełnieniem i przewidywalnie ustrukturyzowanym wynikiem może dostarczyć sposobu rozpoznania poprawnego odgadnięcia, ale jego praktyczność — podobnie jak kanał pozyskania `packageData` — wymaga potwierdzenia laboratoryjnego. Nie wykazano odzyskania kodu ani danych użytkownika.

Liczba 65 536 iteracji jest znacznie niższa od współczesnego punktu odniesienia OWASP wynoszącego 600 000 dla PBKDF2-HMAC-SHA256. Jest to **porównanie z zaleceniem dotyczącym przechowywania haseł**; nie stanowi automatycznego dowodu naruszenia normy tego protokołu ani samodzielnej podatności. Właściwy koszt należy dobrać do entropii sekretu, wydajności urządzeń i modelu zagrożeń. [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html).

**Ocena:** Niskie–Średnie, warunkowe. Stała globalna sól i parametry KDF są potwierdzone statycznie. Podniesienie oceny wymaga wykazania, że kod ma małą przestrzeń, zaszyfrowany pakiet jest dostępny napastnikowi i istnieje wiarygodny sposób sprawdzania zgadywanego kodu poza aplikacją. Brak uwierzytelnienia AES-CBC z MOB-A-05 zwiększa wagę całego przebiegu, ale pozostaje odrębną własnością kryptograficzną i nie powinien być liczony drugi raz jako skutek stałej soli.

**Test P0:** pozyskać w autoryzowanym środowisku co najmniej dwa pakiety testowe dla różnych kont i kontrolowanego tego samego kodu, potwierdzić powtarzalność soli oraz wyprowadzonego klucza, udokumentować alfabet, długość, rozkład, sposób generowania, TTL, jednorazowość i kanał przekazania kodu, a następnie zmierzyć koszt kontrolowanego słownika poza aplikacją dla rzeczywistego formatu. Osobno sprawdzić, czy dopełnienie, parser JSON/CMS lub komunikaty błędów tworzą niezawodny sposób potwierdzenia odgadnięcia. Wyniki należy zachować także wtedy, gdy hipoteza praktycznego ataku zostanie obalona.

**Naprawa:** generować kryptograficznie losową, unikalną sól co najmniej 16-bajtową dla każdego pakietu i przechowywać ją jawnie w uwierzytelnionym nagłówku; przejść na AEAD; wersjonować parametry KDF oraz format i jawnie odrzucać przejście do słabszego wariantu ze stałą solą. Koszt KDF należy dobrać pomiarem na wspieranych urządzeniach i zapisać jako politykę aktualizowalną bez zmiany kodu. Jeżeli kod aktywacyjny ma małą entropię, sama większa liczba iteracji nie zastąpi projektu protokołu odpornego na zgadywanie poza aplikacją.

### 16.5. MOB-A-08 — eksportowalny klucz prywatny w pakiecie aktywacyjnym i bazie aplikacji

MOB-A-06 określa koszt zaatakowania zewnętrznej warstwy pakietu. Analiza tekstu jawnego po jej odszyfrowaniu rozstrzyga, co stanowi chroniony zasób. `StudentPackageData` zawiera trzy pola typu `String`: `pkcs12Pass`, `pkcs12Data` oraz `signedDataList`: `StudentPackageData.java`. Implementacja aktywacji dekoduje Base64 obu pierwszych pól, zamienia hasło na `char[]` i wywołuje moduł zarządzający PKCS#12 z aliasem `mlWork`: `ey3/b.java`. Moduł ten ładuje magazyn Bouncy Castle PKCS#12, pobiera certyfikat X.509 i odpowiadający mu `PrivateKey`, po czym zwraca `CertKeyPair`: `y10/b.java`.

Hasło PKCS#12 i chroniony nim magazyn są współlokowane w tym samym zaszyfrowanym obiekcie. Hasło może wykrywać błędne odszyfrowanie i chronić sam plik PKCS#12 po jego przypadkowym oddzieleniu, ale **nie stanowi niezależnej granicy poufności wobec podmiotu, który odszyfruje cały pakiet**. Taki podmiot otrzymuje jednocześnie `pkcs12Data` i `pkcs12Pass`. Ochroną sieciową pozostają TLS i przypinanie certyfikatów; raport nie twierdzi, że szyfrogram jest dostępny pasywnemu napastnikowi internetowemu. Na poziomie formatu aplikacyjnego zewnętrzną granicą jest jednak PBKDF2 ze stałą solą oraz AES-CBC bez widocznego znacznika AEAD/MAC. Wewnętrzna integralność PKCS#12 nie obejmuje osobnego pola `signedDataList`, którego obiekty CMS są następnie jedynie odczytywane, jak opisano w MOB-A-05.

Po imporcie klucz nie jest zachowywany jako nieeksportowalny uchwyt AndroidKeyStore. `saveUserCertificateUC` otrzymuje typ `UNIVERSITY`, pustą starą frazę oraz `CertKeyPair`: `ey3/b.java`. Repozytorium serializuje `getPrivateKey().getEncoded()` bezpośrednio do pola `privateKey` encji certyfikatu: `xx3/a.java`, `CertificateEntity.java`. Jest to eksportowalna reprezentacja PKCS#8, a nie alias nieeksportowalnego klucza sprzętowego.

Baza nie jest przy tym jawnym SQLite. Jest otwierana przez SQLCipher z kluczem przekazanym do `SupportOpenHelperFactory`: `o20/c.java`. Produkcyjny dostawca pobiera klucz z `masterKeyProvider`: `p20/d.java`, `u54/j2.java`. Materiał klucza głównego jest utrzymywany w pamięci procesu, a szersza ścieżka zarządzania kluczami obsługuje klucze AndroidKeyStore: `v14/a.java`, `co1/z.java`. Oznacza to sensowną warstwę ochrony przechowywanych danych przed prostym skopiowaniem pliku bazy. Nie zmienia jednak podstawowej właściwości: po otwarciu bazy aplikacja odtwarza pełne bajty prywatnego klucza, a ich poufność zależy od klucza SQLCipher i bezpieczeństwa procesu, zamiast od sprzętowo egzekwowanej nieeksportowalności samego klucza.

Klucz `UNIVERSITY` nie jest martwym artefaktem. Warstwa tożsamości pobiera właśnie ten `CertKeyPair` dla legitymacji studenckiej: `u54/h2.java`. Wspólna infrastruktura używa prywatnych kluczy `CertKeyPair` do podpisywania JWS oraz CMS i do odszyfrowywania kluczy danych: `j01/a.java`, `m0.java`, `nw0/h.java`. Analiza statyczna pozwala więc nazwać go **aktywnym prywatnym kluczem certyfikatu tożsamości aplikacyjnej**. Nie dowodzi natomiast, że jest to klucz kwalifikowanego podpisu elektronicznego ani że samodzielnie umożliwia wykonanie czynności prawnej; takiego sformułowania raport celowo nie używa.

flowchart LR TLS[TLS + przypinanie certyfikatów] --> PKG[packageData
AES-CBC] CODE[Kod aktywacyjny] --> KDF[PBKDF2
stała sól] KDF --> PKG PKG --> JSON[StudentPackageData] JSON --> P12[pkcs12Data] JSON --> PASS[pkcs12Pass] P12 --> LOAD[KeyStore.load] PASS --> LOAD LOAD --> PRIV[PrivateKey / CertKeyPair] PRIV --> RAW[getEncoded: PKCS#8] RAW --> DB[(SQLCipher)] DB --> USE[Odtworzenie klucza
JWS / CMS / odszyfrowanie] HW[AndroidKeyStore
nieeksportowalny uchwyt] -. nie zastosowano
do tego klucza .-> PRIV style KDF fill:#ffe0e0,stroke:#ad3030 style RAW fill:#ffe0e0,stroke:#ad3030 style DB fill:#d9f2e6,stroke:#16784b style HW fill:#fff1c7,stroke:#a66a00 

#### Higiena pamięci i ryzyko diagnostyczne

W `ey3/b.java` nie znaleziono `Arrays.fill` ani równoważnego czyszczenia. Odszyfrowany JSON, `pkcs12Data`, `pkcs12Pass`, `char[]`, klucz AES, bufory PKCS#12 i obiekt `PrivateKey` powstają jako kolejne kopie oraz są utrzymywane w polach stanu korutyny. Sekrety zapisane jako niemutowalne `String` nie mogą zostać deterministycznie wyzerowane przed pracą mechanizmu odśmiecania pamięci (GC). Nie jest to samodzielny zdalny wektor, ale zwiększa skutki zrzutu pamięci, narzędzia diagnostycznego, podatności procesu albo analizy urządzenia po aktywacji.

Dwa automatycznie wygenerowane `toString()` są szczególnie niebezpiecznym wzorcem: `StudentPackageData.toString()` wypisuje całe `pkcs12Pass`, `pkcs12Data` i `signedDataList`, a `CertificateEntity.toString()` wypisuje tablice certyfikatu i prywatnego klucza bajt po bajcie: `StudentPackageData.java`, `CertificateEntity.java`. Nie znaleziono bezpośredniego miejsca wywołania, które zapisywałoby te obiekty w logach w analizowanej ścieżce, dlatego raport klasyfikuje to jako **potencjalny wyciek diagnostyczny**, a nie potwierdzoną eksfiltrację do Sentry.

#### Wpływ, model ataku i ocena

Jeżeli napastnik uzyska szyfrogram `packageData` i rzeczywista polityka kodów umożliwi atak słownikowy poza aplikacją, poprawne odgadnięcie odsłoni nie tylko dane dokumentu, lecz również plik PKCS#12, jego hasło i eksportowalny klucz prywatny. Możliwe następstwa zależą od rozszerzeń certyfikatu, akceptowanych operacji, ważności, unieważnienia oraz kontroli serwera. Mogą obejmować odtwarzanie aplikacyjnych podpisów lub uwierzytelnień albo odszyfrowywanie danych przeznaczonych dla tego certyfikatu, ale **nie zostały jeszcze wykazane na produkcyjnym zapleczu serwerowym**.

Atak na kopię bazy ma inną granicę: sam plik SQLCipher nie wystarcza bez klucza bazy. Po zdobyciu klucza SQLCipher pełny PKCS#8 jest jednak odzyskiwalny, ponieważ aplikacja nie przechowuje wyłącznie aliasu sprzętowego. Uprawnienia `root` lub pełna kontrola procesu dają już szerokie możliwości i nie powinny sztucznie podnosić CVSS; właściwym pytaniem jest odporność na kradzież kopii, lukę w procesie, zrzut po odblokowaniu oraz zakres skutków po utracie jednego urządzenia.

**Ocena:** Średnie, warunkowo Wysokie po P0. Potwierdzone są współlokacja klucza i hasła, eksportowalna postać PKCS#8, brak deterministycznego czyszczenia oraz brak izolacji klucza w AndroidKeyStore. Ocena Wysoka wymaga jednoczesnego wykazania praktycznego odzyskania kodu poza aplikacją z dostępnego szyfrogramu i skutecznego użycia odzyskanego klucza wobec nadal akceptującego go systemu. Bez tych dowodów określenie „przejęcie podpisu” byłoby nadinterpretacją.

**Test P0:** w wydzielonym środowisku i na własnych danych testowych: (a) zachować wyłącznie szyfrogram pakietu i przeprowadzić pomiar rzeczywistej przestrzeni kodów; (b) po kontrolowanym odzyskaniu kodu wyodrębnić PKCS#12, zweryfikować hasło i zgodność klucza z certyfikatem; (c) ustalić `KeyUsage`, `ExtendedKeyUsage`, wystawcę, okres ważności i wszystkie protokoły akceptujące certyfikat; (d) wykonać nieszkodliwy test podpisu i odszyfrowania w środowisku testowym; (e) unieważnić certyfikat i potwierdzić odrzucenie starego klucza w każdym punkcie końcowym; (f) po aktywacji wykonać kontrolowany zrzut sterty oraz inspekcję logów/Sentry pod kątem obu DTO i buforów; (g) skopiować bazę przed i po odblokowaniu, wykazać granicę SQLCipher oraz potwierdzić odzyskiwalność PKCS#8 dopiero po zdobyciu klucza bazy. Wyniki muszą rozdzielać przejęcie zawartości pakietu, procesu i bazy.

**Naprawa:** preferowany projekt generuje parę kluczy na urządzeniu wewnątrz AndroidKeyStore, wysyła wyłącznie CSR lub klucz publiczny i przechowuje alias zamiast PKCS#8. Polityka powinna wymagać TEE albo StrongBox tam, gdzie model zagrożeń tego wymaga, z jawnym zachowaniem na urządzeniu niespełniającym wymagań. Jeżeli import klucza wystawionego zewnętrznie jest nieunikniony, należy natychmiast importować go do AndroidKeyStore z restrykcyjnym `KeyProtection`, zweryfikować rzeczywisty poziom zabezpieczenia i usuwać wszystkie kopie importowe; sam import nie gwarantuje sprzętowej ochrony na każdym urządzeniu. Pakiet trzeba chronić AEAD, losową solą dla każdego pakietu, wysoką entropią sekretu oraz związaniem z użytkownikiem, urządzeniem, sesją i wersją protokołu. Hasło PKCS#12 przesłane obok magazynu nie może być przedstawiane jako dodatkowa bariera. Typy zawierające sekrety nie mogą generować `toString()`, `equals()` ani `hashCode()` operujących na pełnym materiale; bufory należy ograniczyć do `byte[]`/`char[]`, czyścić w `finally`, a rotację i natychmiastowe unieważnienie klucza traktować jako element procedury incydentowej.

### 16.6. Ograniczenia skanu zależności

Analiza składu oprogramowania obejmowała komponenty o współrzędnych Maven. Dla bibliotek natywnych `.so` wykonano przegląd utwardzenia ELF oraz identyfikację pochodzenia opisane w sekcji 19, nie przeprowadzono natomiast pełnej korelacji z bazami CVE opartej na wersji i `build-id`. Ta korelacja, wraz z usługami Google dostarczanymi poza APK, kodem zaplecza serwerowego oraz komponentami bez współrzędnych Maven, pozostaje poza zakresem tego etapu. Bazy podatności są opóźnione i niekompletne, dlatego wynik „zero trafień” nie jest dowodem braku błędów, a „trafienie” nie jest jeszcze dowodem osiągalności.

## 17. Detekcja zagrożeń — rzeczywista konfiguracja i jej granice

APK zawiera rozbudowany model raportu SDK BotSense, który teoretycznie przewiduje dane urządzenia, sieci, lokalizacji, integralności i klienta. Analiza samego modelu prowadziłaby do fałszywego wniosku o szerokim zbieraniu danych. Konfiguracja aplikacyjna jest znacznie węższa:

- wyłącza większość raportów urządzenia i usług;
- pozostawia aktywny test emulacji;
- uruchamia osobno RootBeer;
- kwalifikuje poziom poprawki sprzed 1 września 2017 r. jako ryzyko BlueBorne;
- przekazuje SDK literal `testUserID` i nie wskazuje w tej ścieżce zdalnego URL konfiguracji;
- przekazuje wykryte wyniki do zdalnego loggera jako `THREAT_DETECTED`.

Dowody: `h83/c.java`, `h83/c.java`, `h83/c.java`.

Odrębnie od heurystyk RootBeer/BotSense w APK znajduje się rzeczywista ścieżka Play Integrity. Konfiguracja niesie `projectNumber`, klient generuje 16 losowych bajtów przez `SecureRandom`, koduje je jako wartość jednorazową (`nonce`) i wywołuje `requestIntegrityToken`; wynik oraz ta wartość trafiają do raportu: `c74/i.java`, `w64/l4.java`, `w64/g4.java`. Generator kluczy AndroidKeyStore obsługuje także `setAttestationChallenge`, ale wyłącznie dla ścieżki EC i tylko wtedy, gdy wyzwanie jest niepuste: `e20/m.java`. Są to poprawne mechanizmy obronne i należy je odnotować. Analiza statyczna klienta nie pokazuje jednak, jak serwer weryfikuje token, wartość jednorazową, wiek werdyktu, licencję i rozpoznanie urządzenia ani jakie decyzje od nich zależą. Obecność API nie może więc zostać przekształcona w ogólne zapewnienie integralności urządzenia.

RootBeer i BotSense zwiększają koszt prostych modyfikacji, ale nie powinny stanowić jedynej przesłanki autoryzacji. Lokalny klient jest kontrolowany przez właściciela urządzenia; decyzje o wydaniu dokumentu, podpisie lub zmianie stanu muszą opierać się na kryptograficznie związanej sesji, świeżości, poświadczeniu integralności urządzenia o jasno opisanej polityce i walidacji serwera. Wykrycie uprawnień `root` nie naprawia błędu autoryzacji, a ich niewykrycie nie może oznaczać zaufania absolutnego.

## 18. Dziennik hipotez, ślepych uliczek i korekt

Wzorem analiz SKCI i EFITool zachowano nie tylko wynik końcowy, ale również ścieżki, które wyglądały obiecująco i zostały odrzucone. Pozwala to odtworzyć rozumowanie i zapobiega ponownemu przedstawianiu obalonych tez jako odkryć.

| Hipoteza początkowa | Sprawdzenie | Wynik | Znaczenie |
|---|---|---|---|
| HTTP do `gstatic` daje dowolne wstrzyknięcie zasobu | wyszukanie odbiorców, format ZIP, test podpisu | **fałsz w tej postaci** | treść jest podpisana; pozostała możliwość podstawienia starszej listy |
| Dekompilacja pokazuje akceptację złego podpisu CT | drugi tryb JADX + eksperyment: klucz prawidłowy, nieprawidłowy i brak klucza | **artefakt dekompilatora** | błędny podpis jest odrzucany |
| `sslEnabled` jest zdalnym przełącznikiem | manifest, eksporty, produkcyjne wstrzykiwanie zależności, SharedPreferences | **brak ścieżki zwykłego napastnika** | kod akceptujący połączenie mimo błędu nadal powinien zniknąć z wydania produkcyjnego |
| Uprawnienia `root` pozwalają zmienić prywatną flagę i „przełamać TLS” | porównanie uprawnień atakującego | **mylący model zagrożeń** | uprawnienia `root` lub Frida oznaczają już kontrolę nad procesem |
| `ksRandomizer` losuje alias klucza | śledzenie argumentu do Android Keystore | **fałsz** | to `setRandomizedEncryptionRequired()` |
| StrongBox jest bezwzględnie wymagany | ścieżka `PREFERRED` i obsługa `ProviderException` | **fałsz dla polityki ogólnej** | istnieje świadome przejście na wariant zastępczy i pomiar poziomu |
| Tink/SQLCipher dowodzi ochrony konkretnej bazy | referencje i miejsca użycia | **nieudowodnione** | komponent nie jest dowodem przepływu danych |
| Odtwarzanie sesji (Session Replay) jest aktywne, bo SDK jest w APK | wyszukanie konfiguracji aplikacyjnej | **nieudowodnione** | nie przypisano funkcji bez konfiguracji |
| BotSense zbiera cały szeroki model raportu | analiza konfiguratora klienta | **fałsz dla badanej ścieżki** | większość opcji wyłączona; aktywne są węższe kontrole |
| CVSS biblioteki jest oceną mObywatela | SCA, komunikaty projektu źródłowego i wyszukanie miejsc wywołań | **fałsz metodologiczny** | kod wadliwych mechanizmów istnieje, lecz ich osiągalność wymaga osobnego dowodu |
| PBKDF2 wykonuje zero iteracji | maska argumentu domyślnego i konstruktor syntetyczny `lz/d` | **artefakt dekompilacji** | rzeczywista wartość to 65 536; problemem jest stała sól i nieznana entropia kodu |
| Trzy składniki `saltStudentID` są konkatenowane lub zawierają losowość urządzenia | implementacja `hh2/a` i oba miejsca wywołania | **fałsz** | funkcja wykonuje XOR, a trzy identyczne stałe redukują się do tej samej stałej |
| Sól kontenera tożsamości (mDowód) jest globalną stałą umożliwiającą ogólnokrajowe tablice tęczowe | analiza `j.java` (`g()`, `f()`) oraz odczyt `mDoki_0.10.xml` | **częściowo obalone / uściślone** | mDowód używa **losowej `NSalt` per instalacja** (`saltApp ⊕ NSalt ⊕ deviceID`), co wyklucza uniwersalne, ogólnokrajowe tablice tęczowe; po zrzucie danych z urządzenia wszystkie składniki soli są jednak jawne, więc sól jest natychmiast obliczalna, a brute-force PIN-u na GPU trwa poniżej 20 s |
| PKCS#12 z legitymacji zawiera klucz podpisu kwalifikowanego | śledzenie typu `UNIVERSITY`, repozytorium certyfikatów i konsumentów `CertKeyPair` | **nieudowodnione** | potwierdzono aktywny klucz prywatny tożsamości aplikacyjnej; nie potwierdzono kwalifikowanego zastosowania prawnego |
| SQLCipher oznacza, że klucz prywatny jest nieeksportowalny | format `CertificateEntity` i `getPrivateKey().getEncoded()` | **fałsz** | baza jest szyfrowana, ale po jej otwarciu aplikacja odtwarza pełny PKCS#8 zamiast używać wyłącznie aliasu magazynu kluczy |
| `FLAG_SECURE` rozwiązuje atak przez zasłanianie interfejsu | porównanie semantyki flagi ze ścieżką obsługi zasłoniętych dotknięć | **fałsz** | flaga chroni obraz okna; nie zastępuje `filterTouchesWhenObscured`, obsługi częściowego zasłonięcia ani ukrycia nakładek |
| Sprawdzenie istnienia pliku przed zapisem tworzy możliwość nadpisania przez TOCTOU | śledzenie generatora nazw i `FileOutputStream` | **niepotwierdzone jako istotny wektor** | potencjalny wyścig dotyczy pamięci współdzielonej i nie daje zapisu poza uprawnieniami; odrębnym błędem jest brak kanonikalizacji w MOB-A-10 |
| Publiczny kod zawiera przykład `isPies`/`isDog` | pełne `rg` w 192 plikach i dekompilacji | **nieodtworzone** | anegdoty bez pliku i identyfikatora zmiany nie użyto jako ustalenia |
| Stała `ctWhitelistDomains` wyłącza kontrolę przejrzystości certyfikatów dla domen produkcyjnych | wyszukanie odczytów pola w 45 452 plikach oraz kontrola interfejsu `gw3/a` | **fałsz** | pole `d64/a.java:87` nie jest odczytywane w żadnym miejscu i nie występuje w interfejsie konfiguracji; o kontroli decyduje `certificateTransparencyEnabled` w `c64/a.java:65` wraz z `CTLogStore` i `CTPolicy` biblioteki Conscrypt (`z10/c.java`) |
| Hasło konta da się odzyskać metodą pełnego przeglądu PIN-u z pliku `shared_prefs_biometrics` | prześledzenie ładunku od `tv3/m.java:41` przez `tv3/a.java:419-420,561-562` do `tv3/b.java` | **fałsz** | ładunek trafia do `Encrypt` i zostaje zaszyfrowany kluczem `"BiometricKey"` z Android Keystore, założonym ze `STRONG_BIOMETRIC` (`tv3/b.java:508`); tryb `yz.d.PLAIN` opisuje kontener XML, którego zawartość jest już szyfrogramem. Zastrzeżeniem pozostaje mieszanie PIN-u z hasłem operacją XOR zamiast funkcji wyprowadzania klucza |
| Zamiana tablic w `hh2/a.java:23-27` bez zmiennej pomocniczej stanowi defekt wykonywalny | dekompilacja `classes8.dex` do Smali i kontrola metody na poziomie bajtkodu | **obalone** | Smali wykazał poprawną, trójrejestrową zamianę przez rejestr pomocniczy `v6` (`move-object v6, p1` / `p1, p0` / `p0, v6`). Zapis w JADX (`bArr2 = bArr; bArr = bArr2`) był artefaktem dekompilacji SSA, nie błędem źródła — teza MOB-BIN-01 w tej postaci upada |

Ta tabela nie „broni” produktu. Oddziela to, co można zaatakować dowodem, od tego, co jedynie źle wygląda. Pozwala dzięki temu ostrzej sformułować potwierdzone zarzuty bez rozcieńczania ich błędami.

### 18.1. Macierz kompletności przeglądu klas zagrożeń

Poniższa macierz określa, co rzeczywiście sprawdzono w artefakcie. Wynik „brak potwierdzonego wektora” nie znaczy „niemożliwe”; oznacza, że przeszukanie miejsc wywołań nie wykazało przepływu z niezaufanego wejścia do niebezpiecznej operacji w dostępnej analizie statycznej.

| Klasa zagrożenia | Zakres kontroli | Wynik w APK 4.87.2 | Klasyfikacja / dalszy krok |
|---|---|---|---|
| Integralność CMS / pochodzenie dokumentu | konstrukcja, przyjęcie, zapis i odczyt `CMSSignedData`; miejsca wywołań API weryfikacji | brak znalezionej weryfikacji podpisujących w cyklu dokumentu | **MOB-A-05, P0** |
| Kryptografia hasłowa i materiał klucza | parametry PBKDF2, sól, AES, PKCS#12, zapis certyfikatu, higiena pamięci | stała sól; klucz i hasło współlokowane; eksportowalny PKCS#8 | **MOB-A-06/A-08, P0** |
| OAuth / odnośniki do aplikacji / App Links | eksporty manifestu, schematy, walidacja wywołania zwrotnego, `state`, PKCE, warianty flag | starszy wariant bez `state`/PKCE; własne schematy URI nie mają własności App Links | **MOB-A-03/A-09, P0** |
| TLS, przypinanie certyfikatów, CT i WebView | profile klientów, wartości produkcyjne, flagi, obsługa błędów, JS, dostęp do plików i treść mieszana | dobre wartości domyślne, ale kod akceptujący połączenie mimo błędu pozostaje; CT bez kontroli świeżości | **MOB-A-01/A-02** |
| Wyjście poza katalog / eksport plików | `Content-Disposition`, nazwy DTO, `MediaStore`, bezpośrednie ścieżki, FileProvider i uprawnienia | warunkowe wyjście poza katalog na API 26–28; dostawca prywatny, lecz zbyt szeroki | **MOB-A-10 / MOB-O-02** |
| Niebezpieczna deserializacja Java | `ObjectInputStream`, `readObject`, `readSerializable`, źródła danych | WorkManager czyta typy prymitywne; `Serializable` ograniczone do wewnętrznych typów Parcel; brak niezaufanego `readObject` | brak potwierdzonego wektora; testowanie mutacyjne parserów nadal wymagane |
| Deserializacja polimorficzna JSON | adaptery Gson, metadane typów i miejsca wywołań | nie znaleziono aplikacyjnego polimorficznego tworzenia dowolnych klas z niezaufanego JSON | brak potwierdzonego wektora |
| Wstrzyknięcie SQL | zapytania `raw query`/`execSQL`, konkatenacje i wiązanie argumentów | Room/generowany SQL i parametry wiązane; trafienia surowe biblioteczne/literalne | brak potwierdzonego wektora; serwer poza zakresem |
| Dynamiczne ładowanie kodu / wstrzyknięcie polecenia powłoki | `DexClassLoader`/`PathClassLoader`, refleksja, `Runtime.exec`/`ProcessBuilder` | mechanizm ładowania modułów GMS i stałe polecenia RootBeer; brak danych aplikacyjnych sterujących kodem lub komendą | brak potwierdzonego wektora |
| Przejęcie `PendingIntent` | wszystkie bezpośrednie `PendingIntent.get*`, flagi i cele | własne akcje niemutowalne; jedna mutowalna funkcja pomocnicza GMS do nieuprzywilejowanego fikcyjnego pakietu | dług biblioteczny, bez potwierdzonej podatności |
| Atak przez zasłanianie interfejsu | flagi okna, obsługa zasłoniętego dotyku, ukrywanie nakładek, manifest | `FLAG_SECURE` obecny, brak aplikacyjnej ochrony zasłoniętego dotyku | **MOB-O-04, test dynamiczny** |
| Losowość bezpieczeństwa | `Random`, `SecureRandom`, miejsca użycia IV, soli i tokenów | trafienia `Random` dotyczą interfejsu, mediów i bibliotek; ścieżki bezpieczeństwa używają `SecureRandom`, poza stałą solą MOB-A-06 | brak dodatkowego ustalenia |
| Logowanie sekretów / schowek | zdalny mechanizm logowania, `toString()` DTO sekretów, miejsca użycia schowka | brak potwierdzonego logowania wartości sekretów; ryzykowne `toString()` i pełne URL; schowek używany jawnie przez użytkownika | **MOB-O-01/O-03**, test telemetrii |
| Kod natywny | inwentaryzacja ABI, ELF NX/RELRO/BIND\_NOW/ochrona stosu, eksporty wysokiego poziomu | podstawowe utwardzenie ARM64 dobre; brak pełnej inżynierii wstecznej i testowania mutacyjnego | uzupełnić dla 4 ABI i JNI |
| Uprawnienia `root`, instrumentacja i poświadczenie integralności | RootBeer/BotSense, Play Integrity, poświadczenie klucza, punkty decyzji i model zaufania | token z losową wartością jednorazową i wyzwaniem klucza są obecne; polityki walidacji serwera nie widać; klient po przejęciu lub instrumentacji pozostaje środowiskiem kontrolowanym | granica modelu; serwer nie może ufać wyłącznie wynikowi klienta |

## 19. Kod natywny i pozostała nieprzejrzysta powierzchnia

Pakiety dzielone dla `arm64-v8a`, `armeabi-v7a`, `x86` i `x86_64` zawierają po 11 bibliotek natywnych:

| Grupa | Biblioteki |
|---|---|
| TLS / kryptografia / dane | `libconscrypt_jni.so`, `libsqlcipher.so` |
| Telemetria | `libsentry.so`, `libsentry-android.so` |
| OCR / obraz / kody | `libmlkit_google_ocr_pipeline.so`, `libbarhopper_v3.so`, `libimage_processing_util_jni.so`, `libsurface_util_jni.so` |
| AndroidX / DataStore | `libandroidx.graphics.path.so`, `libdatastore_shared_counter.so` |
| Detekcja środowiska | `libtoolChecker.so` |

Kontrola nagłówków plików i nagłówków programu wszystkich 11 bibliotek w pakiecie ARM64 przyniosła wynik pozytywny:

| Kontrola ELF | Wynik ARM64 | Interpretacja |
|---|---|---|
| typ ELF `DYN` | 11/11 | prawidłowy format współdzielonych obiektów pozycyjnie niezależnych; nie należy utożsamiać tego mechanicznie z testem PIE wykonywalnego pliku |
| stos bez prawa wykonania (`GNU_STACK` = `RW`, bez `E`) | 11/11 | NX dla stosu skonfigurowane poprawnie |
| `GNU_RELRO` oraz `BIND_NOW` | 11/11 | pełny RELRO we wszystkich badanych bibliotekach |
| odwołanie do `__stack_chk_fail` | 10/11 | ochrona stosu widoczna w bibliotekach zawierających kwalifikujące funkcje |

Jedynym plikiem bez odwołania do `__stack_chk_fail` jest liczący około 10 KiB `libdatastore_shared_counter.so`. Eksportuje krótkie operacje JNI na liczniku, `mmap` i `ftruncate`; brak symbolu nie dowodzi globalnego wyłączenia ochrony, ponieważ kompilator wstawia ochronę stosu tylko do funkcji spełniających jego kryteria. Dlatego nie utworzono z tego pozornej podatności. Uczciwy wniosek brzmi: **konfiguracja podstawowego utwardzenia ELF dla ARM64 jest dobra**, ale sam zestaw flag nie dowodzi bezpieczeństwa logiki ani parserów. Zestawienie wszystkich jedenastu bibliotek z ich pochodzeniem prowadzi do wniosku odrębnego od samego utwardzenia: każda odpowiada znanemu komponentowi zewnętrznemu — Google i AndroidX (ścieżki graficzne, DataStore, ML Kit OCR, Barhopper, przetwarzanie obrazu i powierzchni), Conscrypt nad BoringSSL, SQLCipher firmy Zetetic, SDK Sentry oraz otwartoźródłowy RootBeer. Pochodzenie tego ostatniego potwierdza sama warstwa Java: `RootBeerNative.java` ładuje bibliotekę wywołaniem `System.loadLibrary("toolChecker")`, co wiąże przemianowany plik `libtoolChecker.so` z pakietem `com.scottyab.rootbeer`. W badanym zestawie nie zidentyfikowano ani jednej biblioteki natywnej wytworzonej przez COI lub PWPW; cała logika własna aplikacji — e-dowód, kryptografia tożsamości, przepływy uwierzytelniania i interfejs — pozostaje w bajtkodzie JVM. Ma to dwojaki skutek: z jednej strony powierzchnia natywna nie kryje autorskiej kryptografii ani parserów, które wymagałyby osobnej inżynierii wstecznej; z drugiej — jedyny natywny mechanizm bezpieczeństwa aplikacji, detekcja roota, jest niezmienioną biblioteką otwartoźródłową o znanym i trywialnie obchodzalnym zachowaniu.

Jedyna pozycja w grupie „detekcja środowiska”, `libtoolChecker.so`, nie jest komponentem własnym. Analiza symboli i ciągów znakowych wskazała przemianowaną natywną część otwartoźródłowego RootBeera: biblioteka eksportuje `Java_com_scottyab_rootbeer_RootBeerNative_checkForRoot` oraz `Java_com_scottyab_rootbeer_RootBeerNative_setLogDebugMessages`, importuje jedynie `fopen`, `fclose` i `__android_log_print`, a jej działanie sprowadza się do sprawdzania obecności znanych binarek roota po ścieżkach (ciąg „LOOKING FOR BINARY: %s”). Nie zawiera `ptrace`, kontroli antydebugowej ani innych mechanizmów. Zmiana nazwy pliku ukrywa pochodzenie, lecz nie zmienia zachowania. Potwierdza to wcześniejszą ocenę: wykrywanie roota podnosi koszt prostych modyfikacji, ale pozostaje heurystyką, a nie granicą zaufania.

Zestaw kontroli RootBeera jest jawny i dobrze udokumentowany. Sprawdzane są: obecność binarki `su` w typowych ścieżkach oraz busyboxa i artefaktów Magiska; zainstalowane pakiety menedżerów roota i aplikacji maskujących; znacznik `test-keys` w `ro.build.tags`; niebezpieczne właściwości systemu, na przykład `ro.debuggable` i `ro.secure`; a także zamontowanie partycji systemowych w trybie do zapisu. Część natywna ogranicza się do `fopen` po ścieżkach przekazanych z warstwy Java.

Wszystkie te kontrole operują na stanie widzianym przez proces aplikacji, dlatego poprawnie skonfigurowany Magisk je neutralizuje. Mechanizm DenyList wraz z Zygiskiem działa w przestrzeni nazw montowań konkretnej aplikacji: odmontowuje nakładki i moduły, tak że partycje systemowe są widziane jako tylko do odczytu, a binarka `su` oraz pliki Magiska pozostają niewidoczne dla tego procesu. Ukrycie i przepakowanie menedżera pod losową nazwą pakietu omija wykrywanie po nazwie, a przywrócenie właściwości systemu do wartości domyślnych usuwa sygnały `test-keys` oraz niebezpiecznych property. Po tej stronie wszystkie heurystyki zwracają wynik negatywny.

Niezależnie od Magiska sama lokalizacja kontroli w procesie klienta czyni ją podatną na instrumentację: przechwycenie metod narzędziem takim jak Frida lub moduł LSPosed sprawia, że wynik `isRooted()` jest zawsze negatywny, bez jakiejkolwiek modyfikacji systemu plików. Płynie stąd wniosek spójny z resztą raportu: rozstrzygający sygnał integralności środowiska musi pochodzić z mechanizmu poświadczanego sprzętowo i weryfikowanego po stronie serwera, takiego jak Play Integrity, a nie z heurystyki wykonywanej przez badany proces.

Przegląd wszystkich bezpośrednich konstrukcji `PendingIntent.get*` również nie wykazał mutowalnej akcji należącej do aplikacji. Własne powiadomienia używają wartości `201326592`, czyli `FLAG_IMMUTABLE | FLAG_UPDATE_CURRENT`: `FirebaseService.java`, `x54/c.java`. Funkcja pomocnicza Google Play Services wybiera flagę niemutowalności dla przebiegu interfejsu: `s5/r.java`. Wyjątkiem jest odziedziczona funkcja pomocnicza rejestracji Google, która od Androida 12 tworzy mutowalny `PendingIntent` do pakietu `com.google.example.invalidpackage`: `fg/c.java`, `wg/a.java`. Jest on przekazywany jako identyfikator pochodzenia, lecz nie wskazuje uprzywilejowanej akcji aplikacji i w badanym śladzie nie daje odbiorcy kontroli nad funkcją mObywatela. Pozostaje długiem bibliotecznym: należy potwierdzić aktualność zależności i usunąć mechanizm, jeżeli współczesny protokół rejestracji go nie wymaga.

Obecny etap nie objął kompletnej inżynierii wstecznej, symbolicznego śledzenia wejść ani testowania mutacyjnego JNI. Szczególnie istotne są parsery obrazu i kodów, SQLCipher, Conscrypt oraz granice Java↔JNI, ponieważ błędy pamięci w kodzie natywnym mogą zmienić obserwację biblioteczną w wykonanie kodu lub eskalację wewnątrz procesu aplikacji.

Niezbędne uzupełnienie: skróty kryptograficzne każdej biblioteki dla każdej ABI, identyfikacja wersji i `build-id`, powtórzenie kontroli utwardzenia dla pozostałych ABI, kontrola CFI/PAC/BTI, mapowanie eksportów JNI, testowanie mutacyjne danych wejściowych i porównanie z komunikatami projektu źródłowego. Brak pełnej analizy kodu natywnego jest jawną luką zakresu, a nie przemilczanym zapewnieniem bezpieczeństwa.

## 20. Publiczny kod, licencja i niezależny demonstrator C++23/WinAPI

### 20.1. Co rzeczywiście opublikowano

Sprawdzona kopia publicznego repozytorium wskazywała zmianę `d39d2f947b1078332d758de086cb3cb7dd163940` z 4 stycznia 2026 r. i zawierała 192 pliki Kotlin. Zakres odpowiada systemowi wzornictwa i interfejsowi użytkownika, a nie pełnej aplikacji. Pliki COI są objęte dołączoną licencją MIT; licencję kopii repozytorium i prawa do innych składników należy rozpatrywać oddzielnie. Licencja na kod nie udziela automatycznie prawa do znaków, nazwy, urzędowych symboli ani sugerowania autoryzacji państwowej.

Oficjalny komunikat z 29 grudnia 2025 r. mówi o publikacji „kodu źródłowego aplikacji”. Technicznie dostępny zakres nie umożliwia zbudowania aplikacji ani audytu warstw bezpieczeństwa. To uzasadnia precyzyjną krytykę braku przejrzystości, ale nie uprawnia do twierdzenia, że każda brakująca część jest wadliwa. [Komunikat Ministerstwa Cyfryzacji](https://www.gov.pl/web/cyfryzacja/ministerstwo-cyfryzacji-opublikowalo-kod-zrodlowy-mobywatela).

### 20.2. Pochodzenie badanej kopii kodu

Ustalenia tej i następnej części opierają się na kopii, której pochodzenie wymaga osobnego opisu, ponieważ wpływa ono na zakres ważności wniosków.

Oficjalna publikacja ma postać strony `https://www.mobywatel.gov.pl/kod-zrodlowy-mobywatel-mobilny`, dostępnej **po potwierdzeniu tożsamości** aplikacją mObywatel, profilem zaufanym, bankowością lub eID. Komunikat BIP Ministerstwa Cyfryzacji wywodzi ten wymóg z opinii CSIRT MON i uzasadnia go „zapewnieniem kryterium rozliczalności użytkowników”. Strona udostępnia kod w przeglądarce galerii, z numerami wierszy wplecionymi w wyświetlany tekst i zablokowanym prawym przyciskiem myszy. Środowisko kontroli wersji, historia zmian, mechanizm zgłaszania błędów i możliwość porównania wydań pozostają poza tą publikacją.

Praktyczną konsekwencją takiej formy jest to, że pozyskanie kodu do analizy wymaga skryptu użytkownika do pobrania plików, skryptu do odtworzenia drzewa katalogów i skryptu do usunięcia numerów wierszy. Narzędzia te powstały w społeczności i są dostępne wraz z kopiami lustrzanymi. Licencja MIT zezwala na redystrybucję, więc kopie te działają legalnie — i to one, a nie kanał urzędowy, stanowią realne źródło dla niezależnego badacza.

**Materiał badany w tej sekcji.** Kopia lokalna obejmuje 192 pliki Kotlin systemu wzorniczego i jest **identyczna bit w bit** z kopią lustrzaną [neziw/mObywatel-mirror](https://github.com/neziw/mObywatel-mirror/tree/master/Android/src/main/kotlin/pl/gov/coi/common/ui/ds): zgadza się 192 z 192 sum kontrolnych blobów Gita, przy zerowej liczbie plików nadmiarowych i brakujących. Kopia lustrzana została utworzona 29 grudnia 2025 r. i zamrożona 30 grudnia 2025 r.

Druga kopia lustrzana, [fajfer/mObywatel](https://github.com/fajfer/mObywatel), zamrożona 4 stycznia 2026 r. na zmianie `d39d2f947b1078332d758de086cb3cb7dd163940` przywołanej w sekcji 20.1, zawiera **ten sam zbiór 192 plików**. Porównanie treści wykazało różnice w 10 plikach, ograniczone wyłącznie do białych znaków na końcach wierszy, usuniętych przez opiekuna tej kopii. Dwa niezależne zgrania tej samej publikacji dają zatem zgodny kod źródłowy, co pozwala traktować badany materiał jako wierny oryginałowi.

**Rozgraniczenie czasowe.** Kod źródłowy stanowi zrzut z przełomu grudnia 2025 i stycznia 2026 r., badany we wrześniu 2026 r., a więc po ośmiu miesiącach. Analiza binarna opisana w pozostałych sekcjach dotyczy pakietu 4.87.2 pobranego i rozłożonego w sierpniu i wrześniu 2026 r. Ustalenia jakościowe z sekcji 20.3 zachowują ważność **dla tego zrzutu** i wymagają ponownej weryfikacji wobec bieżącego stanu publikacji. Ustalenia dotyczące bezpieczeństwa opierają się na binarce i pozostają od tego rozgraniczenia niezależne. Ponieważ kanał urzędowy nie udostępnia historii zmian, ustalenie, które z opisanych usterek naprawiono w międzyczasie, wymaga ponownego zalogowania się i ręcznego porównania plików.

---

### 20.3. Jakość opublikowanego kodu — przegląd statyczny

Publikacja na licencji MIT czyni ten przegląd uprawnionym i odtwarzalnym. Kod powstał ze środków publicznych, a warunki licencji zezwalają na jego badanie, kopiowanie i modyfikowanie z zachowaniem noty licencyjnej. Otwarcie kodu oznacza więc przyjęcie stałej kontroli zewnętrznej: od dnia publikacji każda usterka w tej warstwie pozostaje sprawdzalna przez dowolnego czytelnika.

Ocena obejmuje wyłącznie **rzemiosło programistyczne i architekturę** opublikowanej warstwy interfejsu. Materiał ten pozbawiony jest warstw rozstrzygających o bezpieczeństwie (zob. sekcja 1), wobec czego ustalenia oznaczone `MOB-Q-xx` mają charakter jakościowy i pozostają poza klasyfikacją podatności. Numerację wydzielono z ciągów `MOB-A-xx` i `MOB-O-xx` celowo, aby granica ta pozostała czytelna także po wyrwaniu tabeli z kontekstu.

**Zakres i metoda.** 192 pliki Kotlin, 18 885 wierszy. Z tego 4 894 wiersze (25,9%) w 33 plikach dostawców danych podglądowych pełnią funkcję materiału poglądowego dla środowiska programistycznego; realny kod komponentów obejmuje około 14 tysięcy wierszy. Przegląd objął wzorce Compose, stabilność rekompozycji, trwałość stanu interfejsu, spójność systemu tokenów wzorniczych, granicę publicznego API, warstwową strukturę zależności oraz dostępność. Każde ustalenie wskazuje plik i numer wiersza, co pozwala je zweryfikować niezależnie na kopii opisanej w sekcji 20.2. Ustalenia tej sekcji poddano niezależnej kontroli krzyżowej z użyciem odrębnego zestawu narzędzi analitycznych, prowadzonej na tej samej kopii materiału. Kontrola potwierdziła ustalenia zachowane poniżej, doprowadziła do wycofania jednego zarzutu dotyczącego lokalizacji tekstów — opisanego w części poświęconej warsztatowi — oraz wniosła cztery ustalenia grupy A i D. Tezy zgłoszone w toku tej kontroli, których nie potwierdziło czytanie kodu źródłowego, odrzucono i nie ujęto ich w zestawieniu.

#### Zestawienie ustaleń jakościowych

Ustalenia pogrupowano tematycznie, ponieważ dopiero zestawione razem oddają charakter materiału. Grupa A gromadzi defekty dostępności, grupa B usterki implementacyjne, grupa C decyzje architektoniczne, grupa D spójność i utrzymanie.

| ID | Grupa | Ustalenie | Dowód |
|---|---|---|---|
| MOB-Q-01 | A | Etykieta zamknięcia komunikatu o błędzie powielona z wariantu sukcesu | `alert/AlertData.kt:110` |
| MOB-Q-14 | A | Przycisk zamknięcia banera pozbawiony etykiety we wszystkich czterech wariantach | `banner/BannerData.kt:46,63,80,106` |
| MOB-Q-15 | A | Akcja dostępnościowa zadeklarowana jako nieobsłużona | `custom/clickabletext/CustomClickableText.kt:40` |
| MOB-Q-16 | A | Wyłączone wymuszanie minimalnego celu dotykowego dla wszystkich przycisków | `button/Button.kt:341-346` |
| MOB-Q-17 | A | Wymóg WCAG dotyczący ogniska nagłówka zrealizowany jako oznaczony hack czasowy | `topappbar/TopAppBarUtil.kt:10-28` |
| MOB-Q-02 | B | Literalny znak `$` w identyfikatorze wskutek podwojenia w interpolacji | `textinput/TextField.kt:363` |
| MOB-Q-03 | B | Wiązanie `?:` z `.let` daje identyfikator w postaci łańcucha `null` | `textinput/TextField.kt:104-106` |
| MOB-Q-04 | B | Wywołanie zwrotne otrzymuje stan sprzed zmiany | `textinput/TextField.kt:233-234` |
| MOB-Q-18 | B | Wiersz przełącznika pomija własność `enabled`, w dwóch komponentach przeciwnie | `switchcomponent/SwitchWithExtras.kt:85`, `SwitchWithText.kt:52` |
| MOB-Q-05 | C | Komponenty pozbawione parametru `modifier` i gniazd treści | 50 z 58 publicznych funkcji komponujących |
| MOB-Q-06 | C | System wzorniczy sprzężony z warstwą dziedzinową aplikacji | `Label` w 75 z 192 plików, `ValidationState` w 15 |
| MOB-Q-07 | C | Modele danych przenoszą prezentację i kod komponujący | 18 z 56 plików `*Data*.kt` zawiera `@Composable` |
| MOB-Q-08 | C | Teksty pobierane ze statycznego dostawcy globalnego | 12 plików, 0 użyć `stringResource` |
| MOB-Q-09 | D | Równoległe użycie Material 2 i Material 3 | 16 plików M2, 21 M3, oba w `SingleCardDocumentRow.kt` |
| MOB-Q-10 | D | Adnotacje stabilności postawione poza miejscami, w których działają | 2 ze 113 klas `data class` |
| MOB-Q-11 | D | Stan interfejsu wrażliwy na zmianę konfiguracji | 0 użyć `rememberSaveable` przy 9 miejscach `remember` |
| MOB-Q-12 | D | Kolory wpisane wprost, z luką w palecie tokenów | 9 miejsc, m.in. `alert/AlertData.kt:96`, `controllers/ControllerSwitch.kt:76` |
| MOB-Q-19 | D | Komponenty przestarzałe w wydaniu, część odsyła do kodu nieopublikowanego | 7 z 58 komponentów opatrzonych `@Deprecated` |
| MOB-Q-13 | D | Narzędzia podglądu i pozostałości robocze w publicznym API | 49 z 50 funkcji `@Preview` publicznych; 3 numery zgłoszeń wewnętrznych |

---

#### Grupa A — dostępność

Warstwa dostępności wymaga omówienia w pierwszej kolejności, ponieważ łączy najbogatszą infrastrukturę w całym materiale z największym skupieniem defektów. Wielkość nakładu opisano niżej, w części poświęconej warsztatowi; tutaj zebrano miejsca, w których ten nakład zawodzi.

##### MOB-Q-01 — komunikat o błędzie zapowiadany etykietą sukcesu

Plik `alert/AlertData.kt` definiuje cztery warianty komunikatu, każdy z domyślną etykietą dostępności przycisku zamknięcia. Trzy pierwsze zachowują konsekwencję, czwarty powiela etykietę wariantu trzeciego:

| Wariant | Wiersz | Domyślna etykieta zamknięcia |
|---|---|---|
| `Info` | 47 | `commonAccessibilityCloseInformation()` |
| `Warning` | 67 | `commonAccessibilityCloseWarningInformation()` |
| `Success` | 87 | `commonAccessibilityCloseSuccessInformation()` |
| `Error` | 110 | `commonAccessibilityCloseSuccessInformation()` |

Użytkownik czytnika ekranu zamykający komunikat o błędzie usłyszy zapowiedź przypisaną komunikatowi o powodzeniu. Wartość ta pełni rolę domyślnej, wobec czego zachowanie ujawnia się wszędzie tam, gdzie strona wywołująca pozostawia parametr bez zmiany. Zawartość dostawcy `CommonUILabelProvider` pozostaje poza publikacją, wobec czego istnienie odrębnej etykiety dla wariantu błędu wymaga potwierdzenia po stronie wykonawcy; sama asymetria wobec trzech pozostałych wariantów wynika wprost z kodu.

##### MOB-Q-14 — niema etykieta zamknięcia banera

Bliźniaczy komponent rozwiązuje ten sam problem inaczej i z gorszym skutkiem. Wszystkie cztery warianty w `banner/BannerData.kt` przyjmują jako wartość domyślną etykietę pustą:

```kotlin
closeIconContentDescription: Label = Label.EMPTY   // wiersze 46, 63, 80, 106
```

Wartość ta trafia wprost do `contentDescription` ikony zamknięcia (`banner/BannerData.kt:29,95,121`). Czytnik ekranu napotyka przycisk pozbawiony nazwy i zapowiada go opisem zastępczym systemu. Zestawienie z `AlertData`, gdzie każdy wariant dostarcza etykietę rzeczywistą, pokazuje dwa różne standardy dostępności w obrębie jednego systemu wzorniczego dla dwóch komponentów o tej samej funkcji.

##### MOB-Q-15 — akcja dostępnościowa zgłaszana jako nieobsłużona

`custom/clickabletext/CustomClickableText.kt:38-41`:

```kotlin
.semantics {
  role = Role.Button
  onClick { false }
}
```

Wartość zwracana przez procedurę `onClick` w semantyce informuje usługę dostępności o powodzeniu akcji. Zapis `{ false }` deklaruje akcję trwale nieobsłużoną, przy jednoczesnym ogłoszeniu elementu jako przycisku. Właściwa obsługa dotknięcia realizowana jest przez `ClickableText` na poziomie zdarzeń wskaźnika, a więc poza ścieżką akcji dostępnościowych, co pozbawia ten element drogi zastępczej. Komponent nosi adnotację `@Deprecated` odsyłającą do `Link` i `ButtonText` (zob. MOB-Q-19), co ogranicza zasięg ustalenia do miejsc, w których pozostaje w użyciu.

##### MOB-Q-16 — wyłączone wymuszanie minimalnego celu dotykowego

`button/Button.kt:341-346` opakowuje każdy przycisk systemu w dostawcę wyłączający platformowy mechanizm minimalnego pola dotykowego:

```kotlin
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
  content()
}
```

Mechanizm ten istnieje w Material 3 po to, aby gwarantować cel dotykowy o wymiarach 48×48 dp niezależnie od rozmiaru rysowanego elementu. Wyłączenie obejmuje wszystkie przyciski systemu, w tym warianty `ButtonSize.Small` o wysokości `spacing400` (`button/Button.kt:322`) oraz przyciski ikonowe o wymiarze `spacing400` (`button/Button.kt:334`, `:289`). Wartości liczbowe `AppTheme.dimensions` należą do nieopublikowanego pakietu `theme`, wobec czego wynikowy rozmiar w jednostkach dp wymaga potwierdzenia po stronie wykonawcy. Rozstrzygnięty pozostaje natomiast sam fakt: zabezpieczenie ustanowione dla spełnienia kryteriów WCAG 2.5.5 i 2.5.8 zostało świadomie i globalnie wyłączone, a odpowiedzialność za rozmiar celu przeniesiona na wywołującego.

Wykorzystane API nosi ponadto status wycofanego w nowszych wydaniach Material 3 na rzecz `LocalMinimumInteractiveComponentSize`.

##### MOB-Q-17 — wymóg WCAG zrealizowany jako oznaczony hack

`topappbar/TopAppBarUtil.kt:10-28` zawiera funkcję opatrzoną adnotacją, która sama opisuje swój charakter:

```kotlin
@Deprecated(
  "This is hack for WCAG requirements to focus titles on every screen enter." +
    " Should be deleted with task MOB-72178",
)
@Composable
fun forceFocusOnStart(forceFocusTrigger: Boolean?): FocusHost {
  ...
  LaunchedEffect(forceFocusTrigger) {
    delay(1L)
    focusManager.clearFocus(true)
    delay(1L)
    focusHost.requestFocus()
  }
```

Przeniesienie ogniska uwagi na nagłówek ekranu — wymóg wynikający z kryteriów WCAG — oparto na dwóch opóźnieniach o długości jednej milisekundy, sekwencjonujących wyczyszczenie i zażądanie ogniska. Konstrukcja tego rodzaju zależy od czasu wykonania kompozycji i zachowuje się różnie na urządzeniach o odmiennej wydajności. Funkcja pozostaje w czynnym użyciu we wszystkich czterech wariantach paska: `topappbar/large/LargeTopAppBar.kt:43`, `medium/MediumTopAppBar.kt:42` oraz `small/SmallTopAppBar.kt:49` i `:75`.

Adnotacja przewiduje usunięcie funkcji zadaniem MOB-72178. Zgodność z wymogiem dostępności opiera się zatem na rozwiązaniu, które zespół autorski zakwalifikował jako tymczasowe, a mimo to wydał publicznie i utrzymuje we wszystkich ekranach aplikacji.

---

#### Grupa B — usterki implementacyjne

##### MOB-Q-02 — literalny `$` w identyfikatorze

`textinput/TextField.kt:363`:

```kotlin
.let { tag -> "$tag${indexTag?.let { "_$$indexTag" } ?: ""}" }
```

Zapis `"_$$indexTag"` daje wynik `_$5` zamiast `_5`: pierwszy znak `$` poprzedza kolejny `$`, wobec czego pozostaje znakiem dosłownym. Ta sama klasa dwie funkcje wyżej, w wierszu 311, zawiera zapis poprawny — `"_$indexTag"`. Różnica jednego znaku między dwoma niemal identycznymi fragmentami wskazuje na powielenie kodu bez ponownego przeczytania. Skutek obejmuje identyfikator wykorzystywany przez testy automatyczne oraz warstwę dostępności przycisku odsłaniającego hasło.

##### MOB-Q-03 — łańcuch `null` w identyfikatorze

`textinput/TextField.kt:104-106`:

```kotlin
testTag = data.testTag ?: data.label?.tag
  .let { tag -> "${tag}EditText" }
  .let { tag -> "$tag${data.indexTag?.let { "_${data.indexTag}" } ?: ""}" }
```

Wywołania `.let` wiążą się z wyrażeniem `data.label?.tag`, pozostawiając poza swoim zasięgiem człon poprzedzający operator `?:`. Bezpieczny dostęp zabezpiecza wyłącznie odczyt właściwości `tag`, natomiast następujące po nim `.let` wykonuje się również dla wartości pustej. Gdy `testTag` oraz `label` przyjmują wartość `null` — oba pozostają opcjonalne z domyślnym `null` (`textinput/model/TextInputData.kt:16,29`) — identyfikator przybiera dosłowną postać `nullEditText`. Poprawny zapis wymaga ujęcia całego wyrażenia w nawias albo użycia `?.let`.

##### MOB-Q-04 — wywołanie zwrotne ze stanem sprzed zmiany

`textinput/TextField.kt:233-234`:

```kotlin
changePasswordVisibility(!isPasswordVisible)
data.onPasswordVisibilityChanged(isPasswordVisible)
```

Pierwszy wiersz przełącza widoczność hasła, drugi przekazuje na zewnątrz wartość sprzed przełączenia. Odbiorca `onPasswordVisibilityChanged` otrzymuje zatem informację odwrotną wobec stanu faktycznego. Jeżeli warstwa nadrzędna opiera na tym sygnale rejestrowanie zdarzeń, zasłanianie zrzutu ekranu albo decyzję o maskowaniu pola, zachowanie ulegnie odwróceniu. Warstwa konsumująca ten sygnał pozostaje poza publikacją, wobec czego ustalenie zgłaszam jako wadę poprawności o nieokreślonym zasięgu.

##### MOB-Q-18 — wiersz przełącznika pomijający własność `enabled`

Model `switchcomponent/SwitchData.kt:11` udostępnia własność `val enabled: Boolean`, a oba komponenty przekazują ją do samego przełącznika (`SwitchWithExtras.kt:157`, `SwitchWithText.kt:86`). Obszar dotykowy całego wiersza pomija ją natomiast w obu, i to w przeciwnych kierunkach:

```kotlin
SwitchWithExtras.kt:85    .toggleable(role = Role.Switch, value = data.checked, enabled = true,  …)
SwitchWithText.kt:52      .toggleable(role = Role.Switch, value = data.checked, enabled = false, …)
```

W `SwitchWithExtras` przełącznik przedstawiony użytkownikowi jako wyłączony pozostaje czynny: dotknięcie dowolnego miejsca wiersza wywołuje `onCheckedChange` i zmienia stan. Jest to wada funkcjonalna, obserwowalna dla każdego użytkownika, a nie wyłącznie dla korzystających z technologii wspomagających. W `SwitchWithText` wartość przeciwna wygasza obszar dotykowy wiersza niezależnie od ustawienia modelu. Dwa bliźniacze komponenty wpisały zatem przeciwne wartości tego samego parametru, żaden nie odczytuje własności modelu, a rozbieżność wskazuje, że nikt nie zestawił ich ze sobą.

---

#### Grupa C — architektura

##### MOB-Q-05 — komponenty zamknięte na kompozycję

Spośród 58 publicznych funkcji komponujących najwyższego poziomu — z pominięciem podglądów i funkcji rozszerzających — parametr `modifier` przyjmuje **8**. Pozostałe 50 obejmuje między innymi `button/Button.kt:49`, `accordion/Accordion.kt:46`, `dialog/Dialog.kt:31` i `topappbar/TopAppBar.kt:18`. Gniazda treści (`content: @Composable`) występują w pięciu komponentach publicznych.

Wytyczne API dla Compose traktują `modifier` jako pierwszy parametr opcjonalny każdej funkcji emitującej interfejs. Jego pominięcie odbiera stronie wywołującej możliwość nadania odstępu, rozmiaru, wagi w układzie nadrzędnym oraz własnego identyfikatora bez opakowania komponentu w dodatkowy kontener. W zamian przyjęto wzorzec pojedynczego obiektu `Data` jako jedynego parametru, co dodatkowo wyklucza wartości domyślne dla poszczególnych właściwości i wymusza przebudowę całego obiektu przy zmianie jednej z nich.

Powtarzalność tego rozwiązania — 50 przypadków na 58 — nadaje mu rangę przyjętej konwencji. Konwencja ta obsługuje wygodnie zespół autorski, znający wszystkie miejsca użycia, i utrudnia ponowne wykorzystanie biblioteki każdemu zespołowi zewnętrznemu. Publikacja systemu wzorniczego zakłada odbiorcę zewnętrznego, wobec czego przyjęty wzorzec pozostaje w sprzeczności z celem samej publikacji. Uznaję to ustalenie za najpoważniejsze architektonicznie w całej sekcji.

##### MOB-Q-06 — system wzorniczy związany z dziedziną aplikacji

Typ `Label` z pakietu `pl.gov.coi.common.domain.label` występuje w **75 ze 192 plików**, czyli w 39% materiału. Obok niego warstwa interfejsu sięga po `ValidationState` z `domain.validators` (15 plików) oraz `Mapper` z `domain` (4 pliki). Komponent `textinput/TextField.kt:88` rozgałęzia wygląd obramowania wprost na typie `ValidationState.Invalid`, a więc na typie dziedzinowym aplikacji.

System wzorniczy z założenia pozostaje niezależny od dziedziny i operuje typami własnymi albo typami platformy. Przyjęta struktura wiąże bibliotekę z konkretną aplikacją na poziomie typów, przez co przeniesienie jej do innego produktu wymaga przeniesienia również warstwy dziedzinowej. Tezę tę wzmacnia obecność gotowych ekranów usługowych w materiale: `servicewelcomepage/ServiceWelcomePage.kt` oraz `supportpage/SupportPage.kt` budują pełny ekran wraz z rusztowaniem, paskiem górnym i mechanizmem odświeżania przeciągnięciem. Materiał opisany jako system wzorniczy stanowi zatem warstwę interfejsu jednej aplikacji.

##### MOB-Q-07 — modele danych przenoszące prezentację

Spośród 56 plików o nazwie zawierającej `Data` **18 zawiera kod komponujący** oznaczony `@Composable`, 13 odwołuje się do identyfikatorów zasobów rysunkowych, a 16 do typu `Color`. Plik `alert/AlertData.kt` łączy wszystkie trzy właściwości naraz: definiuje warianty komunikatu wraz z przypisanymi im ikonami (`R.drawable.c1_info`, `c2_warning_mark`, `c4_success`, `c3_error_mark`), kolorami oraz domyślnymi etykietami dostępności.

Rozwiązanie takie umieszcza decyzje wzornicze w warstwie modelu, przez co zmiana ikony albo koloru wariantu wymaga zmiany klasy danych. Testowanie modelu wymaga wówczas środowiska Android wraz z zasobami. Podział na `Komponent` i `KomponentData` zachowuje w tej sytuacji formę rozdziału warstw, pozostawiając odpowiedzialności po obu stronach wymieszane. Ustalenia MOB-Q-01 i MOB-Q-14 pokazują koszt praktyczny: etykiety dostępności obu komponentów żyją w klasach danych, gdzie powielenie wywołania między wariantami przechodzi bez śladu.

##### MOB-Q-08 — teksty ze statycznego dostawcy globalnego

Materiał zawiera **zero użyć `stringResource`**. Teksty pochodzą z obiektu `CommonUILabelProvider`, wywoływanego statycznie wewnątrz funkcji komponujących (`accordion/Accordion.kt:73-75`, `bottomsheet/ModalBottomSheet.kt:144`) oraz w wartościach domyślnych parametrów konstruktorów (`alert/AlertData.kt:43,47,63,67,83,87,106,110`).

Zastrzeżenie dotyczy sposobu dostępu, nie samego istnienia warstwy dostawcy etykiet. Warstwa taka bywa wyborem uzasadnionym, gdy teksty pochodzą ze wspólnego modułu albo z zaplecza serwerowego; badany materiał nie zawiera jednak przesłanek wskazujących na projekt wieloplatformowy, a człon `common` występuje tu jako nazwa modułu. Dostęp statyczny wprowadza natomiast zależność globalną, ukrytą przed sygnaturą funkcji: test jednostkowy komponentu wymaga zainicjowania dostawcy, podmiana tekstów w teście pozostaje niemożliwa bez ingerencji w stan globalny, a mechanizm Compose odpowiadający za reagowanie na zmianę języka i konfiguracji pozostaje nieużyty. Ten sam dostawca przekazywany przez `CompositionLocal` albo parametr zachowałby swoje zalety, usuwając wymienione skutki.

---

#### Grupa D — spójność i utrzymanie

##### MOB-Q-09 — dwa systemy komponentów jednocześnie

Szesnaście plików korzysta z Material 2 (`androidx.compose.material.Card`, `Divider`, `RadioButton`), dwadzieścia jeden z Material 3, a `custom/documentrow/SingleCardDocumentRow.kt` importuje oba naraz. Komponent `Divider` z Material 2 pozostaje wycofany na rzecz `HorizontalDivider`.

Rozkład ten odpowiada obrazowi migracji prowadzonej etapami i stanowi dług migracyjny, a nie zaniedbanie. Konsekwencja pozostaje jednak niezależna od przyczyny: system wzorniczy powołany do ujednolicania wyglądu opiera się na dwóch zestawach domyślnych kolorów, kształtów i wysokości cienia, czytających własne źródła wartości. Rozbieżności wynikające z tego podziału pozostają poza zasięgiem korekty na poziomie tokenów. Wydanie publiczne utrwala stan pośredni i przenosi go na każdego odbiorcę zewnętrznego.

##### MOB-Q-10 — adnotacje stabilności poza miejscem działania

Na 113 klas `data class` adnotację stabilności noszą dwie, obie w `progressbar/ProgressBarData.kt:10,16`. Obejmują one klasy złożone z typów `Int` i `String?`, uznawanych przez kompilator Compose za stabilne z samej definicji. Modele niosące funkcje zwrotne pozostają bez adnotacji, w tym `button/ButtonData.kt:14` z właściwością `onClick: () -> Unit`.

Rozmieszczenie adnotacji wskazuje, że wprowadzono je w reakcji na pojedyncze zgłoszenie, bez przeglądu pozostałych modeli. Skala wynikającego stąd narzutu rekompozycji zależy od tego, czy typy dziedzinowe — przede wszystkim `Label` — pochodzą z modułu kompilowanego z wtyczką Compose; materiał nie obejmuje tego modułu, wobec czego rozstrzygnięcie wymaga raportów metryk kompilatora po stronie wykonawcy.

##### MOB-Q-11 — stan gubiony przy zmianie konfiguracji

Materiał zawiera **zero użyć `rememberSaveable`** przy dziewięciu miejscach przechowujących stan przez `remember { mutableStateOf(...) }`, w tym `accordion/Accordion.kt:70`, `textinput/TextField.kt:68`, `chatbubble/ChatBubble.kt:233` oraz `switchcomponent/SwitchWithExtras.kt:53`.

Rozwinięcie sekcji, odsłonięcie hasła i rozwinięcie listy źródeł powracają do stanu wyjściowego po obrocie ekranu, zmianie rozmiaru czcionki systemowej, przejściu w tryb wielookienny oraz po odtworzeniu procesu. Dla użytkownika oznacza to cofnięcie formularza w trakcie wypełniania — zachowanie odczuwalne szczególnie przy zmianie rozmiaru czcionki, z której korzystają osoby słabowidzące.

##### MOB-Q-12 — kolory poza systemem tokenów

Dziewięć miejsc kodu produkcyjnego definiuje kolor wprost. Osiem posługuje się zapisem szesnastkowym: `alert/Alert.kt:35`, `alert/AlertData.kt:96`, `badge/Badge.kt:27`, `bottomnavigation/BottomNavigation.kt:98`, `chatbubble/ChatBubble.kt:50-51`, `custom/bulletpoint/BulletPoint.kt:38`, `singlecard/SingleCardStatusBadgeData.kt:254`. Wartość `#EEFAE1` występuje dwukrotnie, w dwóch plikach, pod dwiema różnymi nazwami lokalnymi. Dziewiąte miejsce sięga po stałą nazwaną: `controllers/ControllerSwitch.kt:76` ustawia `selectedContentColor = Color.Blue`, czyli czysty `#0000FF`, w przełączniku zakładek.

Szczególnie wymowny pozostaje `alert/AlertData.kt:96`. Trzy warianty komunikatu czerpią kolor ikony z palety tokenów — `supportBlue100`, `supportOrange100`, `supportRed100` — natomiast wariant `Success` deklaruje zmienną lokalną z wartością `Color(0xFF427639)`, po czym natychmiast ją zwraca. Zieleń powodzenia pozostaje więc nieobecna w palecie tokenów, a lukę wypełniono wartością wpisaną wprost. Każdy taki kolor wypada poza obsługę motywu i ustawień kontrastu.

##### MOB-Q-19 — komponenty przestarzałe i odsyłacze do kodu nieopublikowanego

Adnotację `@Deprecated` nosi **siedem z pięćdziesięciu ośmiu** publicznych komponentów, czyli 12% opublikowanego systemu:

| Komponent | Treść adnotacji |
|---|---|
| `banner/Banner.kt:27` | „This component is deprecated, please use DS component Alert." |
| `custom/clickabletext/CustomClickableText.kt:22` | „For external redirections use Link. For internal redirections use ButtonText" |
| `servicewelcomepage/ServiceWelcomePage.kt:30` | „Use BaseScaffold and Header" |
| `supportpage/SupportPage.kt:31` | „Use BaseScaffold with Header" |
| `singlecard/SingleCard.kt:12` | „Deprecated", z odesłaniem do `pl.gov.coi.common.ui.unmapped.singlecard` |
| `singlecard/radiobutton/OldRadioButton.kt:12` | „Do not use." |
| `topappbar/TopAppBarUtil.kt:10` | hack WCAG opisany w MOB-Q-17 |

Trzy z tych odesłań kierują poza opublikowany materiał. Komponent `BaseScaffold`, wskazany jako następca dwóch pełnych ekranów, nie ma w publikacji ani jednej definicji; pakiet `ui.unmapped`, wskazany jako następca `SingleCard`, należy do zbioru pakietów przywoływanych, lecz nieudostępnionych. Czytelnik otrzymuje zatem instrukcję migracji do komponentów, których nie może obejrzeć.

Zestawienie to nakłada się na MOB-Q-14 i MOB-Q-15: dwa spośród opisanych defektów dostępności dotyczą komponentów już oznaczonych jako przestarzałe, co ogranicza ich zasięg, a zarazem pokazuje, że wycofywanie zatrzymało się na adnotacji.

##### MOB-Q-13 — publiczne API i pozostałości robocze

Spośród 50 funkcji `@Preview` **49 pozostaje publicznych**; żadna nie została ograniczona modyfikatorem `private` ani wydzielona do zestawu źródeł przeznaczonego dla kompilacji rozwojowej. Wraz z nimi publiczne API obejmuje 33 dostawców danych podglądowych z treściami zastępczymi oraz cztery wywołania `println` (`checkbox/group/GroupCheckBoxPPP.kt`, `checkbox/single/CheckBoxSinglePPP.kt`). Występują kolizje nazw: trzy funkcje `ButtonPreview` i dwie `StatusBadgePreview` w różnych pakietach.

W kodzie pozostawiono ponadto trzy odrębne numery zgłoszeń wewnętrznych w sześciu wystąpieniach: `MOB-49304` w komentarzach `TODO REMOVE` (`singlecard/SingleCardStatusBadge.kt:34`, `SingleCardStatusBadgeData.kt:13`, `provider/SingleCardStatusBadgePreviewProvider.kt:9`), `MOB-60795` przy nierozstrzygniętej wątpliwości dotyczącej strefy czasowej (`datepicker/DatePickerDataVMS.kt:19,29`) oraz `MOB-72178` w adnotacji opisanej w MOB-Q-17. Identyfikatory te ujawniają fragment numeracji systemu śledzenia zadań wykonawcy.

Osobno odnotowuję brak not licencyjnych w plikach źródłowych: żaden ze 192 plików nie zawiera nagłówka `Copyright` ani `SPDX`. Licencja MIT wymaga zachowania noty przy kopiach i istotnych częściach oprogramowania, wobec czego pojedynczy skopiowany komponent traci informację o pochodzeniu i warunkach. Porządek ten uzupełnili opiekunowie kopii lustrzanych, dodając pliki licencji i konfigurację REUSE we własnym zakresie. Uwagę zgłaszam jako porządkową.

---

#### Ustalenia po stronie warsztatu

Rzetelność wymaga odnotowania ustaleń przeciwnych, opartych na tych samych pomiarach.

W 18 885 wierszach **nie występuje ani jedno wymuszenie niepustości** (`!!`), brakuje również konstrukcji `lateinit` oraz odwołań do `GlobalScope`. Wyciszenie ostrzeżeń analizatora zastosowano jednokrotnie w całym materiale. Wcięcia pozostają jednolite, bez znaków tabulacji. Modelowanie danych opiera się na 53 klasach i interfejsach zapieczętowanych, co wymusza kompletność rozgałęzień `when`. System tokenów istnieje i pozostaje w realnym użyciu: 971 odwołań do `AppTheme` wobec 9 kolorów wpisanych wprost.

Osobnej uwagi wymaga lokalizacja. Kontrola wszystkich łańcuchów zawierających znaki diakrytyczne języka polskiego, przeprowadzona z rozróżnieniem zasięgu funkcji oznaczonych `@Preview`, wykazała **69 wystąpień, z których wszystkie należą do materiału podglądowego** — 68 w plikach dostawców, jedno w funkcji `QrCodeCustomSingleCardPreview` (`custom/singlecard/labelbuttonimage/LabelButtonImageSingleCard.kt:76`). Kod produkcyjny nie zawiera żadnego twardo wpisanego łańcucha polskiego. Rozdział tekstów od komponentów został zatem utrzymany konsekwentnie, mimo zastrzeżeń do sposobu dostępu opisanych w MOB-Q-08.

Warstwa dostępności dysponuje infrastrukturą rzadko spotykaną w produktach tej wielkości: 150 użyć `contentDescription`, 469 identyfikatorów testowych, 16 deklaracji `role`, 6 oznaczeń nagłówka, 5 opisów stanu, 4 obszary aktywne (`liveRegion`) służące ogłaszaniu błędów walidacji, 2 użycia `invisibleToUser`, własne akcje dostępności w komponentach przełącznika oraz odrębny mechanizm zarządzania ogniskiem uwagi (`FocusHost`, 85 odwołań). Nakład ten jest realny i wykracza poza minimum wymagane ustawą o dostępności cyfrowej.

Zestawienie tego nakładu z grupą A prowadzi jednak do wniosku ostrożniejszego niż sama liczba wywołań. Pięć defektów dostępności — pomylona etykieta błędu, niema etykieta zamknięcia banera, akcja zgłaszana jako nieobsłużona, globalnie wyłączone wymuszanie celu dotykowego oraz wymóg WCAG oparty na opóźnieniach czasowych — leży w warstwie najstaranniej rozbudowanej. Infrastruktura powstała; kontrola jej działania nie nadążyła za jej rozmiarem.

#### Ocena

Rzemiosło w opublikowanej warstwie interfejsu oceniam jako **nierówne**. Dyscyplina utrzymuje się w obszarach poddających się kontroli narzędziowej: brak wymuszeń niepustości, jednolite formatowanie, typy zapieczętowane, konsekwentne użycie tokenów, rozdział tekstów od komponentów. Braki układają się we wzorzec charakterystyczny dla zespołu pracującego pod presją terminu, pozbawionego systematycznego przeglądu kodu przez drugą osobę: dwa systemy komponentów obok siebie, adnotacje stabilności postawione poza miejscem działania, stan gubiony przy obrocie ekranu, narzędzia deweloperskie w publicznym API oraz 12% komponentów oznaczonych jako przestarzałe, częścią odsyłających do kodu, którego nie opublikowano.

Warstwa dostępności wymaga oceny osobnej, ponieważ wypada w tym materiale zarówno najlepiej, jak i najgorzej. Rozmiar nakładu opisano wyżej i pozostaje on niekwestionowany. Równocześnie pięć defektów z grupy A dotyczy tej samej warstwy, a dwa z nich — wyłączenie wymuszania celu dotykowego oraz oparcie wymogu ogniska nagłówka na dwóch opóźnieniach jednomilisekundowych — mają charakter decyzji, nie przeoczenia. Zgodność z kryteriami WCAG opiera się w tych miejscach na rozwiązaniach, które zespół autorski sam zakwalifikował jako tymczasowe albo świadomie wyłączył. Dla aplikacji podlegającej ustawie o dostępności cyfrowej, używanej przez blisko jedenaście milionów osób, jest to obszar wymagający przeglądu w pierwszej kolejności.

Warstwa architektoniczna wymaga oceny surowszej. Cztery ustalenia grupy C opisują wybory konsekwentne, powtórzone w dziesiątkach plików, a więc świadome. Komponenty pozbawione parametru `modifier` i gniazd treści, typy dziedzinowe aplikacji obecne w 39% plików biblioteki, decyzje wzornicze przeniesione do klas danych, teksty pobierane statycznie z obiektu globalnego oraz pełne ekrany usługowe wewnątrz biblioteki składają się na warstwę interfejsu jednej aplikacji, opisaną jako system wzorniczy. Zespół autorski, znający wszystkie miejsca użycia, pracuje w takiej strukturze sprawnie. Odbiorca zewnętrzny, dla którego publikacja została przeznaczona, otrzymuje bibliotekę wymagającą przeniesienia warstwy dziedzinowej, pozbawioną możliwości ułożenia komponentów we własnym układzie i związaną z globalnym dostawcą tekstów. Deklarowany cel publikacji i przyjęta architektura rozchodzą się w tym punkcie wyraźnie.

Wniosek dla całości raportu pozostaje ostrożny. Jakość warstwy wystawionej na widok publiczny rozstrzyga o warstwach zamkniętych w stopniu ograniczonym i wymaga traktowania jako przesłanka, nie dowód. Uprawnia natomiast do wniosku węższego: skoro w części przygotowanej do publikacji przetrwało pięć usterek implementacyjnych i pięć defektów dostępności, w tym pomylone wywołanie między sąsiadującymi wariantami jednej klasy oraz dwa bliźniacze komponenty z przeciwnymi wartościami tego samego parametru, **założenie o istnieniu systematycznego przeglądu kodu w częściach nieopublikowanych pozostaje bez oparcia w dowodach**. Ciężar wykazania jakości spoczywa po stronie wykonawcy i wymaga otwarcia warstw rozstrzygających o bezpieczeństwie — zgodnie z linią przyjętą w sekcjach 1 i 20.1.

---

### 20.4. Granice niezależnej implementacji

Poniższa część ma charakter techniczny. Ocenę prawną planowanego sposobu wykorzystania należy powierzyć prawnikowi właściwej specjalności.

Polska ustawa o prawie autorskim stanowi, że ochrona przyznana programowi komputerowemu obejmuje wszystkie formy jego wyrażenia, natomiast idee i zasady leżące u podstaw jego elementów — w tym u podstaw interfejsów (łączy) — ochronie nie podlegają. Osoba uprawniona do korzystania z egzemplarza może obserwować, badać i testować jego działanie. Dekompilacja ma węższą podstawę: musi być konieczna do interoperacyjności niezależnie stworzonego programu i spełniać warunki art. 75 ust. 2 pkt 3. Uzyskanej informacji nie wolno używać do programu o istotnie podobnej formie wyrażenia ani innych naruszeń. Co istotne, ogólnego dozwolonego użytku osobistego z art. 23 nie stosuje się do programów komputerowych. [Ustawa o prawie autorskim, art. 74–77](https://api.sejm.gov.pl/eli/acts/DU/2025/24/text.pdf).

Z tego wynika bezpieczniejszy profil prywatnego demonstratora:

- własny kod C++23, architektura, układ ekranów, nazwa i grafika;
- funkcjonalna inspiracja i interoperacyjność, bez kopiowania zdekompilowanych fragmentów;
- własny wzór Windows 11: Mica, tryb ciemny i Per-Monitor V2 dodatkowo odróżniają formę wyrażenia;
- wyłącznie fikcyjne dane i stałe oznaczenie `DEMO — nie jest dokumentem ani aplikacją państwową`;
- brak oficjalnych kluczy, certyfikatów, kont produkcyjnych i podszywania się pod autoryzowanego klienta;
- oddzielny dziennik pochodzenia wymagań: publiczna dokumentacja/obserwacja → neutralna specyfikacja → własna implementacja.

Niezależna aplikacja nie staje się przez podobną funkcjonalność aplikacją mObywatel ani dokumentem elektronicznym wywołującym skutki ustawowe. Przed dystrybucją, użyciem realnych protokołów lub danych i integracją z systemami państwowymi potrzebna jest pisemna autoryzacja oraz przegląd prawnika od prawa autorskiego, znaków i systemów teleinformatycznych.

### 20.5. Czy WinAPI oznacza mniejszą powierzchnię ataku

WinAPI i niewielka liczba zależności mogą zmniejszyć ryzyko łańcucha dostaw, rozmiar SBOM oraz liczbę parserów i środowisk wykonawczych. „Zero zależności” nie jest jednak celem bezpieczeństwa samym w sobie: Windows, biblioteki systemowe, sterowniki i kompilator nadal są zależnościami, a własny parser, kryptografia lub TLS prawdopodobnie zwiększą ryzyko.

flowchart TD UI[Własny interfejs Win32
Mica + tryb ciemny + PMv2] --> CORE[Mały rdzeń C++23
RAII + jawne typy] CORE --> NET[WinHTTP
TLS systemowy] CORE --> CRYPTO[CNG: BCrypt / NCrypt
bez własnej kryptografii] CORE --> SECRET[DPAPI / Windows Hello
sekrety związane z użytkownikiem] CORE --> DATA[Minimalny parser
limity rozmiaru i czasu] NET --> SERVICE[Wyłącznie środowisko DEMO
lub autoryzowany punkt końcowy] CRYPTO --> TRUST[Magazyn certyfikatów Windows] SECRET --> TRUST classDef own fill:#e2ecff,stroke:#345da8,color:#12213d; classDef system fill:#d9f2e6,stroke:#16784b,color:#102a20; classDef boundary fill:#fff1c7,stroke:#a66a00,color:#342300; class UI,CORE,DATA own; class NET,CRYPTO,SECRET,TRUST system; class SERVICE boundary; 

Zalecana konfiguracja bazowa: `/std:c++23`, ostrzeżenia jako błędy, `/permissive-`, `/sdl`, `/guard:cf`, ASLR/DEP, CET tam, gdzie są dostępne, Control Flow Guard, podpisywanie binarki, analiza MSVC/clang-tidy, testowanie mutacyjne parserów, testy Application Verifier i brak sekretów w logach oraz małych zrzutach pamięci. WebView2 należy dodać tylko wtedy, gdy wymaganie rzeczywiście wymusza HTML; wnosi oddzielne, szybko aktualizowane środowisko wykonawcze.

### 20.6. Deklaracja autorska — kierunek niezależnej implementacji referencyjnej

Ustalenia tego raportu nie mają charakteru wyłącznie krytycznego; wyznaczają zarazem wzorzec, według którego zamierzam zbudować niezależną aplikację referencyjną o zbliżonej funkcjonalności. Przepływ danych i model działania znam na tyle dokładnie, by odtworzyć go samodzielnie w języku C++23, w oparciu o natywne WinAPI, bez pośrednictwa rozbudowanego stosu kilkuset bibliotek. Rezygnacja z tej powłoki nie jest celem samym w sobie, lecz świadomą decyzją inżynierską: każda zbędna zależność to dodatkowy parser, kolejne środowisko uruchomieniowe i wydłużenie łańcucha dostaw, a zatem realny przyrost powierzchni potencjalnego ataku. Rdzeń mniejszy i przejrzysty łatwiej poddaje się higienie kodu, przeglądowi oraz utrzymaniu. Wybór natywnego WinAPI nie zamyka przy tym drogi na inne systemy: ta sama zasada — niewielki rdzeń, minimum zależności i kryptografia powierzona zaudytowanym mechanizmom platformy — przenosi się na Linuksa z nowoczesnym, natywnym zestawem narzędzi (GTK4/libadwaita), czego dowodzi mój edytor rejestru RegEdLin zbudowany dokładnie w tym duchu.

Nie piszę tego z pozycji teoretyka. Pracuję na co dzień poniżej warstw, które niniejszy raport traktuje jako gwarancje: wymuszania podpisu sterowników, HVCI, zabezpieczeń opartych na wirtualizacji i Bezpiecznego Jądra, aż po fazę UEFI — mechanizmów, w które Microsoft inwestuje ogromne środki, a które w moich narzędziach badawczych (zob. [github.com/wesmar](https://github.com/wesmar)) potrafię obejść lub odtworzyć na każdym poziomie: od modyfikacji `g_CiOptions` i przekierowania `SeCiCallbacks`, przez uzyskanie uprawnień SYSTEM jeszcze przed ekranem logowania — bez zapisu na dysk i bez sterownika jądra — po własny sterownik systemu plików NTFS działający w środowisku UEFI. Kto rozumie te warstwy od tej strony, inaczej rozkłada zaufanie w aplikacji tożsamości: nie rozważa, czy wykrywanie roota jest dostatecznie pomysłowe, lecz przyjmuje, że urządzenie bywa w pełni kontrolowane, i przenosi rozstrzygnięcia tam, dokąd klient nie sięga. Po trzydziestu latach tej praktyki architektura takiej aplikacji powstałaby zupełnie inaczej — nie dlatego, że dysponuję większą liczbą bibliotek, lecz dlatego, że mam ich znacznie mniej i wiem, której warstwy trzeba naprawdę pilnować.

Prace prowadzę w reżimie clean-room i na licencji MIT: własny kod, własna architektura, własny interfejs, własna nazwa i grafika — bez przejmowania znaków, symboli urzędowych czy sugerowania autoryzacji państwowej. Bezpieczeństwo opieram nie na zaufaniu do klienta, lecz na dyscyplinie protokołu: pojedynczej granicy „zweryfikuj, zanim odczytasz" dla treści podpisanej, uwierzytelnionym szyfrowaniu w miejsce trybów pozbawionych kontroli integralności, kluczach nieeksportowalnych osadzonych w sprzęcie zamiast materiału przenoszonego do klienta oraz serwerze rozstrzygającym o każdej istotnej decyzji. Świadomie nie piszę przy tym własnej kryptografii ani własnego TLS — sięgam po zweryfikowane mechanizmy systemowe, ponieważ to one, a nie sama liczba zależności, przesądzają o rzeczywistym poziomie zapewnienia.

Nie jest to zapowiedź „aplikacji doskonałej", lecz zobowiązanie do warsztatu, w którym opisane w tym raporcie słabości granicy zaufania nie mają prawa się pojawić.

## 21. Macierz korekt, odpowiedzialności i kryteriów zamknięcia

| ID | Właściciel | Działanie | Dowód zamknięcia | Termin zalecany |
|---|---|---|---|---|
| MOB-A-01 | sieć/PKI | HTTPS, monotoniczna wersja i znacznik czasu, stan oraz przedział czasowy dziennika CT | test starszej podpisanej listy kończy się odrzuceniem | przed następnym wydaniem |
| MOB-A-02 | platforma Android | usunięcie TrustAll, permisywnego weryfikatora i `proceed()` z wydania produkcyjnego | przeszukanie końcowego APK + test błędu certyfikatu | przed następnym wydaniem |
| MOB-A-03 | aplikacja mobilna / serwer | migracja tokenów do App Links HTTPS, jednorazowość, TTL, stan i odbiorca | test aplikacji kolidującej i spreparowanego Intentu | 30 dni |
| MOB-A-04 | bezpieczeństwo aplikacji / budowanie | aktualizacja BC, pełny SBOM i osiągalność kodu dla wszystkich CVE 1.83→bieżące | wynik SCA bez nierozstrzygniętego ryzyka Wysokiego/Krytycznego + testy podpisów/PKI | natychmiast |
| MOB-A-05 | kryptografia / dokumenty | centralne `verifyThenDecode` przy przyjęciu i odczycie; typowanie danych niezaufanych i zweryfikowanych | sześć wariantów negatywnych odrzuconych przed zapisem i interfejsem użytkownika w obu stanach flagi | natychmiast / P0 |
| MOB-A-06 | kryptografia / aktywacja | losowa sól dla każdego pakietu, AEAD, wersjonowanie parametrów KDF i ochrona przed przejściem do słabszego wariantu | dwa pakiety nie współdzielą soli ani klucza; pomiar i test rozpoznawania poprawnej próby poza aplikacją; test migracji starego formatu | natychmiast / P0 |
| MOB-A-08 | kryptografia / tożsamość / serwer | generowanie lub bezpieczny import klucza do AndroidKeyStore, nieeksportowalność, usunięcie sekretów z DTO, rotacja i unieważnienie | pełny test cyklu klucza; w bazie wyłącznie alias; brak PKCS#8 i hasła w stercie oraz logach; stary certyfikat odrzucony | natychmiast / P0 |
| MOB-A-09 | aplikacja mobilna / OAuth / serwer | wycofanie `y12.t` albo przeniesienie `state` + PKCE S256, ścisła walidacja zwrotnego URI i ochrona przed obniżeniem zabezpieczeń | test dwóch sesji i wstrzyknięcia obcego kodu; każde niedopasowanie odrzucone przed wymianą tokenu | natychmiast / P0 |
| MOB-A-10 | aplikacja mobilna / serwer / pliki | oczyszczanie nazwy po obu stronach, kanoniczne związanie celu z katalogiem, usunięcie starszej ścieżki bezpośredniego zapisu | testy nazw z `../`, separatorami i `filename*=` na API 26/28/29; plik nigdy nie powstaje poza `Downloads` | następne wydanie / P1 |
| MOB-O-01/03 | prywatność / telemetria | centralne usuwanie wrażliwych części URL i danych, lista dozwolonych znaczników, retencja | zapis zdarzeń z testu nie zawiera parametrów zapytania, tokenów ani danych osobowych | przed zamknięciem audytu |
| MOB-O-02 | platforma Android | zawężenie `FileProvider` i audyt grantów | test URI spoza katalogu kończy się odmową | następny cykl utwardzenia |
| MOB-O-04 | aplikacja mobilna / bezpieczeństwo interfejsu | ochrona okna i kontrola zasłoniętych dotknięć na ekranach wrażliwych | macierz nakładek nie wykonuje operacji i zachowuje wymaganą dostępność | następny cykl utwardzenia |
| NATYWNE | właściciele JNI | wersje, utwardzenie, CVE i testowanie mutacyjne 11 bibliotek dla 4 ABI | pakiet skrótów, `build-id`, wyniki utwardzenia i testów mutacyjnych | przed zapewnieniem o bezpieczeństwie |
| ŹRÓDŁA | właściciel produktu | pełne źródła, plik blokady zależności, instrukcja odtworzenia, mapowanie zmiany źródłowej na APK | odtworzona kompilacja i porównanie artefaktów | po formalnym przekazaniu kodu |

flowchart LR FIND[Ustalenie] --> OWNER[Jawny właściciel] OWNER --> PATCH[Korekta + test jednostkowy] PATCH --> REL[Finalny podpisany artefakt] REL --> NEG[Test negatywny / regresyjny] NEG -->|zaliczony| EVID[Skrót + log + środowisko] NEG -->|niezaliczony| PATCH EVID --> INDEP[Niezależna weryfikacja] INDEP -->|potwierdzona| CLOSED[Zamknięte] INDEP -->|brak dowodu| OWNER style CLOSED fill:#d9f2e6,stroke:#16784b style FIND fill:#ffe0e0,stroke:#ad3030 

Ustalenie nie jest zamknięte komunikatem prasowym, zmianą w repozytorium ani deklaracją zespołu. Zamknięcie dotyczy konkretnego podpisanego artefaktu i wymaga dowodu testowego możliwego do niezależnego powtórzenia.

## 22. Referencja naprawcza (pseudokod)

Poniższe wzorce są neutralne językowo i mają charakter referencyjny, nie stanowią pełnej implementacji. Ilustrują kierunek naprawy ustaleń, nie zawierają kodu ofensywnego.

Pojedyncza granica „zweryfikuj, zanim odczytasz" dla treści podpisanej (MOB-A-05):

```
function verifyThenDecode(cms: UntrustedCmsBytes, policy) -> VerifiedPayload:
    signed  = parseCms(cms)                     // tylko parsowanie
    signers = signed.getSignerInfos()
    require(signers.count == policy.expectedSignerCount)
    for s in signers:
        cert = resolveFromTrustStore(s)
        require(s.verify(cert.publicKey))                    // podpis
        require(buildAndValidateChain(cert, policy.anchors)) // łańcuch
        require(cert.hasEku(policy.requiredEku))             // EKU / OID
        require(policy.validAt(now(), cert))                 // czas
        require(not isRevoked(cert, policy.revocation))      // revocation
        require(policy.bindsDocType(cert, expectedDocType))  // związanie
    return VerifiedPayload(signed.getSignedContent())        // dopiero teraz
// Brak publicznego decode() zwracającego treść z pominięciem tej ścieżki.
```

Lista dozwolonych algorytmów przy deszyfrowaniu CMS EnvelopedData (MOB-A-04):

```
const ALLOWED = { AES_256_GCM }                 // docelowo AEAD, bez zwinności
function decryptEnvelope(env: CMSEnvelopedData, key):
    require(env.contentEncryptionOid in ALLOWED)  // odrzuć nieoczekiwany OID z blobu
    r = env.recipientFor(key); require(r != null)
    return r.getContent(key)
```

Typ sekretu redagowany i nieobecny w dziennikach (MOB-A-08, EDO-05):

```
final class Secret:                             // char[] w środku, nie String
    toString() -> "Sensitive value, redacted"   // nigdy wartość
    equals()/hashCode() -> nie nad materiałem
    close(): zeroize(buffer)                     // jawne czyszczenie
// PIN/CAN/PUK/hasło zawsze jako Secret; nigdy składnik komunikatu dziennika.
```

Import klucza prywatnego do magazynu sprzętowego zamiast przechowywania PKCS#8 (MOB-A-08):

```
function onboardKey(p12, pass: Secret):
    kp = loadPkcs12(p12, pass)
    importToHardwareKeystore(kp.privateKey, alias,
        { nonExportable: true, strongBoxPreferred: true })
    wipe(kp.privateKey); pass.close()           // brak kopii w bazie i pamięci
```

### 22.1. Konkretyzacja w C++23/WinAPI (demonstrator z sekcji 20.4)

Powyższe wzorce, zinstancjonowane na platformie Windows w niezależnym demonstratorze, sprowadzają się do jednej zasady: **weryfikuj i wiąż tożsamość, zanim odczytasz**. Krytyczny jest krok, którego brakuje po obu stronach systemu (sekcja 9.8) — twarde wiązanie podmiotu certyfikatu z tożsamością w kontenerze. Dodany do `verifyThenDecode`, domyka MOB-A-05 (klient) i lukę serwerową opisaną w 9.8 tym samym mechanizmem. Krypto i TLS powierza się zaudytowanym mechanizmom systemu (CNG, WinHTTP), bez własnych implementacji.

```
// C++23 / WinAPI — verifyThenDecode z twardym wiązaniem tożsamości (referencyjnie)
VerifiedDoc verifyThenDecode(std::span<const std::byte> cms, const Policy& pol) {
    auto signed_ = parseCms(cms);                     // samo parsowanie, bez zaufania
    // 1. Podpis + łańcuch przez CNG — bez własnej kryptografii
    require(CryptVerifyMessageSignature(signed_));    // NCrypt/CNG
    auto cert = resolveTrusted(signed_, pol.anchors);
    require(chainValid(cert, pol.anchors));           // CertGetCertificateChain
    require(cert.hasEku(pol.requiredEku) && pol.validAt(now(), cert));
    require(!revoked(cert, pol.revocation));
    // 2. TWARDE WIĄZANIE TOŻSAMOŚCI — domyka MOB-A-05 i lukę serwerową (9.8)
    auto payload = decode(signed_.content());         // dopiero po weryfikacji podpisu
    require(bindsIdentity(cert.subject(), payload.identity()));  // Subject certu <-> dane kontenera
    return VerifiedDoc{std::move(payload)};
}
```

Odwzorowanie ustaleń na mechanizmy platformy:

| Ustalenie | Mechanizm naprawczy (WinAPI) |
|---|---|
| MOB-A-05 + luka serwerowa (9.8) | `verifyThenDecode` + `bindsIdentity`; `CryptVerifyMessageSignature`, `CertGetCertificateChain` |
| MOB-A-08 | klucz nieeksportowalny w TPM 2.0 / CNG KSP (NCrypt); zero PKCS#8 w bazie |
| MOB-A-04 | lista dozwolonych OID przy `CMSEnvelopedData`, docelowo AES-256-GCM (AEAD) |
| Transport | WinHTTP: TLS 1.3, HTTP/2, przypinanie certyfikatu — bez własnego TLS |
| MOB-DYN-01 | render flagi i zdjęć przez Windows Imaging Component → Direct2D/HLSL zamiast 60 klatek WebP dekodowanych na CPU |
| Sekrety (MOB-A-08) | DPAPI / Windows Hello + typ `Secret` z zerowaniem, poza dziennikami |

Wniosek jest inżynierski, nie ideologiczny: mniejsza liczba zależności i oparcie na zaudytowanych prymitywach platformy zawężają powierzchnię ataku, ale to **jeden dodany warunek — wiązanie tożsamości — likwiduje najcięższą klasę błędu po obu stronach naraz.** Reszta to higiena, nie rewolucja.

## 23. Konkluzja

Najistotniejszym potwierdzonym problemem wersji 4.87.2 nie jest dowolne wstrzyknięcie treści przez `gstatic.com`, lecz **możliwość cofnięcia podpisanej listy zaufanych dzienników CT**, ponieważ podpis chroni integralność i pochodzenie, ale implementacja nie chroni świeżości. Jest to precyzyjny, naprawialny błąd obrony warstwowej o warunkowym wpływie.

Niebezpieczne implementacje `TrustAll`, permisywnej weryfikacji nazwy hosta oraz `SslErrorHandler.proceed()` są obecne w artefakcie wydania i powinny z niego zniknąć. Dostępny materiał nie potwierdza jednak, aby zwykły napastnik mógł przełączyć produkcyjną aplikację na tę ścieżkę. Klasyfikowanie tego jako zdalnej podatności wysokiej wagi byłoby nierzetelne.

Przegląd WebView potwierdził jednocześnie, że nie wszystkie warianty są jednakowe. Płatności mają SSL wymuszone na stałe, bazowe ustawienia treści mieszanej i dostępu do plików są restrykcyjne, a nie znaleziono `addJavascriptInterface`; to należy pochwalić. Z drugiej strony usługi internetowe włączają `allowFileAccess`, a wspólny klient nadal zawiera `proceed()` oraz kod budujący pełny DOM i URL dla mechanizmu logowania. Badany produkcyjny odbiornik odrzuca poziomy `debug`/`info`, więc nie ogłoszono wycieku treści WebView. Jest to jednak konfiguracja krucha: bezpieczeństwo zależy od właściwego wariantu i konkretnej implementacji mechanizmu logowania, zamiast od niezmiennych ograniczeń komponentu.

Odrębny problem dotyczy starszej autoryzacji e-Doręczeń. `y12.t` rozpoczyna przebieg kodu autoryzacyjnego bez `state` i PKCE, akceptuje wywołanie zwrotne na podstawie schematu oraz początku ścieżki, a następnie wysyła `code_verifier=null`. Nowszy moduł obecny w APK implementuje `state` i PKCE S256, zatem luka wynika z niespójności między wariantami, a nie z braku możliwości technicznej. Ponieważ dostępność `y12.t` zależy od serwerowej flagi `NATIVE_LOGIN_EDOR_MODULE_ANDROID`, raport ocenia MOB-A-09 jako średnie i warunkowe oraz wymaga testu P0 zamiast bezpodstawnie deklarować aktywne przejęcie sesji.

W tej samej domenie funkcjonalnej znaleziono warunkowy błąd zapisu załączników na wspieranych nadal Androidach 8–9. Nazwa z `Content-Disposition` dociera bez normalizacji do ścieżki opartej na publicznym `Downloads`, a `../` może skierować nowy plik do innego katalogu pamięci współdzielonej. Kod nie nadpisuje istniejącego celu i gałąź Androida 10+ używa `MediaStore`, dlatego nie jest to uniwersalny dowolny zapis ani zdalne wykonanie kodu (RCE). Jest to jednak klasyczny brak egzekwowania granicy katalogu po stronie klienta, którego nie wolno usprawiedliwiać oczekiwanym oczyszczaniem danych na serwerze. MOB-A-10 wymaga testu P1 z nazwą kontrolowaną przez nadawcę.

Drugi obszar wymagający pilnego działania to łańcuch dostaw. Bouncy Castle 1.83 zawiera kod objęty znanymi CVE, a projekt źródłowy wydał już 1.84 i następnie linię 1.85.x z szerokim zestawem korekt. Nie znaleziono odwołań aplikacyjnych do GOST, LDAP, Composite Signature ani FrodoKEM, dlatego raport nie twierdzi, że tymi mechanizmami osiągnięto eskalację. Brak dowodu wykorzystania nie jest jednak argumentem za pozostawieniem podatnej wersji aktywnie używanego dostawcy kryptograficznego.

Jeszcze poważniejszego rozstrzygnięcia wymaga sposób wykorzystywania CMS. Analiza statyczna potwierdziła tworzenie i zapis `CMSSignedData` na wejściu oraz liczne odczyty `getSignedContent()` bez znalezionej lokalnej weryfikacji podpisujących; uzyskane dane zasilają modele dokumentów. Osobny pakiet aktywacyjny legitymacji przychodzi z FrontSrv, jest chroniony hasłowym AES-CBC bez widocznego znacznika uwierzytelniającego i w obu implementacjach prowadzi do konstrukcji `CMSSignedData` bez znalezionego `verify`. Nie wykazano jeszcze zwykłego zdalnego kanału dostarczenia podrobionego kontenera, dlatego nie ogłoszono przełamania integralności dokumentów. Jednak w produkcie tożsamościowym przyjmowanie obiektu nazwanego i zaprojektowanego jako *SignedData* bez jawnego `verify` wymaga natychmiastowego testu P0 i korekty, a nie kosmetycznego zalecenia.

W tym samym pakiecie aktywacyjnym potwierdzono niezależny błąd KDF: globalną, ośmiobajtową sól PBKDF2. Widoczne w dekompilacji zero iteracji jest artefaktem; rzeczywista wartość to 65 536, dlatego raport nie powiela efektownego, ale fałszywego zarzutu. Stała sól usuwa rozdzielenie kluczy między pakietami i umożliwia amortyzowanie słownika, lecz bez znajomości entropii kodu oraz kanału dostępu do szyfrogramu nie dowodzi jeszcze praktycznego odzyskania danych.

Analiza tekstu jawnego domknęła wpływ tego wątku: pakiet zawiera plik PKCS#12 z aktywnym prywatnym kluczem certyfikatu `UNIVERSITY` oraz hasło do tego pliku. Po aktywacji klucz jest zapisywany jako eksportowalny PKCS#8 w bazie SQLCipher, a nie jako nieeksportowalny uchwyt AndroidKeyStore. SQLCipher i magazyn kluczy zapewniają rzeczywistą ochronę przechowywanych danych i zostały w raporcie odnotowane; nie przywracają jednak nieeksportowalności samego klucza. Praktyczne odzyskanie poza aplikacją i użycie klucza pozostają testem P0, dlatego raport klasyfikuje MOB-A-08 jako Średnie, warunkowo Wysokie po potwierdzeniu, zamiast ogłaszać bez dowodu przejęcie podpisu kwalifikowanego.

Publiczne 192 pliki Kotlin nie obejmują warstw, które przesądzają o bezpieczeństwie. Dlatego obecny raport jest analizą binarną, a nie audytem źródłowym. Po otrzymaniu pełnego kodu należy powiązać wydanie z konkretną zmianą źródłową, odtworzyć kompilację, porównać artefakty i ponownie przejrzeć te same ustalenia na źródłach. Do czasu wykonania testów P0 nie należy wydawać zbiorczego zapewnienia o bezpieczeństwie ani alarmistycznego komunikatu o przełamaniu aplikacji.

---

## 24. Weryfikacja dynamiczna na fizycznym urządzeniu

**Data testu:** 30 sierpnia 2026 r.
**Urządzenie:** Xiaomi 23078PND5G, Android 16 (API 36)
**Pakiet:** `pl.nask.mobywatel`, `4.87.2 (6426)`, `versionCode 56416`, `targetSdk 36`
**Stan aplikacji:** produkcyjna instalacja zalogowanego użytkownika; pobieranie mDowodu i mPrawa Jazdy działało bez błędu bezpośrednio przed testem.

### 24.1. Tożsamość badanego artefaktu

Z urządzenia pobrano dokładnie zainstalowany zestaw APK. `apksigner` potwierdził podpisy v2 i v3 oraz SourceStamp. Certyfikat podpisującego ma odcisk SHA-256 `cb4f1ea4f0be4a91ea803497dda69f31845c9a98f43903c56017cf4ab877d89f`.

| Plik | SHA-256 |
|---|---|
| `base.apk` | `1EDE2B031FFB7E5036308DDA8AD08C98CCD0D30316C3336CBF7B93CAF86ECB72` |
| `split_config.arm64_v8a.apk` | `E22FD78369107F2F4DE2424453CB545C500169DC597F4EE37DD2F82D08AB3892` |
| `split_config.xxhdpi.apk` | `9AB54DB01BF7BED5AAF3974180C78D53D7D59FF7B6C57828AB8F554C9B4424CF` |

Część dynamiczną prowadzono początkowo na powyższym urządzeniu fizycznym (30 sierpnia). W dalszym toku dołączyło drugie, kontrolowane środowisko — Windows Subsystem for Android z rootem (Magisk/Zygisk) — na którym wykonano patch detekcji środowiska (24.5), ekstrakcję kontenerów w spoczynku (24.7) oraz pomiar metadanych protokołu (24.6). Pełne parametry i wersje obu środowisk podano w karcie na początku raportu.

### 24.2. Wyniki bezinwazyjnych testów

- `run-as pl.nask.mobywatel` został odrzucony komunikatem `package not debuggable`. Działa ADB i obserwacja dynamiczna urządzenia, ale produkcyjnego procesu nie można podłączyć do debuggera aplikacyjnego Android Studio bez modyfikacji artefaktu.
- Zrzut ekranu aktywnej aplikacji zwrócił czarny obszar treści, a karta mObywatela w widoku ostatnich aplikacji nie ujawniła zawartości. Potwierdza to działanie `FLAG_SECURE` wobec przechwytywania programowego na Androidzie 16 dla obu sprawdzonych ścieżek — jest to spełnione minimum, klasyka jak w REVOLUT, iPKO, mbank - żaden wyróżnik; granice wobec roota opisuje 24.8.
- Testy dynamiczne potwierdziły skuteczne blokowanie cyfrowego przechwytywania chronionej treści na badanym Androidzie 16. Materiały obrazowe nie są publikowane, ponieważ nie wnoszą czytelnej wartości dowodowej dla odbiorcy raportu.
- Systemowy rejestr ostatnich zadań potwierdził jednoczesną obecność dwóch zadań mObywatela, `#4003` i `#4001`, z tą samą `MainActivity`; wpływ bezpieczeństwa pozostaje niewykazany.
- W bieżącym `logcat` procesu nie znaleziono pełnych adresów URL, tokenów ani błędów TLS/WebView. Widoczne błędy dotyczyły mechanizmów optymalizacyjnych HyperOS/MIUI i nie stanowią dowodu błędu mObywatela.
- Dynamiczne statystyki systemowe potwierdziły ruch sieciowy UID aplikacji przez Wi-Fi. Bez przechwycenia ruchu w kontrolowanym punkcie VPN/proxy nie rozstrzyga to treści żądań, ochrony TLS ani hipotez CT.
- Jawny Intent skierowany bezpośrednio do eksportowanej `MainActivity`, z URI `audit-invalid://untrusted.example/institutions?qrCode=AUDIT_INVALID_7fcb7d9c`, został przyjęty mimo błędnego schematu i hosta. Aplikacja przekazała fikcyjny kod do przebiegu weryfikacji, który zakończył się kontrolowanym błędem domenowym `WRONG_QR_CODE` i komunikatem „Niepoprawny kod QR”. Wynik **dynamicznie potwierdza brak ponownej walidacji schematu i hosta opisany w MOB-A-03**, ale równocześnie nie potwierdza obejścia autoryzacji: niepoprawny kod został odrzucony przez dalszą warstwę.
- Dla ścieżki podpisu kwalifikowanego jawny Intent z błędnym schematem `audit-invalid://eqsig` nie uruchomił przetwarzania tokenu. Intent kontrolny z oczekiwanym `mobywatel://eqsig` dotarł do parsera, a fikcyjna wartość zakończyła się błędem `Failed to decode Base64`. Pogłębiony test z **poprawnym** Base64 (`token=dGVzdA==`) pokazał jednak, że klient nie waliduje treści tokenu: **przekazuje go wprost do produkcyjnego API** (`POST .../digital-signature/mobile/api/qualified-signature-process/start-authentication`), a dopiero serwer odrzuca sfałszowaną wartość (`400`, `INVALID_DEEPLINK`, „Niepoprawny kod QR"). Koryguje to wcześniejsze wrażenie, jakoby ścieżka była „ściślejsza" niż `institutions` — to ta sama klasa otwartej granicy wejściowej: klient przyjmuje deep link od dowolnej aplikacji bez walidacji nadawcy i przepycha go do rządowego API, a cała obrona jest serwerowa. Nie wykazano zmiany stanu ani obejścia logowania (MOB-A-07).

Materiały dowodowe i pobrane APK zachowano lokalnie, poza publikacją. Zrzuty zawierające dane osobowe lub karty innych aplikacji nie są udostępniane bez uprzedniej anonimizacji.

Powyższe wyniki zamykają na Androidzie 16 część P0 dotyczącą zrzutów, nagrywania i widoku ostatnich aplikacji. Nie rozstrzygają testów CMS, KDF, klucza UNIVERSITY, starszego OAuth ani cofnięcia listy CT; te wymagają kontrolowanych danych wejściowych lub kontrolowanej warstwy sieciowej.

### 24.3. Kontynuacja testów wykonaniowych na nowszym wydaniu (4.89.0)

Kolejną sesję dynamiczną przeprowadzono po tym, jak urządzenie zaktualizowało aplikację. Zainstalowane wydanie to **`4.89.0 (6488)`, versionCode `56478`**, z datą aktualizacji 2 września 2026 r., na Androidzie 16 (API 36). Wersja ta zastąpiła audytowany pakiet `4.87.2 (6426)`. Odcisk SHA-256 certyfikatu podpisującego pozostał zgodny z podanym w podrozdziale 24.1, a `run-as pl.nask.mobywatel` nadal kończy się komunikatem `package not debuggable`, co potwierdza autentyczną, nie-debugowalną instalację produkcyjną. Ustalenia statyczne raportu pozostają zakotwiczone w `4.87.2`; poniższe obserwacje wykonaniowe dotyczą wprost `4.89.0` i są tak oznaczone. Zainstalowany na urządzeniu numer wydania zmienia się co kilka–kilkanaście dni, wobec czego kontrola dynamiczna śledzi cel ruchomy, a niniejszy podrozdział dokumentuje, że opisana niżej granica utrzymała się mimo aktualizacji.

Stan weryfikacji odsyłaczy sprawdzono poleceniem `pm get-app-links pl.nask.mobywatel` oraz zrzutem `dumpsys package`. System zwrócił stan `verified` dla hosta `api.mobywatel.gov.pl` ze ścieżką `/application/diia_confirmed`, a odpowiadający filtr intencji nosi w zrzucie `AutoVerify=true`. Oznacza to, że ruch HTTPS do tego adresu Android kieruje wyłącznie do oficjalnej aplikacji, bez okna wyboru i bez udziału użytkownika — mechanizmem Android App Links opartym na kryptograficznym powiązaniu z plikiem `assetlinks.json` domeny.

Weryfikacja ta nie obejmuje — i z założenia nie może obejmować — dwóch pozostałych filtrów o schemacie niestandardowym, które zrzut potwierdza jako obecne bez atrybutu weryfikacji: `mobywatel://` ze ścieżką `/institutions` oraz `mobywatel://eqsig`. Struktura ta odpowiada dokładnie manifestowi audytowanej wersji `4.87.2` (filtr HTTPS z `autoVerify` w wierszach 118–125; schematy własne w wierszach 114–116 i 132–133), co czyni wynik potwierdzeniem międzywersyjnym: granica pozostała niezmieniona między `4.87.2` a `4.89.0`. Schematy inne niż HTTP/HTTPS nie mają systemowego mechanizmu potwierdzania pochodzenia, wobec czego dowolna inna aplikacja może zadeklarować ten sam schemat, wywołując systemowe okno wyboru obsługi albo przejmując intencję. Test wykonaniowy potwierdza więc wprost architektoniczną granicę opisaną w MOB-A-03 i MOB-A-09: ścieżka poświadczana kryptograficznie działa tam, gdzie ją zastosowano, natomiast tokeny przenoszone schematem własnym pozostają poza tą ochroną. Wynik wzmacnia zalecenie migracji przepływów tokenowych na App Links HTTPS wraz z jednorazowością, czasem ważności oraz wiązaniem stanu i odbiorcy.

Kontrola potwierdziła również, na tej samej sesji i na wydaniu `4.89.0`, działanie `FLAG_SECURE` na ekranach dokumentów oraz brak flagi `debuggable` w procesie produkcyjnym — zgodnie z ustaleniami z podrozdziału 24.2 poczynionymi na `4.87.2`.

### 24.4. Obserwacje wykonaniowe poza modelem zagrożeń — wydajność i higiena pamięci

Sesja dynamiczna ujawniła dwie usterki niezwiązane z bezpieczeństwem, które odnotowuję osobno, aby nie mieszać ich z ustaleniami o wpływie na poufność lub integralność. Obie potwierdzono w kodzie audytowanej wersji `4.87.2` i zaobserwowano w działaniu na wydaniu `4.89.0` obecnym wówczas na urządzeniu; klasyfikuję je jako dług jakości i wydajności.

#### 24.4.1. Wyciek instancji Activity przez pole statyczne

Klasa nawigacji starszego modułu `og2/a.java` przechowuje aktywność w polu statycznym `private static androidx.appcompat.app.c activity;` (wiersz 21) i przypisuje je w metodzie `j(...)` przez `activity = activity2` (wiersz 172). W całym pliku nie znaleziono przypisania `activity = null` ani metody czyszczącej to pole. Pole statyczne żyje tak długo jak proces, wobec czego przechowywana w nim aktywność wraz z całym drzewem widoków i obiektami kompozycji nie może zostać zwolniona po zniszczeniu ekranu. Przy każdej zmianie konfiguracji — obrocie ekranu, przełączeniu motywu jasny/ciemny, zmianie rozmiaru czcionki — powstaje nowa aktywność, a poprzednia pozostaje uwięziona. Obserwacja wykonaniowa na `4.89.0` jest z tym spójna: `dumpsys meminfo` wykazał dwie aktywności (`Activities: 2`) przy jednym korzeniu widoku (`ViewRootImpl: 1`) w architekturze pojedynczej aktywności. Jest to klasyczny wyciek kontekstu aktywności, defekt higieny pamięci bez wpływu na bezpieczeństwo. Zalecenie: zerować pole w `onDestroy` lub, lepiej, zrezygnować z przechowywania aktywności w polu statycznym na rzecz przekazywania jej zasięgiem świadomym cyklu życia.

#### 24.4.2. Animacja flagi jako pętla dekodowania rastrowego

Powiewająca flaga na ekranie mDowodu nie jest zasobem wektorowym ani pojedynczą animacją, lecz sekwencją **sześćdziesięciu odrębnych plików WebP** (`flag_pl_0.webp`–`flag_pl_59.webp`, każdy 64×44 px), zebranych w tablicy `coi_common_ui_poland_flag_resources` (`res/values/arrays.xml`). Komponent animacji w `d30/e.java` liczy opóźnienie klatki jako `1000 / maxFramesPerSecond`, z częstotliwością ograniczoną do przedziału 1–60 (wiersz 243 i dalsze gałęzie dekompilacji), po czym inkrementuje indeks klatki i wywołuje `painterResource` dla kolejnego zasobu. Przy częstotliwości przekazywanej przez wywołującego — w obserwacji wykonaniowej około 15 klatek na sekundę — systemowy dekoder odtwarza nową bitmapę z zasobów mniej więcej co 66 ms, w pętli działającej przez cały czas wyświetlania karty. W logach `HWUI` widać odpowiadające temu wpisy `ImageDecoder_nDecodeBitmap: width = 64, height = 44`, a profil pamięci graficznej sięgał około 100 MB warstwy GL. Poza narzutem procesora i układu graficznego pętla ta utrzymuje wątek interfejsu w stanie czynnym, co uniemożliwia jego przejście w spoczynek i zakłóca narzędzia automatyzacji oczekujące na stan bezczynności. Zalecenie: zastąpić sekwencję sześćdziesięciu rastrów pojedynczym zasobem wektorowym animowanym lub animacją poza wątkiem kompozycji.

Żadna z tych obserwacji nie zmienia oceny bezpieczeństwa aplikacji. Odnotowano je, ponieważ dotyczą jakości wydania produkcyjnego i dają się potwierdzić zarówno w kodzie, jak i w pomiarze wykonaniowym.

### 24.5. Empiryczne obejście detekcji środowiska przez modyfikację pakietu

Aby sprawdzić, czy klient-side wykrywanie środowiska stanowi realną granicę bezpieczeństwa, czy jedynie próg zwalniający (teza sekcji 19), przeprowadzono kontrolowaną modyfikację audytowanego pakietu `4.87.2` i uruchomiono go w środowisku z rootem. Test wykonano na kopii legalnie redystrybuowanej (licencja MIT), na koncie i urządzeniu wirtualnym należącym do badacza, wyłącznie w celu weryfikacji mechanizmu obronnego.

**Metoda i narzędzie.** Całość zautomatyzowałem w jednym skrypcie PowerShell, który wraz z jego analizą dołączam do tego opracowania (przycisk pobierania na górze strony). Przebieg jest w pełni jawny: skrypt wykrywa lokalne narzędzia (`java`, `adb`, `zipalign`, `apksigner`), w razie potrzeby pobiera `apktool` z oficjalnego repozytorium, tworzy jednorazowy keystore, po czym odnajduje najnowszą paczkę instalacyjną. Paczkę `.apkm` — będącą **zestawem split-APK w formacie dystrybucyjnym APKMirror, a nie Android App Bundle** — rozpakowuje i dobiera właściwe składniki `split_config.*` według architektury i gęstości ekranu odczytanych z urządzenia przez `adb`. Sedno zmiany to **pojedynczy punkt**: `base.apk` jest rozkładany do postaci smali, a trzy metody klasy `com.scottyab.rootbeer.RootBeer` orzekające o zrootowaniu (`isRooted`, `isRootedWithBusyBoxCheck`, `isRootedWithoutBusyBoxCheck`) zwracają odtąd stałą wartość „urządzenie nie jest zrootowane". Pakiet jest składany ponownie, wyrównywany (`zipalign`) i podpisywany jednym własnym kluczem — plik bazowy oraz oba splity — a następnie instalowany jako aktualizacja (`install -r` tym samym kluczem), co zachowuje aktywowany profil. Na koniec skrypt uruchamia aplikację i sprawdza w dzienniku, że nie pojawia się już żadne orzeczenie RootBeera ani ekran ostrzeżenia.

Publikuję tę procedurę świadomie. Nie dostarcza ona narzędzia do podszywania się pod tożsamość ani nie forsuje żadnej kontroli po stronie serwera: zdejmuje wyłącznie sprawdzenie wykonywane w procesie, który właściciel urządzenia i tak w pełni kontroluje. Nie fałszuje dokumentu, nie odszyfrowuje danych pobieranych z usługi i nie omija atestacji egzekwowanej serwerowo — pokazuje jedynie, że klient-side detekcja środowiska nie jest barierą. To jest sedno ustalenia, a nie środek do nadużycia.

**Wynik.** Przed modyfikacją aplikacja kończyła start ekranem `threat_detection_security_alert_screen` („Nie masz dostępu do aplikacji mObywatel — urządzenie działa w trybie, w którym ze względów bezpieczeństwa nie możesz korzystać z aplikacji"), a w dzienniku widoczne było `RootBeer: com.topjohnwu.magisk ROOT management app detected`. Po modyfikacji **ten sam pakiet uruchamia się do ekranu głównego**: dziennik nie zawiera już żadnego orzeczenia RootBeera ani ekranu ostrzeżenia. Odnotowano przy tym, że usługa atestacji Google (`gms.unstable / DroidGuardService`) nadal się wykonywała — jej uruchomienie samo w sobie nie zatrzymało przebiegu aplikacji po stronie klienta.

**Nagranie demonstracyjne.** Powyższy przebieg utrwaliłem na nagraniu ekranu. Widać na nim kolejno: uruchomienie skryptu patchującego, ponowną instalację pakietu tym samym kluczem (`install -r`) bez utraty aktywnego logowania oraz przejście po interfejsie aplikacji działającej na Windows Subsystem for Android. W tle, przełączane skrótem Alt+Tab, pozostają aplikacja Magisk i Termux z uprawnieniami roota (`uname -a`, Midnight Commander z konta użytkownika) — potwierdzenie, że środowisko jest rzeczywiście zrootowane. Całość uruchomiono na Windows 11 (26H2), a samo WSA — przy użyciu autorskiej łatki [WSAPatch](https://github.com/wesmar/WSAPatch), napisanej w asemblerze. Dane osobowe w nagraniu zanonimizowano. Samo nagranie umieściłem na początku strony.

**Znaczenie.** Cała klient-side warstwa rozpoznania środowiska — RootBeer wraz z modułem `threat_detection` — została zdjęta zmianą w jednym miejscu kodu, przy użyciu ogólnodostępnych narzędzi, w czasie liczonym w minutach. Potwierdza to wprost wniosek sekcji 19: **detekcja roota i emulatora jest heurystyką, nie granicą zaufania.** Modyfikacja nie narusza żadnej właściwości kryptograficznej i nie „łamie" zabezpieczeń serwera; pokazuje wyłącznie, że decyzja podejmowana w procesie klienta jest w całości pod kontrolą właściciela urządzenia. Poprawne, choć podstawowe, działanie `FLAG_SECURE` wobec przechwytywania programowego pozostaje po stronie plusów (zob. 24.2), z zastrzeżeniem jego granic wobec roota (24.8).

**Powtarzalność.** Procedura nie jest jednorazowym trikiem — daje się w pełni zautomatyzować i pozostaje niezależna od wersji. Ponieważ punkt cięcia to niezmienne, nieobfuskowane nazwy metod biblioteki RootBeer, ten sam zabieg stosuje się do każdego kolejnego wydania: rozłożenie pakietu, neutralizacja trzech metod orzekających o zrootowaniu, ponowne złożenie, podpis i instalacja jako aktualizacja tym samym kluczem (co zachowuje aktywowany profil). Automatyzacja całości w jednym skrypcie i jej wielokrotne uruchomienie na kolejnych wersjach usuwa ostatnią wątpliwość co do charakteru tej warstwy: kontrola, którą da się zdjąć bezobsługowo i powtarzalnie, nie jest granicą bezpieczeństwa. Skrypt automatyzujący całość wraz z jego analizą dołączam jako materiał do pobrania (przycisk na górze strony); nagranie przebiegu dojdzie osobno.

**Rozgraniczenie i pytanie otwarte.** Zakres potwierdzony to obejście bramy startowej i uruchomienie interfejsu. Czy po zdjęciu tej bramy wrażliwe dokumenty są renderowane na podstawie danych świeżo pobranych z serwera na urządzeniu o naruszonej integralności, czy jedynie ze stanu lokalnego, wymaga osobnej, ostrożnej weryfikacji i nie jest tu przesądzane. To rozgraniczenie wyznacza zarazem kryterium naprawy.

**Zalecenie.** Wykrywanie środowiska po stronie klienta należy traktować jako sygnał telemetryczny i element komfortu, nigdy jako kontrolę dostępu. Rozstrzygające zapewnienie integralności urządzenia musi pochodzić z atestacji poświadczanej sprzętowo i egzekwowanej po stronie serwera (np. werdykt sprzętowy Play Integrity jako twardy warunek wydania danych), a udostępnienie dokumentu tożsamości nie może zależeć od wyniku kontroli wykonywanej w procesie, który atakujący w pełni kontroluje. Wynik ten jest empirycznym uzupełnieniem MOB-A oraz sekcji 19 i 17.

### 24.6. Protokół pobierania dokumentów

Ścieżkę komunikacji zrekonstruowano z kodu i potwierdzono wykonaniowo na poziomie metadanych połączeń, bez przechwytywania ani odszyfrowywania treści — ruch pozostaje chroniony przypinaniem certyfikatu, a jego zawartość nie była przedmiotem tego testu.

**Endpointy (z `c64/a.java:163`).** Aplikacja korzysta z pojedynczej bramy API pod hostem `api.mobywatel.gov.pl`. Pobranie dokumentu przebiega jako sekwencja wyzwanie–odpowiedź oparta na JWT: `authentication/mobile/api/challenge` oraz `authentication/mobile/api/authentication/challenge` (pobranie wyzwania), `authentication/mobile/api/jwt` (wymiana na token), a następnie **`authentication/mobile/api/source-documents/download-async`** — asynchroniczne pobranie dokumentu źródłowego. Odświeżanie listy zaufanych certyfikatów idzie przez `mobile-settings/mobile/api/trusted-certificates`.

**Potwierdzenie wykonaniowe (metadane).** Na patchowanym wydaniu uruchomionym w WSA odnotowano, że operacje „Aktualizuj dane" dokumentu, wgląd w dane pojazdu oraz pobranie punktów karnych **łączą się z tym samym hostem** — `api.mobywatel.gov.pl`, rozwiązywanym na `185.41.93.206`, port `443`. Potwierdza to, że poszczególne usługi nie mają osobnych backendów, lecz korzystają ze wspólnej bramy. Zarejestrowano wyłącznie adres i port połączenia; zawartości nie przechwytywano.

**Transport i krypto.** Warstwa transportowa to HTTPS z przypinaniem certyfikatu i przejrzystością certyfikatów (zob. MOB-A-01). Ładunek aplikacyjny jest dodatkowo opakowany w CMS: żądanie jako `CMSEnvelopedData` szyfrowane do certyfikatu użytkownika (`dh2/a.java`), odpowiedź jako `CMSSignedData`. Istotne dla oceny: **odpowiedź jest dekodowana i parsowana do modelu dokumentu bez lokalnej weryfikacji podpisu podpisującego** — czyli ustalenie MOB-A-05 dotyczy tej samej, wspólnej ścieżki `source-documents/download-async`, a więc **każdego pobieranego dokumentu** (mDowód, dowód rejestracyjny, punkty karne i pozostałe), nie zaś pojedynczego typu. To rozszerza zasięg MOB-A-05 z konkretnego dokumentu na cały mechanizm dystrybucji dokumentów.

**Granica testu.** Zakres celowo ograniczono do rekonstrukcji z kodu i metadanych połączeń. Nie prowadzono przechwytywania treści ani deszyfrowania ruchu produkcyjnego — wymagałoby to złamania przypinania i operowania na realnych danych tożsamości oraz tokenach uwierzytelniających, co wykracza poza autoryzację posiadacza konta i poza zakres tego audytu.

**Topologia i parametry TLS (metadane).** W osobnej sesji, na koncie badacza, zarejestrowano metadane ruchu (`tcpdump`, bez deszyfrowania treści TLS 1.3). Ruch tożsamościowy zamyka się w zakresach IP COI: `api.mobywatel.gov.pl` (`185.41.93.206`, AS199953) obsługuje pobieranie dokumentów przez TLS 1.3 (krzywe X25519/secp256r1; szyfry `TLS_AES_128_GCM_SHA256`, `TLS_AES_256_GCM_SHA384`, `TLS_CHACHA20_POLY1305_SHA256`; ALPN `h2`), a telemetria trafia do `sentry.puch.coi.gov.pl` (`185.32.48.239:3000`, AS200244). Zarejestrowano wyłącznie adresy, porty i parametry sesji; zawartości nie przechwytywano.

**Weryfikacja wykonaniowa dokumentów.** Na koncie badacza sprawdzono wykonaniowo trzy aktywne dokumenty — mDowód, mPrawo jazdy i mPojazdy — odwzorowując dane wykonaniowe na klasy modelu z dekompilacji: `MobileIdCardContainerDto` / `MobileIdCardPersonalDataContainerDto`, `DrivingLicenceScopeDto` / `DrivingLicenceDataContainerDto` oraz `VehicleCardDataDto` (warianty `VehicleBasicDocumentDto` / `VehicleFullDocumentDto`). Wszystkie trzy przechodzą tą samą ścieżką `source-documents/download-async` i tym samym potokiem CMS, co potwierdza wykonaniowo, że zasięg MOB-A-05 obejmuje cały mechanizm dokumentów. Danych osobowych nie odtwarzam.

### 24.7. Szyfrowanie tożsamości w spoczynku — audyt wykonaniowy

Ekstrakcja kontenerów z pamięci masowej (na koncie badacza, WSA z rootem) pozwoliła zweryfikować ochronę danych w spoczynku. Obraz jest mieszany i pouczający: tam, gdzie sięgnięto po gotowe, zaudytowane biblioteki, jest dobrze; tam, gdzie napisano kryptografię własnym sumptem, pojawia się **chałupnictwo** — rozwiązania, które biblioteka wykluczyłaby z urzędu.

**Co zrobiono dobrze.** Rejestry `SharedPreferences` są w pełni chronione przez `EncryptedSharedPreferences` z biblioteką Google Tink: nazwy kluczy szyfrowane deterministycznie (`AesSivKey`), wartości uwierzytelnionym `AesGcmKey`. To jest wzorcowe: AEAD z gotowego, sprawdzonego źródła.

**Kontener tożsamości — pięć sygnałów chałupnictwa.** Sam plik kontenera jest zaszyfrowany, ale sposób jego złożenia obnaża rękodzieło:

1. **AES-256-CBC bez uwierzytelnienia.** Tryb to `AES/CBC/PKCS5Padding` (`ch2/c.java:22`) — CBC **bez MAC**, czyli bez kontroli integralności. W 2026 r. standardem jest AEAD (AES-GCM), a Tink, którego COI używa obok w `SharedPreferences`, daje je za darmo. Tutaj świadomie sięgnięto po własny CBC.
2. **Wektor IV doklejany na końcu pliku.** `ch2/a.java` zwraca `concatenate(prefiks, doFinal(dane), IV)` — IV trafia na koniec strumienia zamiast, jak w każdej normalnej bibliotece, na początek. Działa, lecz to jednoznaczny ślad formatu składanego ręcznie, wbrew konwencji.
3. **17-bajtowy losowy „junk prefix".** Przed właściwym strumieniem ZLIB wstrzykiwane jest siedemnaście losowych bajtów z CSPRNG (`ch2/a.java`, `cipher.update(random(17))`), co przesuwa granicę pierwszego bloku AES o jeden bajt. Cel: **chałupnicze maskowanie nagłówka ZLIB**, żeby utrudnić rozpoznanie formatu pod spodem. To security-by-obscurity w czystej postaci — nie dodaje ani grama realnego bezpieczeństwa (z kluczem odczyt i tak następuje, bez klucza CBC i tak nie zdradza treści), a komplikuje format. Podręcznikowa definicja chałupnictwa: rozwiązywanie nieistniejącego problemu własnym pomysłem zamiast użyć AEAD.
4. **Sól KDF bez realnej wartości ochronnej po zrzucie danych.** Klucz kontenera wyprowadza PBKDF2 z soli `saltApp ⊕ NSalt ⊕ deviceID` (`j.java`, `g()`); mimo losowej `NSalt` wszystkie składniki są odczytywalne z danych aplikacji, więc sól nie chroni pliku w modelu utraty urządzenia. Odrębny, mocniejszy przypadek **stałej** soli dotyczy legitymacji studenckiej (MOB-A-06).
5. **Brak wiązania integralności z tożsamością** — spójne z MOB-A-05 i 9.8: szyfrowanie chroni poufność, ale nic nie wiąże kontenera z okazicielem na poziomie kryptograficznym.

**Wyciek, który obchodzi całą tę kryptografię.** Podczas gdy kontenery i rejestry są szyfrowane, dwie bazy Room — powiadomień (`LocalNotificationsDatabase`, plik `local_notifications_db`) oraz indeks wyszukiwania (`GlobalSearchDatabase`) — zainicjowane standardowym `Room.databaseBuilder` bez fabryki `openHelperFactory`/SQLCipher, leżą **w czystym tekście** i trzyma dane pochodne o wysokiej wrażliwości: numer tablicy rejestracyjnej, typ dokumentu oraz daty ważności — badania technicznego, ubezpieczenia OC, a także ważności mDowodu i prawa jazdy. Model danych potwierdza dekompilacja (`br1/p0.java`: pola `registerNo`, `insuranceDate`, `technicalExaminationDate`; encja `x04/LocalVehicleNotificationEntity` z polem `registerNo`, obok `LocalDocumentNotifications`). Dowolny proces z dostępem do danych aplikacji albo ekstrakcja powypadkowa czyta te fakty **z pominięciem KeyStore, SQLCipher i Tink**. To jest realna luka poufności: cała inwestycja w kryptografię kontenera zostaje obejściem bocznym kanałem, który te same wrażliwe informacje przechowuje jawnie.

Przykład odczytany wprost z bazy (dane badacza, częściowo zredagowane — to realny wyciek z mojego urządzenia): `RegisterNo = WN42XXA`, `DocType = VEHICLE_CARD`, badanie techniczne `2026-07-xx`, OC `2027-03-xx`; w tabeli dokumentów ważność mDowodu `2029-…` i prawa jazdy `2028-…`. Wszystko w otwartym SQLite, bez jednego bajtu szyfrowania.

**Brak jakiegokolwiek Keystore w ścieżce kontenera — i co z tego wynika.** Przeszukanie całego pakietu `legacy/storage` nie wykazało ani jednego odwołania do Android Keystore, `KeyGenParameterSpec` ani StrongBox; materiał klucza żyje w zwykłej mapie w pamięci (`ContainerManagerNew.java:57`, `Map<d, byte[]>`). Klucz kontenera powstaje w całości programowo: `PBKDF2WithHmacSHA256(sekret, sól, 65 536 iteracji, 256 bitów)` (`bh2/a.java:58`; `CMSAlgorithm.AES256_CBC`, `bh2/a.java:55`), a sól — inaczej niż w legitymacji studenckiej (MOB-A-06) — nie jest pojedynczą stałą: wyprowadza się ją operacją XOR z trzech składników `saltApp ⊕ NSalt ⊕ deviceID` (`j.java`, metoda `g()`), gdzie stała `saltApp` pochodzi z DEX (`d64/p.java:11`), `NSalt` jest **losowana per instalacja** i zapisana jawnie w `mDoki_0.10.xml` (`j.java`, metoda `f()`), a `deviceID` bazuje na Android ID / IMEI (`j.java`, metoda `c()`). Sekretem jest hasło odblokowujące — w ścieżce aktywacji do wyprowadzenia klucza trafia `b0.e(password).getBytes()` (`ei2/b.java`), a w typowym trybie jest to czterocyfrowy lub sześciocyfrowy PIN. Ochrona Keystore opisana w MOB-BIN-02 dotyczy **wyłącznie loginowego bloba biometrii, nie samego pliku kontenera**. Konsekwencja jest poważna i podnosi ocenę tej klasy: **plik kontenera tożsamości jest atakowalny offline po zrzucie danych aplikacji** — pliku kontenera wraz z `NSalt` z `mDoki_0.10.xml` i identyfikatorem urządzenia — bez sprzętowego klucza. W trybie PIN przestrzeń sekretu wynosi 10⁴–10⁶ kombinacji; bez sprzętowego ograniczania prób pełny przegląd na konsumenckiej karcie GPU rzędu RTX 4090 zamyka się w sekundach — od ułamka sekundy dla czterech cyfr do kilkunastu–dwudziestu sekund dla sześciu. W trybie hasła koszt zależy od jego siły; losowa `NSalt` wyklucza jedynie tablice tęczowe współdzielone między ofiarami, natomiast **nie chroni pojedynczego celu — wszystkie składniki soli są odczytywalne z danych urządzenia**. Dodatkowo **65 536 iteracji PBKDF2 jest niemal dziesięciokrotnie poniżej bieżących zaleceń OWASP** (≥ 600 000), a brak układu TEE pozwala na nieograniczony, zrównoleglony atak słownikowy na klastrze GPU.

Zjawisko jest **niezależne od urządzenia** — ścieżka kontenera jest identyczna w kodzie, więc dotyczy zwykłego telefonu tak samo jak środowiska wirtualnego. Co więcej, na telefonie z PIN-em ochrona jest **słabsza** (sekundy), a **biometria nie zabezpiecza samego pliku**: klucz „BiometricKey" w Keystore owija jedynie przechowywaną kopię sekretu dla wygody odblokowania, natomiast atakujący dysponujący kopią danych aplikacji łamie PIN wprost przeciw plikowi, z pominięciem Keystore. Sprzętowy magazyn chroni wejście do aplikacji, nie dane w spoczynku. W żadnym wariancie nie ma sprzętowego wiązania klucza ani ograniczania tempa prób. To jest dysonans w czystej postaci: nowoczesne rejestry chroni Tink z Keystore, a najcenniejszy plik — tożsamość obywatela — kryptografia własnej roboty bez sprzętu.

flowchart TD subgraph ZAPIS A[DTO tozsamosci] -->|Gson toJson| B[JSON] B -->|Deflater ZLIB| C[Skompresowany strumien] C -->|17B losowy prefiks| D[Bufor Cipher] D -->|AES-256-CBC PKCS5| E[Szyfrogram] E -->|16B IV na koncu| F[Plik files/UUID] end subgraph ODCZYT F2[Plik files/UUID] -->|Odciecie 16B IV| G[IV] F2 -->|Deszyfrowanie blokow| H[AES-256-CBC decrypt] H -->|Odciecie 17B prefiksu| I[Strumien ZLIB] I -->|Inflater| J[JSON] J -->|Gson fromJson| K[DTO w RAM] end style D fill:#ffe0e0,stroke:#ad3030 style E fill:#ffe0e0,stroke:#ad3030 

Dysonans widać w jednym zestawieniu:

| Cecha kryptograficzna | Rejestry aplikacji (Tink) | Plik tożsamości (kontener mDowód) |
|---|---|---|
| Zarządca kryptografii | Google Tink (`androidx.security.crypto`) | własny moduł `bh2.a` / `ContainerManagerNew` |
| Algorytm i tryb | AES-256-GCM + AES-256-SIV | AES-256-CBC + PKCS#5 |
| Uwierzytelnienie (AEAD/MAC) | **tak** (tag uwierzytelniający) | **brak** (czysty CBC, podatny na bit-flipping) |
| Pochodzenie klucza | Android Keystore (TEE/StrongBox) | PBKDF2(PIN/hasło, sól z danych urządzenia) — czysto programowo |
| Sól KDF | z CSPRNG, klucz chroniony w KeyStore | `saltApp ⊕ NSalt ⊕ deviceID` — wszystkie składniki jawne w danych aplikacji, brak ochrony sprzętowej |
| Wektor IV | nonce na początku | 16-bajtowy IV **na końcu** pliku |
| Odporność na atak offline | bardzo wysoka (klucz nie opuszcza sprzętu) | **niska** — atak po zrzucie danych aplikacji; tryb PIN: sekundy, tryb hasła: koszt zależny od siły hasła, osłabiony niską liczbą 65 536 iteracji i brakiem sprzętowego ogranicznika prób |

**Ocena i naprawa.** Wzorzec jest podręcznikowy: gdzie użyto biblioteki (Tink), jest dobrze; gdzie pisano samemu (CBC bez MAC, IV na końcu, junk-prefix, sól w całości odtwarzalna z danych urządzenia, jawne bazy pomocnicze), tam błędy. Naprawa jest nudna i znana: **AEAD (AES-GCM) z prawdziwym MAC na kontenerze, sole z CSPRNG osadzone w magazynie kluczy, i szyfrowanie wszystkich baz trzymających dane pochodne** — `openHelperFactory` SQLCipher także dla powiadomień i indeksu wyszukiwania, nie tylko dla głównych kontenerów. Kierunek domyka wzorzec z 22.1.

### 24.8. Granice FLAG\_SECURE wobec uprawnień root

Podrozdział 24.2 potwierdził, że `FLAG_SECURE` blokuje zrzut ekranu i nagrywanie **programowe** — i tak jest, lecz wyłącznie wobec napastnika nieuprzywilejowanego. Wobec właściciela urządzenia dysponującego rootem flaga jest ochroną iluzoryczną. Nie jest to wada mObywatela, tylko granica samego mechanizmu; odnotowuję ją, ponieważ audyt nie może traktować `FLAG_SECURE` jako gwarancji przed ekstrakcją lokalną (spójne z sekcją 18 i z wnioskiem 24.7 o danych w spoczynku).

Flaga jest własnością obiektu `Window` w procesie aplikacji, więc z rootem zdejmuje się ją co najmniej na trzy znane, generyczne sposoby — niezależne od tej aplikacji:

- **Instrumentacja w locie (Frida).** Hook na tworzeniu widoku czyści flagę, po czym telefonowy zrzut działa normalnie: ```
    Activity.onResume.implementation = function () {
    this.getWindow().clearFlags(0x2000); // 0x2000 = FLAG_SECURE
    this.onResume();
    };
    ```
- **Moduł Xposed/LSPosed** (klasy „Disable FLAG\_SECURE"). Hook `setFlags` w procesie `system_server` sprawia, że system ignoruje żądanie aplikacji — okno trafia do SurfaceFlingera jako jawne, a zrzuty działają globalnie, w każdej aplikacji.
- **Patch SurfaceFlinger (Magisk).** Podmiana/łatka `libgui.so` lub procesu `surfaceflinger` tak, by `layer->isSecure()` zawsze zwracało `false`, powoduje kopiowanie każdego bufora graficznego niezależnie od ustawień aplikacji.

**Wniosek z modelu zagrożeń.** `FLAG_SECURE` zaprojektowano wyłącznie jako ochronę przed **nieuprzywilejowanym malware** — na przykład by aplikacja ze sklepu, nadużywająca usług ułatwień dostępu lub nagrywania ekranu w tle, nie podejrzała wpisywanego hasła czy dowodu. Nigdy nie było barierą przed właścicielem urządzenia ani przed rootem. Traktowanie go jako gwaranta ochrony danych przed ekstrakcją lokalną jest błędem architektonicznym — i właśnie dlatego ochrona tożsamości w spoczynku (24.7) nie może się na nim opierać.

## 25. Źródła zewnętrzne

- [KVC Research — wzorzec publikacji technicznej](https://kvc.pl/research)
- [Oficjalny Digital Asset Links mObywatel](https://api.mobywatel.gov.pl/.well-known/assetlinks.json)
- [Google Play — mObywatel](https://play.google.com/store/apps/details)
- [CERT Polska — CVE-2025-11598](https://cert.pl/posts/2026/02/CVE-2025-11598/)
- [NVD — CVE-2025-11598](https://nvd.nist.gov/vuln/detail/CVE-2025-11598)
- [Oficjalna publikacja kodu — dostęp po potwierdzeniu tożsamości](https://www.mobywatel.gov.pl/kod-zrodlowy-mobywatel-mobilny)
- [BIP Ministerstwa Cyfryzacji — komunikat o udostępnieniu kodu źródłowego](https://mc.bip.gov.pl/aplikacja-mobywatel/kod-zrodlowy-aplikacji-mobywatel.html)
- [Kopia lustrzana kodu — fajfer/mObywatel, zmiana `d39d2f94` z 4 stycznia 2026 r.](https://github.com/fajfer/mObywatel)
- [Kopia lustrzana kodu — neziw/mObywatel-mirror, materiał badany w sekcji 20.3](https://github.com/neziw/mObywatel-mirror/tree/master/Android/src/main/kotlin/pl/gov/coi/common/ui/ds)
- [Analiza zakresu publikacji kodu mObywatel](https://informatykzakladowy.pl/rzekoma-publikacja-kodu-zrodlowego-aplikacji-mobywatel/)
- [SecuRing — How we helped secure Poland's digital ID system (analiza server-side, ang.)](https://www.securing.pl/en/how-we-helped-secure-polands-digital-id-system-technical-analysis/)
- [CyberDefence24 — opublikowano fragmenty kodu źródłowego mObywatela](https://cyberdefence24.pl/polityka-i-prawo/opublikowano-fragmenty-kodu-zrodlowego-mobywatela)
- [Ministerstwo Cyfryzacji — komunikat o publikacji kodu](https://www.gov.pl/web/cyfryzacja/ministerstwo-cyfryzacji-opublikowalo-kod-zrodlowy-mobywatela)
- [Prawo autorskie — tekst jednolity, art. 74–77](https://api.sejm.gov.pl/eli/acts/DU/2025/24/text.pdf)
- [Ustawa o aplikacji mObywatel — tekst jednolity](https://api.sejm.gov.pl/eli/acts/DU/2024/1275/text.pdf)
- [Bouncy Castle Java 1.85 — nota wydania i lista poprawek CVE](https://www.bouncycastle.org/resources/new-release-bouncy-castle-java-1-85/)
- [Bouncy Castle — CVE-2026-5588](https://github.com/bcgit/bc-java/wiki/CVE%E2%80%902026%E2%80%905588)
- [Bouncy Castle — CVE-2025-14813](https://github.com/bcgit/bc-java/wiki/CVE%E2%80%902025%E2%80%9014813)
- [Bouncy Castle — CVE-2026-0636](https://github.com/bcgit/bc-java/wiki/CVE%E2%80%902026%E2%80%900636)
- [Bouncy Castle — CVE-2026-3505](https://github.com/bcgit/bc-java/wiki/CVE%E2%80%902026%E2%80%903505)
- [Bouncy Castle — CVE-2026-5598](https://github.com/bcgit/bc-java/wiki/CVE%E2%80%902026%E2%80%905598)
- [Bouncy Castle Java — bieżące wydania 1.85.x](https://www.bouncycastle.org/download/bouncy-castle-java/)
- [OSV API](https://osv.dev/docs/)
- [Bouncy Castle API — `CMSSignedData`](https://downloads.bouncycastle.org/java/docs/bcpkix-jdk15to18-javadoc/org/bouncycastle/cms/CMSSignedData.html)
- [OWASP Password Storage Cheat Sheet — punkt odniesienia dla PBKDF2](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [RFC 9700 / BCP 240 — OAuth 2.0 Security Best Current Practice](https://www.rfc-editor.org/rfc/rfc9700.html)
- [Android Developers — Tapjacking](https://developer.android.com/privacy-and-security/risks/tapjacking)

### Add a comment

---

## Navigation

- Parent: [Analizy techniczne (PL)](https://kvc.pl/analizy-techniczne-pl.md)
- Next: [eDO App 1.7.1 — audyt bezpieczeństwa i reverse engineering na WSA](https://kvc.pl/analizy-techniczne-pl/edo_app-audyt-bezpieczenstwa.md)
