have you experienced the case when Android Fingerprint APIs are behaving in the not expected way? The issue I have is when I generate PrivateKey with given attestationChallenge and userAuthenticationValidityDurationSeconds for data signing. Everything works as expected if I don't enroll new fingerprints however after enrollment of a new fingerprint I expect for that key to be permanently invalidated but this is not the case. Have you experienced the similar?
Below is the code I'm using to generate the PrivateKey.
val keyStore = KeyStore.getInstance("AndroidKeystore")
keyStore.load(null)
val keyPairGenerator = KeyPairGenerator.getInstance(algorithm, "AndroidKeyStore")
val builder = KeyGenParameterSpec.Builder("alias", KeyProperties.PURPOSE_SIGN)
builder.apply {
setDigests(digests)
setSignaturePaddings(paddings)
setUserAuthenticationRequired(true)
setInvalidatedByBiometricEnrollment(true)
setAttestationChallenge(attestationChallenge)
setUserAuthenticationValidityDurationSeconds(userAuthenticationValidityDurationSeconds)
}
keyPairGenerator.initialize(builder.build())
keyPairGenerator.generateKeyPair()
Am I doing something wrong or missing something? Thanks!
To answer my own question. PrivateKey will not be invalidated by OS if userAuthenticationValidityDurationSeconds is set to a positive value. The key will only be invalidated on the enrolment of new fingerprint if its purpose is to be used only when authenticated with fingerprint.
Related
PROBLEM DESCRIPTION: When accessing the Android KeyStore with BiometricPrompt authentication, the BiometricPrompt appears every time when I have to perform read or write operation to the KeyStore. I am looking for a solution to authenticate only once, and then manipulate data in the keystore as I wish, in a similar way as it is done on iOS KeyChain.
I have implemented biometric authentication for Android so that I can store app API AuthRefreshToken in the application KeyStore secured by the biometric authentication, by calling setUserAuthenticationRequired(true) on the KeyStore params builder (see below). I have followed examples from Google (https://github.com/android/security-samples/tree/main/BiometricLoginKotlin) and other developers and have made solution working successfully. I am now trying to resolve the problem described above for second working day without success, and now considering using BiometricPrompt without CryptoObject, which would be a big disappointment. I suspect there is a way to authenticate once for a period of time, perhaps by setting paramsBuilder.setUserAuthenticationValidityDurationSeconds(30), but I am unable to achieve the intended result.
To get access to the KeyStore and read the API AuthRefreshToken, I use this code:
biometricPrompt = BiometricPromptUtils.createBiometricPrompt(this, ::decryptServerTokenFromStorage)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
fun decryptServerTokenFromStorage(authResult: BiometricPrompt.AuthenticationResult) {
ciphertextWrapper?.let { textWrapper ->
authResult.cryptoObject?.cipher?.let {
val authRefreshToken = cryptographyManager.decryptData(textWrapper.ciphertext, it)
// Use authRefreshToken to get authToken from the API
// The API returns new authRefreshToken which has to be saved back to the KeyStore
}
}
}
Everything works fine, and I get decrypted data. However, after each authentication on the app API using AuthRefreshToken, the token changes and I have to immediately save it back to the KeyStore. When this happens, I use below code, which displays the BiometricPrompt again. This causes the UI flow to show the BiometricPrompt twice:
biometricPrompt = BiometricPromptUtils.createBiometricPrompt(this, ::encryptServerTokenToStorage)
val promptInfo = BiometricPromptUtils.createPromptInfo(this)
biometricPrompt.authenticate(promptInfo, BiometricPrompt.CryptoObject(cipher))
fun encryptServerTokenToStorage(authResult: BiometricPrompt.AuthenticationResult) {
authResult.cryptoObject?.cipher?.apply {
SampleAppUser.refreshAuthToken?.let { refreshAuthToken ->
Log.d(TAG, "The token from server is $refreshAuthToken")
val encryptedServerTokenWrapper = cryptographyManager.encryptData(refreshAuthToken, this)
// Now save encrypted authRefreshToken together with initializationVector in the app prefs for future authentications
)
}
}
}
How can I authenticate at once with the BiometricPrompt so that I have full read/write access to the KeyStore for, let say, 1 minute or a longer interval without calling the BiometricPrompt multiple times?
I have tried different approaches and tried recreating the Cipher or reinitializing it for a different purpose, however in all these and similar attempts I am getting Javax.Crypto.IllegalBlockSizeException with message 'Key user not authenticated'
The keystore initialization is as follows:
// If Secretkey was previously created for that keyName, then grab and return it.
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE)
keyStore.load(null) // Keystore must be loaded before it can be accessed
keyStore.getKey(keyName, null)?.let { return it as SecretKey }
// if you reach here, then a new SecretKey must be generated for that keyName
val paramsBuilder = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
paramsBuilder.apply {
setBlockModes(ENCRYPTION_BLOCK_MODE) // KeyProperties.BLOCK_MODE_GCM
setEncryptionPaddings(ENCRYPTION_PADDING) // KeyProperties.ENCRYPTION_PADDING_NONE
setKeySize(KEY_SIZE) // 256
setUserAuthenticationRequired(true) // This is required for BiometricPrompt to work properly
}
val keyGenParams = paramsBuilder.build()
val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(keyGenParams)
return keyGenerator.generateKey()
}
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?
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.
Attempting to use SpongyCastle to provide the preferred encryption algorithm of RSA/ECB/OAEPwithSHA-512andMGF1Padding for Asymmetric encryption/decryption tasks on all supported Android device versions and having issues.
Encryption appears to be working fine.
But decryption is proving some trouble:
No provider for RSA/ECB/OAEPwithSHA-512andMGF1Padding
KeyGen spec is as follows:
val generatorSpec = KeyPairGeneratorSpec.Builder(context)
.setAlias(ALIAS)
.setSubject(X500Principal(ASYMMETRIC_KEY_COMMON_NAME_PREFIX + ALIAS))
.setSerialNumber(BigInteger.TEN)
.setStartDate(creationDate.time)
.setEndDate(expiryDate.time)
.build()
val keyPairGenerator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore")
keyPairGenerator.initialize(generatorSpec)
keyPairGenerator.generateKeyPair()
I'm now grabbing this value from the keyStore and trying to use it for decryption/encryption:
private fun rsaEncrypt(data: ByteArray, key: KeyStore.PrivateKeyEntry): ByteArray {
val encryptionCipher = Cipher.getInstance("RSA/ECB/OAEPwithSHA-512andMGF1Padding", "SC")
encryptionCipher.init(Cipher.ENCRYPT_MODE, key.certificate.publicKey)
val outputStream = ByteArrayOutputStream()
val cipherOutputStream = CipherOutputStream(outputStream as OutputStream, encryptionCipher)
cipherOutputStream.write(data)
cipherOutputStream.close()
return outputStream.toByteArray()
}
This appears to work fine, however decryption is where my issue lies:
private fun rsaDecrypt(data: ByteArray, key: KeyStore.PrivateKeyEntry): ByteArray {
val decryptionCipher = Cipher.getInstance("RSA/ECB/OAEPwithSHA-512andMGF1Padding", "SC")
decryptionCipher.init(Cipher.DECRYPT_MODE, key.privateKey)
// Rest of code for cipher streaming etc. etc.
}
initialising the decryptionCipher is giving me:
java.security.ProviderException: No provider for RSA/ECB/OAEPwithSHA-512andMGF1Padding
Which is strange due to my cipher instance returning fine and encryption working fine.
Also tried specifying the provider as “BC” rather than “SC” which gives a private exponent cannot be extracted error which I’m thinking is by design.
Trying to give a algorithm that isn't supported will break on the cipher initialisation and Encryption via Provider SC doesn't provide xxx so what gives?
TLDR: The encryption cipher has the same provider as decryption. But only decryption breaks....
There has to be something I’m missing here but can’t seem to put my finger on it. I've been working on this a while so any help is appreciated!
Edit: For interest I'm providing SpongyCastle through:
init {
Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
Security.addProvider(BouncyCastleProvider())
keyStore.load(null)
}
You can not decrypt with BC/SC using a key managed by AndroidKeyStore because the private key content is protected and its parameters (such as private exponent) are hidden, so any attempt to initialize a cipher with that key will fail.
The error message No provider for RSA/ECB/OAEPwithSHA-512andMGF1Padding using SC is probably due to an incorrect error handling by the library, but the private exponent cannot be extracted error for BC is clear. Encryption works because it uses the public key, which is not protected.
You need to use AndroidKeyStore for decryption (or use SC/BC also to generate the keys).
I am investigating the use of the Android KeyStore for Marshmallow and above.
I would like to simultaneously verify both the data integrity and the authentication of my data by employing HMAC's.
How do I go about achieving this?
I am current generating an Encrypt/Decrypt key as follows:-
mKeyStore = KeyStore.getInstance(keyStoreName);
mKeyStore.load(mKeyStoreLoadStoreParameter);
if (mKeyStore.containsAlias(keyStoreAlias)) {
mSecretKey = (SecretKey) mKeyStore.getKey(keyStoreAlias, KEY_STORE_PASSWORD);
} else {
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, keyStoreName);
final int keyPurpose = KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT;
keyGenerator.init(
new KeyGenParameterSpec.Builder(keyStoreAlias, keyPurpose)
.setKeySize(KEY_STORE_KEY_SIZE)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setRandomizedEncryptionRequired(true)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build());
mSecretKey = keyGenerator.generateKey();
I have found this sample for generating HMAC's
SecretKey key = ...; // HMAC key of algorithm "HmacSHA512".
KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
keyStore.load(null);
keyStore.setEntry(
"key1",
new KeyStore.SecretKeyEntry(key),
new KeyProtection.Builder(KeyProperties.PURPOSE_SIGN).build());
// Key imported, obtain a reference to it.
SecretKey keyStoreKey = (SecretKey) keyStore.getKey("key1", null);
// The original key can now be discarded.
Mac mac = Mac.getInstance("HmacSHA512");
mac.init(keyStoreKey);
However, how do I use this when encrypting/decrypting my data?
EXPLANATION
I have a number of choices/decisions to make when implementing security/cryptography within any Android application.
1). Do I implement cryptography of any sort Yes or No?
2). If Yes then... I should attempt to achieve the "most" secure solution possible.
If I am going to employ cryptography then I need to ensure the following.
a). I store passwords/secret keys in a "Safe Place" e.g. Android Key Store.
b). I use the "strongest" cryptography available.
c). I would like to simultaneously verify both the data integrity and the authentication of my data, e.g. I would like to detect if my encrypted data has been tampered with.
As I understand what I have read about HMAC's, they provide this functionality. I would like to know how I code the use of HMAC's into my Android application to ensure both the data integrity and the authentication of my data.
You can apply HMAC to the plain text HMAC(plain text) before encrypting and recompute the HMAC after decrypting to check that the original message is the same.
It may be redundant because if the cipher text is altered you will not be able to decrypt it.
First generate a HMAC key inside AndroidKeyStore. I found an example here
KeyGenerator keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_HMAC_SHA256, "AndroidKeyStore");
keyGenerator.initialize(
new KeyGenParameterSpec.Builder(hmacKeyAlias, KeyProperties.PURPOSE_SIGN).build());
SecretKey key = keyGenerator.generateKey();
Then Apply HMAC to the original data and store the result somewhere
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte hmacOriginalData[] = mac.doFinal(dataToEncrypt);
//Store hmacOriginalData
After decrypting, get HMAC key from AndroidKeyStore, recompute HMAC and check both macs are equal
Key key = keyStore.getKey(hmacKeyAlias, null);
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(key);
byte hmacDecryptedData[] = mac.doFinal(decryptedData);
//Check equals(hmacDecryptedData, hmacOriginalData);
Security is not my area of expertise but I have a question regarding storing a secret key in the Android KeyStore using API 18 and above. I use the following code to try and store my key:
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
KeyStore.SecretKeyEntry sKeyEntry = new KeyStore.SecretKeyEntry(ivKey);
ks.setEntry(ALIAS, sKeyEntry, null); // This is where the issue is
I understand that "null" should be a KeyProtection param that I build but this isn't available for API 18. Is there a workaround for this issue? I have been having difficulty finding anything that works.
EDIT I should mention that leaving it as null throws the error:
java.security.KeyStoreException: Protection parameters must be specified when importing a symmetric key
AndroidKeyStore did not support secret keys until API level 23. To do what you're trying to do you have to target 23 or above.
What you could do is use AndroidKeyStore's support for public/private key pairs (e.g. RSA) to encrypt the secret key material, then store it in a local file. When you want to use it, you'd need to use the private key to decrypt it, then once you have the key material use the normal Java crypto APIs (i.e. don't specify "AndroidKeyStore") to do cipher operations with it.
To see how to use AndroidKeyStore RSA key pairs to encrypt and decrypt, take a look at http://www.androidauthority.com/use-android-keystore-store-passwords-sensitive-information-623779/
However, I don't think that actually achieves anything from a security perspective. What security goals are you trying to achieve?
For creating a RSA(Public/Private) key using AndroidKeyStore you can use following methods.
Getting a KeyStore
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
Check if KeyStore contains our key or not
If KeyStore does not contains key, then create keys
if (!keyStore.containsAlias(KEY_NAME)) {
val keyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore")
keyPairGenerator.initialize(
KeyGenParameterSpec.Builder(
KEY_NAME,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
.setAlgorithmParameterSpec(RSAKeyGenParameterSpec(2048, F4))
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.build()
)
val keyPair = keyPairGenerator.generateKeyPair()
val publicKey = keyPair.public
val privateKey = keyPair.private
}
Retrieve keys using KEY_NAME
val privateKeyEntry = keyStore.getEntry(KEY_NAME, null) as KeyStore.PrivateKeyEntry
val privateKey = privateKeyEntry.privateKey
val publicKey = privateKeyEntry.certificate.publicKey
For more information you can refer this: