Log.d("Kotlin", "Авторизация по биометрии")

Hi ;-)

Поскольку это моя первая статья на ресурсе, то хотелось бы вообще изначально познакомиться. Возможно, вам это будет неинтересно сейчас читать, поэтому можете спокойно пролистать ниже, где уже будет разбор этой темы. Вся статья будет написана в таком свободном стиле, чтобы не нагружать вас какой-либо терминологией и скукотой, поэтому тут спокойно могут быть ответвление (вдруг анекдот захотелось рассказать), но в общем и целом — не суть. Постараюсь как можно более кратко и понятно все описать, а если будут вопросы, то смело можете писать их тут ниже. Ну... Let's Go.

Кто ты такой?

Я молодой такой разработчик с 2-х летним not official стажем разработки в разных областях, начиная от чистого C, заканчивая видеоигровой (Unity) и мобильной (Flutter, Kotlin) разработкой. Жизнь немного помотала, поэтому и рассматриваю несколько профилей.

На моей практике было не так уж и много (но лично для меня) достойных проектов: сервис по доставке еды «Food in Flight», 1.5-часовая сюжетная около-RPG игра «Game Of Pulpits» и мини-игра в угадай слово, по типу вордли (бесила реклама, вот и решил что-то своё бесплатное сделать).

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

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

Ну, вроде и так много мыла, поэтому стартуем.

Постановка задачи

Возможно, вы знаете, что есть такая штука, как «менеджер паролей», где хранятся абсолютно все ваши логины, пароли и сайты, к которым они прикреплены. Удобная штука, чтобы не запоминать пароли и уж тем более не писать их вручную. Кому такое надо common. Поэтому многие и пользуются такими штуками по типу KeePass. А теперь смоделируем ситуацию:

Вы решили сходить в магазин и купить какой‑нибудь чешский нефильтрованный напиток, посмотрели на время и увидели, что сейчас 21:45, и совсем скоро магазины закроются. Поэтому вам пришла гениальная идея — побежать, пока магазин не закрылся, но по пути вы уронили телефон и даже этого не заметили. Когда вы возвращались домой с напитком, вы обнаружили, что вашего телефона нет, а каким‑то чудом вы пообещали себе 2 дня назад, что поставите пароль на телефон «на следующий день», что, конечно же, не сделали. Через пару минут вы заходите через ноутбук или компьютер, нажимаете на кнопку «выйти со всех устройств» и думаете, что всё — никаких проблем не будет, но тут вы заходите в вашу любимую социальную сеть и видите, что вашим друзьям или коллегам было отправлено сообщение о займе 1000 рублей. В этот момент вы понимаете, что у вас был включен тот самый менеджер паролей, в который мог зайти абсолютно любой, получив ваш телефон в руки.

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

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

Подготовка нашего проекта

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

Зависимость: biometric
Разрешение: запись данных

Как установить зависимость?
Заходим в файл build.gradle, который располагается в проекте, например, так: NameApp/app/build.gradle и внизу в блок "dependency" (зависимость) вставляем вот эту строку:

implementation 'androidx.biometric:biometric:1.1.0'

1.1.0 — последняя версия на 17.07.2023 устанавливаемого пакета, если вдруг выйдет новая, то её можно посмотреть вот тут: *клик*

Как установить разрешение?
Заходим в файл AndroidManifest.xml, который располагается в проекте, например, так: NameApp/app/src/main/AndroidManifest.xml и перед блоком <application ... /> вставляем эту строку:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Всё. Поздравляю, теперь можем начинать. Два файла — две строки, и мы уже можем создавать наш вход по биометрии! Magic.

Создание входа по биометрии

Рисунок 1 — Основные экраны
Рисунок 1 — Основные экраны

На рисунке 1 продемонстрированы 3 основных экрана: регистрация, авторизация и ваше основное окошко.

Мы не будем подробно разбирать прямо сейчас реализацию механик «ввод пароля», «проверка пароля» и тому подобного. Мы разберём одну лишь кнопку «Log in using the biometric»

