Zwiększanie bezpieczeństwa aplikacji

Zwiększają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

Zabezpieczając dane wymieniane między Twoją aplikacją a innymi aplikacjami lub między Twoją aplikacją a witryną, zwiększasz stabilność aplikacji i chronisz wysyłane i odbierane dane.

Zabezpieczanie komunikacji między aplikacjami

Aby bezpieczniej komunikować się między aplikacjami, używaj niejawnych intencji z selektorem aplikacji, uprawnień opartych na sygnaturze i nieeksportowanych dostawców treści.

Wyświetlanie selektora aplikacji

Jeśli niejawny zamiar może uruchomić co najmniej 2 aplikacje na urządzeniu użytkownika, wyświetl 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

Podczas udostępniania danych między 2 aplikacjami, które kontrolujesz lub których jesteś właścicielem, używaj uprawnień opartych na sygnaturze. Te uprawnienia nie wymagają potwierdzenia przez użytkownika, a zamiast tego sprawdzają, czy aplikacje uzyskujące dostęp do danych są podpisane tym samym kluczem podpisu. Dlatego te uprawnienia zapewniają bardziej przejrzystą i bezpieczną obsługę.

<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 w aplikacji

Jeśli nie zamierzasz wysyłać danych z aplikacji do innej aplikacji, która nie jest Twoją własnością, wyraźnie zabroń aplikacjom innych deweloperów dostępu do obiektów ContentProvider Twojej aplikacji. To ustawienie jest szczególnie ważne, jeśli aplikację można zainstalować na urządzeniach z Androidem 4.1.1 (API na poziomie 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>

Proś o dane logowania przed wyświetleniem informacji poufnych

Gdy prosisz użytkowników o podanie danych logowania, aby mogli uzyskać dostęp do informacji poufnych lub treści premium w Twojej aplikacji, poproś o kod PIN, hasło lub wzór albo o dane biometryczne, takie jak rozpoznawanie twarzy lub odcisków palców.

Więcej informacji o tym, jak poprosić o dane biometryczne, znajdziesz w przewodniku po uwierzytelnianiu biometrycznym.

Stosowanie środków bezpieczeństwa sieci

W sekcjach poniżej opisujemy, jak możesz zwiększyć bezpieczeństwo sieci w aplikacji.

Używanie ruchu TLS

Jeśli aplikacja komunikuje się z serwerem internetowym, który ma certyfikat wydany przez znanego, zaufanego urzędu certyfikacji, użyj żądania HTTPS, takiego jak to poniżej:

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 bezpieczeństwa sieci w pliku konfiguracyjnym. Dzięki temu możesz utworzyć konfigurację bez modyfikowania kodu aplikacji.

Aby dodać do aplikacji plik konfiguracji zabezpieczeń sieci, wykonaj te czynności:

  1. Zadeklaruj konfigurację w manifeście 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 lokalizacji 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 jawny:

    <network-security-config>
        <domain-config cleartextTrafficPermitted="false">
            <domain includeSubdomains="true">secure.example.com</domain>
            ...
        </domain-config>
    </network-security-config>

    Podczas procesu programowania możesz użyć elementu <debug-overrides>, aby wyraźnie zezwolić na certyfikaty zainstalowane przez użytkownika. Ten element zastępuje opcje krytyczne dla bezpieczeństwa aplikacji podczas debugowania i testowania bez wpływu na konfigurację wersji aplikacji. Poniższy fragment 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

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

  • Komunikujesz się z serwerem internetowym, który ma certyfikat podpisany przez nowy lub niestandardowy urząd certyfikacji.
  • Ten urząd certyfikacji nie jest zaufany na urządzeniu, którego używasz.
  • Nie możesz używać konfiguracji bezpieczeństwa sieci.

Więcej informacji o wykonywaniu tych czynności znajdziesz w rozdziale o obsłudze nieznanego urzędu certyfikacji.

Powiązane informacje:

Rozważne używanie obiektów WebView

WebViewObiekty w aplikacji nie powinny umożliwiać użytkownikom przechodzenia do witryn, nad którymi nie masz kontroli. W miarę możliwości używaj listy dozwolonych, aby ograniczać 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 aplikacji i im ufasz.

Korzystanie z kanałów wiadomości HTML

Jeśli Twoja aplikacja musi korzystać z obsługi interfejsu JavaScript na urządzeniach z Androidem 6.0 (poziom interfejsu API 23) lub nowszym, używaj kanałów wiadomości HTML zamiast komunikacji między witryną a aplikacją, jak pokazano 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ś tylko o minimalną liczbę uprawnień niezbędnych do prawidłowego działania aplikacji. W miarę możliwości zwalniaj uprawnienia, gdy aplikacja nie będzie ich już potrzebować.

Używanie intencji do odraczania uprawnień

Jeśli to możliwe, nie dodawaj do aplikacji uprawnień, aby wykonać działanie, które można wykonać w innej aplikacji. Zamiast tego użyj intencji, aby przekazać żądanie do innej aplikacji, która ma już niezbędne uprawnienia.

Poniższy przykład pokazuje, jak użyć intencji, aby przekierować użytkowników do aplikacji do 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);
}

