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,
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 user enters the PIN code, if SensorState.READY , then we save the PIN code, run the MainActivity .
- Restart the application, if SensorState.READY , then we read the fingerprint, we get the pin-code, we simulate its input, we start the MainActivity .
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:
- Secure storage for keys.
- Cryptographic key.
- 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:
- KEY_ALIAS is the alias of the key by which we will pull it out of the keystar, the usual psfs.
- .setUserAuthenticationRequired (true) - this flag indicates that every time we need to use a key, we will need to confirm ourselves, in our case, with a fingerprint.
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;
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) {
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.