Повысьте безопасность вашего приложения

Повышая безопасность своего приложения, вы помогаете сохранить доверие пользователей и целостность устройства.

На этой странице представлено несколько рекомендаций, которые окажут существенное положительное влияние на безопасность вашего приложения.

Обеспечить безопасную связь

Защищая данные, которыми вы обмениваетесь между своим приложением и другими приложениями или между своим приложением и веб-сайтом, вы повышаете стабильность своего приложения и защищаете данные, которые вы отправляете и получаете.

Защитите связь между приложениями

Чтобы взаимодействие между приложениями было более безопасным, используйте неявные намерения с выбором приложений, разрешения на основе подписей и поставщиков неэкспортированного контента.

Показать выбор приложения

Если неявное намерение может запустить как минимум два возможных приложения на устройстве пользователя, явно отобразите окно выбора приложений. Такая стратегия взаимодействия позволяет пользователям передавать конфиденциальную информацию в приложение, которому они доверяют.

Котлин

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

Ява

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

Дополнительная информация:

Применить разрешения на основе подписи

При обмене данными между двумя приложениями, которыми вы управляете или владеете, используйте разрешения на основе подписи . Эти разрешения не требуют подтверждения пользователя и вместо этого проверяют, что приложения, получающие доступ к данным, подписаны одним и тем же ключом. Таким образом, эти разрешения обеспечивают более удобный и безопасный пользовательский интерфейс.

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">
    <permission android:name="my_custom_permission_name"
                android:protectionLevel="signature" />

Дополнительная информация:

Запретить доступ поставщикам контента вашего приложения

Если вы не собираетесь отправлять данные из своего приложения в другое приложение, владельцем которого вы не являетесь, явно запретите приложениям других разработчиков доступ к объектам ContentProvider вашего приложения. Этот параметр особенно важен, если ваше приложение можно установить на устройства под управлением Android 4.1.1 (API уровня 16) или ниже, поскольку атрибут android:exported элемента <provider> по умолчанию имеет true в этих версиях Android.

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

Прежде чем показывать конфиденциальную информацию, запросите учетные данные.

При запросе у пользователей учетных данных для доступа к конфиденциальной информации или премиум-контенту в вашем приложении попросите либо PIN-код/пароль/графический ключ, либо биометрические учетные данные, например распознавание лица или отпечатков пальцев.

Дополнительную информацию о том, как запросить биометрические данные, см. в руководстве по биометрической аутентификации .

Применяйте меры сетевой безопасности

В следующих разделах описывается, как можно улучшить сетевую безопасность вашего приложения.

Использовать TLS-трафик

Если ваше приложение взаимодействует с веб-сервером, имеющим сертификат, выданный известным доверенным центром сертификации (CA), используйте HTTPS-запрос, подобный следующему:

Котлин

val url = URL("https://www.google.com")
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.connect()
urlConnection.inputStream.use {
    ...
}

Ява

URL url = new URL("https://www.google.com");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.connect();
InputStream in = urlConnection.getInputStream();

Добавить конфигурацию сетевой безопасности

Если ваше приложение использует новые или пользовательские центры сертификации, вы можете указать параметры безопасности сети в файле конфигурации. Это позволяет создать конфигурацию, не изменяя код приложения.

Чтобы добавить файл конфигурации сетевой безопасности в приложение, выполните следующие действия:

  1. Объявите конфигурацию в манифесте вашего приложения:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
            ... >
            <!-- Place child elements of <application> element here. -->
        </application>
    </manifest>
  3. Добавьте XML-файл ресурсов, расположенный по адресу res/xml/network_security_config.xml .

    Укажите, что весь трафик к определенным доменам должен использовать HTTPS, отключив открытый текст:

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

    В процессе разработки вы можете использовать элемент <debug-overrides> , чтобы явно разрешить использование сертификатов, установленных пользователем. Этот элемент переопределяет критически важные для безопасности параметры вашего приложения во время отладки и тестирования, не влияя на конфигурацию выпуска приложения. В следующем фрагменте кода показано, как определить этот элемент в XML-файле конфигурации сетевой безопасности вашего приложения:

    <network-security-config>
        <debug-overrides>
            <trust-anchors>
                <certificates src="user" />
            </trust-anchors>
        </debug-overrides>
    </network-security-config>

Дополнительная информация: Конфигурация сетевой безопасности

Создайте своего собственного управляющего трастом

Ваш инструмент проверки TLS не должен принимать все сертификаты. Вам может потребоваться настроить менеджер доверия и обрабатывать все предупреждения TLS, возникающие, если к вашему варианту использования применимо одно из следующих условий:

  • Вы взаимодействуете с веб-сервером, сертификат которого подписан новым или пользовательским центром сертификации.
  • Устройство, которое вы используете, не доверяет этому центру сертификации.
  • Вы не можете использовать конфигурацию сетевой безопасности .

