Android Fingerprint API Encryption and Decryption

15,828

I found the final piece of the puzzle on the Android Issue Tracker, another known bug causes the unrestricted PublicKey to be incompatible with the Cipher when using OAEP. The work around is to add a new OAEPParameterSpec to the Cipher upon initialization:

OAEPParameterSpec spec = new OAEPParameterSpec(
        "SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT);

mCipher.init(opmode, unrestricted, spec);

Below is the final 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/ECB/OAEPWithSHA-256AndMGF1Padding");
    } catch(NoSuchAlgorithmException | NoSuchPaddingException exception) {
        throw new RuntimeException("Failed to get an instance of Cipher", exception);
    }
}

private void createKeyPair() {
    try {
        mKeyPairGenerator.initialize(
                new KeyGenParameterSpec.Builder(KEY_ALIAS, 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("Failed to generate key pair", exception);
    }
}

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

        if(opmode == Cipher.ENCRYPT_MODE) {
            PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).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_ALIAS, null);
            mCipher.init(opmode, key);
        }

        return true;
    } catch (KeyPermanentlyInvalidatedException exception) {
        return false;
    } catch(KeyStoreException | CertificateException | UnrecoverableKeyException
            | IOException | NoSuchAlgorithmException | InvalidKeyException
            | 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());
        String encrypted = Base64.encodeToString(bytes, Base64.NO_WRAP);
        mPreferences.getString("password").set(encrypted);
    } catch(IllegalBlockSizeException | BadPaddingException exception) {
        throw new RuntimeException("Failed to encrypt password", exception);
    }
}

private String decrypt(Cipher cipher) {
    try {
        String encoded = mPreferences.getString("password").get();
        byte[] bytes = Base64.decode(encoded, Base64.NO_WRAP);
        return new String(cipher.doFinal(bytes));
    } catch (IllegalBlockSizeException | BadPaddingException exception) {
        throw new RuntimeException("Failed to decrypt password", exception);
    }
}
Share:
15,828
Bryan
Author by

Bryan

Professional software engineer and game development hobbyist.

Updated on June 06, 2022

