add support for card filling

This commit is contained in:
Kyle Spearrin 2017-11-18 15:09:09 -05:00
parent 4b24fe1bf4
commit c45a77d538
8 changed files with 248 additions and 97 deletions

View file

@ -2,26 +2,38 @@
using System.Collections.Generic; using System.Collections.Generic;
using Android.Content; using Android.Content;
using Android.Service.Autofill; using Android.Service.Autofill;
using Android.Views;
using Android.Widget; using Android.Widget;
using System.Linq; using System.Linq;
using Android.App; using Android.App;
using Bit.App.Abstractions; using Bit.App.Abstractions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.App.Resources;
namespace Bit.Android.Autofill namespace Bit.Android.Autofill
{ {
public static class AutofillHelpers public static class AutofillHelpers
{ {
public static async Task<List<IFilledItem>> GetFillItemsAsync(ICipherService service, string uri) public static async Task<List<IFilledItem>> GetFillItemsAsync(Parser parser, ICipherService service)
{ {
var items = new List<IFilledItem>(); var items = new List<IFilledItem>();
var ciphers = await service.GetAllAsync(uri);
if(ciphers.Item1.Any() || ciphers.Item2.Any()) if(parser.FieldCollection.FillableForLogin)
{ {
var allCiphers = ciphers.Item1.ToList(); var ciphers = await service.GetAllAsync(parser.Uri);
allCiphers.AddRange(ciphers.Item2.ToList()); if(ciphers.Item1.Any() || ciphers.Item2.Any())
foreach(var cipher in allCiphers) {
var allCiphers = ciphers.Item1.ToList();
allCiphers.AddRange(ciphers.Item2.ToList());
foreach(var cipher in allCiphers)
{
items.Add(new CipherFilledItem(cipher));
}
}
}
else if(parser.FieldCollection.FillableForCard)
{
var ciphers = await service.GetAllAsync();
foreach(var cipher in ciphers.Where(c => c.Type == App.Enums.CipherType.Card))
{ {
items.Add(new CipherFilledItem(cipher)); items.Add(new CipherFilledItem(cipher));
} }
@ -30,14 +42,14 @@ namespace Bit.Android.Autofill
return items; return items;
} }
public static FillResponse BuildFillResponse(Context context, FieldCollection fields, List<IFilledItem> items) public static FillResponse BuildFillResponse(Context context, Parser parser, List<IFilledItem> items)
{ {
var responseBuilder = new FillResponse.Builder(); var responseBuilder = new FillResponse.Builder();
if(items != null && items.Count > 0) if(items != null && items.Count > 0)
{ {
foreach(var item in items) foreach(var item in items)
{ {
var dataset = BuildDataset(context, fields, item); var dataset = BuildDataset(context, parser.FieldCollection, item);
if(dataset != null) if(dataset != null)
{ {
responseBuilder.AddDataset(dataset); responseBuilder.AddDataset(dataset);
@ -45,8 +57,8 @@ namespace Bit.Android.Autofill
} }
} }
AddSaveInfo(responseBuilder, fields); AddSaveInfo(responseBuilder, parser.FieldCollection);
responseBuilder.SetIgnoredIds(fields.IgnoreAutofillIds.ToArray()); responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
return responseBuilder.Build(); return responseBuilder.Build();
} }
@ -64,8 +76,8 @@ namespace Bit.Android.Autofill
public static FillResponse BuildAuthResponse(Context context, FieldCollection fields, string uri) public static FillResponse BuildAuthResponse(Context context, FieldCollection fields, string uri)
{ {
var responseBuilder = new FillResponse.Builder(); var responseBuilder = new FillResponse.Builder();
var view = BuildListView(context.PackageName, "Auto-fill with bitwarden", var view = BuildListView(context.PackageName, AppResources.AutofillWithBitwarden,
"Vault is locked", Resource.Drawable.icon); AppResources.VaultIsLocked, Resource.Drawable.icon);
var intent = new Intent(context, typeof(MainActivity)); var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true); intent.PutExtra("autofillFramework", true);
intent.PutExtra("autofillFrameworkUri", uri); intent.PutExtra("autofillFrameworkUri", uri);
@ -87,36 +99,14 @@ namespace Bit.Android.Autofill
public static void AddSaveInfo(FillResponse.Builder responseBuilder, FieldCollection fields) public static void AddSaveInfo(FillResponse.Builder responseBuilder, FieldCollection fields)
{ {
var saveInfo = new SaveInfo.Builder(SaveDataType.Password, fields.AutofillIds.ToArray()).Build(); var saveType = fields.SaveType;
responseBuilder.SetSaveInfo(saveInfo); if(saveType == SaveDataType.Generic)
}
public static List<string> FilterForSupportedHints(string[] hints)
{
return hints?.Where(h => IsValidHint(h)).ToList() ?? new List<string>();
}
public static bool IsValidHint(string hint)
{
switch(hint)
{ {
case View.AutofillHintCreditCardExpirationDate: return;
case View.AutofillHintCreditCardExpirationDay:
case View.AutofillHintCreditCardExpirationMonth:
case View.AutofillHintCreditCardExpirationYear:
case View.AutofillHintCreditCardNumber:
case View.AutofillHintCreditCardSecurityCode:
case View.AutofillHintEmailAddress:
case View.AutofillHintPhone:
case View.AutofillHintName:
case View.AutofillHintPassword:
case View.AutofillHintPostalAddress:
case View.AutofillHintPostalCode:
case View.AutofillHintUsername:
return true;
default:
return false;
} }
var saveInfo = new SaveInfo.Builder(saveType, fields.AutofillIds.ToArray()).Build();
responseBuilder.SetSaveInfo(saveInfo);
} }
} }
} }

View file

@ -34,7 +34,8 @@ namespace Bit.Android.Autofill
parser.Parse(); parser.Parse();
if(string.IsNullOrWhiteSpace(parser.Uri) || parser.Uri == "androidapp://com.x8bit.bitwarden" || if(string.IsNullOrWhiteSpace(parser.Uri) || parser.Uri == "androidapp://com.x8bit.bitwarden" ||
parser.Uri == "androidapp://android" || !parser.FieldCollection.FillableForLogin) parser.Uri == "androidapp://android" ||
(!parser.FieldCollection.FillableForLogin && !parser.FieldCollection.FillableForCard))
{ {
return; return;
} }
@ -58,8 +59,8 @@ namespace Bit.Android.Autofill
} }
// build response // build response
var items = await AutofillHelpers.GetFillItemsAsync(_cipherService, parser.Uri); var items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
var response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items); var response = AutofillHelpers.BuildFillResponse(this, parser, items);
callback.OnSuccess(response); callback.OnSuccess(response);
} }

