aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
diff options
context:
space:
mode:
authorwongma7 <wongma7@users.noreply.github.com>2015-08-25 13:31:07 -0400
committerwongma7 <wongma7@users.noreply.github.com>2015-08-25 13:31:07 -0400
commit327945f3b8aa56f77559a25bc9160dae0a0cc6d2 (patch)
treec92d1450ee4930cf2b91a7b34efa5c97cee826b7 /app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
parenteced1dd314ba651f862396c340492c6f2d9da926 (diff)
parenta73e8625f55e869ba0d74bfa370178c54be3662d (diff)
downloadHardenedPass-327945f3b8aa56f77559a25bc9160dae0a0cc6d2.tar.gz
HardenedPass-327945f3b8aa56f77559a25bc9160dae0a0cc6d2.zip
Merge pull request #117 from zeapo/autofill
Autofill
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java274
1 files changed, 274 insertions, 0 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
new file mode 100644
index 00000000..ea09c863
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
@@ -0,0 +1,274 @@
+package com.zeapo.pwdstore.autofill;
+
+import android.accessibilityservice.AccessibilityService;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v7.app.AlertDialog;
+import android.util.Log;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityWindowInfo;
+import android.widget.Toast;
+
+import com.zeapo.pwdstore.R;
+import com.zeapo.pwdstore.utils.PasswordItem;
+import com.zeapo.pwdstore.utils.PasswordRepository;
+
+import org.apache.commons.io.FileUtils;
+import org.openintents.openpgp.OpenPgpError;
+import org.openintents.openpgp.util.OpenPgpApi;
+import org.openintents.openpgp.util.OpenPgpServiceConnection;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+
+public class AutofillService extends AccessibilityService {
+ private OpenPgpServiceConnection serviceConnection;
+ private SharedPreferences settings;
+ private AccessibilityNodeInfo info; // the original source of the event (the edittext field)
+ private ArrayList<PasswordItem> items; // password choices
+ private AlertDialog dialog;
+ private AccessibilityWindowInfo window;
+ private static boolean unlockOK = false; // if openkeychain user interaction was successful
+ private CharSequence packageName;
+ private boolean ignoreActionFocus = false;
+
+ public final class Constants {
+ public static final String TAG = "Keychain";
+ }
+
+ public static void setUnlockOK() { unlockOK = true; }
+
+ @Override
+ protected void onServiceConnected() {
+ super.onServiceConnected();
+ serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain");
+ serviceConnection.bindToService();
+ settings = PreferenceManager.getDefaultSharedPreferences(this);
+ }
+ // TODO change search/search results (just use first result)
+ @Override
+ public void onAccessibilityEvent(AccessibilityEvent event) {
+ // if returning to the source app from a successful AutofillActivity
+ if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ && event.getPackageName().equals(packageName) && unlockOK) {
+ decryptAndVerify();
+ }
+
+ // nothing to do if not password field focus, android version, or field is keychain app
+ if (!event.isPassword()
+ || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2
+ || event.getPackageName().equals("org.sufficientlysecure.keychain")) {
+ // the default keyboard showing/hiding is a window state changed event
+ // on Android 5+ we can use getWindows() to determine when the original window is not visible
+ // on Android 4.3 we have to use window state changed events and filter out the keyboard ones
+ // there may be other exceptions...
+ boolean dismiss;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ dismiss = !getWindows().contains(window);
+ } else {
+ dismiss = !(event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ && event.getPackageName().toString().contains("inputmethod"));
+ }
+ if (dismiss && dialog != null && dialog.isShowing()) {
+ dialog.dismiss();
+ }
+ return;
+ }
+
+ if (dialog != null && dialog.isShowing()) {
+ // if the view was clicked, the click event follows the focus event
+ // since the focus event was already handled, ignore click event
+ if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_CLICKED) {
+ return;
+ }
+ // if past this point, a new dialog will be created, so dismiss the existing
+ dialog.dismiss();
+ }
+
+ // ignore the ACTION_FOCUS from decryptAndVerify otherwise dialog will appear after Fill
+ if (ignoreActionFocus) {
+ ignoreActionFocus = false;
+ return;
+ }
+
+ info = event.getSource();
+
+ // save the dialog's corresponding window so we can use getWindows() above to check whether dismiss
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ window = info.getWindow();
+ }
+
+ // get the app name and find a corresponding password
+ PackageManager packageManager = getPackageManager();
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ applicationInfo = null;
+ }
+ final String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString();
+
+ // if autofill_default is checked and prefs.getString DNE, 'Automatically match with password'/"first" otherwise "never"
+ String defValue = settings.getBoolean("autofill_default", true) ? "/first" : "/never";
+ SharedPreferences prefs = getSharedPreferences("autofill", Context.MODE_PRIVATE);
+ String preference = prefs.getString(event.getPackageName().toString(), defValue);
+ switch (preference) {
+ case "/first":
+ if (!PasswordRepository.isInitialized()) {
+ PasswordRepository.initialize(this);
+ }
+ items = recursiveFilter(appName, null);
+ break;
+ case "/never":
+ return;
+ default:
+ if (!PasswordRepository.isInitialized()) {
+ PasswordRepository.initialize(this);
+ }
+ String path = PasswordRepository.getWorkTree() + "/" + preference + ".gpg";
+ File file = new File(path);
+ items = new ArrayList<>();
+ items.add(PasswordItem.newPassword(file.getName(), file, PasswordRepository.getRepositoryDirectory(this)));
+ }
+ if (items.isEmpty()) {
+ return;
+ }
+
+ if (dialog == null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
+ builder.setNegativeButton(R.string.dialog_cancel, null);
+ builder.setPositiveButton(R.string.autofill_fill, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ decryptAndVerify();
+ }
+ });
+ builder.setNeutralButton("Settings", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) { //TODO make icon? gear?
+ // the user will have to return to the app themselves.
+ Intent intent = new Intent(AutofillService.this, AutofillPreferenceActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra("packageName", info.getPackageName());
+ intent.putExtra("appName", appName);
+ startActivity(intent);
+ }
+ });
+ dialog = builder.create();
+ dialog.setIcon(R.drawable.ic_launcher);
+ dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
+ dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
+ dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
+ dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
+ }
+ dialog.setTitle(items.get(0).toString());
+ dialog.show();
+ }
+
+ private ArrayList<PasswordItem> recursiveFilter(String filter, File dir) {
+ ArrayList<PasswordItem> items = new ArrayList<>();
+ ArrayList<PasswordItem> passwordItems = dir == null ?
+ PasswordRepository.getPasswords(PasswordRepository.getRepositoryDirectory(this)) :
+ PasswordRepository.getPasswords(dir, PasswordRepository.getRepositoryDirectory(this));
+ for (PasswordItem item : passwordItems) {
+ if (item.getType() == PasswordItem.TYPE_CATEGORY) {
+ items.addAll(recursiveFilter(filter, item.getFile()));
+ }
+ if (item.toString().toLowerCase().contains(filter.toLowerCase())) {
+ items.add(item);
+ }
+ }
+ return items;
+ }
+
+ @Override
+ public void onInterrupt() {
+
+ }
+
+ public void decryptAndVerify() {
+ unlockOK = false;
+ packageName = info.getPackageName();
+ Intent data = new Intent();
+ data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY);
+ InputStream is = null;
+ try {
+ is = FileUtils.openInputStream(items.get(0).getFile());
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ ByteArrayOutputStream os = new ByteArrayOutputStream();
+
+ OpenPgpApi api = new OpenPgpApi(AutofillService.this, serviceConnection.getService());
+ Intent result = api.executeApi(data, is, os);
+ switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
+ case OpenPgpApi.RESULT_CODE_SUCCESS: {
+ try {
+ String[] passContent = os.toString("UTF-8").split("\n");
+
+ // if the user focused on something else, take focus back
+ // but this will open another dialog...hack to ignore this
+ ignoreActionFocus = info.performAction(AccessibilityNodeInfo.ACTION_FOCUS);
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Bundle args = new Bundle();
+ args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE,
+ passContent[0]);
+ info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
+ } else {
+ ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]);
+ clipboard.setPrimaryClip(clip);
+ info.performAction(AccessibilityNodeInfo.ACTION_PASTE);
+
+ clip = ClipData.newPlainText("autofill_pm", "MyPasswordIsDaBest!");
+ clipboard.setPrimaryClip(clip);
+ if (settings.getBoolean("clear_clipboard_20x", false)) {
+ for (int i = 0; i < 19; i++) {
+ clip = ClipData.newPlainText(String.valueOf(i), String.valueOf(i));
+ clipboard.setPrimaryClip(clip);
+ }
+ }
+ }
+ } catch (UnsupportedEncodingException e) {
+ Log.e(Constants.TAG, "UnsupportedEncodingException", e);
+ }
+ break;
+ }
+ case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
+ Log.i("PgpHandler", "RESULT_CODE_USER_INTERACTION_REQUIRED");
+ PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);
+ // need to start a blank activity to call startIntentSenderForResult
+ Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
+ intent.putExtra("pending_intent", pi);
+ startActivity(intent);
+ break;
+ }
+ case OpenPgpApi.RESULT_CODE_ERROR: {
+ OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
+ Toast.makeText(AutofillService.this,
+ "Error from OpenKeyChain : " + error.getMessage(),
+ Toast.LENGTH_LONG).show();
+ Log.e(Constants.TAG, "onError getErrorId:" + error.getErrorId());
+ Log.e(Constants.TAG, "onError getMessage:" + error.getMessage());
+ break;
+ }
+ }
+ }
+}