TheAndroidDev TheAndroidDev - 1 month ago 10
Android Question

How to get key from keystore on successful fingerprint auth

I'm creating an app where the user has two options to unlock their app, one is using a pin and the other is using a fingerprint. In order to use the fingerprint they must first set up a pin because this pin is the decryption key to get their encrypted details out of

SharedPreferences
.

So i've followed this tutorial here: http://www.techotopia.com/index.php/An_Android_Fingerprint_Authentication_Tutorial#Accessing_the_Android_Keystore_and_KeyGenerator

I've managed to get the app to read a fingerprint and say whether it is valid or not. But when the fingerprint is authorised I have no idea how to get that pin out of the Android keystore.

Here is some code to demonstrate:

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
fingerprintManager = (FingerprintManager) getSystemService(FINGERPRINT_SERVICE);

if (!keyguardManager.isKeyguardSecure()) {

Toast.makeText(this, "Lock screen security not enabled in Settings", Toast.LENGTH_LONG).show();
return;
}

if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.USE_FINGERPRINT) !=
PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Fingerprint authentication permission not enabled", Toast.LENGTH_LONG).show();

return;
}

if (!fingerprintManager.hasEnrolledFingerprints()) {

// This happens when no fingerprints are registered.
Toast.makeText(this, "Register at least one fingerprint in Settings", Toast.LENGTH_LONG).show();
return;
}

generateKey();

if (cipherInit()) {
cryptoObject = new FingerprintManager.CryptoObject(cipher);
FingerprintHandler helper = new FingerprintHandler(this);

helper.startAuth(fingerprintManager, cryptoObject);
}

}

protected void generateKey() {
try {
keyStore = KeyStore.getInstance("AndroidKeyStore");
} catch (Exception e) {
e.printStackTrace();
}

try {
keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
} catch (NoSuchAlgorithmException |
NoSuchProviderException e) {
throw new RuntimeException("Failed to get KeyGenerator instance", e);
}

try {
keyStore.load(null);
keyGenerator.init(new
KeyGenParameterSpec.Builder(KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT |
KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setEncryptionPaddings(
KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build());
keyGenerator.generateKey();
} catch (NoSuchAlgorithmException |
InvalidAlgorithmParameterException
| CertificateException | IOException e) {
throw new RuntimeException(e);
}
}

public boolean cipherInit() {
try {
cipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
} catch (NoSuchAlgorithmException |
NoSuchPaddingException e) {
throw new RuntimeException("Failed to get Cipher", e);
}

try {
keyStore.load(null);
SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME,
null);

cipher.init(Cipher.ENCRYPT_MODE, key);
return true;
} catch (KeyPermanentlyInvalidatedException e) {
return false;
} catch (KeyStoreException | CertificateException
| UnrecoverableKeyException | IOException
| NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Failed to init Cipher", e);
}
}


KEY_NAME is the key(pin) i'm trying to store (I think).

Then in the
FingerprintHandler
class there is this method:

public void onAuthenticationSucceeded(
FingerprintManager.AuthenticationResult result) {

Toast.makeText(appContext,
"Authentication succeeded.",
Toast.LENGTH_LONG).show();

}


But how do i get the key i want out of the
result
if at all?

Answer

So to do this I ended up encrypting the users pin in to shared preferences and then decrypting when the fingerprint auth was successful:

So to save the pin:

private static final String CHARSET_NAME = "UTF-8";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String TRANSFORMATION = KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
        + KeyProperties.ENCRYPTION_PADDING_PKCS7;

private static final int AUTHENTICATION_DURATION_SECONDS = 30;

private KeyguardManager keyguardManager;
private static final int SAVE_CREDENTIALS_REQUEST_CODE = 1;


public void saveUserPin(String pin) throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, UnsupportedEncodingException, BadPaddingException, IllegalBlockSizeException {
    // encrypt the password
    try {
        SecretKey secretKey = createKey();
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secretKey);
        byte[] encryptionIv = cipher.getIV();
        byte[] passwordBytes = pin.getBytes(CHARSET_NAME);
        byte[] encryptedPasswordBytes = cipher.doFinal(passwordBytes);
        String encryptedPassword = Base64.encodeToString(encryptedPasswordBytes, Base64.DEFAULT);

        // store the login data in the shared preferences
        // only the password is encrypted, IV used for the encryption is stored
        SharedPreferences.Editor editor = BaseActivity.prefs.edit();
        editor.putString("password", encryptedPassword);
        editor.putString("encryptionIv", Base64.encodeToString(encryptionIv, Base64.DEFAULT));
        editor.apply();
    } catch (UserNotAuthenticatedException e) {
        e.printStackTrace();
        showAuthenticationScreen(SAVE_CREDENTIALS_REQUEST_CODE);
    }
}

private SecretKey createKey() {
    try {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
        keyGenerator.init(new KeyGenParameterSpec.Builder(Constants.KEY_NAME,
                KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                .setUserAuthenticationRequired(true)
                .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS)
                .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                .build());
        return keyGenerator.generateKey();
    } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
        throw new RuntimeException("Failed to create a symmetric key", e);
    }
}

Then to decrypt:

public String getUserPin() throws KeyStoreException, CertificateException, NoSuchAlgorithmException, IOException, NoSuchPaddingException, UnrecoverableKeyException, InvalidAlgorithmParameterException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
    // load login data from shared preferences (
    // only the password is encrypted, IV used for the encryption is loaded from shared preferences
    SharedPreferences sharedPreferences = BaseActivity.prefs;
    String base64EncryptedPassword = sharedPreferences.getString("password", null);
    String base64EncryptionIv = sharedPreferences.getString("encryptionIv", null);
    byte[] encryptionIv = Base64.decode(base64EncryptionIv, Base64.DEFAULT);
    byte[] encryptedPassword = Base64.decode(base64EncryptedPassword, Base64.DEFAULT);

    // decrypt the password
    KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
    keyStore.load(null);
    SecretKey secretKey = (SecretKey) keyStore.getKey(Constants.KEY_NAME, null);
    Cipher cipher = Cipher.getInstance(TRANSFORMATION);
    cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(encryptionIv));
    byte[] passwordBytes = cipher.doFinal(encryptedPassword);

    String string = new String(passwordBytes, CHARSET_NAME);

    return string;
}

The showAuthenticationScreen method that is called looks like this:

private void showAuthenticationScreen(int requestCode) {
    Intent intent = keyguardManager.createConfirmDeviceCredentialIntent(null, null);
    if (intent != null) {
        startActivityForResult(intent, requestCode);
    }
}

And then to get the result back from showAuthenticationScreen just override onActivityResult and call saveUserPin or getUserPin again whichever is required.