Zwiększanie bezpieczeństwa aplikacji

Zwiększając bezpieczeństwo swojej aplikacji, pomagasz zachować zaufanie użytkowników i integralność urządzeń.

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 niejawna intencja 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ą płynniejsze i bezpieczniejsze korzystanie z aplikacji.

<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:

Blokowanie dostępu do dostawców 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 instalować na urządzeniach z Androidem 4.1.1 (poziom interfejsu API 16) lub starszym, ponieważ atrybut android:exported elementu <provider> w tych wersjach Androida domyślnie ma wartość true.

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

Korzystanie z ruchu TLS

Jeśli Twoja aplikacja komunikuje się z serwerem WWW, który ma certyfikat wystawiony przez dobrze znany, zaufany urząd certyfikacji (CA), użyj żądania HTTPS w ten sposób:

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 używa nowych lub niestandardowych urzędów certyfikacji, możesz zadeklarować ustawienia zabezpieczeń sieci w pliku konfiguracyjnym. Pozwala to utworzyć konfigurację bez modyfikowania kodu aplikacji.

Aby dodać do aplikacji plik konfiguracji zabezpieczeń sieci:

  1. Zadeklaruj konfigurację w pliku manifestu aplikacji:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
  3. Dodaj plik zasobu XML znajdujący się w folderzeres/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>

Powiązane informacje: konfiguracja zabezpieczeń sieci

Tworzenie własnego menedżera zaufania

Sprawdzacz TLS nie powinien akceptować każdego certyfikatu. Może być konieczne skonfigurowanie menedżera zaufania i obsługa wszystkich ostrzeżeń TLS, które pojawiają się, jeśli w Twoim przypadku użycia występuje jeden z tych warunków:

  • 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 używaj obiektów WebView

Obiekty WebView w aplikacji nie powinny pozwalać użytkownikom na przechodzenie do witryn, które są poza Twoją kontrolą. W miarę możliwości używaj listy dozwolonych, aby ograniczyć treści wczytywane przez obiekty WebView w aplikacji.

Dodatkowo nigdy nie włączaj obsługi interfejsu JavaScript, chyba że całkowicie kontrolujesz zawartość obiektów WebView swojej aplikacji i nie masz do niej zaufania.

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:

Przyznaj odpowiednie uprawnienia

Poproś o dostęp do minimalnej liczby uprawnień, która jest niezbędna 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

W miarę możliwości nie dodawaj do aplikacji uprawnień pozwalających wykonać działanie, które można wykonać w innej aplikacji. Zamiast tego użyj intencji, która odroczy żądanie do innej aplikacji, która ma już odpowiednie uprawnienia.

Ten przykład pokazuje, jak użyć zamiaru, aby kierować użytkowników do aplikacji kontaktów zamiast prosić o uprawnienia READ_CONTACTSWRITE_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);
}

Gdy aplikacja musi przeprowadzać operację wejścia-wyjścia na podstawie plików, np. uzyskać dostęp do pamięci lub wybrać plik, nie potrzebuje specjalnych uprawnień, ponieważ system może wykonywać te operacje w imieniu aplikacji. Co więcej, gdy użytkownik wybierze treści w określonym identyfikatorze URI, wywoływana aplikacja otrzymuje uprawnienia do wybranego zasobu.

Powiązane informacje:

Bezpieczne udostępnianie danych w aplikacjach

Aby bezpieczniej udostępniać treści innym aplikacjom, postępuj zgodnie z tymi sprawdzonymi metodami:

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 ze strony głównej aplikacji z możliwością zapisu stanowi naruszenie zasad 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. Oprócz tego aplikacje kierowane na Androida 10 lub nowszego nie mogą modyfikować kodu wykonywalnego z plików, które zostały otwarte przy użyciu dlopen(). Obejmuje to wszystkie pliki obiektów udostępnionych (.so) z przemieszczonym tekstem.

Powiązane informacje: android:grantUriPermissions

Bezpieczne przechowywanie danych

Chociaż aplikacja może wymagać dostępu do poufnych informacji użytkownika, użytkownicy przyznają jej dostęp do swoich danych tylko wtedy, gdy będą mieli pewność, że odpowiednio je zabezpieczasz.

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 wewnętrznej pamięci.

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 w zależności od 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 (w zależności od przypadku użycia):

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 mieć 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 zapisane w pamięci podręcznej dane aplikacji.

Ten fragment kodu pokazuje, jak zapisać w pamięci podręcznej plik pobrany ostatnio 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żyjesz getExternalCacheDir(), aby umieścić pamięć podręczną aplikacji w pamięci współdzielonej, użytkownik może wysunąć z niego nośnik znajdujący się w tym miejscu w trakcie działania aplikacji. Uwzględnij logikę, która pozwoli Ci sprawnie 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 uprawnienia WRITE_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 funkcji getSharedPreferences() do tworzenia obiektów SharedPreferences w aplikacji lub do uzyskiwania do nich dostępu, użyj funkcji 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 między aplikacjami, 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 otacza 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. Utrzymywanie aktualności 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 kontrolę asynchronicznie poza wątkiem UI. Jeśli urządzenie nie jest aktualne, wywołaj błąd autoryzacji.

Aby sprawdzić, czy Usługi Google Play są aktualne na urządzeniu, na którym jest zainstalowana aplikacja, wykonaj czynności opisane w tym przewodniku: Aktualizowanie dostawcy zabezpieczeń w celu ochrony przed atakami na SSL.

Powiązane informacje:

Zaktualizuj wszystkie 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łasnych, 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: