42

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)
Bryan
  • 13,244
  • 9
  • 62
  • 114
  • Hey would you be able to create a gist for the full code needed to do this? Ive been looking for a solution to this for about 2 weeks now – MichaelStoddart Nov 23 '16 at 09:38
  • @TheAndroidDev To be honest, I didn't separate the UI code from the encryption/decryption code very well, and the code relies on Dagger and RxJava, so making an easily reusable gist may not be trivial. I will see what I can come up with. But for now, most of the code (sans Dagger) is in another question of mine: [How to Use Unsupported Exception for Lower Platform Version](http://stackoverflow.com/q/37553650/5115932). – Bryan Nov 23 '16 at 13:52
  • would you be able to answer my SO question? http://stackoverflow.com/questions/40724749/how-to-get-key-from-keystore-on-successful-fingerprint-auth – MichaelStoddart Nov 23 '16 at 16:27

1 Answers1

37

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);
    }
}
Bryan
  • 13,244
  • 9
  • 62
  • 114
  • 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? – atasoyh Jun 08 '16 at 05:19
  • @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? – Bryan Jun 08 '16 at 13:16
  • Yes I called initChipper. I changed to AES implementation. I am working with it now. – atasoyh Jun 09 '16 at 05:32
  • @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. – Bryan Jun 09 '16 at 12:31
  • 1
    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. – atasoyh Jun 10 '16 at 04:32
  • What is the sequence of calls for the decryption process? What do you call before calling 'decrypt(cipher)'? – Caleb Jul 15 '16 at 19:00
  • 1
    @Caleb I am calling `decrypt(cipher)` after I obtain the previously generated `KeyPair` from the `KeyStore`, which is done in `onAuthenticationSucceeded` of `FingerprintManagerCompat.AuthenticationCallback`. – Bryan Jul 15 '16 at 19:23
  • Hi bryan can you add all your flow ?? i am getting "Crypto primitive not initialized" – WolfJee Jan 31 '17 at 14:13
  • @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](http://stackoverflow.com/questions/37553650/how-to-use-unsupported-exception-for-lower-platform-version). 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. – Bryan Jan 31 '17 at 14:40
  • @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 – Androidme Feb 13 '17 at 15:38
  • 1
    @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. – Bryan Feb 13 '17 at 19:47
  • 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". – Androidme Feb 14 '17 at 14:48
  • 2
    @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. – Bryan Feb 14 '17 at 15:14
  • That's what I did @Bryan. Thanks for your help – Androidme Feb 15 '17 at 08:45
  • @atasoyh I was wondering if you could share some of your code for using AES instead of RSA for this example. Greatly appreciated! Thanks! – A_Kiniyalocts Mar 06 '17 at 19:54
  • @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](https://developer.android.com/samples/FingerprintDialog/index.html) using AES, I recommend taking a look at that. – Bryan Mar 06 '17 at 21:36