Poprawiając bezpieczeństwo aplikacji, pomagasz zachować zaufanie użytkowników i integralność urządzenia.
Na tej stronie znajdziesz kilka sprawdzonych metod, które mają znaczący, pozytywny wpływ na bezpieczeństwo aplikacji.
Wymuszanie bezpiecznej komunikacji
Zabezpieczenie danych wymienianych między aplikacją a innymi aplikacjami lub między aplikacją a witryną zwiększa stabilność aplikacji i chroni przesyłane i odbierane dane.
Zabezpieczanie komunikacji między aplikacjami
Aby zapewnić bezpieczniejszą komunikację między aplikacjami, używaj domyślnych intencji z wybierakiem aplikacji, uprawnień na podstawie sygnatury i nieeksportowanych dostawców treści.
Wyświetlanie selektora aplikacji
Jeśli domyślny zamiar może uruchomić co najmniej 2 aplikacje na urządzeniu użytkownika, wyświetl wyraźnie selektor aplikacji. Ta strategia interakcji umożliwia użytkownikom przesyłanie informacji poufnych do aplikacji, której ufają.
Kotlin
val intent = Intent(Intent.ACTION_SEND) val possibleActivitiesList: List<ResolveInfo> = packageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL) // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." val chooser = resources.getString(R.string.chooser_title).let { title -> Intent.createChooser(intent, title) } startActivity(chooser) } else if (intent.resolveActivity(packageManager) != null) { startActivity(intent) }
Java
Intent intent = new Intent(Intent.ACTION_SEND); List<ResolveInfo> possibleActivitiesList = getPackageManager() .queryIntentActivities(intent, PackageManager.MATCH_ALL); // Verify that an activity in at least two apps on the user's device // can handle the intent. Otherwise, start the intent only if an app // on the user's device can handle the intent. if (possibleActivitiesList.size() > 1) { // Create intent to show chooser. // Title is something similar to "Share this photo with." String title = getResources().getString(R.string.chooser_title); Intent chooser = Intent.createChooser(intent, title); startActivity(chooser); } else if (intent.resolveActivity(getPackageManager()) != null) { startActivity(intent); }
Powiązane informacje:
Stosowanie uprawnień na podstawie podpisu
Udostępniając dane między 2 aplikacjami, które są Twoją własnością lub są przez Ciebie kontrolowane, używaj uprawnień na podstawie sygnatury. Te uprawnienia nie wymagają potwierdzenia przez użytkownika. Zamiast tego sprawdzają, czy aplikacje uzyskujące dostęp do danych są podpisane tym samym kluczem podpisywania. Dzięki temu te uprawnienia zapewniają większą wygodę i bezpieczeństwo użytkowników.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <permission android:name="my_custom_permission_name" android:protectionLevel="signature" />
Powiązane informacje:
Odmowa dostępu dostawcom treści aplikacji
Jeśli nie zamierzasz wysyłać danych z aplikacji do innej aplikacji, której nie jesteś właścicielem, wyraźnie zablokuj dostęp do obiektów ContentProvider
aplikacji innych deweloperów. To ustawienie jest szczególnie ważne, jeśli aplikację można zainstalować na urządzeniach z Androidem 4.1.1 (poziom interfejsu API 16) lub starszym, ponieważ atrybut android:exported
elementu <provider>
jest domyślnie ustawiony na true
w tych wersjach Androida.
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.myapp"> <application ... > <provider android:name="android.support.v4.content.FileProvider" android:authorities="com.example.myapp.fileprovider" ... android:exported="false"> <!-- Place child elements of <provider> here. --> </provider> ... </application> </manifest>
Pytaj o dane uwierzytelniające przed wyświetleniem informacji poufnych
Gdy prosisz użytkowników o dane logowania, aby mogli uzyskać dostęp do poufnych informacji lub treści premium w aplikacji, poproś o podanie kodu PIN, hasła, wzoru lub danych biometrycznych, takich jak rozpoznawanie twarzy lub odcisków palców.
Więcej informacji o tym, jak poprosić o dane biometryczne, znajdziesz w przewodniku na temat uwierzytelniania biometrycznego.
Stosowanie zabezpieczeń sieciowych
W sekcjach poniżej znajdziesz informacje o tym, jak poprawić bezpieczeństwo sieci aplikacji.
Używanie ruchu TLS
Jeśli aplikacja komunikuje się z serwerem internetowym, który ma certyfikat wydany przez znany i zaufany urząd certyfikacji (CA), użyj żądania HTTPS takiego jak to:
Kotlin
val url = URL("https://www.google.com") val urlConnection = url.openConnection() as HttpsURLConnection urlConnection.connect() urlConnection.inputStream.use { ... }
Java
URL url = new URL("https://www.google.com"); HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection(); urlConnection.connect(); InputStream in = urlConnection.getInputStream();
Dodawanie konfiguracji zabezpieczeń sieci
Jeśli Twoja aplikacja korzysta z nowych lub niestandardowych urzędów certyfikacji, możesz zadeklarować ustawienia zabezpieczeń sieci w pliku konfiguracyjnym. Dzięki temu możesz utworzyć konfigurację bez modyfikowania kodu aplikacji.
Aby dodać do aplikacji plik konfiguracji zabezpieczeń sieci:
- Zadeklaruj konfigurację w manifeście aplikacji:
-
Dodaj plik zasobu XML znajdujący się w folderze
res/xml/network_security_config.xml
.Określ, że cały ruch do określonych domen musi używać protokołu HTTPS, wyłączając tekst zwykły:
<network-security-config> <domain-config cleartextTrafficPermitted="false"> <domain includeSubdomains="true">secure.example.com</domain> ... </domain-config> </network-security-config>
W trakcie procesu tworzenia możesz użyć elementu
<debug-overrides>
, aby zezwolić na zainstalowane przez użytkownika certyfikaty. Ten element zastępuje opcje krytyczne dla bezpieczeństwa w aplikacji podczas debugowania i testowania, nie wpływając na konfigurację wersji aplikacji. Poniższy fragment kodu pokazuje, jak zdefiniować ten element w pliku XML konfiguracji zabezpieczeń sieci aplikacji:<network-security-config> <debug-overrides> <trust-anchors> <certificates src="user" /> </trust-anchors> </debug-overrides> </network-security-config>
<manifest ... > <application android:networkSecurityConfig="@xml/network_security_config" ... > <!-- Place child elements of <application> element here. --> </application> </manifest>
Powiązane informacje: konfiguracja zabezpieczeń sieci
Tworzenie własnego menedżera zaufania
Sprawdzacz TLS nie powinien akceptować każdego certyfikatu. W przypadku niektórych zastosowań może być konieczne skonfigurowanie menedżera zaufania i obsługa wszystkich ostrzeżeń dotyczących TLS, które się pojawiają:
- Komunikujesz się z serwerem WWW, który ma certyfikat podpisany przez nowy lub niestandardowy urząd certyfikacji.
- Urządzenie, którego używasz, nie ufa temu urzędowi certyfikacji.
- Nie możesz używać konfiguracji bezpieczeństwa sieci.
Więcej informacji o wykonaniu tych czynności znajdziesz w dyskusji na temat obsługi nieznanej instytucji certyfikującej.
Powiązane informacje:
Ostrożnie korzystaj z obiektów WebView
WebView
Elementy w aplikacji nie powinny umożliwiać użytkownikom przechodzenia do witryn, które nie są pod Twoją kontrolą. W miarę możliwości używaj listy dozwolonych, aby ograniczyć treści wczytywane przez obiekty WebView
w aplikacji.
Ponadto nigdy nie włączaj obsługi interfejsu JavaScript, chyba że masz pełną kontrolę nad treściami w obiektach WebView
w aplikacji i są one wiarygodne.
Korzystanie z kanałów wiadomości HTML
Jeśli Twoja aplikacja musi korzystać z interfejsu JavaScript na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) lub nowszym, zamiast komunikacji między witryną a aplikacją używaj kanałów wiadomości HTML, jak w tym fragmencie kodu:
Kotlin
val myWebView: WebView = findViewById(R.id.webview) // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. val channel: Array<out WebMessagePort> = myWebView.createWebMessageChannel() // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(object : WebMessagePort.WebMessageCallback() { override fun onMessage(port: WebMessagePort, message: WebMessage) { Log.d(TAG, "On port $port, received this message: $message") } }) // Send a message from channel[1] to channel[0]. channel[1].postMessage(WebMessage("My secure message"))
Java
WebView myWebView = (WebView) findViewById(R.id.webview); // channel[0] and channel[1] represent the two ports. // They are already entangled with each other and have been started. WebMessagePort[] channel = myWebView.createWebMessageChannel(); // Create handler for channel[0] to receive messages. channel[0].setWebMessageCallback(new WebMessagePort.WebMessageCallback() { @Override public void onMessage(WebMessagePort port, WebMessage message) { Log.d(TAG, "On port " + port + ", received this message: " + message); } }); // Send a message from channel[1] to channel[0]. channel[1].postMessage(new WebMessage("My secure message"));
Powiązane informacje:
Przyznawanie odpowiednich uprawnień
Proś o minimalną liczbę uprawnień niezbędnych do prawidłowego działania aplikacji. Jeśli to możliwe, usuń uprawnienia, gdy aplikacja ich już nie potrzebuje.
Odkładanie uprawnień do czasu wykonania zamierzonego działania
Jeśli to możliwe, nie dodawaj do aplikacji uprawnień, które umożliwiają wykonanie działania, które może zostać wykonane w innej aplikacji. Zamiast tego użyj intencji, aby odroczyć żądanie do innej aplikacji, która już ma niezbędne uprawnienia.
Ten przykład pokazuje, jak użyć zamiaru, aby kierować użytkowników do aplikacji kontaktów zamiast prosić o uprawnienia READ_CONTACTS
i WRITE_CONTACTS
:
Kotlin
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent(Intent.ACTION_INSERT).apply { type = ContactsContract.Contacts.CONTENT_TYPE }.also { intent -> // Make sure that the user has a contacts app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
Java
// Delegates the responsibility of creating the contact to a contacts app, // which has already been granted the appropriate WRITE_CONTACTS permission. Intent insertContactIntent = new Intent(Intent.ACTION_INSERT); insertContactIntent.setType(ContactsContract.Contacts.CONTENT_TYPE); // Make sure that the user has a contacts app installed on their device. if (insertContactIntent.resolveActivity(getPackageManager()) != null) { startActivity(insertContactIntent); }
Jeśli aplikacja musi wykonywać operacje wejścia/wyjścia związane z plikami (np. dostęp do pamięci lub wybranie pliku), nie potrzebuje specjalnych uprawnień, ponieważ system może wykonać te operacje w imieniu aplikacji. Co więcej, gdy użytkownik wybierze treści w określonym identyfikatorze URI, wywoływana aplikacja otrzyma uprawnienia do wybranego zasobu.
Powiązane informacje:
Bezpieczne udostępnianie danych w aplikacjach
Aby udostępniać zawartość aplikacji innym aplikacjom w bardziej bezpieczny sposób, postępuj zgodnie z tymi sprawdzonymi metodami:
- W razie potrzeby zastosuj uprawnienia tylko do odczytu lub tylko do zapisu.
-
Za pomocą flagi
FLAG_GRANT_READ_URI_PERMISSION
iFLAG_GRANT_WRITE_URI_PERMISSION
możesz przyznać klientom jednorazowy dostęp do danych. - Podczas udostępniania danych używaj identyfikatorów URI
content://
, a niefile://
. Przykłady:FileProvider
Ten fragment kodu pokazuje, jak za pomocą flag przyznawania uprawnień dotyczących identyfikatora URI i uprawnień dostawcy treści wyświetlić plik PDF aplikacji w oddzielnej aplikacji do przeglądania plików PDF:
Kotlin
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent(Intent.ACTION_VIEW).apply { data = Uri.parse("content://com.example/personal-info.pdf") // This flag gives the started app read access to the file. addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }.also { intent -> // Make sure that the user has a PDF viewer app installed on their device. intent.resolveActivity(packageManager)?.run { startActivity(intent) } }
Java
// Create an Intent to launch a PDF viewer for a file owned by this app. Intent viewPdfIntent = new Intent(Intent.ACTION_VIEW); viewPdfIntent.setData(Uri.parse("content://com.example/personal-info.pdf")); // This flag gives the started app read access to the file. viewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); // Make sure that the user has a PDF viewer app installed on their device. if (viewPdfIntent.resolveActivity(getPackageManager()) != null) { startActivity(viewPdfIntent); }
Uwaga: uruchamianie plików z katalogu głównego aplikacji, w której można zapisywać, jest naruszeniem zasady W^X.
Z tego powodu zaufane aplikacje kierowane na Androida 10 (poziom interfejsu API 29) lub nowszego nie mogą wywoływać funkcji exec()
w plikach w katalogu domowym aplikacji, tylko w kodzie binarnym zawartym w pliku APK aplikacji.
Aplikacje kierowane na Androida 10 lub nowszego nie mogą modyfikować w pamięci kodu wykonywalnego z plików otwartych za pomocą dlopen()
. Obejmuje to wszystkie pliki obiektów udostępnionych (.so
) z przemieszczonym tekstem.
Powiązane informacje:
android:grantUriPermissions
Bezpieczne przechowywanie danych
Aplikacja może wymagać dostępu do poufnych informacji o użytkownikach, ale użytkownicy udzielają jej dostępu do swoich danych tylko wtedy, gdy ufają, że są one odpowiednio chronione.
przechowywanie prywatnych danych w pamięci wewnętrznej;
przechowywać wszystkie prywatne dane użytkownika w pamięci wewnętrznej urządzenia, która jest oddzielona od innych aplikacji, Jako dodatkowy środek bezpieczeństwa, gdy użytkownik odinstaluje aplikację, urządzenie usuwa wszystkie pliki zapisane przez tę aplikację na pamięci wewnętrznej.
Poniższy fragment kodu pokazuje jedną z możliwości zapisywania danych na wewnętrznym dysku:
Kotlin
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. val FILE_NAME = "sensitive_info.txt" val fileContents = "This is some top-secret information!" File(filesDir, FILE_NAME).bufferedWriter().use { writer -> writer.write(fileContents) }
Java
// Creates a file with this name, or replaces an existing file // that has the same name. Note that the file name cannot contain // path separators. final String FILE_NAME = "sensitive_info.txt"; String fileContents = "This is some top-secret information!"; try (BufferedWriter writer = new BufferedWriter(new FileWriter(new File(getFilesDir(), FILE_NAME)))) { writer.write(fileContents); } catch (IOException e) { // Handle exception. }
Ten fragment kodu pokazuje odwrotną operację, czyli odczyt danych z pamięci wewnętrznej:
Kotlin
val FILE_NAME = "sensitive_info.txt" val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines -> lines.fold("") { working, line -> "$working\n$line" } }
Java
final String FILE_NAME = "sensitive_info.txt"; StringBuffer stringBuffer = new StringBuffer(); try (BufferedReader reader = new BufferedReader(new FileReader(new File(getFilesDir(), FILE_NAME)))) { String line = reader.readLine(); while (line != null) { stringBuffer.append(line).append('\n'); line = reader.readLine(); } } catch (IOException e) { // Handle exception. }
Powiązane informacje:
Przechowywanie danych w pamięci zewnętrznej na podstawie przypadku użycia
Używaj zewnętrznego miejsca na dane w przypadku dużych plików bez poufnych danych, które są specyficzne dla Twojej aplikacji, a także plików, które Twoja aplikacja udostępnia innym aplikacjom. Konkretne interfejsy API, których używasz, zależą od tego, czy Twoja aplikacja ma mieć dostęp do plików związanych z aplikacją, czy do plików współdzielonych.
Jeśli plik nie zawiera informacji prywatnych ani poufnych, ale jest przydatny dla użytkownika tylko w Twojej aplikacji, przechowuj go w katalogu aplikacji na zewnętrznym urządzeniu pamięci masowej.
Jeśli Twoja aplikacja musi uzyskać dostęp do pliku, który jest przydatny dla innych aplikacji, albo musi go przechowywać, użyj jednego z tych interfejsów API:
- Pliki multimedialne: aby przechowywać obrazy, pliki audio i filmy udostępniane między aplikacjami oraz uzyskiwać do nich dostęp, użyj interfejsu Media Store API.
- Inne pliki: aby przechowywać inne typy plików udostępnionych, w tym pobrane pliki, i uzyskać do nich dostęp, użyj interfejsu Storage Access Framework.
Sprawdzanie dostępności miejsca na dane
Jeśli Twoja aplikacja współpracuje z wymiennym zewnętrznym urządzeniem do przechowywania danych, pamiętaj, że użytkownik może je odłączyć, gdy aplikacja będzie próbować uzyskać do niego dostęp. Uwzględnij logikę, aby sprawdzać, czy urządzenie do przechowywania danych jest dostępne.
Sprawdzanie poprawności danych
Jeśli aplikacja korzysta z danych z zewnętrznego magazynu, sprawdź, czy zawartość danych nie została uszkodzona ani zmodyfikowana. Uwzględnij logikę do obsługi plików, które nie są już w stabilnym formacie.
Ten fragment kodu zawiera przykład weryfikatora hasha:
Kotlin
val hash = calculateHash(stream) // Store "expectedHash" in a secure location. if (hash == expectedHash) { // Work with the content. } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. suspend fun calculateHash(stream: InputStream): String { return withContext(Dispatchers.IO) { val digest = MessageDigest.getInstance("SHA-512") val digestStream = DigestInputStream(stream, digest) while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } digest.digest().joinToString(":") { "%02x".format(it) } } }
Java
Executor threadPoolExecutor = Executors.newFixedThreadPool(4); private interface HashCallback { void onHashCalculated(@Nullable String hash); } boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -> { if (Objects.equals(hash, expectedHash)) { // Work with the content. } }); if (!hashRunning) { // There was an error setting up the hash function. } private boolean calculateHash(@NonNull InputStream stream, @NonNull Executor executor, @NonNull HashCallback hashCallback) { final MessageDigest digest; try { digest = MessageDigest.getInstance("SHA-512"); } catch (NoSuchAlgorithmException nsa) { return false; } // Calculating the hash code can take quite a bit of time, so it shouldn't // be done on the main thread. executor.execute(() -> { String hash; try (DigestInputStream digestStream = new DigestInputStream(stream, digest)) { while (digestStream.read() != -1) { // The DigestInputStream does the work; nothing for us to do. } StringBuilder builder = new StringBuilder(); for (byte aByte : digest.digest()) { builder.append(String.format("%02x", aByte)).append(':'); } hash = builder.substring(0, builder.length() - 1); } catch (IOException e) { hash = null; } final String calculatedHash = hash; runOnUiThread(() -> hashCallback.onHashCalculated(calculatedHash)); }); return true; }
przechowywać w plikach pamięci podręcznej tylko dane niepoufne.
Aby zapewnić szybszy dostęp do niepoufnych danych aplikacji, przechowuj je w pamięci podręcznej urządzenia. W przypadku pamięci podręcznej większej niż 1 MB użyj opcji getExternalCacheDir()
.
W przypadku pamięci podręcznej o rozmiarze 1 MB lub mniejszym użyj opcji getCacheDir()
.
Obie metody zwracają obiekt File
, który zawiera dane z poziomu pamięci podręcznej Twojej aplikacji.
Ten fragment kodu pokazuje, jak zapisać w pamięci podręcznej plik, który został niedawno pobrany przez aplikację:
Kotlin
val cacheFile = File(myDownloadedFileUri).let { fileToCache -> File(cacheDir.path, fileToCache.name) }
Java
File cacheDir = getCacheDir(); File fileToCache = new File(myDownloadedFileUri); String fileToCacheName = fileToCache.getName(); File cacheFile = new File(cacheDir.getPath(), fileToCacheName);
Uwaga: jeśli używasz getExternalCacheDir()
do umieszczania pamięci podręcznej aplikacji w wspólnym miejscu na dane, użytkownik może wyjąć nośnik zawierający to miejsce na dane, gdy aplikacja jest uruchomiona. Uwzględnij logikę, która pozwoli Ci łagodnie obsłużyć brak pamięci podręcznej spowodowany przez takie zachowanie użytkownika.
Uwaga: te pliki nie są chronione.
Dlatego każda aplikacja kierowana na Androida 10 (poziom interfejsu API 29) lub niższego i mająca uprawnieniaWRITE_EXTERNAL_STORAGE
może uzyskać dostęp do zawartości tej pamięci podręcznej.
Powiązane informacje: omówienie przechowywania danych i plików
Korzystanie z SharedPreferences w trybie prywatnym
Jeśli używasz interfejsu getSharedPreferences()
do tworzenia obiektów SharedPreferences
w aplikacji lub uzyskiwania do nich dostępu, użyj interfejsu MODE_PRIVATE
. Dzięki temu tylko Twoja aplikacja będzie mieć dostęp do informacji w udostępnionym pliku preferencji.
Jeśli chcesz udostępniać dane w różnych aplikacjach, nie używaj obiektów SharedPreferences
. Zamiast tego wykonaj czynności opisane w artykule Bezpieczne udostępnianie danych w aplikacjach.
Biblioteka Security udostępnia też klasę EncryptedSharedPreferences, która zawiera klasę SharedPreferences i automatycznie szyfruje klucze i wartości.
Powiązane informacje:
Aktualizuj usługi i zależne komponenty
Większość aplikacji do wykonywania zadań specjalistycznych korzysta z bibliotek zewnętrznych i informacji o systemie urządzenia. Aktualizowanie zależności aplikacji zwiększa bezpieczeństwo tych punktów komunikacji.
Sprawdzanie dostawcy zabezpieczeń usług Google Play
Uwaga: ta sekcja dotyczy tylko aplikacji kierowanych na urządzenia z zainstalowanymi usługami Google Play.
Jeśli aplikacja korzysta z usług Google Play, sprawdź, czy są one aktualne na urządzeniu, na którym jest zainstalowana. Wykonaj sprawdzanie asynchronicznie, poza wątkiem interfejsu. Jeśli urządzenie nie jest aktualne, może wystąpić błąd autoryzacji.
Aby sprawdzić, czy Usługi Google Play są aktualne na urządzeniu, na którym zainstalowana jest aplikacja, wykonaj czynności opisane w tym przewodniku: Aktualizowanie usług dostawcy zabezpieczeń w celu ochrony przed atakami na protokół SSL.
Powiązane informacje:
Aktualizowanie wszystkich zależności aplikacji
Przed wdrożeniem aplikacji upewnij się, że wszystkie biblioteki, pakiety SDK i inne zależności są aktualne:
- W przypadku zależności własnego kodu, takich jak pakiet Android SDK, użyj narzędzi do aktualizacji dostępnych w Android Studio, np. Menedżera pakietu SDK.
- W przypadku zależności zewnętrznych sprawdź strony internetowe bibliotek, których używa Twoja aplikacja, i zainstaluj wszystkie dostępne aktualizacje i poprawki zabezpieczeń.
Powiązane informacje: dodawanie zależności kompilacji
Więcej informacji
Aby dowiedzieć się więcej o tym, jak zwiększyć bezpieczeństwo aplikacji, zapoznaj się z tymi materiałami:
- Lista kontrolna zabezpieczeń dotycząca ogólnej jakości aplikacji
- Program ulepszania zabezpieczeń aplikacji
- Kanał dla deweloperów aplikacji na Androida w YouTube
- Android Konfiguracja zabezpieczeń sieci – Codelab
- Android Protected Confirmation: Nowy wymiar bezpieczeństwa transakcji