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,21 +2,24 @@
using System.Collections.Generic;
using Android.Content;
using Android.Service.Autofill;
using Android.Views;
using Android.Widget;
using System.Linq;
using Android.App;
using Bit.App.Abstractions;
using System.Threading.Tasks;
using Bit.App.Resources;
namespace Bit.Android.Autofill
{
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 ciphers = await service.GetAllAsync(uri);
if(parser.FieldCollection.FillableForLogin)
{
var ciphers = await service.GetAllAsync(parser.Uri);
if(ciphers.Item1.Any() || ciphers.Item2.Any())
{
var allCiphers = ciphers.Item1.ToList();
@ -26,18 +29,27 @@ namespace Bit.Android.Autofill
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));
}
}
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();
if(items != null && items.Count > 0)
{
foreach(var item in items)
{
var dataset = BuildDataset(context, fields, item);
var dataset = BuildDataset(context, parser.FieldCollection, item);
if(dataset != null)
{
responseBuilder.AddDataset(dataset);
@ -45,8 +57,8 @@ namespace Bit.Android.Autofill
}
}
AddSaveInfo(responseBuilder, fields);
responseBuilder.SetIgnoredIds(fields.IgnoreAutofillIds.ToArray());
AddSaveInfo(responseBuilder, parser.FieldCollection);
responseBuilder.SetIgnoredIds(parser.FieldCollection.IgnoreAutofillIds.ToArray());
return responseBuilder.Build();
}
@ -64,8 +76,8 @@ namespace Bit.Android.Autofill
public static FillResponse BuildAuthResponse(Context context, FieldCollection fields, string uri)
{
var responseBuilder = new FillResponse.Builder();
var view = BuildListView(context.PackageName, "Auto-fill with bitwarden",
"Vault is locked", Resource.Drawable.icon);
var view = BuildListView(context.PackageName, AppResources.AutofillWithBitwarden,
AppResources.VaultIsLocked, Resource.Drawable.icon);
var intent = new Intent(context, typeof(MainActivity));
intent.PutExtra("autofillFramework", true);
intent.PutExtra("autofillFrameworkUri", uri);
@ -87,36 +99,14 @@ namespace Bit.Android.Autofill
public static void AddSaveInfo(FillResponse.Builder responseBuilder, FieldCollection fields)
{
var saveInfo = new SaveInfo.Builder(SaveDataType.Password, fields.AutofillIds.ToArray()).Build();
var saveType = fields.SaveType;
if(saveType == SaveDataType.Generic)
{
return;
}
var saveInfo = new SaveInfo.Builder(saveType, fields.AutofillIds.ToArray()).Build();
responseBuilder.SetSaveInfo(saveInfo);
}
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:
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

@ -34,7 +34,8 @@ namespace Bit.Android.Autofill
parser.Parse();
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;
}
@ -58,8 +59,8 @@ namespace Bit.Android.Autofill
}
// build response
var items = await AutofillHelpers.GetFillItemsAsync(_cipherService, parser.Uri);
var response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items);
var items = await AutofillHelpers.GetFillItemsAsync(parser, _cipherService);
var response = AutofillHelpers.BuildFillResponse(this, parser, items);
callback.OnSuccess(response);
}

View file

@ -5,12 +5,17 @@ using System.Linq;
using Bit.App.Models;
using Bit.App.Enums;
using Bit.App.Models.Page;
using Android.Views;
namespace Bit.Android.Autofill
{
public class CipherFilledItem : IFilledItem
{
private Lazy<string> _password;
private string _cardNumber;
private Lazy<string> _cardExpMonth;
private Lazy<string> _cardExpYear;
private Lazy<string> _cardCode;
public CipherFilledItem(Cipher cipher)
{
@ -21,8 +26,24 @@ namespace Bit.Android.Autofill
{
case CipherType.Login:
Subtitle = cipher.Login.Username?.Decrypt() ?? string.Empty;
_password = new Lazy<string>(() => cipher.Login.Password?.Decrypt());
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;
default:
break;
@ -58,32 +79,68 @@ namespace Bit.Android.Autofill
return false;
}
var setValues = false;
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;
datasetBuilder.SetValue(f.AutofillId, AutofillValue.ForText(_password.Value));
}
foreach(var passwordField in fieldCollection.PasswordFields)
{
datasetBuilder.SetValue(passwordField.AutofillId, AutofillValue.ForText(_password.Value));
}
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));
}
}
}
else if(Type == CipherType.Card)
{
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 true;
}
else
{
return false;
}
return setValues;
}
}
}

View file

@ -23,7 +23,7 @@ namespace Bit.Android.Autofill
Selected = node.IsSelected;
Clickable = node.IsClickable;
Visible = node.Visibility == ViewStates.Visible;
Hints = AutofillHelpers.FilterForSupportedHints(node.GetAutofillHints());
Hints = FilterForSupportedHints(node.GetAutofillHints());
AutofillOptions = node.GetAutofillOptions()?.ToList();
if(node.AutofillValue != null)
@ -168,5 +168,33 @@ namespace Bit.Android.Autofill
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
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 List<AutofillId> AutofillIds { get; private set; } = new List<AutofillId>();
public SaveDataType SaveType { get; private set; } = SaveDataType.Generic;
public List<string> Hints { get; private set; } = new List<string>();
public List<string> FocusedHints { get; private set; } = new List<string>();
public SaveDataType SaveType
{
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 IDictionary<int, Field> IdToFieldMap { get; private set; } =
new Dictionary<int, Field>();
@ -35,7 +50,11 @@ namespace Bit.Android.Autofill
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
{
@ -59,15 +78,20 @@ namespace Bit.Android.Autofill
return _usernameFields;
}
_usernameFields = new List<Field>();
if(Hints.Any())
{
_usernameFields = Fields
.Where(f => f.Hints.Any(fh => fh == View.AutofillHintEmailAddress || fh == View.AutofillHintUsername))
.ToList();
if(HintToFieldsMap.ContainsKey(View.AutofillHintEmailAddress))
{
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintEmailAddress]);
}
if(HintToFieldsMap.ContainsKey(View.AutofillHintUsername))
{
_usernameFields.AddRange(HintToFieldsMap[View.AutofillHintUsername]);
}
}
else
{
_usernameFields = new List<Field>();
foreach(var passwordField in PasswordFields)
{
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)
{
@ -95,20 +127,19 @@ namespace Bit.Android.Autofill
Ids.Add(field.Id);
Fields.Add(field);
SaveType |= field.SaveType;
AutofillIds.Add(field.AutofillId);
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)
{
Hints.Add(hint);
if(field.Focused)
{
FocusedHints.Add(hint);
}
if(!HintToFieldsMap.ContainsKey(hint))
{
HintToFieldsMap.Add(hint, new List<Field>());
@ -121,11 +152,8 @@ namespace Bit.Android.Autofill
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)
{
@ -149,5 +177,28 @@ namespace Bit.Android.Autofill
return savedItem;
}
else if(SaveType == SaveDataType.CreditCard)
{
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 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 response = AutofillHelpers.BuildFillResponse(this, parser.FieldCollection, items);
var response = AutofillHelpers.BuildFillResponse(this, parser, items);
var replyIntent = new Intent();
replyIntent.PutExtra(AutofillManager.ExtraAuthenticationResult, response);
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>
/// Looks up a localized string similar to Avoid Ambiguous Characters.
/// </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>
/// Looks up a localized string similar to Verification Code.
/// </summary>

View file

@ -1185,4 +1185,10 @@
<data name="IconsUrl" xml:space="preserve">
<value>Icons Server URL</value>
</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>