So, if I understand correctly, you want to encrypt the data (a password) without scanning the fingerprint but require the fingerprint when decrypting the data.
First of all you need to generate a key pair for encryption/decryption. This can be done as follows:
private static final String KEY_STORE_ID = "AndroidKeyStore";
private static final String KEY_PAIR_ALIAS = "MyKeyPair"
private PrivateKey getPrivateKey() {
KeyStore keyStore = getKeyStore();
try {
return (PrivateKey) keyStore.getKey(KEY_PAIR_ALIAS, null);
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
throw new RuntimeException(e);
}
}
private PublicKey getPublicKey() {
KeyStore keyStore = getKeyStore();
Certificate certificate;
try {
certificate = keyStore.getCertificate(KEY_PAIR_ALIAS);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
}
if (certificate == null) {
throw new RuntimeException("Key pair not found");
}
PublicKey publicKey = certificate.getPublicKey();
// This conversion is currently needed on API Level 23 (Android M) due to a platform bug which prevents the
// use of Android Keystore public keys when their private keys require user authentication. This conversion
// creates a new public key which is not backed by Android Keystore and thus is not affected by the bug.
// See https://developer.android.com/reference/android/security/keystore/KeyGenParameterSpec.html
try {
KeyFactory keyFactory = KeyFactory.getInstance(publicKey.getAlgorithm());
return keyFactory.generatePublic(new X509EncodedKeySpec(publicKey.getEncoded()));
} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
throw new RuntimeException("Failed to copy public key.", e);
}
}
private KeyStore getKeyStore() {
try {
KeyStore keyStore = KeyStore.getInstance(KEY_STORE_ID);
keyStore.load(null);
if (!keyStore.containsAlias(KEY_PAIR_ALIAS)) {
resetKeyPair();
}
return keyStore;
} catch (KeyStoreException | NoSuchAlgorithmException | CertificateException | IOException e) {
throw new RuntimeException(e);
}
}
private void resetKeyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, KEY_STORE_ID);
keyPairGenerator.initialize(new KeyGenParameterSpec.Builder(KEY_PAIR_ALIAS, KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setUserAuthenticationRequired(true).build());
keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new RuntimeException("Could not generate key pair", e);
}
}
Now you can use getPrivateKey()
to get the decryption key (which requires authentication) and getPublicKey()
to get the encryption key (which does not require authentication). You can use them like this:
public byte[] encrypt(byte[] input) throws KeyPermanentlyInvalidatedException, BadPaddingException, IllegalBlockSizeException {
Cipher cipher = getCipher(Cipher.ENCRYPT_MODE, getPublicKey());
return cipher.doFinal(input);
}
public interface Callback {
void done(byte[] result);
}
public CancellationSignal decrypt(Context context, final byte[] input, final Callback callback) throws KeyPermanentlyInvalidatedException {
CancellationSignal cancellationSignal = new CancellationSignal();
FingerprintManager fingerprintManager = (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(getCipher(Cipher.DECRYPT_MODE, getPrivateKey()));
fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, new FingerprintManager.AuthenticationCallback() {
@Override
public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
Cipher cipher = result.getCryptoObject().getCipher();
byte[] output;
try {
output = cipher.doFinal(input);
} catch (IllegalBlockSizeException | BadPaddingException e) {
throw new RuntimeException(e);
}
callback.done(output);
}
// Override other methods as well
}, null);
return cancellationSignal;
}
private Cipher getCipher(int mode, Key key) throws KeyPermanentlyInvalidatedException {
Cipher cipher;
try {
cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_RSA + "/" + KeyProperties.BLOCK_MODE_ECB + "/" + "OAEPWithSHA-256AndMGF1Padding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
OAEPParameterSpec algorithmParameterSpec = new OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
try {
cipher.init(mode, key, algorithmParameterSpec);
} catch (KeyPermanentlyInvalidatedException e) {
// The key pair has been invalidated!
throw e;
} catch (InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new RuntimeException(e);
}
return cipher;
}
Note that this is just an example. Some suggested improvements:
- Improve error handling.
- Don't throw
RuntimeException
.
- Override more methods of
FingerprintManager.AuthenticationCallback
.
- If you don't want the key pair to be invalidated when the user enrolls a new fingerprint or deletes an old one you can call
setInvalidatedByBiometricEnrollment(false)
(added in API level 24) on the KeyGenParameterSpec.Builder
in resetKeyPair()
. This will reduce security but improve usability.