Создадим в нашем активити две приватные переменные:

  private var isHaveBiometric: Boolean = true
  private lateinit var biometricPrompt: BiometricPrompt

isHaveBiometric — булевая переменная, которая будет отвечать у нас за то, есть ли у пользователя на устройстве поддержка биометрии
biometricPrompt — объект, который представляет собой диалоговое окно системной аутентификации по биометрии

Возможны, вы спросите: «А зачем нам нужна эта булевая переменная isHaveBiometric?». Ответ прост: не у всех пользователей есть эта самая система авторизации по биометрии, поэтому было бы странно, что пользователь будет видеть кнопку «Вход по биометрии», когда его устройство это не поддерживает. Поэтому создадим метод checkBiometricInDevice и вызовем его при запуске текущего экрана.

private fun checkBiometricInDevice() {
        val biometricManager = BiometricManager.from(this)
        val buttonOpenBiometric = findViewById<Button>(R.id.buttonOpenCamera)

        when (biometricManager.canAuthenticate()) {
            BiometricManager.BIOMETRIC_SUCCESS -> {
                buttonOpenBiometric.visibility = View.VISIBLE
                isHaveBiometric = true
            }

            else -> {
                buttonOpenBiometric.visibility = View.GONE
                isHaveBiometric = false
            }
        }
    }

2 строка — создадим объект, который будем использовать для проверки возможности аутентификации пользователя с помощью биометрии.
3 строка — наша кнопка, при нажатии на которую пользователь сможет войти с помощью биометрии.
5-15 строки — в зависимости от результата проверки он устанавливает видимость кнопки и значение булевой переменной.

Поздравляю, половина дела сделана. Да-да, всё настолько просто. Уже полпути позади.

Теперь сделаем так, чтобы при нажатии на кнопку «Вход по биометрии» появлялось окно авторизации. Для этого создадим метод initializeButtonForBiometric()

private fun initializeButtonForBiometric() {
       val buttonBiometric = findViewById<Button>(R.id.buttonOpenBiometric)

        biometricPrompt = BiometricPrompt(this@MainActivity, ContextCompat.getMainExecutor(this), object:androidx.biometric.BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                val cryptoObject = result.cryptoObject
                SetActivity(SecondActivity::class.java, true)
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                Toast.makeText(this@MainActivity, "Error", Toast.LENGTH_SHORT).show()
            }

            override fun onAuthenticationFailed() {
                Toast.makeText(this@MainActivity, "Error", Toast.LENGTH_SHORT).show()
            }
        }
        )

        buttonBiometric.setOnClickListener {
            biometricPrompt.authenticate(createBiometricPromptInfo())
        }
    }

2 строка — наша кнопка, при нажатии на которую вся эта штука сработает
4 строка — создание объекта, где в конструктор передаются ссылка на текущую активность, исполнитель ContextCompat.getMainExecutor(this) и анонимный объект AuthenticationCallback.

Нам необходимо переопределить 3 метода:

  1. onAuthenticationSucceeded — для успешного входа;

  2. onAuthenticationError — для обработки ошибки при входе по биометрии (например, когда несколько раз не считывается отпечаток пальца);

  3. onAuthenticationFailed — для обработки ошибки, когда вход по биометрии не удался.

Для 2 и 3 метода вы можете прописать такую логику, которую захотите. Я написал просто вывод одного сообщения «Error». Креативно? А то.

Для 1 метода идёт реализация логики при успешном вводе биометрических данных, в нашем случае — переход к следующему экрану.

На 20-22 строках реализована обработка нажатия по кнопке «вход», где будет открываться как раз наше окно. Дополнительно я создал метод createBiometricPromptInfo. Вот код этой самой функции:

private fun createBiometricPromptInfo(): PromptInfo {
        return PromptInfo.Builder()
            .setTitle("Authorization")
            .setNegativeButtonText("Cancel")
            .build()
    }

Надеюсь, тут ничего пояснять не надо, поскольку тут типичный инглиш.

  • Название окна — Авторизация (Authorization)

  • Кнопка выхода из авторизации по биометрии — Отмена (Cancel)