Jeśli aplikacja musi wykonywać operacje wejścia/wyjścia na plikach, np. uzyskiwać dostęp do pamięci lub wybierać plik, nie potrzebuje specjalnych uprawnień, ponieważ system może wykonywać te operacje w jej imieniu. Co więcej, gdy użytkownik wybierze treść pod określonym identyfikatorem URI, aplikacja wywołująca otrzyma uprawnienia do wybranego zasobu.

Powiązane informacje:

Bezpieczne udostępnianie danych w aplikacjach

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

Ten fragment kodu pokazuje, jak używać flag przyznawania uprawnień dotyczących identyfikatora URI i uprawnień dostawcy treści do wyświetlania pliku PDF aplikacji w osobnej 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: wykonywanie plików z katalogu głównego aplikacji z możliwością zapisu jest naruszeniem zasady W^X. Z tego powodu niezaufane aplikacje kierowane na Androida 10 (poziom interfejsu API 29) i nowsze nie mogą wywoływać funkcji exec() w przypadku plików w katalogu domowym aplikacji, a jedynie w przypadku kodu binarnego osadzonego w pliku APK aplikacji. Aplikacje kierowane na Androida 10 i nowsze nie mogą też modyfikować w pamięci kodu wykonywalnego z plików otwartych za pomocą funkcji dlopen(). Obejmuje to wszystkie pliki obiektów współdzielonych (.so) z relokacjami tekstu.

Powiązane informacje: android:grantUriPermissions

Bezpieczne przechowywanie danych

Aplikacja może wymagać dostępu do poufnych informacji o użytkownikach, ale użytkownicy przyznają jej dostęp do swoich danych tylko wtedy, gdy mają pewność, że odpowiednio je zabezpieczasz.

przechowywać dane prywatne w pamięci wewnętrznej,

Wszystkie prywatne dane użytkownika przechowuj w pamięci wewnętrznej urządzenia, która jest odseparowana od innych aplikacji. Aplikacja nie musi prosić o uprawnienia do wyświetlania tych plików, a inne aplikacje nie mają do nich dostępu. Dodatkowo, gdy użytkownik odinstaluje aplikację, urządzenie usunie wszystkie pliki, które aplikacja zapisała w pamięci wewnętrznej.

Poniższy fragment kodu pokazuje jeden ze sposobów zapisywania danych w pamięci wewnętrznej:

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

Poniższy fragment kodu pokazuje operację odwrotną, czyli odczytywanie 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 pamięci zewnętrznej w przypadku dużych, niepoufnych plików, które są specyficzne dla Twojej aplikacji, oraz plików, które aplikacja udostępnia innym aplikacjom. Konkretne interfejsy API, z których korzystasz, zależą od tego, czy aplikacja ma dostęp do plików specyficznych dla aplikacji, czy do plików udostępnionych.

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

