Search code examples
javaandroidencryptionandroid-keystoreandroid-fingerprint-api

Initializing FingerpringManager.Crypto Object, getting Crypto primitive not backed by AndroidKeyStore provider?


I am using Android FingerPrintManager API and creating key pair using KeyPairGenerator, i want to encrypt a password with public key and then decrypt when user is authenticated by entring fingerPrint but as soon i run my project it gets crash and gives

Caused by: java.lang.IllegalArgumentException: Crypto primitive not backed by AndroidKeyStore provider

i hved used code from here : Android Fingerprint API Encryption and Decryption this post says that he is able to do ecryption and decryption an di have followed the same code and steps. here is my code

public KeyStore getKeyStore() {
    try {
        return KeyStore.getInstance("AndroidKeyStore");
    } catch (KeyStoreException exception) {
        throw new RuntimeException("Failed to get an instance of KeyStore", exception);
    }
}

public KeyPairGenerator getKeyPairGenerator() {
    try {
        return KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
    } catch (NoSuchAlgorithmException | NoSuchProviderException exception) {
        throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
    }
}

public Cipher getCipher() {
    try {
        return Cipher.getInstance("RSA");
    } catch (NoSuchAlgorithmException | NoSuchPaddingException exception) {
        throw new RuntimeException("Failed to get an instance of Cipher", exception);
    }
}

private void createKeyPair() {
    try {
        mKeyPairGenerator = getKeyPairGenerator();
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_DECRYPT)
                        .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
                        .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
                        .setUserAuthenticationRequired(true)
                        .build());
        mKeyPairGenerator.generateKeyPair();
    } catch (InvalidAlgorithmParameterException exception) {
        throw new RuntimeException(exception);
    }
}

private boolean initCipher(int opmode) {
    try {
        mKeyStore = getKeyStore();
        mKeyStore.load(null);

        mCipher = getCipher();

        if (opmode == Cipher.ENCRYPT_MODE) {

            PublicKey key = mKeyStore.getCertificate(KEY_NAME).getPublicKey();

            PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
                    .generatePublic(new X509EncodedKeySpec(key.getEncoded()));

            OAEPParameterSpec spec = new OAEPParameterSpec(
                    "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);
            mCipher.init(opmode, unrestricted, spec);
        } else {
            PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null);
            mCipher.init(opmode, key);
        }

        return true;
    } catch (KeyPermanentlyInvalidatedException exception) {
        return false;
    } catch (KeyStoreException | CertificateException | UnrecoverableKeyException
            | IOException | NoSuchAlgorithmException | InvalidKeyException | InvalidKeySpecException | InvalidAlgorithmParameterException exception) {
        throw new RuntimeException("Failed to initialize Cipher", exception);
    }
}


private void encrypt(String password) {
    try {
        initCipher(Cipher.ENCRYPT_MODE);
        byte[] bytes = mCipher.doFinal(password.getBytes());
        enrcyptedPassword = Base64.encodeToString(bytes, Base64.NO_WRAP);
        Log.d("EncryptedText", enrcyptedPassword);
    } catch (IllegalBlockSizeException | BadPaddingException exception) {
        throw new RuntimeException("Failed to encrypt password", exception);
    }
}

private String decryptPassword(Cipher cipher) {
    try {
        initCipher(Cipher.DECRYPT_MODE);
        byte[] bytes = Base64.decode(enrcyptedPassword, Base64.NO_WRAP);
        return new String(cipher.doFinal(bytes));
    } catch (IllegalBlockSizeException | BadPaddingException | RuntimeException exception) {
        throw new RuntimeException("Failed to decrypt password", exception);
    }
}

and from here i am initializing my CryptoObject:

createKeyPair();
    if (initCipher(Cipher.ENCRYPT_MODE)) {
        mCryptoObject = new FingerprintManager.CryptoObject
                (mCipher);
        encrypt("1111");
        if (!isFingerprintAuthAvailable()) {
            return;
        }
        mCancellationSignal = new CancellationSignal();
        mSelfCancelled = false;
        mFingerprintManager.authenticate(mCryptoObject, mCancellationSignal, 0 /* flags */, this, null);

I am getting exception at this line :

mFingerprintManager.authenticate(mCryptoObject, mCancellationSignal, 0 /* flags */, this, null);

Solution

  • @AlexKlyubin is right, you do not need to use the fingerprint manager for encryption, only decryption. In order to encrypt the text, all you need to do is call the encrypt(String password) method above.

    For decryption, you should be using FingerprintManagerCompat instead of FingerprintManager. In order to listen for fingerprint events and decrypt the password, you need to extend the FingerprintManagerCompat.AuthenticationCallback. I extended this class, and implemented a callback interface:

    public class FingerprintAuthentication extends FingerprintManagerCompat.AuthenticationCallback {
    
        private final Callback mCallback;
    
        public FingerprintCallback(Callback callback) {
            mCallback = callback;
        }
    
        @Override
        public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
            mCallback.onAuthenticationSucceeded(result);
        }
    
        @Override
        public void onAuthenticationHelp(int messageId, CharSequence message) {
            mCallback.onAuthenticationHelp(messageId, message);
        }
    
        @Override
        public void onAuthenticationError(int messageId, CharSequence message) {
            mCallback.onAuthenticationError(messageId, message);
        }
    
        @Override
        public void onAuthenticationFailed() {
            mCallback.onAuthenticationFailed();
        }
    
        public interface Callback {
    
            void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result);
    
            void onAuthenticationHelp(int messageId, CharSequence message);
    
            void onAuthenticationError(int messageId, CharSequence message);
    
            void onAuthenticationFailed();
        }
    }
    

    With this you can implement the Callback interface in your Fragment or Activity, then start listening for events:

    private void startListening(boolean cipher) {
        Timber.v("Start listening for fingerprint input");
        mCancellationSignal = new CancellationSignal();
        if(cipher) {
            mFingerprintManager.authenticate(new FingerprintManagerCompat.CryptoObject(mCipher),
                    0, mCancellationSignal, new FingerprintAuthentication(this), null);
        } else {
            setStage(Stage.CREDENTIALS);
        }
    }
    

    Lastly, only after the fingerprint authentication has succeeded can you decrypt the password:

    @Override
    public void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {
        try {
            mPassword = decryptPassword(result.getCryptoObject().getCipher());
        } catch (IllegalBlockSizeException | BadPaddingException exception) {
            exception.printStackTrace();
        }
    }
    

    Basically, when the user first signs in, you want to show an option for them to "use fingerprint in the future":

    enter image description here

    If the user selects this option and clicks login, this is when you call encrypt(). Then, the next time the user is required to login, you present the fingerprint dialog instead:

    enter image description here

    This is when you call startListening(initializeCipher(Cipher.DECRYPT_MODE)).