Skip to content
2 changes: 1 addition & 1 deletion app/src/main/java/com/termux/api/TermuxApiReceiver.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
155 changes: 104 additions & 51 deletions app/src/main/java/com/termux/api/apis/FingerprintAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@

import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.util.JsonWriter;
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;
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -53,22 +58,26 @@ 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);
if (SENSOR_TIMEOUT != -1) SENSOR_TIMEOUT *= 1000;

resetFingerprintResult();

Expand All @@ -79,6 +88,22 @@ 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 != -1) {
if (!condition.await(SENSOR_TIMEOUT+5000, TimeUnit.MILLISECONDS)) {
timedOut = true;
Logger.logDebug(LOG_TAG, "Lock timed out");
}
} else condition.await();
} catch (InterruptedException e) {
// If interrupted, nothing currently
} finally {
lock.unlock();
}
}
} else {
postFingerprintResult(context, intent, fingerprintResult);
}
Expand All @@ -88,28 +113,38 @@ 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();
if (context instanceof Activity) ((Activity) context).finish();
}
} 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;
}
});
}
}

/**
Expand Down Expand Up @@ -161,48 +196,64 @@ 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"));
}
if (intent.hasExtra("subtitle")) {
builder.setSubtitle(intent.getStringExtra("subtitle"));
}

if (auths == null || !auths[0] || Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q) {
builder.setNegativeButtonText(intent.hasExtra("cancel") ?
intent.getStringExtra("cancel") : "Cancel");
builder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
} else if (!auths[1]) {
builder.setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL); //Can't test yet
} else {
builder.setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG |
BiometricManager.Authenticators.DEVICE_CREDENTIAL);
}

// listen to fingerprint sensor
biometricPrompt.authenticate(builder.build());

addSensorTimeout(context, intent, biometricPrompt);
if (SENSOR_TIMEOUT != -1) {
addSensorTimeout(context, intent, biometricPrompt);
}
}

/**
Expand All @@ -215,7 +266,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);
}
Expand Down
Loading