View file

@ -5,12 +5,17 @@ using System.Linq;
using Bit.App.Models; using Bit.App.Models;
using Bit.App.Enums; using Bit.App.Enums;
using Bit.App.Models.Page; using Bit.App.Models.Page;
using Android.Views;
namespace Bit.Android.Autofill namespace Bit.Android.Autofill
{ {
public class CipherFilledItem : IFilledItem public class CipherFilledItem : IFilledItem
{ {
private Lazy<string> _password; private Lazy<string> _password;
private string _cardNumber;
private Lazy<string> _cardExpMonth;
private Lazy<string> _cardExpYear;
private Lazy<string> _cardCode;
public CipherFilledItem(Cipher cipher) public CipherFilledItem(Cipher cipher)
{ {
@ -21,8 +26,24 @@ namespace Bit.Android.Autofill
{ {
case CipherType.Login: case CipherType.Login:
Subtitle = cipher.Login.Username?.Decrypt() ?? string.Empty; Subtitle = cipher.Login.Username?.Decrypt() ?? string.Empty;
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt());
Icon = Resource.Drawable.login; Icon = Resource.Drawable.login;
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt());
break;
case CipherType.Card:
Subtitle = cipher.Card.Brand?.Decrypt();
_cardNumber = cipher.Card.Number?.Decrypt();
if(!string.IsNullOrWhiteSpace(_cardNumber) && _cardNumber.Length >= 4)
{
if(!string.IsNullOrWhiteSpace(_cardNumber))
{
Subtitle += ", ";
}
Subtitle += ("*" + _cardNumber.Substring(_cardNumber.Length - 4));
}
Icon = Resource.Drawable.card;
_cardCode = new Lazy<string>(() => cipher.Card.Code?.Decrypt());
_cardExpMonth = new Lazy<string>(() => cipher.Card.ExpMonth?.Decrypt());
_cardExpYear = new Lazy<string>(() => cipher.Card.ExpYear?.Decrypt());
break; break;
default: default:
break; break;
@ -58,32 +79,68 @@ namespace Bit.Android.Autofill
return false; return false;
} }
var setValues = false;
if(Type == CipherType.Login) if(Type == CipherType.Login)
{ {
if(!fieldCollection.PasswordFields.Any() || string.IsNullOrWhiteSpace(_password.Value)) if(fieldCollection.PasswordFields.Any() && !string.IsNullOrWhiteSpace(_password.Value))
{ {
return false; foreach(var f in fieldCollection.PasswordFields)
} {
setValues = true;
foreach(var passwordField in fieldCollection.PasswordFields) datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_password.Value));
{ }
datasetBuilder.SetValue(passwordField.AutofillId, AutofillValue.ForText(_password.Value));
} }
if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle)) if(fieldCollection.UsernameFields.Any() && !string.IsNullOrWhiteSpace(Subtitle))
{ {
foreach(var usernameField in fieldCollection.UsernameFields) foreach(var f in fieldCollection.UsernameFields)
{ {
datasetBuilder.SetValue(usernameField.AutofillId, AutofillValue.ForText(Subtitle)); setValues = true;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(Subtitle));
} }
} }
return true;
} }
else else if(Type == CipherType.Card)
{ {
return false; if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber) &&
!string.IsNullOrWhiteSpace(_cardNumber))
{
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardNumber])
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardNumber));
}
}
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardSecurityCode) &&
!string.IsNullOrWhiteSpace(_cardCode.Value))
{
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardSecurityCode])
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardCode.Value));
}
}
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationMonth) &&
!string.IsNullOrWhiteSpace(_cardExpMonth.Value))
{
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardExpirationMonth])
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardExpMonth.Value));
}
}
if(fieldCollection.HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardExpirationYear) &&
!string.IsNullOrWhiteSpace(_cardExpYear.Value))
{
foreach(var f in fieldCollection.HintToFieldsMap[View.AutofillHintCreditCardExpirationYear])
{
setValues = true;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_cardExpYear.Value));
}
}
} }
return setValues;
} }
} }
} }

