Статьи

Обеспечение безопасности ваших приложений для Android с помощью Fingerprint API

У паролей был свой день, когда поставщики программного обеспечения и производители оборудования поощряли пользователей использовать другие методы защиты своих данных. Считыватели отпечатков пальцев становятся все более распространенными на устройствах Android, поскольку Marshmallow представил новый API-интерфейс Fingerprint, и в этом уроке я покажу вам, как реализовать их в своих приложениях.

Вы можете скачать полный проект с GitHub .

Начиная

Это пример приложения, ограниченного в пользовательском опыте, и будет сосредоточено на «как».

Для начала создайте новый проект и назовите его «FingerprintApi». Установите уровень API 15 в качестве минимального SDK и добавьте активность входа . Нажмите « Готово» , и Android Studio начнет создавать необходимые файлы.

Новый проект GIF

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 может потребовать усилий, для пользователей с совместимым устройством это значительно улучшит взаимодействие с пользователем.

Что вы думаете о считывателях отпечатков пальцев? Они улучшают приложение?