Implement Encrypt and Decrypt in KeyStore [+more KeyStore support]
Feature description
Implement encrypt and decrypt in the termux-keystore (currently it only supports signing and verifying) via Cipher
This would enable passcodes, secrets, et cetera to be stored in the Android KeyStore, an example would be for automatic decryption of an rclone config file without storing the password in a text file (e.g. encrypted by gpg) using rclone's --password-command, and would enable easy integration with the FingerprintAPI/Biometric Authentication, which would resolve #246 and would also be more convenient than a passphrase or using pass (possibly more secure)
Reference implementation
- Using the KeyGenParameterSpec.Builder with PURPOSE_ENCRYPT | PURPOSE_DECRYPT, as well as examples of encrypting and decrypting a text with Cipher (this example would need to store the IV)
-
LokileCrypt is an implemented example of Android KeyStore supporting encryption/decryption, it merges the encrypted data and a random IV header, as already supported with
cipher.getIV()which may be preferable so the IV is not stored separately. termux-keystore can also set a constant IV usingIVParameterSpecbut not preferable or derived from the alias, secret, such as what rclone does - How to get key from keystore on successful fingerprint auth
- Android Fingerprint API Encryption and Decryption
- [x] Implement Cipher Encrypt/Decrypt
- [x] Support Encrypt/Decrypt purposes, Termux currently uses 12 for sign+verify
- [x] Update to API level 30 by replacing
setUserAuthenticationValidityDurationSecondswithsetUserAuthenticationParameters - [x] Support non-Biometric
AUTH_DEVICE_CREDENTIAL(see above) - [x] Credential/Biometric Prompt if key locked
- [x] Option to keep key validated if Biometric enrollment changed using
setInvalidatedByBiometricEnrollment - [x] Random IV header constructor and reader
I will contribute to this as much as I can; however, I am not an Android Engineer nor do I usually use Java, @agnostic-apollo would you be able to assist/refactor my code? As well so it fits nicely into KeystoreAPI Thanks
I am not an Android Engineer nor do I usually use Java
Directly working on encryption seems like a great way to start learning! :D
It's been a while since I looked/used keystore apis, so it will require me to research things as well. Encryption stuff needs to be handled with care and proper knowledge. It would also be good to have an external review as well from a good encryption apps dev as well, since its not my domain currently.
But yes, I can try to assist. Any refactoring will have to be done after next termux-app release, since that's a priority right now.
You can open a pull request and we can see.
Thanks
Directly working on encryption seems like a great way to start learning! :D
Exactly what I've been up to ;)
@agnostic-apollo would you happen to remember if you had issues with using the "AndroidKeyStore" PROVIDER such as in the key .getInstance calls https://github.com/termux/termux-api/blob/6e80b07f66a6b959a8b92e91daaf52ac30b371c0/app/src/main/java/com/termux/api/apis/KeystoreAPI.java#L227
I keep getting java.security.NoSuchProviderException: no such provider: AndroidKeyStore apparently this happens with unit tests (regardless of using Roboletric, such as in my case using termux with ecj and dx).
I have tried so many different ways of circumventing it, that has been the better part of 2 days, and I cannot get a security provider implemented via Termux and ECJ, I've tried using BouncyCastle, custom providers, using GCMParameterSpec or IVParameterSpec directly, and all I can find for solutions to these online is to use the provider as "AndroidKeyStore", looping back to the same problem. I cannot run the KeystoreAPI.java generateKey either (removing Termux calls) for the same reason.
For now, I am just using anonymous keys without an alias using Javax Crypto, as Android KeyStore uses as a backend (analogues to Java Security for symmetric encryption it seems), just to test the encryption/decryption ciphers, but in order to use Android's Keystore API for Termux, Biometric Authentication, et cetera for integration with Android, I need to resolve this Provider issue
What is/was your development environment? If I can get the provider working I'm quite confident in getting this implemented
termux-api app doesn't have any unit tests so that can't be the reason.
Apps should be built with gradle. Are you building on mobile? Don't have access to pc? I use Android Studio mainly.
For building in termux, check https://github.com/termux/termux-packages/pull/7227#issuecomment-893022283. Some github release urls are inactive now.
Some reference implementation I used many years ago is at
https://github.com/agnostic-apollo/FTP/blob/master/app/src/main/java/com/allonsy/android/ftp/KeyHelper.java
https://github.com/agnostic-apollo/FTP/blob/master/app/src/main/java/com/allonsy/android/ftp/FTPLab.java
I have a PC, just wanted access to my phone's hardware-backed keystore for testing. Again, not familiar with Android development so not sure if it's possible to do this in Android Studio over ADB, although this isn't mandatory, I just thought it would be easier to test e.g. Biometric Authentication, I don't know how these are handled in Studio. Same reason I've not used gradle as I rarely use it and just used ECJ in Termux, but I assume there's a reason Android developers use the tools they do :-) So will migrate the project over
Your KeyHelper code is similar to the other implementations of e.g. AES that I've been seeing, and in the Documentation, I just have a problem getting the AndroidKeyStore provider working as you have here
Play button in android studio will build and install the app on your phone, then you can test however you like.
Code will be similar since it would have been copied and modified a bit.
termux-apiapp doesn't have any unit tests so that can't be the reason.
BTW by "unit testing" I meant a stand-alone Java code that I built in ECJ on Termux, I took the generateKey method, removed the Termux references, et cetera, just to see if I could use the same Java and Android Keystore API calls, as termux-keystore works, but referencing those libraries in a stand-alone code seems to be an issue
Play button in android studio will build and install the app on your phone, then you can test however you like.
Sounds good, but I should rebuild Termux-API and Termux, right? So they are signed by the same source?
Without more info on code content and design and how you building and running, can't comment. Any ECJ issues is not something I have time to deal with anyway.
If you are using Github action builds or releases, all apps will have same key as local debug builds sourced from dev_keystore.jks.
I was using the FDroid builds, but I've switched over and got everything running via Github builds in Android Studio, tested and working
Just a quick question, I'd prefer the "Apply Code Changes" in Android Studio to rebuilding every time, but I get
Error Changes were not applied. Class not found: com.termux.api.apis.KeystoreAPI$1 Reinstall and restart app
From what I read online, this is a problem with older Gradle versions, but updating to 7.2.2 does not resolve it. Any ideas off the top of your head?
Thank you so much for your help, that should be the last of my questions and now I can implement this
Click app drop down next to play button -> Edit Configuration -> Enable Always install with package manager
Welcome.
Have to exit Termux and have Termux:API open while doing it for it to work, assuming it's a conflict
@agnostic-apollo this is nearly done, I am just trying to support an option to store the encrypted data in SharedPreferences but am not familiar with the whole Context part of Android, for example in FTPLab it seems that it is passed to the constructor, but from where?
Let's say given this snippet
public class KeystoreAPI {
public static void onReceive(TermuxApiReceiver apiReceiver, Intent intent) {
switch (intent.getStringExtra("command")) {
....
case "encrypt":
encryptData(apiReceiver, intent);
break;
}
}
...
public static void encryptData(TermuxApiReceiver apiReceiver, final Intent intent) {
ResultReturner.returnData(apiReceiver, intent, new WithInput() {
@Override
public void writeResult(PrintWriter out) throws ...{
...
encryptedData = ...
}
}
}
}
Would it make the most sense to:
- Call some version of
import com.termux.shared.termux.settings.preferences.*;asTermuxAPIApplication.javadoes
or
- Include context in
onReceivecall as UsbAPI.java does https://github.com/termux/termux-api/blob/ba2836ba60083b54dcdb6a3a926777f3f0704829/app/src/main/java/com/termux/api/apis/UsbAPI.java#L33 send it toencryptData(apiReceiver, context, intent);and received aspublic static void encryptData(TermuxApiReceiver apiReceiver, final Context context, final Intent intent) {, do I also need to add it intoResultReturner.returnData(...)?
Then I should be able to ??
encryptedData = ...
SharedPreferences sharedPref = context.getPreferences(Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPref.edit();
editor.putString("FooPrefName", encryptedData);
editor.apply();
...
for example in FTPLab it seems that it is passed to the constructor, but from where?
Different places like https://github.com/agnostic-apollo/FTP/blob/068f9d57247cc70609d612bd6272fcc916b5ffd4/app/src/main/java/com/allonsy/android/ftp/FTPListFragment.java#L170
- Call some version of
import com.termux.shared.termux.settings.preferences.*;asTermuxAPIApplication.javadoes
Yes, you must use it. Rebase against master branch for 9e72088 and then you should be able to do something like following. The last true is for commitToFile immediately, some phones have issues with saving shared preferences so it must be done, otherwise will lose data. The key name would likely vary if clients want to store different data under different keys, but should prefix them with keystore_api__encrypted_data__<client_key>. Make sure client key is not empty, and possibly matches ^[a-zA-Z0-9_-]+$
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context);
if (preferences == null) return; // log and return error
SharedPreferenceUtils.setString(preferences.getSharedPreferences(), key, data, true);
Include context in onReceive
Yeah, do that.
What I did was add context in the onReceive API call https://github.com/termux/termux-api/blob/b296a0c29eea7f9f9f88eb560e53b088605ad575/app/src/main/java/com/termux/api/TermuxApiReceiver.java#L148-L149
case "Keystore":
KeystoreAPI.onReceive(this, **context**, intent);
then
public class KeystoreAPI {
public static void onReceive(TermuxApiReceiver apiReceiver, **Context context**, Intent intent) {
switch (intent.getStringExtra("command")) {
....
case "encrypt":
encryptData(apiReceiver, **context**, intent);
break;
}
}
...
public static void encryptData(TermuxApiReceiver apiReceiver, **final Context context**, final Intent intent) {
ResultReturner.returnData(apiReceiver, intent, new WithInput() {
@Override
public void writeResult(PrintWriter out) throws ...{
...
encryptedData = ...
SharedPreferences.Editor editor = context.getSharedPreferences("foo", Context.MODE_PRIVATE).edit();
editor.putString("foobar", encryptedData);
editor.apply()
}
}
}
}
Is this the best way? This is one thing I am not familiar with so feedback would be appreciated
Edit: You responded while I was typing, will check haha
Okay, so given what you posted, all is the same except
encryptedData = ...
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context);
SharedPreferenceUtils.setString(preferences.getSharedPreferences(),
"keystore_api__encrypted_data__"+userSuppliedFoo,
encryptedData,
true);
And for decryption
TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context);
toBeDecrypted = SharedPreferenceUtils.getString(preferences.getSharedPreferences(),
"keystore_api__encrypted_data__"+userSuppliedFoo,
null,
true);
It works, so long as this seems proper
Looks good, other than static constant. Also make sure to use space between + and items as per convention.
public static final String KEYSTORE_API__ENCRYPTED_DATA_PREFERENCES_SCOPE = "keystore_api__encrypted_data__";
...
KEYSTORE_API__ENCRYPTED_DATA_PREFERENCES_SCOPE + userSuppliedFoo,
Code is available
It would also be good to have an external review as well from a good encryption apps dev as well
Any suggestions? Some of my references were @lokile, @nelenkov, @yakivmospan, and @WithSecureLabs has a Keystore Audit
@agnostic-apollo I'm trying to prompt for authentication when the key is locked by calling FingerprintAPI.onReceive(context, intent);
From KeystoreAPI but the subsequent code executes regardless of if FingerprintAPI has been finished (such as initializing cipher)
Any idea how to wait for that receiver to finish before moving on? I've tried creating
Intent fingerprintIntent = new Intent(context, FingerprintAPI.FingerprintActivity.class);
fingerprintIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(fingerprintIntent);
with the same problems
Not sure what the easiest way to do this is - create a new receiver? Add activity to manifest? Or do I have to edit FingerprintAPI https://github.com/termux/termux-api/blob/9e72088e8b1112deae67992f1f94029a58a7445d/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L81
You would need to start FingerprintActivity and send it a PendingIntent or special extras so that it sends result back to KeystoreAPI instead of normal behaviour to socket server. Will have to send back result on all error outs and authentication success. Then in KeystoreAPI, do whatever you originally intended to do. That should work.
https://github.com/termux/termux-api/blob/9e72088e8b1112deae67992f1f94029a58a7445d/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L163
https://github.com/termux/termux-api/blob/9e72088e8b1112deae67992f1f94029a58a7445d/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L83
I've been trying things for the past 2 days on this and am not able to get the KeystoreAPI to wait, I've been searching a lot online but to no avail, I think I am understanding PendingIntent wrong. Here are some methods I tried:
Not waiting:
Intent fingerprintIntent = new Intent(context, FingerprintAPI.FingerprintActivity.class);
fingerprintIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, fingerprintIntent, 0);
pendingIntent.send();
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
pendingIntent.send();
- Using any form of
context.startActivity(...) - Calling
FingerprintAPI.onReceive(...)will wait up until it calls .startActivity(...) thenKeystoreAPIcontinues, obviously. Though, this doesn't matter because oncebiometricPrompt.authenticate(...)is called it seems that a new thread is created anyway (this is why I thought you mentioned I need to have the callback in everypublic void onAuthentication...(...)in thenew BiometricPrompt.AuthenticationCallback(){...}) https://github.com/termux/termux-api/blob/9e72088e8b1112deae67992f1f94029a58a7445d/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L164
The way I understand Pending Intents is that I initialize the request code and then in the onAuthentication...(...) methods I would do the pendingIntent.send(requestCode), which would start something back in KeystoreAPI. My issue is 1) How to send the Pending Intent, 2) How to "wait" for the request code. of course I could just add in the method arguments of authenticatedWithFingerprint(..., PendingIntent pendingIntent) and then call that from KeystoreAPI but then I have the issue of sending FragmentActivity context
https://github.com/termux/termux-api/blob/9e72088e8b1112deae67992f1f94029a58a7445d/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L163 "Method addObserver must be called on the main thread", checking online this seems to be a whole lot of other issues
I think I am making this far too complicated. But even once I am able to send PendingIntent, how would KeystoreAPI know to wait for the request code? Wouldn't I need to create a new activity within KeystoreAPI, create a new Intent from that class, wrap it with a PendingIntent pendingIntent = ..., send that pendingIntent to FingerprintAPI (previously mentioned problem), then in the onAuthentication...(...) call that pendingIntent.send()? I don't know if this helps what I'm trying to do as right now I try cipher.init(...), if it catches UserNotAuthenticatedException then it tries the fingerprint and retries the block, I need it to wait before retrying:
int count = 0;
do {
try {
cipher.init(mode, key);
} catch (UserNotAuthenticatedException e) {
if (count < MAX_AUTH_RETRIES) {
// Fingerprint
} else {
Logger.logError(LOG_TAG, String.valueOf(e));
throw e;
}
}
} while (count++ < MAX_AUTH_RETRIES);
So I need it to wait at // Fingerprint, do I need to create a BroadcastReceiver? As I mentioned, biometricPrompt.authenticate(...) seems to create a new thread regardless, which just initiates the continuation on KeystoreAPI before any onAuthentication...(...), so I really do not know how to make KeystoreAPI "wait" or "listen" for the callback
Sorry for the long information, I've been trying numerous things
So the only way so far I can get this to "work" is by adding a new activity to AndroidManifest which is defined in KeystoreAPI as
public static class FooActivity extends FragmentActivity{
@Override
public void onCreate(Bundle savedInstanceState) {
...
}
}
where onCreate will call the cipher action
within KeystoreAPI a mutable (so intent extras are inherited, but hopefully you can give more information on this) PendingIntent wraps an Intent created referencing this FooActivity class, which I include as an extra in the original intent sent to FingerprintAPI.onReceive(...)
Intent fooIntent = new Intent(context, FooActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, fooIntent, 0);
intent.putExtra("pi", pendingIntent);
FingerprintAPI.onReceive(context, intent);
then in the onAuthentication...{...} callback of the BiometricPrompt
PendingIntent pi = (PendingIntent) intent.getParcelableExtra("pi");
pi.send();
which instantiates the FooActivity onCreate
If this seems like the proper/safest way to do it please let me know as it will take a bit of code-editing and before I commit I would like to know if there is a better way, thanks
You would need to start
FingerprintActivityand send it aPendingIntentor special extras so that it sends result back toKeystoreAPIinstead of normal behaviour to socket server. Will have to send back result on all error outs and authentication success. Then inKeystoreAPI, do whatever you originally intended to do. That should work.
@agnostic-apollo sorry if you are just busy, not sure if my replies pinged you
My responses often take days and weeks. I get requests from people everyday and have my own work as well, can't respond immediately to everyone.
In KeystoreAPI to call FingerprintAPI use
public static final String EXTRA_ORIGINAL_INTENT = "original_intent";
public static final String EXTRA_PENDING_INTENT = "pending_intent";
public static final String EXTRA_FINGERPRINT_RESULT = "fingerprint_result";
public static final String EXTRA_RESULT_BUNDLE = "result";
Intent keyStoreAPIIntent = new Intent(context, TermuxApiReceiver.class);
keyStoreAPIIntent.putExtra("api_method", "Keystore");
keyStoreAPIIntent.putExtra(EXTRA_ORIGINAL_INTENT, intent);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, getLastPendingIntentRequestCode(context), keyStoreAPIIntent, PendingIntent.FLAG_ONE_SHOT);
Intent fingerprintAPIIntent = new Intent(context, TermuxApiReceiver.class);
fingerprintAPIIntent.putExtra("api_method", "Fingerprint");
fingerprintAPIIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
// Put additional extras like title, description, etc
context.sendBroadcast(fingerprintAPIIntent);
In FingerprintAPI to send result to KeystoreAPI use
PendingIntent pendingIntent = intent.getParcelableExtra(KeystoreAPI.EXTRA_PENDING_INTENT);
if(pendingIntent != null) {
final Bundle resultBundle = new Bundle();
resultBundle.putSerializable(KeystoreAPI.EXTRA_FINGERPRINT_RESULT, fingerprintResult);
Intent resultIntent = new Intent();
resultIntent.putExtra(KeystoreAPI.EXTRA_RESULT_BUNDLE, resultBundle);
try {
pendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
}
} else {
postFingerprintResult(context, intent, fingerprintResult);
}
To receive result from FingerprintAPI at start of KeystoreAPI use
final Intent originalIntent = intent.getParcelableExtra(EXTRA_ORIGINAL_INTENT);
if (originalIntent != null) {
// Must be response from FingerprintAPI
final Bundle resultBundle = intent.getBundleExtra(EXTRA_RESULT_BUNDLE);
if (resultBundle == null) {
Logger.logError(context, "The intent passed to KeystoreAPI() must contain the result bundle at the " + EXTRA_RESULT_BUNDLE + " key.");
return;
}
// Do whatever and then send result back
ResultReturner.returnData(apiReceiver, originalIntent /* not intent */, out -> {
}
}
For getLastPendingIntentRequestCode(), check https://github.com/termux/termux-app/commit/ac32fbc53d8e91a5b7e53d08c06b936699d53ca2 and https://github.com/termux/termux-tasker/commit/d9a172d759adf080b6d8e05be0b64df7051699c1
Handle this case and any other potential ones in both apis.
https://github.com/termux/termux-api/blob/master/app/src/main/java/com/termux/api/apis/FingerprintAPI.java#L83
For reference, check https://github.com/termux/termux-tasker/blob/d52f84fa6759ba3efbf6d12c91fc1943b78d33a9/app/src/main/java/com/termux/tasker/PluginResultsService.java
I haven't tested this, but should work.
@agnostic-apollo thank you :) I've been trying this, it does work with slight modifications but it has the issue as my previous suggestion that now I'd have to send the cipher over broadcast. I'm assuming it is safe as it's an explicit intent, but I'd prefer not sending the cipher and encrypted/decrypted data over broadcasts or intent extras when the user specifies the -q quiet flag, but that seems unavoidable using these methods as I'd have to send them from before the pending intent and then to the onReceive call
This is why I wanted to try and create a "lock" that would wait until the FingerprintActivity finished. I have figured out a round-about way but I'd like to know if this follows Java/Android standards
**Did something new, see comment below**
from `KeystoreAPI` after the code finds that `cipher.init(...)` returns a `UserNotAuthenticatedException`, I create an `Intent` calling `FingerprintActivity` and start the activity. a `while(fooResult = fooWaiting)` loops until the `onAuthentication...(...)` sets the `KeystoreAPI.fooResult` var to something else. In order to make sure that the while loop doesn't infinitely continue, I call a `timeoutHandler.postDelayed(...)` beforeHere is the logic
public class KeystoreAPI {
protected static final String EXTRA_CALLER_FOO = "FOO_CALLED_BY";
protected static final String AUTH_RESULT_WAITING = "AUTH_RESULT_WAITING";
protected static final String AUTH_RESULT_SUCCESS = "AUTH_RESULT_SUCCESS";
protected static string AUTH_RESULT = AUTH_RESULT_WAITING;
... // Also includes AUTH_RESULT_FAILURE, et cetera...
... // The rest as before
public static void cipherCall(Context context, ...) {
try {
cipher.init(...);
... // Returns after cipher successfully initialized
} catch {UserNotAuthenticatedException e) {
Intent fingerprintIntent = new Intent(context, FingerprintAPI.FingerprintActivity.class);
fingerprintIntent.putExtra(EXTRA_CALLER_FOO, "IveBeenCalledByKeystore");
... // Add other extras
fingerprintIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(fingerprintIntent);
final Handler timeoutHandler = new Handler(Looper.getMainLooper());
timeoutHandler.postDelayed(() -> {
if (AUTH_RESULT.equals(AUTH_RESULT_WAITING)) AUTH_RESULT = AUTH_RESULT_FAILURE;
}, FingerprintAPI.SENSOR_TIMEOUT);
while (AUTH_RESULT.equals(AUTH_RESULT_WAITING) ;
... // Continues after Biometric Prompt or Handler timeout changes AUTH_RESULT
// If AUTH_RESULT is successful then retries cipher.init(...) and returns
}
}
}
then in FingerprintActivity the BiometricPrompt.AuthenticationCallback() {...}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
if (intent.hasExtra(KeystoreAPI.EXTRA_CALLER_FOO)) KeystoreAPI.AUTH_RESULT = KeystoreAPI.AUTH_RESULT_SUCCESS;
else {
... // The rest as before
}
}
And of course handling the other callbacks
Is this safe? I think the only potential attack is another application could change KeystoreAPI.AUTH_RESULT, which at worst would either fail the cipher call or try to call the cipher, which would not be successful if fingerprint wasn't anyway. I think it is better than sending ciphers and sensitive data over intent extras, and there is minimal rewriting of existing code
My concern is if it is OK programming practice to change global variables of external classes in Java, or if running a busy-wait loop is OK as long as I include a timer (Android would timeout after a few seconds anyway) since it is for a short time
Edit: From reading, it seems I should make protected static string AUTH_RESULT a volatile, if you could confirm
Thanks
Okay, I have found a way to avoid external global variable setting and busy-wait loops
Essentially, I had a context.wait() after calling the fingerprintIntent, which then calls pendingIntent.send(...) and in KeystoreAPI onReceive I call the context.notifyAll()
This seems like a good compromise, but looking for your input
public class KeystoreAPI {
...
public static void onReceive(...) {
if (intent.hasExtra("FOO_RESULT")) {
synchronized(context) { context.notifyAll(); }
}
...
}
...
public static void cipherCall(Context context, ...) {
try {
cipher.init(...);
... // Returns after cipher successfully initialized
} catch {UserNotAuthenticatedException e) {
Intent keystoreAPIIntent = new Intent(context, TermuxApiReceiver.class);
keystoreAPIIntent.putExtra("api_method", "Keystore");
keystoreAPIIntent.putExtra(EXTRA_ORIGINAL_INTENT, intent);
PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
getLastPendingIntentRequestCode(context),
keystoreAPIIntent,
PendingIntent.FLAG_ONE_SHOT);
Intent fingerprintAPIIntent = new Intent(context, FingerprintAPI.FingerprintActivity.class);
fingerprintAPIIntent.putExtra(EXTRA_PENDING_INTENT, pendingIntent);
... // Add other extras
fingerprintIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(fingerprintIntent);
synchronized (context) {
try {
context.wait();
} catch (InterruptedException otherE) {
// Error handling
}
}
... // Continues after BiometricPrompt calls KeystoreAPI which notifies .wait() then retries cipher.init
}
}
}
Then in `FingerprintActivity` the `BiometricPrompt.AuthenticationCallback() {...}`
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
if (intent.hasExtra(KeystoreAPI.EXTRA_PENDING_INTENT)) {
PendingIntent pendingIntent = intent.getParcelableExtra(KeystoreAPI.EXTRA_PENDING_INTENT);
Intent resultIntent = new Intent();
resultIntent.putExtra("FOO_RESULT", fooResult);
try {
pendingIntent.send(context, Activity.RESULT_OK, resultIntent);
} catch (PendingIntent.CanceledException e) {
// The caller doesn't want the result? That's fine, just ignore
}
} else {
postFingerprintResult(...);
}
}
Then, finally, `getLastPendingIntentRequestCode` is
public static int getLastPendingIntentRequestCode(final Context context) {
if (context == null) return 0;
SharedPreferences preferences = SharedPreferenceUtils.getPrivateSharedPreferences(context,
TermuxConstants.TERMUX_API_DEFAULT_PREFERENCES_FILE_BASENAME_WITHOUT_EXTENSION);
if (preferences == null) return 0;
int lastPendingIntentRequestCode = SharedPreferenceUtils.getInt(preferences,
"last_pending_intent_request_code", 0);
int nextPendingIntentRequestCode = lastPendingIntentRequestCode + 1;
if (nextPendingIntentRequestCode == Integer.MAX_VALUE || nextPendingIntentRequestCode < 0) {
nextPendingIntentRequestCode = 0;
}
SharedPreferenceUtils.setInt(preferences, "last_pending_intent_request_code",
nextPendingIntentRequestCode, false);
return nextPendingIntentRequestCode;
}
from PluginUtils and TermuxTaskerAppSharedPreferences, I will add the analogous TermuxTaskerAppSharedPreferences to TermuxAPIAppSharedPreferences instead, with similar logic and adding values to termux-shared
Why do you need to wait at all? The shell command will keep on waiting if ResultSender is not called anyways. Once PendingIntent is sent after authentication, and KeystoreApi receives second intent, it will continue on like it were processing the first intent. A postDelayed() itself is a good idea to timeout in case user doesn't authenticate within a certain time or something went wrong and PendingIntent is not received back. The SENSOR_TIMEOUT could default to like 60s but should be configurable from command line where no timeout if value is 0 in case command was started from background and user wasn't active on the device to authenticate in time, although not sure what happens if display is off and fingerprint authentication is started.
Why do you need to wait at all? The shell command will keep on waiting if
ResultSenderis not called anyways.
Mainly because of the logic that follows before creating and sending the PendingIntent, I have to first create the cipher, try to initialize it, and only if it catches a UserNotAuthenticatedException then it will call FingerprintActivity, wait for it to finish, then retry the cipher initialization loop n times until either a successful cipher initialization (if authorization was successful), or n times did not work
So, if I refactor the code to include everything in the onReceive, it means I would need to send the cipher as an intent extra which onReceive would then call a function to try and cipher.init(...), otherwise I need to re-create the cipher again. The way I sent in the last comment hangs the thread until a BiometricCallback and then continues as normal, so the cipher, sensitive data, et cetera are still in the initial thread, less things having to be sent around between threads and keeps all data and crypto objects in the initial thread
I hope that makes sense, if you agree?
The SENSOR_TIMEOUT could default to like 60s but should be configurable from command line where no timeout if value is 0 in case command was started from background and user wasn't active on the device to authenticate in time
Currently I was using FingerprintAPI.SENSOR_TIMEOUT (=10s), but with the method I sent in the last comment, a postDelayed() is not necessary, although I may want to add a time to the context.wait(fooTimeout) lock
According to the documentation the BiometricPrompt times out on its own ~30s
not sure what happens if display is off and fingerprint authentication is started.
Nor am I :) One of the things I was going to test once I get this all working, but here is a tracker
No need to create the cipher again, just send it in PendingIntent and any other required data and skip generation on result intent. Explicit intents are secure and there will be minimal overhead for putting and sending an extra. The intent and arg limits would also apply though.
https://github.com/termux/termux-tasker#arguments-and-result-data-limits
https://www.reddit.com/r/tasker/comments/prro8t/autoshare_crashed_when_i_pasted_the_file_path/
BiometricPrompt times out on its own ~30s
I see but there should be some extra seconds for intents overhead.
There would also be issue of multiple commands requesting Fingerprint authentication at a given time, so some kind of queuing would be required in FingerprintApi, not in KeystoreApi and so timeouts would need to consider that too. Timer could technically be started in queue entry, so that time used by other earlier requests is ignored.
Nor am I :) One of the things I was going to test once I get this all working, but here is a tracker
Cool. I guess that will need to be handled as well as the comment or wait till screen is unlocked.
No need to create the cipher again, just send it in PendingIntent and any other required data and skip generation on result intent. Explicit intents are secure and there will be minimal overhead for putting and sending an extra. The intent and arg limits would also apply though.
Couldn't this be avoided by using the context.wait() and context.notifyAll() though? Or is there a problem with that method? I would also have to send the input data and other input args over intent extras, which is a bit, I'd like to avoid that if unnecessary