autofill custom fields for iOS extension

This commit is contained in:
Kyle Spearrin 2017-09-26 14:38:12 -04:00
parent 4598c3d852
commit 163ad248af
6 changed files with 175 additions and 25 deletions

View file

@ -20,6 +20,7 @@ using Bit.App.Resources;
using Bit.iOS.Core.Controllers;
using SimpleInjector;
using XLabs.Ioc.SimpleInjectorContainer;
using System.Collections.Generic;
namespace Bit.iOS.Extension
{
@ -191,12 +192,13 @@ namespace Bit.iOS.Extension
}
}
public void CompleteUsernamePasswordRequest(string username, string password, string totp)
public void CompleteUsernamePasswordRequest(string username, string password,
List<Tuple<string, string>> fields, string totp)
{
NSDictionary itemData = null;
if(_context.ProviderType == UTType.PropertyList)
{
var fillScript = new FillScript(_context.Details, username, password);
var fillScript = new FillScript(_context.Details, username, password, fields);
var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict);
@ -210,7 +212,7 @@ namespace Bit.iOS.Extension
else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction
|| _context.ProviderType == Constants.UTTypeAppExtensionFillWebViewAction)
{
var fillScript = new FillScript(_context.Details, username, password);
var fillScript = new FillScript(_context.Details, username, password, fields);
var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
}

View file

@ -177,7 +177,7 @@ namespace Bit.iOS.Extension
else if(LoadingController != null)
{
LoadingController.CompleteUsernamePasswordRequest(UsernameCell.TextField.Text, PasswordCell.TextField.Text,
null);
null, null);
}
}
else if(saveTask.Result.Errors.Count() > 0)

View file

@ -207,7 +207,8 @@ namespace Bit.iOS.Extension
totp = GetTotp(item);
}
_controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password, totp);
_controller.LoadingController.CompleteUsernamePasswordRequest(item.Username, item.Password,
item.Fields.Value, totp);
}
else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) ||
!string.IsNullOrWhiteSpace(item.Totp.Value))

View file