Jeśli aplikacja musi uzyskać dostęp do pliku, który jest przydatny dla innych aplikacji, lub go przechowywać, użyj jednego z tych interfejsów API w zależności od przypadku użycia:

Sprawdzanie dostępności woluminu pamięci

Jeśli Twoja aplikacja wchodzi w interakcję z wymiennym zewnętrznym urządzeniem pamięci masowej, pamiętaj, że użytkownik może usunąć to urządzenie, gdy aplikacja będzie próbować uzyskać do niego dostęp. Dodaj logikę, która sprawdzi, czy urządzenie pamięci masowej jest dostępne.

Sprawdzanie poprawności danych

Jeśli aplikacja korzysta z danych z pamięci zewnętrznej, upewnij się, że zawartość danych nie została uszkodzona ani zmodyfikowana. Uwzględnij logikę obsługi plików, które nie są już w stabilnym formacie.

Poniższy fragment kodu zawiera przykład weryfikatora skrótu:

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;
}

W plikach pamięci podręcznej przechowuj tylko dane niewrażliwe

Aby zapewnić szybszy dostęp do niepoufnych danych aplikacji, przechowuj je w pamięci podręcznej urządzenia. W przypadku pamięci podręcznych większych niż 1 MB użyj getExternalCacheDir(). W przypadku pamięci podręcznych o rozmiarze 1 MB lub mniejszym użyj getCacheDir(). Obie metody zwracają obiekt File, który zawiera dane aplikacji zapisane w pamięci podręcznej.

Ten fragment kodu pokazuje, jak zapisać w pamięci podręcznej plik, który aplikacja niedawno pobrała:

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 pamięci współdzielonej, użytkownik może wyjąć nośnik zawierający tę pamięć podczas działania aplikacji. Dodaj logikę, która będzie prawidłowo obsługiwać brak w pamięci podręcznej spowodowany przez to zachowanie użytkownika.

Uwaga: te pliki nie są zabezpieczone. Dlatego każda aplikacja, która jest kierowana na Androida 10 (API na poziomie 29) lub starszego i ma uprawnienie 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

Używanie SharedPreferences w trybie prywatnym

Podczas korzystania z usługi getSharedPreferences() w celu tworzenia obiektów SharedPreferences aplikacji lub uzyskiwania do nich dostępu używaj MODE_PRIVATE. Dzięki temu tylko Twoja aplikacja będzie mieć dostęp do informacji w pliku ustawień współdzielonych.

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.

Powiązane informacje:

Aktualizowanie usług i zależności

Większość aplikacji korzysta z zewnętrznych bibliotek i informacji o systemie urządzenia, aby wykonywać specjalistyczne zadania. Aktualizując zależności aplikacji, zwiększasz bezpieczeństwo tych punktów komunikacji.

Sprawdzanie dostawcy zabezpieczeń Usług Google Play

Uwaga: ta sekcja dotyczy tylko aplikacji kierowanych na urządzenia, na których są zainstalowane usługi Google Play.

Jeśli Twoja aplikacja korzysta z Usług Google Play, sprawdź, czy są one zaktualizowane na urządzeniu, na którym jest zainstalowana aplikacja. Przeprowadź sprawdzanie asynchronicznie, poza wątkiem interfejsu. Jeśli urządzenie nie jest aktualne, wywołaj błąd autoryzacji.

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

Powiązane informacje:

Aktualizowanie wszystkich zależności aplikacji

Zanim wdrożysz aplikację, 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żywaj narzędzi do aktualizacji dostępnych w Android Studio, np. Menedżera SDK.
  • W przypadku zależności zewnętrznych sprawdź witryny bibliotek, z których korzysta Twoja aplikacja, i zainstaluj wszystkie dostępne aktualizacje i poprawki zabezpieczeń.

Powiązane informacje: Dodawanie zależności kompilacji

Więcej informacji

Więcej informacji o tym, jak zwiększyć bezpieczeństwo aplikacji, znajdziesz w tych materiałach: