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

Привіт, Хабре! Пройшло досить багато часу, як з'явився 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.
Що нам потрібно для шифрування та розшифрування:
- Захищене сховище для ключів
- Криптографічний ключ.
- Шифрувальник
Для роботи з відбитками система надає нам свій кейстор -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(), отримати пін-код, зімітувати його введення та показати користувачеві його дані.