autofill cleanup WIP

This commit is contained in:
Kyle Spearrin 2017-01-23 23:32:52 -05:00
parent 759df9bdd5
commit 61e0379eb3
3 changed files with 135 additions and 153 deletions

View file

@ -7,58 +7,59 @@ using Android.App;
using Android.Content; using Android.Content;
using Android.OS; using Android.OS;
using Android.Runtime; using Android.Runtime;
using Android.Views;
using Android.Widget;
using Bit.App.Models; using Bit.App.Models;
namespace Bit.Android 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 public class AutofillActivity : Activity
{ {
private string _lastQueriedUri;
public static Credentials LastCredentials;
protected override void OnCreate(Bundle bundle) protected override void OnCreate(Bundle bundle)
{ {
base.OnCreate(bundle); base.OnCreate(bundle);
_lastQueriedUri = Intent.GetStringExtra("uri");
var url = Intent.GetStringExtra("url"); var intent = new Intent(this, typeof(AutofillSelectLoginActivity));
_lastQueriedUrl = url; intent.PutExtra("uri", _lastQueriedUri);
Intent intent = new Intent(this, typeof(AutofillSelectLoginActivity));
intent.PutExtra("url", url);
StartActivityForResult(intent, 123); StartActivityForResult(intent, 123);
} }
string _lastQueriedUrl;
protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent data) 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); base.OnActivityResult(requestCode, resultCode, data);
try try
{ {
LastReceivedCredentials = new Credentials { User = username, Password = password, Url = _lastQueriedUrl }; var uri = data.GetStringExtra("uri");
} var username = data.GetStringExtra("username");
catch(Exception e) var password = data.GetStringExtra("password");
{
//Android.Util.Log.Debug("KP2AAS", "Exception while receiving credentials: " + e.ToString()); LastCredentials = new Credentials
{
User = username,
Password = password,
Uri = _lastQueriedUri
};
} }
catch { }
finally finally
{ {
Finish(); Finish();
} }
} }
public static Credentials LastReceivedCredentials;
public class Credentials public class Credentials
{ {
public string User; public string User;
public string Password; public string Password;
public string Url; public string Uri;
} }
} }
} }

View file

