Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 114 additions & 40 deletions app/src/main/java/com/zeapo/pwdstore/autofill/AutofillService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.zeapo.pwdstore.autofill;

import android.accessibilityservice.AccessibilityService;
import android.annotation.TargetApi;
import android.app.PendingIntent;
import android.content.ClipData;
import android.content.ClipboardManager;
Expand Down Expand Up @@ -58,16 +59,20 @@ public class AutofillService extends AccessibilityService {
private boolean ignoreActionFocus = false;
private String webViewTitle = null;
private String webViewURL = null;
private PasswordEntry lastPassword;
private long lastPasswordMaxDate;

public final class Constants {
public static final String TAG = "Keychain";
final class Constants {
static final String TAG = "Keychain";
}

public static AutofillService getInstance() {
return instance;
}

public void setResultData(Intent data) { resultData = data; }
public void setResultData(Intent data) {
resultData = data;
}

public void setPickedPassword(String path) {
items.add(new File(PasswordRepository.getRepositoryDirectory(getApplicationContext()) + "/" + path + ".gpg"));
Expand Down Expand Up @@ -96,6 +101,11 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
return;
}

// remove stored password from cache
if (lastPassword != null && System.currentTimeMillis() > lastPasswordMaxDate) {
lastPassword = null;
}

// if returning to the source app from a successful AutofillActivity
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
&& event.getPackageName() != null && event.getPackageName().equals(packageName)
Expand All @@ -107,9 +117,9 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
// or if page changes in chrome
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
|| (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
&& event.getPackageName() != null
&& (event.getPackageName().equals("com.android.chrome")
|| event.getPackageName().equals("com.android.browser")))) {
&& event.getPackageName() != null
&& (event.getPackageName().equals("com.android.chrome")
|| event.getPackageName().equals("com.android.browser")))) {
// there is a chance for getRootInActiveWindow() to return null at any time. save it.
try {
AccessibilityNodeInfo root = getRootInActiveWindow();
Expand Down Expand Up @@ -140,15 +150,25 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
}
}

// nothing to do if not password field focus, field is keychain app
if (!event.isPassword()
|| event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
// nothing to do if field is keychain app or system ui
if (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
|| event.getPackageName() != null && event.getPackageName().equals("org.sufficientlysecure.keychain")
|| event.getPackageName() != null && event.getPackageName().equals("com.android.systemui")) {
dismissDialog(event);
return;
}

if (!event.isPassword()) {
if (lastPassword != null && event.getEventType() == AccessibilityEvent.TYPE_VIEW_FOCUSED && event.getSource().isEditable()) {
showPasteUsernameDialog(event.getSource(), lastPassword);
return;
} else {
// nothing to do if not password field focus
dismissDialog(event);
return;
}
}

if (dialog != null && dialog.isShowing()) {
// the current dialog must belong to this window; ignore clicks on this password field
// why handle clicks at all then? some cases e.g. Paypal there is no initial focus event
Expand Down Expand Up @@ -220,8 +240,9 @@ public void onAccessibilityEvent(AccessibilityEvent event) {
if (items.isEmpty() && !settings.getBoolean("autofill_always", false)) {
return;
}
showDialog(packageName, appName, isWeb);
showSelectPasswordDialog(packageName, appName, isWeb);
}

private String searchWebView(AccessibilityNodeInfo source) {
return searchWebView(source, 10);
}
Expand Down Expand Up @@ -282,13 +303,16 @@ private String setWebMatchingPasswords(String webViewTitle, String webViewURL) {
prefs = getSharedPreferences("autofill_web", Context.MODE_PRIVATE);
preference = defValue;
if (webViewURL != null) {
final String webViewUrlLowerCase = webViewURL.toLowerCase();
Map<String, ?> prefsMap = prefs.getAll();
for (String key : prefsMap.keySet()) {
// for websites unlike apps there can be blank preference of "" which
// means use default, so ignore it.
if ((webViewURL.toLowerCase().contains(key.toLowerCase()) || key.toLowerCase().contains(webViewURL.toLowerCase()))
&& !prefs.getString(key, null).equals("")) {
preference = prefs.getString(key, null);
final String value = prefs.getString(key, null);
final String keyLowerCase = key.toLowerCase();
if (value != null && !value.equals("")
&& (webViewUrlLowerCase.contains(keyLowerCase) || keyLowerCase.contains(webViewUrlLowerCase))) {
preference = value;
settingsURL = key;
}
}
Expand Down Expand Up @@ -374,7 +398,44 @@ private ArrayList<File> searchPasswords(File path, String appName) {
return items;
}

private void showDialog(final String packageName, final String appName, final boolean isWeb) {
private void showPasteUsernameDialog(final AccessibilityNodeInfo node, final PasswordEntry password) {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}

AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
dialog.dismiss();
dialog = null;
}
});
builder.setPositiveButton(R.string.autofill_paste, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface d, int which) {
pasteText(node, password.getUsername());
dialog.dismiss();
dialog = null;
}
});
builder.setMessage(getString(R.string.autofill_paste_username, password.getUsername()));

dialog = builder.create();
//noinspection ConstantConditions
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();
}