Дополнительную информацию о выполнении этих шагов см. в обсуждении работы с неизвестным центром сертификации .

Дополнительная информация:

Используйте объекты WebView осторожно

Объекты WebView в вашем приложении не должны позволять пользователям переходить на сайты, находящиеся вне вашего контроля. По возможности используйте белый список, чтобы ограничить контент, загружаемый объектами WebView вашего приложения.

Кроме того, никогда не включайте поддержку интерфейса JavaScript, если вы не контролируете полностью и не доверяете содержимому объектов WebView вашего приложения.

Используйте каналы HTML-сообщений

Если ваше приложение должно использовать поддержку интерфейса JavaScript на устройствах под управлением Android 6.0 (уровень API 23) и выше, используйте каналы сообщений HTML вместо связи между веб-сайтом и вашим приложением, как показано в следующем фрагменте кода:

Котлин

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"))

Ява

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"));

Дополнительная информация:

Предоставьте правильные разрешения

Запрашивайте только минимальное количество разрешений, необходимое для корректной работы вашего приложения. По возможности отзывайте разрешения, когда они больше не нужны вашему приложению.

Используйте намерения для отсрочки разрешений

По возможности не добавляйте в приложение разрешение на выполнение действия, которое можно выполнить в другом приложении. Вместо этого используйте намерение, чтобы отложить запрос другому приложению, у которого уже есть необходимое разрешение.

В следующем примере показано, как использовать намерение для направления пользователей в приложение контактов вместо запроса разрешений READ_CONTACTS и WRITE_CONTACTS :

Котлин

// 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)
    }
}

Ява

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

Кроме того, если вашему приложению требуется выполнять файловый ввод-вывод, например, доступ к хранилищу или выбор файла, ему не требуются специальные разрешения, поскольку система может выполнить эти операции от имени вашего приложения. Более того, после того, как пользователь выбирает контент по определённому URI, вызывающее приложение получает разрешение на доступ к выбранному ресурсу.

Дополнительная информация:

Безопасный обмен данными между приложениями

Чтобы более безопасно делиться контентом своего приложения с другими приложениями, следуйте этим рекомендациям:

  • При необходимости используйте разрешения только на чтение или только на запись.
  • Предоставьте клиентам одноразовый доступ к данным с помощью флагов FLAG_GRANT_READ_URI_PERMISSION и FLAG_GRANT_WRITE_URI_PERMISSION .
  • При обмене данными используйте URI типа content:// , а не file:// . Экземпляры FileProvider сделают это за вас.

В следующем фрагменте кода показано, как использовать флаги предоставления разрешений URI и разрешения поставщика контента для отображения PDF-файла приложения в отдельном приложении для просмотра PDF-файлов:

Котлин

// 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)
    }
}

Ява

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

Примечание: Выполнение файлов из домашнего каталога приложения, доступного для записи, является нарушением W^X . По этой причине ненадёжные приложения, предназначенные для Android 10 (уровень API 29) и выше, не могут вызывать exec() для файлов в домашнем каталоге приложения, а только для двоичного кода, встроенного в APK-файл приложения. Кроме того, приложения, предназначенные для Android 10 и выше, не могут изменять исполняемый код в памяти из файлов, открытых с помощью dlopen() . Это включает в себя любые файлы общих объектов ( .so ) с перемещением текста.

Связанная информация: android:grantUriPermissions

Храните данные безопасно

Хотя вашему приложению может потребоваться доступ к конфиденциальной информации пользователя, пользователи предоставляют вашему приложению доступ к своим данным только в том случае, если они уверены, что вы обеспечиваете их надлежащую защиту.

Хранить личные данные во внутреннем хранилище

Храните все личные данные пользователя во внутренней памяти устройства, которая изолирована для каждого приложения. Вашему приложению не нужно запрашивать разрешение на просмотр этих файлов, и другие приложения не смогут получить к ним доступ. В качестве дополнительной меры безопасности, когда пользователь удаляет приложение, устройство удаляет все файлы, сохраненные приложением во внутренней памяти.

Следующий фрагмент кода демонстрирует один из способов записи данных во внутреннее хранилище:

Котлин

// 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)
}

Ява

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

В следующем фрагменте кода показана обратная операция — чтение данных из внутреннего хранилища:

Котлин

val FILE_NAME = "sensitive_info.txt"
val contents = File(filesDir, FILE_NAME).bufferedReader().useLines { lines ->
    lines.fold("") { working, line ->
        "$working\n$line"
    }
}

Ява

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

Дополнительная информация:

Хранить данные во внешнем хранилище в зависимости от варианта использования

