Search code examples
androidandroid-fingerprint-api

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?


Solution

  • 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.