Готово. Теперь нужно всё это использовать в нашем методе "OnCreate"

override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        checkBiometricInDevice() // Тут проверяем есть ли вход по биометрии у пользователя
        initializeButtons() // Тут инициализированы кнопки "Log in" и "Sign in"

        if (isHaveBiometric) { // Тут проверка 
            initializeButtonForBiometric() // тут инициализируем кнопку входа по биометрии
        }
    }

Вот и все, теперь пользователь может без ввода пароля спокойно зайти в приложение.

Рисунок 2 — Вход
Рисунок 2 — Вход

Всё представленное ниже приложение вы можете найти вот тут: RskullW/AuthenticatorMobile (github.com)

Спасибо, что прочитали. Может быть, для кого-то эта статья стала полезной. Пишите в комментариях механики, которые хотели бы разобрать. И это может быть не только Kotlin, но и C++, Dart/Flutter, C#, Unity.

Всем кчау ;-)

Комментарии (6)


  1. quaer
    19.07.2023 14:46

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

    А кому доступна биометрия, которая хранится на устройстве?

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


    1. RskullW Автор
      19.07.2023 14:46

      А кому доступна биометрия, которая хранится на устройстве?

      Как вы можете заметить по коду, что биометрия доступна только пользователю. Никуда это не передается.

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

      "Скопировать пароль в буфер обмена" на самом деле есть, как и на компе, так и в мобильной версии. Безопасно это? Нет. Удобно? Да. Тут уже на ваш вкус, как разработчик - вы можете это реализовать, а как пользователь - это ваше дело: копировать или нет. Так что проблем тут никаких нет)


      1. quaer
        19.07.2023 14:46

        Вот что написано в документации Google.

        Biometrics offer a more convenient, but potentially less secure way of confirming your identity with a device.


  1. s097t0r1
    19.07.2023 14:46

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


    1. RskullW Автор
      19.07.2023 14:46

      Тут уже на свой вкус и цвет, но идея топ. Обычно это кстати и используют в менеджерах, где все данные хранятся в зашифрованном файле, который можно открыть с помощью ключ-файла. Поэтому можно даже спокойно сделать: вход по биометрии -> ключ-файл -> пароль. И каждый из этих пунктов - функционален для пользователя, то есть насколько сам юзер хочет защитить свой пользователь. А как разработчики - мы должны обеспечить юзера всей возможной защитой, для нашего продукта, чтобы пользователь хотел его скачать. Спасибо за идею, может быть, в следующих статьях опишем этот процесс ;-)


  1. iamkatrechko
    19.07.2023 14:46

    По поводу смоделированной ситуации, есть пара моментов:

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

    • Если вы сумели включить в приложении вход по отпечатку пальцев, значит на устройстве уже стоит блокировка паролем. А если стоит блокировка паролем, то и телефон потерять не так страшно. Тем более, что в последних версиях Android пароль устройства по-умолчанию используется также и для шифрования данных. А если вы вошли в свою учётку Google, то и сброс не всегда поможет воспользоваться устройством, т.к. при первом запуске система будет требовать войти в ранее привязанный аккаунт. Это всё при умении в какой-то степени можно и обойти, но вот пароли из вашего приложения уже вряд-ли удастся получить

    В общем, ситуация не вполне реалистичная, как минимум на большинстве устройств, но звучит она красиво :)

    А если серьезно, то всё же стоит хотя бы упомянуть, что данный способ реализации входа по отпечатку не является безопасным, т.к. легко обходится с помощью той же Frida. Другое дело, если вы реализуете подход с шифрованием через полученный от системы Cipher (как ранее писал s097t0r1)
    Описывать данный способ необязательно, но оставить дисклеймер все же стоит, т.к. иначе в сети будет появляться всё больше "небезопасных" приложений. А мы, как пользователи, будем ими пользоваться

    P.S. На тему "правильного" шифрования по отпечатку есть отличное объяснение в докладе с Mobius: Дмитрий Терёшин — Как два пальца: Локальные атаки на мобильные приложения