javaandroidencryptionfingerprint

BadPaddingException when trying to decrypt Fingerprint API cypher


In my app, I save an encrypted version of the user's access code when they scan their finger in the initial setup [fingerprint enrollment]. When the user tries to unlock the app at a later time, I will attempt to decrypt this access code using the Fingerprint API [fingerprint verification].

However, Cipher.doFinal throws the following exception on decryption:

javax.crypto.BadPaddingException
 at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:482)
 at javax.crypto.Cipher.doFinal(Cipher.java:1502)
 (...)

Caused by: android.security.KeyStoreException: Invalid argument
         at android.security.KeyStore.getKeyStoreException(KeyStore.java:940)
         at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224)
         at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:473)
        ... 12 more

The fingerprint scanner is displayed in a DialogFragment. The following functions are always called in order from the constructor, regardless of whether the fingerprint is being enrolled or verified.

Initialize the keystore:

private void initializeKeystore() {

    try {
        mKeyStore = KeyStore.getInstance(KEY_STORE_NAME); //AndroidKeyStore
    } catch (KeyStoreException e) {
        mKeyStore = null;
    }

    try {
        mKeyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_NAME);
    } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
        mKeyGenerator = null;
    }
}

Create a key:

private void createKey() {
    if (mKeyGenerator != null) {
        try {
            mKeyStore.load(null);

            KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(KEY_NAME,
                    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7);

            mKeyGenerator.init(builder.build());
            mKeyGenerator.generateKey();
        } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException |
                CertificateException | IOException e) {
            mKeyGenerator = null;
        }
    }
}

Create cipher object:

private void createCipher() {
    try {
        mCipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/"
                + KeyProperties.BLOCK_MODE_CBC + "/"
                + KeyProperties.ENCRYPTION_PADDING_PKCS7);
    } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
        mCipher = null;
    }
}

When the the fingerprint scanner API successfully authenticates the user, the following code is called in sequence:

@Nullable Cipher getCipher(@NonNull final FingerprintStore ivStore) {
    if (mKeyStore != null && mKeyGenerator != null && mCipher != null) {
        try {
            mKeyStore.load(null);
            SecretKey key = (SecretKey)mKeyStore.getKey(KEY_NAME, null);

            switch (mEncryptionMode) {
                case MODE_ENCRYPT:
                    mCipher.init(Cipher.ENCRYPT_MODE, key);
                    ivStore.writeIv(mCipher.getParameters().getParameterSpec(IvParameterSpec.class).getIV());
                    break;
                case MODE_DECRYPT:
                    byte[] iv = ivStore.readIv();
                    mCipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
                    break;
            }
            return mCipher;
        } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException
                | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException
                | InvalidParameterSpecException | NullPointerException e) {
            return null;
        }
    }

    return null;
}

And, using the instance of Cipher just returned from the previous call:

@Nullable byte[] encryptOrDecrypt(@NonNull Cipher cipher, @NonNull byte[] subject) {
    try {
        return cipher.doFinal(subject);
    } catch (BadPaddingException | IllegalBlockSizeException e) {
        e.printStackTrace();
        return null;
    }
}

This call to doFinal works fine when encrypting the data, but throws the exception on decryption. I have inspected the byte[]'s of the both the initialization vector and the encrypted data and have found that they are being stored to disk (Base64) and read back into memory correctly.


Solution

  • Never mind, apparently I overlooked the fact that the SecretKey should only be generated in the enrollment stage. Because createKey was called when trying to decrypt the data, it was overwritten with a newly generated key before doFinal was called. The code works perfectly now.