12

I have an encrypted password stored in the Android KeyStore.

I want to decrypt that password by authenticating the user using the fingerprint API.

As far as I understand, I have to call the FingerprintManager.authenticate(CryptoObject cryptoObject) method to start listening for the fingerprint result. The CryptoObject parameter is created like this:

public static Cipher getDecryptionCipher(Context context) throws KeyStoreException {
    try {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        SecretKey secretKey = getKeyFromKeyStore();
        final IvParameterSpec ivParameterSpec = getIvParameterSpec(context);

        cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec);
        return cipher;

    } catch (NoSuchAlgorithmException | NoSuchPaddingException | IOException | UnrecoverableKeyException | CertificateException | InvalidAlgorithmParameterException | InvalidKeyException e) {
        e.printStackTrace();

    }

    return null;
}

Cipher cipher = FingerprintCryptoHelper.getDecryptionCipher(getContext());
FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher);
fingerprintManager.authenticate(cryptoObject, ...);

The method getDecryptionCipher() works correctly until the cipher.init()call. On this call I get an UserNotAuthenticatedException, because the user is not authenticated for this secretKey. Which makes sense somehow. But isn't this a loop, impossible to fulfill:

  • To authenticate the user, I want to use his/her fingerprint
  • To listen for his/her fingerprint, I need to init the Cipher, which in return needs an authenticated user

What's wrong here??

EDIT:

I work with the emulator (Nexus 4, API 23).

Here's the code I use to create the key.

private SecretKey createKey() {
    try {
        KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
        keyGenerator.init(new KeyGenParameterSpec.Builder(
                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);
    }
}
muetzenflo
  • 4,344
  • 3
  • 27
  • 65

2 Answers2

4

I've found a method to escape the Catch 22!

You do it like this:

You try{ .init yout Cipher as usual

  1. if there's no UserNotAuthenticatedException (because user WAS authenticated within validity period of your key i.e. because he unlocked his device a few seconds ago) then do your encrypt/decrypt routine. The end!

  2. you caught UserNotAuthenticatedException - run FingerprintManager.authenticate workflow with null (yes!) CryptoObject, then in onAuthenticationSucceeded callback init your cipher again (yes!), but this time it won't throw UserNotAuthenticatedException and use this initialized instance to encrypt/decrypt (the callback returns null as we called it with null CryptoObject, so we can't use this). The end!

As simple as that...

But it took me two days to find this method by trail and error. Not to mention - it seems all authentication examples present online are wrong!

ssuukk
  • 6,870
  • 6
  • 31
  • 43
1

It turned out that the problem was related to a known issue with the KeyGenParameterSpec which prevents the use of the public key without authentication (which is exactly what the public key does NOT need).

A related question/answer can be found here: Android Fingerprint API Encryption and Decryption

The workaround is to create a PublicKeyfrom the originally created key and use this unrestricted PublicKey to init the cipher. So my final cipher uses AES/CBC/PKCS7Padding and is initialized by this method:

public boolean initCipher(int opMode) {
    try {
        Key key = mKeyStore.getKey(KEY_NAME, null);

        if (opMode == Cipher.ENCRYPT_MODE) {
            final byte[] encoded = key.getEncoded();
            final String algorithm = key.getAlgorithm();
            final X509EncodedKeySpec keySpec = new X509EncodedKeySpec(encoded);
            PublicKey unrestricted = KeyFactory.getInstance(algorithm).generatePublic(keySpec);

            mCipher.init(opMode, unrestricted);

        } else {
            final IvParameterSpec ivParameterSpec = getIvParameterSpec();
            mCipher.init(opMode, key, ivParameterSpec);

        }

        return true;

    } catch (KeyPermanentlyInvalidatedException exception) {
        return false;

    } catch ( NoSuchAlgorithmException | InvalidKeyException
            | InvalidKeySpecException | InvalidAlgorithmParameterException | UnrecoverableKeyException | KeyStoreException exception) {
        throw new RuntimeException("Failed to initialize Cipher or Key: ", exception);
    }
}

@NonNull
public IvParameterSpec getIvParameterSpec() {
    // the IV is stored in the Preferences after encoding.
    String base64EncryptionIv = PreferenceHelper.getEncryptionIv(mContext);
    byte[] encryptionIv = Base64.decode(base64EncryptionIv, Base64.DEFAULT);
    return new IvParameterSpec(encryptionIv);
}
Community
  • 1
  • 1
muetzenflo
  • 4,344
  • 3
  • 27
  • 65
  • 1
    is this realy working? You use AES and this is a symmetric algorithm without a public key. How is it possible to generate a public key from an AES key? – UKoehler Jan 16 '18 at 14:12
  • it definitely works, but I can't really explain why. I had the same thoughts as you mentioned, but it's the only way I got it to word "as intended". Please feel free to improve this approach! – muetzenflo Jan 16 '18 at 14:33
  • 1
    I don't see how this possibly could work with an AES key. First of all, there's a good chance that `getEncoded` will return `null` for a `SecretKey`/`PrivateKey`, to avoid having such key material being exposed outside the TEE. Even if that isn't the case here, `KeyFactory.getInstance` should fail for AES since it only supports DH, DSA, EC, RSA, and X.509. Seems to me like you're actually using some other key here. – Michael Jan 16 '18 at 15:23
  • It does return null, indeed – Pierre-Olivier Dybman Oct 23 '18 at 19:24