@ -2,12 +2,17 @@
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace Bit.iOS.Extension.Models
{
public class FillScript
{
public FillScript(PageDetails pageDetails, string fillUsername, string fillPassword)
private static string[] _usernameFieldNames = new[]{ "username", "user name", "email",
"email address", "e-mail", "e-mail address", "userid", "user id" };
public FillScript(PageDetails pageDetails, string fillUsername, string fillPassword,
List<Tuple<string, string>> fillFields)
{
if(pageDetails == null)
{
@ -16,6 +21,38 @@ namespace Bit.iOS.Extension.Models
DocumentUUID = pageDetails.DocumentUUID;
var filledOpIds = new HashSet<string>();
if(fillFields?.Any() ?? false)
{
var fieldNames = fillFields.Select(f => f.Item1?.ToLower()).ToArray();
foreach(var field in pageDetails.Fields.Where(f => f.Viewable))
{
if(filledOpIds.Contains(field.OpId))
{
continue;
}
var matchingIndex = FindMatchingFieldIndex(field, fieldNames);
if(matchingIndex > -1)
{
filledOpIds.Add(field.OpId);
Script.Add(new List<string> { "click_on_opid", field.OpId });
Script.Add(new List<string> { "fill_by_opid", field.OpId, fillFields[matchingIndex].Item2 });
}
}
}
if(string.IsNullOrWhiteSpace(fillPassword))
{
// No password for this login. Maybe they just wanted to auto-fill some custom fields?
if(filledOpIds.Any())
{
Script.Add(new List<string> { "focus_by_opid", filledOpIds.Last() });
}
return;
}
List<PageDetails.Field> usernames = new List<PageDetails.Field>();
List<PageDetails.Field> passwords = new List<PageDetails.Field>();
@ -76,38 +113,132 @@ namespace Bit.iOS.Extension.Models
}
}
foreach(var username in usernames)
if(!passwordFields.Any())
{
// No password fields on this page. Let's try to just fuzzy fill the username.
var usernameFieldNamesList = _usernameFieldNames.ToList();
foreach(var f in pageDetails.Fields)
{
if((f.Type == "text" || f.Type == "email" || f.Type == "tel") &&
FieldIsFuzzyMatch(f, usernameFieldNamesList))
{
usernames.Add(f);
}
}
}
foreach(var username in usernames.Where(u => !filledOpIds.Contains(u.OpId)))
{
filledOpIds.Add(username.OpId);
Script.Add(new List<string> { "click_on_opid", username.OpId });
Script.Add(new List<string> { "fill_by_opid", username.OpId, fillUsername });
}
foreach(var password in passwords)
foreach(var password in passwords.Where(p => !filledOpIds.Contains(p.OpId)))
{
filledOpIds.Add(password.OpId);
Script.Add(new List<string> { "click_on_opid", password.OpId });
Script.Add(new List<string> { "fill_by_opid", password.OpId, fillPassword });
}
if(passwords.Any())
if(filledOpIds.Any())
{
AutoSubmit = new Submit { FocusOpId = passwords.First().OpId };
Script.Add(new List<string> { "focus_by_opid", filledOpIds.Last() });
}
}
private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden,
bool checkForm)
{
return pageDetails.Fields.LastOrDefault(f =>
(!checkForm || f.Form == passwordField.Form)
&& (canBeHidden || f.Viewable)
&& f.ElementNumber < passwordField.ElementNumber
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel"));
PageDetails.Field usernameField = null;
foreach(var f in pageDetails.Fields)
{
if(f.ElementNumber >= passwordField.ElementNumber)
{
break;
}
if((!checkForm || f.Form == passwordField.Form)
&& (canBeHidden || f.Viewable)
&& f.ElementNumber < passwordField.ElementNumber
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel"))
{
usernameField = f;
if(FindMatchingFieldIndex(f, _usernameFieldNames) > -1)
{
// We found an exact match. No need to keep looking.
break;
}
}
}
return usernameField;
}
private int FindMatchingFieldIndex(PageDetails.Field field, string[] names)
{
var matchingIndex = -1;
if(!string.IsNullOrWhiteSpace(field.HtmlId))
{
matchingIndex = Array.IndexOf(names, field.HtmlId.ToLower());
}
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.HtmlName))
{
matchingIndex = Array.IndexOf(names, field.HtmlName.ToLower());
}
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.LabelTag))
{
matchingIndex = Array.IndexOf(names, CleanLabel(field.LabelTag));
}
if(matchingIndex < 0 && !string.IsNullOrWhiteSpace(field.Placeholder))
{
matchingIndex = Array.IndexOf(names, field.Placeholder.ToLower());
}
return matchingIndex;
}
private bool FieldIsFuzzyMatch(PageDetails.Field field, List<string> names)
{
if(!string.IsNullOrWhiteSpace(field.HtmlId) && FuzzyMatch(names, field.HtmlId.ToLower()))
{
return true;
}
if(!string.IsNullOrWhiteSpace(field.HtmlName) && FuzzyMatch(names, field.HtmlName.ToLower()))
{
return true;
}
if(!string.IsNullOrWhiteSpace(field.LabelTag) && FuzzyMatch(names, CleanLabel(field.LabelTag)))
{
return true;
}
if(!string.IsNullOrWhiteSpace(field.Placeholder) && FuzzyMatch(names, field.Placeholder.ToLower()))
{
return true;
}
return false;
}
private bool FuzzyMatch(List<string> options, string value)
{
if((!options?.Any() ?? true) || string.IsNullOrWhiteSpace(value))
{
return false;
}
return options.Any(o => value.Contains(o));
}
private string CleanLabel(string label)
{
return Regex.Replace(label, @"(?:\r\n|\r|\n)", string.Empty).Trim().ToLower();
}
[JsonProperty(PropertyName = "script")]
public List<List<string>> Script { get; set; } = new List<List<string>>();
[JsonProperty(PropertyName = "autosubmit")]
public Submit AutoSubmit { get; set; }
[JsonProperty(PropertyName = "documentUUID")]
public object DocumentUUID { get; set; }
[JsonProperty(PropertyName = "properties")]
@ -116,12 +247,5 @@ namespace Bit.iOS.Extension.Models
public object Options { get; set; } = new { animate = false };
[JsonProperty(PropertyName = "metadata")]
public object MetaData { get; set; } = new object();
public class Submit
{
[JsonProperty(PropertyName = "focusOpid")]
public string FocusOpId { get; set; }
}
}
}

View file

@ -1,5 +1,7 @@
using Bit.App.Models;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Bit.iOS.Extension.Models
{
@ -13,6 +15,22 @@ namespace Bit.iOS.Extension.Models
Password = login.Password?.Decrypt(login.OrganizationId);
Uri = login.Uri?.Decrypt(login.OrganizationId);
Totp = new Lazy<string>(() => login.Totp?.Decrypt(login.OrganizationId));
Fields = new Lazy<List<Tuple<string, string>>>(() =>
{
if(login.Fields?.Any() ?? true)
{
return null;
}
var fields = new List<Tuple<string, string>>();
foreach(var field in login.Fields)
{
fields.Add(new Tuple<string, string>(
field.Name?.Decrypt(login.OrganizationId),
field.Value?.Decrypt(login.OrganizationId)));
}
return fields;
});
}
public string Id { get; set; }
@ -21,5 +39,6 @@ namespace Bit.iOS.Extension.Models
public string Password { get; set; }
public string Uri { get; set; }
public Lazy<string> Totp { get; set; }
public Lazy<List<Tuple<string, string>>> Fields { get; set; }
}
}

View file

@ -1,4 +1,5 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
@ -36,6 +37,9 @@ namespace Bit.iOS.Extension.Models
public string HtmlClass { get; set; }
public string LabelRight { get; set; }
public string LabelLeft { get; set; }
[JsonProperty("label-tag")]
public string LabelTag { get; set; }
public string Placeholder { get; set; }
public string Type { get; set; }
public string Value { get; set; }
public bool Disabled { get; set; }