From 02bfcf6c3f5ddd806dd9a92823926eced139842f Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Tue, 28 Jul 2015 03:11:02 -0400 Subject: Create app autofill service: a dialog pops up for all password fields & has a button to paste/set a password found in the store with name matching app's --- .../java/com/zeapo/pwdstore/AutofillActivity.java | 33 ++++ .../java/com/zeapo/pwdstore/AutofillService.java | 200 +++++++++++++++++++++ 2 files changed, 233 insertions(+) create mode 100644 app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java create mode 100644 app/src/main/java/com/zeapo/pwdstore/AutofillService.java (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java new file mode 100644 index 00000000..d7561029 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java @@ -0,0 +1,33 @@ +package com.zeapo.pwdstore; + +import android.app.PendingIntent; +import android.content.Intent; +import android.content.IntentSender; +import android.os.Bundle; +import android.support.v7.app.AppCompatActivity; +import android.util.Log; + + +public class AutofillActivity extends AppCompatActivity { + public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; + private boolean bound = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + Intent intent = getIntent(); + PendingIntent pi = intent.getExtras().getParcelable("pending_intent"); + try { + startIntentSenderForResult(pi.getIntentSender() + , REQUEST_CODE_DECRYPT_AND_VERIFY, null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(AutofillService.Constants.TAG, "SendIntentException", e); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + finish(); + AutofillService.getService().decryptAndVerify(); + } +} diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java new file mode 100644 index 00000000..ca738c67 --- /dev/null +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java @@ -0,0 +1,200 @@ +package com.zeapo.pwdstore; + +import android.accessibilityservice.AccessibilityService; +import android.app.PendingIntent; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +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.View; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.Button; +import android.widget.Toast; + +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 items; + private static AutofillService service; + + public final class Constants { + public static final String TAG = "Keychain"; + } + + @Override + protected void onServiceConnected() { + super.onServiceConnected(); + serviceConnection = new OpenPgpServiceConnection(AutofillService.this, "org.sufficientlysecure.keychain"); + serviceConnection.bindToService(); + settings = PreferenceManager.getDefaultSharedPreferences(this); + service = this; + } + + public static AutofillService getService() { + return service; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if (!event.isPassword() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + return; + } + info = event.getSource(); + PackageManager packageManager = getPackageManager(); + ApplicationInfo applicationInfo; + try { + applicationInfo = packageManager.getApplicationInfo(event.getPackageName().toString(), 0); + } catch (PackageManager.NameNotFoundException e) { + applicationInfo = null; + } + String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); + if (appName.equals("OpenKeychain")) { + return; + } + items = recursiveFilter(appName, null); + if (items.isEmpty()) { + return; + } + ArrayList itemNames = new ArrayList<>(); + for (PasswordItem item : items) { + itemNames.add(item.toString()); + } + + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Light_Dialog_Alert); + builder.setNegativeButton("Cancel", null); + builder.setView(R.layout.autofill_layout); + final AlertDialog dialog = builder.create(); + + dialog.setTitle("Fill with Password Store"); + 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.show(); + ((Button) dialog.findViewById(R.id.button)).setText(itemNames.get(0).toString()); + dialog.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + decryptAndVerify(); + dialog.dismiss(); + } + }); + } + + private ArrayList recursiveFilter(String filter, File dir) { + ArrayList items = new ArrayList<>(); + if (!PasswordRepository.isInitialized()) { + return items; + } + ArrayList passwordItems = dir == null ? + PasswordRepository.getPasswords() : + PasswordRepository.getPasswords(dir); + for (PasswordItem item : passwordItems) { + if (item.getType() == PasswordItem.TYPE_CATEGORY) { + recursiveFilter(filter, item.getFile()); + } + if (item.toString().toLowerCase().contains(filter.toLowerCase())) { + items.add(item); + } + } + return items; + } + + @Override + public void onInterrupt() { + + } + + public void decryptAndVerify() { + 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + // if the user focused on something else, take focus back + // but this will open another dialog... + info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); + Bundle args = new Bundle(); + args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, + passContent[0]); + info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); + } else { + info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); + 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.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + 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; + } + } + } +} -- cgit v1.2.3 From e937ddf11121e919d27bcb9fd3c3e11e88e3ba3c Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Tue, 28 Jul 2015 03:33:04 -0400 Subject: Only try again paste/set after decryption if the correct password was entered and not e.g. cancel button pressed --- app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java index d7561029..f52fe0a2 100644 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java @@ -28,6 +28,8 @@ public class AutofillActivity extends AppCompatActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { finish(); - AutofillService.getService().decryptAndVerify(); + if (resultCode == RESULT_OK) { + AutofillService.getService().decryptAndVerify(); + } } } -- cgit v1.2.3 From 8c266187cb1a1264034fe2637d3b22e67284b0b5 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Tue, 28 Jul 2015 09:15:41 -0400 Subject: Fill in field immediately after user unlocks --- .../java/com/zeapo/pwdstore/AutofillActivity.java | 6 +- .../java/com/zeapo/pwdstore/AutofillService.java | 65 ++++++++++++---------- app/src/main/res/xml/autofill_config.xml | 2 +- 3 files changed, 40 insertions(+), 33 deletions(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java index f52fe0a2..2a1227b9 100644 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillActivity.java @@ -7,7 +7,7 @@ import android.os.Bundle; import android.support.v7.app.AppCompatActivity; import android.util.Log; - +// blank activity started by service for calling startIntentSenderForResult public class AutofillActivity extends AppCompatActivity { public static final int REQUEST_CODE_DECRYPT_AND_VERIFY = 9913; private boolean bound = false; @@ -27,9 +27,9 @@ public class AutofillActivity extends AppCompatActivity { @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - finish(); + finish(); // go back to the password field app if (resultCode == RESULT_OK) { - AutofillService.getService().decryptAndVerify(); + AutofillService.setUnlockOK(); // report the result to service } } } diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java index ca738c67..58bb0994 100644 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java @@ -40,29 +40,40 @@ 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 items; - private static AutofillService service; + private ArrayList items; // password choices + private AlertDialog dialog; + private static boolean unlockOK = false; // if openkeychain user interaction was successful + private static CharSequence packageName; + private static 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); - service = this; - } - - public static AutofillService getService() { - return service; } @Override public void onAccessibilityEvent(AccessibilityEvent event) { - if (!event.isPassword() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED + && event.getPackageName().equals(packageName) && unlockOK) { + decryptAndVerify(); + } + if (!event.isPassword() + || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 + || (dialog != null && dialog.isShowing()) + || event.getPackageName().equals("org.sufficientlysecure.keychain")) { + return; + } + if (ignoreActionFocus) { + ignoreActionFocus = false; return; } info = event.getSource(); @@ -74,29 +85,23 @@ public class AutofillService extends AccessibilityService { applicationInfo = null; } String appName = (applicationInfo != null ? packageManager.getApplicationLabel(applicationInfo) : "").toString(); - if (appName.equals("OpenKeychain")) { - return; - } items = recursiveFilter(appName, null); if (items.isEmpty()) { return; } - ArrayList itemNames = new ArrayList<>(); - for (PasswordItem item : items) { - itemNames.add(item.toString()); - } - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Light_Dialog_Alert); - builder.setNegativeButton("Cancel", null); - builder.setView(R.layout.autofill_layout); - final AlertDialog dialog = builder.create(); - - dialog.setTitle("Fill with Password Store"); - dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); - dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE); - dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + if (dialog == null) { + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Light_Dialog_Alert); + builder.setNegativeButton("Cancel", null); + builder.setView(R.layout.autofill_layout); + dialog = builder.create(); + dialog.setTitle("Fill with Password Store"); + 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.show(); - ((Button) dialog.findViewById(R.id.button)).setText(itemNames.get(0).toString()); + ((Button) dialog.findViewById(R.id.button)).setText(items.get(0).getName()); dialog.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -131,6 +136,8 @@ public class AutofillService extends AccessibilityService { } public void decryptAndVerify() { + unlockOK = false; + packageName = info.getPackageName(); Intent data = new Intent(); data.setAction(OpenPgpApi.ACTION_DECRYPT_VERIFY); InputStream is = null; @@ -147,16 +154,16 @@ public class AutofillService extends AccessibilityService { 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 = true; + info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - // if the user focused on something else, take focus back - // but this will open another dialog... - info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); Bundle args = new Bundle(); args.putCharSequence(AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE, passContent[0]); info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args); } else { - info.performAction(AccessibilityNodeInfo.ACTION_FOCUS); ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); ClipData clip = ClipData.newPlainText("autofill_pm", passContent[0]); clipboard.setPrimaryClip(clip); diff --git a/app/src/main/res/xml/autofill_config.xml b/app/src/main/res/xml/autofill_config.xml index 618442ab..93125a51 100644 --- a/app/src/main/res/xml/autofill_config.xml +++ b/app/src/main/res/xml/autofill_config.xml @@ -1,6 +1,6 @@ Date: Tue, 28 Jul 2015 09:59:28 -0400 Subject: Dismiss dialog if non password field or window change (the accessibility events already needed to be handled) --- app/src/main/java/com/zeapo/pwdstore/AutofillService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java index 58bb0994..1636702a 100644 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java @@ -43,8 +43,8 @@ public class AutofillService extends AccessibilityService { private ArrayList items; // password choices private AlertDialog dialog; private static boolean unlockOK = false; // if openkeychain user interaction was successful - private static CharSequence packageName; - private static boolean ignoreActionFocus = false; + private CharSequence packageName; + private boolean ignoreActionFocus = false; public final class Constants { public static final String TAG = "Keychain"; @@ -68,10 +68,15 @@ public class AutofillService extends AccessibilityService { } if (!event.isPassword() || Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2 - || (dialog != null && dialog.isShowing()) || event.getPackageName().equals("org.sufficientlysecure.keychain")) { + if (dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } return; } + if (!event.getSource().equals(info) && dialog != null && dialog.isShowing()) { + dialog.dismiss(); + } if (ignoreActionFocus) { ignoreActionFocus = false; return; -- cgit v1.2.3 From 485b621b042041fb92f9b1c0765ef2edd83745f4 Mon Sep 17 00:00:00 2001 From: Matthew Wong Date: Tue, 28 Jul 2015 11:48:42 -0400 Subject: Change dialog appearance, smaller (wraps content) --- .../java/com/zeapo/pwdstore/AutofillService.java | 25 +++++++++++----------- app/src/main/res/layout/autofill_layout.xml | 12 ----------- 2 files changed, 12 insertions(+), 25 deletions(-) delete mode 100644 app/src/main/res/layout/autofill_layout.xml (limited to 'app/src/main/java') diff --git a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java index 1636702a..5093dac3 100644 --- a/app/src/main/java/com/zeapo/pwdstore/AutofillService.java +++ b/app/src/main/java/com/zeapo/pwdstore/AutofillService.java @@ -5,6 +5,7 @@ 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; @@ -14,11 +15,9 @@ import android.os.Bundle; import android.preference.PreferenceManager; import android.support.v7.app.AlertDialog; import android.util.Log; -import android.view.View; import android.view.WindowManager; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityNodeInfo; -import android.widget.Button; import android.widget.Toast; import com.zeapo.pwdstore.utils.PasswordItem; @@ -96,24 +95,24 @@ public class AutofillService extends AccessibilityService { } if (dialog == null) { - AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Light_Dialog_Alert); + AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog); builder.setNegativeButton("Cancel", null); - builder.setView(R.layout.autofill_layout); + builder.setPositiveButton("Fill", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + decryptAndVerify(); + } + }); dialog = builder.create(); - dialog.setTitle("Fill with Password Store"); + 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.setTitle(items.get(0).getName()); dialog.show(); - ((Button) dialog.findViewById(R.id.button)).setText(items.get(0).getName()); - dialog.findViewById(R.id.button).setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - decryptAndVerify(); - dialog.dismiss(); - } - }); + dialog.getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); } private ArrayList recursiveFilter(String filter, File dir) { diff --git a/app/src/main/res/layout/autofill_layout.xml b/app/src/main/res/layout/autofill_layout.xml deleted file mode 100644 index 0108160d..00000000 --- a/app/src/main/res/layout/autofill_layout.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - -