From 07939c72c7e0d4505c58d8e352d259434e530c6f Mon Sep 17 00:00:00 2001 From: David Kramer Date: Wed, 13 Jun 2018 15:49:36 -0600 Subject: [PATCH 1/2] Add FingerprintAPI --- app/src/main/AndroidManifest.xml | 5 + .../java/com/termux/api/FingerprintAPI.java | 338 ++++++++++++++++++ .../com/termux/api/TermuxApiReceiver.java | 3 + 3 files changed, 346 insertions(+) create mode 100644 app/src/main/java/com/termux/api/FingerprintAPI.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d3595a67f..545746b9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,6 +21,7 @@ + @@ -46,6 +47,10 @@ android:theme="@android:style/Theme.Material.Light" > + = android.os.Build.VERSION_CODES.M) { + FingerprintManager fingerprintManager = (FingerprintManager)context.getSystemService(Context.FINGERPRINT_SERVICE); + + // make sure we have a valid fingerprint sensor before attempting to launch Fingerprint activity + if (validateFingerprintSensor(context, fingerprintManager)) { + Intent fingerprintIntent = new Intent(context, FingerprintActivity.class); + fingerprintIntent.putExtras(intent.getExtras()); + context.startActivity(fingerprintIntent); + } else { + postFingerprintResult(context, intent, fingerprintResult); + } + } else { + // pre-marshmallow is unsupported + appendFingerprintError(ERROR_UNSUPPORTED_OS_VERSION); + postFingerprintResult(context, intent, fingerprintResult); + } + } + + /** + * Writes the result of our fingerprint result to the console + * @param context + * @param intent + * @param result + */ + 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(); + + out.name("errors"); + out.beginArray(); + + 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.flush(); + out.close(); + postedResult = true; + } + }); + } + + /** + * Ensure that we have a fingerprint sensor and that the user has already enrolled fingerprints + * @param fingerprintManager + * @return + */ + @TargetApi(Build.VERSION_CODES.M) + protected static boolean validateFingerprintSensor(Context context, FingerprintManager fingerprintManager) { + boolean result = true; + + if (!fingerprintManager.isHardwareDetected()) { + Toast.makeText(context, "No fingerprint scanner found!", Toast.LENGTH_SHORT).show(); + appendFingerprintError(ERROR_NO_HARDWARE); + result = false; + } + + if (!fingerprintManager.hasEnrolledFingerprints()) { + Toast.makeText(context, "No fingerprints enrolled", Toast.LENGTH_SHORT).show(); + appendFingerprintError(ERROR_NO_ENROLLED_FINGERPRINTS); + result = false; + } + return result; + } + + + + /** + * Activity that is necessary for authenticating w/ fingerprint sensor + */ + @TargetApi(Build.VERSION_CODES.M) + public static class FingerprintActivity extends Activity { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + handleFingerprint(); + finish(); + } + + /** + * Handle setup and listening of fingerprint sensor + */ + protected void handleFingerprint() { + FingerprintManager fingerprintManager = (FingerprintManager)getSystemService(Context.FINGERPRINT_SERVICE); + Cipher cipher = null; + boolean hasError = false; + + try { + KeyStore keyStore = KeyStore.getInstance(KEYSTORE_NAME); + generateKey(keyStore); + cipher = getCipher(); + keyStore.load(null); + SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null); + cipher.init(Cipher.ENCRYPT_MODE, key); + } catch (Exception e) { + TermuxApiLogger.error(TAG, e); + hasError = true; + } + + if (cipher != null && !hasError) { + authenticateWithFingerprint(this, getIntent(), fingerprintManager, cipher); + } + } + + /** + * Handles authentication callback from our fingerprint sensor + * @param context + * @param intent + * @param fingerprintManager + * @param cipher + */ + protected static void authenticateWithFingerprint(final Context context, final Intent intent, final FingerprintManager fingerprintManager, Cipher cipher) { + FingerprintManager.AuthenticationCallback authenticationCallback = new FingerprintManager.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, CharSequence errString) { + if (errorCode == FingerprintManager.FINGERPRINT_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); + postFingerprintResult(context, intent, fingerprintResult); + TermuxApiLogger.error(errString.toString()); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + setAuthResult(AUTH_RESULT_SUCCESS); + postFingerprintResult(context, intent, fingerprintResult); + } + + @Override + public void onAuthenticationFailed() { + addFailedAttempt(); + } + + // unused + @Override + public void onAuthenticationHelp(int helpCode, CharSequence helpString) { } + }; + + Toast.makeText(context, "Scan fingerprint", Toast.LENGTH_LONG).show(); + + // listen to fingerprint sensor + FingerprintManager.CryptoObject cryptoObject = new FingerprintManager.CryptoObject(cipher); + final CancellationSignal cancellationSignal = new CancellationSignal(); + fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, authenticationCallback, null); + + addSensorTimeout(context, intent, cancellationSignal); + } + + /** + * Adds a timeout for our fingerprint sensor which will force a result return if we + * haven't already received one + * @param context + * @param intent + * @param cancellationSignal + */ + protected static void addSensorTimeout(final Context context, final Intent intent, final CancellationSignal cancellationSignal) { + final Handler timeoutHandler = new Handler(Looper.getMainLooper()); + timeoutHandler.postDelayed(new Runnable() { + @Override + public void run() { + if (!postedResult) { + appendFingerprintError(ERROR_TIMEOUT); + cancellationSignal.cancel(); + postFingerprintResult(context, intent, fingerprintResult); + } + } + }, SENSOR_TIMEOUT); + } + + /** + * Generates our key + * @param keyStore + */ + protected static void generateKey(KeyStore keyStore) { + try { + KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE_NAME); + keyStore.load(null); + keyGenerator.init( + new KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) + .setBlockModes(KeyProperties.BLOCK_MODE_CBC) + .setUserAuthenticationRequired(true) + .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) + .build()); + + keyGenerator.generateKey(); + } catch (Exception e) { + TermuxApiLogger.error(TAG, e); + appendFingerprintError(ERROR_KEY_GENERATOR); + } + } + + /** + * Create the cipher needed for use with our SecretKey + * @return + */ + protected static Cipher getCipher() { + Cipher cipher = null; + try { + cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + + "/" + KeyProperties.BLOCK_MODE_CBC + + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7); + } catch (Exception e) { + TermuxApiLogger.error(TAG, e); + appendFingerprintError(ERROR_CIPHER); + } + return cipher; + } + } + + /** + * Clear out previous fingerprint result + */ + protected static void resetFingerprintResult() { + fingerprintResult = new FingerprintResult(); + } + + /** + * Increment failed authentication attempts + */ + protected static void addFailedAttempt() { + fingerprintResult.failedAttempts++; + } + + /** + * Add an error to our fingerprint result + * @param error + */ + protected static void appendFingerprintError(String error) { + fingerprintResult.errors.add(error); + } + + /** + * Set the final result of our authentication + * @param authResult + */ + protected static void setAuthResult(String authResult) { + fingerprintResult.authResult = authResult; + } + + + /** + * Simple class to encapsulate information about result of a fingerprint authentication attempt + */ + static class FingerprintResult { + public String authResult = AUTH_RESULT_UNKNOWN; + public int failedAttempts = 0; + public List errors = new ArrayList<>(); + } +} diff --git a/app/src/main/java/com/termux/api/TermuxApiReceiver.java b/app/src/main/java/com/termux/api/TermuxApiReceiver.java index e52351a1f..49b13801a 100644 --- a/app/src/main/java/com/termux/api/TermuxApiReceiver.java +++ b/app/src/main/java/com/termux/api/TermuxApiReceiver.java @@ -52,6 +52,9 @@ public void onReceive(Context context, Intent intent) { case "Download": DownloadAPI.onReceive(this, context, intent); break; + case "Fingerprint": + FingerprintAPI.onReceive(context, intent); + break; case "InfraredFrequencies": if (TermuxApiPermissionActivity.checkAndRequestPermissions(context, intent, Manifest.permission.TRANSMIT_IR)) { InfraredAPI.onReceiveCarrierFrequency(this, context, intent); From a5b81cf382e2cae7866fa4375afa66dffcf97ab1 Mon Sep 17 00:00:00 2001 From: David Kramer Date: Thu, 14 Jun 2018 14:24:06 -0600 Subject: [PATCH 2/2] Reset postedResult flag when resetFingerprintResult is called --- app/src/main/java/com/termux/api/FingerprintAPI.java | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/termux/api/FingerprintAPI.java b/app/src/main/java/com/termux/api/FingerprintAPI.java index dcf95bb9d..a26b73f05 100644 --- a/app/src/main/java/com/termux/api/FingerprintAPI.java +++ b/app/src/main/java/com/termux/api/FingerprintAPI.java @@ -301,6 +301,7 @@ protected static Cipher getCipher() { */ protected static void resetFingerprintResult() { fingerprintResult = new FingerprintResult(); + postedResult = false; } /**