Comments

  • Bryan
    Bryan about 2 years

    I am using the Android M Fingerprint API to allow users to login to the application. To do this I would need to store the username and password on the device. Currently I have the login working, as well as the Fingerprint API, but the username and password are both stored as plaintext. I would like to encrypt the password before I store it, and be able to retrieve it after the user authenticates with their fingerprint.

    I am having a great amount of difficulty getting this to work. I have been trying to apply what I can from the Android Security samples, but each example seems to only handle encryption or signing, and never decryption.

    What I have so far is that I have to obtain an instance of the AndroidKeyStore, a KeyPairGenerator and a Cipher, using asymmetric cryptography to allow the use of the Android KeyGenParameterSpec.Builder().setUserAuthenticationRequired(true). The reason for asymmetric cryptography is because the setUserAuthenticationRequired method will block any use of the key if the user is not authenticated, but:

    This authorization applies only to secret key and private key operations. Public key operations are not restricted.

    This should allow me to encrypt the password using the public key before the user authenticates with their fingerprint, then decrypt using the private key only after the user is authenticated.

    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("EC", "AndroidKeyStore");
        } catch(NoSuchAlgorithmException | NoSuchProviderException exception) {
            throw new RuntimeException("Failed to get an instance of KeyPairGenerator", exception);
        }
    }
    
    public Cipher getCipher() {
        try {
            return Cipher.getInstance("EC");
        } catch(NoSuchAlgorithmException | NoSuchPaddingException exception) {
            throw new RuntimeException("Failed to get an instance of Cipher", exception);
        }
    }
    
    private void createKey() {
        try {
            mKeyPairGenerator.initialize(
                    new KeyGenParameterSpec.Builder(KEY_ALIAS,
                            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
                            .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")
                            .setUserAuthenticationRequired(true)
                            .build());
            mKeyPairGenerator.generateKeyPair();
        } catch(InvalidAlgorithmParameterException exception) {
            throw new RuntimeException(exception);
        }
    }
    
    private boolean initCipher(int opmode) {
        try {
            mKeyStore.load(null);
    
            if(opmode == Cipher.ENCRYPT_MODE) {
                PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
                mCipher.init(opmode, key);
            } else {
                PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
                mCipher.init(opmode, key);
            }
    
            return true;
        } catch (KeyPermanentlyInvalidatedException exception) {
            return false;
        } catch(KeyStoreException | CertificateException | UnrecoverableKeyException
                | IOException | NoSuchAlgorithmException | InvalidKeyException
                | 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());
            String encryptedPassword = Base64.encodeToString(bytes, Base64.NO_WRAP);
            mPreferences.getString("password").set(encryptedPassword);
        } catch(IllegalBlockSizeException | BadPaddingException exception) {
            throw new RuntimeException("Failed to encrypt password", exception);
        }
    }
    
    private String decryptPassword(Cipher cipher) {
        try {
            String encryptedPassword = mPreferences.getString("password").get();
            byte[] bytes = Base64.decode(encryptedPassword, Base64.NO_WRAP);
            return new String(cipher.doFinal(bytes));
        } catch (IllegalBlockSizeException | BadPaddingException exception) {
            throw new RuntimeException("Failed to decrypt password", exception);
        }
    }
    

    To be honest, I am not sure if any of this is right, it is bits and pieces from anything I could find on the subject. Everything I change throws a different exception, and this particular build does not run because I cannot instantiate the Cipher, it throws a NoSuchAlgorithmException: No provider found for EC. I have tried switch to RSA as well, but I get similar errors.

    So my question is basically this; how can I encrypt plaintext on Android, and make it available for decryption after the user is authenticated by the Fingerprint API?


    I have made some progress, mostly due to the discovery of the information on the KeyGenParameterSpec documentation page.

    I have kept getKeyStore, encryptePassword, decryptPassword, getKeyPairGenerator and getCipher mostly the same, but I changed the KeyPairGenerator.getInstance and Cipher.getInstance to "RSA" and "RSA/ECB/OAEPWithSHA-256AndMGF1Padding" respectively.

    I also changed the rest of the code to RSA instead of Elliptic Curve, because from what I understand, Java 1.7 (and therefore Android) does not support encryption and decryption with EC. I changed my createKeyPair method based on the "RSA key pair for encryption/decryption using RSA OAEP" example on the documentation page:

    private void createKeyPair() {
        try {
            mKeyPairGenerator.initialize(
                    new KeyGenParameterSpec.Builder(KEY_ALIAS, 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);
        }
    }
    

    I also altered my initCipher method based on the known issue in the KeyGenParameterSpec documentation:

    A known bug in Android 6.0 (API Level 23) causes user authentication-related authorizations to be enforced even for public keys. To work around this issue extract the public key material to use outside of Android Keystore.

    private boolean initCipher(int opmode) {
        try {
            mKeyStore.load(null);
    
            if(opmode == Cipher.ENCRYPT_MODE) {
                PublicKey key = mKeyStore.getCertificate(KEY_ALIAS).getPublicKey();
    
                PublicKey unrestricted = KeyFactory.getInstance(key.getAlgorithm())
                        .generatePublic(new X509EncodedKeySpec(key.getEncoded()));
    
                mCipher.init(opmode, unrestricted);
            } else {
                PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_ALIAS, null);
                mCipher.init(opmode, key);
            }
    
            return true;
        } catch (KeyPermanentlyInvalidatedException exception) {
            return false;
        } catch(KeyStoreException | CertificateException | UnrecoverableKeyException
                | IOException | NoSuchAlgorithmException | InvalidKeyException
                | InvalidAlgorithmParameterException exception) {
            throw new RuntimeException("Failed to initialize Cipher", exception);
        }
    }
    

    Now I can encrypt the password, and save the encrypted password. But when I obtain the encrypted password and attempt to decrypt, I get a KeyStoreException Unknown error...

    03-15 10:06:58.074 14702-14702/com.example.app E/LoginFragment: Failed to decrypt password
            javax.crypto.IllegalBlockSizeException
                at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:486)
                at javax.crypto.Cipher.doFinal(Cipher.java:1502)
                at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251)
                at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21)
                at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301)
                at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96)
                at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805)
                at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757)
                at android.os.Handler.dispatchMessage(Handler.java:102)
                at android.os.Looper.loop(Looper.java:148)
                at android.app.ActivityThread.main(ActivityThread.java:5417)
                at java.lang.reflect.Method.invoke(Native Method)
                at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
            Caused by: android.security.KeyStoreException: Unknown error
                at android.security.KeyStore.getKeyStoreException(KeyStore.java:632)
                at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:224)
                at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:473)
                at javax.crypto.Cipher.doFinal(Cipher.java:1502) 
                at com.example.app.ui.fragment.util.LoginFragment.onAuthenticationSucceeded(LoginFragment.java:251) 
                at com.example.app.ui.controller.FingerprintCallback.onAuthenticationSucceeded(FingerprintCallback.java:21) 
                at android.support.v4.hardware.fingerprint.FingerprintManagerCompat$Api23FingerprintManagerCompatImpl$1.onAuthenticationSucceeded(FingerprintManagerCompat.java:301) 
                at android.support.v4.hardware.fingerprint.FingerprintManagerCompatApi23$1.onAuthenticationSucceeded(FingerprintManagerCompatApi23.java:96) 
                at android.hardware.fingerprint.FingerprintManager$MyHandler.sendAuthenticatedSucceeded(FingerprintManager.java:805) 
                at android.hardware.fingerprint.FingerprintManager$MyHandler.handleMessage(FingerprintManager.java:757) 
                at android.os.Handler.dispatchMessage(Handler.java:102) 
                at android.os.Looper.loop(Looper.java:148) 
                at android.app.ActivityThread.main(ActivityThread.java:5417) 
                at java.lang.reflect.Method.invoke(Native Method) 
                at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
    
  • atasoyh
    atasoyh about 8 years
    Hi, I tried much more code and one of them is your code. I get an error "Crypto primitive not initialized". Can you run the code succesfully?
  • Bryan
    Bryan about 8 years
    @atasoyh Yes, I have been using this code. The error you are getting seems to say that the Cipher is not initialized. Are you sure you are calling initCipher() before you try to use it?
  • atasoyh
    atasoyh about 8 years
    Yes I called initChipper. I changed to AES implementation. I am working with it now.
  • Bryan
    Bryan about 8 years
    @atasoyh AES is used for symmetric cryptography, this shows an asymmetric example using RSA. If you want to use AES you may have to rework the code significantly.
  • atasoyh
    atasoyh about 8 years
    I need encryption, decryption. I decided to RSA firstly, but I couldn't solve my problem and I changed to AES my code. Thanks for your interest in.
  • Caleb
    Caleb almost 8 years
    What is the sequence of calls for the decryption process? What do you call before calling 'decrypt(cipher)'?
  • Bryan
    Bryan almost 8 years
    @Caleb I am calling decrypt(cipher) after I obtain the previously generated KeyPair from the KeyStore, which is done in onAuthenticationSucceeded of FingerprintManagerCompat.AuthenticationCallback.
  • WolfJee
    WolfJee over 7 years
    Hi bryan can you add all your flow ?? i am getting "Crypto primitive not initialized"
  • Bryan
    Bryan over 7 years
    @WolfJee As I stated in another comment, making a reusable piece of code based on my work is not trivial; I didn't do the best job of separating my code. Though I am working on it, it is not a main priority; but most of my code is posted in another question. In any case, the error Crypto primitive not initialized means that mCipher.init() was not called before mCipher.doFinal(). If you are still having trouble feel free to post a question and link to it here, I will take a look.
  • Androidme
    Androidme over 7 years
    @Bryan: Thanks for detailed explanation and code. Encryption and decryption works fine if I am in the same session. However, if I generate the encrypted password and save it in preferences and close the app. Now if I start the app again and use the private key to decrypt the password, I get "Unknown error" that you were getting. So, just wanted to ask you if you tried it with saving and relaunching the app or everything was done in single session....Thanks a lot
  • Bryan
    Bryan over 7 years
    @Androidme Everything works as expected even if the app is closed and relaunched. It sounds like you could be generating a new PrivateKey upon each launch, replacing the PrivateKey stored in the KeyStore. But I cannot be certain without seeing some code. Ask a new question and I will take a look.
  • Androidme
    Androidme over 7 years
    You are right @Bryan. I was generating new PrivateKey. It is working now, however I can decrypt only one value. If I save username and password both, and try to decrypt both, first one decrypts fine, however second one gives error "android.security.KeyStoreException: Key user not authenticated".
  • Bryan
    Bryan over 7 years
    @Androidme That is a limitation of the API; setUserAuthenticationRequired(true) requires authentication for every use of the PrivateKey. So the user would need to re-authenticate to use the key twice. A simple work-around for this would be to concatenate the username and password into a single String separated by a space and encrypt/decrypt them together.
  • Androidme
    Androidme over 7 years
    That's what I did @Bryan. Thanks for your help
  • A_Kiniyalocts
    A_Kiniyalocts over 7 years
    @atasoyh I was wondering if you could share some of your code for using AES instead of RSA for this example. Greatly appreciated! Thanks!
  • Bryan
    Bryan over 7 years
    @A_Kiniyalocts As I mentioned to atasoyh, using AES would not be a simple drop-in replacement. A significant amount of my code would have to be rewritten to make use of symmetric cryptography; and I do not have the time nor the inclination to work on it. Google does have a symmetric key sample application using AES, I recommend taking a look at that.