Currently I'm using Android Keystore in my app to obfuscate Shared Preferences, in order to store personal data from the user.
It currently happens right now that when trying to use Keystore with an specific device (Oneplus 3T, running Android 7.1.1), the Cipher crashes using the generated Key, with the error KeyNotYetValidException. In other devices it does not crash.
I tried setting the key validity start to the past year, but got the same issue.
Here's the Key creation code:
val start = GregorianCalendar()
start.add(Calendar.YEAR, -1)
val end = GregorianCalendar()
end.add(Calendar.YEAR, 25)
lateinit var spec: AlgorithmParameterSpec
spec = KeyGenParameterSpec.Builder(ALIAS, KeyProperties.PURPOSE_ENCRYPT.or(KeyProperties.PURPOSE_DECRYPT))
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setUserAuthenticationRequired(false)
.setCertificateSubject(X500Principal("CN=$ALIAS"))
.setCertificateSerialNumber(BigInteger.valueOf(1337))
.setKeyValidityStart(start.time)
.setKeyValidityEnd(end.time)
.build()
And this is the stacktrace I get:
Caused by: android.security.keystore.KeyNotYetValidException: Key not yet valid
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:684)
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:748)
at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
at android.security.keystore.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:89)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:265)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:109)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2977)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2884)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2789)
at javax.crypto.Cipher.chooseProvider(Cipher.java:956)
at javax.crypto.Cipher.init(Cipher.java:1199)
at javax.crypto.Cipher.init(Cipher.java:1143)
at com.app.myapp.SecureStorage.booo006F006F006Foooo
Any recommendation here?
Related
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?
I am using AndroidKeyStore in my app to store a secret key and I am generating that key using KeyGenParameterSpec class. Here's the code:
final SecretKey secretKey;
final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setUserAuthenticationRequired(true)
.setUserAuthenticationValidityDurationSeconds(<validityInSeconds>)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setRandomizedEncryptionRequired(false)
.build();
keyGenerator.init(keyGenParameterSpec);
secretKey = keyGenerator.generateKey();
And after that, I am trying to init the cipher with this key:
final Cipher cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
Now, AndroidKeyStore seems to have a weird issue. cipher.init is failing in some cases and the weird part is, I am keeping everything same/constant except trying out different values for validityInSecs. When it fails, it fails with this error:
Caused by: java.lang.NullPointerException: Attempt to get length of null array
at org.spongycastle.crypto.params.KeyParameter.<init>(KeyParameter.java:13)
at org.spongycastle.jcajce.provider.symmetric.util.BaseBlockCipher.engineInit(BaseBlockCipher.java:615)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2659)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2570)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2475)
at javax.crypto.Cipher.chooseProvider(Cipher.java:566)
at javax.crypto.Cipher.init(Cipher.java:973)
at javax.crypto.Cipher.init(Cipher.java:908)
I've tried with different values for validityInSec and it fails from some (for values which are quite low) and succeeds for some (when I increase the value). Here are some data points:
10s -> fail 1800s -> success 100s -> fail 750s ->
success
Another interesting thing is that for the same value, it's failing sometimes and succeeding sometimes. Examples of these values are 750s and 625s.
I have no clue what's going on here.
Device: One plus 3t
OS: 8.0.0
Please help.
Edit:
The stack trace is a little misleading here, as it has traces from Spongycastle library.
That's bug in android framework classes. For a cipher, it tries with all the different providers one by one and if it fails, it stores the exception in a single variable. The problem is, it doesn't update that exception variable. So if the cipher failed for all the providers, the exception that it throws is from the first provider. So, it gives you this expression that only that provider was tried.
Here is the relevant code:
http://androidxref.com/8.0.0_r4/xref/libcore/ojluni/src/main/java/javax/crypto/Cipher.java#2546
For my case, after debugging class-by-class over the success case, I found out that it IS trying other providers too apart from SC. Relevant screenshot:
UPDATE 2:
I tried final Cipher cipher = Cipher.getInstance(<transformation>,"AndroidKeyStoreBCWorkaround");.
Now, when I pass duration as 1800s, it succeeds. And when I pass 40s, it fails with the following error:
06-28 20:06:55.462 624-624/in.zeta.android E/EncryptedStoreService: android.security.keystore.UserNotAuthenticatedException: User not authenticated
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:741)
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:777)
at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
at android.security.keystore.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:89)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:265)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:109)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2663)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2556)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2475)
at javax.crypto.Cipher.chooseProvider(Cipher.java:566)
at javax.crypto.Cipher.init(Cipher.java:830)
at javax.crypto.Cipher.init(Cipher.java:771)
I am gonna try authenticating user just before trying to init the cipher and see if it works
Although I do wanna solve this, In my production env, my time would anyways be 1800s. Is it okay to forget that it fails at lower duration?
I tried
final Cipher cipher = Cipher.getInstance(<transformation>,"AndroidKeyStoreBCWorkaround");.
By putting the breakpoints, I figured out that this is the provider that actually works.
Now, when I passed duration as 1800s, it succeeded. And when I pass 40s, it failed with the following error:
06-28 20:06:55.462 624-624/in.zeta.android E/EncryptedStoreService: android.security.keystore.UserNotAuthenticatedException: User not authenticated
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:741)
at android.security.KeyStore.getInvalidKeyException(KeyStore.java:777)
at android.security.keystore.KeyStoreCryptoOperationUtils.getInvalidKeyExceptionForInit(KeyStoreCryptoOperationUtils.java:54)
at android.security.keystore.KeyStoreCryptoOperationUtils.getExceptionForCipherInit(KeyStoreCryptoOperationUtils.java:89)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreCipherSpiBase.java:265)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineInit(AndroidKeyStoreCipherSpiBase.java:109)
at javax.crypto.Cipher.tryTransformWithProvider(Cipher.java:2663)
at javax.crypto.Cipher.tryCombinations(Cipher.java:2556)
at javax.crypto.Cipher$SpiAndProviderUpdater.updateAndGetSpiAndProvider(Cipher.java:2475)
at javax.crypto.Cipher.chooseProvider(Cipher.java:566)
at javax.crypto.Cipher.init(Cipher.java:830)
at javax.crypto.Cipher.init(Cipher.java:771)
Relevant source code links from the stack trace:
https://android.googlesource.com/platform/frameworks/base/+/508e665/keystore/java/android/security/keystore/AndroidKeyStoreCipherSpiBase.java#215
https://android.googlesource.com/platform/frameworks/base/+/83a86c5/keystore/java/android/security/KeyStoreCryptoOperationUtils.java#42
https://android.googlesource.com/platform/frameworks/base/+/master/keystore/java/android/security/KeyStore.java#707
Following the second link, it's not going into KeyStore.OP_AUTH_NEEDED, otherwise it'd have returned null from there. So mostly, it's in LOCKED state.
(https://android.googlesource.com/platform/frameworks/base/+/master/keystore/java/android/security/KeyStore.java#58)
I understand if it needs user authentication while initing the cipher, but was curious why it's successful for the duration of 1800s. I left my app open for > 1800s and then tried with this duration, it failed with the same exception. And then I passed 18000s and it worked. So, turns out it checks if user was authenticated within past keyvalidity secs, which perfectly makes sense, is obvious and if I am not wrong, present in docs too.
So the whole confusion was there due to the wrong exception thrown by Cipher class >.<
I am gonna try authenticating user just before trying to init the cipher and see if it works
I do not know how the validityInSeconds can affect the code, but the exception does not have to do with this
Caused by: java.lang.NullPointerException: Attempt to get length of null array
at org.spongycastle.crypto.params.KeyParameter.<init>(KeyParameter.java:13)
the exception is caused because the key is managed by the AndroidKeyStore but the Cipher is using the cryptographic provider spongycastle. The AES key is not extractable and spongycastle needs it to be able to encrypt
Remove spongycastle from your project or force to use AndroidKeyStore when you init the Cipher
Cipher cipher = Cipher.getInstance(algorithm, "AndroidKeyStore");
The cryptographic providers are selected in order of installation in the operating system. If a cryptographic provider is not explicitly indicated, Android will select spongycastle or AndroidKeyStore according to the one above. I do not know how validityInSeconds affects, but there may be some internal Android behavior that changes the order
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).
In our app we've been having issues with data in the Android Keystore suddenly becoming inaccessible. The specific exception we're seeing is here:
java.security.UnrecoverableKeyException: Failed to obtain information about private key
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStorePublicKeyFromKeystore(AndroidKeyStoreProvider.java:223)
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStoreKeyPairFromKeystore(AndroidKeyStoreProvider.java:259)
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStorePrivateKeyFromKeystore(AndroidKeyStoreProvider.java:269)
at android.security.keystore.AndroidKeyStoreSpi.engineGetKey(AndroidKeyStoreSpi.java:94)
at java.security.KeyStoreSpi.engineGetEntry(KeyStoreSpi.java:474)
at java.security.KeyStore.getEntry(KeyStore.java:1560)
at <PACKAGE_NAME>.EncryptionInteractor.generateKeys(EncryptionInteractor.java:104)
at <PACKAGE_NAME>.EncryptionInteractor.generateKeys(EncryptionInteractor.java:100)
at <PACKAGE_NAME>.EncryptionInteractor.init(EncryptionInteractor.java:93)
at <PACKAGE_NAME>.EncryptionInteractor.<init>(EncryptionInteractor.java:80)
at <PACKAGE_NAME>.EncryptionInteractor.init(EncryptionInteractor.java:65)
at <PACKAGE_NAME>.<APPLICATION_CLASS>.onCreate(APPLICATION_CLASS.java:17)
at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1118)
at android.app.ActivityThread.handleBindApplication(ActivityThread.java:5791)
at android.app.ActivityThread.-wrap1(Unknown Source:0)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1661)
at android.os.Handler.dispatchMessage(Handler.java:105)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
Caused by: android.security.KeyStoreException: Invalid key blob
at android.security.KeyStore.getKeyStoreException(KeyStore.java:695)
at android.security.keystore.AndroidKeyStoreProvider.loadAndroidKeyStorePublicKeyFromKeystore(AndroidKeyStoreProvider.java:224)
... 21 more
We have not been able to come up with a reliable way of reproducing the issue. Several articles mention possible states that can cause the Keystore to "forget" a Key or become locked such as here. However, as far as I can tell we have not fallen into any of these edge cases. It seems to happen after letting the device sit for a while after the first setup of the key. We have seen this happen across multiple emulators and devices, ranging from 21 to 26. Additionally these devices have used either swipe to unlock or a PIN. Changing the PIN or security method does not seem to cause the issue. Again, this issue seems to occur after the device has been sitting unused for several days.
I have found two other SOs here and here as well as one Google issue. If I'm understanding correctly, the answer linked in both seems to rely upon the premise that the caller has called setUserAuthenticationValidityDurationSeconds when creating the Key, and we have not done so. Additionally the given solution seems to rely upon just deleting the key and generating a new one.
Below is our setup for the key on for versions >= API 23. I've left out our key generation for versions older than 23, since we've primarily seen this on APIs >= 23.
private static final int RSA_KEY_SIZE = 2048;
private static final String CERT_SUBJECT_STRING = "CN=<COMPANY_NAME> Android App O=<COMPANY_NAME>";
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
try {
String alias = KEY_NAME;
KeyPairGenerator generator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA, ANDROID_KEY_STORE);
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 1);
KeyGenParameterSpec spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(RSA_KEY_SIZE, RSAKeyGenParameterSpec.F4))
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB)
.setCertificateNotAfter(end.getTime())
.setCertificateNotBefore(start.getTime())
.setCertificateSerialNumber(BigInteger.ONE)
.setCertificateSubject(new X500Principal(CERT_SUBJECT_STRING))
.build();
generator.initialize(spec);
generator.generateKeyPair();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
e.printStackTrace();
}
We then attempt to access the keys later via keyStore.getEntry(KEY_NAME, null). Again, this works for a while, but then will begin to throw the above exception.
I also faced issues with stability in KeyStore.
The solution was to use for private key
PrivateKey privKey = ks.getKey(alias, password)
And this to public key
PublicKey pubKey = ks.getCertificate(alias).getPublicKey();
instead of getEntry
ks.getEntry(alias, password)
The problem wasn't in the way you're creating the key but the way you're reading it.
Over more than a year never saw this issue again.
My app is using Android's keystore to encrypt some data after authenticating with a fingerprint. This seems to work on most devices but I have received error reports of OnePlus2 users with the exception
android.security.KeyStoreException: Signature/MAC verification failed
at android.security.KeyStore.getKeyStoreException(KeyStore.java:632)
at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.update(KeyStoreCryptoOperationChunkedStreamer.java:132)
at android.security.keystore.KeyStoreCryptoOperationChunkedStreamer.doFinal(KeyStoreCryptoOperationChunkedStreamer.java:217)
at android.security.keystore.AndroidKeyStoreCipherSpiBase.engineDoFinal(AndroidKeyStoreCipherSpiBase.java:473)
at javax.crypto.Cipher.doFinal(Cipher.java:1502)
My code basically does this (Written in Mono for Android):
Cipher _cipher = Cipher.GetInstance(KeyProperties.KeyAlgorithmAes + "/"
+ KeyProperties.BlockModeCbc + "/"
+ KeyProperties.EncryptionPaddingPkcs7);
KeyStore _keystore = KeyStore.GetInstance("AndroidKeyStore");
FingerprintManager _fingerprintManager = (FingerprintManager) Context.GetSystemService(Context.FingerprintService);
_keystore.Load(null);
var key = _keystore.GetKey(_keyId, null);
_cipher.Init(CipherMode.EncryptMode, key);
_cryptoObject = new FingerprintManager.CryptoObject(_cipher);
_fingerprintManager.Authenticate(_cryptoObject, _cancellationSignal, 0 /* flags */, this, null);
//OnAuthSucceeded:
var mySecret = _cipher.DoFinal(System.Text.Encoding.UTF8.GetBytes(textToEncrypt));
Is there anything wrong with the code? What does the exception mean?
First, your code looks fine.
In my experience Android fingerprint tends to have a lot of weird edge case errors across various devices.. I can't answer exactly but it sounds like a HW or implementation issue with the FP api on oneplus's part. I know XiaoMi and even Google has acknowledged various weird issues with their implementation.
Tips:
make sure you are listening only once for fingerprint. if you listen twice, you can receive the incorrect cipher object, so the encryption won't match.
update your gradle/min sdk/support libraries, all that stuff
hold on to your butt