I am working on an app that uses Key Store Keys to enable authentication with the server. When the user logs in I am creating a key pair that requires Authentication to be used either by fingerprint or device credentials.
The problem I have is that I am trying for the keys to get invalidated every time the user changes the security in his device either by enrolling fingerprints or changing the PIN/PASSWORD/PATTERN.
I know that the Key specs have the setInvalidatedByBiometricEnrollment() method but from what I read it only invalidates the keys if there's a change in the biometrics, that doesn't help me if the user is only using device credentials. Furthermore, that method was added on API 24 and I am targetting devices starting with API 23.
This is the way I am creating the keys:
//Purposes of the key
int keyPurp =
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT |
KeyProperties.PURPOSE_SIGN | KeyProperties.PURPOSE_VERIFY;
//Init a builder for the key.
KeyGenParameterSpec.Builder keyBuilder = new KeyGenParameterSpec.Builder(keyAlias,keyPurp)
//We set the valid formats of the digests for signing
keyBuilder.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512);
//Require the user to authenticate and session expiry
keyBuilder.setUserAuthenticationRequired(true);
keyBuilder.setUserAuthenticationValidityDurationSeconds(10);
KeyGenParameterSpec keySpec = keyBuilder.build();
//Initialize the generator of the keys
generator.initialize(keySpec);
//Get the keys
generator.generateKeyPair();
Is there any Android API to detect changes in the Pin/Pattern/Password configuration?
Check out the docs for the isInvalidatedByBiometricEnrollment and setInvalidatedByBiometricEnrollment.
isInvalidatedByBiometricEnrollment
Returns true if the key is irreversibly invalidated when a new biometric is enrolled or all enrolled biometrics are removed. This has effect only for keys that require biometric user authentication for every use.
^ Important part is the last bit has effect only for keys that require biometric user authentication for every use.
setInvalidatedByBiometricEnrollment
Sets whether this key should be invalidated on biometric enrollment. This applies only to keys which require user authentication (see setUserAuthenticationRequired(boolean)) and if no positive validity duration has been set (see setUserAuthenticationValidityDurationSeconds(int), meaning the key is valid for biometric authentication only.
^ Important part there is if no positive validity duration has been set.
So if you want your keys to be invalidated by changes to biometry, they cannot have a duration attached to them. They must be single use.
We have noticed the keys actually cannot be used even if they have validity duration set to them, but you do not get correct Exception. And you cannot tell difference between a usage outside of duration period or if the key is invalidated.
Related
Issue
Biometric authentication iris and face-detection is not prompting with
biometricPrompt.authenticate(**crypto**, promptInfo) call.
Source reference:
Securing data with BiometricPrompt (19 May 2019)
One Biometric API Over all Android (30 October 2019)
Biometrich API
Device used for testing:
Samsung S8 (Android OS 9)
Steps of Authentication I'm following:
val biometricPrompt = BiometricPrompt(...)
val promptInfo = BiometricPrompt.PromptInfo.Builder()...
biometricPrompt.authenticate(promptInfo) (PFA: option A, B)
and there is another authentication method which take cipher object to make sure
biometricPrompt.authenticate(crypto, promptInfo). (PFA: option C)
Everything worked just as expected with new and older API device support. Until unless realize tested application for other biometric authentication option iris and using face detection.
If I follow
biometricPrompt.authenticate(promptInfo) then application simply display authentication option based on user preference which he has to choose from Device Setting -> Biometric preference.
And perform authentication independently. (PFA: option A, B)
But if use biometricPrompt.**authenticate**(crypto, promptInfo) then it displays only fingerprint authentication option ONLY. For other preference option iris and face-detection, it does not display anything on authenticate(..) method call. (PFA: option C)
Question
Why other Biometric authentication is not prompting with crypto object authentication.
Some devices only have one form factor, some have many form factors. Which form factor your app ends up using isn't really up to you; it's up to the OEM implementation. As explained in this blog post, whether a form factor is Strong or Weak doesn't depend on your code -- the OEM decides. However, you can request that a device uses Strong authentication for your app by specifying a CryptoObject when you call authenticate().
What you are experiencing is that the OEMs of your devices decided to make Fingerprint the default for Strong biometrics. Therefore, when you pass in a CryptoObject to authenticate() those devices show the user the UI for Fingerprint.
Face-Id is considered as WEAK authenticator. If you set .setAllowedAuthenticators(BIOMETRIC_WEAK or DEVICE_CREDENTIAL) in BiometricPrompt Info and performs any Key based crypto operations. It will throw
java.lang.IllegalArgumentException: Crypto-based authentication is not supported for Class 2 (Weak) biometrics.
For crypto-based authentication only allowed authenticators are BIOMETRIC_STRONG or DEVICE_CREDENTIAL
Refer table here: https://source.android.com/docs/security/features/biometric
I have a question to ask you guys as below.
Using FingerprintManager passing crytoObject type is Cipher.
The cipher I have init with a privateKey in decryption mode.
The privateKey is generated from AndroidKeystore with setUserAuthenticationRequired (true).
So, this private key will be used in cipher like cipher.init(DecryptionMode, privatekey), passing this cipher in Fingerprintmanager.authenticate.
After user verify I can successfully use the cryptoobject result, successfully get the cipher and do decryption (doFinal) in an encrypted String.
After done decrypt the string, I have to proceed signature because I need to use same private key second time for signature and get a complete string.
Here is the problem, it will always throw exception user not authenticated, is that mean cryptoObject result return from FingerprintManager after successfully verified the fingerprint its cipher type object can use for once only ? I can use it once for dofinal decrypt an encrypted string and after that I have to use the same private key for signature it will hit user not authenticated exception.
Does it mean user need to verify biometric again? It can only be used 1 time after user successfully verify, the cipher which I done init(decrypt mode , private key) it return and cannot use twice. I already tested setUserAuthenticationValidityDurationSeconds (int seconds) this will always hit not authenticated and I didnt use setUserAuthenticationValidityDurationSeconds .
Please I need expert help on fingerprintManager (cryptoObject result) when authenticate successful, the cipher type with privateKey can only use one time. I need to continue use it second time without hitting user not authenticated. Please.
setUserAuthenticationValidityDurationSeconds this is not working.
cipher init PrivateKey and pass into fingerprintManager.authenticate(cryptoObject)
When fingerprint successfully verified, the cipher can be used one time only. If need to use the same private key in signature it will hit user not authenticated. How to make the private key available ?
You can think of there being two "types" of keys: per-use keys, meaning authentication must take place for every use of the key, and time-based keys, which are available for an app-specified time after authentication.
Both types of keys are created with setUserAuthenticationRequired(true), but with different time parameters in setUserAuthenticationValidityDurationSeconds(t). Per-use keys (t == -1) can only be unlocked by biometrics, whereas time-based keys (t > 0) can be unlocked by biometrics or pin/pattern/password.
Biometric only
Create key using t = -1 (default)
Unlock using BiometricPrompt#authenticate(CryptoObject)
Password or biometric
Create key using t > 0
Unlock using lockscreen, or BiometricPrompt with setDeviceCredentialAllowed(true)
You can see the sample androidx.biometric app here, which implements both use cases.
KeyGenParameterSpec.Builder.setUserAuthenticationRequired(true) signifies that a Key in the Android Key Store is only authorized when:
The user is authenticated using a subset of their secure lock screen
credentials (pattern/PIN/password, fingerprint).
If enabled, UserNotAuthenticatedException will be thrown any time a Key is generated or the KeyStore is accessed when the user is not authenticated.
Is there an API call to reliably check that the user is indeed authenticated prior to interacting with the KeyStore?...rather than relying solely on catching the exception after the fact.
Android provides the ability to check this through KeyguardManager
Here is some Kotlin code to help you out:
val kgManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
kgManager.isKeyguardSecure //true if the user set up their Lock Screen securely.
There is no such API, you have to catch the exception and handle it to ask user authentication when using pattern/PIN/password.
One solution if targeting API 23+ and only using Fingerprint is to use the FingerprintManager : https://developer.android.com/reference/android/hardware/fingerprint/FingerprintManager.html
You have an example on how to use it there:
https://github.com/deivitaka/AndroidFingerprintAPI
https://github.com/googlesamples/android-FingerprintDialog
I wish to use the Android hardware-backed KeyStore, but I'm concerned about security and usability.
From what I've read here, KeyStore gets wiped when the user changes the device lock, unless setEncryptionRequired() is omitted. For usability sake, it seems this needs to be done, otherwise all hardware-backed keys would get wiped once the device lock is modified.
However, I've also read here that hardware-backed keys are not actually stored in the TEE, but rather, stored as key files in /data/misc/keystore/user_0/, encrypted by a device specific key that is stored within the TEE. Since a change in device lock wipes the KeyStore, it seems that the device specific key is derived from the device lock.
For security reasons, it makes sense to encrypt the key file, otherwise any root user would be able to read the key files and extract the private key, since they'd presumably be in they clear.
So I'm kind of in a dilemma. For usability sake, I should omit setEncryptionRequired(), but for security sake, I should set setEncryptionRequired().
Lastly, is it possible import a private key into the hardware-backed KeyStore using setKeyEntry()? I'm able to do so with no errors but I'm not sure if it's hardware-backed.
Is my understanding correct?
setEncryptionRequired() was deprecated in Android 6.0 (Marshmallow), and never really accomplished very much. The security of Android KeyStore depends on the TEE, not the password.
The blog post you linked to is out of date, at least on devices running Android 6.0 or later. On those devices, you should not use setEncryptionRequired(), and your keys will not be deleted until your app is uninstalled (or a factory reset is done, or your app deletes them). Your keys will be securely wrapped by secret keys that never leave the TEE. In fact, your keys will never leave the TEE in plaintext. When you use your keys, the data is passed into the TEE along with the encrypted key. The TEE unwraps the key then processes and returns the encrypted/signed/whatever data.
Yes, you can import private keys using setKeyEntry(). If you want to be sure that your key is hardware-backed, use KeyInfo.isInsideSecureHardware(). For example (this is from the documentation):
PrivateKey key = ...; // Android KeyStore key
KeyFactory factory = KeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore");
KeyInfo keyInfo;
boolean isHardwareBacked = false;
try {
keyInfo = factory.getKeySpec(key, KeyInfo.class);
isHardwareBacked = keyInfo.isInsideSecureHardware();
} catch (InvalidKeySpecException e) {
// Not an Android KeyStore key.
}
I have to get a JWT using the SHA-256 algorithm and a secret key (for example "blablablamysecretkey").
Despite checking SO, several libraries and their documentations I don't know yet how to perform this.
If I use this library https://github.com/jwtk/jjwt (one of the most used) this is the code sample:
Key key = MacProvider.generateKey();
String s = Jwts.builder().setSubject("stringtoencode").signWith(SignatureAlgorithm.HS512, key).compact();
Since I have to use SHA-256 algorithm I guess that I should use:
Key key = MacProvider.generateKey();
String s = Jwts.builder().setSubject("stringtoencode").signWith(SignatureAlgorithm.HS256, key).compact();
My problem is that this sample (and all of the samples I've seen by the way) use Key key = MacProvider.generateKey();, and if I'm not wrong this generates a generic key. In fact this is what the documentation says:
// We need a signing key, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
So my problem is how could I convert my secret key (string) into something of Key class?
MacProvider.generateKey() generates a random secret key, which is safer than using a passphrase. Keys need to be chosen at random. Read this post if you want to know how hmac keys have to be generated https://security.stackexchange.com/questions/95972/what-are-requirements-for-hmac-secret-key
// We need a signing key, so we'll create one just for this example. Usually
// the key would be read from your application configuration instead.
The text you have highlighted means that you have to persist the key in your server in order to verify JWT signature when a client sends a token. HMAC keys are symmetric, the key is used both for sign and verify
If you want to generate a Key from a passphrase String use
byte hmacKey[] = passphrase.getBytes(StandardCharsets.UTF8);
Key key = new SecretKeySpec(hmacKey,signatureAlgorithm.getJcaName());