Хакер - Android: ошибки работы с разрешениями и библиотека Security-App-Authenticator

Хакер - Android: ошибки работы с разрешениями и библиотека Security-App-Authenticator

hacker_frei

https://t.me/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.

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

  1. Уро­вень раз­решения не ука­зан. Если в пре­дыду­щем при­мере не ука­зать уро­вень раз­решения (android:protectionLevel="dangerous"), он ста­нет normal и в ито­ге дос­туп к защищен­ной активнос­ти смо­жет получить какое угод­но при­ложе­ние (дос­таточ­но ука­зать в манифес­те, что оно исполь­зует это раз­решение).
  2. Уро­вень раз­решения signature не опре­делен во всех при­ложе­ниях. Допус­тим, у тебя есть два при­ложе­ния, которые дол­жны обме­нивать­ся дан­ными. Что­бы защитить их от несан­кци­они­рован­ного дос­тупа, ты добав­ляешь в при­ложе­ние 1 опре­деле­ние раз­решения:
  3. <permission android:name="com.mycoolcam.USE_COOL_CAMERA" android:protectionLevel="signature" />
  4. При этом в при­ложе­нии 1 такого опре­деле­ния нет, так как оно толь­ко исполь­зует это раз­решение. В ито­ге если на устрой­ство уста­новить толь­ко вто­рое при­ложе­ние, то Android, ничего не зна­ющий о раз­решении com.mycoolcam.USE_COOL_CAMERA, авто­мати­чес­ки наз­начит ему уро­вень normal вмес­то опре­делен­ного в при­ложе­нии 1 уров­ня signature.
  5. До­бав­ляй опре­деле­ние раз­решения во все при­ложе­ния.
  6. От­сутс­твие каких‑либо раз­решений. Допус­тим, есть при­ложе­ние, которое исполь­зует сис­темное раз­решение 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 — оче­ред­ная статья об уско­рении холод­ного запус­ка при­ложе­ния. Основные момен­ты:

  1. Ле­нивая ини­циали­зация. Ленивая ини­циали­зация объ­ектов с помощью Dagger Lazy поз­воля­ет силь­но сок­ратить вре­мя стар­та при­ложе­ния.
  2. Фо­новые потоки. Что­бы основной поток при­ложе­ния не тра­тил вре­мя на ини­циали­зацию ком­понен­тов, эту задачу луч­ше вынес­ти в фоновый поток:
  3. override fun onCreate() {
  4. super.onCreate()
  5. setupDependencyInjection()
  6. observeProcessLifecycle()
  7. Executors.newSingleThreadExecutor().execute {
  8. firebaseRemoteConfig.get().fetchAndActivate()
  9. setupJobScheduler()
  10. setupAppsFlyer()
  11. setupAnalytics()
  12. trackDeviceInfo()
  13. }
  14. }
  15. Оп­тимиза­ция эле­мен­тов UI. Чем мень­ше в UI исполь­зует­ся слож­ных вло­жен­ных друг в дру­га эле­мен­тов интерфей­са, тем быс­трее отра­баты­вает отри­сов­ка. Самый прос­той спо­соб сде­лать это — исполь­зовать Constraint Layout, спо­соб­ный заменить сло­еный пирог дру­гих лей­аутов. При этом самым быс­трым в отри­сов­ке счи­тает­ся FrameLayout. Имен­но его сле­дует исполь­зовать в качес­тве кон­тей­нера.
  16. Уда­ление Firebase. Firebase — отличный инс­тру­мент ана­лити­ки, но на его ини­циали­зацию ухо­дит слиш­ком мно­го вре­мени. Если уда­лить Firebase и заменить его более лег­ким решени­ем, то мож­но выиг­рать нес­коль­ко десят­ков мил­лисекунд.
  17. Из­бавле­ние от Joda Time. Joda Time — прек­расная биб­лиоте­ка для работы со вре­менем, но в ней исполь­зуют­ся край­не неэф­фектив­ные решения. Луч­ше либо заменить эту биб­лиоте­ку java.time, либо отло­жить ее ини­циали­зацию на как мож­но более поз­днее вре­мя.
  18. Ле­нивая ини­циали­зация content provider’ов. Ини­циали­зация про­вай­деров кон­тента (а сре­ди них есть и WorkManager, и тот же Firebase) про­исхо­дит в основном потоке до запус­ка самого при­ложе­ния. Исполь­зуя биб­лиоте­ку Jetpack AndroidX App Startup, ини­циали­зацию content provider’ов мож­но отло­жить и сэконо­мить еще нес­коль­ко десят­ков мил­лисекунд.
  19. От­каз от 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"

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

Все выг­лядит поч­ти оди­нако­вым, но есть раз­личия:

  1. В отли­чие от род­ных методов, рас­ширения нуж­но импорти­ровать отдель­но.
  2. Рас­ширения не вир­туаль­ные, то есть их нель­зя пере­опре­делить в клас­сах‑нас­ледни­ках.
  3. Фун­кции‑рас­ширения работа­ют с типом, а не с клас­сом.
  4. Фор­маль­но фун­кции‑рас­ширения не счи­тают­ся частью клас­са.

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

Вто­рое важ­ное раз­личие: фун­кции‑рас­ширения добав­ляют­ся к типу, а не к клас­су. Это поз­воля­ет писать, нап­ример, такие фун­кции‑рас­ширения:

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-кода не про­исхо­дит авто­мати­чес­кая раз­бло­киров­ка экра­на — ее сле­дует рас­коммен­тировать:

  1. Пе­рехо­дим в Android Studio и откры­ваем парамет­ры сбор­ки (Edit configuration...).
  2. В открыв­шемся окне нажима­ем кноп­ку + и выбира­ем Shell Script.
  3. Ука­зыва­ем в пер­вом поле вво­да путь к скрип­ту.
  4. Воз­вра­щаем­ся обратно в кон­фигура­цию при­ложе­ния и в раз­деле Before launch нажима­ем + и выбира­ем Run Another Configuration, выбира­ем наш скрипт.

В ори­гиналь­ной статье есть кар­тинки, пояс­няющие эти шаги.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei



Report Page