Хакер - Android: ошибки работы с разрешениями и библиотека Security-App-Authenticator
hacker_frei
Евгений Зобнин
Содержание статьи
- Почитать
- Ошибки работы с разрешениями
- Разработчику
- Библиотека Security-App-Authenticator
- Оптимизация загрузки приложений
- Группировка данных в Kotlin
- Value-классы в Kotlin 1.5
- Методы vs функции-расширения
- Авторазблокировка смартфона после установки сборки
- Библиотеки
Сегодня в выпуске: разбор типичных ошибок работы с разрешениями Android, библиотека Security-App-Authenticator для проверки подлинности приложений, статья об оптимизации загрузки приложения, а также функции‑расширения, value-классы и функции группировки данных в Kotlin. И конечно же, очередная подборка библиотек.
ПОЧИТАТЬ
Ошибки работы с разрешениями
Common mistakes when using permissions in Android — статья об ошибках, которые допускают разработчики при декларации собственных разрешений.
Преамбула: в Android есть система разрешений (permissions). Разрешения могут быть уровня normal (они предоставляются без вопросов), dangerous (приложение обязано запросить разрешение у пользователя), signature (приложение должно быть подписано тем же ключом, что и компонент, предоставляемый по разрешению) и нескольких других системных типов, используемых только приложениями из комплекта прошивки.
Кроме того, приложения могут декларировать свои собственные разрешения, которые должны использовать другие приложения для доступа к возможностям этого приложения (запускать активности, получать данные из content provider’ов и так далее). Декларация таких разрешений в манифесте выглядит примерно так:
<permission android:name="com.mycoolcam.USE_COOL_CAMERA" android:protectionLevel="dangerous" />
<activity android:name=".CoolCamActivity" android:exported="true" android:permission="com.mycoolcam.USE_COOL_CAMERA">
<intent-filter>
<action android:name="com.mycoolcam.LAUNCH_COOL_CAM" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
Сейчас доступ к активности CoolCamActivity защищен с помощью разрешения com.mycoolcam.USE_COOL_CAMERA.
А теперь ошибки, которые совершают разработчики, когда декларируют свои разрешения и защищают ими компоненты:
- Уровень разрешения не указан. Если в предыдущем примере не указать уровень разрешения (
android:protectionLevel="dangerous"), он станет normal и в итоге доступ к защищенной активности сможет получить какое угодно приложение (достаточно указать в манифесте, что оно использует это разрешение). - Уровень разрешения signature не определен во всех приложениях. Допустим, у тебя есть два приложения, которые должны обмениваться данными. Чтобы защитить их от несанкционированного доступа, ты добавляешь в приложение 1 определение разрешения:
<permission android:name="com.mycoolcam.USE_COOL_CAMERA" android:protectionLevel="signature" />- При этом в приложении 1 такого определения нет, так как оно только использует это разрешение. В итоге если на устройство установить только второе приложение, то Android, ничего не знающий о разрешении
com.mycoolcam.USE_COOL_CAMERA, автоматически назначит ему уровень normal вместо определенного в приложении 1 уровня signature. - Добавляй определение разрешения во все приложения.
- Отсутствие каких‑либо разрешений. Допустим, есть приложение, которое использует системное разрешение
android.permission.READ_CONTACTS. У этого приложения есть content provider, который предоставляет доступ к базе данных приложения, включая контакты пользователей. И этот content provider не защищен никаким разрешением. В итоге сторонние приложения могут получить доступ к контактам пользователя опосредованно, через это приложение.
РАЗРАБОТЧИКУ
Библиотека Security-App-Authenticator
Hands on with Jetpack’s Security App Authenticator library — небольшая заметка о новой библиотеке в комплекте Android Jetpack.
Библиотека нужна для проверки подлинности сертификатов приложений, с которыми будет контактировать твое приложение. Делается это с помощью сверки контрольной суммы сертификата приложения и его сравнения с уже сохраненным списком контрольных сумм. Этакий SSL Pinning для приложений.
Перед тем как использовать библиотеку, в каталог подкаталоге xml ресурсов приложения необходимо поместить XML-файл с произвольным именем и примерно следующим содержимым:
<?xml version="1.0" encoding="utf-8"?>
<app-authenticator>
<expected-identity>
<package name="com.example.app">
<cert-digest>7d5ac0f764d5ae47a051777bb5fc9a96f30b6b4d3bbb95cddb1c32932fb28b10</cert-digest>
</package>
</expected-identity>
</app-authenticator>
Этот файл говорит, что приложение с именем пакета com.example.app должно иметь сертификат с указанной контрольной суммой SHA-256.
Далее можно начать использовать библиотеку:
// Создаем экземпляр аутентификатора из описанного выше XML-файла
val authenticator = AppAuthenticator.createFromResource(context, R.xml.expected_app_identities)
// Первый способ проверки сертификата
val result = when (authenticator.checkAppIdentity("com.example.app")) {
AppAuthenticator.SIGNATURE_MATCH -> "Signature matches"
AppAuthenticator.SIGNATURE_NO_MATCH -> "Signature does not match"
else -> return
}
// Второй способ проверки с выбросом исключения
try {
authenticator.enforceAppIdentity("com.example.app)
// ...работаем с приложением
} catch (e: SecurityException) {
// ...не стоит доверять этому приложению
}
Как видно, есть несколько способов проверки. Первый удобно использовать для простых проверок. Второй удобен для использования в функциях работы с проверяемым приложением. Просто добавляем в их начало проверку, и все остальное сделает система обработки исключений.
Оптимизация загрузки приложений
How OkCredit Android App improved Cold Startup by 70% Anjal Saneen — очередная статья об ускорении холодного запуска приложения. Основные моменты:
- Ленивая инициализация. Ленивая инициализация объектов с помощью Dagger Lazy позволяет сильно сократить время старта приложения.
- Фоновые потоки. Чтобы основной поток приложения не тратил время на инициализацию компонентов, эту задачу лучше вынести в фоновый поток:
override fun onCreate() {super.onCreate()setupDependencyInjection()observeProcessLifecycle()Executors.newSingleThreadExecutor().execute {firebaseRemoteConfig.get().fetchAndActivate()setupJobScheduler()setupAppsFlyer()setupAnalytics()trackDeviceInfo()}}- Оптимизация элементов UI. Чем меньше в UI используется сложных вложенных друг в друга элементов интерфейса, тем быстрее отрабатывает отрисовка. Самый простой способ сделать это — использовать Constraint Layout, способный заменить слоеный пирог других лейаутов. При этом самым быстрым в отрисовке считается FrameLayout. Именно его следует использовать в качестве контейнера.
- Удаление Firebase. Firebase — отличный инструмент аналитики, но на его инициализацию уходит слишком много времени. Если удалить Firebase и заменить его более легким решением, то можно выиграть несколько десятков миллисекунд.
- Избавление от Joda Time. Joda Time — прекрасная библиотека для работы со временем, но в ней используются крайне неэффективные решения. Лучше либо заменить эту библиотеку
java.time, либо отложить ее инициализацию на как можно более позднее время. - Ленивая инициализация content provider’ов. Инициализация провайдеров контента (а среди них есть и WorkManager, и тот же Firebase) происходит в основном потоке до запуска самого приложения. Используя библиотеку Jetpack AndroidX App Startup, инициализацию content provider’ов можно отложить и сэкономить еще несколько десятков миллисекунд.
- Отказ от I/O-операции и десериализации JSON в основном потоке. Здесь все просто и логично.
Группировка данных в Kotlin
Kotlin Grouping — статья о встроенных функциях группировки в языке Kotlin.
В Kotlin есть функция‑расширение groupBy(), которую можно применить к коллекциям. Работает она так:

Как видно, функция разбивает List на Map, где в качестве ключей используются значения, полученные при выполнении указанной в аргументе groupBy() лямбды, а в качестве значений — список значений, входящих в эту группу. В данном случае тело лямбды — it.first(), поэтому группировка происходит по первому символу.
Интересно, что groupBy() принимает и второй аргумент. В нем можно передать функцию трансформации, которая будет выполнена над значениями коллекции.

Еще одна функция группировки — это groupingBy(). В отличие от предыдущей, она предназначена не для группировки как таковой, а для выполнения над сгруппированными данными какой‑либо общей операции. Допустим, мы хотим посчитать количество слов на каждую букву в коллекции. Мы могли бы сделать это в цикле, но можно обойтись всего одной строкой.

Обрати внимание, что функция возвращает не Map или List, а объект Grouping, над которым мы как раз и выполняем операцию eachCount() — подсчет общего количества элементов в каждой группе. Также Grouping поддерживает операции fold(), reduce() и aggregate(), о которых можно прочитать в официальной документации.
Более сложный пример, использующий обе функции: подсчет количества законченных и незаконченных задач.

Value-классы в Kotlin 1.5
How I use the new Inline Value Classes in Kotlin — хорошая статья с рассказом о новом типе классов в Kotlin 1.5.
Представь себе следующую ситуацию. Есть такой класс:
class Person(
val firstName: String,
val lastName: String,
val age: Int,
val siblings: Int,
...
)
Банальнейший класс, аналоги которого создавал любой программист. Где‑то в коде также есть такая функция:
fun setPersonData(firstName: String, lastName: String, age: Int, siblings: Int)
Пока все абсолютно нормально, но что, если ты просто перепутаешь порядок аргументов:
setPersonData("Smith", "John", 0, 24)
В этом случае функция отработает абсолютно неправильно, а среда разработки не сделает ни единого предупреждения. К тому же если в будущем внутренний формат хранения данных изменится (в этом примере проблем нет, но в реальном коде ID типа Int может замениться, например, на текстовый UID), то перерабатывать придется весь код, работающий с функцией и классом.
Решить эти проблемы можно так:
class Person(
val firstName: FirstName,
val lastName: LastName,
val age: Age,
val siblings: Siblings,
...
)
fun setPersonData(firstName: FirstName, lastName: LastName, age: Age, siblings: Siblings)
setPersonData(
FirstName("John"),
LastName("Smith"),
Age(24),
Siblings(0),
)
То есть обернуть простые типы данных в объекты. Но! Во‑первых, это неудобно, во‑вторых, постоянное создание объектов в итоге приведет к падению производительности.
Как раз здесь на сцену выходит value class. По сути, это класс с одним полем, например:
value class FirstName(val value: String)
Однако, в отличие от обычного класса, value-класс инлайновый. Он не будет существовать в результирующем байт‑коде приложения. Компилятор развернет все value-классы и будет использовать вместо них сохраненные внутри значения.
Более того, value-класс может не просто хранить значение, но и валидировать его. Например:
value class Age(
val value: Int,
) {
init {
require(value >= 0) { "age must be >= 0: '$value'" }
}
}
Такой value-класс выбросит исключение IllegalArgumentException, если создать его с отрицательным значением.
Методы vs функции-расширения
Consider extracting non-essential parts of your API into extensions — статья о различиях между методами класса и функциями‑расширениями.
Допустим, у нас есть два фрагмента кода. Первый:
class Workshop(/*...*/) {
fun makeEvent(date: DateTime): Event = //...
val permalink
get() = "/workshop/$name"
}
Второй:
class Workshop(/*...*/) {
}
fun Workshop.makeEvent(date: DateTime): Event = //...
val Workshop.permalink
get() = "/workshop/$name"
Они очень похожи, в том смысле, что добавляют в один класс одни и те же функции. Но первый фрагмент делает это классическим способом — добавляя в класс методы, а второй — через использование функций‑расширений.
Все выглядит почти одинаковым, но есть различия:
- В отличие от родных методов, расширения нужно импортировать отдельно.
- Расширения не виртуальные, то есть их нельзя переопределить в классах‑наследниках.
- Функции‑расширения работают с типом, а не с классом.
- Формально функции‑расширения не считаются частью класса.
Благодаря первому различию функции‑расширения можно рассматривать как отличных кандидатов для дополнительных функций, которые могут понадобиться, но в большинстве случаев не нужны. Более того, можно создать несколько файлов функций‑расширений, которые изменяют класс по‑разному. В одной ситуации могут быть нужны одни дополнительные функции, в другой — другие.
Второе важное различие: функции‑расширения добавляются к типу, а не к классу. Это позволяет писать, например, такие функции‑расширения:
inline fun CharSequence?.isNullOrBlank(): Boolean {
contract {
returns(false) implies (this@isNullOrBlank != null)
}
return this == null || this.isBlank()
}
С точки зрения компилятора, CharSequence? и CharSequence — это разные типы.
Авторазблокировка смартфона после установки сборки
Auto Unlock Android Device on App Deploy — короткая заметка о том, как автоматически разблокировать смартфон после установки новой сборки приложения.
Способ работает только в UNIX-системах и требует скрипт device_awake.sh. Но, чтобы он заработал, необходимо исправить несколько строк:
adb shell input keyevent KEYCODE_WAKEUP # wakeup device
adb shell input touchscreen swipe 530 1420 530 1120 # swipe up gesture
adb shell input text "000000" # <- Change to the your device PIN/Password
#adb shell input keyevent 66 # simulate press enter, if your keyguard requires it
Первую строку оставляем как есть. Она включает экран.
Вторая строка делает свайп снизу вверх, но если используется телефон с высоким разрешением, то последнюю цифру лучше исправить на 500 или даже меньше (иначе свайп будет слишком коротким).
Третья строка вводит PIN-код, его нужно вписать вместо строки "000000".
Четвертая строка нужна не на всех телефонах и поэтому закомментирована. Если после ввода PIN-кода не происходит автоматическая разблокировка экрана — ее следует раскомментировать:
- Переходим в Android Studio и открываем параметры сборки (Edit configuration...).
- В открывшемся окне нажимаем кнопку + и выбираем Shell Script.
- Указываем в первом поле ввода путь к скрипту.
- Возвращаемся обратно в конфигурацию приложения и в разделе Before launch нажимаем + и выбираем Run Another Configuration, выбираем наш скрипт.
В оригинальной статье есть картинки, поясняющие эти шаги.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei