diff --git a/src/KeePass.sln b/src/KeePass.sln index 27d6c9ae..bcd542c4 100644 --- a/src/KeePass.sln +++ b/src/KeePass.sln @@ -25,7 +25,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCloudBindings", "PCloudBin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keepass2android\keepass2android-app.csproj", "{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParserTest", "Kp2aAutofillParserTest\Kp2aAutofillParserTest.csproj", "{3D1560FF-86BB-4CB4-8367-80BA13B81C38}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -309,6 +311,30 @@ Global {39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU {39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU {39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|x64.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.ActiveCfg = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.Build.0 = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.ActiveCfg = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.Build.0 = Debug|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU + {3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Kp2aAutofillParser/AutofillParser.cs b/src/Kp2aAutofillParser/AutofillParser.cs index 489f81b4..049aeb0c 100644 --- a/src/Kp2aAutofillParser/AutofillParser.cs +++ b/src/Kp2aAutofillParser/AutofillParser.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using System.Xml; +using Newtonsoft.Json; +using Formatting = System.Xml.Formatting; namespace Kp2aAutofillParser { @@ -192,54 +194,54 @@ namespace Kp2aAutofillParser return false; } } - class AutofillHintsHelper + public class AutofillHintsHelper { - const string AutofillHint2faAppOtp = "2faAppOTPCode"; - const string AutofillHintBirthDateDay = "birthDateDay"; - const string AutofillHintBirthDateFull = "birthDateFull"; - const string AutofillHintBirthDateMonth = "birthDateMonth"; - const string AutofillHintBirthDateYear = "birthDateYear"; - const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; - const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; - const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; - const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; - const string AutofillHintCreditCardNumber = "creditCardNumber"; - const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; - const string AutofillHintEmailAddress = "emailAddress"; - const string AutofillHintEmailOtp = "emailOTPCode"; - const string AutofillHintGender = "gender"; - const string AutofillHintName = "name"; - const string AutofillHintNewPassword = "newPassword"; - const string AutofillHintNewUsername = "newUsername"; - const string AutofillHintNotApplicable = "notApplicable"; - const string AutofillHintPassword = "password"; - const string AutofillHintPersonName = "personName"; - const string AutofillHintPersonNameFAMILY = "personFamilyName"; - const string AutofillHintPersonNameGIVEN = "personGivenName"; - const string AutofillHintPersonNameMIDDLE = "personMiddleName"; - const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; - const string AutofillHintPersonNamePREFIX = "personNamePrefix"; - const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; - const string AutofillHintPhone = "phone"; - const string AutofillHintPhoneContryCode = "phoneCountryCode"; - const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; - const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; - const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; - const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; - const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; - const string AutofillHintPostalAddressLOCALITY = "addressLocality"; - const string AutofillHintPostalAddressREGION = "addressRegion"; - const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; - const string AutofillHintPostalCode = "postalCode"; - const string AutofillHintPromoCode = "promoCode"; - const string AutofillHintSMS_OTP = "smsOTPCode"; - const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; - const string AutofillHintUsername = "username"; - const string AutofillHintWifiPassword = "wifiPassword"; - const string AutofillHintPhoneNational = "phoneNational"; - const string AutofillHintPhoneNumber = "phoneNumber"; - const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; - const string AutofillHintPostalAddress = "postalAddress"; + public const string AutofillHint2faAppOtp = "2faAppOTPCode"; + public const string AutofillHintBirthDateDay = "birthDateDay"; + public const string AutofillHintBirthDateFull = "birthDateFull"; + public const string AutofillHintBirthDateMonth = "birthDateMonth"; + public const string AutofillHintBirthDateYear = "birthDateYear"; + public const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; + public const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; + public const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; + public const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; + public const string AutofillHintCreditCardNumber = "creditCardNumber"; + public const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; + public const string AutofillHintEmailAddress = "emailAddress"; + public const string AutofillHintEmailOtp = "emailOTPCode"; + public const string AutofillHintGender = "gender"; + public const string AutofillHintName = "name"; + public const string AutofillHintNewPassword = "newPassword"; + public const string AutofillHintNewUsername = "newUsername"; + public const string AutofillHintNotApplicable = "notApplicable"; + public const string AutofillHintPassword = "password"; + public const string AutofillHintPersonName = "personName"; + public const string AutofillHintPersonNameFAMILY = "personFamilyName"; + public const string AutofillHintPersonNameGIVEN = "personGivenName"; + public const string AutofillHintPersonNameMIDDLE = "personMiddleName"; + public const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; + public const string AutofillHintPersonNamePREFIX = "personNamePrefix"; + public const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; + public const string AutofillHintPhone = "phone"; + public const string AutofillHintPhoneContryCode = "phoneCountryCode"; + public const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; + public const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; + public const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; + public const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; + public const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; + public const string AutofillHintPostalAddressLOCALITY = "addressLocality"; + public const string AutofillHintPostalAddressREGION = "addressRegion"; + public const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; + public const string AutofillHintPostalCode = "postalCode"; + public const string AutofillHintPromoCode = "promoCode"; + public const string AutofillHintSMS_OTP = "smsOTPCode"; + public const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; + public const string AutofillHintUsername = "username"; + public const string AutofillHintWifiPassword = "wifiPassword"; + public const string AutofillHintPhoneNational = "phoneNational"; + public const string AutofillHintPhoneNumber = "phoneNumber"; + public const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; + public const string AutofillHintPostalAddress = "postalAddress"; private static readonly HashSet _allSupportedHints = new HashSet(StringComparer.OrdinalIgnoreCase) { @@ -540,8 +542,29 @@ namespace Kp2aAutofillParser { bool IsTrustedApp(string packageName); bool IsTrustedLink(string domain, string targetPackage); + bool IsEnabled(); } + + class TimeUtil + { + private static DateTime? m_dtUnixRoot = null; + public static DateTime ConvertUnixTime(double dtUnix) + { + try + { + if (!m_dtUnixRoot.HasValue) + m_dtUnixRoot = (new DateTime(1970, 1, 1, 0, 0, 0, 0, + DateTimeKind.Utc)).ToLocalTime(); + + return m_dtUnixRoot.Value.AddSeconds(dtUnix); + } + catch (Exception) { Debug.Assert(false); } + + return DateTime.UtcNow; + } + } + public class FilledAutofillField where FieldT : InputField { private string[] _autofillHints; @@ -671,7 +694,7 @@ namespace Kp2aAutofillParser } /// - /// Base class for everything that is a input field which might (or might not) be autofilled. + /// Base class for everything that is (or could be) an input field which might (or might not) be autofilled. /// For testability, this is independent from Android classes like ViewNode /// public abstract class InputField @@ -687,11 +710,12 @@ namespace Kp2aAutofillParser public string HtmlInfoTag { get; set; } public string HtmlInfoTypeAttribute { get; set; } - public abstract void FillFilledAutofillValue(FilledAutofillField filledField); - - } + /// + /// Serializable structure defining the contents of the current view (from an autofill perspective) + /// + /// public class AutofillView where TField : InputField { public List InputFields { get; set; } = new List(); @@ -710,8 +734,17 @@ namespace Kp2aAutofillParser private readonly ILogger _log; private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource; + private readonly List _autofillHintsForLogin = new List + { + AutofillHintsHelper.AutofillHintPassword, + AutofillHintsHelper.AutofillHintUsername, + AutofillHintsHelper.AutofillHintEmailAddress + }; + public string PackageId { get; set; } + public Dictionary FieldsMappedToHints = new Dictionary(); + public StructureParserBase(ILogger logger, IKp2aDigitalAssetLinksDataSource digitalAssetLinksDataSource) { _log = logger; @@ -763,151 +796,86 @@ namespace Kp2aAutofillParser /// The parse. /// If set to true for fill. /// - AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView autofillView) + protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView autofillView) { - AutofillTargetId result = new AutofillTargetId(); + AutofillTargetId result = new AutofillTargetId() + { + PackageName = autofillView.PackageId, + WebDomain = autofillView.WebDomain + }; - - _editTextsWithoutHint.Clear(); - _log.Log("parsing autofillStructure..."); - - //TODO remove from production - _log.Log("will log the autofillStructure..."); - string debugInfo = JsonConvert.SerializeObject(autofillView, Formatting.Indented); - _log.Log("will log the autofillStructure... size is " + debugInfo.Length); - _log.Log("This is the autofillStructure: \n\n " + debugInfo); - foreach (var viewNode in autofillView.InputFields) + if (LogAutofillView) { - string[] viewHints = viewNode.AutofillHints; - if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused && - isManualRequest) - viewHints[0] = "on"; - /*if (viewHints != null && viewHints.Any()) - { - CommonUtil.logd("viewHints=" + viewHints); - CommonUtil.logd("class=" + viewNode.ClassName); - CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)")); - }*/ - - if (IsPassword(viewNode) || HasPasswordHint(viewNode) || (HasUsernameHint(viewNode))) - { - if (forFill) - { - AutofillFields.Add(new AutofillFieldMetadata(viewNode.ViewNode)); - } - else - { - FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode.ViewNode); - ClientFormData.Add(filledAutofillField); - } - } - else if (viewNode.ClassName == "android.widget.EditText" - || viewNode.ClassName == "android.widget.AutoCompleteTextView" - || viewNode.HtmlInfoTag == "input" - || ((viewHints?.Length ?? 0) > 0)) - { - _log.Log("Found something that looks fillable " + viewNode.ClassName); - - } - - if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/) - { - } - else - { - - if (viewNode.ClassName == "android.widget.EditText" - || viewNode.ClassName == "android.widget.AutoCompleteTextView" - || viewNode.HtmlInfoTag == "input") - { - _editTextsWithoutHint.Add(viewNode); - } - - } + string debugInfo = JsonConvert.SerializeObject(autofillView, Newtonsoft.Json.Formatting.Indented); + _log.Log("This is the autofillStructure: \n\n " + debugInfo); } - List passwordFields = new List(); - List usernameFields = new List(); - if (AutofillFields.Empty) + + //go through each input field and determine username/password fields. + //Depending on the target this can require more or less heuristics. + // * if there is a valid & supported autofill hint, we assume that all fields which should be filled do have an appropriate Autofill hint + // * if there is no such autofill hint, we use IsPassword to + + HashSet autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null) + .SelectMany(f => f.AutofillHints).ToHashSet(); + bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any(); + + if (hasLoginAutofillHints) { - passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList(); - if (!passwordFields.Any()) + foreach (var viewNode in autofillView.InputFields) { - passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList(); - } - - usernameFields = _editTextsWithoutHint.Where(HasUsernameHint).ToList(); - - if (usernameFields.Any() == false) - { - - foreach (var passwordField in passwordFields) + string[] viewHints = viewNode.AutofillHints; + if (viewHints == null) + continue; + if (viewHints.Intersect(_autofillHintsForLogin).Any()) { - var usernameField = _editTextsWithoutHint - .TakeWhile(f => f != passwordField).LastOrDefault(); - if (usernameField != null) - { - usernameFields.Add(usernameField); - } - } - } - if (usernameFields.Any() == false) - { - //for some pages with two-step login, we don't see a password field and don't display the autofill for non-manual requests. But if the user forces autofill, - //let's assume it is a username field: - if (isManualRequest && !passwordFields.Any() && _editTextsWithoutHint.Count == 1) - { - usernameFields.Add(_editTextsWithoutHint.First()); - } - } - - - } - - //force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints) - if (isManualRequest) - { - foreach (var editText in _editTextsWithoutHint) - { - if (editText.IsFocused) - { - if (IsPassword(editText) || HasPasswordHint(editText)) - passwordFields.Add(editText); - else - usernameFields.Add(editText); - break; + FieldsMappedToHints.Add(viewNode, viewHints); } } } - - if (forFill) - { - foreach (var uf in usernameFields) - AutofillFields.Add(new AutofillFieldMetadata(uf.ViewNode, new[] { View.AutofillHintUsername })); - foreach (var pf in passwordFields) - AutofillFields.Add(new AutofillFieldMetadata(pf.ViewNode, new[] { View.AutofillHintPassword })); - - } else { - foreach (var uf in usernameFields) - ClientFormData.Add(new FilledAutofillField(uf.ViewNode, new[] { View.AutofillHintUsername })); - foreach (var pf in passwordFields) - ClientFormData.Add(new FilledAutofillField(pf.ViewNode, new[] { View.AutofillHintPassword })); + //determine password fields, first by type, then by hint: + List passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && IsPassword(f)).ToList(); + if (!passwordFields.Any()) + { + passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && HasPasswordHint(f)).ToList(); + } + + //determine username fields. Try by hint, if that fails use the one before the password + List usernameFields = autofillView.InputFields.Where(f => IsEditText(f) && HasUsernameHint(f)).ToList(); + if (!usernameFields.Any()) + { + foreach (var passwordField in passwordFields) + { + var lastInputBeforePassword = autofillView.InputFields + .TakeWhile(f => IsEditText(f) && f != passwordField && !passwordFields.Contains(f)).LastOrDefault(); + if (lastInputBeforePassword != null) + usernameFields.Add(lastInputBeforePassword); + } + + } + + //for "heuristic determination" we demand that one of the filled fields is focused: + if (passwordFields.Concat(usernameFields).Any(f => f.IsFocused)) + { + foreach (var uf in usernameFields) + FieldsMappedToHints.Add(uf, new string[] { AutofillHintsHelper.AutofillHintUsername }); + foreach (var pf in passwordFields) + FieldsMappedToHints.Add(pf, new string[] { AutofillHintsHelper.AutofillHintPassword }); + } } + - - result.WebDomain = autofillView.WebDomain; - result.PackageName = Structure.ActivityComponent.PackageName; - if (!string.IsNullOrEmpty(autofillView.WebDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false)) + if (!string.IsNullOrEmpty(autofillView.WebDomain) && _digitalAssetLinksDataSource.IsEnabled()) { - result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); + result.IncompatiblePackageAndDomain = !_digitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); if (result.IncompatiblePackageAndDomain) { - CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); + _log.Log($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); } } else @@ -916,29 +884,40 @@ namespace Kp2aAutofillParser } return result; } - private static readonly HashSet _passwordHints = new HashSet { "password", "passwort", "passwordAuto", "pswd" }; + + public bool LogAutofillView { get; set; } + + private bool IsEditText(FieldT f) + { + return (f.ClassName == "android.widget.EditText" + || f.ClassName == "android.widget.AutoCompleteTextView" + || f.HtmlInfoTag == "input"); + } + + private static readonly HashSet _passwordHints = new HashSet { "password", "passwort" + /*, "passwordAuto", "pswd"*/ }; private static bool HasPasswordHint(InputField f) { - return ContainsAny(f.IdEntry, _passwordHints) || - ContainsAny(f.Hint, _passwordHints); + return IsAny(f.IdEntry, _passwordHints) || + IsAny(f.Hint, _passwordHints); } private static readonly HashSet _usernameHints = new HashSet { "email", "e-mail", "username" }; private static bool HasUsernameHint(InputField f) { - return ContainsAny(f.IdEntry, _usernameHints) || - ContainsAny(f.Hint, _usernameHints); + return IsAny(f.IdEntry, _usernameHints) || + IsAny(f.Hint, _usernameHints); } - private static bool ContainsAny(string value, IEnumerable terms) + private static bool IsAny(string value, IEnumerable terms) { if (string.IsNullOrWhiteSpace(value)) { return false; } var lowerValue = value.ToLowerInvariant(); - return terms.Any(t => lowerValue.Contains(t)); + return terms.Any(t => lowerValue == t); } private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass) @@ -970,12 +949,13 @@ namespace Kp2aAutofillParser || IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword) ) ) + || (f.AutofillHints != null && f.AutofillHints.First() == "passwordAuto") || (f.HtmlInfoTypeAttribute == "password") ); } - AssistStructure Structure; - private List _editTextsWithoutHint = new List(); + + diff --git a/src/Kp2aAutofillParserTest/AutofillTest.cs b/src/Kp2aAutofillParserTest/AutofillTest.cs new file mode 100644 index 00000000..fe4a44de --- /dev/null +++ b/src/Kp2aAutofillParserTest/AutofillTest.cs @@ -0,0 +1,114 @@ +using Kp2aAutofillParser; +using Newtonsoft.Json; +using System.IO; +using System.Reflection; +using Xunit.Abstractions; + +namespace Kp2aAutofillParserTest +{ + public class AutofillTest + { + private readonly ITestOutputHelper _testOutputHelper; + + public AutofillTest(ITestOutputHelper testOutputHelper) + { + _testOutputHelper = testOutputHelper; + } + + class TestInputField: InputField + { + public string[] ExpectedAssignedHints { get; set; } + } + + [Fact] + public void TestNotFocusedPasswordAutoIsNotFilled() + { + var resourceName = "Kp2aAutofillParserTest.com-servicenet-mobile-no-focus.json"; + RunTestFromAutofillInput(resourceName, "com.servicenet.mobile"); + } + + [Fact] + public void TestFocusedPasswordAutoIsFilled() + { + var resourceName = "Kp2aAutofillParserTest.com-servicenet-mobile-focused.json"; + RunTestFromAutofillInput(resourceName, "com.servicenet.mobile" ); + } + + [Fact] + public void TestMulitpleUnfocusedLoginsIsFilled() + { + var resourceName = "Kp2aAutofillParserTest.firefox-amazon-it.json"; + RunTestFromAutofillInput(resourceName, "org.mozilla.firefox", "www.amazon.it"); + } + + private void RunTestFromAutofillInput(string resourceName, string expectedPackageName = null, string expectedWebDomain = null) + { + var assembly = Assembly.GetExecutingAssembly(); + + + string input; + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + input = reader.ReadToEnd(); + } + + AutofillView? autofillView = + JsonConvert.DeserializeObject>(input); + + StructureParserBase parser = + new StructureParserBase(new TestLogger(), new TestDalSourceTrustAll()); + + var result = parser.ParseForFill(false, autofillView); + if (expectedPackageName != null) + Assert.Equal(expectedPackageName, result.PackageName); + if (expectedWebDomain != null) + Assert.Equal(expectedWebDomain, result.WebDomain); + foreach (var field in autofillView.InputFields) + { + string[] expectedHints = field.ExpectedAssignedHints; + if (expectedHints == null) + expectedHints = new string[0]; + string[] actualHints; + parser.FieldsMappedToHints.TryGetValue(field, out actualHints); + if (actualHints == null) + actualHints = new string[0]; + if (actualHints.Any() || expectedHints.Any()) + { + _testOutputHelper.WriteLine($"field = {field.IdEntry} {field.Hint} {string.Join(",", field.AutofillHints)}"); + _testOutputHelper.WriteLine("actual Hints = " + string.Join(", ", actualHints)); + _testOutputHelper.WriteLine("expected Hints = " + string.Join(", ", expectedHints)); + } + + Assert.Equal(expectedHints.Length, actualHints.Length); + Assert.Equal(expectedHints.OrderBy(x => x), actualHints.OrderBy(x => x)); + } + } + } + + public class TestDalSourceTrustAll : IKp2aDigitalAssetLinksDataSource + { + public bool IsTrustedApp(string packageName) + { + return true; + } + + public bool IsTrustedLink(string domain, string targetPackage) + { + return true; + } + + public bool IsEnabled() + { + return true; + } + } + + public class TestLogger : ILogger + { + public void Log(string x) + { + Console.WriteLine(x); + } + } +} \ No newline at end of file diff --git a/src/Kp2aAutofillParserTest/Kp2aAutofillParserTest.csproj b/src/Kp2aAutofillParserTest/Kp2aAutofillParserTest.csproj new file mode 100644 index 00000000..bda6ab66 --- /dev/null +++ b/src/Kp2aAutofillParserTest/Kp2aAutofillParserTest.csproj @@ -0,0 +1,46 @@ + + + + net6.0 + enable + enable + + false + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/src/Kp2aAutofillParserTest/Usings.cs b/src/Kp2aAutofillParserTest/Usings.cs new file mode 100644 index 00000000..8c927eb7 --- /dev/null +++ b/src/Kp2aAutofillParserTest/Usings.cs @@ -0,0 +1 @@ +global using Xunit; \ No newline at end of file diff --git a/src/Kp2aAutofillParserTest/com-servicenet-mobile-focused.json b/src/Kp2aAutofillParserTest/com-servicenet-mobile-focused.json new file mode 100644 index 00000000..60885b56 --- /dev/null +++ b/src/Kp2aAutofillParserTest/com-servicenet-mobile-focused.json @@ -0,0 +1,121 @@ +{ + "InputFields": [ + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": true, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_bar_root", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_mode_bar_stub", + "Hint": null, + "ClassName": "android.view.View", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "content", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "username_text_input_layout", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "username", + "Hint": "Username", + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": true, + "InputType": 97, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null, + "ExpectedAssignedHints": [ "username" ] + }, + { + "IdEntry": "password_text_input_layout", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "password", + "Hint": "Password", + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "passwordAuto" + ], + "IsFocused": false, + "InputType": 129, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null, + + "ExpectedAssignedHints": [ "password" ] + }, + { + "IdEntry": "login_button", + "Hint": null, + "ClassName": "android.widget.Button", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "progressBar", + "Hint": null, + "ClassName": "android.widget.ProgressBar", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "forgot_password", + "Hint": null, + "ClassName": "android.widget.TextView", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + } + ], + "PackageId": "com.servicenet.mobile", + "WebDomain": null +} \ No newline at end of file diff --git a/src/Kp2aAutofillParserTest/com-servicenet-mobile-no-focus.json b/src/Kp2aAutofillParserTest/com-servicenet-mobile-no-focus.json new file mode 100644 index 00000000..50d7fb20 --- /dev/null +++ b/src/Kp2aAutofillParserTest/com-servicenet-mobile-no-focus.json @@ -0,0 +1,119 @@ +{ + "InputFields": [ + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": true, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_bar_root", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_mode_bar_stub", + "Hint": null, + "ClassName": "android.view.View", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "content", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "username_text_input_layout", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "username", + "Hint": "Username", + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": false, + "InputType": 97, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "password_text_input_layout", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "password", + "Hint": "Password", + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "passwordAuto" + ], + "IsFocused": false, + "InputType": 129, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null, + + }, + { + "IdEntry": "login_button", + "Hint": null, + "ClassName": "android.widget.Button", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "progressBar", + "Hint": null, + "ClassName": "android.widget.ProgressBar", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "forgot_password", + "Hint": null, + "ClassName": "android.widget.TextView", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + } + ], + "PackageId": "com.servicenet.mobile", + "WebDomain": null +} \ No newline at end of file diff --git a/src/Kp2aAutofillParserTest/firefox-amazon-it.json b/src/Kp2aAutofillParserTest/firefox-amazon-it.json new file mode 100644 index 00000000..149526ff --- /dev/null +++ b/src/Kp2aAutofillParserTest/firefox-amazon-it.json @@ -0,0 +1,469 @@ +{ + "InputFields": [ + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_bar_root", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "action_mode_bar_stub", + "Hint": null, + "ClassName": "android.view.View", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "rootContainer", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "navigationToolbarStub", + "Hint": null, + "ClassName": "android.view.View", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "container", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "container", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "gestureLayout", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "browserWindow", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "browserLayout", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "swipeRefresh", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "engineView", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": true, + "InputType": 0, + "HtmlInfoTag": "", + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": "form", + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "password" + ], + "IsFocused": false, + "InputType": 225, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "password", + "ExpectedAssignedHints": [ "password" ] + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "emailAddress" + ], + "IsFocused": false, + "InputType": 33, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "email", + "ExpectedAssignedHints": [ "emailAddress" ] + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "checkbox" + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "submit" + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": "form", + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "password" + ], + "IsFocused": false, + "InputType": 225, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "password", + "ExpectedAssignedHints": [ "password" ] + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": [ + "emailAddress" + ], + "IsFocused": false, + "InputType": 33, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "email", + + "ExpectedAssignedHints": [ "emailAddress" ] + }, + { + "IdEntry": null, + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": "input", + "HtmlInfoTypeAttribute": "submit" + }, + { + "IdEntry": "stubFindInPage", + "Hint": null, + "ClassName": "android.view.View", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "viewDynamicDownloadDialog", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "crash_reporter_view", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "toolbar", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_navigation_actions", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_origin_view", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_title_view", + "Hint": null, + "ClassName": "android.widget.TextView", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_url_view", + "Hint": "Suche oder Adresse", + "ClassName": "android.widget.TextView", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_page_actions", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_browser_actions", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "counter_root", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "counter_text", + "Hint": null, + "ClassName": "android.widget.TextView", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_menu", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_progress", + "Hint": null, + "ClassName": "android.widget.ProgressBar", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_container", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_edit_actions_start", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_edit_url_view", + "Hint": null, + "ClassName": "android.widget.EditText", + "AutofillHints": null, + "IsFocused": false, + "InputType": 17, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "mozac_browser_toolbar_edit_actions_end", + "Hint": null, + "ClassName": "android.widget.LinearLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "readerViewControlsBar", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "addressSelectBar", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "creditCardSelectBar", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "loginSelectBar", + "Hint": null, + "ClassName": "android.view.ViewGroup", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + }, + { + "IdEntry": "tabPreview", + "Hint": null, + "ClassName": "android.widget.FrameLayout", + "AutofillHints": null, + "IsFocused": false, + "InputType": 0, + "HtmlInfoTag": null, + "HtmlInfoTypeAttribute": null + } + ], + "PackageId": "org.mozilla.firefox", + "WebDomain": "www.amazon.it" +} \ No newline at end of file diff --git a/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs b/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs index 28c19adc..a448e32b 100644 --- a/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs +++ b/src/keepass2android/services/AutofillBase/AutofillFieldMetadata.cs @@ -5,6 +5,7 @@ using Android.App.Assist; using Android.Service.Autofill; using Android.Views; using Android.Views.Autofill; +using Kp2aAutofillParser; namespace keepass2android.services.AutofillBase { @@ -40,7 +41,6 @@ namespace keepass2android.services.AutofillBase var supportedHints = AutofillHintsHelper.FilterForSupportedHints(autofillHints); var canonicalHints = AutofillHintsHelper.ConvertToCanonicalHints(supportedHints); SetHints(canonicalHints.ToArray()); - } void SetHints(string[] value) diff --git a/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs b/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs index 874725bd..ccdb86d8 100644 --- a/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs +++ b/src/keepass2android/services/AutofillBase/AutofillServiceBase.cs @@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline; using AndroidX.AutoFill.Inline.V1; using Java.Util.Concurrent.Atomic; using keepass2android.services.AutofillBase.model; +using Kp2aAutofillParser; namespace keepass2android.services.AutofillBase { @@ -255,7 +256,7 @@ namespace keepass2android.services.AutofillBase if (warning == DisplayWarning.None) { - FilledAutofillFieldCollection partitionData = + FilledAutofillFieldCollection partitionData = AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints); Kp2aLog.Log("AF: Add dataset"); @@ -299,7 +300,7 @@ namespace keepass2android.services.AutofillBase } - protected abstract List GetSuggestedEntries(string query); + protected abstract List> GetSuggestedEntries(string query); public enum DisplayWarning { diff --git a/src/keepass2android/services/AutofillBase/ChooseForAutofillActivityBase.cs b/src/keepass2android/services/AutofillBase/ChooseForAutofillActivityBase.cs index 73ba4b31..7319f80d 100644 --- a/src/keepass2android/services/AutofillBase/ChooseForAutofillActivityBase.cs +++ b/src/keepass2android/services/AutofillBase/ChooseForAutofillActivityBase.cs @@ -12,6 +12,7 @@ using Java.Util; using keepass2android.services.AutofillBase.model; using System.Linq; using Android.Content.PM; +using Kp2aAutofillParser; #if !NoNet using Com.Dropbox.Core.V2.Teamlog; #endif @@ -173,7 +174,7 @@ namespace keepass2android.services.AutofillBase ReplyIntent = null; } - protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap, bool isManual) + protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap, bool isManual) { var intent = Intent; AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure); @@ -229,7 +230,7 @@ namespace keepass2android.services.AutofillBase /// /// Creates the FilledAutofillFieldCollection from the intent returned from the query activity /// - protected abstract FilledAutofillFieldCollection GetDataset(); + protected abstract FilledAutofillFieldCollection GetDataset(); public abstract IAutofillIntentBuilder IntentBuilder { get; } diff --git a/src/keepass2android/services/AutofillBase/Kp2aDigitalAssetLinksDataSource.cs b/src/keepass2android/services/AutofillBase/Kp2aDigitalAssetLinksDataSource.cs index 31c01157..14fa2367 100644 --- a/src/keepass2android/services/AutofillBase/Kp2aDigitalAssetLinksDataSource.cs +++ b/src/keepass2android/services/AutofillBase/Kp2aDigitalAssetLinksDataSource.cs @@ -39,6 +39,11 @@ namespace keepass2android.services.AutofillBase return trustedLinks.Contains(BuildLink(domain, targetPackage)); } + public bool IsEnabled() + { + return !PreferenceManager.GetDefaultSharedPreferences(_ctx).GetBoolean(_ctx.GetString(Resource.String.NoDalVerification_key), false); + } + public void RememberAsTrustedApp(string packageName) { var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx); diff --git a/src/keepass2android/services/AutofillBase/StructureParser.cs b/src/keepass2android/services/AutofillBase/StructureParser.cs index 820e9241..932d95ac 100644 --- a/src/keepass2android/services/AutofillBase/StructureParser.cs +++ b/src/keepass2android/services/AutofillBase/StructureParser.cs @@ -1,22 +1,12 @@ using System; -using System.Collections.Generic; using System.Linq; + using Android.App.Assist; using Android.Content; -using Android.Preferences; -using Android.Text; -using Android.Util; -using Android.Views; using Android.Views.Autofill; -using Android.Views.InputMethods; using DomainNameParser; -using keepass2android.services.AutofillBase.model; using Kp2aAutofillParser; using Newtonsoft.Json; -using static Android.App.Assist.AssistStructure; -using static Java.IO.ObjectOutputStream; -using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection; -using InputTypes = Kp2aAutofillParser.InputTypes; namespace keepass2android.services.AutofillBase { @@ -38,7 +28,7 @@ namespace keepass2android.services.AutofillBase [JsonIgnore] public AssistStructure.ViewNode ViewNode { get; set; } - public override void FillFilledAutofillValue(FilledAutofillField filledField) + public void FillFilledAutofillValue(FilledAutofillField filledField) { AutofillValue autofillValue = ViewNode.AutofillValue; if (autofillValue != null) @@ -130,7 +120,6 @@ namespace keepass2android.services.AutofillBase } autofillView.InputFields.Add(new ViewNodeInputField(viewNode)); - Kp2aLog.Log($"Now we have {autofillView.InputFields.Count} fields, just added {autofillView.InputFields.Last().IdEntry} of type {autofillView.InputFields.Last().ClassName}"); var childrenSize = viewNode.ChildCount; if (childrenSize > 0) @@ -148,290 +137,63 @@ namespace keepass2android.services.AutofillBase /// AssistStructure from the client Activity, representing its View hierarchy. In this sample, it /// parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way. /// - public sealed class StructureParser + public sealed class StructureParser: StructureParserBase { - public Context mContext { get; } + private readonly AssistStructure _structure; + public Context _context { get; } public AutofillFieldMetadataCollection AutofillFields { get; set; } - public FilledAutofillFieldCollection ClientFormData { get; set; } + public FilledAutofillFieldCollection ClientFormData { get; set; } public string PackageId { get; set; } public StructureParser(Context context, AssistStructure structure) + : base(new Kp2aLogger(), new Kp2aDigitalAssetLinksDataSource(context)) { - kp2aDigitalAssetLinksDataSource = new Kp2aDigitalAssetLinksDataSource(context); - mContext = context; - Structure = structure; - AutofillFields = new AutofillFieldMetadataCollection(); - - } - - public class AutofillTargetId - { - public string PackageName { get; set; } - - public string PackageNameWithPseudoSchema - { - get { return KeePass.AndroidAppScheme + PackageName; } - } - - public string WebDomain { get; set; } - - /// - /// If PackageName and WebDomain are not compatible (by DAL or because PackageName is a trusted browser in which case we treat all domains as "compatible" - /// we need to issue a warning. If we would fill credentials for the package, a malicious website could try to get credentials for the app. - /// If we would fill credentials for the domain, a malicious app could get credentials for the domain. - /// - public bool IncompatiblePackageAndDomain { get; set; } - - public string DomainOrPackage - { - get - { - return WebDomain ?? PackageNameWithPseudoSchema; - } - } + _context = context; + _structure = structure; + AutofillFields = new AutofillFieldMetadataCollection(); + } - public AutofillTargetId ParseForFill(bool isManual) - { - return Parse(true, isManual); - } - - public AutofillTargetId ParseForSave() - { - return Parse(false, true); - } - - /// - /// Traverse AssistStructure and add ViewNode metadata to a flat list. - /// - /// The parse. - /// If set to true for fill. - /// - AutofillTargetId Parse(bool forFill, bool isManualRequest) + protected override AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView autofillView) { - AutofillTargetId result = new AutofillTargetId(); - CommonUtil.logd("Parsing structure for " + Structure.ActivityComponent); - - ClientFormData = new FilledAutofillFieldCollection(); - - _editTextsWithoutHint.Clear(); + var result = base.Parse(forFill, isManualRequest, autofillView); - Kp2aLog.Log("parsing autofillStructure..."); - - AutofillView autofillView = new AutofillViewFromAssistStructureFinder(mContext, Structure).GetAutofillView(isManualRequest); - - //TODO remove from production - Kp2aLog.Log("will log the autofillStructure..."); - string debugInfo = JsonConvert.SerializeObject(autofillView, Formatting.Indented); - Kp2aLog.Log("will log the autofillStructure... size is " + debugInfo.Length); - Kp2aLog.Log("This is the autofillStructure: \n\n " + debugInfo); - - foreach (var viewNode in autofillView.InputFields) + if (forFill) { - string[] viewHints = viewNode.AutofillHints; - if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused && - isManualRequest) - viewHints[0] = "on"; - /*if (viewHints != null && viewHints.Any()) - { - CommonUtil.logd("viewHints=" + viewHints); - CommonUtil.logd("class=" + viewNode.ClassName); - CommonUtil.logd("tag=" + (viewNode?.HtmlInfo?.Tag ?? "(null)")); - }*/ - - if (IsPassword(viewNode) || HasPasswordHint(viewNode) || (HasUsernameHint(viewNode))) - { - if (forFill) - { - AutofillFields.Add(new AutofillFieldMetadata(viewNode.ViewNode)); - } - else - { - FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode.ViewNode); - ClientFormData.Add(filledAutofillField); - } - } - else if (viewNode.ClassName == "android.widget.EditText" - || viewNode.ClassName == "android.widget.AutoCompleteTextView" - || viewNode.HtmlInfoTag == "input" - || ((viewHints?.Length ?? 0) > 0)) - { - Kp2aLog.Log("Found something that looks fillable " + viewNode.ClassName); - - } - - if (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/) - { - } - else - { - - if (viewNode.ClassName == "android.widget.EditText" - || viewNode.ClassName == "android.widget.AutoCompleteTextView" - || viewNode.HtmlInfoTag == "input") - { - _editTextsWithoutHint.Add(viewNode); - } - - } - } - - List passwordFields = new List(); - List usernameFields = new List(); - if (AutofillFields.Empty) - { - passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList(); - if (!passwordFields.Any()) - { - passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList(); - } - - usernameFields = _editTextsWithoutHint.Where(HasUsernameHint).ToList(); - - if (usernameFields.Any() == false) - { - - foreach (var passwordField in passwordFields) - { - var usernameField = _editTextsWithoutHint - .TakeWhile(f => f != passwordField).LastOrDefault(); - if (usernameField != null) - { - usernameFields.Add(usernameField); - } - } - } - if (usernameFields.Any() == false) - { - //for some pages with two-step login, we don't see a password field and don't display the autofill for non-manual requests. But if the user forces autofill, - //let's assume it is a username field: - if (isManualRequest && !passwordFields.Any() && _editTextsWithoutHint.Count == 1) - { - usernameFields.Add(_editTextsWithoutHint.First()); - } - } - - - } - - //force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints) - if (isManualRequest) - { - foreach (var editText in _editTextsWithoutHint) - { - if (editText.IsFocused) - { - if (IsPassword(editText) || HasPasswordHint(editText)) - passwordFields.Add(editText); - else - usernameFields.Add(editText); - break; - } - - } - } - - if (forFill) - { - foreach (var uf in usernameFields) - AutofillFields.Add(new AutofillFieldMetadata(uf.ViewNode, new[] { View.AutofillHintUsername })); - foreach (var pf in passwordFields) - AutofillFields.Add(new AutofillFieldMetadata(pf.ViewNode, new[] { View.AutofillHintPassword })); - + foreach (var p in FieldsMappedToHints) + AutofillFields.Add(new AutofillFieldMetadata(p.Key.ViewNode, p.Value)); } - else - { - foreach (var uf in usernameFields) - ClientFormData.Add(new FilledAutofillField(uf.ViewNode, new[] { View.AutofillHintUsername })); - foreach (var pf in passwordFields) - ClientFormData.Add(new FilledAutofillField(pf.ViewNode, new[] { View.AutofillHintPassword })); - } - - - result.WebDomain = autofillView.WebDomain; - result.PackageName = Structure.ActivityComponent.PackageName; - if (!string.IsNullOrEmpty(autofillView.WebDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false)) - { - result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); - if (result.IncompatiblePackageAndDomain) - { - CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); - } - } else { - result.IncompatiblePackageAndDomain = false; + foreach (var p in FieldsMappedToHints) + ClientFormData.Add(new FilledAutofillField(p.Key, p.Value)); } - return result; - } - private static readonly HashSet _passwordHints = new HashSet { "password","passwort", "passwordAuto", "pswd" }; - private static bool HasPasswordHint(InputField f) - { - return ContainsAny(f.IdEntry, _passwordHints) || - ContainsAny(f.Hint, _passwordHints); - } - - private static readonly HashSet _usernameHints = new HashSet { "email","e-mail","username" }; - private readonly Kp2aDigitalAssetLinksDataSource kp2aDigitalAssetLinksDataSource; - - private static bool HasUsernameHint(InputField f) - { - return ContainsAny(f.IdEntry, _usernameHints) || - ContainsAny(f.Hint, _usernameHints); - } - - private static bool ContainsAny(string value, IEnumerable terms) - { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - var lowerValue = value.ToLowerInvariant(); - return terms.Any(t => lowerValue.Contains(t)); - } - - private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass) - { - if (!InputTypes.MaskClass.HasFlag(inputTypeClass)) - throw new Exception("invalid inputTypeClas"); - return (((int)inputType) & (int)InputTypes.MaskClass) == (int) (inputTypeClass); - } - private static bool IsInputTypeVariation(InputTypes inputType, InputTypes inputTypeVariation) - { - if (!InputTypes.MaskVariation.HasFlag(inputTypeVariation)) - throw new Exception("invalid inputTypeVariation"); - bool result = (((int)inputType) & (int)InputTypes.MaskVariation) == (int)(inputTypeVariation); - if (result) - Kp2aLog.Log("found " + ((int)inputTypeVariation).ToString("X") + " in " + ((int)inputType).ToString("X")); - return result; - } - private static bool IsPassword(InputField f) - { - InputTypes inputType = f.InputType; - - return - (!f.IdEntry?.ToLowerInvariant().Contains("search") ?? true) && - (!f.Hint?.ToLowerInvariant().Contains("search") ?? true) && - ( - (IsInputTypeClass(inputType, InputTypes.ClassText) - && - ( - IsInputTypeVariation(inputType, InputTypes.TextVariationPassword) - || IsInputTypeVariation(inputType, InputTypes.TextVariationVisiblePassword) - || IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword) - ) - ) - || (f.HtmlInfoTypeAttribute == "password") - ); - } + return result; + } - AssistStructure Structure; - private List _editTextsWithoutHint = new List(); - + public AutofillTargetId ParseForSave() + { + var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(true); + return Parse(false, true, autofillView); + } + + public StructureParserBase.AutofillTargetId ParseForFill(bool isManual) + { + var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(isManual); + return Parse(true, isManual, autofillView); + } - } + } + + public class Kp2aLogger : ILogger + { + public void Log(string x) + { + Kp2aLog.Log(x); + } + } } diff --git a/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs b/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs index bd2f9b3c..9c4fbb21 100644 --- a/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs +++ b/src/keepass2android/services/Kp2aAutofill/ChooseForAutofillActivity.cs @@ -15,6 +15,7 @@ using keepass2android.services.AutofillBase.model; using Keepass2android.Pluginsdk; using KeePassLib; using KeePassLib.Utility; +using Kp2aAutofillParser; namespace keepass2android.services.Kp2aAutofill { @@ -41,7 +42,7 @@ namespace keepass2android.services.Kp2aAutofill protected override Result ExpectedActivityResult => KeePass.ExitCloseAfterTaskComplete; - protected override FilledAutofillFieldCollection GetDataset() + protected override FilledAutofillFieldCollection GetDataset() { if (App.Kp2a.CurrentDb==null || (App.Kp2a.QuickLocked)) return null; @@ -50,18 +51,18 @@ namespace keepass2android.services.Kp2aAutofill return GetFilledAutofillFieldCollectionFromEntry(entryOutput, this); } - public static FilledAutofillFieldCollection GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context) + public static FilledAutofillFieldCollection GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context) { if (pwEntryOutput == null) return null; - FilledAutofillFieldCollection fieldCollection = new FilledAutofillFieldCollection(); + FilledAutofillFieldCollection fieldCollection = new FilledAutofillFieldCollection(); var pwEntry = pwEntryOutput.Entry; foreach (string key in pwEntryOutput.OutputStrings.GetKeys()) { - FilledAutofillField field = - new FilledAutofillField + FilledAutofillField field = + new FilledAutofillField { AutofillHints = GetCanonicalHintsFromKp2aField(key).ToArray(), TextValue = pwEntryOutput.OutputStrings.ReadSafe(key) @@ -72,8 +73,8 @@ namespace keepass2android.services.Kp2aAutofill if (IsCreditCard(pwEntry, context) && pwEntry.Expires) { DateTime expTime = pwEntry.ExpiryTime; - FilledAutofillField field = - new FilledAutofillField + FilledAutofillField field = + new FilledAutofillField { AutofillHints = new[] {View.AutofillHintCreditCardExpirationDate}, DateValue = (long) (1000 * TimeUtil.SerializeUnix(expTime)) @@ -81,7 +82,7 @@ namespace keepass2android.services.Kp2aAutofill fieldCollection.Add(field); field = - new FilledAutofillField + new FilledAutofillField { AutofillHints = new[] {View.AutofillHintCreditCardExpirationDay}, TextValue = expTime.Day.ToString() @@ -89,7 +90,7 @@ namespace keepass2android.services.Kp2aAutofill fieldCollection.Add(field); field = - new FilledAutofillField + new FilledAutofillField { AutofillHints = new[] {View.AutofillHintCreditCardExpirationMonth}, TextValue = expTime.Month.ToString() @@ -97,7 +98,7 @@ namespace keepass2android.services.Kp2aAutofill fieldCollection.Add(field); field = - new FilledAutofillField + new FilledAutofillField { AutofillHints = new[] {View.AutofillHintCreditCardExpirationYear}, TextValue = expTime.Year.ToString() diff --git a/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs b/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs index 4098003a..e4fe1e54 100644 --- a/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs +++ b/src/keepass2android/services/Kp2aAutofill/Kp2aAutofillService.cs @@ -12,6 +12,7 @@ using Keepass2android.Pluginsdk; using KeePassLib; using KeePassLib.Collections; using KeePassLib.Utility; +using Kp2aAutofillParser; using Org.Json; using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase; @@ -33,10 +34,10 @@ namespace keepass2android.services { } - protected override List GetSuggestedEntries(string query) + protected override List> GetSuggestedEntries(string query) { if (!App.Kp2a.DatabaseIsUnlocked) - return new List(); + return new List>(); var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList()) .Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e))) .ToList(); diff --git a/src/keepass2android/services/Kp2aAutofillParser/AutofillParser.cs b/src/keepass2android/services/Kp2aAutofillParser/AutofillParser.cs new file mode 100644 index 00000000..2824d35b --- /dev/null +++ b/src/keepass2android/services/Kp2aAutofillParser/AutofillParser.cs @@ -0,0 +1,956 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Newtonsoft.Json; +using Formatting = System.Xml.Formatting; + +namespace Kp2aAutofillParser +{ + public class W3cHints + { + + // Supported W3C autofill tokens (https://html.spec.whatwg.org/multipage/forms.html#autofill) + public const string HONORIFIC_PREFIX = "honorific-prefix"; + public const string NAME = "name"; + public const string GIVEN_NAME = "given-name"; + public const string ADDITIONAL_NAME = "additional-name"; + public const string FAMILY_NAME = "family-name"; + public const string HONORIFIC_SUFFIX = "honorific-suffix"; + public const string USERNAME = "username"; + public const string NEW_PASSWORD = "new-password"; + public const string CURRENT_PASSWORD = "current-password"; + public const string ORGANIZATION_TITLE = "organization-title"; + public const string ORGANIZATION = "organization"; + public const string STREET_ADDRESS = "street-address"; + public const string ADDRESS_LINE1 = "address-line1"; + public const string ADDRESS_LINE2 = "address-line2"; + public const string ADDRESS_LINE3 = "address-line3"; + public const string ADDRESS_LEVEL4 = "address-level4"; + public const string ADDRESS_LEVEL3 = "address-level3"; + public const string ADDRESS_LEVEL2 = "address-level2"; + public const string ADDRESS_LEVEL1 = "address-level1"; + public const string COUNTRY = "country"; + public const string COUNTRY_NAME = "country-name"; + public const string POSTAL_CODE = "postal-code"; + public const string CC_NAME = "cc-name"; + public const string CC_GIVEN_NAME = "cc-given-name"; + public const string CC_ADDITIONAL_NAME = "cc-additional-name"; + public const string CC_FAMILY_NAME = "cc-family-name"; + public const string CC_NUMBER = "cc-number"; + public const string CC_EXPIRATION = "cc-exp"; + public const string CC_EXPIRATION_MONTH = "cc-exp-month"; + public const string CC_EXPIRATION_YEAR = "cc-exp-year"; + public const string CC_CSC = "cc-csc"; + public const string CC_TYPE = "cc-type"; + public const string TRANSACTION_CURRENCY = "transaction-currency"; + public const string TRANSACTION_AMOUNT = "transaction-amount"; + public const string LANGUAGE = "language"; + public const string BDAY = "bday"; + public const string BDAY_DAY = "bday-day"; + public const string BDAY_MONTH = "bday-month"; + public const string BDAY_YEAR = "bday-year"; + public const string SEX = "sex"; + public const string URL = "url"; + public const string PHOTO = "photo"; + // Optional W3C prefixes + public const string PREFIX_SECTION = "section-"; + public const string SHIPPING = "shipping"; + public const string BILLING = "billing"; + // W3C prefixes below... + public const string PREFIX_HOME = "home"; + public const string PREFIX_WORK = "work"; + public const string PREFIX_FAX = "fax"; + public const string PREFIX_PAGER = "pager"; + // ... require those suffix + public const string TEL = "tel"; + public const string TEL_COUNTRY_CODE = "tel-country-code"; + public const string TEL_NATIONAL = "tel-national"; + public const string TEL_AREA_CODE = "tel-area-code"; + public const string TEL_LOCAL = "tel-local"; + public const string TEL_LOCAL_PREFIX = "tel-local-prefix"; + public const string TEL_LOCAL_SUFFIX = "tel-local-suffix"; + public const string TEL_EXTENSION = "tel_extension"; + public const string EMAIL = "email"; + public const string IMPP = "impp"; + + private W3cHints() + { + } + + + + public static bool isW3cSectionPrefix(string hint) + { + return hint.ToLower().StartsWith(W3cHints.PREFIX_SECTION); + } + + public static bool isW3cAddressType(string hint) + { + switch (hint.ToLower()) + { + case W3cHints.SHIPPING: + case W3cHints.BILLING: + return true; + } + return false; + } + + public static bool isW3cTypePrefix(string hint) + { + switch (hint.ToLower()) + { + case W3cHints.PREFIX_WORK: + case W3cHints.PREFIX_FAX: + case W3cHints.PREFIX_HOME: + case W3cHints.PREFIX_PAGER: + return true; + } + return false; + } + + public static bool isW3cTypeHint(string hint) + { + switch (hint.ToLower()) + { + case W3cHints.TEL: + case W3cHints.TEL_COUNTRY_CODE: + case W3cHints.TEL_NATIONAL: + case W3cHints.TEL_AREA_CODE: + case W3cHints.TEL_LOCAL: + case W3cHints.TEL_LOCAL_PREFIX: + case W3cHints.TEL_LOCAL_SUFFIX: + case W3cHints.TEL_EXTENSION: + case W3cHints.EMAIL: + case W3cHints.IMPP: + return true; + } + return false; + } + } + /// + /// FilledAutofillFieldCollection is the model that holds all of the data on a client app's page, + /// plus the dataset name associated with it. + /// + public class FilledAutofillFieldCollection where FieldT:InputField + { + public Dictionary> HintMap { get; } + public string DatasetName { get; set; } + + public FilledAutofillFieldCollection(Dictionary> hintMap, string datasetName = "") + { + //recreate hint map making sure we compare case insensitive + HintMap = BuildHintMap(); + foreach (var p in hintMap) + HintMap.Add(p.Key, p.Value); + DatasetName = datasetName; + } + + public FilledAutofillFieldCollection() : this(BuildHintMap()) + { } + + private static Dictionary> BuildHintMap() + { + return new Dictionary>(StringComparer.OrdinalIgnoreCase); + } + + /// + /// Adds a filledAutofillField to the collection, indexed by all of its hints. + /// + /// The add. + /// Filled autofill field. + public void Add(FilledAutofillField filledAutofillField) + { + foreach (string hint in filledAutofillField.AutofillHints) + { + if (AutofillHintsHelper.IsSupportedHint(hint)) + { + HintMap.TryAdd(hint, filledAutofillField); + } + } + + } + + + + + /// + /// Takes in a list of autofill hints (`autofillHints`), usually associated with a View or set of + /// Views. Returns whether any of the filled fields on the page have at least 1 of these + /// `autofillHint`s. + /// + /// true, if with hints was helpsed, false otherwise. + /// Autofill hints. + public bool HelpsWithHints(List autofillHints) + { + for (int i = 0; i < autofillHints.Count; i++) + { + var autofillHint = autofillHints[i]; + if (HintMap.ContainsKey(autofillHint) && !HintMap[autofillHint].IsNull()) + { + return true; + } + } + return false; + } + } + public class AutofillHintsHelper + { + public const string AutofillHint2faAppOtp = "2faAppOTPCode"; + public const string AutofillHintBirthDateDay = "birthDateDay"; + public const string AutofillHintBirthDateFull = "birthDateFull"; + public const string AutofillHintBirthDateMonth = "birthDateMonth"; + public const string AutofillHintBirthDateYear = "birthDateYear"; + public const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; + public const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; + public const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; + public const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; + public const string AutofillHintCreditCardNumber = "creditCardNumber"; + public const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; + public const string AutofillHintEmailAddress = "emailAddress"; + public const string AutofillHintEmailOtp = "emailOTPCode"; + public const string AutofillHintGender = "gender"; + public const string AutofillHintName = "name"; + public const string AutofillHintNewPassword = "newPassword"; + public const string AutofillHintNewUsername = "newUsername"; + public const string AutofillHintNotApplicable = "notApplicable"; + public const string AutofillHintPassword = "password"; + public const string AutofillHintPersonName = "personName"; + public const string AutofillHintPersonNameFAMILY = "personFamilyName"; + public const string AutofillHintPersonNameGIVEN = "personGivenName"; + public const string AutofillHintPersonNameMIDDLE = "personMiddleName"; + public const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; + public const string AutofillHintPersonNamePREFIX = "personNamePrefix"; + public const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; + public const string AutofillHintPhone = "phone"; + public const string AutofillHintPhoneContryCode = "phoneCountryCode"; + public const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; + public const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; + public const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; + public const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; + public const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; + public const string AutofillHintPostalAddressLOCALITY = "addressLocality"; + public const string AutofillHintPostalAddressREGION = "addressRegion"; + public const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; + public const string AutofillHintPostalCode = "postalCode"; + public const string AutofillHintPromoCode = "promoCode"; + public const string AutofillHintSMS_OTP = "smsOTPCode"; + public const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; + public const string AutofillHintUsername = "username"; + public const string AutofillHintWifiPassword = "wifiPassword"; + public const string AutofillHintPhoneNational = "phoneNational"; + public const string AutofillHintPhoneNumber = "phoneNumber"; + public const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; + public const string AutofillHintPostalAddress = "postalAddress"; + + private static readonly HashSet _allSupportedHints = new HashSet(StringComparer.OrdinalIgnoreCase) + { + AutofillHintCreditCardExpirationDate, + AutofillHintCreditCardExpirationDay, + AutofillHintCreditCardExpirationMonth, + AutofillHintCreditCardExpirationYear, + AutofillHintCreditCardNumber, + AutofillHintCreditCardSecurityCode, + AutofillHintEmailAddress, + AutofillHintPhone, + AutofillHintName, + AutofillHintPassword, + AutofillHintPostalAddress, + AutofillHintPostalCode, + AutofillHintUsername, + W3cHints.HONORIFIC_PREFIX, + W3cHints.NAME, + W3cHints.GIVEN_NAME, + W3cHints.ADDITIONAL_NAME, + W3cHints.FAMILY_NAME, + W3cHints.HONORIFIC_SUFFIX, + W3cHints.USERNAME, + W3cHints.NEW_PASSWORD, + W3cHints.CURRENT_PASSWORD, + W3cHints.ORGANIZATION_TITLE, + W3cHints.ORGANIZATION, + W3cHints.STREET_ADDRESS, + W3cHints.ADDRESS_LINE1, + W3cHints.ADDRESS_LINE2, + W3cHints.ADDRESS_LINE3, + W3cHints.ADDRESS_LEVEL4, + W3cHints.ADDRESS_LEVEL3, + W3cHints.ADDRESS_LEVEL2, + W3cHints.ADDRESS_LEVEL1, + W3cHints.COUNTRY, + W3cHints.COUNTRY_NAME, + W3cHints.POSTAL_CODE, + W3cHints.CC_NAME, + W3cHints.CC_GIVEN_NAME, + W3cHints.CC_ADDITIONAL_NAME, + W3cHints.CC_FAMILY_NAME, + W3cHints.CC_NUMBER, + W3cHints.CC_EXPIRATION, + W3cHints.CC_EXPIRATION_MONTH, + W3cHints.CC_EXPIRATION_YEAR, + W3cHints.CC_CSC, + W3cHints.CC_TYPE, + W3cHints.TRANSACTION_CURRENCY, + W3cHints.TRANSACTION_AMOUNT, + W3cHints.LANGUAGE, + W3cHints.BDAY, + W3cHints.BDAY_DAY, + W3cHints.BDAY_MONTH, + W3cHints.BDAY_YEAR, + W3cHints.SEX, + W3cHints.URL, + W3cHints.PHOTO, + W3cHints.TEL, + W3cHints.TEL_COUNTRY_CODE, + W3cHints.TEL_NATIONAL, + W3cHints.TEL_AREA_CODE, + W3cHints.TEL_LOCAL, + W3cHints.TEL_LOCAL_PREFIX, + W3cHints.TEL_LOCAL_SUFFIX, + W3cHints.TEL_EXTENSION, + W3cHints.EMAIL, + W3cHints.IMPP, + }; + + private static readonly List> partitionsOfCanonicalHints = new List>() + { + + new HashSet(StringComparer.OrdinalIgnoreCase) + { + AutofillHintEmailAddress, + AutofillHintPhone, + AutofillHintName, + AutofillHintPassword, + AutofillHintUsername, + W3cHints.HONORIFIC_PREFIX, + W3cHints.NAME, + W3cHints.GIVEN_NAME, + W3cHints.ADDITIONAL_NAME, + W3cHints.FAMILY_NAME, + W3cHints.HONORIFIC_SUFFIX, + W3cHints.ORGANIZATION_TITLE, + W3cHints.ORGANIZATION, + W3cHints.LANGUAGE, + W3cHints.BDAY, + W3cHints.BDAY_DAY, + W3cHints.BDAY_MONTH, + W3cHints.BDAY_YEAR, + W3cHints.SEX, + W3cHints.URL, + W3cHints.PHOTO, + W3cHints.TEL, + W3cHints.TEL_COUNTRY_CODE, + W3cHints.TEL_NATIONAL, + W3cHints.TEL_AREA_CODE, + W3cHints.TEL_LOCAL, + W3cHints.TEL_LOCAL_PREFIX, + W3cHints.TEL_LOCAL_SUFFIX, + W3cHints.TEL_EXTENSION, + W3cHints.IMPP, + }, + new HashSet(StringComparer.OrdinalIgnoreCase) + { + AutofillHintPostalAddress, + AutofillHintPostalCode, + + W3cHints.STREET_ADDRESS, + W3cHints.ADDRESS_LINE1, + W3cHints.ADDRESS_LINE2, + W3cHints.ADDRESS_LINE3, + W3cHints.ADDRESS_LEVEL4, + W3cHints.ADDRESS_LEVEL3, + W3cHints.ADDRESS_LEVEL2, + W3cHints.ADDRESS_LEVEL1, + W3cHints.COUNTRY, + W3cHints.COUNTRY_NAME + }, + new HashSet(StringComparer.OrdinalIgnoreCase) + { + AutofillHintCreditCardExpirationDate, + AutofillHintCreditCardExpirationDay, + AutofillHintCreditCardExpirationMonth, + AutofillHintCreditCardExpirationYear, + AutofillHintCreditCardNumber, + AutofillHintCreditCardSecurityCode, + + W3cHints.CC_NAME, + W3cHints.CC_GIVEN_NAME, + W3cHints.CC_ADDITIONAL_NAME, + W3cHints.CC_FAMILY_NAME, + W3cHints.CC_TYPE, + W3cHints.TRANSACTION_CURRENCY, + W3cHints.TRANSACTION_AMOUNT, + }, + + }; + + private static readonly Dictionary hintToCanonicalReplacement = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + {W3cHints.EMAIL, AutofillHintEmailAddress}, + {W3cHints.USERNAME, AutofillHintUsername}, + {W3cHints.CURRENT_PASSWORD, AutofillHintPassword}, + {W3cHints.NEW_PASSWORD, AutofillHintPassword}, + {W3cHints.CC_EXPIRATION_MONTH, AutofillHintCreditCardExpirationMonth }, + {W3cHints.CC_EXPIRATION_YEAR, AutofillHintCreditCardExpirationYear }, + {W3cHints.CC_EXPIRATION, AutofillHintCreditCardExpirationDate }, + {W3cHints.CC_NUMBER, AutofillHintCreditCardNumber }, + {W3cHints.CC_CSC, AutofillHintCreditCardSecurityCode }, + {W3cHints.POSTAL_CODE, AutofillHintPostalCode }, + + + }; + + public static bool IsSupportedHint(string hint) + { + return _allSupportedHints.Contains(hint); + } + + + public static string[] FilterForSupportedHints(string[] hints) + { + if (hints == null) + return Array.Empty(); + var filteredHints = new string[hints.Length]; + int i = 0; + foreach (var hint in hints) + { + if (IsSupportedHint(hint)) + { + filteredHints[i++] = hint; + } + + } + var finalFilteredHints = new string[i]; + Array.Copy(filteredHints, 0, finalFilteredHints, 0, i); + return finalFilteredHints; + } + + + + /// + /// transforms hints by replacing some W3cHints by their Android counterparts and transforming everything to lowercase + /// + public static List ConvertToCanonicalHints(string[] supportedHints) + { + List result = new List(); + foreach (string hint in supportedHints) + { + string canonicalHint; + if (!hintToCanonicalReplacement.TryGetValue(hint, out canonicalHint)) + canonicalHint = hint; + result.Add(canonicalHint.ToLower()); + } + return result; + + } + + public static int GetPartitionIndex(string hint) + { + for (int i = 0; i < partitionsOfCanonicalHints.Count; i++) + { + if (partitionsOfCanonicalHints[i].Contains(hint)) + { + return i; + } + } + return -1; + } + + public static FilledAutofillFieldCollection FilterForPartition(FilledAutofillFieldCollection autofillFields, int partitionIndex) where FieldT: InputField + { + FilledAutofillFieldCollection filteredCollection = + new FilledAutofillFieldCollection { DatasetName = autofillFields.DatasetName }; + + if (partitionIndex == -1) + return filteredCollection; + + foreach (var field in autofillFields.HintMap.Values.Distinct()) + { + foreach (var hint in field.AutofillHints) + { + if (GetPartitionIndex(hint) == partitionIndex) + { + filteredCollection.Add(field); + break; + } + } + } + + return filteredCollection; + } + + public static FilledAutofillFieldCollection FilterForPartition(FilledAutofillFieldCollection filledAutofillFieldCollection, List autofillFieldsFocusedAutofillCanonicalHints) where FieldT: InputField + { + + //only apply partition data if we have FocusedAutofillCanonicalHints. This may be empty on buggy Firefox. + if (autofillFieldsFocusedAutofillCanonicalHints.Any()) + { + int partitionIndex = AutofillHintsHelper.GetPartitionIndex(autofillFieldsFocusedAutofillCanonicalHints.FirstOrDefault()); + return AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, partitionIndex); + } + + return filledAutofillFieldCollection; + } + } + /// + /// This enum represents the Android.Text.InputTypes values. For testability, this is duplicated here. + /// + public enum InputTypes + { + ClassDatetime = 4, + ClassNumber = 2, + ClassPhone = 3, + ClassText = 1, + DatetimeVariationDate = 16, + DatetimeVariationNormal = 0, + DatetimeVariationTime = 32, + MaskClass = 15, + MaskFlags = 16773120, + MaskVariation = 4080, + Null = 0, + NumberFlagDecimal = 8192, + NumberFlagSigned = 4096, + NumberVariationNormal = 0, + NumberVariationPassword = 16, + TextFlagAutoComplete = 65536, + TextFlagAutoCorrect = 32768, + TextFlagCapCharacters = 4096, + TextFlagCapSentences = 16384, + TextFlagCapWords = 8192, + TextFlagEnableTextConversionSuggestions = 1048576, + TextFlagImeMultiLine = 262144, + TextFlagMultiLine = 131072, + TextFlagNoSuggestions = 524288, + TextVariationEmailAddress = 32, + TextVariationEmailSubject = 48, + TextVariationFilter = 176, + TextVariationLongMessage = 80, + TextVariationNormal = 0, + TextVariationPassword = 128, + TextVariationPersonName = 96, + TextVariationPhonetic = 192, + TextVariationPostalAddress = 112, + TextVariationShortMessage = 64, + TextVariationUri = 16, + TextVariationVisiblePassword = 144, + TextVariationWebEditText = 160, + TextVariationWebEmailAddress = 208, + TextVariationWebPassword = 224 + } + + public interface IKp2aDigitalAssetLinksDataSource + { + bool IsTrustedApp(string packageName); + bool IsTrustedLink(string domain, string targetPackage); + bool IsEnabled(); + + } + + class TimeUtil + { + private static DateTime? m_dtUnixRoot = null; + public static DateTime ConvertUnixTime(double dtUnix) + { + try + { + if (!m_dtUnixRoot.HasValue) + m_dtUnixRoot = (new DateTime(1970, 1, 1, 0, 0, 0, 0, + DateTimeKind.Utc)).ToLocalTime(); + + return m_dtUnixRoot.Value.AddSeconds(dtUnix); + } + catch (Exception) { Debug.Assert(false); } + + return DateTime.UtcNow; + } + } + + public class FilledAutofillField where FieldT : InputField + { + private string[] _autofillHints; + public string TextValue { get; set; } + public long? DateValue { get; set; } + public bool? ToggleValue { get; set; } + + public string ValueToString() + { + if (DateValue != null) + { + return TimeUtil.ConvertUnixTime((long)DateValue / 1000.0).ToLongDateString(); + } + if (ToggleValue != null) + return ToggleValue.ToString(); + return TextValue; + } + + /// + /// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison. + /// + public string[] AutofillHints + { + get + { + return _autofillHints; + } + set + { + _autofillHints = value; + for (int i = 0; i < _autofillHints.Length; i++) + _autofillHints[i] = _autofillHints[i].ToLower(); + } + } + + + public FilledAutofillField() + { } + + public FilledAutofillField(FieldT inputField) + : this(inputField, inputField.AutofillHints) + { + + } + + public FilledAutofillField(FieldT inputField, string[] hints) + { + + string[] rawHints = AutofillHintsHelper.FilterForSupportedHints(hints); + List hintList = new List(); + + string nextHint = null; + for (int i = 0; i < rawHints.Length; i++) + { + string hint = rawHints[i]; + if (i < rawHints.Length - 1) + { + nextHint = rawHints[i + 1]; + } + // First convert the compound W3C autofill hints + if (W3cHints.isW3cSectionPrefix(hint) && i < rawHints.Length - 1) + { + hint = rawHints[++i]; + + if (i < rawHints.Length - 1) + { + nextHint = rawHints[i + 1]; + } + } + if (W3cHints.isW3cTypePrefix(hint) && nextHint != null && W3cHints.isW3cTypeHint(nextHint)) + { + hint = nextHint; + i++; + + } + if (W3cHints.isW3cAddressType(hint) && nextHint != null) + { + hint = nextHint; + i++; + + } + + // Then check if the "actual" hint is supported. + if (AutofillHintsHelper.IsSupportedHint(hint)) + { + hintList.Add(hint); + } + else + { + + } + } + AutofillHints = AutofillHintsHelper.ConvertToCanonicalHints(hintList.ToArray()).ToArray(); + + + } + + public bool IsNull() + { + return TextValue == null && DateValue == null && ToggleValue == null; + } + + public override bool Equals(object obj) + { + if (this == obj) return true; + if (obj == null || GetType() != obj.GetType()) return false; + + FilledAutofillField that = (FilledAutofillField)obj; + + if (!TextValue?.Equals(that.TextValue) ?? that.TextValue != null) + return false; + if (DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null) + return false; + return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null; + } + + public override int GetHashCode() + { + unchecked + { + var result = TextValue != null ? TextValue.GetHashCode() : 0; + result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0); + result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0); + return result; + } + } + } + + /// + /// Base class for everything that is a input field which might (or might not) be autofilled. + /// For testability, this is independent from Android classes like ViewNode + /// + public abstract class InputField + { + public string IdEntry { get; set; } + public string Hint { get; set; } + public string ClassName { get; set; } + public string[] AutofillHints { get; set; } + public bool IsFocused { get; set; } + + public InputTypes InputType { get; set; } + + public string HtmlInfoTag { get; set; } + public string HtmlInfoTypeAttribute { get; set; } + + } + + public class AutofillView where TField : InputField + { + public List InputFields { get; set; } = new List(); + + public string PackageId { get; set; } = null; + public string WebDomain { get; set; } = null; + } + + public interface ILogger + { + void Log(string x); + } + + public class StructureParserBase where FieldT: InputField + { + private readonly ILogger _log; + private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource; + + private readonly List _autofillHintsForLogin = new List + { + AutofillHintsHelper.AutofillHintPassword, + AutofillHintsHelper.AutofillHintUsername, + AutofillHintsHelper.AutofillHintEmailAddress + }; + + public string PackageId { get; set; } + + public Dictionary FieldsMappedToHints = new Dictionary(); + + public StructureParserBase(ILogger logger, IKp2aDigitalAssetLinksDataSource digitalAssetLinksDataSource) + { + _log = logger; + _digitalAssetLinksDataSource = digitalAssetLinksDataSource; + } + + public class AutofillTargetId + { + public string PackageName { get; set; } + + public string PackageNameWithPseudoSchema + { + get { return AndroidAppScheme + PackageName; } + } + + public const string AndroidAppScheme = "androidapp://"; + + public string WebDomain { get; set; } + + /// + /// If PackageName and WebDomain are not compatible (by DAL or because PackageName is a trusted browser in which case we treat all domains as "compatible" + /// we need to issue a warning. If we would fill credentials for the package, a malicious website could try to get credentials for the app. + /// If we would fill credentials for the domain, a malicious app could get credentials for the domain. + /// + public bool IncompatiblePackageAndDomain { get; set; } + + public string DomainOrPackage + { + get + { + return WebDomain ?? PackageNameWithPseudoSchema; + } + } + } + + public AutofillTargetId ParseForFill(bool isManual, AutofillView autofillView) + { + return Parse(true, isManual, autofillView); + } + + public AutofillTargetId ParseForSave(AutofillView autofillView) + { + return Parse(false, true, autofillView); + } + + /// + /// Traverse AssistStructure and add ViewNode metadata to a flat list. + /// + /// The parse. + /// If set to true for fill. + /// + protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView autofillView) + { + AutofillTargetId result = new AutofillTargetId() + { + PackageName = autofillView.PackageId, + WebDomain = autofillView.WebDomain + }; + + _log.Log("parsing autofillStructure..."); + + //TODO remove from production + _log.Log("will log the autofillStructure..."); + string debugInfo = JsonConvert.SerializeObject(autofillView, Newtonsoft.Json.Formatting.Indented); + _log.Log("will log the autofillStructure... size is " + debugInfo.Length); + _log.Log("This is the autofillStructure: \n\n " + debugInfo); + + //go through each input field and determine username/password fields. + //Depending on the target this can require more or less heuristics. + // * if there is a valid & supported autofill hint, we assume that all fields which should be filled do have an appropriate Autofill hint + // * if there is no such autofill hint, we use IsPassword to + + HashSet autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null) + .SelectMany(f => f.AutofillHints).ToHashSet(); + bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any(); + + if (hasLoginAutofillHints) + { + foreach (var viewNode in autofillView.InputFields) + { + string[] viewHints = viewNode.AutofillHints; + if (viewHints == null) + continue; + if (viewHints.Intersect(_autofillHintsForLogin).Any()) + { + FieldsMappedToHints.Add(viewNode, viewHints); + } + + } + } + else + { + //determine password fields, first by type, then by hint: + List passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && IsPassword(f)).ToList(); + if (!passwordFields.Any()) + { + passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && HasPasswordHint(f)).ToList(); + } + + //determine username fields. Try by hint, if that fails use the one before the password + List usernameFields = autofillView.InputFields.Where(f => IsEditText(f) && HasUsernameHint(f)).ToList(); + if (!usernameFields.Any()) + { + foreach (var passwordField in passwordFields) + { + var lastInputBeforePassword = autofillView.InputFields + .TakeWhile(f => IsEditText(f) && f != passwordField && !passwordFields.Contains(f)).LastOrDefault(); + if (lastInputBeforePassword != null) + usernameFields.Add(lastInputBeforePassword); + } + + } + + //for "heuristic determination" we demand that one of the filled fields is focused: + if (passwordFields.Concat(usernameFields).Any(f => f.IsFocused)) + { + foreach (var uf in usernameFields) + FieldsMappedToHints.Add(uf, new string[] { AutofillHintsHelper.AutofillHintUsername }); + foreach (var pf in passwordFields) + FieldsMappedToHints.Add(pf, new string[] { AutofillHintsHelper.AutofillHintPassword }); + } + } + + + if (!string.IsNullOrEmpty(autofillView.WebDomain) && _digitalAssetLinksDataSource.IsEnabled()) + { + result.IncompatiblePackageAndDomain = !_digitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName); + if (result.IncompatiblePackageAndDomain) + { + _log.Log($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); + } + } + else + { + result.IncompatiblePackageAndDomain = false; + } + return result; + } + + private bool IsEditText(FieldT f) + { + return (f.ClassName == "android.widget.EditText" + || f.ClassName == "android.widget.AutoCompleteTextView" + || f.HtmlInfoTag == "input"); + } + + private static readonly HashSet _passwordHints = new HashSet { "password", "passwort" + /*, "passwordAuto", "pswd"*/ }; + private static bool HasPasswordHint(InputField f) + { + return IsAny(f.IdEntry, _passwordHints) || + IsAny(f.Hint, _passwordHints); + } + + private static readonly HashSet _usernameHints = new HashSet { "email", "e-mail", "username" }; + + private static bool HasUsernameHint(InputField f) + { + return IsAny(f.IdEntry, _usernameHints) || + IsAny(f.Hint, _usernameHints); + } + + private static bool IsAny(string value, IEnumerable terms) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + var lowerValue = value.ToLowerInvariant(); + return terms.Any(t => lowerValue == t); + } + + private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass) + { + if (!InputTypes.MaskClass.HasFlag(inputTypeClass)) + throw new Exception("invalid inputTypeClass"); + return (((int)inputType) & (int)InputTypes.MaskClass) == (int)(inputTypeClass); + } + private static bool IsInputTypeVariation(InputTypes inputType, InputTypes inputTypeVariation) + { + if (!InputTypes.MaskVariation.HasFlag(inputTypeVariation)) + throw new Exception("invalid inputTypeVariation"); + return (((int)inputType) & (int)InputTypes.MaskVariation) == (int)(inputTypeVariation); + } + + private static bool IsPassword(InputField f) + { + InputTypes inputType = f.InputType; + + return + (!f.IdEntry?.ToLowerInvariant().Contains("search") ?? true) && + (!f.Hint?.ToLowerInvariant().Contains("search") ?? true) && + ( + (IsInputTypeClass(inputType, InputTypes.ClassText) + && + ( + IsInputTypeVariation(inputType, InputTypes.TextVariationPassword) + || IsInputTypeVariation(inputType, InputTypes.TextVariationVisiblePassword) + || IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword) + ) + ) + || (f.AutofillHints != null && f.AutofillHints.First() == "passwordAuto") + || (f.HtmlInfoTypeAttribute == "password") + ); + } + + + + + + + } +} diff --git a/src/keepass2android/services/Kp2aAutofillParser/Kp2aAutofillParser.csproj b/src/keepass2android/services/Kp2aAutofillParser/Kp2aAutofillParser.csproj new file mode 100644 index 00000000..375762cd --- /dev/null +++ b/src/keepass2android/services/Kp2aAutofillParser/Kp2aAutofillParser.csproj @@ -0,0 +1,12 @@ + + + + netstandard2.1 + enable + + + + + + +