termux-api icon indicating copy to clipboard operation
termux-api copied to clipboard

Implement Encrypt and Decrypt in KeyStore [+more KeyStore support]

Open EduardDurech opened this issue 3 years ago • 42 comments

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

  1. 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)
  2. 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 using IVParameterSpec but not preferable or derived from the alias, secret, such as what rclone does
  3. How to get key from keystore on successful fingerprint auth
  4. Android Fingerprint API Encryption and Decryption

EduardDurech avatar Sep 06 '22 09:09 EduardDurech

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

EduardDurech avatar Sep 06 '22 09:09 EduardDurech

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

agnostic-apollo avatar Sep 06 '22 21:09 agnostic-apollo

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

EduardDurech avatar Sep 12 '22 18:09 EduardDurech

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

agnostic-apollo avatar Sep 12 '22 19:09 agnostic-apollo

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

KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, AndroidKeyStore);

EduardDurech avatar Sep 12 '22 19:09 EduardDurech

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.

agnostic-apollo avatar Sep 12 '22 19:09 agnostic-apollo

termux-api app 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?

EduardDurech avatar Sep 12 '22 19:09 EduardDurech

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.

agnostic-apollo avatar Sep 12 '22 22:09 agnostic-apollo

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

EduardDurech avatar Sep 13 '22 01:09 EduardDurech

Click app drop down next to play button -> Edit Configuration -> Enable Always install with package manager

Welcome.

agnostic-apollo avatar Sep 13 '22 01:09 agnostic-apollo

Have to exit Termux and have Termux:API open while doing it for it to work, assuming it's a conflict

EduardDurech avatar Sep 13 '22 03:09 EduardDurech

@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:

or

  • Include context in onReceive call 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 to encryptData(apiReceiver, context, intent); and received as public static void encryptData(TermuxApiReceiver apiReceiver, final Context context, final Intent intent) {, do I also need to add it into ResultReturner.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();
                ...

EduardDurech avatar Sep 28 '22 20:09 EduardDurech

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

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.

agnostic-apollo avatar Sep 28 '22 22:09 agnostic-apollo

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

EduardDurech avatar Sep 28 '22 22:09 EduardDurech

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

EduardDurech avatar Sep 28 '22 22:09 EduardDurech

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,

agnostic-apollo avatar Sep 28 '22 23:09 agnostic-apollo

Code is available

EduardDurech avatar Sep 29 '22 18:09 EduardDurech

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

EduardDurech avatar Sep 29 '22 18:09 EduardDurech

@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

EduardDurech avatar Sep 30 '22 14:09 EduardDurech

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

agnostic-apollo avatar Sep 30 '22 21:09 agnostic-apollo

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(...) then KeystoreAPI continues, obviously. Though, this doesn't matter because once biometricPrompt.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 every public void onAuthentication...(...) in the new 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

EduardDurech avatar Oct 03 '22 08:10 EduardDurech

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

EduardDurech avatar Oct 04 '22 12:10 EduardDurech

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.

@agnostic-apollo sorry if you are just busy, not sure if my replies pinged you

EduardDurech avatar Oct 05 '22 17:10 EduardDurech

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 avatar Oct 06 '22 03:10 agnostic-apollo

@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(...)` before

Here 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

EduardDurech avatar Oct 12 '22 10:10 EduardDurech

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

EduardDurech avatar Oct 12 '22 12:10 EduardDurech

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.

agnostic-apollo avatar Oct 12 '22 13:10 agnostic-apollo

Why do you need to wait at all? The shell command will keep on waiting if ResultSender is 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

EduardDurech avatar Oct 12 '22 13:10 EduardDurech

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.

agnostic-apollo avatar Oct 12 '22 14:10 agnostic-apollo

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

EduardDurech avatar Oct 12 '22 14:10 EduardDurech