3

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);
Community
  • 1
  • 1
Tushar Purohit
  • 503
  • 7
  • 21

2 Answers2

2

@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)).

Bryan
  • 13,244
  • 9
  • 62
  • 114
  • I am encrypting the password before i call start listning and decrypting it in onAuthenticationSuccess call back, what exactly i need to do – Tushar Purohit Jun 25 '16 at 07:57
  • @TusharPurohit You don't need to create a `CryptoObject`, or do anything with `FingerprintManager` upon encryption. All you need to do make sure the key pair is initialized, and initialize the `Cipher` in `ENCRYPT_MODE`, then call `encrypt` on your string. This will allow you to avoid the `IllegalArgumentException`, the rest of the code you added is unnecessary. – Bryan Jun 27 '16 at 12:58
  • Do we need to remember the encrypted password in shared preference during activation? Else how we can we create cipher upon decrypt. – Bulu Jan 24 '17 at 21:09
1

In encryption mode, you're initializing the Cipher instance using a public key (unrestricted) which is not an Android Keystore key. To resolve the issue, you could initialize it with Android Keystore public key (i.e., mKeyStore.getCertificate(KEY_NAME).getPublicKey() instead of unrestricted).

However, it's not clear what you gain by gating public key encryption on user authorization. Anybody can perform the encryption operation, without the user authorizing that, because encryption uses a public key which by definition is not secret. In asymmetric crypto, the only operations that involve private keys (which are not publicly known by definition) are decryption and signing. Thus, it usually only makes sense to gate decryption or signing on user authorization.

Alex Klyubin
  • 4,614
  • 23
  • 22
  • I am the author of the original code mentioned in the question. The reason for the `unrestricted` password is because of a bug in Android 6.0 that causes the public key to be locked behind fingerprint authentication. Only the private key should be locked in that way, using a copy of the public key is a work-around for this bug. – Bryan Jun 24 '16 at 20:08
  • The main issue here is that gating crypto operations which use the public key (as opposed to the private or secret key) on fingerprint auth is likely wrong. The public key is not secret and thus anybody can perform the same operation, without the user authorizing them. – Alex Klyubin Jun 25 '16 at 05:22
  • @Bryan so do i need to unrestricted key from init call while encrypting password, can u privide bit detailed code for encryption and decryption flow – Tushar Purohit Jun 25 '16 at 07:55
  • @AlexKlyubin You are correct, it would be incorrect to do a public operation upon fingerprint authentication. I address this in my answer. – Bryan Jun 27 '16 at 12:53
  • Then what is the correct workflow if we require users to authenticate with fingerprint to be able to enable fingerprint authentication inside our app? This is a requirement that we can't avoid. What I did was use the public key with fingerprintManager, then once it's done I encrypt users password with that public key. But now on Android 8 it gives this error. – Jonas Oct 04 '17 at 07:32