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 trybudebuggableoraz rzeczywistą lukę walidacji jawnego Intentuinstitutions: 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.
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.| 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.
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.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:
settings.gradle;AndroidManifest.xml;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:
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.
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:
gstatic.com z możliwością podmiany fontów, JavaScriptu i dowolnych zasobów;ksRandomizer jako losowania aliasu klucza;android:autoVerify dla własnego schematu URI, mimo że weryfikacja Android App Links dotyczy adresów HTTP/HTTPS;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:
base.apk, zgodny z odciskiem opublikowanym przez domenę mobywatel.gov.pl;FLAG_SECURE w wariancie produkcyjnym;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;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;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;FileProvider i uprawnienia systemowe na eksportowanych komponentach bibliotek;FLAG_SECURE, potwierdzone statycznie i dynamicznie na Androidzie 16; regresja na Androidzie 8 i 14 pozostaje do wykonania;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;MainActivity jawnego Intentu institutions z obcym schematem i hostem; fikcyjny kod został przekazany do weryfikacji, lecz poprawnie odrzucony jako WRONG_QR_CODE;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.
| 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:
verified = true
v1 = false
v2 = true
v3 = true
v4 = false
Certyfikat podpisujący:
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. 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.
fallback);run-as, zrzut ekranu, ostatnie aplikacje, screenrecord, logi procesu oraz statystyki sieciowe UID;MainActivity dla ścieżek institutions i eqsig, z jednoznacznie fikcyjnymi znacznikami audytowymi;| 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.
| 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.
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
Konfiguracja zezwala na HTTP do gstatic.com:
<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:
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:
Bieżący ZIP pobrany podczas badania miał SHA-256:
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.
gstatic.com.log_list_timestamp oraz monotoniczny wzrost version.temporal_interval względem czasu SCT/certyfikatu.Ocena: niska jako higiena i odporność produktu
Pewność: istnienie kodu potwierdzone; dostępność dla zwykłego napastnika niepotwierdzona i w standardowej konfiguracji zaprzeczona
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.
Wartości produkcyjne są bezpieczne:
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.
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.
TrustAll, permisywny HostnameVerifier i SslErrorHandler.proceed() z wariantu produkcyjnego przez rozdzielenie zestawów źródeł;allowFileAccess=false we wszystkich WebView sieciowych oraz dodać jawne listy dozwolonych schematów i hostów dla nawigacji uprzywilejowanych;SslErrorHandler.proceed();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.Manifest eksportuje MainActivity dla:
mobywatel://app/institutions;mobywatel://eqsig;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:
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.
state i PKCEOcena: ś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.
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.
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.
FileProviderDostawca 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.
Produkcja inicjalizuje Sentry własnym kodem; automatyczna inicjalizacja manifestowa jest wyłączona. Konfiguracja nadaje zdarzeniom między innymi tagi:
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.
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).
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.
Downloads na API 26–28Ocena: 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.
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.
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, nie jako czynną podatność. Wykazania wymaga, 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.
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.
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.
CERT Polska opisał 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 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;8192, czyli WindowManager.LayoutParams.FLAG_SECURE;Dowód: a04/c.java, y10/x.java.
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.
Potwierdzone dobre praktyki:
AndroidKeyStore;KeyInfo.securityLevel i raportowanie poziomu.Korekty:
ksRandomizer jest przekazywane do setRandomizedEncryptionRequired(). Nie losuje aliasu klucza;StrongBox.PREFERRED celowo dopuszcza ponowienie generacji bez StrongBox po ProviderException;SOFTWARE;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.
| 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.
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.| 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. |
Uczciwy audyt obejmuje również kontrole działające poprawnie:
android:allowBackup="false" i reguły tworzenia kopii oraz ekstrakcji wykluczające dane aplikacji;android:usesCleartextTraffic="false", z jednym jawnie opisanym wyjątkiem;FLAG_SECURE domyślnie aktywne w produkcji;addJavascriptInterface w przejrzanym kodzie;allowFileAccess=false, zablokowana treść mieszana) oraz twarde sslEnabled=true w ekranach płatności;debug/info nie utrwala treści pełnego URL ani DOM budowanego przez wspólny WebView; nie znaleziono zdalnej wysyłki tych komunikatów;state oraz PKCE S256 i odrzuca wywołanie zwrotne z niezgodnym state;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;FileProvider nieeksportowany;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.
Przykładowe polecenia:
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.
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.
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 | 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 | 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 | 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:
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;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, bieżące pliki i noty wydania.
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.
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:
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. 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.
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.
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.
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.
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ą.
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.
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.
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.
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.
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:
testUserID i nie wskazuje w tej ścieżce zdalnego URL konfiguracji;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.
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.
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 |
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.
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.
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: 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, 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.
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.
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 |
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.
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.
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ą:
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.
custom/clickabletext/CustomClickableText.kt:38-41:
.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.
button/Button.kt:341-346 opakowuje każdy przycisk systemu w dostawcę wyłączający platformowy mechanizm minimalnego pola dotykowego:
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.
topappbar/TopAppBarUtil.kt:10-28 zawiera funkcję opatrzoną adnotacją, która sama opisuje swój charakter:
@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.
$ w identyfikatorzetextinput/TextField.kt:363:
.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.
null w identyfikatorzetextinput/TextField.kt:104-106:
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.
textinput/TextField.kt:233-234:
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.
enabledModel 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:
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ą.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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ą.
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.
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.
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.
Z tego wynika bezpieczniejszy profil prywatnego demonstratora:
DEMO — nie jest dokumentem ani aplikacją państwową;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.
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.
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.
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) 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ć.
| 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 |
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.
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
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.
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.
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.
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.
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.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.#4003 i #4001, z tą samą MainActivity; wpływ bezpieczeństwa pozostaje niewykazany.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.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ę.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.
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.
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.
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.
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.
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, 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.
Ś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.
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:
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.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.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.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).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.
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.
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:
Activity.onResume.implementation = function () {
this.getWindow().clearFlags(0x2000); // 0x2000 = FLAG_SECURE
this.onResume();
};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.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ć.
d39d2f94 z 4 stycznia 2026 r.CMSSignedData
Add a comment