From 61e0379eb310fa6bbeb8365bd311baf76fe8ca8f Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 23 Jan 2017 23:32:52 -0500 Subject: [PATCH] autofill cleanup WIP --- src/Android/AutofillActivity.cs | 45 ++-- src/Android/AutofillSelectLoginActivity.cs | 6 +- src/Android/AutofillService.cs | 237 ++++++++++----------- 3 files changed, 135 insertions(+), 153 deletions(-) diff --git a/src/Android/AutofillActivity.cs b/src/Android/AutofillActivity.cs index 217fbb5e5..7c69d1cb7 100644 --- a/src/Android/AutofillActivity.cs +++ b/src/Android/AutofillActivity.cs @@ -7,58 +7,59 @@ using Android.App; using Android.Content; using Android.OS; using Android.Runtime; -using Android.Views; -using Android.Widget; using Bit.App.Models; namespace Bit.Android { - //[Activity(Label = "Autofill", LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance, Theme = "@style/android:Theme.Material.Light")] + [Activity(Label = "bitwarden Autofill", + LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance, + Theme = "@style/android:Theme.Material.Light")] public class AutofillActivity : Activity { + private string _lastQueriedUri; + + public static Credentials LastCredentials; + protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); + _lastQueriedUri = Intent.GetStringExtra("uri"); - var url = Intent.GetStringExtra("url"); - _lastQueriedUrl = url; - - Intent intent = new Intent(this, typeof(AutofillSelectLoginActivity)); - intent.PutExtra("url", url); + var intent = new Intent(this, typeof(AutofillSelectLoginActivity)); + intent.PutExtra("uri", _lastQueriedUri); StartActivityForResult(intent, 123); } - string _lastQueriedUrl; protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) { - var url = data.GetStringExtra("url"); - var username = data.GetStringExtra("username"); - var password = data.GetStringExtra("password"); - base.OnActivityResult(requestCode, resultCode, data); try { - LastReceivedCredentials = new Credentials { User = username, Password = password, Url = _lastQueriedUrl }; - } - catch(Exception e) - { - //Android.Util.Log.Debug("KP2AAS", "Exception while receiving credentials: " + e.ToString()); + var uri = data.GetStringExtra("uri"); + var username = data.GetStringExtra("username"); + var password = data.GetStringExtra("password"); + + LastCredentials = new Credentials + { + User = username, + Password = password, + Uri = _lastQueriedUri + }; } + catch { } finally { Finish(); } } - public static Credentials LastReceivedCredentials; - public class Credentials { public string User; public string Password; - public string Url; + public string Uri; } } -} \ No newline at end of file +} diff --git a/src/Android/AutofillSelectLoginActivity.cs b/src/Android/AutofillSelectLoginActivity.cs index 01e3cc800..8277b283a 100644 --- a/src/Android/AutofillSelectLoginActivity.cs +++ b/src/Android/AutofillSelectLoginActivity.cs @@ -9,16 +9,16 @@ using Android.OS; namespace Bit.Android { - //[Activity(LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance)] + [Activity(LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance)] public class AutofillSelectLoginActivity : Activity { protected override void OnCreate(Bundle bundle) { base.OnCreate(bundle); - var url = Intent.GetStringExtra("url"); + var uri = Intent.GetStringExtra("uri"); Intent data = new Intent(); - data.PutExtra("url", url); + data.PutExtra("uri", uri); data.PutExtra("username", "user123"); data.PutExtra("password", "pass123"); diff --git a/src/Android/AutofillService.cs b/src/Android/AutofillService.cs index a232e70b5..8ca5b55d8 100644 --- a/src/Android/AutofillService.cs +++ b/src/Android/AutofillService.cs @@ -1,102 +1,80 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; using Android.AccessibilityServices; using Android.App; using Android.Content; -using Android.Graphics; using Android.OS; -using Android.Runtime; -using Android.Views; using Android.Views.Accessibility; -using Android.Widget; namespace Bit.Android { - //[Service(Permission = "android.permission.BIND_ACCESSIBILITY_SERVICE", Label = "bitwarden")] - //[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })] - //[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")] + [Service(Permission = "android.permission.BIND_ACCESSIBILITY_SERVICE", Label = "bitwarden")] + [IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })] + [MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")] public class AutofillService : AccessibilityService { - private const int autoFillNotificationId = 0; - private const string androidAppPrefix = "androidapp://"; + private const int AutoFillNotificationId = 34573; + private const string AndroidAppProtocol = "androidapp://"; + private const string SystemUiPackage = "com.android.systemui"; + private const string ChromePackage = "com.android.chrome"; + private const string BrowserPackage = "com.android.browser"; public override void OnAccessibilityEvent(AccessibilityEvent e) { var eventType = e.EventType; - System.Diagnostics.Debug.WriteLine(DateTime.UtcNow.ToString() + " Event Type: " + eventType); - var package = e.PackageName; + var packageName = e.PackageName; + + if(packageName == SystemUiPackage) + { + return; + } + switch(eventType) { - case EventTypes.ViewTextSelectionChanged: - //if(e.Source.Password && string.IsNullOrWhiteSpace(e.Source.Text)) - //{ - // var bundle = new Bundle(); - // bundle.PutCharSequence(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, "mypassword"); - // e.Source.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle); - //} - break; case EventTypes.WindowContentChanged: case EventTypes.WindowStateChanged: - if(e.PackageName == "com.android.systemui") - { - break; - } var root = RootInActiveWindow; - if((ExistsNodeOrChildren(root, n => n.WindowId == e.WindowId) && !ExistsNodeOrChildren(root, n => (n.ViewIdResourceName != null) && (n.ViewIdResourceName.StartsWith("com.android.systemui"))))) + var isChrome = root == null ? false : root.PackageName == ChromePackage; + var cancelNotification = true; + var avialablePasswordNodes = GetNodeOrChildren(root, n => AvailablePasswordField(n, isChrome)); + + if(avialablePasswordNodes.Any() && AnyNodeOrChildren(root, n => n.WindowId == e.WindowId && + !(n.ViewIdResourceName != null && n.ViewIdResourceName.StartsWith(SystemUiPackage)))) { - bool cancelNotification = true; - - var allEditTexts = GetNodeOrChildren(root, n => { return IsEditText(n); }); - - var usernameEdit = allEditTexts.TakeWhile(edit => (edit.Password == false)).LastOrDefault(); - - string searchString = androidAppPrefix + root.PackageName; - - string url = androidAppPrefix + root.PackageName; - - if(root.PackageName == "com.android.chrome") + var uri = string.Concat(AndroidAppProtocol, root.PackageName); + if(isChrome) { - var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar").FirstOrDefault(); - UrlFromAddressField(ref url, addressField); + var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar") + .FirstOrDefault(); + uri = ExtractUriFromAddressField(uri, addressNode); } - else if(root.PackageName == "com.android.browser") + else if(root.PackageName == BrowserPackage) { - var addressField = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url").FirstOrDefault(); - UrlFromAddressField(ref url, addressField); + var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url") + .FirstOrDefault(); + uri = ExtractUriFromAddressField(uri, addressNode); } - var emptyPasswordFields = GetNodeOrChildren(root, n => { return IsPasswordField(n); }).ToList(); - if(emptyPasswordFields.Any()) + var allEditTexts = GetNodeOrChildren(root, n => EditText(n)); + var usernameEditText = allEditTexts.TakeWhile(n => !n.Password).LastOrDefault(); + + if(AutofillActivity.LastCredentials != null && SameUri(AutofillActivity.LastCredentials.Uri, uri)) { - if((AutofillActivity.LastReceivedCredentials != null) && IsSame(AutofillActivity.LastReceivedCredentials.Url, url)) - { - //Android.Util.Log.Debug("KP2AAS", "Filling credentials for " + url); - - FillPassword(url, usernameEdit, emptyPasswordFields); - } - else - { - //Android.Util.Log.Debug("KP2AAS", "Notif for " + url); - if(AutofillActivity.LastReceivedCredentials != null) - { - //Android.Util.Log.Debug("KP2AAS", LookupCredentialsActivity.LastReceivedCredentials.Url); - //Android.Util.Log.Debug("KP2AAS", url); - } - - AskFillPassword(url, usernameEdit, emptyPasswordFields); - cancelNotification = false; - } - + FillCredentials(usernameEditText, avialablePasswordNodes); } - if(cancelNotification) + else { - ((NotificationManager)GetSystemService(NotificationService)).Cancel(autoFillNotificationId); - //Android.Util.Log.Debug("KP2AAS", "Cancel notif"); + AskFillPassword(uri, usernameEditText, avialablePasswordNodes); + cancelNotification = false; } } + + if(cancelNotification) + { + ((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId); + } break; default: break; @@ -108,66 +86,69 @@ namespace Bit.Android } - private static void UrlFromAddressField(ref string url, AccessibilityNodeInfo addressField) + private string ExtractUriFromAddressField(string uri, AccessibilityNodeInfo addressNode) { - if(addressField != null) + if(addressNode != null) { - url = addressField.Text; - if(!url.Contains("://")) - url = "http://" + url; + uri = addressNode.Text; + if(!uri.Contains("://")) + { + uri = string.Concat("http://", uri); + } } + return uri; } - private bool IsSame(string url1, string url2) + private bool SameUri(string uriString1, string uriString2) { - if(url1.StartsWith("androidapp://")) - return url1 == url2; - // TODO: host check - return url1 == url2; - //return false; - } - - private static bool IsPasswordField(AccessibilityNodeInfo n) - { - //if (n.Password) Android.Util.Log.Debug(_logTag, "pwdx with " + (n.Text == null ? "null" : n.Text)); - var res = n.Password && string.IsNullOrEmpty(n.Text); - // if (n.Password) Android.Util.Log.Debug(_logTag, "pwd with " + n.Text + res); - return res; - } - - private static bool IsEditText(AccessibilityNodeInfo n) - { - //it seems like n.Editable is not a good check as this is false for some fields which are actually editable, at least in tests with Chrome. - return (n.ClassName != null) && (n.ClassName.Contains("EditText")); - } - - private void AskFillPassword(string url, AccessibilityNodeInfo usernameEdit, IEnumerable passwordFields) - { - var runSearchIntent = new Intent(this, typeof(AutofillActivity)); - runSearchIntent.PutExtra("url", url); - runSearchIntent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); - var pending = PendingIntent.GetActivity(this, 0, runSearchIntent, PendingIntentFlags.UpdateCurrent); - - var targetName = url; - - if(url.StartsWith(androidAppPrefix)) + Uri uri1, uri2; + if(Uri.TryCreate(uriString1, UriKind.RelativeOrAbsolute, out uri1) && + Uri.TryCreate(uriString2, UriKind.RelativeOrAbsolute, out uri2) && uri1.Host == uri2.Host) { - var packageName = url.Substring(androidAppPrefix.Length); + return true; + } + + return false; + } + + private static bool AvailablePasswordField(AccessibilityNodeInfo n, bool isChrome) + { + // chrome sends password field values in many conditions when the field is still actually empty + // ex. placeholders, nearby label, etc + return n.Password && (isChrome || string.IsNullOrWhiteSpace(n.Text)); + } + + private static bool EditText(AccessibilityNodeInfo n) + { + return n.ClassName != null && n.ClassName.Contains("EditText"); + } + + private void AskFillPassword(string uri, AccessibilityNodeInfo usernameNode, + IEnumerable passwordNodes) + { + var intent = new Intent(this, typeof(AutofillActivity)); + intent.PutExtra("uri", uri); + intent.SetFlags(ActivityFlags.NewTask | ActivityFlags.SingleTop | ActivityFlags.ClearTop); + var pendingIntent = PendingIntent.GetActivity(this, 0, intent, PendingIntentFlags.UpdateCurrent); + + var targetName = uri; + if(uri.StartsWith(AndroidAppProtocol)) + { + var packageName = uri.Substring(AndroidAppProtocol.Length); try { var appInfo = PackageManager.GetApplicationInfo(packageName, 0); - targetName = (string)(appInfo != null ? PackageManager.GetApplicationLabel(appInfo) : packageName); + targetName = appInfo != null ? PackageManager.GetApplicationLabel(appInfo) : packageName; } - catch(Exception e) + catch { - //Android.Util.Log.Debug(_logTag, e.ToString()); targetName = packageName; } } else { - //targetName = KeePassLib.Utility.UrlUtil.GetHost(url); + //targetName = KeePassLib.Utility.UrlUtil.GetHost(uri); } @@ -175,44 +156,44 @@ namespace Bit.Android //TODO icon //TODO plugin icon builder.SetSmallIcon(Resource.Drawable.icon) - .SetContentText("Content Text") - .SetContentTitle("Content Title") + .SetContentText("Tap this notification to autofill a login from your bitwarden vault.") + .SetContentTitle("bitwarden Autofill Service") .SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis()) - .SetTicker("Ticker Text") + .SetTicker("Tap this notification to autofill a login from your bitwarden vault.") .SetVisibility(NotificationVisibility.Secret) - .SetContentIntent(pending); + .SetContentIntent(pendingIntent); var notificationManager = (NotificationManager)GetSystemService(NotificationService); - notificationManager.Notify(autoFillNotificationId, builder.Build()); - + notificationManager.Notify(AutoFillNotificationId, builder.Build()); } - private void FillPassword(string url, AccessibilityNodeInfo usernameEdit, IEnumerable passwordFields) + private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable passwordNodes) { + FillEditText(usernameNode, AutofillActivity.LastCredentials.User); + foreach(var pNode in passwordNodes) + { + FillEditText(pNode, AutofillActivity.LastCredentials.Password); + } - FillDataInTextField(usernameEdit, AutofillActivity.LastReceivedCredentials.User); - foreach(var pwd in passwordFields) - FillDataInTextField(pwd, AutofillActivity.LastReceivedCredentials.Password); - - AutofillActivity.LastReceivedCredentials = null; + AutofillActivity.LastCredentials = null; } - private static void FillDataInTextField(AccessibilityNodeInfo edit, string newValue) + private static void FillEditText(AccessibilityNodeInfo editTextNode, string value) { - Bundle b = new Bundle(); - b.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, newValue); - edit.PerformAction(global::Android.Views.Accessibility.Action.SetText, b); + var bundle = new Bundle(); + bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value); + editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle); } - private bool ExistsNodeOrChildren(AccessibilityNodeInfo n, Func p) + private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func p) { return GetNodeOrChildren(n, p).Any(); } - private IEnumerable GetNodeOrChildren(AccessibilityNodeInfo n, Func p) + private IEnumerable GetNodeOrChildren(AccessibilityNodeInfo n, + Func p) { if(n != null) { - if(p(n)) { yield return n; @@ -220,9 +201,9 @@ namespace Bit.Android for(int i = 0; i < n.ChildCount; i++) { - foreach(var x in GetNodeOrChildren(n.GetChild(i), p)) + foreach(var node in GetNodeOrChildren(n.GetChild(i), p)) { - yield return x; + yield return node; } } }