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:
*
- * - detailed: if set, key parameters (modulus etc.) are included in the response
+ * -
+ * detailed: if set, shows key parameters (modulus etc.)
+ * or values of shared preferences.
+ *
+ * - pref: if set, shows shared preferences instead of key parameters.
*
*/
@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() {