@ -9,16 +9,16 @@ using Android.OS;
namespace Bit.Android namespace Bit.Android
{ {
//[Activity(LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance)] [Activity(LaunchMode = global::Android.Content.PM.LaunchMode.SingleInstance)]
public class AutofillSelectLoginActivity : Activity public class AutofillSelectLoginActivity : Activity
{ {
protected override void OnCreate(Bundle bundle) protected override void OnCreate(Bundle bundle)
{ {
base.OnCreate(bundle); base.OnCreate(bundle);
var url = Intent.GetStringExtra("url"); var uri = Intent.GetStringExtra("uri");
Intent data = new Intent(); Intent data = new Intent();
data.PutExtra("url", url); data.PutExtra("uri", uri);
data.PutExtra("username", "user123"); data.PutExtra("username", "user123");
data.PutExtra("password", "pass123"); data.PutExtra("password", "pass123");

View file

@ -1,102 +1,80 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text;
using Android.AccessibilityServices; using Android.AccessibilityServices;
using Android.App; using Android.App;
using Android.Content; using Android.Content;
using Android.Graphics;
using Android.OS; using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Views.Accessibility; using Android.Views.Accessibility;
using Android.Widget;
namespace Bit.Android namespace Bit.Android
{ {
//[Service(Permission = "android.permission.BIND_ACCESSIBILITY_SERVICE", Label = "bitwarden")] [Service(Permission = "android.permission.BIND_ACCESSIBILITY_SERVICE", Label = "bitwarden")]
//[IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })] [IntentFilter(new string[] { "android.accessibilityservice.AccessibilityService" })]
//[MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")] [MetaData("android.accessibilityservice", Resource = "@xml/accessibilityservice")]
public class AutofillService : AccessibilityService public class AutofillService : AccessibilityService
{ {
private const int autoFillNotificationId = 0; private const int AutoFillNotificationId = 34573;
private const string androidAppPrefix = "androidapp://"; 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) public override void OnAccessibilityEvent(AccessibilityEvent e)
{ {
var eventType = e.EventType; var eventType = e.EventType;
System.Diagnostics.Debug.WriteLine(DateTime.UtcNow.ToString() + " Event Type: " + eventType); var packageName = e.PackageName;
var package = e.PackageName;
if(packageName == SystemUiPackage)
{
return;
}
switch(eventType) 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.WindowContentChanged:
case EventTypes.WindowStateChanged: case EventTypes.WindowStateChanged:
if(e.PackageName == "com.android.systemui")
{
break;
}
var root = RootInActiveWindow; 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 uri = string.Concat(AndroidAppProtocol, root.PackageName);
if(isChrome)
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 addressField = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar").FirstOrDefault(); var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.chrome:id/url_bar")
UrlFromAddressField(ref url, addressField); .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(); var addressNode = root.FindAccessibilityNodeInfosByViewId("com.android.browser:id/url")
UrlFromAddressField(ref url, addressField); .FirstOrDefault();
uri = ExtractUriFromAddressField(uri, addressNode);
} }
var emptyPasswordFields = GetNodeOrChildren(root, n => { return IsPasswordField(n); }).ToList(); var allEditTexts = GetNodeOrChildren(root, n => EditText(n));
if(emptyPasswordFields.Any()) 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)) FillCredentials(usernameEditText, avialablePasswordNodes);
{
//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;
}
} }
if(cancelNotification) else
{ {
((NotificationManager)GetSystemService(NotificationService)).Cancel(autoFillNotificationId); AskFillPassword(uri, usernameEditText, avialablePasswordNodes);
//Android.Util.Log.Debug("KP2AAS", "Cancel notif"); cancelNotification = false;
} }
} }
if(cancelNotification)
{
((NotificationManager)GetSystemService(NotificationService)).Cancel(AutoFillNotificationId);
}
break; break;
default: default:
break; 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; uri = addressNode.Text;
if(!url.Contains("://")) if(!uri.Contains("://"))
url = "http://" + url; {
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://")) Uri uri1, uri2;
return url1 == url2; if(Uri.TryCreate(uriString1, UriKind.RelativeOrAbsolute, out uri1) &&
// TODO: host check Uri.TryCreate(uriString2, UriKind.RelativeOrAbsolute, out uri2) && uri1.Host == uri2.Host)
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<AccessibilityNodeInfo> 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))
{ {
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<AccessibilityNodeInfo> 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 try
{ {
var appInfo = PackageManager.GetApplicationInfo(packageName, 0); 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; targetName = packageName;
} }
} }
else else
{ {
//targetName = KeePassLib.Utility.UrlUtil.GetHost(url); //targetName = KeePassLib.Utility.UrlUtil.GetHost(uri);
} }
@ -175,44 +156,44 @@ namespace Bit.Android
//TODO icon //TODO icon
//TODO plugin icon //TODO plugin icon
builder.SetSmallIcon(Resource.Drawable.icon) builder.SetSmallIcon(Resource.Drawable.icon)
.SetContentText("Content Text") .SetContentText("Tap this notification to autofill a login from your bitwarden vault.")
.SetContentTitle("Content Title") .SetContentTitle("bitwarden Autofill Service")
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis()) .SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetTicker("Ticker Text") .SetTicker("Tap this notification to autofill a login from your bitwarden vault.")
.SetVisibility(NotificationVisibility.Secret) .SetVisibility(NotificationVisibility.Secret)
.SetContentIntent(pending); .SetContentIntent(pendingIntent);
var notificationManager = (NotificationManager)GetSystemService(NotificationService); var notificationManager = (NotificationManager)GetSystemService(NotificationService);
notificationManager.Notify(autoFillNotificationId, builder.Build()); notificationManager.Notify(AutoFillNotificationId, builder.Build());
} }
private void FillPassword(string url, AccessibilityNodeInfo usernameEdit, IEnumerable<AccessibilityNodeInfo> passwordFields) private void FillCredentials(AccessibilityNodeInfo usernameNode, IEnumerable<AccessibilityNodeInfo> passwordNodes)
{ {
FillEditText(usernameNode, AutofillActivity.LastCredentials.User);
foreach(var pNode in passwordNodes)
{
FillEditText(pNode, AutofillActivity.LastCredentials.Password);
}
FillDataInTextField(usernameEdit, AutofillActivity.LastReceivedCredentials.User); AutofillActivity.LastCredentials = null;
foreach(var pwd in passwordFields)
FillDataInTextField(pwd, AutofillActivity.LastReceivedCredentials.Password);
AutofillActivity.LastReceivedCredentials = null;
} }
private static void FillDataInTextField(AccessibilityNodeInfo edit, string newValue) private static void FillEditText(AccessibilityNodeInfo editTextNode, string value)
{ {
Bundle b = new Bundle(); var bundle = new Bundle();
b.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, newValue); bundle.PutString(AccessibilityNodeInfo.ActionArgumentSetTextCharsequence, value);
edit.PerformAction(global::Android.Views.Accessibility.Action.SetText, b); editTextNode.PerformAction(global::Android.Views.Accessibility.Action.SetText, bundle);
} }
private bool ExistsNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p) private bool AnyNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p)
{ {
return GetNodeOrChildren(n, p).Any(); return GetNodeOrChildren(n, p).Any();
} }
private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n, Func<AccessibilityNodeInfo, bool> p) private IEnumerable<AccessibilityNodeInfo> GetNodeOrChildren(AccessibilityNodeInfo n,
Func<AccessibilityNodeInfo, bool> p)
{ {
if(n != null) if(n != null)
{ {
if(p(n)) if(p(n))
{ {
yield return n; yield return n;
@ -220,9 +201,9 @@ namespace Bit.Android
for(int i = 0; i < n.ChildCount; i++) 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;
} }
} }
} }