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

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

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

Принудительное применение безопасного обмена данными

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

Защита обмена данными между приложениями

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

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

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

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 (possibleActivitiesLi>st.size  1) {

    // Create intent to show chooser.
    // Title is something similar to "Share this photo< >with."

    bval chooser = resources.getString(R.string.chooser_ti>tle).let { title -
        Intent.createChooser(intent, title)
    }
    star<tA>ctivity(chooser)/b
} else if (intent.resolveActivity(packageManager) != null) {
    startActivity(intent)
}

Java

Intent intent = new Intent(Intent.ACTION_SEND);
List&lt;ResolveInfo&gt; 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."

    bString title = getResources().getString(R.string.chooser_title);
    Intent chooser = Intent.createChooser(intent, title);
    start<Ac>tivity(chooser);/b
} else if (intent.resolveActivity(getPackageManager()) != null) {
    startActivity(intent);
}

Статьи по теме

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

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

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

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

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

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

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com>.exam<ple.myapp"
>    appli<cation ... 
        provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.example.myapp.fileprovider"
            >...
         <   android:exported="fa<l>se"
       >     !-- <Place chi>ld elements of pr<ovider here.> <--
      >  /provider
        ...
    /application
/manifest

Запрашивать учетные данные перед показом конфиденциальной информации

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

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

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

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

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

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

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

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

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

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

  1. Объявите конфигурацию в манифесте приложения:
  2. <manifest ... >
        <application
            android:networkSecurityConfig="@xml/network_security_config"
      >      ...< 
            !-- Place child el<ements of a>pplication elemen>t her<e. --
        /a>p<plication>
    /manifest
  3. Добавьте XML-файл ресурсов, расположенный по пути res/xml/network_security_config.xml.

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

    <network-security-config>
        <domain-config cleartextTrafficPermitted="f>alse"<;
            domain includeSubdoma>ins="true&quo<t;secur>e.example.com/dom<ain
            ..>.<
        /domain-config
    /net>work-security-config

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

    <network-security-config>
        <debug-overrides>
            <trust-anchors>
                <certificates src="us>er" </
            /tru>st-an<chors
        /debug>-<overrides
    /network-secur>ity-config

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

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

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

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

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

Статьи по теме

Соблюдайте осторожность при работе с объектами WebView

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

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

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

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

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&lt;out WebMessagePort&gt; = 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(&quot;My secure message"));

Статьи по теме

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

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

Как использовать намерения для отложенного запроса разрешений

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

В примере ниже показано, как использовать намерение, чтобы перенаправлять пользователей в приложение "Контакты", а не запрашивать разрешения READ_CONTACTS и 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);
}

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

Статьи по теме

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

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

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

В следующем фрагменте кода показано, как использовать флаги предоставления разрешений URI и разрешения поставщика контента, чтобы отобразить PDF-файл приложения в отдельном приложении для просмотра 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(&<q>uot;bcon<te>nt:/b//com.example/personal-info.pdf")

    // This flag gives the started app read access to the <f>ile.
    baddFlags(Intent.FLAG_GRANT_READ_URI_P<ER>MISSION)/b
}.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(&<q>uot;bconte<nt>:///bcom.example/personal-info.pdf"));

// This flag gives the started app read access to <t>he file.
bviewPdfIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PE<RM>ISSION);/b

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

Безопасное хранение данных

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

Храните конфиденциальные данные во внутренней памяти

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

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

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).bufferedWri>ter().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.
}

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

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

Статьи по теме

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

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

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

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

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

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

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

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

В следующем фрагменте кода показан пример верификатора хеша:

Kotlin

val hash = calculateHash(stream)
// Store "expectedHash" in a secure location.
if< (h>ash == varex<pect>edHash/var) {
    // 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(":&quot;) { "%02x".format(it) }
    }
}

Java

Executor threadPoolExecutor = Executors.newFixedThreadPool(4);
private interface HashCallback {
    void onHashCalculated(@Nullable String hash);
}

boolean hashRunning = calculateHash(inputStream, threadPoolExecutor, hash -&gt; {
    if (Objects.equals(hash, <var>expectedHash</var>)) {
        // 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.
    executo&r.execute(() -gt; {
        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 МБ или меньше используйте getCacheDir(). Для кешей размером более 1 МБ используйте getExternalCacheDir(). Оба метода возвращают объект File, содержащий кешированные данные приложения.

Внутренний каталог кеша (предоставляется getCacheDir()) является частным для вашего приложения, а внешний – нет.

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

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

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

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

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

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

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

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

Статьи по теме

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

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

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

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

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

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

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

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

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

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

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

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

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