View file

@ -23,7 +23,7 @@ namespace Bit.Android.Autofill
Selected = node.IsSelected; Selected = node.IsSelected;
Clickable = node.IsClickable; Clickable = node.IsClickable;
Visible = node.Visibility == ViewStates.Visible; Visible = node.Visibility == ViewStates.Visible;
Hints = AutofillHelpers.FilterForSupportedHints(node.GetAutofillHints()); Hints = FilterForSupportedHints(node.GetAutofillHints());
AutofillOptions = node.GetAutofillOptions()?.ToList(); AutofillOptions = node.GetAutofillOptions()?.ToList();
if(node.AutofillValue != null) if(node.AutofillValue != null)
@ -168,5 +168,33 @@ namespace Bit.Android.Autofill
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
return result; return result;
} }
private static List<string> FilterForSupportedHints(string[] hints)
{
return hints?.Where(h => IsValidHint(h)).ToList() ?? new List<string>();
}
private static bool IsValidHint(string hint)
{
switch(hint)
{
case View.AutofillHintCreditCardExpirationDate:
case View.AutofillHintCreditCardExpirationDay:
case View.AutofillHintCreditCardExpirationMonth:
case View.AutofillHintCreditCardExpirationYear:
case View.AutofillHintCreditCardNumber:
case View.AutofillHintCreditCardSecurityCode:
case View.AutofillHintEmailAddress:
case View.AutofillHintPhone:
case View.AutofillHintName:
case View.AutofillHintPassword:
case View.AutofillHintPostalAddress:
case View.AutofillHintPostalCode:
case View.AutofillHintUsername:
return true;
default:
return false;
}
}
} }
} }

