I'm seeing app crashes in production on Android 12 and 13 (so far) as follows when calling Signature.initSign:
android.security.keystore.UserNotAuthenticatedException: User not authenticated
at android.security.keystore2.KeyStoreCryptoOperationUtils.getInvalidKeyException(KeyStoreCryptoOperationUtils.java:128)
at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.ensureKeystoreOperationInitialized(AndroidKeyStoreSignatureSpiBase.java:217)
at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:123)
at android.security.keystore2.AndroidKeyStoreSignatureSpiBase.engineInitSign(AndroidKeyStoreSignatureSpiBase.java:101)
at java.security.Signature$Delegate.init(Signature.java:1357)
at java.security.Signature$Delegate.chooseProvider(Signature.java:1310)
at java.security.Signature$Delegate.engineInitSign(Signature.java:1385)
at java.security.Signature.initSign(Signature.java:679)
The flow I'm following is:
generate a key pair in the android keystore with a sole purpose of
KeyProperties.PURPOSE_SIGN. The code is similar to the KeyPairGenerator sample here: https://developer.android.com/training/articles/keystore#UsingAndroidKeyStore (In my case UserAuthenticationRequired is
true, invalidatedByBiometricEnrollment is false)
later, when signing is required, retrieve the private key from the keystore and call initSign(privateKey)
Now launch the BiometricPrompt (passing in the initialised Signature as part of the Cipher object)
Once the user has successfully authenticated, then call Signature.update and Signature.sign on the returned Signature.
I might expect the crash at step 4 if the user had failed to authenticate, or if a weak authenticator was used, but this crash is happening at step 2 when calling initSign. The user can't have authenticated here, as initSign must be called on the signature before passing it to the biometric prompt.
What could cause this? The crashes occur across multiple devices and once users experience this it repeats consistently, so doesn't seem to be a rare edge case. I cannot replicate this locally on a variety of devices on Android 12 and 13 so in theory the flow works.
Related
I've recently been developing a solution around the Secure Key Import feature of Android (info here) and have run into a problem.
I follow the procedure as documented. On the final step, when calling keyStore.setEntry(...) I get thrown an error with the code -1000 which is KM_ERROR_UNKNOWN_ERROR (error codes). I really don't have an idea on how to proceed from here. Any ideas on where the problem might be?
Some relevant code:
// (app) send attestation challenge request to server
// (server) generate and send challenge to the app
// (app) use challenge to generate a PURPOSE_WRAP_KEY key pair
// (app) get certificate and send to server
// (server) do wrap operations and return a blob (ASN.1 sequence as required in docs)
// (app) code below
byte[] wrappedKeySequence = response.body().getSequenceAsBytes();
AlgorithmParameterSpec spec = new KeyGenParameterSpec.Builder(WRAP_KEY_ALIAS, KeyProperties.PURPOSE_WRAP_KEY)
.setDigests(KeyProperties.DIGEST_SHA256)
.build();
KeyStore.Entry wrappedKeyEntry = new WrappedKeyEntry(wrappedKeySequence, WRAP_KEY_ALIAS, WRAP_ALGORITHM, spec);
String keyAlias = "SECRET_KEY";
keyStore.setEntry(keyAlias, wrappedKeyEntry, null);
More random details:
I'm trying to import an AES128 key
It'll be only used for encrypting data
Targeting API 28 and above, as required by the docs
Again, any help would be greatly appreciated.
Thanks,
G.
Update:
I've found the reason for this specific error, but have come to another error.
Namely, I used the tag 403 which defines MIN_SECONDS_BETWEEN_OPS. It being in the types.hal file, one would expect it to be implemented/valid everywhere, but it seems this isn't the case. However, I'm testing only on one Samsung phone, so it might be implemented by other manufacturers, or even on other Samsung phones.
Anyway, the next error is INVALID_ARGUMENT (-38) which, unlike the name suggests is as cryptic as this one. The docs say that it should occur for the RSA stuff (I'm trying to import an AES key), so the saga continues.
I'll update this answer if I find anything else.
Update 2:
I don't have any good news regarding the INVALID_ARGUMENT error. I get it even when I execute the unedited CTS test code, which is supposed to work, as the manufacturers use the CTS tests for validating that the devices work before leaving the factory.
For now I've paused work on that feature, if I ever come back to it I'll update as necessary.
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 am particularly new to App development and have learnt lately about the entire Signing process of an apk, why it's mandatory and it's importance to prevent unauthorized and tampering of the app.
One of the famous checks for Signature refers to using the PackageManager class to do signature verification. Is there any other method which checks for the META-INF directory in the apk itself for tampering or other abusive activities to verify the App is not tampered and intact with its original signature?
Best practice is Server Side Tampering Detection which can not be patched.
Here is how:
Use Android SafetyNet. This is how Android Pay validates itself.
The basic flow is:
Your server generates a nonce that it sends to the client app.
The app sends a verification request with the nonce via Google Play Services.
SafetyNet verifies that the local device is unmodified and passed the CTS.
A Google-signed response ("attestation") is returned to your app with a pass/fail result and information about your app's APK (hash and sigining certificate).
Your app sends the attestation to your server.
Your server validates the nonce and APK signature, and then submits the attestation to a Google server for verification. Google checks the attestation signature and tells you if it is genuine.
If this passes, you can be fairly confident that the user is running a genuine version of your app on an unmodified system. The app should get an attestation when it starts up and send it along to your sever with every transaction request.
Note, however, this means:
Users who have rooted their phone will not pass these checks
Users who have installed custom or third-party ROM/firmware/OS (eg Cyanogen) will not pass these checks
Users who do not have access to Google Play Services (eg Amazon
devices, people in China) will not pass these checks
...and therefore will be unable to use your app. Your company needs to make a business decision as to whether or not these restrictions (and the accompanying upset users) are acceptable.
Finally, realize that this is not an entirely airtight solution. With root access and perhaps Xposed, it is possible to modify the SafetyNet library to lie to Google's servers, telling them the "right" answers to get a verification pass result that Google signs. In reality, SafetyNet just moves the goalposts and makes it harder for malicious actors. Since these checks ultimately have to run on a device out of your control, it is indeed impossible to design an entirely secure system.
Read an excellent analysis of how the internals of SafetyNet work here.
This Code Get CRC Checksum of Classes.dex file and compare it with provided one.
private void crcTest() throws IOException {
boolean modified = false;
// required dex crc value stored as a text string.
// it could be any invisible layout element
long dexCrc = Long.parseLong(Main.MyContext.getString(R.string.dex_crc));
ZipFile zf = new ZipFile(Main.MyContext.getPackageCodePath());
ZipEntry ze = zf.getEntry("classes.dex");
if ( ze.getCrc() != dexCrc ) {
// dex has been modified
modified = true;
}
else {
// dex not tampered with
modified = false;
}
}
We are developing a library which handles authentication for several apps with different signing certificates. This library, exposes several methods to call our server (create users, delete them, update data, etc).
The library also takes care of interacting with the AccountManager to store the credentials, which are username, authtoken and refresh token. When a request is made, it's listening for a "401 Unauthorized - token expired" and, in that case, invalidating the token and getting a new one from the AccountManager (which has obtained a new one from our server by using the stored refresh token).
AccountManager has the following feature. When several account share the same account type, the first installed app's AccountAuthenticator (the class which holds all the logic for the AccountAuthenticator) class is used.
To put some names on it, we got AppA and AuthA (AccountAuthenticator class) signed with certificateA and AppB with AuthB signed with certificateB. Both use accountType "sharedname"
AppA is first installed so AuthA is associated with account type "sharedname". Then AppB is installed, as there is already an AccountAuthenticator already associated with accountType "sharedname", AuthA will be used when calling AccountManager from AppB.
Since API 22, some methods from the AccountManager throw a SecurityException when being called from an app signed with a different certificate than the used AccountAuthenticator.
Here goes the problem (finally!):
We got a method createUser in our library that creates the user in the server and, if success, inserts the new account in the AccountManager. When calling createUser from AppB, a call to `AccountManager.get(mContext).addAccountExplicity(...)' is done and it produces a SecurityException as the used authenticator is AuthA.
AddAccount is not an option (as far as I know) because createUser is being called from the login activity of the different apps.
¿How can we add an account to the AccountManager from AppB without using an Activity?
We got the same problems to remove the account with AccountManager.get(mContext).removeAccount(...);
I hope it is clear, let me know if you need a better explanation, which is possible.
Thanks!
Using the Mobile Backend Starter (MBS) Android classes (those distributed as a sample project when creating a new project in Google Dev Console and demoed at Google I/O 2013) I'm able to insert new entities to the cloud datastore via calls to CloudBackendMessaging.insertAll or .updateAll. The latter will create entities if none exist so seems functionally identical to insert for new records.
The insertion/creation works fine. However when I attempt to update existing entries in the datastore, I received permissions errors e.g. (from the backend log)
Method: mobilebackend.endpointV1.updateAll
Error Code: 401
Reason: required
Message: Insuffient permission for updating a CloudEntity: XXXXXX by: USER: YYYYYYY
which results in a matching access error in the logcat client side.
In all cases I am using Secured access authenticating with a valid Google account (my own).
The entities being inserted are thus showing as "owned" by my user ID with "updated by" and "created by" showing my Google account's email address.
However when the update of the existing record is made, using exactly the same CloudBackendMessenger object and thus same credentials etc. the backend is telling me I can't update due to permissions issues. But surely if I just made the entity with the same credentials this can't be correct? Looking at the documentation it appears that I should be able to edit entities owned by the same user ID in all cases (regardless of the KindName and whether it is prepended [public], [private] or nothing).
Can anyone who has received permissions errors on UPDATES via Mobile Backend Starter for Datascore please shed any light? I have been banging my head over this for most of today.
I've faced the similar error "Insuffient permission for updating a CloudEntity" when using cloudBackendAsync.update(cloudEntity). I resolved it by making sure the cloudEntity has it's createdAt field set. createdAt is autogenerated and I think I am not supposed to touch it. But it worked for me. In my case I am first obtaining list of cloud entities. This is when I get createdAt field of cloud entities. Then when I am updating I setting the createdAt field from previously obtained entities.
Edit: Had to do similar thing for owner field also.
Similar to one of the comments above, I successfully got around this by getting the original CloudEntity before doing the insert/update/delete function.
CloudQuery cq = new CloudQuery("datastoretype");
cq.setLimit(1);
cq.setFilter(Filter.eq("_id",id));
cloudEntity.setId(id);
mProcessingFragment.getCloudBackend().get(cloudEntity, handler);
Thereafter it was trivial to do the following:
mProcessingFragment.getCloudBackend().update(cloudEntity, handler);
The docs definitely ought to be more clear on this, whether it is a strict requirement or bug.
The answers posted so far work around the problem if you don't mind all users being able to access the entity you are trying to update. However, a better solution that retains the access permissions is detailed by google here - https://cloud.google.com/cloud/samples/mbs/authentication
If you want to pass the user’s Google Account info to the backend on
each call, use the CloudBackend#setCredential() method (also available
on the subclasses, CloudBackendAsync and CloudBackendMessaging) to set
a GoogleAccountCredential object before calling any Mobile Backend
methods.
GoogleAccountCredential credential = GoogleAccountCredential.usingAudience(this, "<Web Client ID>");
credential.setSelectedAccountName("<Google Account Name>");
cloudBackend.setCredential(credential);
Setting credientials enables the client to operate when the backend is
in “Secured by Client ID” mode and also sets createdBy/updatedBy/owner
properties of CloudEntity automatically.