📜 ⬆️ ⬇️

Android Fingerprint API: we assign fingerprint authentication

Hi, Habr! It took quite a lot of time, as the Fingerprint API for Android appeared, there are many separate samples of code for its implementation and use on the network, but for some reason this topic was avoided on Habré. In my opinion, it is time to correct this misunderstanding. All interested in asking under the cat.



The shortest educational program


So what is the Fingerprint API? The API allows the user to authenticate with his fingerprint, obviously . To work with the sensor, the API offers us a FingerprintManager , fairly simple to learn.

How to use it? But this is more interesting. Almost everywhere where password authentication is required, you can fasten fingerprint authentication. Imagine an application consisting of LoginActivity and MainActivity . When you start, we get to the login screen, enter the pin code, go to the data. But we want to replace the input by pin-code to the input by print. By the way, it will not be possible to completely replace, we can only save the user from manually entering the PIN code, substituting the previously saved PIN code (meaning the client-server application, in which you need to send the password to the server).
')
Let's get started

Where is the sensor?


To start getting profit from the new API, first of all you need to add permission in the manifest:

<uses-permission android:name="android.permission.USE_FINGERPRINT"/> 

Of course, you can use the Fingerprint API only on devices that support it: respectively, these are Android 6+ devices with a sensor.

Compatibility can be easily verified using the method:

 public static boolean checkFingerprintCompatibility(@NonNull Context context) { return FingerprintManagerCompat.from(context).isHardwareDetected(); } 

FingerprintManagerCompat is a handy wrapper for the regular FingerprintManager , which simplifies device compatibility testing by encapsulating an API version check. In this case, isHardwareDetected () will return false if the API is below 23.

Next, we need to understand whether the sensor is ready for use. To do this, we define the enum states:

 public enum SensorState { NOT_SUPPORTED, NOT_BLOCKED, //     ,    NO_FINGERPRINTS, //      READY } 

And use the method:

 public static SensorState checkSensorState(@NonNull Context context) { if (checkFingerprintCompatibility(context)) { KeyguardManager keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE); if (!keyguardManager.isKeyguardSecure()) { return SensorState.NOT_BLOCKED; } FingerprintManagerCompat fingerprintManager = FingerprintManagerCompat.from(context); if (!fingerprintManager.hasEnrolledFingerprints()) { return SensorState.NO_FINGERPRINTS; } return SensorState.READY; } else { return SensorState.NOT_SUPPORTED; } } 

The code is quite trivial. A small misunderstanding can cause a moment when we check whether the device is locked. We need this check, since, although Android does not allow adding fingerprints to an unprotected device, some manufacturers bypass this, so it will not hurt to be safe.

Different states can be used to make the user understand what is happening and direct him to the true path.

Training


So, without dwelling on checking the pin-code for validity, let's estimate the following simplified logic of actions:


The scheme would be quite simple if it were not for one thing: Google strongly recommends not to store private user data in the clear. Therefore, we need an encryption and decryption mechanism to store and use, respectively. Let's do it.

What we need to encrypt and decrypt:

  1. Secure storage for keys.
  2. Cryptographic key.
  3. Cryptographer

Storage


To work with prints, the system provides us with its keystory - “AndroidKeyStore” and guarantees protection against unauthorized access. We use it:

 private static KeyStore sKeyStore; private static boolean getKeyStore() { try { sKeyStore = KeyStore.getInstance("AndroidKeyStore"); sKeyStore.load(null); return true; } catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) { e.printStackTrace(); } return false; } 

It should be accepted, understood and forgiven that the keystor only stores cryptographic keys . Passwords, pins and other private data can not be stored there.

Key


We have two options to choose from: a symmetric key and a pair of public and private keys. For UX reasons, we will use a pair. This will allow us to separate fingerprint input from pin-code encryption.