View file

@ -14,9 +14,24 @@ namespace Bit.Android.Autofill
public HashSet<int> Ids { get; private set; } = new HashSet<int>(); public HashSet<int> Ids { get; private set; } = new HashSet<int>();
public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>(); public List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
public SaveDataType SaveType { get; private set; } = SaveDataType.Generic; public SaveDataType SaveType
public List<string> Hints { get; private set; } = new List<string>(); {
public List<string> FocusedHints { get; private set; } = new List<string>(); get
{
if(FillableForLogin)
{
return SaveDataType.Password;
}
else if(FillableForCard)
{
return SaveDataType.CreditCard;
}
return SaveDataType.Generic;
}
}
public HashSet<string> Hints { get; private set; } = new HashSet<string>();
public HashSet<string> FocusedHints { get; private set; } = new HashSet<string>();
public List<Field> Fields { get; private set; } = new List<Field>(); public List<Field> Fields { get; private set; } = new List<Field>();
public IDictionary<int, Field> IdToFieldMap { get; private set; } = public IDictionary<int, Field> IdToFieldMap { get; private set; } =
new Dictionary<int, Field>(); new Dictionary<int, Field>();
@ -35,7 +50,11 @@ namespace Bit.Android.Autofill
if(Hints.Any()) if(Hints.Any())
{ {
_passwordFields = Fields.Where(f => f.Hints.Contains(View.AutofillHintPassword)).ToList(); _passwordFields = new List<Field>();
if(HintToFieldsMap.ContainsKey(View.AutofillHintPassword))
{
_passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]);
}
} }
else else
{ {
@ -59,15 +78,20 @@ namespace Bit.Android.Autofill
return _usernameFields; return _usernameFields;
} }
_usernameFields = new List<Field>();
if(Hints.Any()) if(Hints.Any())
{ {
_usernameFields = Fields if(HintToFieldsMap.ContainsKey(View.AutofillHintEmailAddress))
.Where(f => f.Hints.Any(fh => fh == View.AutofillHintEmailAddress || fh == View.AutofillHintUsername)) {
.ToList(); _usernameFields.AddRange(HintToFieldsMap[View.AutofillHintEmailAddress]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintUsername))
{
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
}
} }
else else
{ {
_usernameFields = new List<Field>();
foreach(var passwordField in PasswordFields) foreach(var passwordField in PasswordFields)
{ {
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault(); var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
@ -82,7 +106,15 @@ namespace Bit.Android.Autofill
} }
} }
public bool FillableForLogin => UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused); public bool FillableForLogin => FocusedHintsContain(
new string[] { View.AutofillHintUsername, View.AutofillHintEmailAddress, View.AutofillHintPassword }) ||
UsernameFields.Any(f => f.Focused) || PasswordFields.Any(f => f.Focused);
public bool FillableForCard => FocusedHintsContain(
new string[] { View.AutofillHintCreditCardNumber, View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear, View.AutofillHintCreditCardSecurityCode});
public bool FillableForIdentity => FocusedHintsContain(
new string[] { View.AutofillHintName, View.AutofillHintPhone, View.AutofillHintPostalAddress,
View.AutofillHintPostalCode });
public void Add(Field field) public void Add(Field field)
{ {
@ -95,20 +127,19 @@ namespace Bit.Android.Autofill
Ids.Add(field.Id); Ids.Add(field.Id);
Fields.Add(field); Fields.Add(field);
SaveType |= field.SaveType;
AutofillIds.Add(field.AutofillId); AutofillIds.Add(field.AutofillId);
IdToFieldMap.Add(field.Id, field); IdToFieldMap.Add(field.Id, field);
if((field.Hints?.Count ?? 0) > 0) if(field.Hints != null)
{ {
Hints.AddRange(field.Hints);
if(field.Focused)
{
FocusedHints.AddRange(field.Hints);
}
foreach(var hint in field.Hints) foreach(var hint in field.Hints)
{ {
Hints.Add(hint);
if(field.Focused)
{
FocusedHints.Add(hint);
}
if(!HintToFieldsMap.ContainsKey(hint)) if(!HintToFieldsMap.ContainsKey(hint))
{ {
HintToFieldsMap.Add(hint, new List<Field>()); HintToFieldsMap.Add(hint, new List<Field>());
@ -121,33 +152,53 @@ namespace Bit.Android.Autofill
public SavedItem GetSavedItem() public SavedItem GetSavedItem()
{ {
if(!Fields?.Any() ?? true) if(SaveType == SaveDataType.Password)
{ {
return null; var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue));
} if(passwordField == null)
var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue));
if(passwordField == null)
{
return null;
}
var savedItem = new SavedItem
{
Type = App.Enums.CipherType.Login,
Login = new SavedItem.LoginItem
{ {
Password = passwordField.TextValue return null;
} }
};
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault(); var savedItem = new SavedItem
if(usernameField != null && !string.IsNullOrWhiteSpace(usernameField.TextValue)) {
Type = App.Enums.CipherType.Login,
Login = new SavedItem.LoginItem
{
Password = passwordField.TextValue
}
};
var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault();
if(usernameField != null && !string.IsNullOrWhiteSpace(usernameField.TextValue))
{
savedItem.Login.Username = usernameField.TextValue;
}
return savedItem;
}
else if(SaveType == SaveDataType.CreditCard)
{ {
savedItem.Login.Username = usernameField.TextValue; var savedItem = new SavedItem
{
Type = App.Enums.CipherType.Card,
Card = new SavedItem.CardItem
{
Number = HintToFieldsMap.ContainsKey(View.AutofillHintCreditCardNumber) ?
HintToFieldsMap[View.AutofillHintCreditCardNumber].FirstOrDefault(
f => !string.IsNullOrWhiteSpace(f.TextValue))?.TextValue : null
}
};
return savedItem;
} }
return savedItem; return null;
}
private bool FocusedHintsContain(IEnumerable<string> hints)
{
return hints.Any(h => FocusedHints.Contains(h));
} }
} }
} }

