From 40f51ba08a4470f8c46aec294043a524f701f44c Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Thu, 29 Sep 2022 19:12:13 +0200 Subject: [PATCH 1/8] Supply context to KeyStoreAPI --- app/src/main/java/com/termux/api/TermuxApiReceiver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/termux/api/TermuxApiReceiver.java b/app/src/main/java/com/termux/api/TermuxApiReceiver.java index 6efaafe16..95f016687 100644 --- a/app/src/main/java/com/termux/api/TermuxApiReceiver.java +++ b/app/src/main/java/com/termux/api/TermuxApiReceiver.java @@ -146,7 +146,7 @@ private void doWork(Context context, Intent intent) { JobSchedulerAPI.onReceive(this, context, intent); break; case "Keystore": - KeystoreAPI.onReceive(this, intent); + KeystoreAPI.onReceive(this, context, intent); break; case "Location": if (TermuxApiPermissionActivity.checkAndRequestPermissions(context, intent, Manifest.permission.ACCESS_FINE_LOCATION)) { From 3263d3ce013bf4b6bb081ecb9b35f5d2210d9a80 Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Thu, 29 Sep 2022 19:54:21 +0200 Subject: [PATCH 2/8] Encrypt/Decrypt + more Keystore Support + Receiver receives context, sent to list, delete, encrypt, and decrypt for shared preferences + listKeys is now listData and supports showing secret keys and preferences + printKey supports secret keys and more KeyInfo parameters + deleteKey is now deleteData and supports deleting preferences + deleteData deletes all key-associated preferences upon deletion + generateKey supports more key parameters (mode, padding, purposes, unlocked, invalidate, auths) + generateKey supports secret keys + generateKey refactored + encryptData added + encryptData supports Keystore Ciphers + encryptData supports reading from path or stdin + encryptData supports writing to shared preferences or stdout + encryptData writes output in the form [IV.length][IV][Encrypted Data], if IV.length is 0 then IV omitted + encryptedData never exposes data to Strings, stays as byte arrays and is flushed with zeroes after use + encryptedData supports a quiet flag so encrypted data does not show in stdout + encryptedData supports multiple shared preferences stored as a JSON with a key, value pair + encryptedData encodes output to Base64 + decryptData added + decryptData supports Keystore Ciphers + decryptData supports reading from path, shared preferences, or stdin + decryptData supports writing to stdout + decryptData reads output in the form [IV.length][IV][Encrypted Data], if IV.length is 0 then IV omitted + decryptedData never exposes data to Strings, stays as byte arrays and is flushed with zeroes after use + decryptedData supports a quiet flag so decrypted data does not show in stdout + decryptedData supports reading from JSON shared preferences with a key, value pair + decryptedData encodes output to Base64 + decomposeBinary (for purposes and authorizations) + getPrefsJSON and setPrefJSON (preferences as JSON) + getIVSpec (support different AlgorithmParameterSpecs) + getKey (get Public or Secret key for encryption, Private or Secret key for decryption) + readFile (also supports Android < 8.0) + Replaced ECGenParameterSpec and RSAKeyGenParameterSpec with AlgorithmParameterSpec - Removed unnecessary imports and casts --- .../java/com/termux/api/apis/KeystoreAPI.java | 524 +++++++++++++++--- 1 file changed, 444 insertions(+), 80 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java index ffe008dd4..7205f310f 100644 --- a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java +++ b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java @@ -1,41 +1,65 @@ package com.termux.api.apis; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Build; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; -import androidx.annotation.RequiresApi; import android.util.Base64; import android.util.JsonWriter; +import androidx.annotation.RequiresApi; + import com.termux.api.TermuxApiReceiver; import com.termux.api.util.ResultReturner; import com.termux.api.util.ResultReturner.ResultJsonWriter; import com.termux.api.util.ResultReturner.WithInput; import com.termux.shared.logger.Logger; +import com.termux.shared.settings.preferences.SharedPreferenceUtils; +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; + +import org.json.JSONException; +import org.json.JSONObject; +import java.io.BufferedInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Paths; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyFactory; import java.security.KeyPairGenerator; import java.security.KeyStore; import java.security.KeyStore.Entry; import java.security.KeyStore.PrivateKeyEntry; +import java.security.KeyStore.SecretKeyEntry; import java.security.PrivateKey; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; -import java.security.spec.ECGenParameterSpec; -import java.security.spec.RSAKeyGenParameterSpec; +import java.security.spec.AlgorithmParameterSpec; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.GCMParameterSpec; +import javax.crypto.spec.IvParameterSpec; + +import java.util.ArrayList; +import java.util.Arrays; import java.util.Enumeration; +import java.util.Iterator; +import java.util.stream.Collectors; public class KeystoreAPI { @@ -43,20 +67,21 @@ public class KeystoreAPI { // this is the only provider name that is supported by Android private static final String PROVIDER = "AndroidKeyStore"; + private static final String PREFERENCES_PREFIX = "keystore_api__encrypted_data"; @SuppressLint("NewApi") - public static void onReceive(TermuxApiReceiver apiReceiver, Intent intent) { + public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); switch (intent.getStringExtra("command")) { case "list": - listKeys(apiReceiver, intent); + listData(apiReceiver, context, intent); break; case "generate": generateKey(apiReceiver, intent); break; case "delete": - deleteKey(apiReceiver, intent); + deleteData(apiReceiver, context, intent); break; case "sign": signData(apiReceiver, intent); @@ -64,24 +89,37 @@ public static void onReceive(TermuxApiReceiver apiReceiver, Intent intent) { case "verify": verifyData(apiReceiver, intent); break; + case "encrypt": + encryptData(apiReceiver, context, intent); + break; + case "decrypt": + decryptData(apiReceiver, context, intent); + break; } } /** - * List the keys inside the keystore.
+ * List either the keys inside the keystore or data in shared preferences.
* Optional intent extras: * */ @RequiresApi(api = Build.VERSION_CODES.M) - private static void listKeys(TermuxApiReceiver apiReceiver, final Intent intent) { + private static void listData(TermuxApiReceiver apiReceiver, final Context context, + final Intent intent) { ResultReturner.returnData(apiReceiver, intent, new ResultJsonWriter() { @Override - public void writeJson(JsonWriter out) throws GeneralSecurityException, IOException { + public void writeJson(JsonWriter out) + throws GeneralSecurityException, IOException, JSONException { KeyStore keyStore = getKeyStore(); Enumeration aliases = keyStore.aliases(); - boolean detailed = intent.getBooleanExtra("detailed", false); + boolean detailed = (intent.getIntExtra("detailed", 0) == 1); + boolean pref = (intent.getIntExtra("pref", 0) == 1); out.beginArray(); while (aliases.hasMoreElements()) { @@ -90,9 +128,36 @@ public void writeJson(JsonWriter out) throws GeneralSecurityException, IOExcepti String alias = aliases.nextElement(); out.name("alias").value(alias); - Entry entry = keyStore.getEntry(alias, null); - if (entry instanceof PrivateKeyEntry) { - printPrivateKey(out, (PrivateKeyEntry) entry, detailed); + if (pref) { + JSONObject prefsJSON = getPrefsJSON(context, alias); + Iterator prefKeys =prefsJSON.keys(); + out.name("Preferences"); + out.beginObject(); + while (prefKeys.hasNext()) { + String transientKey = prefKeys.next(); + out.name(transientKey) + .value(detailed ? + prefsJSON.getString(transientKey) : ""); + } + out.endObject(); + } else { + Entry entry = keyStore.getEntry(alias, null); + if (entry instanceof PrivateKeyEntry) { + PrivateKeyEntry privateEntry = (PrivateKeyEntry) entry; + PrivateKey privateKey = privateEntry.getPrivateKey(); + PublicKey publicKey = privateEntry.getCertificate().getPublicKey(); + String algorithm = privateKey.getAlgorithm(); + KeyInfo keyInfo = KeyFactory.getInstance(algorithm) + .getKeySpec(privateKey, KeyInfo.class); + printKey(out, algorithm, keyInfo, detailed, publicKey); + } else if (entry instanceof SecretKeyEntry) { + SecretKeyEntry secretEntry = (SecretKeyEntry) entry; + SecretKey secretKey = secretEntry.getSecretKey(); + String algorithm = secretKey.getAlgorithm(); + KeyInfo keyInfo = (KeyInfo) SecretKeyFactory.getInstance(algorithm) + .getKeySpec(secretKey, KeyInfo.class); + printKey(out, algorithm, keyInfo, detailed, null); + } } out.endObject(); @@ -106,60 +171,101 @@ public void writeJson(JsonWriter out) throws GeneralSecurityException, IOExcepti * Helper function for printing the parameters of a given key. */ @RequiresApi(api = Build.VERSION_CODES.M) - private static void printPrivateKey(JsonWriter out, PrivateKeyEntry entry, boolean detailed) - throws GeneralSecurityException, IOException { - PrivateKey privateKey = entry.getPrivateKey(); - String algorithm = privateKey.getAlgorithm(); - KeyInfo keyInfo = KeyFactory.getInstance(algorithm).getKeySpec(privateKey, KeyInfo.class); - - PublicKey publicKey = entry.getCertificate().getPublicKey(); + private static void printKey(JsonWriter out, String algorithm, KeyInfo keyInfo, + boolean detailed, Key pubKey) throws GeneralSecurityException, IOException { + String mode = String.join(",", keyInfo.getBlockModes()); + String padding = String.join(",", keyInfo.getEncryptionPaddings()); + boolean authRequired = keyInfo.isUserAuthenticationRequired(); + int validityDuration = keyInfo.getUserAuthenticationValidityDurationSeconds(); - out.name("algorithm").value(algorithm); + out.name("algorithm"); + out.beginObject(); + out.name("name").value(algorithm); + if (!mode.isEmpty()) { + out.name("block_mode").value(mode); + out.name("encryption_padding").value(padding); + } + out.endObject(); out.name("size").value(keyInfo.getKeySize()); + out.name("purposes").value(decomposeBinary(keyInfo.getPurposes())); + + out.name("inside_secure_hardware").value(keyInfo.isInsideSecureHardware()); - if (detailed && publicKey instanceof RSAPublicKey) { - RSAPublicKey rsa = (RSAPublicKey) publicKey; + out.name("user_authentication"); + out.beginObject(); + out.name("required").value(authRequired); + out.name("enforced_by_secure_hardware") + .value(keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware()); + if (validityDuration >= 0) out.name("validity_duration_seconds") + .value(validityDuration); + if (detailed && authRequired) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + out.name("authentication_type") + .value(decomposeBinary(keyInfo.getUserAuthenticationType())); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + out.name("invalidated_by_new_biometric") + .value(keyInfo.isInvalidatedByBiometricEnrollment()); + } + } + out.endObject(); + + if (detailed && pubKey instanceof RSAPublicKey) { + RSAPublicKey rsa = (RSAPublicKey) pubKey; // convert to hex out.name("modulus").value(rsa.getModulus().toString(16)); out.name("exponent").value(rsa.getPublicExponent().toString(16)); } - if (detailed && publicKey instanceof ECPublicKey) { - ECPublicKey ec = (ECPublicKey) publicKey; + if (detailed && pubKey instanceof ECPublicKey) { + ECPublicKey ec = (ECPublicKey) pubKey; // convert to hex out.name("x").value(ec.getW().getAffineX().toString(16)); out.name("y").value(ec.getW().getAffineY().toString(16)); } + } - out.name("inside_secure_hardware").value(keyInfo.isInsideSecureHardware()); - - out.name("user_authentication"); - - out.beginObject(); - out.name("required").value(keyInfo.isUserAuthenticationRequired()); - - out.name("enforced_by_secure_hardware"); - out.value(keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware()); - - int validityDuration = keyInfo.getUserAuthenticationValidityDurationSeconds(); - if (validityDuration >= 0) { - out.name("validity_duration_seconds").value(validityDuration); + /** + * Decomposes binary for options (e.g. 3->'1|2'). + */ + private static String decomposeBinary(int binary) { + ArrayList values = new ArrayList<>(); + int power = 0; + while (binary != 0) { + if ((binary & 1) != 0) values.add(1<>= 1; } - out.endObject(); + return values.stream().map(String::valueOf).collect(Collectors.joining("|")); } /** - * Permanently delete a key from the keystore.
+ * Permanently delete a key from the keystore or a specified shared preference.
* Required intent extras: *
    - *
  • alias: key alias
  • + *
  • alias: key alias.
  • + *
  • pref: deletes specified preference of alias instead of key.
  • *
*/ - private static void deleteKey(TermuxApiReceiver apiReceiver, final Intent intent) { + private static void deleteData(TermuxApiReceiver apiReceiver, final Context context, + final Intent intent) { ResultReturner.returnData(apiReceiver, intent, out -> { String alias = intent.getStringExtra("alias"); - // unfortunately this statement does not return anything - // nor does it throw an exception if the alias does not exist - getKeyStore().deleteEntry(alias); + String pref = intent.getStringExtra("pref"); + + if (!pref.equals("-1")) { + JSONObject prefsJSON = getPrefsJSON(context, alias); + prefsJSON.remove(pref); + setPrefsJSON(context, alias, prefsJSON); + } else { + // unfortunately this statement does not return anything + // nor does it throw an exception if the alias does not exist + getKeyStore().deleteEntry(alias); + TermuxAPIAppSharedPreferences.build(context) + .getSharedPreferences() + .edit() + .remove(String.join("__", PREFERENCES_PREFIX, alias)) + .commit(); + } }); } @@ -167,28 +273,53 @@ private static void deleteKey(TermuxApiReceiver apiReceiver, final Intent intent * Create a new key inside the keystore.
* Required intent extras: *
    - *
  • alias: key alias
  • + *
  • alias: key alias.
  • *
  • * algorithm: key algorithm, should be one of the KeyProperties.KEY_ALGORITHM_* * values, for example {@link KeyProperties#KEY_ALGORITHM_RSA} or - * {@link KeyProperties#KEY_ALGORITHM_EC}. + * {@link KeyProperties#KEY_ALGORITHM_AES}. + *
  • + *
  • + * mode: encryption block mode, should be one of the KeyProperties.BLOCK_MODE_* + * values, for example {@link KeyProperties#BLOCK_MODE_GCM} or + * {@link KeyProperties#BLOCK_MODE_CBC}. + *
  • + *
  • + * padding: encryption padding, should be one of the KeyProperties.ENCRYPTION_PADDING_* + * values, for example {@link KeyProperties#ENCRYPTION_PADDING_NONE} or + * {@link KeyProperties#ENCRYPTION_PADDING_PKCS7}. (the full list of supported Cipher + * combinations can be found at + * + * the Android documentation). *
  • + *
  • size: key size.
  • *
  • * purposes: purposes of this key, should be a combination of * KeyProperties.PURPOSE_*, for example 12 for * {@link KeyProperties#PURPOSE_SIGN}+{@link KeyProperties#PURPOSE_VERIFY} + * or 3 for + * {@link KeyProperties#PURPOSE_ENCRYPT}+{@link KeyProperties#PURPOSE_DECRYPT}. *
  • *
  • * digests: set of hashes this key can be used with, should be an array of * KeyProperties.DIGEST_* values, for example - * {@link KeyProperties#DIGEST_SHA256} and {@link KeyProperties#DIGEST_SHA512} + * {@link KeyProperties#DIGEST_SHA256} and {@link KeyProperties#DIGEST_SHA512}. + *
  • + *
  • + * unlocked: set whether key is only valid if device is unlocked. + *
  • + *
  • + * validity: number of seconds where it is allowed to use this key for signing + * after unlocking the device (re-locking and unlocking restarts the timer), if set to + * -1 then key requires authentication for every use. + *
  • + *
  • + * invalidate: set whether new biometric enrollments invalidate the key. *
  • - *
  • size: key size, only used for RSA keys
  • - *
  • curve: elliptic curve name, only used for EC keys
  • *
  • - * userValidity: number of seconds where it is allowed to use this key for signing - * after unlocking the device (re-locking and unlocking restarts the timer), if set to 0 - * this feature is disabled (i.e. the key can be used anytime) + * auth: key authorizations which can enable key access, for example + * {@link KeyProperties#AUTH_DEVICE_CREDENTIAL} and + * {@link KeyProperties#AUTH_BIOMETRIC_STRONG}. *
  • *
*/ @@ -198,35 +329,51 @@ private static void generateKey(TermuxApiReceiver apiReceiver, final Intent inte ResultReturner.returnData(apiReceiver, intent, out -> { String alias = intent.getStringExtra("alias"); String algorithm = intent.getStringExtra("algorithm"); + String mode = intent.getStringExtra("mode"); + String padding = intent.getStringExtra("padding"); + int size = intent.getIntExtra("size", 2048); int purposes = intent.getIntExtra("purposes", 0); String[] digests = intent.getStringArrayExtra("digests"); - int size = intent.getIntExtra("size", 2048); - String curve = intent.getStringExtra("curve"); - int userValidity = intent.getIntExtra("validity", 0); + boolean unlocked = (intent.getIntExtra("unlocked", 1) == 1); + int userValidity = intent.getIntExtra("validity", -1); + boolean invalidate = (intent.getIntExtra("invalidate", 0) == 1); + int authorizations = intent.getIntExtra("auth", 0); KeyGenParameterSpec.Builder builder = - new KeyGenParameterSpec.Builder(alias, purposes); - - builder.setDigests(digests); - if (algorithm.equals(KeyProperties.KEY_ALGORITHM_RSA)) { - // only the exponent 65537 is supported for now - builder.setAlgorithmParameterSpec( - new RSAKeyGenParameterSpec(size, RSAKeyGenParameterSpec.F4)); - builder.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); + new KeyGenParameterSpec.Builder(alias, purposes) + .setKeySize(size) + .setUserAuthenticationRequired((authorizations != 0)) + .setUserAuthenticationValidityDurationSeconds(userValidity); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + builder.setInvalidatedByBiometricEnrollment(invalidate); //Not working? Cannot turn off invalidation (test with keyInfo.isInvalidatedByBiometricEnrollment()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setUnlockedDeviceRequired(unlocked); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + builder.setUserAuthenticationParameters(userValidity, authorizations); + } + } } + if (!mode.equals("-1")) builder.setBlockModes(mode).setEncryptionPaddings(padding); - if (algorithm.equals(KeyProperties.KEY_ALGORITHM_EC)) { - builder.setAlgorithmParameterSpec(new ECGenParameterSpec(curve)); + if (mode.equals(KeyProperties.BLOCK_MODE_ECB) && + (padding.equals(KeyProperties.ENCRYPTION_PADDING_NONE) || + padding.equals(KeyProperties.ENCRYPTION_PADDING_PKCS7))) { + builder.setRandomizedEncryptionRequired(false); } - if (userValidity > 0) { - builder.setUserAuthenticationRequired(true); - builder.setUserAuthenticationValidityDurationSeconds(userValidity); + if (algorithm.equals(KeyProperties.KEY_ALGORITHM_AES)) { + KeyGenerator generator = KeyGenerator.getInstance(algorithm, PROVIDER); + generator.init(builder.build()); + generator.generateKey(); + } else { + builder.setDigests(digests); + if (algorithm.equals(KeyProperties.KEY_ALGORITHM_RSA)) { + builder.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); + } + KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, PROVIDER); + generator.initialize(builder.build()); + generator.generateKeyPair(); } - - KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, PROVIDER); - generator.initialize(builder.build()); - generator.generateKeyPair(); }); } @@ -235,12 +382,12 @@ private static void generateKey(TermuxApiReceiver apiReceiver, final Intent inte * The output is encoded using base64.
* Required intent extras: *
    - *
  • alias: key alias
  • + *
  • alias: key alias.
  • *
  • * algorithm: key algorithm and hash combination to use, e.g. SHA512withRSA * (the full list can be found at * - * the Android documentation) + * the Android documentation). *
  • *
*/ @@ -252,7 +399,8 @@ public void writeResult(PrintWriter out) throws Exception { String algorithm = intent.getStringExtra("algorithm"); byte[] input = readStream(in); - PrivateKeyEntry key = (PrivateKeyEntry) getKeyStore().getEntry(alias, null); + PrivateKeyEntry key = (PrivateKeyEntry) getKeyStore() + .getEntry(alias, null); Signature signature = Signature.getInstance(algorithm); signature.initSign(key.getPrivateKey()); signature.update(input); @@ -270,14 +418,14 @@ public void writeResult(PrintWriter out) throws Exception { * The file is read from stdin, and a "true" or "false" message is printed to the stdout.
* Required intent extras: *
    - *
  • alias: key alias
  • + *
  • alias: key alias.
  • *
  • * algorithm: key algorithm and hash combination that was used to create this signature, * e.g. SHA512withRSA (the full list can be found at * - * the Android documentation) + * the Android documentation). *
  • - *
  • signature: path of the signature file
  • + *
  • signature: path of the signature file.
  • *
*/ private static void verifyData(TermuxApiReceiver apiReceiver, final Intent intent) { @@ -303,6 +451,207 @@ public void writeResult(PrintWriter out) throws GeneralSecurityException, IOExce }); } + /** + * Encrypt a given byte stream. + * The data is read from a file or stdin (in that precedence), and the encrypted data is + * encoded using base64 then output to stdout and/or shared preferences with a given name. + * Output is of the form [IV.length][IV][Encrypted Data] (IV omitted if IV.length is 0).
+ * Required intent extras: + *
    + *
  • alias: key alias.
  • + *
  • + * algorithm: key algorithm, should be of the form 'ALG/MODE/PADDING' + * (the full list of supported Cipher combinations can be found at + * + * the Android documentation). + *
  • + *
  • path: the input file containing data to be encrypted.
  • + *
  • store: the name of the shared preference to store the encrypted data.
  • + *
  • quiet: if set, will not output to stdout.
  • + *
+ */ + private static void encryptData(TermuxApiReceiver apiReceiver, final Context context, + final Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new WithInput() { + @Override + public void writeResult(PrintWriter out) + throws GeneralSecurityException, IOException, JSONException { + String alias = intent.getStringExtra("alias"); + String algorithm = intent.getStringExtra("algorithm"); + String path = intent.getStringExtra("filepath"); + String store = intent.getStringExtra("store"); + boolean quiet = (intent.getIntExtra("quiet", 0) == 1); + + byte[] input; + ByteArrayOutputStream encrypted = new ByteArrayOutputStream(); + + if (path.equals("-1")) { + input = readStream(in); + } else { + input = readFile(path); + } + + String[] alg = algorithm.split("/", 3); + Cipher cipher = Cipher.getInstance(algorithm); + cipher.init(Cipher.ENCRYPT_MODE, getKey(alias, alg[0], true)); + byte[] encryptedData = cipher.doFinal(input); Arrays.fill(input, (byte) 0); + + byte[] iv = cipher.getIV(); + if (iv == null) { + encrypted.write((byte) 0); + } else { + encrypted.write(iv.length); + encrypted.write(iv); + Arrays.fill(iv, (byte) 0); + } + encrypted.write(encryptedData); Arrays.fill(encryptedData, (byte) 0); + + // we are not allowed to output bytes in this function + // one option is to encode using base64 which is a plain string + if (!quiet) out.write(Base64.encodeToString( + encrypted.toByteArray(), Base64.NO_WRAP)); + if (!store.equals("-1")) { + JSONObject prefsJSON = getPrefsJSON(context, alias); + prefsJSON.put(store, + Base64.encodeToString(encrypted.toByteArray(), Base64.NO_WRAP)); + setPrefsJSON(context, alias, prefsJSON); + } + encrypted.reset(); + } + }); + } + + /** + * Decrypt a given byte stream. + * The data is read from a file, shared preferences, or stdin (in that precedence), and the + * decrypted data can be output to stdout. Input is expected in the form + * [IV.length][IV][Encrypted Data] (IV omitted if IV.length is 0).
+ * Required intent extras: + *
    + *
  • alias: key alias.
  • + *
  • + * algorithm: key algorithm, should be of the form 'ALG/MODE/PADDING' + * (the full list of supported Cipher combinations can be found at + * + * the Android documentation). + *
  • + *
  • path: the input file containing data to be decrypted.
  • + *
  • store: the name of the shared preference containing data to be decrypted.
  • + *
  • quiet: if set, will not output to stdout.
  • + *
+ */ + private static void decryptData(TermuxApiReceiver apiReceiver, final Context context, + final Intent intent) { + ResultReturner.returnData(apiReceiver, intent, new WithInput() { + @Override + public void writeResult(PrintWriter out) + throws GeneralSecurityException, IOException, JSONException { + String alias = intent.getStringExtra("alias"); + String algorithm = intent.getStringExtra("algorithm"); + String path = intent.getStringExtra("filepath"); + String store = intent.getStringExtra("store"); + boolean quiet = (intent.getIntExtra("quiet", 0) == 1); + + byte[] input; + ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); + + if (path.equals("-1")) { + if (!store.equals("-1")) { + JSONObject prefsJSON = getPrefsJSON(context, alias); + input = Base64.decode(prefsJSON.getString(store), Base64.NO_WRAP); + } else { + input = readStream(in); + } + } else { + input = readFile(path); + } + + String[] alg = algorithm.split("/", 3); + Cipher cipher = Cipher.getInstance(algorithm); + + if (input[0] == 0) { + cipher.init(Cipher.DECRYPT_MODE, + getKey(alias, alg[0], false)); + } else { + cipher.init(Cipher.DECRYPT_MODE, + getKey(alias, alg[0], false), getIVSpec(input, alg[1])); + } + + byte[] decryptedData = cipher.doFinal(input, + input[0]+1, input.length-input[0]-1); + Arrays.fill(input, (byte) 0); + decrypted.write(decryptedData); Arrays.fill(decryptedData, (byte) 0); + + // we are not allowed to output bytes in this function + // one option is to encode using base64 which is a plain string + if (!quiet) out.write(Base64.encodeToString( + decrypted.toByteArray(), Base64.NO_WRAP)); + decrypted.reset(); + } + }); + } + + /** + * Get Shared Preferences in JSON for given key alias. + */ + private static JSONObject getPrefsJSON(Context context, String alias) throws JSONException { + SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) + .getSharedPreferences(); + return new JSONObject(SharedPreferenceUtils.getString(preferences, + String.join("__", PREFERENCES_PREFIX, alias), + "{}", true)); + } + + /** + * Set Shared Preferences in JSON for given key alias. + */ + private static void setPrefsJSON(Context context, String alias, JSONObject value) { + SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) + .getSharedPreferences(); + SharedPreferenceUtils.setString(preferences, + String.join("__", PREFERENCES_PREFIX, alias), + value.toString(), true); + } + + /** + * Get IV Parameter Spec. + */ + private static AlgorithmParameterSpec getIVSpec(byte[] input, String mode) { + switch(mode) { + case "CBC": + case "CTR": { + return new IvParameterSpec(input, 1, input[0]); + } + case "GCM": { + return new GCMParameterSpec(128, input, 1, input[0]); + } + default: throw new IllegalArgumentException( + "Invalid Cipher Block. See: Android keystore#SupportedCiphers"); + } + } + + /** + * Get Key. + */ + private static Key getKey(String alias, String algorithm, boolean encrypt) + throws GeneralSecurityException, IOException { + switch(algorithm) { + case "RSA": { + PrivateKeyEntry entry = + (PrivateKeyEntry) getKeyStore().getEntry(alias, null); + return encrypt ? entry.getCertificate().getPublicKey() : entry.getPrivateKey(); + } + case "AES": { + SecretKeyEntry entry = + (SecretKeyEntry) getKeyStore().getEntry(alias, null); + return entry.getSecretKey(); + } + default: + throw new IllegalArgumentException( + "Invalid Cipher Algorithm. See: Android keystore#SupportedCiphers"); + } + } + /** * Set up and return the keystore. */ @@ -312,6 +661,21 @@ private static KeyStore getKeyStore() throws GeneralSecurityException, IOExcepti return keyStore; } + /** + * Read file to byte array. + */ + private static byte[] readFile(String path) throws IOException { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return Files.readAllBytes(Paths.get(path)); + } else { + File file = new File(path); + byte[] data = new byte[(int) file.length()]; + BufferedInputStream buffer = new BufferedInputStream(new FileInputStream(file)); + buffer.read(data, 0, data.length); + buffer.close(); + return data; + } + } /** * Read a given stream to a byte array. Should not be used with large streams. From e34589b85f902a062828bef9cfa5c5650e955bee Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Wed, 12 Oct 2022 21:40:58 +0200 Subject: [PATCH 3/8] Add PluginUtils and update Keystore Receiver --- .../java/com/termux/api/util/PluginUtils.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/com/termux/api/util/PluginUtils.java diff --git a/app/src/main/java/com/termux/api/util/PluginUtils.java b/app/src/main/java/com/termux/api/util/PluginUtils.java new file mode 100644 index 000000000..0fc85e113 --- /dev/null +++ b/app/src/main/java/com/termux/api/util/PluginUtils.java @@ -0,0 +1,38 @@ +package com.termux.api.util; + +import android.content.Context; + +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; +import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP; + +/** + * Based on com.termux.tasker.utils.PluginUtils, reference for more information. + */ +public class PluginUtils { + + /** + * Try to get the next unique {PendingIntent} request code that isn't already being used by + * the app and which would create a unique {PendingIntent} that doesn't conflict with that + * of any other execution commands. + * + * @param context The {@link Context} for operations. + * @return Returns the request code that should be safe to use. + */ + public synchronized static int getLastPendingIntentRequestCode(final Context context) { + if (context == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context); + if (preferences == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + int lastPendingIntentRequestCode = preferences.getLastPendingIntentRequestCode(); + + int nextPendingIntentRequestCode = lastPendingIntentRequestCode + 1; + + if (nextPendingIntentRequestCode == Integer.MAX_VALUE || nextPendingIntentRequestCode < 0) + nextPendingIntentRequestCode = TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + preferences.setLastPendingIntentRequestCode(nextPendingIntentRequestCode); + return nextPendingIntentRequestCode; + } + +} \ No newline at end of file From 601caf76c04c1d57f89f6f3df8032b81ee757853 Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Wed, 12 Oct 2022 22:14:41 +0200 Subject: [PATCH 4/8] Add PluginUtils and update Keystore Receiver --- .../java/com/termux/api/util/PluginUtils.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/src/main/java/com/termux/api/util/PluginUtils.java diff --git a/app/src/main/java/com/termux/api/util/PluginUtils.java b/app/src/main/java/com/termux/api/util/PluginUtils.java new file mode 100644 index 000000000..0fc85e113 --- /dev/null +++ b/app/src/main/java/com/termux/api/util/PluginUtils.java @@ -0,0 +1,38 @@ +package com.termux.api.util; + +import android.content.Context; + +import com.termux.shared.termux.settings.preferences.TermuxAPIAppSharedPreferences; +import com.termux.shared.termux.settings.preferences.TermuxPreferenceConstants.TERMUX_API_APP; + +/** + * Based on com.termux.tasker.utils.PluginUtils, reference for more information. + */ +public class PluginUtils { + + /** + * Try to get the next unique {PendingIntent} request code that isn't already being used by + * the app and which would create a unique {PendingIntent} that doesn't conflict with that + * of any other execution commands. + * + * @param context The {@link Context} for operations. + * @return Returns the request code that should be safe to use. + */ + public synchronized static int getLastPendingIntentRequestCode(final Context context) { + if (context == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + TermuxAPIAppSharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context); + if (preferences == null) return TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + int lastPendingIntentRequestCode = preferences.getLastPendingIntentRequestCode(); + + int nextPendingIntentRequestCode = lastPendingIntentRequestCode + 1; + + if (nextPendingIntentRequestCode == Integer.MAX_VALUE || nextPendingIntentRequestCode < 0) + nextPendingIntentRequestCode = TERMUX_API_APP.DEFAULT_VALUE_KEY_LAST_PENDING_INTENT_REQUEST_CODE; + + preferences.setLastPendingIntentRequestCode(nextPendingIntentRequestCode); + return nextPendingIntentRequestCode; + } + +} \ No newline at end of file From e8aaf70bf0d379bbb898746bb9cf14dd0785307b Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Sat, 15 Oct 2022 20:14:31 +0200 Subject: [PATCH 5/8] Support for authentication call FingerprintAPI + Support for `authenticationTimeout` extra + Supports `EXTRA_LOCK_ACTION` which locks FingerprintAPI call until Authentication Callback + Supports specifying authentication scheme(s) KeystoreAPI + Supports retrying authentication + Code optimization (cipherCall(), getKey(), getKeyInfo(), string compares) --- .../com/termux/api/apis/FingerprintAPI.java | 144 +++++--- .../java/com/termux/api/apis/KeystoreAPI.java | 307 ++++++++++-------- 2 files changed, 263 insertions(+), 188 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java index 233f2044f..bec3d2913 100644 --- a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java +++ b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.Intent; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; @@ -9,6 +10,7 @@ import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.biometric.BiometricManager; import androidx.biometric.BiometricPrompt; import androidx.core.hardware.fingerprint.FingerprintManagerCompat; import androidx.fragment.app.FragmentActivity; @@ -20,6 +22,10 @@ import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; @@ -32,8 +38,7 @@ public class FingerprintAPI { protected static final String KEY_NAME = "TermuxFingerprintAPIKey"; protected static final String KEYSTORE_NAME = "AndroidKeyStore"; - // milliseconds to wait before canceling - protected static final int SENSOR_TIMEOUT = 10000; + protected static int SENSOR_TIMEOUT; // maximum authentication attempts before locked out protected static final int MAX_ATTEMPTS = 5; @@ -53,22 +58,25 @@ public class FingerprintAPI { protected static final String AUTH_RESULT_FAILURE = "AUTH_RESULT_FAILURE"; protected static final String AUTH_RESULT_UNKNOWN = "AUTH_RESULT_UNKNOWN"; - - // store result of fingerprint initialization / authentication protected static FingerprintResult fingerprintResult = new FingerprintResult(); // have we posted our result back? protected static boolean postedResult = false; - + protected static boolean timedOut = false; private static final String LOG_TAG = "FingerprintAPI"; + private static final Lock lock = new ReentrantLock(); + private static final Condition condition = lock.newCondition(); + protected static final String EXTRA_LOCK_ACTION = "EXTRA_LOCK_ACTION"; + /** * Handles setup of fingerprint sensor and writes Fingerprint result to console */ public static void onReceive(final Context context, final Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); + SENSOR_TIMEOUT = intent.getIntExtra("authenticationTimeout", 10)*1000; resetFingerprintResult(); @@ -79,6 +87,21 @@ public static void onReceive(final Context context, final Intent intent) { fingerprintIntent.putExtras(intent.getExtras()); fingerprintIntent.setFlags(FLAG_ACTIVITY_NEW_TASK); context.startActivity(fingerprintIntent); + + if (intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { + lock.lock(); + try { + if (SENSOR_TIMEOUT >= 0) { + if (!condition.await(SENSOR_TIMEOUT, TimeUnit.MILLISECONDS)) { + timedOut = true; + } + } else condition.await(); + } catch (InterruptedException e) { + // If interrupted, nothing currently + } finally { + lock.unlock(); + } + } } else { postFingerprintResult(context, intent, fingerprintResult); } @@ -88,28 +111,37 @@ public static void onReceive(final Context context, final Intent intent) { * Writes the result of our fingerprint result to the console */ protected static void postFingerprintResult(Context context, Intent intent, final FingerprintResult result) { - ResultReturner.returnData(context, intent, new ResultReturner.ResultJsonWriter() { - @Override - public void writeJson(JsonWriter out) throws Exception { - out.beginObject(); + if (intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { + lock.lock(); + try { + condition.signalAll(); + } finally { + lock.unlock(); + } + } else { + ResultReturner.returnData(context, intent, new ResultReturner.ResultJsonWriter() { + @Override + public void writeJson(JsonWriter out) throws Exception { + out.beginObject(); - out.name("errors"); - out.beginArray(); + out.name("errors"); + out.beginArray(); - for (String error : result.errors) { - out.value(error); - } - out.endArray(); + for (String error : result.errors) { + out.value(error); + } + out.endArray(); - out.name("failed_attempts").value(result.failedAttempts); - out.name("auth_result").value(result.authResult); - out.endObject(); + out.name("failed_attempts").value(result.failedAttempts); + out.name("auth_result").value(result.authResult); + out.endObject(); - out.flush(); - out.close(); - postedResult = true; - } - }); + out.flush(); + out.close(); + postedResult = true; + } + }); + } } /** @@ -161,37 +193,40 @@ protected void handleFingerprint() { * Handles authentication callback from our fingerprint sensor */ protected static void authenticateWithFingerprint(final FragmentActivity context, final Intent intent, final Executor executor) { - BiometricPrompt biometricPrompt = new BiometricPrompt(context, executor, new BiometricPrompt.AuthenticationCallback() { - @Override - public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { - if (errorCode == BiometricPrompt.ERROR_LOCKOUT) { - appendFingerprintError(ERROR_LOCKOUT); - - // first time locked out, subsequent auth attempts will fail immediately for a bit - if (fingerprintResult.failedAttempts >= MAX_ATTEMPTS) { - appendFingerprintError(ERROR_TOO_MANY_FAILED_ATTEMPTS); + BiometricPrompt biometricPrompt = new BiometricPrompt(context, executor, + new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + if (errorCode == BiometricPrompt.ERROR_LOCKOUT) { + appendFingerprintError(ERROR_LOCKOUT); + + // first time locked out, subsequent auth attempts will fail immediately for a bit + if (fingerprintResult.failedAttempts >= MAX_ATTEMPTS) { + appendFingerprintError(ERROR_TOO_MANY_FAILED_ATTEMPTS); + } + } + setAuthResult(AUTH_RESULT_FAILURE); + if (timedOut) timedOut = false; + else postFingerprintResult(context, intent, fingerprintResult); + Logger.logError(LOG_TAG, errString.toString()); } - } - setAuthResult(AUTH_RESULT_FAILURE); - postFingerprintResult(context, intent, fingerprintResult); - Logger.logError(LOG_TAG, errString.toString()); - } - @Override - public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { - setAuthResult(AUTH_RESULT_SUCCESS); - postFingerprintResult(context, intent, fingerprintResult); - } + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + setAuthResult(AUTH_RESULT_SUCCESS); + postFingerprintResult(context, intent, fingerprintResult); + } - @Override - public void onAuthenticationFailed() { - addFailedAttempt(); - } - }); + @Override + public void onAuthenticationFailed() { + addFailedAttempt(); + } + }); + boolean[] auths = intent.getBooleanArrayExtra("auths"); BiometricPrompt.PromptInfo.Builder builder = new BiometricPrompt.PromptInfo.Builder(); - builder.setTitle(intent.hasExtra("title") ? intent.getStringExtra("title") : "Authenticate"); - builder.setNegativeButtonText(intent.hasExtra("cancel") ? intent.getStringExtra("cancel") : "Cancel"); + builder.setTitle(intent.hasExtra("title") ? + intent.getStringExtra("title") : "Authenticate"); if (intent.hasExtra("description")) { builder.setDescription(intent.getStringExtra("description")); } @@ -199,10 +234,19 @@ public void onAuthenticationFailed() { builder.setSubtitle(intent.getStringExtra("subtitle")); } + if (auths == null || !auths[0]) { + builder.setNegativeButtonText(intent.hasExtra("cancel") ? + intent.getStringExtra("cancel") : "Cancel"); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !auths[1]) { + builder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL); //Can't test yet + } else builder.setDeviceCredentialAllowed(true); + // listen to fingerprint sensor biometricPrompt.authenticate(builder.build()); - addSensorTimeout(context, intent, biometricPrompt); + if (!intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { + addSensorTimeout(context, intent, biometricPrompt); + } } /** diff --git a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java index 7205f310f..5c12ca96a 100644 --- a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java +++ b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java @@ -8,6 +8,7 @@ import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyProperties; +import android.security.keystore.UserNotAuthenticatedException; import android.util.Base64; import android.util.JsonWriter; @@ -41,8 +42,8 @@ import java.security.KeyStore.Entry; import java.security.KeyStore.PrivateKeyEntry; import java.security.KeyStore.SecretKeyEntry; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; -import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPublicKey; @@ -55,6 +56,7 @@ import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.IvParameterSpec; +import java.security.spec.InvalidKeySpecException; import java.util.ArrayList; import java.util.Arrays; import java.util.Enumeration; @@ -68,6 +70,7 @@ public class KeystoreAPI { // this is the only provider name that is supported by Android private static final String PROVIDER = "AndroidKeyStore"; private static final String PREFERENCES_PREFIX = "keystore_api__encrypted_data"; + private static final int MAX_AUTH_RETRIES = 1; @SuppressLint("NewApi") public static void onReceive(TermuxApiReceiver apiReceiver, Context context, Intent intent) { @@ -130,34 +133,19 @@ public void writeJson(JsonWriter out) if (pref) { JSONObject prefsJSON = getPrefsJSON(context, alias); - Iterator prefKeys =prefsJSON.keys(); + Iterator prefKeys = prefsJSON.keys(); out.name("Preferences"); out.beginObject(); - while (prefKeys.hasNext()) { - String transientKey = prefKeys.next(); - out.name(transientKey) - .value(detailed ? - prefsJSON.getString(transientKey) : ""); - } + while (prefKeys.hasNext()) { + String transientKey = prefKeys.next(); + out.name(transientKey) + .value(detailed ? prefsJSON.getString(transientKey) : ""); + } out.endObject(); } else { - Entry entry = keyStore.getEntry(alias, null); - if (entry instanceof PrivateKeyEntry) { - PrivateKeyEntry privateEntry = (PrivateKeyEntry) entry; - PrivateKey privateKey = privateEntry.getPrivateKey(); - PublicKey publicKey = privateEntry.getCertificate().getPublicKey(); - String algorithm = privateKey.getAlgorithm(); - KeyInfo keyInfo = KeyFactory.getInstance(algorithm) - .getKeySpec(privateKey, KeyInfo.class); - printKey(out, algorithm, keyInfo, detailed, publicKey); - } else if (entry instanceof SecretKeyEntry) { - SecretKeyEntry secretEntry = (SecretKeyEntry) entry; - SecretKey secretKey = secretEntry.getSecretKey(); - String algorithm = secretKey.getAlgorithm(); - KeyInfo keyInfo = (KeyInfo) SecretKeyFactory.getInstance(algorithm) - .getKeySpec(secretKey, KeyInfo.class); - printKey(out, algorithm, keyInfo, detailed, null); - } + Key key = getKey(alias, false); + printKey(out, detailed, key, + key instanceof PrivateKey ? getKey(alias, true) : null); } out.endObject(); @@ -171,44 +159,51 @@ public void writeJson(JsonWriter out) * Helper function for printing the parameters of a given key. */ @RequiresApi(api = Build.VERSION_CODES.M) - private static void printKey(JsonWriter out, String algorithm, KeyInfo keyInfo, - boolean detailed, Key pubKey) throws GeneralSecurityException, IOException { + private static void printKey(JsonWriter out, boolean detailed, Key key, Key pubKey) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + String algorithm = key.getAlgorithm(); + KeyInfo keyInfo = getKeyInfo(key); + String mode = String.join(",", keyInfo.getBlockModes()); String padding = String.join(",", keyInfo.getEncryptionPaddings()); boolean authRequired = keyInfo.isUserAuthenticationRequired(); int validityDuration = keyInfo.getUserAuthenticationValidityDurationSeconds(); out.name("algorithm"); - out.beginObject(); - out.name("name").value(algorithm); - if (!mode.isEmpty()) { - out.name("block_mode").value(mode); - out.name("encryption_padding").value(padding); - } - out.endObject(); + out.beginObject(); + out.name("name").value(algorithm); + if (!mode.isEmpty()) { + out.name("block_mode").value(mode); + out.name("encryption_padding").value(padding); + } + out.endObject(); out.name("size").value(keyInfo.getKeySize()); - out.name("purposes").value(decomposeBinary(keyInfo.getPurposes())); + out.name("purposes").value(decomposeBinary(keyInfo.getPurposes()) + .stream().map(String::valueOf) + .collect(Collectors.joining("|"))); out.name("inside_secure_hardware").value(keyInfo.isInsideSecureHardware()); out.name("user_authentication"); - out.beginObject(); - out.name("required").value(authRequired); - out.name("enforced_by_secure_hardware") - .value(keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware()); - if (validityDuration >= 0) out.name("validity_duration_seconds") - .value(validityDuration); - if (detailed && authRequired) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - out.name("authentication_type") - .value(decomposeBinary(keyInfo.getUserAuthenticationType())); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - out.name("invalidated_by_new_biometric") - .value(keyInfo.isInvalidatedByBiometricEnrollment()); - } - } - out.endObject(); + out.beginObject(); + out.name("required").value(authRequired); + out.name("enforced_by_secure_hardware") + .value(keyInfo.isUserAuthenticationRequirementEnforcedBySecureHardware()); + if (validityDuration >= 0) out.name("validity_duration_seconds") + .value(validityDuration); + if (detailed && authRequired) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { //Can't test yet + out.name("authentication_type") + .value(decomposeBinary(keyInfo.getUserAuthenticationType()) + .stream().map(String::valueOf) + .collect(Collectors.joining("|"))); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + out.name("invalidated_by_new_biometric") + .value(keyInfo.isInvalidatedByBiometricEnrollment()); + } + } + out.endObject(); if (detailed && pubKey instanceof RSAPublicKey) { RSAPublicKey rsa = (RSAPublicKey) pubKey; @@ -225,9 +220,9 @@ private static void printKey(JsonWriter out, String algorithm, KeyInfo keyInfo, } /** - * Decomposes binary for options (e.g. 3->'1|2'). + * Decomposes binary for options (e.g. 3->{1,2}). */ - private static String decomposeBinary(int binary) { + private static ArrayList decomposeBinary(int binary) { ArrayList values = new ArrayList<>(); int power = 0; while (binary != 0) { @@ -235,7 +230,7 @@ private static String decomposeBinary(int binary) { power += 1; binary >>= 1; } - return values.stream().map(String::valueOf).collect(Collectors.joining("|")); + return values; } /** @@ -247,12 +242,12 @@ private static String decomposeBinary(int binary) { * */ private static void deleteData(TermuxApiReceiver apiReceiver, final Context context, - final Intent intent) { + final Intent intent) { ResultReturner.returnData(apiReceiver, intent, out -> { String alias = intent.getStringExtra("alias"); String pref = intent.getStringExtra("pref"); - if (!pref.equals("-1")) { + if (!"-1".equals(pref)) { JSONObject prefsJSON = getPrefsJSON(context, alias); prefsJSON.remove(pref); setPrefsJSON(context, alias, prefsJSON); @@ -261,10 +256,10 @@ private static void deleteData(TermuxApiReceiver apiReceiver, final Context cont // nor does it throw an exception if the alias does not exist getKeyStore().deleteEntry(alias); TermuxAPIAppSharedPreferences.build(context) - .getSharedPreferences() - .edit() - .remove(String.join("__", PREFERENCES_PREFIX, alias)) - .commit(); + .getSharedPreferences() + .edit() + .remove(String.join("__", PREFERENCES_PREFIX, alias)) + .commit(); } }); } @@ -341,33 +336,33 @@ private static void generateKey(TermuxApiReceiver apiReceiver, final Intent inte KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder(alias, purposes) - .setKeySize(size) - .setUserAuthenticationRequired((authorizations != 0)) - .setUserAuthenticationValidityDurationSeconds(userValidity); + .setKeySize(size) + .setUserAuthenticationRequired((authorizations != 0)) + .setUserAuthenticationValidityDurationSeconds(userValidity); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - builder.setInvalidatedByBiometricEnrollment(invalidate); //Not working? Cannot turn off invalidation (test with keyInfo.isInvalidatedByBiometricEnrollment()) + builder.setInvalidatedByBiometricEnrollment(invalidate); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { builder.setUnlockedDeviceRequired(unlocked); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - builder.setUserAuthenticationParameters(userValidity, authorizations); + builder.setUserAuthenticationParameters(userValidity, authorizations); //Can't test yet } } } - if (!mode.equals("-1")) builder.setBlockModes(mode).setEncryptionPaddings(padding); + if (!"-1".equals(mode)) builder.setBlockModes(mode).setEncryptionPaddings(padding); - if (mode.equals(KeyProperties.BLOCK_MODE_ECB) && - (padding.equals(KeyProperties.ENCRYPTION_PADDING_NONE) || - padding.equals(KeyProperties.ENCRYPTION_PADDING_PKCS7))) { + if (KeyProperties.BLOCK_MODE_ECB.equals(mode) && + (KeyProperties.ENCRYPTION_PADDING_NONE.equals(padding) || + KeyProperties.ENCRYPTION_PADDING_PKCS7.equals(padding))) { builder.setRandomizedEncryptionRequired(false); } - if (algorithm.equals(KeyProperties.KEY_ALGORITHM_AES)) { + if (KeyProperties.KEY_ALGORITHM_AES.equals(algorithm)) { KeyGenerator generator = KeyGenerator.getInstance(algorithm, PROVIDER); generator.init(builder.build()); generator.generateKey(); } else { builder.setDigests(digests); - if (algorithm.equals(KeyProperties.KEY_ALGORITHM_RSA)) { + if (KeyProperties.KEY_ALGORITHM_RSA.equals(algorithm)) { builder.setSignaturePaddings(KeyProperties.SIGNATURE_PADDING_RSA_PKCS1); } KeyPairGenerator generator = KeyPairGenerator.getInstance(algorithm, PROVIDER); @@ -399,8 +394,7 @@ public void writeResult(PrintWriter out) throws Exception { String algorithm = intent.getStringExtra("algorithm"); byte[] input = readStream(in); - PrivateKeyEntry key = (PrivateKeyEntry) getKeyStore() - .getEntry(alias, null); + PrivateKeyEntry key = (PrivateKeyEntry) getKeyStore().getEntry(alias, null); Signature signature = Signature.getInstance(algorithm); signature.initSign(key.getPrivateKey()); signature.update(input); @@ -482,20 +476,14 @@ public void writeResult(PrintWriter out) String store = intent.getStringExtra("store"); boolean quiet = (intent.getIntExtra("quiet", 0) == 1); - byte[] input; ByteArrayOutputStream encrypted = new ByteArrayOutputStream(); - - if (path.equals("-1")) { - input = readStream(in); - } else { - input = readFile(path); - } - String[] alg = algorithm.split("/", 3); + byte[] input = "-1".equals(path) ? readStream(in) : readFile(path); + Cipher cipher = Cipher.getInstance(algorithm); - cipher.init(Cipher.ENCRYPT_MODE, getKey(alias, alg[0], true)); - byte[] encryptedData = cipher.doFinal(input); Arrays.fill(input, (byte) 0); + cipherCall(context, intent, alias, cipher, Cipher.ENCRYPT_MODE, null); + byte[] encryptedData = cipher.doFinal(input); Arrays.fill(input, (byte) 0); byte[] iv = cipher.getIV(); if (iv == null) { encrypted.write((byte) 0); @@ -509,11 +497,11 @@ public void writeResult(PrintWriter out) // we are not allowed to output bytes in this function // one option is to encode using base64 which is a plain string if (!quiet) out.write(Base64.encodeToString( - encrypted.toByteArray(), Base64.NO_WRAP)); - if (!store.equals("-1")) { + encrypted.toByteArray(), Base64.NO_WRAP)); + if (!"-1".equals(store)) { JSONObject prefsJSON = getPrefsJSON(context, alias); - prefsJSON.put(store, - Base64.encodeToString(encrypted.toByteArray(), Base64.NO_WRAP)); + prefsJSON.put(store, Base64.encodeToString( + encrypted.toByteArray(), Base64.NO_WRAP)); setPrefsJSON(context, alias, prefsJSON); } encrypted.reset(); @@ -552,11 +540,11 @@ public void writeResult(PrintWriter out) String store = intent.getStringExtra("store"); boolean quiet = (intent.getIntExtra("quiet", 0) == 1); - byte[] input; ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); - - if (path.equals("-1")) { - if (!store.equals("-1")) { + String[] alg = algorithm.split("/", 3); + byte[] input; + if ("-1".equals(path)) { + if (!"-1".equals(store)) { JSONObject prefsJSON = getPrefsJSON(context, alias); input = Base64.decode(prefsJSON.getString(store), Base64.NO_WRAP); } else { @@ -566,16 +554,9 @@ public void writeResult(PrintWriter out) input = readFile(path); } - String[] alg = algorithm.split("/", 3); Cipher cipher = Cipher.getInstance(algorithm); - - if (input[0] == 0) { - cipher.init(Cipher.DECRYPT_MODE, - getKey(alias, alg[0], false)); - } else { - cipher.init(Cipher.DECRYPT_MODE, - getKey(alias, alg[0], false), getIVSpec(input, alg[1])); - } + cipherCall(context, intent, alias, cipher, Cipher.DECRYPT_MODE, + input[0] == 0 ? null : getIVSpec(input, alg[1])); byte[] decryptedData = cipher.doFinal(input, input[0]+1, input.length-input[0]-1); @@ -585,32 +566,49 @@ public void writeResult(PrintWriter out) // we are not allowed to output bytes in this function // one option is to encode using base64 which is a plain string if (!quiet) out.write(Base64.encodeToString( - decrypted.toByteArray(), Base64.NO_WRAP)); + decrypted.toByteArray(), Base64.NO_WRAP)); decrypted.reset(); } }); } /** - * Get Shared Preferences in JSON for given key alias. - */ - private static JSONObject getPrefsJSON(Context context, String alias) throws JSONException { - SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) - .getSharedPreferences(); - return new JSONObject(SharedPreferenceUtils.getString(preferences, - String.join("__", PREFERENCES_PREFIX, alias), - "{}", true)); - } - - /** - * Set Shared Preferences in JSON for given key alias. + * Tries to initialize cipher and prompts for authentication if timed-out. */ - private static void setPrefsJSON(Context context, String alias, JSONObject value) { - SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) - .getSharedPreferences(); - SharedPreferenceUtils.setString(preferences, - String.join("__", PREFERENCES_PREFIX, alias), - value.toString(), true); + private static void cipherCall(Context context, Intent intent, String alias, Cipher cipher, + int mode, AlgorithmParameterSpec spec) + throws GeneralSecurityException, IOException { + int count = 0; + boolean[] auths = {true, true}; + Key key = getKey(alias, (mode == Cipher.ENCRYPT_MODE)); + do { + try { + if (spec == null) cipher.init(mode, key); + else cipher.init(mode, key, spec); + break; + } catch (UserNotAuthenticatedException e) { + if (count == 0) { + KeyInfo keyInfo = getKeyInfo(key); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + ArrayList authList = decomposeBinary(keyInfo + .getUserAuthenticationType()); + auths[0] = authList.contains(KeyProperties.AUTH_DEVICE_CREDENTIAL); + auths[1] = authList.contains(KeyProperties.AUTH_BIOMETRIC_STRONG); + } + intent.putExtra("auths", auths); + intent.putExtra("title", " "); + intent.putExtra("subtitle", "Authentication required for key"); + intent.putExtra("description", ""); + intent.putExtra(FingerprintAPI.EXTRA_LOCK_ACTION, true); + } + if (count <= MAX_AUTH_RETRIES) { + FingerprintAPI.onReceive(context, intent); + } else { + Logger.logError(LOG_TAG, String.valueOf(e)); + throw e; + } + } + } while (count++ <= MAX_AUTH_RETRIES); } /** @@ -625,33 +623,44 @@ private static AlgorithmParameterSpec getIVSpec(byte[] input, String mode) { case "GCM": { return new GCMParameterSpec(128, input, 1, input[0]); } - default: throw new IllegalArgumentException( - "Invalid Cipher Block. See: Android keystore#SupportedCiphers"); + default: { + String e = "Invalid Cipher Block. See: Android keystore#SupportedCiphers"; + Logger.logError(LOG_TAG, e); + throw new IllegalArgumentException(e); + } } } /** * Get Key. */ - private static Key getKey(String alias, String algorithm, boolean encrypt) + private static Key getKey(String alias, boolean encrypt) throws GeneralSecurityException, IOException { - switch(algorithm) { - case "RSA": { - PrivateKeyEntry entry = - (PrivateKeyEntry) getKeyStore().getEntry(alias, null); - return encrypt ? entry.getCertificate().getPublicKey() : entry.getPrivateKey(); - } - case "AES": { - SecretKeyEntry entry = - (SecretKeyEntry) getKeyStore().getEntry(alias, null); - return entry.getSecretKey(); - } - default: - throw new IllegalArgumentException( - "Invalid Cipher Algorithm. See: Android keystore#SupportedCiphers"); + Entry entry = getKeyStore().getEntry(alias, null); + if (entry instanceof PrivateKeyEntry) { + return encrypt ? ((PrivateKeyEntry) entry).getCertificate().getPublicKey() + : ((PrivateKeyEntry) entry).getPrivateKey(); + } else if (entry instanceof SecretKeyEntry) { + return ((SecretKeyEntry) entry).getSecretKey(); + } else { + String e = "Invalid Cipher Algorithm. See: Android keystore#SupportedCiphers"; + Logger.logError(LOG_TAG, e); + throw new IllegalArgumentException(e); } } + /** + * Get KeyInfo + */ + private static KeyInfo getKeyInfo(Key key) + throws NoSuchAlgorithmException, InvalidKeySpecException { + String algorithm = key.getAlgorithm(); + return (key instanceof PrivateKey) ? + KeyFactory.getInstance(algorithm).getKeySpec(key, KeyInfo.class) + : (KeyInfo) SecretKeyFactory.getInstance(algorithm) + .getKeySpec((SecretKey) key, KeyInfo.class); + } + /** * Set up and return the keystore. */ @@ -661,6 +670,28 @@ private static KeyStore getKeyStore() throws GeneralSecurityException, IOExcepti return keyStore; } + /** + * Set Shared Preferences in JSON for given key alias. + */ + private static void setPrefsJSON(Context context, String alias, JSONObject value) { + SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) + .getSharedPreferences(); + SharedPreferenceUtils.setString(preferences, + String.join("__", PREFERENCES_PREFIX, alias), + value.toString(), true); + } + + /** + * Get Shared Preferences in JSON for given key alias. + */ + private static JSONObject getPrefsJSON(Context context, String alias) throws JSONException { + SharedPreferences preferences = TermuxAPIAppSharedPreferences.build(context) + .getSharedPreferences(); + return new JSONObject(SharedPreferenceUtils.getString(preferences, + String.join("__", PREFERENCES_PREFIX, alias), + "{}", true)); + } + /** * Read file to byte array. */ @@ -693,4 +724,4 @@ private static byte[] readStream(InputStream stream) throws IOException { private static void printErrorMessage(TermuxApiReceiver apiReceiver, Intent intent) { ResultReturner.returnData(apiReceiver, intent, out -> out.println("termux-keystore requires at least Android 6.0 (Marshmallow).")); } -} +} \ No newline at end of file From 4cb238885765204b147d237cebf90b745c4164e2 Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Tue, 18 Oct 2022 16:51:37 +0200 Subject: [PATCH 6/8] Fixed authentication time-out For API<=29 (unfortunately, had to stop support for device credentials <=29 as there is inconsistent callback behaviour) --- .../com/termux/api/apis/FingerprintAPI.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java index bec3d2913..f08d1bcb9 100644 --- a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java +++ b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java @@ -76,7 +76,8 @@ public class FingerprintAPI { */ public static void onReceive(final Context context, final Intent intent) { Logger.logDebug(LOG_TAG, "onReceive"); - SENSOR_TIMEOUT = intent.getIntExtra("authenticationTimeout", 10)*1000; + SENSOR_TIMEOUT = intent.getIntExtra("authenticationTimeout", 10); + if (SENSOR_TIMEOUT != -1) SENSOR_TIMEOUT *= 1000; resetFingerprintResult(); @@ -91,9 +92,10 @@ public static void onReceive(final Context context, final Intent intent) { if (intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { lock.lock(); try { - if (SENSOR_TIMEOUT >= 0) { - if (!condition.await(SENSOR_TIMEOUT, TimeUnit.MILLISECONDS)) { + if (SENSOR_TIMEOUT != -1) { + if (!condition.await(SENSOR_TIMEOUT+5000, TimeUnit.MILLISECONDS)) { timedOut = true; + Logger.logDebug(LOG_TAG, "Lock timed out"); } } else condition.await(); } catch (InterruptedException e) { @@ -234,17 +236,21 @@ public void onAuthenticationFailed() { builder.setSubtitle(intent.getStringExtra("subtitle")); } - if (auths == null || !auths[0]) { + if (auths == null || !auths[0] || Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) { builder.setNegativeButtonText(intent.hasExtra("cancel") ? intent.getStringExtra("cancel") : "Cancel"); - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !auths[1]) { + builder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + } else if (!auths[1]) { builder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL); //Can't test yet - } else builder.setDeviceCredentialAllowed(true); + } else { + builder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG | + BiometricManager.Authenticators.DEVICE_CREDENTIAL); + } // listen to fingerprint sensor biometricPrompt.authenticate(builder.build()); - if (!intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { + if (SENSOR_TIMEOUT != -1) { addSensorTimeout(context, intent, biometricPrompt); } } @@ -259,7 +265,9 @@ protected static void addSensorTimeout(final Context context, final Intent inten if (!postedResult) { appendFingerprintError(ERROR_TIMEOUT); biometricPrompt.cancelAuthentication(); - postFingerprintResult(context, intent, fingerprintResult); + if (!intent.getBooleanExtra(EXTRA_LOCK_ACTION, false)) { + postFingerprintResult(context, intent, fingerprintResult); + } } }, SENSOR_TIMEOUT); } From c378dd8fda74f3bff815d43ba0d6b0019a18d420 Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Sat, 22 Oct 2022 11:58:07 +0200 Subject: [PATCH 7/8] Encrypt & Decrypt do not require algorithm specified --- .../java/com/termux/api/apis/KeystoreAPI.java | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java index 5c12ca96a..4396ac4e5 100644 --- a/app/src/main/java/com/termux/api/apis/KeystoreAPI.java +++ b/app/src/main/java/com/termux/api/apis/KeystoreAPI.java @@ -477,11 +477,10 @@ public void writeResult(PrintWriter out) boolean quiet = (intent.getIntExtra("quiet", 0) == 1); ByteArrayOutputStream encrypted = new ByteArrayOutputStream(); - String[] alg = algorithm.split("/", 3); byte[] input = "-1".equals(path) ? readStream(in) : readFile(path); - Cipher cipher = Cipher.getInstance(algorithm); - cipherCall(context, intent, alias, cipher, Cipher.ENCRYPT_MODE, null); + Cipher cipher = cipherCall(context, intent, Cipher.ENCRYPT_MODE, + alias, algorithm, null); byte[] encryptedData = cipher.doFinal(input); Arrays.fill(input, (byte) 0); byte[] iv = cipher.getIV(); @@ -541,7 +540,6 @@ public void writeResult(PrintWriter out) boolean quiet = (intent.getIntExtra("quiet", 0) == 1); ByteArrayOutputStream decrypted = new ByteArrayOutputStream(); - String[] alg = algorithm.split("/", 3); byte[] input; if ("-1".equals(path)) { if (!"-1".equals(store)) { @@ -554,9 +552,8 @@ public void writeResult(PrintWriter out) input = readFile(path); } - Cipher cipher = Cipher.getInstance(algorithm); - cipherCall(context, intent, alias, cipher, Cipher.DECRYPT_MODE, - input[0] == 0 ? null : getIVSpec(input, alg[1])); + Cipher cipher = cipherCall(context, intent, Cipher.DECRYPT_MODE, alias, algorithm, + input[0] == 0 ? null : input); byte[] decryptedData = cipher.doFinal(input, input[0]+1, input.length-input[0]-1); @@ -575,20 +572,31 @@ public void writeResult(PrintWriter out) /** * Tries to initialize cipher and prompts for authentication if timed-out. */ - private static void cipherCall(Context context, Intent intent, String alias, Cipher cipher, - int mode, AlgorithmParameterSpec spec) + private static Cipher cipherCall(Context context, Intent intent, int mode, String alias, + String algorithm, byte[] input) throws GeneralSecurityException, IOException { int count = 0; boolean[] auths = {true, true}; Key key = getKey(alias, (mode == Cipher.ENCRYPT_MODE)); + KeyInfo keyInfo = getKeyInfo(key); + if ("-1".equals(algorithm)) { + algorithm = String.join("/", + key.getAlgorithm(), + keyInfo.getBlockModes()[0], + keyInfo.getEncryptionPaddings()[0]); + Logger.logDebug(LOG_TAG, "Cipher algorithm not specified, using: " + algorithm); + } + Cipher cipher = Cipher.getInstance(algorithm); + do { try { - if (spec == null) cipher.init(mode, key); - else cipher.init(mode, key, spec); - break; + if (input == null) cipher.init(mode, key); + else { + cipher.init(mode, key, getIVSpec(input, algorithm.split("/", 3)[1])); + } + return cipher; } catch (UserNotAuthenticatedException e) { if (count == 0) { - KeyInfo keyInfo = getKeyInfo(key); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { ArrayList authList = decomposeBinary(keyInfo .getUserAuthenticationType()); @@ -609,6 +617,8 @@ private static void cipherCall(Context context, Intent intent, String alias, Cip } } } while (count++ <= MAX_AUTH_RETRIES); + + return null; } /** From 04e1f1a6f96842c93d265d11bd9d9ab165c133cd Mon Sep 17 00:00:00 2001 From: EduardDurech <39579228+EduardDurech@users.noreply.github.com> Date: Sun, 23 Oct 2022 21:38:43 +0200 Subject: [PATCH 8/8] Added `activity.finish()` --- app/src/main/java/com/termux/api/apis/FingerprintAPI.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java index f08d1bcb9..92780117c 100644 --- a/app/src/main/java/com/termux/api/apis/FingerprintAPI.java +++ b/app/src/main/java/com/termux/api/apis/FingerprintAPI.java @@ -119,6 +119,7 @@ protected static void postFingerprintResult(Context context, Intent intent, fina condition.signalAll(); } finally { lock.unlock(); + if (context instanceof Activity) ((Activity) context).finish(); } } else { ResultReturner.returnData(context, intent, new ResultReturner.ResultJsonWriter() {