Используйте внешнее хранилище для больших, неконфиденциальных файлов, специфичных для вашего приложения, а также для файлов, которыми ваше приложение делится с другими приложениями. Конкретные используемые API зависят от того, предназначено ли ваше приложение для доступа к файлам, специфичным для приложения, или к общим файлам.

Если файл не содержит личной или конфиденциальной информации, но представляет ценность для пользователя только в вашем приложении, сохраните файл в каталоге приложения на внешнем хранилище .

Если вашему приложению необходимо получить доступ к файлу, представляющему ценность для других приложений, или сохранить его, используйте один из следующих API в зависимости от вашего варианта использования:

  • Медиафайлы: для хранения и доступа к изображениям, аудиофайлам и видео, которые передаются между приложениями, используйте API Media Store .
  • Другие файлы: для хранения и доступа к другим типам общих файлов, включая загруженные файлы, используйте Storage Access Framework .

Проверить доступность объема хранилища

Если ваше приложение взаимодействует со съёмным внешним устройством хранения данных, учтите, что пользователь может извлечь устройство, пока приложение пытается к нему получить доступ. Включите логику для проверки доступности устройства хранения данных .

Проверить достоверность данных

Если ваше приложение использует данные из внешнего хранилища, убедитесь, что содержимое данных не повреждено и не изменено. Включите логику для обработки файлов, формат которых уже нестабилен.

Следующий фрагмент кода содержит пример хэш-верификатора:

Котлин

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

Ява

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

Хранить в кэш-файлах только неконфиденциальные данные

Чтобы обеспечить более быстрый доступ к неконфиденциальным данным приложения, сохраните их в кэше устройства. Для кэшей размером более 1 МБ используйте getExternalCacheDir() . Для кэшей размером 1 МБ и меньше используйте getCacheDir() . Оба метода предоставляют объект File , содержащий кэшированные данные вашего приложения.

В следующем фрагменте кода показано, как кэшировать файл, который ваше приложение недавно загрузило:

Котлин

val cacheFile = File(myDownloadedFileUri).let { fileToCache ->
    File(cacheDir.path, fileToCache.name)
}

Ява

File cacheDir = getCacheDir();
File fileToCache = new File(myDownloadedFileUri);
String fileToCacheName = fileToCache.getName();
File cacheFile = new File(cacheDir.getPath(), fileToCacheName);

Примечание: Если вы используете getExternalCacheDir() для размещения кэша приложения в общем хранилище, пользователь может извлечь носитель с этим хранилищем во время работы приложения. Включите логику для корректной обработки промахов кэша, возникающих из-за такого поведения пользователя.

Внимание: эти файлы не защищены никакими средствами безопасности. Поэтому любое приложение для Android 10 (уровень API 29) или ниже, имеющее разрешение WRITE_EXTERNAL_STORAGE , может получить доступ к содержимому этого кэша.

Дополнительная информация: Обзор хранилища данных и файлов

Используйте SharedPreferences в приватном режиме

При использовании getSharedPreferences() для создания или доступа к объектам SharedPreferences вашего приложения используйте MODE_PRIVATE . Таким образом, только ваше приложение сможет получить доступ к информации в файле общих настроек.

Если вы хотите обмениваться данными между приложениями, не используйте объекты SharedPreferences . Вместо этого следуйте инструкциям по безопасному обмену данными между приложениями .

Дополнительная информация:

Поддерживайте актуальность служб и зависимостей

Большинство приложений используют внешние библиотеки и системную информацию устройства для выполнения специализированных задач. Поддерживая зависимости приложения в актуальном состоянии, вы повышаете безопасность этих точек взаимодействия.

Проверьте поставщика безопасности сервисов Google Play

Примечание: этот раздел применим только к приложениям, предназначенным для устройств, на которых установлены сервисы Google Play .

Если ваше приложение использует сервисы Google Play, убедитесь, что оно обновлено на устройстве, где установлено. Проверка выполняется асинхронно, вне потока пользовательского интерфейса. Если устройство не обновлено, возникает ошибка авторизации.

Чтобы определить, обновлены ли сервисы Google Play на устройстве, где установлено ваше приложение, следуйте инструкциям в руководстве Обновление поставщика безопасности для защиты от атак SSL .

Дополнительная информация:

Обновить все зависимости приложения

Перед развертыванием приложения убедитесь, что все библиотеки, SDK и другие зависимости обновлены:

  • Для основных зависимостей, таких как Android SDK, используйте инструменты обновления, имеющиеся в Android Studio, например SDK Manager .
  • На предмет сторонних зависимостей проверьте веб-сайты библиотек, которые использует ваше приложение, и установите все доступные обновления и исправления безопасности.

Дополнительная информация: Добавление зависимостей сборки

Дополнительная информация

Чтобы узнать больше о том, как сделать свое приложение более безопасным, ознакомьтесь со следующими ресурсами: