From c45a77d53848b9e618dfd66ab83997eac8f0d539 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Sat, 18 Nov 2017 15:09:09 -0500 Subject: [PATCH] add support for card filling --- src/Android/Autofill/AutofillHelpers.cs | 72 +++++------- src/Android/Autofill/AutofillService.cs | 7 +- src/Android/Autofill/CipherFilledItem.cs | 85 +++++++++++--- src/Android/Autofill/Field.cs | 30 ++++- src/Android/Autofill/FieldCollection.cs | 125 +++++++++++++++------ src/Android/MainActivity.cs | 2 +- src/App/Resources/AppResources.Designer.cs | 18 +++ src/App/Resources/AppResources.resx | 6 + 8 files changed, 248 insertions(+), 97 deletions(-) diff --git a/src/Android/Autofill/AutofillHelpers.cs b/src/Android/Autofill/AutofillHelpers.cs index 5bf44bc61..32ae0ce4e 100644 --- a/src/Android/Autofill/AutofillHelpers.cs +++ b/src/Android/Autofill/AutofillHelpers.cs @@ -2,26 +2,38 @@ 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> GetFillItemsAsync(ICipherService service, string uri) + public static async Task> GetFillItemsAsync(Parser parser, ICipherService service) { var items = new List(); - var ciphers = await service.GetAllAsync(uri); - if(ciphers.Item1.Any() || ciphers.Item2.Any()) + + if(parser.FieldCollection.FillableForLogin) { - var allCiphers = ciphers.Item1.ToList(); - allCiphers.AddRange(ciphers.Item2.ToList()); - foreach(var cipher in allCiphers) + var ciphers = await service.GetAllAsync(parser.Uri); + if(ciphers.Item1.Any() || ciphers.Item2.Any()) + { + 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)); } @@ -30,14 +42,14 @@ namespace Bit.Android.Autofill return items; } - public static FillResponse BuildFillResponse(Context context, FieldCollection fields, List items) + public static FillResponse BuildFillResponse(Context context, Parser parser, List 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(); - responseBuilder.SetSaveInfo(saveInfo); - } - - public static List FilterForSupportedHints(string[] hints) - { - return hints?.Where(h => IsValidHint(h)).ToList() ?? new List(); - } - - public static bool IsValidHint(string hint) - { - switch(hint) + var saveType = fields.SaveType; + if(saveType == SaveDataType.Generic) { - 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; + return; } + + var saveInfo = new SaveInfo.Builder(saveType, fields.AutofillIds.ToArray()).Build(); + responseBuilder.SetSaveInfo(saveInfo); } } } \ No newline at end of file diff --git a/src/Android/Autofill/AutofillService.cs b/src/Android/Autofill/AutofillService.cs index faafbfb67..05cc7059d 100644 --- a/src/Android/Autofill/AutofillService.cs +++ b/src/Android/Autofill/AutofillService.cs @@ -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); } diff --git a/src/Android/Autofill/CipherFilledItem.cs b/src/Android/Autofill/CipherFilledItem.cs index ef53a865a..b94d31d2d 100644 --- a/src/Android/Autofill/CipherFilledItem.cs +++ b/src/Android/Autofill/CipherFilledItem.cs @@ -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 _password; + private string _cardNumber; + private Lazy _cardExpMonth; + private Lazy _cardExpYear; + private Lazy _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(() => cipher.Login.Password?.Decrypt()); Icon = Resource.Drawable.login; + _password = new Lazy(() => 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(() => cipher.Card.Code?.Decrypt()); + _cardExpMonth = new Lazy(() => cipher.Card.ExpMonth?.Decrypt()); + _cardExpYear = new Lazy(() => 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 passwordField in fieldCollection.PasswordFields) - { - datasetBuilder.SetValue(passwordField.AutofillId, AutofillValue.ForText(_password.Value)); + foreach(var f in fieldCollection.PasswordFields) + { + setValues = true; + datasetBuilder.SetValue(f.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)); } } - - 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; } } } \ No newline at end of file diff --git a/src/Android/Autofill/Field.cs b/src/Android/Autofill/Field.cs index e83b4a43e..9353a5b17 100644 --- a/src/Android/Autofill/Field.cs +++ b/src/Android/Autofill/Field.cs @@ -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 FilterForSupportedHints(string[] hints) + { + return hints?.Where(h => IsValidHint(h)).ToList() ?? new List(); + } + + 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; + } + } } } diff --git a/src/Android/Autofill/FieldCollection.cs b/src/Android/Autofill/FieldCollection.cs index 14480f153..6cdfc1fc6 100644 --- a/src/Android/Autofill/FieldCollection.cs +++ b/src/Android/Autofill/FieldCollection.cs @@ -14,9 +14,24 @@ namespace Bit.Android.Autofill public HashSet Ids { get; private set; } = new HashSet(); public List AutofillIds { get; private set; } = new List(); - public SaveDataType SaveType { get; private set; } = SaveDataType.Generic; - public List Hints { get; private set; } = new List(); - public List FocusedHints { get; private set; } = new List(); + public SaveDataType SaveType + { + get + { + if(FillableForLogin) + { + return SaveDataType.Password; + } + else if(FillableForCard) + { + return SaveDataType.CreditCard; + } + + return SaveDataType.Generic; + } + } + public HashSet Hints { get; private set; } = new HashSet(); + public HashSet FocusedHints { get; private set; } = new HashSet(); public List Fields { get; private set; } = new List(); public IDictionary IdToFieldMap { get; private set; } = new Dictionary(); @@ -35,7 +50,11 @@ namespace Bit.Android.Autofill if(Hints.Any()) { - _passwordFields = Fields.Where(f => f.Hints.Contains(View.AutofillHintPassword)).ToList(); + _passwordFields = new List(); + if(HintToFieldsMap.ContainsKey(View.AutofillHintPassword)) + { + _passwordFields.AddRange(HintToFieldsMap[View.AutofillHintPassword]); + } } else { @@ -59,15 +78,20 @@ namespace Bit.Android.Autofill return _usernameFields; } + _usernameFields = new List(); 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(); 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()); @@ -121,33 +152,53 @@ 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) - { - return null; - } - - var savedItem = new SavedItem - { - Type = App.Enums.CipherType.Login, - Login = new SavedItem.LoginItem + var passwordField = PasswordFields.FirstOrDefault(f => !string.IsNullOrWhiteSpace(f.TextValue)); + if(passwordField == null) { - Password = passwordField.TextValue + return null; } - }; - var usernameField = Fields.TakeWhile(f => f.Id != passwordField.Id).LastOrDefault(); - if(usernameField != null && !string.IsNullOrWhiteSpace(usernameField.TextValue)) + var savedItem = new SavedItem + { + 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 hints) + { + return hints.Any(h => FocusedHints.Contains(h)); } } } \ No newline at end of file diff --git a/src/Android/MainActivity.cs b/src/Android/MainActivity.cs index 4816b4fe8..92d7375e8 100644 --- a/src/Android/MainActivity.cs +++ b/src/Android/MainActivity.cs @@ -170,7 +170,7 @@ namespace Bit.Android } var items = new List { 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); diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 180d3cf2d..bc31aa222 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -376,6 +376,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Auto-fill with bitwarden. + /// + public static string AutofillWithBitwarden { + get { + return ResourceManager.GetString("AutofillWithBitwarden", resourceCulture); + } + } + /// /// Looks up a localized string similar to Avoid Ambiguous Characters. /// @@ -2860,6 +2869,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Vault is locked. + /// + public static string VaultIsLocked { + get { + return ResourceManager.GetString("VaultIsLocked", resourceCulture); + } + } + /// /// Looks up a localized string similar to Verification Code. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index c3bbb9d9e..919fbfef6 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -1185,4 +1185,10 @@ Icons Server URL + + Auto-fill with bitwarden + + + Vault is locked + \ No newline at end of file