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 Bit.iOS.Core.Controllers;
using SimpleInjector; using SimpleInjector;
using XLabs.Ioc.SimpleInjectorContainer; using XLabs.Ioc.SimpleInjectorContainer;
using System.Collections.Generic;
namespace Bit.iOS.Extension 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; NSDictionary itemData = null;
if(_context.ProviderType == UTType.PropertyList) 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 scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson); var scriptDict = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict); itemData = new NSDictionary(NSJavaScriptExtension.FinalizeArgumentKey, scriptDict);
@ -210,7 +212,7 @@ namespace Bit.iOS.Extension
else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction else if(_context.ProviderType == Constants.UTTypeAppExtensionFillBrowserAction
|| _context.ProviderType == Constants.UTTypeAppExtensionFillWebViewAction) || _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); var scriptJson = JsonConvert.SerializeObject(fillScript, _jsonSettings);
itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson); itemData = new NSDictionary(Constants.AppExtensionWebViewPageFillScript, scriptJson);
} }

View File

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

View File

@ -207,7 +207,8 @@ namespace Bit.iOS.Extension
totp = GetTotp(item); 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) || else if(!string.IsNullOrWhiteSpace(item.Username) || !string.IsNullOrWhiteSpace(item.Password) ||
!string.IsNullOrWhiteSpace(item.Totp.Value)) !string.IsNullOrWhiteSpace(item.Totp.Value))

View File

@ -2,12 +2,17 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Text.RegularExpressions;
namespace Bit.iOS.Extension.Models namespace Bit.iOS.Extension.Models
{ {
public class FillScript 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) if(pageDetails == null)
{ {
@ -16,6 +21,38 @@ namespace Bit.iOS.Extension.Models
DocumentUUID = pageDetails.DocumentUUID; 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> usernames = new List<PageDetails.Field>();
List<PageDetails.Field> passwords = 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> { "click_on_opid", username.OpId });
Script.Add(new List<string> { "fill_by_opid", username.OpId, fillUsername }); 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> { "click_on_opid", password.OpId });
Script.Add(new List<string> { "fill_by_opid", password.OpId, fillPassword }); 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, private PageDetails.Field FindUsernameField(PageDetails pageDetails, PageDetails.Field passwordField, bool canBeHidden,
bool checkForm) bool checkForm)
{ {
return pageDetails.Fields.LastOrDefault(f => PageDetails.Field usernameField = null;
(!checkForm || f.Form == passwordField.Form)
&& (canBeHidden || f.Viewable) foreach(var f in pageDetails.Fields)
&& f.ElementNumber < passwordField.ElementNumber {
&& (f.Type == "text" || f.Type == "email" || f.Type == "tel")); 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")] [JsonProperty(PropertyName = "script")]
public List<List<string>> Script { get; set; } = new List<List<string>>(); public List<List<string>> Script { get; set; } = new List<List<string>>();
[JsonProperty(PropertyName = "autosubmit")]
public Submit AutoSubmit { get; set; }
[JsonProperty(PropertyName = "documentUUID")] [JsonProperty(PropertyName = "documentUUID")]
public object DocumentUUID { get; set; } public object DocumentUUID { get; set; }
[JsonProperty(PropertyName = "properties")] [JsonProperty(PropertyName = "properties")]
@ -116,12 +247,5 @@ namespace Bit.iOS.Extension.Models
public object Options { get; set; } = new { animate = false }; public object Options { get; set; } = new { animate = false };
[JsonProperty(PropertyName = "metadata")] [JsonProperty(PropertyName = "metadata")]
public object MetaData { get; set; } = new object(); 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 Bit.App.Models;
using System; using System;
using System.Collections.Generic;
using System.Linq;
namespace Bit.iOS.Extension.Models namespace Bit.iOS.Extension.Models
{ {
@ -13,6 +15,22 @@ namespace Bit.iOS.Extension.Models
Password = login.Password?.Decrypt(login.OrganizationId); Password = login.Password?.Decrypt(login.OrganizationId);
Uri = login.Uri?.Decrypt(login.OrganizationId); Uri = login.Uri?.Decrypt(login.OrganizationId);
Totp = new Lazy<string>(() => login.Totp?.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; } public string Id { get; set; }
@ -21,5 +39,6 @@ namespace Bit.iOS.Extension.Models
public string Password { get; set; } public string Password { get; set; }
public string Uri { get; set; } public string Uri { get; set; }
public Lazy<string> Totp { 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.Collections.Generic;
using System.Linq; using System.Linq;
@ -36,6 +37,9 @@ namespace Bit.iOS.Extension.Models
public string HtmlClass { get; set; } public string HtmlClass { get; set; }
public string LabelRight { get; set; } public string LabelRight { get; set; }
public string LabelLeft { 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 Type { get; set; }
public string Value { get; set; } public string Value { get; set; }
public bool Disabled { get; set; } public bool Disabled { get; set; }