View file

@ -170,7 +170,7 @@ namespace Bit.Android
} }
var items = new List<IFilledItem> { new CipherFilledItem(cipher) }; var items = new List<IFilledItem> { new CipherFilledItem(cipher) };
var response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items); var response = AutofillHelpers.BuildFillResponse(this, parser, items);
var replyIntent = new Intent(); var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, response); replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, response);
SetResult(Result.Ok, replyIntent); SetResult(Result.Ok, replyIntent);

View file

@ -376,6 +376,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Auto-fill with bitwarden.
/// </summary>
public static string AutofillWithBitwarden {
get {
return ResourceManager.GetString("AutofillWithBitwarden", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Avoid Ambiguous Characters. /// Looks up a localized string similar to Avoid Ambiguous Characters.
/// </summary> /// </summary>
@ -2860,6 +2869,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Vault is locked.
/// </summary>
public static string VaultIsLocked {
get {
return ResourceManager.GetString("VaultIsLocked", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Verification Code. /// Looks up a localized string similar to Verification Code.
/// </summary> /// </summary>

View file

@ -1185,4 +1185,10 @@
<data name="IconsUrl" xml:space="preserve"> <data name="IconsUrl" xml:space="preserve">
<value>Icons Server URL</value> <value>Icons Server URL</value>
</data> </data>
<data name="AutofillWithBitwarden" xml:space="preserve">
<value>Auto-fill with bitwarden</value>
</data>
<data name="VaultIsLocked" xml:space="preserve">
<value>Vault is locked</value>
</data>
</root> </root>