private void showSelectPasswordDialog(final String packageName, final String appName, final boolean isWeb) {
if (dialog != null) {
dialog.dismiss();
dialog = null;
}

AlertDialog.Builder builder = new AlertDialog.Builder(this, R.style.Theme_AppCompat_Dialog);
builder.setNegativeButton(R.string.dialog_cancel, new DialogInterface.OnClickListener() {
@Override
Expand Down Expand Up @@ -410,7 +471,7 @@ public void onClick(DialogInterface dialog, int which) {
lastWhichItem = which;
if (which < items.size()) {
bindDecryptAndVerify();
} else if (which == items.size()){
} else if (which == items.size()) {
Intent intent = new Intent(AutofillService.this, AutofillActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra("pick", true);
Expand All @@ -428,6 +489,7 @@ public void onClick(DialogInterface dialog, int which) {
});

dialog = builder.create();
//noinspection ConstantConditions
dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
dialog.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
dialog.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND);
Expand All @@ -451,6 +513,7 @@ private class onBoundListener implements OpenPgpServiceConnection.OnBound {
public void onBound(IOpenPgpService2 service) {
decryptAndVerify();
}

@Override
public void onError(Exception e) {
e.printStackTrace();
Expand Down Expand Up @@ -494,32 +557,15 @@ private void decryptAndVerify() {
case OpenPgpApi.RESULT_CODE_SUCCESS: {
try {
final PasswordEntry entry = new PasswordEntry(os);

// if the user focused on something else, take focus back
// but this will open another dialog...hack to ignore this
// & need to ensure performAction correct (i.e. what is info now?)
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,
entry.getPassword());
info.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
} else {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("autofill_pm", entry.getPassword());
clipboard.setPrimaryClip(clip);
info.performAction(AccessibilityNodeInfo.ACTION_PASTE);

clip = ClipData.newPlainText("autofill_pm", "");
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);
}
}
pasteText(info, entry.getPassword());

// save password entry for pasting the username as well
if (entry.hasUsername()) {
lastPassword = entry;
final int ttl = Integer.parseInt(settings.getString("general_show_time", "45"));
Toast.makeText(this, getString(R.string.autofill_toast_username, ttl), Toast.LENGTH_LONG).show();
lastPasswordMaxDate = System.currentTimeMillis() + ttl * 1000L;
}
info.recycle();
} catch (UnsupportedEncodingException e) {
Log.e(Constants.TAG, "UnsupportedEncodingException", e);
}
Expand All @@ -546,4 +592,32 @@ private void decryptAndVerify() {
}
}
}

@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
private void pasteText(final AccessibilityNodeInfo node, final String text) {
// if the user focused on something else, take focus back
// but this will open another dialog...hack to ignore this
// & need to ensure performAction correct (i.e. what is info now?)
ignoreActionFocus = node.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, text);
node.performAction(AccessibilityNodeInfo.ACTION_SET_TEXT, args);
} else {
ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("autofill_pm", text);
clipboard.setPrimaryClip(clip);
node.performAction(AccessibilityNodeInfo.ACTION_PASTE);

clip = ClipData.newPlainText("autofill_pm", "");
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);
}
}
}
node.recycle();
}
}
12 changes: 8 additions & 4 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@
<string name="show_extra_content_pref_title">Zeige weiteren Inhalt</string>
<string name="show_extra_content_pref_summary">Soll weiterer Inhalt sichtbar sein?</string>
<string name="pwd_generate_button">Generieren</string>
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
<string name="show_password">Password wiedergeben</string>
<string name="repository_uri">Repository URI</string>

<!-- Autofill -->
<string name="autofill_description">Füge das Passwort automatisch in Apps ein (Autofill). Funktioniert nur unter Android 4.3 und höher. Dies basiert nicht auf der Zwischenablage für Android 5.0 oder höher.</string>
Expand All @@ -180,8 +185,7 @@
<string name="autofill_apps_delete">Löschen</string>
<string name="autofill_pick">Auswählen…</string>
<string name="autofill_pick_and_match">Auswählen und merken…</string>
<string name="no_repo_selected">Kein externes Repository ausgewählt</string>
<string name="edit_commit_text">[ANDROID PwdStore] Edit &#160;</string>
<string name="send_plaintext_password_to">Passwort senden als Nur-Text mit behilfe von…</string>
<string name="show_password">Password wiedergeben</string>
<string name="autofill_paste">Einfügen</string>
<string name="autofill_paste_username">Benutzername einfügen?\n\n%s</string>
<string name="autofill_toast_username">Wähle ein editierbares Feld um den Benutzernamen einzufügen.\nDer Benutzername ist für %d Sekunden verfügbar.</string>
</resources>
13 changes: 8 additions & 5 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,11 @@
<string name="show_extra_content_pref_title">Show extra content</string>
<string name="show_extra_content_pref_summary">Control the visibility of the extra content once decrypted</string>
<string name="pwd_generate_button">Generate</string>
<string name="refresh_list">Refresh list</string>
<string name="no_repo_selected">No external repository selected</string>
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
<string name="show_password">Show password</string>
<string name="repository_uri">Repository URI</string>

<!-- Autofill -->
<string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string>
Expand All @@ -188,9 +193,7 @@
<string name="autofill_apps_delete">Delete</string>
<string name="autofill_pick">Pick…</string>
<string name="autofill_pick_and_match">Pick and match…</string>
<string name="refresh_list">Refresh list</string>
<string name="no_repo_selected">No external repository selected</string>
<string name="send_plaintext_password_to">Send password as plaintext using…</string>
<string name="show_password">Show password</string>
<string name="repository_uri">Repository URI</string>
<string name="autofill_paste">Paste</string>
<string name="autofill_paste_username">Paste username?\n\n%s</string>
<string name="autofill_toast_username">Select an editable field to past the username.\nUsername is available for %d seconds.</string>
</resources>