I have an android app where I am installing the client certificate using the following code.
val inputStream: InputStream = resources.openRawResource(R.raw.client)
val intent = KeyChain.createInstallIntent()
val p12: ByteArray = inputStream.readBytes()
intent.putExtra(KeyChain.EXTRA_PKCS12, p12)
intent.putExtra(KeyChain.EXTRA_NAME, "Sample cert")
startActivityForResult(intent,3)
Now once user installs the certificate, I dont want to repeat this again so I want to check if the certificate is already installed.
I used the following code to check it, but doest get the certificate with both "AndroidCAStore" and "PKCS12".
"AndroidCAStore" - returns all trusted CA certs but my certificate is in user credentials.
"PKCS12" - IS empty
//val ks = KeyStore.getInstance("AndroidCAStore")
val ks: KeyStore = KeyStore.getInstance("PKCS12")
if (ks != null) {
ks.load(null, null)
val aliases = ks.aliases()
while (aliases.hasMoreElements()) {
val alias = aliases.nextElement() as String
val cert = ks.getCertificate(alias) as X509Certificate
Log.d("Cert ---->",cert.issuerDN.name)
if (cert.issuerDN.name.contains(issuerDn)) {
return true
}
}
}
Can some one help me fix this.
When you call
val intent = KeyChain.createInstallIntent()
you are storing the Certificate in the Android Keychain and I don't think there's a way of accessing the Certificates stored there programatecally, see this unanswered post.
Since you want to check if the certificate was installed in the KeyChain, you can call KeyChain.getPrivateKey() or KeyChain.getCertificateChain() and if they return null, then it means that the Certificate has not been installed yet.
Note: You have the limitation that you have to call KeyChain.choosePrivateKeyAlias first to establish trust between the app and the KeyChain, otherwise you'll get a KeyChain exception.
If you don't need to use the KeyChain, then you can simply create your own KeyStore and add your certificates to it. Then you will be able to call aliases() to get all of the aliases of the certificates in the KeyStore.
Related
I'm implementing SCA (Strong Customer Authentication) on Android. There are 3 factors on SCA which are following:
Something you know (customer's 6 digit passCode)
Something you have (device secure hardware)
Something you are (biometrics)
So for this, I need to generate public&private keys which is protected by customer's 6 digit passcode. "AndroidKeyStore" can protect public private keys with device-owner's lock-screen credentials but I need to protect it with my user's passCode on my app.
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null, passCode.toCharArray())
But, "AndroidKeyStore" does not support password. ( Is there any workaround to implement it with AndroidKeyStore? ) Following exception is thrown:
Caused by: java.lang.IllegalArgumentException: password not supported
at android.security.keystore.AndroidKeyStoreSpi.engineLoad(AndroidKeyStoreSpi.java:1031)
at java.security.KeyStore.load(KeyStore.java:1484)
If there is no way to use "AndroidKeyStore" direction, I need to change my path to the custom keystore.
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null)
val keyPairGenerator: KeyPairGenerator = KeyPairGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_EC,
)
val parameterSpec = ECGenParameterSpec("secp256r1")
keyPairGenerator.initialize(parameterSpec)
val keyPair = keyPairGenerator.generateKeyPair()
val passwordProtection = KeyStore.PasswordProtection(passCode.toCharArray())
keyStore.setEntry(
ALIAS_PUBLIC_KEY,
KeyStore.SecretKeyEntry(SecretKeySpec(
keyPair.public.encoded,
ALGORITHM_ELLIPTIC_CURVE
)),
passwordProtection
)
keyStore.setEntry(
ALIAS_PRIVATE_KEY,
KeyStore.SecretKeyEntry(SecretKeySpec(
keyPair.private.encoded,
ALGORITHM_ELLIPTIC_CURVE
)),
passwordProtection
)
keyStore.store(getOutputStream(), passCode.toCharArray())
So, I generated custom keystore with customer's passcode on PasswordProtection and I stored it in my app's data directory. After that, I generated public&private keys and put them on custom keystore. But I can not put this private key on secure hardware on Android.
The question is, how to implement proper SCA on Android with user's passCode with/without using "AndroidKeyStore" ?
I am using Android's keystore to implement fingerprint unlock of my Android app. I therefore use KeyGenerator to create a key using
var _keyGen = KeyGenerator.GetInstance(KeyProperties.KeyAlgorithmAes, "AndroidKeyStore")
KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(GetAlias(_keyId),
KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt)
.SetBlockModes(KeyProperties.BlockModeCbc)
// Require the user to authenticate with biometry to authorize every use
// of the key
.SetEncryptionPaddings(KeyProperties.EncryptionPaddingPkcs7)
.SetUserAuthenticationRequired(true);
_keyGen.Init(
builder
.Build());
_keyGen.GenerateKey();
When I later enumerate the aliases in the store I find the key I have created:
_keystore.Load(null);
var aliases = _keystore.Aliases();
if (aliases == null)
{
og("KS: no aliases");
}
else
{
while (aliases.HasMoreElements)
{
var o = aliases.NextElement();
Log("alias: " + o?.ToString());
}
}
While this is working reliably on most devices, some devices (e.g. Google Pixel 4a) seem to "lose" the keys in the Keystore quite regularly. When enumerating the aliases as above, no key is listed anymore. I can reproduce this behavior by updating my app using a debugger (settings are such that SharedPreferences and app data are kept and I do not have this behavior on another device).
Is there anything I can do to prevent losing the keys?
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()
}
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.
I have implemented javax.net.ssl.X509TrustManager in my code so I can validate my self-signed cert, which my software accesses. However, I still need to validate some other "standard" website SSL certificates. I am using CertPathValidator.validate() to do that, but I just realized that one cert chain I am being passed (for maps.googleapis.com) doesn't actually contain the complete chain - it contains the whole chain but the Root CA (Equifax), which does exist on the phone, but validate() still fails because (apparently) it's not explicitly in the chain. What am I missing here, so that the validation succeeds? Thanks for any input.
Edit - Relevant code (with exception checking removed):
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// chain is of type X509Certificate[]
CertPath cp = cf.generateCertPath(Arrays.asList(chain));
CertPathValidator cpv = CertPathValidator.getInstance(CertPathValidator.getDefaultType());
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
FileInputStream is = new FileInputStream("/system/etc/security/cacerts.bks");
ks.load(is, null);
PKIXParameters params = new PKIXParameters(ks);
CertPathValidatorResult cpvr = cpv.validate(cp, params);
Implementing your own TrustManager is generally a bad idea. The better way is to add your certificates to a keystore, and add it as a trust store. See this for an example of how to do it.
You probably need to add the default trust store to your validator. Also, Android does not do revocation (CRL) checking by default, so if you enabled it, you need to get the related CRL's manually. Post your code.