September 19, 2018

Аутентификация с помощью отпечатков пальцев в Android

Мой канал: @VolfsChannel

Биометрия... Ваши отпечатки пальцев в смартфонах... Ещё 5-7 лет назад данный вид аутентификации казался баловством. Я что, храню коды запуска ракет? Зачем мне этот сканер? Да графический ключ удобнее и быстрее! Знакомые фразы? Может быть вы и сами так думали, но сейчас глупо отрицать - такого рода биометрическая аутентификация является лидирующей на портативных устройствах. Сказывается дешевизна ($3-5 за модуль сканера) и относительная безопасность.

Сканеры отпечатков пальцев в смартфонах стали появляться ещё во времена Android 4.4. Только тогда, из-за отсутствия API, данный сканер мог работать только в приложениях от производителя, и был скорее забавной игрушкой, чем методом авторизации. Всё изменилось с релизом Android 6.0. Было представлено Fingerprint Manager API, с помощью которого разработчики могут реализовать авторизацию по фингерпринту.

Почитав пару статей на Хабре, я так ничего и не понял. Что? Как? Зачем? Путаные инструкции и нуль конкретики.

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

Подготовка

Для использования сканера отпечатков ваше приложение должно удовлетворять следующим требованиям:

  1. Целевая версия SDK 23 или выше
  2. Разрешение <uses-permission android:name="android.permission.USE_FINGERPRINT" /> в манифесте

Реализация

Желательно завести объект-наследник класса android.app.Application, чтобы без труда получить контекст.

import android.app.Application;

public class Girl extends Application {
	private static Girl instance;
	
	public Girl(){
		instance = this;
		}

	public static Girl get() {
			return instance;
		}

	@Override
	public void onCreate() {
			// TODO: Implement this method
			super.onCreate();
		}
	
	
}

Зарегистрируйте класс в файле AndroidManifest.xml

<application
    android:name=".Girl"
    ...
    ...
    ...>
...
...
...
</application>

Создайте класс FingerprintHelper.java, который и будет отвечать за работу со сканером:

import android.annotation.TargetApi;
import android.app.KeyguardManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import android.os.CancellationSignal;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyPermanentlyInvalidatedException;
import android.security.keystore.KeyProperties;

import java.security.KeyStore;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;

@TargetApi(Build.VERSION_CODES.M)
public abstract class FingerprintHelper {

  private FingerprintManager manager;
  private Cipher cipher;
  private CancellationSignal cancel;
	
	private String keystoreTag = "GirlTag";
	
	// Проверяем наличие сканера
  public static boolean canUseFingerprint() {
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
      return false;
    Girl girl = Girl.get();
    KeyguardManager km = girl.getSystemService(KeyguardManager.class);
    FingerprintManager fm = girl.getSystemService(FingerprintManager.class);
    return km.isKeyguardSecure() && fm != null && fm.isHardwareDetected() && fm.hasEnrolledFingerprints();
  }
	
	// Мы не можем просто так взять и использовать отпечаток без пароля
	// Нам необходимо создать хранилище ключей, по которому будет
	// будет производиться авторизация
  protected FingerprintHelper() throws Exception {
		// Получение контекста
		Girl girl = Girl.get();
		// Создание экземпляра хранилища ключей
    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
		// Стучимся к сервису распознавания отпечатков
    manager = girl.getSystemService(FingerprintManager.class);
		// Создание экземпляра шифровальщика
    cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
        + KeyProperties.BLOCK_MODE_CBC + "/"
        + KeyProperties.ENCRYPTION_PADDING_PKCS7);
		// Загрузка хранилища ключей
    keyStore.load(null);
		// Получаем секретный ключ из загруженного хранилища
		// если отсутствует - создаём
    SecretKey key = (SecretKey) keyStore.getKey(keystoreTag, null);
    if (key == null) {
      key = generateKey();
    }
		// Расшифровка
    try {
      cipher.init(Cipher.ENCRYPT_MODE, key);
    } catch (KeyPermanentlyInvalidatedException e) {
      // Фикс бага Android Marshmallow
      key = generateKey();
      cipher.init(Cipher.ENCRYPT_MODE, key);
    }
  }
	
	// Если в процессе авторизации произошла ошибка
  public abstract void onAuthenticationError(int errorCode, CharSequence errString);
	
	// Получение статуса авторизации
  public abstract void onAuthenticationHelp(int helpCode, CharSequence helpString);
	
	// Успешная авторизация
  public abstract void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result);
	// Ошибка
  public abstract void onAuthenticationFailed();
	
	// Запуск авторизации
  public void startAuth() {
    cancel = new CancellationSignal();
    FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
    manager.authenticate(cryptoObject, cancel, 0, new FingerprintManager.AuthenticationCallback() {
      @Override
      public void onAuthenticationError(int errorCode, CharSequence errString) {
        FingerprintHelper.this.onAuthenticationError(errorCode, errString);
      }

      @Override
      public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
        FingerprintHelper.this.onAuthenticationHelp(helpCode, helpString);
      }

      @Override
      public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
        FingerprintHelper.this.onAuthenticationSucceeded(result);
      }

      @Override
      public void onAuthenticationFailed() {
        FingerprintHelper.this.onAuthenticationFailed();
      }
    }, null);
  }
	
	// Если пользователь отменил операцию
  public void cancel() {
    if (cancel != null)
      cancel.cancel();
  }
	
	// Создание ключа авторизации
  private SecretKey generateKey() throws Exception {
    KeyGenerator keygen = KeyGenerator
        .getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
    KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(
        keystoreTag,
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
        .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
        .setUserAuthenticationRequired(true)
        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
      builder.setInvalidatedByBiometricEnrollment(false);
    }
    keygen.init(builder.build());
    return keygen.generateKey();
  }
}

Я добавил комментарии к коду, так что вопросов возникнуть не должно.

Пример реализации в Activity:

private FingerprintHelper fingerprint;
private TextView warning;

private void init() {
				if (FingerprintHelper.canUseFingerprint()) {
						try {
								fingerprint = new FingerprintHelper() {
										@Override
										public void onAuthenticationError(int errorCode, CharSequence errString) {
												warning.setText(errString);
											}

										@Override
										public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
												warning.setText(helpString);
											}

										@Override
										public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
												warning.setText("Success!");
											}

										@Override
										public void onAuthenticationFailed() {
												warning.setText("Failed!");
											}
									};
								fingerprint.startAuth();
							} catch (Exception e) {
								e.printStackTrace();

							}
					}
			}

		@Override
		public void finish() {

				if (fingerprint != null)
					fingerprint.cancel();
				super.finish();
			}

А наглядно?

Конечно. Пример приложения и его исходный код вы найдете на канале.


@VolfsChannel