У паролей был свой день, когда поставщики программного обеспечения и производители оборудования поощряли пользователей использовать другие методы защиты своих данных. Считыватели отпечатков пальцев становятся все более распространенными на устройствах Android, поскольку Marshmallow представил новый API-интерфейс Fingerprint, и в этом уроке я покажу вам, как реализовать их в своих приложениях.
Вы можете скачать полный проект с GitHub .
Начиная
Это пример приложения, ограниченного в пользовательском опыте, и будет сосредоточено на «как».
Для начала создайте новый проект и назовите его «FingerprintApi». Установите уровень API 15 в качестве минимального SDK и добавьте активность входа . Нажмите « Готово» , и Android Studio начнет создавать необходимые файлы.
LoginActivity
поможет начать работу быстрее, но изменения по-прежнему необходимы.
В файле activity_login.xml измените метку кнопки « Вход» или «Зарегистрироваться» на « Регистрация» и добавьте еще одну кнопку с надписью « Войти с помощью отпечатка пальца» . Ниже этого, добавьте TextView
именем noteTextView
. Отладочная информация будет напечатана здесь, вместо использования диалогов.
<Button style="?android:textAppearanceSmall" android:id="@+id/fingerprint_sign_in_button" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="16dp" android:text="Sign in using Fingerprint" android:textStyle="bold" /> <TextView android:layout_width="match_parent" android:layout_height="match_parent" android:id="@+id/noteTextView" android:layout_gravity="center_horizontal" android:layout_margin="20dp" android:textAlignment="center" />
В LoginActivity.java замените существующие переменные на приведенные ниже, которые вы будете использовать позже.
private static final int REQUEST_READ_CONTACTS = 0; private AutoCompleteTextView mEmailView; private EditText mPasswordView; private View mProgressView; private View mLoginFormView; private TextView mNoteTextView; private UserLoginTask mAuthTask; private static final String KEY_ALIAS = "sitepoint"; private static final String KEYSTORE = "AndroidKeyStore"; private static final String PREFERENCES_KEY_EMAIL = "email"; private static final String PREFERENCES_KEY_PASS = "pass"; private static final String PREFERENCES_KEY_IV = "iv"; private KeyStore keyStore; private KeyGenerator generator; private Cipher cipher; private FingerprintManager fingerprintManager; private KeyguardManager keyguardManager; private FingerprintManager.CryptoObject cryptoObject; private SharedPreferences sharedPreferences; private boolean encrypting;
Замените метод onCreate
на приведенный ниже.
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // Set up the login form. mEmailView = (AutoCompleteTextView) findViewById(R.id.email); populateAutoComplete(); mPasswordView = (EditText) findViewById(R.id.password); mPasswordView.setOnEditorActionListener(new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) { if (id == R.id.login || id == EditorInfo.IME_NULL) { //attemptRegister(); return true; } return false; } }); Button mRegisterButton = (Button) findViewById(R.id.email_sign_in_button); mRegisterButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { //attemptRegister(); } }); Button mFingerprintSignInButton = (Button) findViewById(R.id.fingerprint_sign_in_button); mFingerprintSignInButton.setOnClickListener(new OnClickListener() { @Override public void onClick(View view) { //attemptFingerprintLogin(); } }); mLoginFormView = findViewById(R.id.login_form); mProgressView = findViewById(R.id.login_progress); mNoteTextView = (TextView)findViewById(R.id.noteTextView); sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); }
Это переименовывает метод attemptLogin()
в attemptRegister()
, объявляет и инициализирует переменные для fingerprintSignInButton
и noteTextView
. инициализируйте sharedPreferences
, где вы сохраняете учетные данные пользователей, включая зашифрованный пароль.
Чтобы добавить текст в noteTextView
добавьте эти два метода. Текст передается как параметр и добавляется в верхней части TextView
.
public void print(String text) { mNoteTextView.setText(text + "\n" + mNoteTextView.getText()); } public void print(int id) { print(getString(id)); }
В классе UserLoginTask
удалите код внутри методов doInBackground
и onPostExecute
, оставив нижеприведенное.
@Override protected Boolean doInBackground(Void... params) { return true; } @Override protected void onPostExecute(final Boolean success) { onCancelled(); }
Для первого запуска приложению потребуется разрешение на доступ к контактам, но это не имеет значения. Он включен в реализацию LoginActivity
по умолчанию, которую вы можете удалить в этом случае.
Если заявления
Чтобы использовать Fingerprint API, вы должны запросить разрешение. Добавьте эту строку в AndroidManifest.xml в верхней части тега приложения.
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
Конечно, не у каждого устройства есть датчик отпечатка пальца или Android 6.0 и выше. Из-за этого вам следует проверить аппаратную / программную поддержку, прежде чем пытаться использовать методы API, которые могут вызвать исключение.
@SuppressLint("NewApi") private boolean testFingerPrintSettings() { print("Testing Fingerprint Settings"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { print("This Android version does not support fingerprint authentication."); return false; } keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE); fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { if (!keyguardManager.isKeyguardSecure()) { print("User hasn't enabled Lock Screen"); return false; } } if (ActivityCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED) { print("User hasn't granted permission to use Fingerprint"); return false; } if (!fingerprintManager.hasEnrolledFingerprints()) { print("User hasn't registered any fingerprints"); return false; } print("Fingerprint authentication is set.\n"); return true; }
Сначала проверяется минимальная версия Android M. Затем проверяется, что пользователь включил блокировку экрана с использованием шаблона, PIN-кода или пароля с помощью isKeyguardSecure()
.
Помимо включения блокировки экрана, пользователь должен зарегистрировать отпечаток пальца на устройстве, вы можете проверить это с hasEnrolledFingerprints
метода hasEnrolledFingerprints
, но он требует, чтобы разрешение на использование аутентификации по отпечатку пальца было явно проверено.
Если все проверки пройдены, метод возвращает true.
Еще одна вещь, чтобы проверить, пытается ли зарегистрированный пользователь войти в систему. Первое, что нужно посмотреть, это SharedPreferences.
private boolean usersRegistered() { if (sharedPreferences.getString(PREFERENCES_KEY_EMAIL, null) == null) { print("No user is registered"); return false; } return true; }
Хелпер Класс
Затем вам понадобится вспомогательный класс для обработки методов Fingerprint API. Щелкните правой кнопкой мыши текущий пакет, затем выберите « Создать» -> «Класс Java» . Назовите новый класс FingerprintHelper
и FingerprintHelper
чтобы он расширял FingerprintManager.AuthenticationCallback
. Класс должен быть нацелен на API, больший или равный Android M, сделайте это, добавив @TargetApi(Build.VERSION_CODES.M)
в начало класса:
@TargetApi(Build.VERSION_CODES.M) public class FingerprintHelper extends FingerprintManager.AuthenticationCallback { ... }
Объявите внутренний интерфейс с помощью следующих методов.
interface FingerprintHelperListener { public void authenticationFailed(String error); public void authenticationSucceeded(FingerprintManager.AuthenticationResult result); }
Все классы, которые будут использовать FingerprintHelper
должны реализовывать этот интерфейс для уведомления о состоянии аутентификации. Чтобы иметь возможность вызывать эти методы, вам нужен экземпляр вызывающего класса, взятый в качестве параметра в конструкторе.
private FingerprintHelperListener listener; public FingerprintHelper(FingerprintHelperListener listener) { this.listener = listener; }
Чтобы сканер отпечатков пальцев начал прослушивать ввод, вызовите метод FingerprintManager
authenticate
. Вы используете CancellationSignal
чтобы остановить прослушивание, когда пользователь отменяет аутентификацию, или когда приложение переходит в фоновый режим и т. Д.
Этот метод позволяет другим классам отменить вызов:
private CancellationSignal cancellationSignal; public void startAuth(FingerprintManager manager, FingerprintManager.CryptoObject cryptoObject) { cancellationSignal = new CancellationSignal(); try { manager.authenticate(cryptoObject, cancellationSignal, 0, this, null); } catch (SecurityException ex) { listener.authenticationFailed("An error occurred:\n" + ex.getMessage()); } catch (Exception ex) { listener.authenticationFailed("An error occurred\n" + ex.getMessage()); } } public void cancel() { if (cancellationSignal != null) cancellationSignal.cancel(); }
Метод authenticate
использует объект CryptoObject
, это класс-оболочка для объектов шифрования, поддерживаемых FingerprintManager
. Он содержит такие объекты, как Cipher и Signature, для этого урока я буду использовать Cipher для обработки шифрования и дешифрования.
Наконец, переопределите методы AuthenticationCallback
чтобы уведомить FingerprintHelperListener
.
@Override public void onAuthenticationError(int errMsgId, CharSequence errString) { listener.authenticationFailed("Authentication error\n" + errString); } @Override public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { listener.authenticationFailed("Authentication help\n" + helpString); } @Override public void onAuthenticationFailed() { listener.authenticationFailed("Authentication failed."); } @Override public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { listener.authenticationSucceeded(result); }
Я упомяну эти методы, когда буду говорить о тестовых случаях, но пока вспомогательный класс готов к использованию.
Проверка подлинности
Перед сохранением учетных данных, есть еще пара шагов. Вернуться к LoginActivity
, добавить FingerprintHelper.FingerprintHelperListener
после implements LoaderCallbacks<Cursor>
и реализовать два метода интерфейса.
@Override public void authenticationFailed(String error) { print(error); } @TargetApi(Build.VERSION_CODES.M) @Override public void authenticationSucceeded(FingerprintManager.AuthenticationResult result) { print("Authentication succeeded!"); }
TargetApi
указывает минимальную версию Android для методов, которые будут вызываться внутри authenticationSucceeded
. Раскомментируйте вызовы attemptRegister()
внутри onCreate
и добавьте эти строки в этот метод.
if (!testFingerPrintSettings()) return;
Объявите переменную типа FingerprintHelper
в переменных класса.
private FingerprintHelper fingerprintHelper;
Переопределите метод onPause
чтобы остановить задачи, если по какой-либо причине действие входит в это условие.
@Override protected void onPause() { super.onPause(); if (fingerprintHelper != null) fingerprintHelper.cancel(); if (mAuthTask != null) mAuthTask.cancel(true); }
Обновите конструктор UserLoginTask
и добавьте еще одну переменную следующим образом.
private final Boolean mRegister; // if false, authenticate instead UserLoginTask(String email, String password) { mEmail = email; mPassword = password; mRegister = true; fingerprintHelper = new FingerprintHelper(LoginActivity.this); }
В дополнение к инициализации объекта fingerprintHelper
используйте логическое значение, чтобы указать, используется ли API-интерфейс Fingerprint для регистрации или аутентификации.
Обновите метод onPostExecute
, чтобы он запрашивал аутентификацию у пользователя.
@Override protected void onPostExecute(final Boolean success) { onCancelled(); if (!success) { if (mRegister) print(R.string.register_fail); else print(R.string.fingerprint_authenticate_fail); } else { fingerprintHelper.startAuth(LoginActivity.this.fingerprintManager, cryptoObject); print("Authenticate using fingerprint!"); } }
Добавьте эти ресурсы в файл string.xml .
<string name="fingerprint_authenticate_success">Successfully authenticated!</string> <string name="register_success">Successfully registered!</string> <string name="fingerprint_authenticate_fail">Authentication failed!</string> <string name="register_fail">Registration failed!</string>
На этом этапе аутентификация по отпечатку пальца готова для попытки. Запустите приложение, введите учетные данные и нажмите «Зарегистрироваться». Приложение отобразит «Аутентификация с использованием отпечатка пальца!», Если на вашем устройстве или эмуляторе все установлено. Чтобы смоделировать отпечаток пальца в эмуляторе Android, откройте Расширенные инструменты , нажав « Дополнительно» на правой панели, затем выберите « Отпечаток пальца» в меню. Окно будет следующим.
При успешной аутентификации вид будет следующим.
Теперь давайте сохраним эти учетные данные.
Сохранение учетных данных
Я буду doInBackground
метод doInBackground
новые функции. Если какой-либо из этих вызовов завершается неудачно, процесс прерывается.
if (!getKeyStore()) return false; private boolean getKeyStore() { print("Getting keystore..."); try { keyStore = KeyStore.getInstance(KEYSTORE); keyStore.load(null); // Create empty keystore return true; } catch (KeyStoreException e) { print(e.getMessage()); } catch (CertificateException e) { print(e.getMessage()); } catch (NoSuchAlgorithmException e) { print(e.getMessage()); } catch (IOException e) { print(e.getMessage()); } return false; }
KeyStore — это хранилище ключей и сертификатов в виде базы данных. Каждая запись идентифицируется строкой под названием «псевдоним», но для доступа к ней сначала необходимо загрузить определенное хранилище ключей. Я назвал мой AndroidKeyStore.
if (!createNewKey(false)) return false; @TargetApi(Build.VERSION_CODES.M) public boolean createNewKey(boolean forceCreate) { print("Creating new key..."); try { if (forceCreate) keyStore.deleteEntry(KEY_ALIAS); if (!keyStore.containsAlias(KEY_ALIAS)) { generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE); generator.init(new KeyGenParameterSpec.Builder (KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) .setUserAuthenticationRequired(true) .build() ); generator.generateKey(); print("Key created."); } else print("Key exists."); return true; } catch (Exception e) { print(e.getMessage()); } return false; }
Поскольку хранилище ключей было только что создано, оно не содержит псевдонимов. Этот метод создает один в случае, если он еще не существует. KeyGenerator
генерирует новый ключ с заданными свойствами. При инициализации .setUserAuthenticationRequired(true)
метод .setUserAuthenticationRequired(true)
, который делает ключ доступным, если пользователь был идентифицирован.
if (!getCipher()) return false; private boolean getCipher() { print("Getting cipher..."); try { cipher = Cipher.getInstance( KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); return true; } catch (NoSuchAlgorithmException e) { print(e.getMessage()); } catch (NoSuchPaddingException e) { print(e.getMessage()); } return false; }
Шифр отвечает за шифрование и дешифрование, поэтому ожидает, что алгоритм будет использоваться в качестве параметра.
if (mRegister) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(PREFERENCES_KEY_EMAIL, mEmail); editor.commit(); encrypting = true; if (!initCipher(Cipher.ENCRYPT_MODE)) return false; } @TargetApi(Build.VERSION_CODES.M) private boolean initCipher(int mode) { print("Initializing cipher..."); try { keyStore.load(null); SecretKey keyspec = (SecretKey)keyStore.getKey(KEY_ALIAS, null); if (mode == Cipher.ENCRYPT_MODE) { cipher.init(mode, keyspec); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(PREFERENCES_KEY_IV, Base64.encodeToString(cipher.getIV(), Base64.NO_WRAP)); editor.commit(); } return true; } catch (KeyPermanentlyInvalidatedException e) { print(e.getMessage()); createNewKey(true); // Retry after clearing entry } catch (Exception e) { print(e.getMessage()); } return false; }
При регистрации вам необходимо сохранить письмо. Я использую sharedPreferences
, но в зависимости от реализации вы можете использовать базу данных. Затем установите для параметра шифрования значение true и инициализируйте шифр в режиме шифрования.
Шифр примет сгенерированный SecretKey
в качестве параметра при инициализации. В sharedPreferences
сохраните Вектор sharedPreferences
(IV) из Шифра. IV — соль для шифрования пароля. Он генерируется случайным образом каждый раз, когда новый пароль зашифрован, поэтому его можно безопасно сохранить в виде строки в кодировке Base64.
KeyPermanentlyInvalidatedException
произойдет, когда ключ станет недействительным. Очистка записи KeyStore с этим псевдонимом решит проблему.
... if (!initCryptObject()) return false; return true; //end of doInBackground } @TargetApi(Build.VERSION_CODES.M) private boolean initCryptObject() { print("Initializing crypt object..."); try { cryptoObject = new FingerprintManager.CryptoObject(cipher); return true; } catch (Exception ex) { print(ex.getMessage()); } return false; }
Последний шаг — инициализация CryptoObject
из Cipher и инициализация всех объектов. Вы сохранили электронную почту и IV, остается только пароль.
Когда пользователь успешно аутентифицируется с использованием отпечатка пальца, зашифруйте пароль и сохраните его. Обновите метод authenticationSucceeded
из FingerprintHelperListener
.
@TargetApi(Build.VERSION_CODES.M) @Override public void authenticationSucceeded(FingerprintManager.AuthenticationResult result) { print("Authentication succeeded!"); cipher = result.getCryptoObject().getCipher(); if (encrypting) { String textToEncrypt = mPasswordView.getText().toString(); encryptString(textToEncrypt); print(R.string.register_success); mEmailView.setText(""); mPasswordView.setText(""); } } public void encryptString(String initialText) { print("Encrypting..."); try { byte[] bytes = cipher.doFinal(initialText.getBytes()); String encryptedText = Base64.encodeToString(bytes, Base64.NO_WRAP); SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(PREFERENCES_KEY_PASS, encryptedText); editor.commit(); } catch (Exception e) { print(e.getMessage()); } }
Возвращаясь назад, шифр теперь взят из CryptObject
. Внутри encryptString
метод doFinal
шифрует пароль с использованием шифра SecureKey
, а затем сохраняет пароль. Сообщения будут отображаться в следующем порядке.
Используя точку останова, вы можете увидеть, как выглядит encryptedText
.
Первая аутентификация
В следующий раз, когда вы запустите приложение, пользователь должен иметь возможность войти в систему, используя свой отпечаток пальца (потому что нет другого варианта входа). Процесс аутентификации учетных данных пользователя расшифровывает пароль. Я прошел через большинство методов, но есть небольшие изменения, которые делают разницу между шифрованием и дешифрованием с помощью Fingerprint API.
Внутри метода onCreate
раскомментируйте вызов attemptFingerprintLogin
и добавьте реализацию.
private void attemptFingerprintLogin() { if (!testFingerPrintSettings()) return; if (!usersRegistered()) return; showProgress(true); mAuthTask = new UserLoginTask(); mAuthTask.execute((Void) null); }
Здесь я использую методы, которые я упоминал, чтобы проверить настройки отпечатка пальца, и есть ли пользователь, чтобы войти. UserLoginTask
отметить, что конструктор UserLoginTask
отличается от конструктора, используемого для регистрации.
UserLoginTask() { mRegister = false; mEmail = null; mPassword = null; fingerprintHelper = new FingerprintHelper(LoginActivity.this); }
Внутри метода initCipher
добавьте регистр для случая, когда режим равен Cipher.DECRYPT_MODE
. После утверждения if
напишите следующее.
else { byte[] iv = Base64.decode(sharedPreferences.getString(PREFERENCES_KEY_IV, ""), Base64.NO_WRAP); IvParameterSpec ivspec = new IvParameterSpec(iv); cipher.init(mode, keyspec, ivspec); }
Сделайте то же самое для метода UserLoginTask
класса UserLoginTask
после закрывающей скобки для if(mRegister)
.
else { encrypting = false; if (!initCipher(Cipher.DECRYPT_MODE)) return false; }
Теперь расшифруйте пароль, если аутентификация прошла успешно. После оператора if
в методе authenticationSucceeded
вставьте следующее.
else { String encryptedText = sharedPreferences.getString(PREFERENCES_KEY_PASS, ""); decryptString(encryptedText); print(R.string.fingerprint_authenticate_success); }
Метод decryptString
использует шифр из возвращенного CryptObject
чтобы расшифровать пароль, а затем обновляет поля пользовательскими данными, чтобы представить успешный вход в систему.
public void decryptString(String cipherText) { print("Decrypting..."); try { byte[] bytes = Base64.decode(cipherText, Base64.NO_WRAP); String finalText = new String(cipher.doFinal(bytes)); mPasswordView.setText(finalText); mEmailView.setText(sharedPreferences.getString(PREFERENCES_KEY_EMAIL, "")); } catch (Exception e) { print(e.getMessage()); } }
Результат будет выглядеть следующим образом.
Используя точку останова, вы можете увидеть расшифрованный пароль.
Тестовые случаи
При использовании Fingerprint API многое может пойти не так. Я попытался составить список различных тестовых случаев, чтобы принять во внимание.
- Ни один пользователь не зарегистрирован, и пользователь пытается войти.
- Пользователь пытается зарегистрироваться, и его адрес электронной почты или пароль недействительны.
- Блокировка экрана не настроена.
- Блокировка экрана установлена на Swipe.
- Блокировка экрана установлена на PIN-код / пароль / шаблон, без настройки отпечатка пальца.
- Пользователь добавляет отпечаток после регистрации.
- Пользователь удаляет все отпечатки пальцев.
- Пользователь удаляет все отпечатки пальцев, затем добавляет новый (
KeyPermanentlyInvalidatedException
). - Устройство перезагружается (я заметил, что в эмуляторе ключ недействителен при перезагрузке, как если бы все отпечатки были удалены).
- Пользователь пытается
onAuthenticationFailed
аутентификацию с незарегистрированным отпечатком (в этом случаеonAuthenticationFailed
). - Пользователь пытается несколько раз аутентифицироваться с незарегистрированным отпечатком (в этом случае
onAuthenticationError
).
Защитите свои приложения
Хотя реализация Fingerprint API может потребовать усилий, для пользователей с совместимым устройством это значительно улучшит взаимодействие с пользователем.
Что вы думаете о считывателях отпечатков пальцев? Они улучшают приложение?