We will get the keys from keystor, but first we need to put them there. To create a key, use the generator .

 private static KeyPairGenerator sKeyPairGenerator; private static boolean getKeyPairGenerator() { try { sKeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"); return true; } catch (NoSuchAlgorithmException | NoSuchProviderException e) { e.printStackTrace(); } return false; } 

During initialization, we specify in which keystars the generated keys go and for what algorithm this key is intended.

The generation itself is as follows:

 private static boolean generateNewKey() { if (getKeyPairGenerator()) { try { sKeyPairGenerator.initialize(new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP) .setUserAuthenticationRequired(true) .build()); sKeyPairGenerator.generateKeyPair(); return true; } catch (InvalidAlgorithmParameterException e) { e.printStackTrace(); } } return false; } 

Here you should pay attention to two places:


We will check the availability of the key as follows:

 private static boolean isKeyReady() { try { return sKeyStore.containsAlias(KEY_ALIAS) || generateNewKey(); } catch (KeyStoreException e) { e.printStackTrace(); } return false; } 

Cryptographer


Encryption and decryption in Java is handled by a Cipher object.

We initialize it:

 private static Cipher sCipher; private static boolean getCipher() { try { sCipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding"); return true; } catch (NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); } return false; } 

The hell hash in the argument is a transformation string that includes the algorithm , blend mode, and padding .

After we got Cipher , we need to prepare it for work. When generating the key, we indicated that we would use it only for encryption and decryption. Accordingly, Cipher will also be for these purposes:

 private static boolean initCipher(int mode) { try { sKeyStore.load(null); switch (mode) { case Cipher.ENCRYPT_MODE: initEncodeCipher(mode); break; case Cipher.DECRYPT_MODE: initDecodeCipher(mode); break; default: return false; //this cipher is only for encode\decode } return true; } catch (KeyPermanentlyInvalidatedException exception) { deleteInvalidKey(); } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | InvalidAlgorithmParameterException e) { e.printStackTrace(); } return false; } 

where initDecodeCipher () and initEncodeCiper () are as follows:

 private static void initDecodeCipher(int mode) throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException, InvalidKeyException { PrivateKey key = (PrivateKey) sKeyStore.getKey(KEY_ALIAS, null); sCipher.init(mode, key); } 

 private static void initEncodeCipher(int mode) throws KeyStoreException, InvalidKeySpecException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException { PublicKey key = sKeyStore.getCertificate(KEY_ALIAS).getPublicKey(); PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm()).generatePublic(new X509EncodedKeySpec(key.getEncoded())); OAEPParameterSpec spec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT); sCipher.init(mode, unrestricted, spec); } 

It is easy to see that the encrypting Cipher is somewhat more difficult to initialize. This is a cant of Google itself, the essence of which is that the public key requires user confirmation. We circumvent this requirement with a key cast ( crutch , yeah).

The moment with KeyPermanentlyInvalidatedException - if for some reason the key cannot be used, it will fire this exception. Possible reasons - adding a new fingerprint to an existing one, changing or completely removing a lock. Then the key no longer has to be stored, and we delete it.

 public static void deleteInvalidKey() { if (getKeyStore()) { try { sKeyStore.deleteEntry(KEY_ALIAS); } catch (KeyStoreException e) { e.printStackTrace(); } } } 

The method that collects the entire training chain:

 private static boolean prepare() { return getKeyStore() && getCipher() && isKeyReady(); } 

Encryption and decryption


We describe a method that encrypts a string argument:

 public static String encode(String inputString) { try { if (prepare() && initCipher(Cipher.ENCRYPT_MODE)) { byte[] bytes = sCipher.doFinal(inputString.getBytes()); return Base64.encodeToString(bytes, Base64.NO_WRAP); } } catch (IllegalBlockSizeException | BadPaddingException exception) { exception.printStackTrace(); } return null; } 

As a result, we get a Base64 string, which can be safely stored in the preferences of the application.

To decrypt, use the following method:

 public static String decode(String encodedString, Cipher cipherDecrypter) { try { byte[] bytes = Base64.decode(encodedString, Base64.NO_WRAP); return new String(cipherDecrypter.doFinal(bytes)); } catch (IllegalBlockSizeException | BadPaddingException exception) { exception.printStackTrace(); } return null; } 

Oops, at the entrance he gets not only an encrypted string, but also a Cipher object. Where he came from there, it will become clear later.

Wrong finger


In order to finally use the sensor, you need to use the FingerprintManagerCompat method:

 void authenticate (FingerprintManagerCompat.CryptoObject crypto, CancellationSignal cancel, int flags, FingerprintManagerCompat.AuthenticationCallback callback, Handler handler) 

Handler and flags are not needed right now, the signal is used to cancel fingerprint reading mode (when minimizing the application, for example), callbacks return the result of a specific readout, but let's stop in more detail above the crypto object.

CryptoObject in this case is used as a wrapper for Cipher . To get it, use the method:

 public static FingerprintManagerCompat.CryptoObject getCryptoObject() { if (prepare() && initCipher(Cipher.DECRYPT_MODE)) { return new FingerprintManagerCompat.CryptoObject(sCipher); } return null; } 

As can be seen from the code, the crypto object is created from the decrypting Cipher . If this Cipher is sent to the decode () method right now, an exception will be thrown indicating that we are trying to use the key without confirmation.

Strictly speaking, we create a crypto object and send it to the entrance to authenticate () just to receive this confirmation.

If getCryptoObject () returns null , this means that during the initialization of the Chiper , a KeyPermanentlyInvalidatedException occurred. There is nothing you can do except to let the user know that the fingerprint input is unavailable and he will have to re-enter the PIN code.

As I said before, we get the results of sensor reading in the kollbek methods. Here's what they look like:

 @Override public void onAuthenticationHelp(int helpCode, CharSequence helpString) { // ,    //  helpString    } @Override public void onAuthenticationFailed() { // ,    } @Override public void onAuthenticationError(int errorCode, CharSequence errString) { //    (5) //        (30 ) } @Override public void onAuthenticationSucceeded(@NonNull FingerprintManagerCompat.AuthenticationResult result) { //   } 

In case of successful recognition, we get an AuthenticationResult , from which we can get a Cipher object with an already confirmed key:

 result.getCryptoObject().getCipher() 

Now you can send it to decode () with a clear conscience, get a pin code, simulate its input and show the user its data.

That's all for today, comments, comments, suggestions and questions are welcome.
The simplest version of the code can look at the githaba .

Thanks for attention.

Source: https://habr.com/ru/post/317706/


All Articles