How do I require user authentication only for decryption but not encryption

Viewed 714

I have a public/private keypair in AndroidKeyStore which I generated as follows:

val spec = KeyGenParameterSpec.Builder(alias(username), KeyProperties.PURPOSE_DECRYPT or KeyProperties.PURPOSE_ENCRYPT)
                .setKeySize(keySize)
                .setUserAuthenticationRequired(true)
                .setBlockModes(ablockMode)
                .setEncryptionPaddings(apaddingMode)
                .setCertificateSubject(X500Principal("CN=Itsami Mario, OU=Adventure Unit, O=Plumber Bros, C=US"))
                .setKeyValidityStart(Date())
                .setKeyValidityEnd(Date(Date().time + 1000 * 60 * 60 * 24 * 7))
                .setCertificateSerialNumber(BigInteger(64, SecureRandom()))
                .setDigests(digest)
                .build()

        keyPairGen.initialize(spec)
        return keyPairGen.genKeyPair()

I want to require biometric authentication every time the private key is used, but I don't want to require a biometric prompt when encrypting with the public key. However, when use I use setUserAuthenticationRequired(true) in the KeyGeneratior and then I try to encrypt without first showing the BiometricPrompt, I get an android.security.KeyStoreException with the message: Key user not authenticated

How can I require authentication for decryption but not encryption?

1 Answers

You must be testing on a device running Android 6, Marshmallow. This is a known issue in that release, which was fixed in Android 7.

To work around the problem, you can extract the encoding of the public key and create a new PublicKey object from it, like so:

PublicKey publicKey = keyPair.getPublicKey();
PublicKey unrestrictedPublicKey =
         KeyFactory.getInstance(publicKey.getAlgorithm()).generatePublic(
                 new X509EncodedKeySpec(publicKey.getEncoded()));

This will work on all versions.

Note that it's also possible to create AES keys that require authentication when decrypting but not when encrypting, which is rather cool (AES is much, much faster than RSA). The trick is to generate the key outside of AndroidKeyStore and then import it twice, once with PURPOSE_ENCRYPT and once with PURPOSE_DECRYPT, under two different aliases, and specifying user authentication requirements on the DECRYPT version. Something like:

// Note that we do *not* specify "AndroidKeyStore" when we call getInstance()
KeyGenerator keyGen = KeyGenerator.getInstance("AES");
keyGen.init(128);
SecretKey secretKey = keyGen.generateKey();

// This time we do specify "AndroidKeyStore".
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);

// Now we import the encryption key, with no authentication requirements.
keyStore.setEntry(
     "encrypt_key",
     new KeyStore.SecretKeyEntry(secretKey),
     new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
             .setBlockMode(KeyProperties.BLOCK_MODE_GCM)
             .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
             .build());

// And the decryption key, this time requiring user authentication.
keyStore.setEntry(
     "decrypt_key",
     new KeyStore.SecretKeyEntry(secretKey),
     new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
             .setBlockMode(KeyProperties.BLOCK_MODE_GCM)
             .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
             .setUserAuthentication(true)
             .build());

Now, you can encrypt with the key alias "encrypt_key" at any time, no user authentication required, and you can decrypt with the key alias "decrypt_key", but only when you do the BiometricPrompt thing.

The downside of this is that the secret exists briefly in non-secure memory. In practice, this matters only if an attacker has already compromised the device when the key is created, and in that case you have most likely already lost.

Related