Сделав свое приложение более безопасным, вы поможете сохранить доверие пользователей и целостность устройства.
На этой странице представлено несколько рекомендаций, которые окажут существенное положительное влияние на безопасность вашего приложения.
Обеспечить безопасную связь
Защищая данные, которыми вы обмениваетесь между своим приложением и другими приложениями или между своим приложением и веб-сайтом, вы повышаете стабильность своего приложения и защищаете данные, которые вы отправляете и получаете.
Защитите связь между приложениями
Чтобы обеспечить более безопасную связь между приложениями, используйте неявные намерения с выбором приложений, разрешения на основе подписей и поставщиков неэкспортируемого контента.
Показать выбор приложения
Если неявное намерение может запустить по крайней мере два возможных приложения на устройстве пользователя, явно покажите средство выбора приложений. Эта стратегия взаимодействия позволяет пользователям передавать конфиденциальную информацию в приложение, которому они доверяют.
Котлин
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();
Добавить конфигурацию сетевой безопасности
Если ваше приложение использует новые или пользовательские CA, вы можете объявить настройки безопасности вашей сети в файле конфигурации. Этот процесс позволяет вам создать конфигурацию, не изменяя код приложения.
Чтобы добавить файл конфигурации сетевой безопасности в приложение, выполните следующие действия:
- Объявите конфигурацию в манифесте вашего приложения:
Добавьте файл ресурсов 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>
<manifest ... > <application android:networkSecurityConfig="@xml/network_security_config" ... > <!-- Place child elements of <application> element here. --> </application> </manifest>
Сопутствующая информация: Конфигурация сетевой безопасности
Создайте своего собственного доверительного управляющего
Ваш 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 .
- На предмет сторонних зависимостей проверьте веб-сайты библиотек, которые использует ваше приложение, и установите все доступные обновления и исправления безопасности.
Связанная информация: Добавить зависимости сборки
Дополнительная информация
Чтобы узнать больше о том, как сделать свое приложение более безопасным, ознакомьтесь со следующими ресурсами:
- Контрольный список безопасности основного приложения
- Программа повышения безопасности приложений
- Канал разработчиков Android на YouTube
- Android Protected Confirmation: безопасность транзакций на новом уровне