Android Fingerprint API додаємо авторизацію за відбитком

fingerprint

Привіт, Хабре! Пройшло досить багато часу, як з'явився Fingerprint API для Android, в мережі багато розрізнених семплів коду з його впровадження та використання, але на Хабре з якоїсь причини цю тему оминали. На мою думку, настав час виправити це непорозуміння. Всіх, хто зацікавився, прошу під кат.

Найкоротший лікнеп

Отже, що ж є Fingerprint API? API дозволяє користувачеві автентифікуватись за допомогою свого відбитка, очевидно. Для роботи з сенсором API пропонує намFingerprintManager, досить простий у освоєнні.

Де детектор?

Щоб почати отримувати прибуток від нового API, насамперед потрібно додатиpermissionв маніфесті:

Само собою, використовувати Fingerprint API можна тільки на пристроях, що його підтримують: відповідно, це пристрої Android 6 із сенсором. Сумісність можна легко перевірити за допомогою методу:

FingerprintManagerCompat— це зручна обгортка для звичайногоFingerprintManager'а, яка спрощує перевірку пристрою на сумісність, інкапсулюючи в собі перевірку версії API. В даному випадкуisHardwareDetected()повернеfalse, якщо API нижче 23.

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

І скористаємося методом:

Код досить очевидний. Невелике нерозуміння може викликати момент, коли ми перевіряємо заблокований пристрій. Нам потрібна ця перевірка, оскільки хоч Android і не дозволяє додавати відбитки в незахищений пристрій, деякі виробники це оминають, тому підстрахуватися не завадить. Різні стани можна використовувати для того, щоб дати користувачеві зрозуміти, що відбувається і направитийого на правдивий шлях.

Підготовка

Отже, не зациклюючись на перевірці пін-коду на валідність, прикинемо таку спрощену логіку дій:

  • Користувач вводить пін-код, якщоSensorState.READY, ми зберігаємо пін-код, запускаємоMainActivity.
  • Рестартим додаток, якщоSensorState.READY, то зчитуємо відбиток, дістаємо пін-код, імітуємо його введення, запускаємоMainActivity.
Схема була б досить простою, якби не одне але: Google наполегливо рекомендує не зберігати приватні дані користувача у відкритому вигляді. Тому нам потрібен механізм шифрування та розшифрування для, відповідно, збереження та використання. Займемося цим.

Що нам потрібно для шифрування та розшифрування:

  1. Захищене сховище для ключів
  2. Криптографічний ключ.
  3. Шифрувальник

Для роботи з відбитками система надає нам свій кейстор -AndroidKeyStoreі гарантує захист від несанкціонованого доступу. Скористаємося ним:

Слід прийняти, зрозуміти і пробачити, щокейстор зберігає лише криптографічні ключі. Паролі, пін та інші приватні дані там зберігати не можна.

На вибір у нас два варіанти ключів: симетричний ключ та пара з публічного та приватного ключа. З міркувань UX ми скористаємося парою. Це дозволить нам відокремити відбиток від шифрування пін-коду.

Ключі ми діставатимемо з кейстора, але спочатку потрібно їх туди покласти. Для створення ключа скористаємося генератором.

При ініціалізації ми вказуємо, який кейстор підуть згенеровані ключі й у якого алгоритму призначений цей ключ. Сама ж генерація відбувається так:

Тут слід звернути увагу на два місця:

Перевіряти наявність ключабудемо в такий спосіб:

Шифрування та дешифрування в Java займається об'єктCipher. Ініціалізуємо його:

Адова мішанина в аргументі - це рядок трансформації, який включає алгоритм, режим змішування і доповнення.

Після того, як ми отрималиCipher, потрібно підготувати його до роботи. При генерації ключа ми вказали, що будемо використовувати його тільки для шифрування та розшифрування. Відповідно,Cipherтеж буде для цих цілей:

деinitDecodeCipher()таinitEncodeCiper()наступні:

Неважко помітити, що зашифровуючийCipherдещо складніше ініціалізувати. Це косяк самого Гугла, суть якого полягає в тому, що публічний ключ вимагає підтвердження користувача. Ми обходимо цю вимогу за допомогою зліпка ключа (милиця, ага).

Момент ізKeyPermanentlyInvalidatedException— якщо з якоїсь причини ключ не можна використовувати, вистрілює цей виняток. Можливі причини – додати новий відбиток до існуючого, зміну або повне видалення блокування. Тоді ключ більше немає сенсу зберігати, і ми його видаляємо.

Метод, який збирає весь ланцюжок підготовки:

Шифрування та розшифровка

Опишемо метод, який зашифровує рядок аргумент:

В результаті ми отримуємоBase64-рядок, який можна спокійно зберігати в преференсах програми.

Для розшифровки використовуємо наступний метод:

Опа, на вхід він отримує не тільки зашифрований рядок, а й об'єктCipher. Звідки він там узявся, стане зрозуміло пізніше.

Не той палець

Для того щоб нарешті використовувати сенсор, потрібно скористатися методомFingerprintManagerCompat:

Хендлер та прапори нам зараз не потрібні, сигнал використовується, щоб скасувати режимзчитування відбитків (при згортанні програми, наприклад), коллбеки повертають результат конкретного зчитування, а ось над криптооб'єктом зупинимося докладніше.CryptoObjectу цьому випадку використовується як обгортка дляCipher'a. Щоб його отримати, використовуємо метод:

Як очевидно з коду, криптообъект створюється зрозшифровуєCipher. Якщо цейCipherпрямо зараз відправити в методdecode(), то вилетить виняток, який повідомляє про те, що ми намагаємося використовувати ключ без підтвердження. Строго кажучи, ми створюємо криптооб'єкт і відправляємо його на вхід доauthenticate()саме для отримання цього самого підтвердження. ЯкщоgetCryptoObject()повернувnull, це означає, що з ініціалізаціїChiper'а ставсяKeyPermanentlyInvalidatedException. Тут уже нічого не вдієш, крім як дати користувачеві знати, що вхід по відбитку недоступний і йому доведеться заново ввести пін-код.

Як я вже казав, результати зчитування сенсора ми отримуємо у методах колббека. Ось як вони виглядають:

У разі успішного розпізнавання ми отримуємоAuthenticationResult, з якого можемо дістати об'єктCipherз вже підтвердженим ключем:

Тепер можна з чистою совістю відправити його на вхід уdecode(), отримати пін-код, зімітувати його введення та показати користувачеві його дані.