refactoring of autofill implementation, extracted some pieces to be independant of Android framework, added some xUnit tests

This commit is contained in:
Philipp Crocoll
2023-02-28 22:31:28 +01:00
parent e350e8788c
commit 914224e4fa
17 changed files with 2096 additions and 481 deletions

View File

@@ -25,7 +25,9 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCloudBindings", "PCloudBin
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keepass2android\keepass2android-app.csproj", "{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keepass2android\keepass2android-app.csproj", "{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}"
EndProject 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 EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution 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|Win32.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|x64.ActiveCfg = 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 {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 EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

View File

@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Xml; using Newtonsoft.Json;
using Formatting = System.Xml.Formatting;
namespace Kp2aAutofillParser namespace Kp2aAutofillParser
{ {
@@ -192,54 +194,54 @@ namespace Kp2aAutofillParser
return false; return false;
} }
} }
class AutofillHintsHelper public class AutofillHintsHelper
{ {
const string AutofillHint2faAppOtp = "2faAppOTPCode"; public const string AutofillHint2faAppOtp = "2faAppOTPCode";
const string AutofillHintBirthDateDay = "birthDateDay"; public const string AutofillHintBirthDateDay = "birthDateDay";
const string AutofillHintBirthDateFull = "birthDateFull"; public const string AutofillHintBirthDateFull = "birthDateFull";
const string AutofillHintBirthDateMonth = "birthDateMonth"; public const string AutofillHintBirthDateMonth = "birthDateMonth";
const string AutofillHintBirthDateYear = "birthDateYear"; public const string AutofillHintBirthDateYear = "birthDateYear";
const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate"; public const string AutofillHintCreditCardExpirationDate = "creditCardExpirationDate";
const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay"; public const string AutofillHintCreditCardExpirationDay = "creditCardExpirationDay";
const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth"; public const string AutofillHintCreditCardExpirationMonth = "creditCardExpirationMonth";
const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear"; public const string AutofillHintCreditCardExpirationYear = "creditCardExpirationYear";
const string AutofillHintCreditCardNumber = "creditCardNumber"; public const string AutofillHintCreditCardNumber = "creditCardNumber";
const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode"; public const string AutofillHintCreditCardSecurityCode = "creditCardSecurityCode";
const string AutofillHintEmailAddress = "emailAddress"; public const string AutofillHintEmailAddress = "emailAddress";
const string AutofillHintEmailOtp = "emailOTPCode"; public const string AutofillHintEmailOtp = "emailOTPCode";
const string AutofillHintGender = "gender"; public const string AutofillHintGender = "gender";
const string AutofillHintName = "name"; public const string AutofillHintName = "name";
const string AutofillHintNewPassword = "newPassword"; public const string AutofillHintNewPassword = "newPassword";
const string AutofillHintNewUsername = "newUsername"; public const string AutofillHintNewUsername = "newUsername";
const string AutofillHintNotApplicable = "notApplicable"; public const string AutofillHintNotApplicable = "notApplicable";
const string AutofillHintPassword = "password"; public const string AutofillHintPassword = "password";
const string AutofillHintPersonName = "personName"; public const string AutofillHintPersonName = "personName";
const string AutofillHintPersonNameFAMILY = "personFamilyName"; public const string AutofillHintPersonNameFAMILY = "personFamilyName";
const string AutofillHintPersonNameGIVEN = "personGivenName"; public const string AutofillHintPersonNameGIVEN = "personGivenName";
const string AutofillHintPersonNameMIDDLE = "personMiddleName"; public const string AutofillHintPersonNameMIDDLE = "personMiddleName";
const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial"; public const string AutofillHintPersonNameMIDDLE_INITIAL = "personMiddleInitial";
const string AutofillHintPersonNamePREFIX = "personNamePrefix"; public const string AutofillHintPersonNamePREFIX = "personNamePrefix";
const string AutofillHintPersonNameSUFFIX = "personNameSuffix"; public const string AutofillHintPersonNameSUFFIX = "personNameSuffix";
const string AutofillHintPhone = "phone"; public const string AutofillHintPhone = "phone";
const string AutofillHintPhoneContryCode = "phoneCountryCode"; public const string AutofillHintPhoneContryCode = "phoneCountryCode";
const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber"; public const string AutofillHintPostalAddressAPT_NUMBER = "aptNumber";
const string AutofillHintPostalAddressCOUNTRY = "addressCountry"; public const string AutofillHintPostalAddressCOUNTRY = "addressCountry";
const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality"; public const string AutofillHintPostalAddressDEPENDENT_LOCALITY = "dependentLocality";
const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress"; public const string AutofillHintPostalAddressEXTENDED_ADDRESS = "extendedAddress";
const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode"; public const string AutofillHintPostalAddressEXTENDED_POSTAL_CODE = "extendedPostalCode";
const string AutofillHintPostalAddressLOCALITY = "addressLocality"; public const string AutofillHintPostalAddressLOCALITY = "addressLocality";
const string AutofillHintPostalAddressREGION = "addressRegion"; public const string AutofillHintPostalAddressREGION = "addressRegion";
const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress"; public const string AutofillHintPostalAddressSTREET_ADDRESS = "streetAddress";
const string AutofillHintPostalCode = "postalCode"; public const string AutofillHintPostalCode = "postalCode";
const string AutofillHintPromoCode = "promoCode"; public const string AutofillHintPromoCode = "promoCode";
const string AutofillHintSMS_OTP = "smsOTPCode"; public const string AutofillHintSMS_OTP = "smsOTPCode";
const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress"; public const string AutofillHintUPI_VPA = "upiVirtualPaymentAddress";
const string AutofillHintUsername = "username"; public const string AutofillHintUsername = "username";
const string AutofillHintWifiPassword = "wifiPassword"; public const string AutofillHintWifiPassword = "wifiPassword";
const string AutofillHintPhoneNational = "phoneNational"; public const string AutofillHintPhoneNational = "phoneNational";
const string AutofillHintPhoneNumber = "phoneNumber"; public const string AutofillHintPhoneNumber = "phoneNumber";
const string AutofillHintPhoneNumberDevice = "phoneNumberDevice"; public const string AutofillHintPhoneNumberDevice = "phoneNumberDevice";
const string AutofillHintPostalAddress = "postalAddress"; public const string AutofillHintPostalAddress = "postalAddress";
private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase) private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{ {
@@ -540,8 +542,29 @@ namespace Kp2aAutofillParser
{ {
bool IsTrustedApp(string packageName); bool IsTrustedApp(string packageName);
bool IsTrustedLink(string domain, string targetPackage); 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<FieldT> where FieldT : InputField public class FilledAutofillField<FieldT> where FieldT : InputField
{ {
private string[] _autofillHints; private string[] _autofillHints;
@@ -671,7 +694,7 @@ namespace Kp2aAutofillParser
} }
/// <summary> /// <summary>
/// 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 /// For testability, this is independent from Android classes like ViewNode
/// </summary> /// </summary>
public abstract class InputField public abstract class InputField
@@ -687,11 +710,12 @@ namespace Kp2aAutofillParser
public string HtmlInfoTag { get; set; } public string HtmlInfoTag { get; set; }
public string HtmlInfoTypeAttribute { get; set; } public string HtmlInfoTypeAttribute { get; set; }
public abstract void FillFilledAutofillValue(FilledAutofillField<FieldT> filledField);
} }
/// <summary>
/// Serializable structure defining the contents of the current view (from an autofill perspective)
/// </summary>
/// <typeparam name="TField"></typeparam>
public class AutofillView<TField> where TField : InputField public class AutofillView<TField> where TField : InputField
{ {
public List<TField> InputFields { get; set; } = new List<TField>(); public List<TField> InputFields { get; set; } = new List<TField>();
@@ -710,8 +734,17 @@ namespace Kp2aAutofillParser
private readonly ILogger _log; private readonly ILogger _log;
private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource; private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource;
private readonly List<string> _autofillHintsForLogin = new List<string>
{
AutofillHintsHelper.AutofillHintPassword,
AutofillHintsHelper.AutofillHintUsername,
AutofillHintsHelper.AutofillHintEmailAddress
};
public string PackageId { get; set; } public string PackageId { get; set; }
public Dictionary<FieldT, string[]> FieldsMappedToHints = new Dictionary<FieldT, string[]>();
public StructureParserBase(ILogger logger, IKp2aDigitalAssetLinksDataSource digitalAssetLinksDataSource) public StructureParserBase(ILogger logger, IKp2aDigitalAssetLinksDataSource digitalAssetLinksDataSource)
{ {
_log = logger; _log = logger;
@@ -763,151 +796,86 @@ namespace Kp2aAutofillParser
/// <returns>The parse.</returns> /// <returns>The parse.</returns>
/// <param name="forFill">If set to <c>true</c> for fill.</param> /// <param name="forFill">If set to <c>true</c> for fill.</param>
/// <param name="isManualRequest"></param> /// <param name="isManualRequest"></param>
AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> autofillView) protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> autofillView)
{ {
AutofillTargetId result = new AutofillTargetId(); AutofillTargetId result = new AutofillTargetId()
{
PackageName = autofillView.PackageId,
_editTextsWithoutHint.Clear(); WebDomain = autofillView.WebDomain
};
_log.Log("parsing autofillStructure..."); _log.Log("parsing autofillStructure...");
//TODO remove from production if (LogAutofillView)
_log.Log("will log the autofillStructure..."); {
string debugInfo = JsonConvert.SerializeObject(autofillView, Formatting.Indented); 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); _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<string> 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) foreach (var viewNode in autofillView.InputFields)
{ {
string[] viewHints = viewNode.AutofillHints; string[] viewHints = viewNode.AutofillHints;
if (viewHints != null && viewHints.Length == 1 && viewHints.First() == "off" && viewNode.IsFocused && if (viewHints == null)
isManualRequest) continue;
viewHints[0] = "on"; if (viewHints.Intersect(_autofillHintsForLogin).Any())
/*if (viewHints != null && viewHints.Any())
{ {
CommonUtil.logd("viewHints=" + viewHints); FieldsMappedToHints.Add(viewNode, 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 else
{ {
FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode.ViewNode); //determine password fields, first by type, then by hint:
ClientFormData.Add(filledAutofillField); List<FieldT> passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && IsPassword(f)).ToList();
}
}
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);
}
}
}
List<ViewNodeInputField> passwordFields = new List<ViewNodeInputField>();
List<ViewNodeInputField> usernameFields = new List<ViewNodeInputField>();
if (AutofillFields.Empty)
{
passwordFields = _editTextsWithoutHint.Where(IsPassword).ToList();
if (!passwordFields.Any()) if (!passwordFields.Any())
{ {
passwordFields = _editTextsWithoutHint.Where(HasPasswordHint).ToList(); passwordFields = autofillView.InputFields.Where(f => IsEditText(f) && HasPasswordHint(f)).ToList();
} }
usernameFields = _editTextsWithoutHint.Where(HasUsernameHint).ToList(); //determine username fields. Try by hint, if that fails use the one before the password
List<FieldT> usernameFields = autofillView.InputFields.Where(f => IsEditText(f) && HasUsernameHint(f)).ToList();
if (usernameFields.Any() == false) if (!usernameFields.Any())
{ {
foreach (var passwordField in passwordFields) foreach (var passwordField in passwordFields)
{ {
var usernameField = _editTextsWithoutHint var lastInputBeforePassword = autofillView.InputFields
.TakeWhile(f => f != passwordField).LastOrDefault(); .TakeWhile(f => IsEditText(f) && f != passwordField && !passwordFields.Contains(f)).LastOrDefault();
if (usernameField != null) if (lastInputBeforePassword != null)
{ usernameFields.Add(lastInputBeforePassword);
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) //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) foreach (var uf in usernameFields)
AutofillFields.Add(new AutofillFieldMetadata(uf.ViewNode, new[] { View.AutofillHintUsername })); FieldsMappedToHints.Add(uf, new string[] { AutofillHintsHelper.AutofillHintUsername });
foreach (var pf in passwordFields) foreach (var pf in passwordFields)
AutofillFields.Add(new AutofillFieldMetadata(pf.ViewNode, new[] { View.AutofillHintPassword })); FieldsMappedToHints.Add(pf, new string[] { AutofillHintsHelper.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 }));
} }
result.WebDomain = autofillView.WebDomain; if (!string.IsNullOrEmpty(autofillView.WebDomain) && _digitalAssetLinksDataSource.IsEnabled())
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); result.IncompatiblePackageAndDomain = !_digitalAssetLinksDataSource.IsTrustedLink(autofillView.WebDomain, result.PackageName);
if (result.IncompatiblePackageAndDomain) if (result.IncompatiblePackageAndDomain)
{ {
CommonUtil.loge($"DAL verification failed for {result.PackageName}/{result.WebDomain}"); _log.Log($"DAL verification failed for {result.PackageName}/{result.WebDomain}");
} }
} }
else else
@@ -916,29 +884,40 @@ namespace Kp2aAutofillParser
} }
return result; return result;
} }
private static readonly HashSet<string> _passwordHints = new HashSet<string> { "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<string> _passwordHints = new HashSet<string> { "password", "passwort"
/*, "passwordAuto", "pswd"*/ };
private static bool HasPasswordHint(InputField f) private static bool HasPasswordHint(InputField f)
{ {
return ContainsAny(f.IdEntry, _passwordHints) || return IsAny(f.IdEntry, _passwordHints) ||
ContainsAny(f.Hint, _passwordHints); IsAny(f.Hint, _passwordHints);
} }
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email", "e-mail", "username" }; private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email", "e-mail", "username" };
private static bool HasUsernameHint(InputField f) private static bool HasUsernameHint(InputField f)
{ {
return ContainsAny(f.IdEntry, _usernameHints) || return IsAny(f.IdEntry, _usernameHints) ||
ContainsAny(f.Hint, _usernameHints); IsAny(f.Hint, _usernameHints);
} }
private static bool ContainsAny(string value, IEnumerable<string> terms) private static bool IsAny(string value, IEnumerable<string> terms)
{ {
if (string.IsNullOrWhiteSpace(value)) if (string.IsNullOrWhiteSpace(value))
{ {
return false; return false;
} }
var lowerValue = value.ToLowerInvariant(); 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) private static bool IsInputTypeClass(InputTypes inputType, InputTypes inputTypeClass)
@@ -970,12 +949,13 @@ namespace Kp2aAutofillParser
|| IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword) || IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword)
) )
) )
|| (f.AutofillHints != null && f.AutofillHints.First() == "passwordAuto")
|| (f.HtmlInfoTypeAttribute == "password") || (f.HtmlInfoTypeAttribute == "password")
); );
} }
AssistStructure Structure;
private List<ViewNodeInputField> _editTextsWithoutHint = new List<ViewNodeInputField>();

View File

@@ -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<TestInputField>? autofillView =
JsonConvert.DeserializeObject<AutofillView<TestInputField>>(input);
StructureParserBase<TestInputField> parser =
new StructureParserBase<TestInputField>(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);
}
}
}

View File

@@ -0,0 +1,46 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<None Remove="com-servicenet-mobile-focused.json" />
<None Remove="com-servicenet-mobile-no-focus.json" />
<None Remove="firefox-amazon-it.json" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kp2aAutofillParser\Kp2aAutofillParser.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="firefox-amazon-it.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="com-servicenet-mobile-focused.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="com-servicenet-mobile-no-focus.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
global using Xunit;

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -5,6 +5,7 @@ using Android.App.Assist;
using Android.Service.Autofill; using Android.Service.Autofill;
using Android.Views; using Android.Views;
using Android.Views.Autofill; using Android.Views.Autofill;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase namespace keepass2android.services.AutofillBase
{ {
@@ -40,7 +41,6 @@ namespace keepass2android.services.AutofillBase
var supportedHints = AutofillHintsHelper.FilterForSupportedHints(autofillHints); var supportedHints = AutofillHintsHelper.FilterForSupportedHints(autofillHints);
var canonicalHints = AutofillHintsHelper.ConvertToCanonicalHints(supportedHints); var canonicalHints = AutofillHintsHelper.ConvertToCanonicalHints(supportedHints);
SetHints(canonicalHints.ToArray()); SetHints(canonicalHints.ToArray());
} }
void SetHints(string[] value) void SetHints(string[] value)

View File

@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1; using AndroidX.AutoFill.Inline.V1;
using Java.Util.Concurrent.Atomic; using Java.Util.Concurrent.Atomic;
using keepass2android.services.AutofillBase.model; using keepass2android.services.AutofillBase.model;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase namespace keepass2android.services.AutofillBase
{ {
@@ -255,7 +256,7 @@ namespace keepass2android.services.AutofillBase
if (warning == DisplayWarning.None) if (warning == DisplayWarning.None)
{ {
FilledAutofillFieldCollection partitionData = FilledAutofillFieldCollection<ViewNodeInputField> partitionData =
AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints); AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints);
Kp2aLog.Log("AF: Add dataset"); Kp2aLog.Log("AF: Add dataset");
@@ -299,7 +300,7 @@ namespace keepass2android.services.AutofillBase
} }
protected abstract List<FilledAutofillFieldCollection> GetSuggestedEntries(string query); protected abstract List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query);
public enum DisplayWarning public enum DisplayWarning
{ {

View File

@@ -12,6 +12,7 @@ using Java.Util;
using keepass2android.services.AutofillBase.model; using keepass2android.services.AutofillBase.model;
using System.Linq; using System.Linq;
using Android.Content.PM; using Android.Content.PM;
using Kp2aAutofillParser;
#if !NoNet #if !NoNet
using Com.Dropbox.Core.V2.Teamlog; using Com.Dropbox.Core.V2.Teamlog;
#endif #endif
@@ -173,7 +174,7 @@ namespace keepass2android.services.AutofillBase
ReplyIntent = null; ReplyIntent = null;
} }
protected void OnSuccess(FilledAutofillFieldCollection clientFormDataMap, bool isManual) protected void OnSuccess(FilledAutofillFieldCollection<ViewNodeInputField> clientFormDataMap, bool isManual)
{ {
var intent = Intent; var intent = Intent;
AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure); AssistStructure structure = (AssistStructure)intent.GetParcelableExtra(AutofillManager.ExtraAssistStructure);
@@ -229,7 +230,7 @@ namespace keepass2android.services.AutofillBase
/// <summary> /// <summary>
/// Creates the FilledAutofillFieldCollection from the intent returned from the query activity /// Creates the FilledAutofillFieldCollection from the intent returned from the query activity
/// </summary> /// </summary>
protected abstract FilledAutofillFieldCollection GetDataset(); protected abstract FilledAutofillFieldCollection<ViewNodeInputField> GetDataset();
public abstract IAutofillIntentBuilder IntentBuilder { get; } public abstract IAutofillIntentBuilder IntentBuilder { get; }

View File

@@ -39,6 +39,11 @@ namespace keepass2android.services.AutofillBase
return trustedLinks.Contains(BuildLink(domain, targetPackage)); 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) public void RememberAsTrustedApp(string packageName)
{ {
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx); var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);

View File

@@ -1,22 +1,12 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Android.App.Assist; using Android.App.Assist;
using Android.Content; using Android.Content;
using Android.Preferences;
using Android.Text;
using Android.Util;
using Android.Views;
using Android.Views.Autofill; using Android.Views.Autofill;
using Android.Views.InputMethods;
using DomainNameParser; using DomainNameParser;
using keepass2android.services.AutofillBase.model;
using Kp2aAutofillParser; using Kp2aAutofillParser;
using Newtonsoft.Json; 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 namespace keepass2android.services.AutofillBase
{ {
@@ -38,7 +28,7 @@ namespace keepass2android.services.AutofillBase
[JsonIgnore] [JsonIgnore]
public AssistStructure.ViewNode ViewNode { get; set; } public AssistStructure.ViewNode ViewNode { get; set; }
public override void FillFilledAutofillValue(FilledAutofillField filledField) public void FillFilledAutofillValue(FilledAutofillField<ViewNodeInputField> filledField)
{ {
AutofillValue autofillValue = ViewNode.AutofillValue; AutofillValue autofillValue = ViewNode.AutofillValue;
if (autofillValue != null) if (autofillValue != null)
@@ -130,7 +120,6 @@ namespace keepass2android.services.AutofillBase
} }
autofillView.InputFields.Add(new ViewNodeInputField(viewNode)); 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; var childrenSize = viewNode.ChildCount;
if (childrenSize > 0) if (childrenSize > 0)
@@ -148,290 +137,63 @@ namespace keepass2android.services.AutofillBase
/// AssistStructure from the client Activity, representing its View hierarchy. In this sample, it /// 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. /// parses the hierarchy and collects autofill metadata from {@link ViewNode}s along the way.
/// </summary> /// </summary>
public sealed class StructureParser public sealed class StructureParser: StructureParserBase<ViewNodeInputField>
{ {
public Context mContext { get; } private readonly AssistStructure _structure;
public Context _context { get; }
public AutofillFieldMetadataCollection AutofillFields { get; set; } public AutofillFieldMetadataCollection AutofillFields { get; set; }
public FilledAutofillFieldCollection ClientFormData { get; set; } public FilledAutofillFieldCollection<ViewNodeInputField> ClientFormData { get; set; }
public string PackageId { get; set; } public string PackageId { get; set; }
public StructureParser(Context context, AssistStructure structure) public StructureParser(Context context, AssistStructure structure)
: base(new Kp2aLogger(), new Kp2aDigitalAssetLinksDataSource(context))
{ {
kp2aDigitalAssetLinksDataSource = new Kp2aDigitalAssetLinksDataSource(context); _context = context;
mContext = context; _structure = structure;
Structure = structure;
AutofillFields = new AutofillFieldMetadataCollection(); AutofillFields = new AutofillFieldMetadataCollection();
} }
public class AutofillTargetId protected override AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<ViewNodeInputField> autofillView)
{ {
public string PackageName { get; set; } var result = base.Parse(forFill, isManualRequest, autofillView);
public string PackageNameWithPseudoSchema if (forFill)
{ {
get { return KeePass.AndroidAppScheme + PackageName; } foreach (var p in FieldsMappedToHints)
AutofillFields.Add(new AutofillFieldMetadata(p.Key.ViewNode, p.Value));
}
else
{
foreach (var p in FieldsMappedToHints)
ClientFormData.Add(new FilledAutofillField<ViewNodeInputField>(p.Key, p.Value));
} }
public string WebDomain { get; set; }
/// <summary> return result;
/// 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.
/// </summary>
public bool IncompatiblePackageAndDomain { get; set; }
public string DomainOrPackage
{
get
{
return WebDomain ?? PackageNameWithPseudoSchema;
}
}
}
public AutofillTargetId ParseForFill(bool isManual)
{
return Parse(true, isManual);
} }
public AutofillTargetId ParseForSave() public AutofillTargetId ParseForSave()
{ {
return Parse(false, true); var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(true);
return Parse(false, true, autofillView);
} }
/// <summary> public StructureParserBase<ViewNodeInputField>.AutofillTargetId ParseForFill(bool isManual)
/// Traverse AssistStructure and add ViewNode metadata to a flat list.
/// </summary>
/// <returns>The parse.</returns>
/// <param name="forFill">If set to <c>true</c> for fill.</param>
/// <param name="isManualRequest"></param>
AutofillTargetId Parse(bool forFill, bool isManualRequest)
{ {
AutofillTargetId result = new AutofillTargetId(); var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(isManual);
CommonUtil.logd("Parsing structure for " + Structure.ActivityComponent); return Parse(true, isManual, autofillView);
ClientFormData = new FilledAutofillFieldCollection();
_editTextsWithoutHint.Clear();
Kp2aLog.Log("parsing autofillStructure...");
AutofillView<ViewNodeInputField> 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)
{
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<ViewNodeInputField> passwordFields = new List<ViewNodeInputField>();
List<ViewNodeInputField> usernameFields = new List<ViewNodeInputField>();
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) public class Kp2aLogger : ILogger
if (isManualRequest)
{ {
foreach (var editText in _editTextsWithoutHint) public void Log(string x)
{ {
if (editText.IsFocused) Kp2aLog.Log(x);
{
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 }));
}
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;
}
return result;
}
private static readonly HashSet<string> _passwordHints = new HashSet<string> { "password","passwort", "passwordAuto", "pswd" };
private static bool HasPasswordHint(InputField f)
{
return ContainsAny(f.IdEntry, _passwordHints) ||
ContainsAny(f.Hint, _passwordHints);
}
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "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<string> 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")
);
}
AssistStructure Structure;
private List<ViewNodeInputField> _editTextsWithoutHint = new List<ViewNodeInputField>();
} }
} }

View File

@@ -15,6 +15,7 @@ using keepass2android.services.AutofillBase.model;
using Keepass2android.Pluginsdk; using Keepass2android.Pluginsdk;
using KeePassLib; using KeePassLib;
using KeePassLib.Utility; using KeePassLib.Utility;
using Kp2aAutofillParser;
namespace keepass2android.services.Kp2aAutofill namespace keepass2android.services.Kp2aAutofill
{ {
@@ -41,7 +42,7 @@ namespace keepass2android.services.Kp2aAutofill
protected override Result ExpectedActivityResult => KeePass.ExitCloseAfterTaskComplete; protected override Result ExpectedActivityResult => KeePass.ExitCloseAfterTaskComplete;
protected override FilledAutofillFieldCollection GetDataset() protected override FilledAutofillFieldCollection<ViewNodeInputField> GetDataset()
{ {
if (App.Kp2a.CurrentDb==null || (App.Kp2a.QuickLocked)) if (App.Kp2a.CurrentDb==null || (App.Kp2a.QuickLocked))
return null; return null;
@@ -50,18 +51,18 @@ namespace keepass2android.services.Kp2aAutofill
return GetFilledAutofillFieldCollectionFromEntry(entryOutput, this); return GetFilledAutofillFieldCollectionFromEntry(entryOutput, this);
} }
public static FilledAutofillFieldCollection GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context) public static FilledAutofillFieldCollection<ViewNodeInputField> GetFilledAutofillFieldCollectionFromEntry(PwEntryOutput pwEntryOutput, Context context)
{ {
if (pwEntryOutput == null) if (pwEntryOutput == null)
return null; return null;
FilledAutofillFieldCollection fieldCollection = new FilledAutofillFieldCollection(); FilledAutofillFieldCollection<ViewNodeInputField> fieldCollection = new FilledAutofillFieldCollection<ViewNodeInputField>();
var pwEntry = pwEntryOutput.Entry; var pwEntry = pwEntryOutput.Entry;
foreach (string key in pwEntryOutput.OutputStrings.GetKeys()) foreach (string key in pwEntryOutput.OutputStrings.GetKeys())
{ {
FilledAutofillField field = FilledAutofillField<ViewNodeInputField> field =
new FilledAutofillField new FilledAutofillField<ViewNodeInputField>
{ {
AutofillHints = GetCanonicalHintsFromKp2aField(key).ToArray(), AutofillHints = GetCanonicalHintsFromKp2aField(key).ToArray(),
TextValue = pwEntryOutput.OutputStrings.ReadSafe(key) TextValue = pwEntryOutput.OutputStrings.ReadSafe(key)
@@ -72,8 +73,8 @@ namespace keepass2android.services.Kp2aAutofill
if (IsCreditCard(pwEntry, context) && pwEntry.Expires) if (IsCreditCard(pwEntry, context) && pwEntry.Expires)
{ {
DateTime expTime = pwEntry.ExpiryTime; DateTime expTime = pwEntry.ExpiryTime;
FilledAutofillField field = FilledAutofillField<ViewNodeInputField> field =
new FilledAutofillField new FilledAutofillField<ViewNodeInputField>
{ {
AutofillHints = new[] {View.AutofillHintCreditCardExpirationDate}, AutofillHints = new[] {View.AutofillHintCreditCardExpirationDate},
DateValue = (long) (1000 * TimeUtil.SerializeUnix(expTime)) DateValue = (long) (1000 * TimeUtil.SerializeUnix(expTime))
@@ -81,7 +82,7 @@ namespace keepass2android.services.Kp2aAutofill
fieldCollection.Add(field); fieldCollection.Add(field);
field = field =
new FilledAutofillField new FilledAutofillField<ViewNodeInputField>
{ {
AutofillHints = new[] {View.AutofillHintCreditCardExpirationDay}, AutofillHints = new[] {View.AutofillHintCreditCardExpirationDay},
TextValue = expTime.Day.ToString() TextValue = expTime.Day.ToString()
@@ -89,7 +90,7 @@ namespace keepass2android.services.Kp2aAutofill
fieldCollection.Add(field); fieldCollection.Add(field);
field = field =
new FilledAutofillField new FilledAutofillField<ViewNodeInputField>
{ {
AutofillHints = new[] {View.AutofillHintCreditCardExpirationMonth}, AutofillHints = new[] {View.AutofillHintCreditCardExpirationMonth},
TextValue = expTime.Month.ToString() TextValue = expTime.Month.ToString()
@@ -97,7 +98,7 @@ namespace keepass2android.services.Kp2aAutofill
fieldCollection.Add(field); fieldCollection.Add(field);
field = field =
new FilledAutofillField new FilledAutofillField<ViewNodeInputField>
{ {
AutofillHints = new[] {View.AutofillHintCreditCardExpirationYear}, AutofillHints = new[] {View.AutofillHintCreditCardExpirationYear},
TextValue = expTime.Year.ToString() TextValue = expTime.Year.ToString()

View File

@@ -12,6 +12,7 @@ using Keepass2android.Pluginsdk;
using KeePassLib; using KeePassLib;
using KeePassLib.Collections; using KeePassLib.Collections;
using KeePassLib.Utility; using KeePassLib.Utility;
using Kp2aAutofillParser;
using Org.Json; using Org.Json;
using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase; using AutofillServiceBase = keepass2android.services.AutofillBase.AutofillServiceBase;
@@ -33,10 +34,10 @@ namespace keepass2android.services
{ {
} }
protected override List<FilledAutofillFieldCollection> GetSuggestedEntries(string query) protected override List<FilledAutofillFieldCollection<ViewNodeInputField>> GetSuggestedEntries(string query)
{ {
if (!App.Kp2a.DatabaseIsUnlocked) if (!App.Kp2a.DatabaseIsUnlocked)
return new List<FilledAutofillFieldCollection>(); return new List<FilledAutofillFieldCollection<ViewNodeInputField>>();
var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList<PwEntry>()) var foundEntries = (ShareUrlResults.GetSearchResultsForUrl(query)?.Entries ?? new PwObjectList<PwEntry>())
.Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e))) .Select(e => new PwEntryOutput(e, App.Kp2a.FindDatabaseForElement(e)))
.ToList(); .ToList();

View File

@@ -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;
}
}
/// <summary>
/// FilledAutofillFieldCollection is the model that holds all of the data on a client app's page,
/// plus the dataset name associated with it.
/// </summary>
public class FilledAutofillFieldCollection<FieldT> where FieldT:InputField
{
public Dictionary<string, FilledAutofillField<FieldT>> HintMap { get; }
public string DatasetName { get; set; }
public FilledAutofillFieldCollection(Dictionary<string, FilledAutofillField<FieldT>> 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<string, FilledAutofillField<FieldT>> BuildHintMap()
{
return new Dictionary<string, FilledAutofillField<FieldT>>(StringComparer.OrdinalIgnoreCase);
}
/// <summary>
/// Adds a filledAutofillField to the collection, indexed by all of its hints.
/// </summary>
/// <returns>The add.</returns>
/// <param name="filledAutofillField">Filled autofill field.</param>
public void Add(FilledAutofillField<FieldT> filledAutofillField)
{
foreach (string hint in filledAutofillField.AutofillHints)
{
if (AutofillHintsHelper.IsSupportedHint(hint))
{
HintMap.TryAdd(hint, filledAutofillField);
}
}
}
/// <summary>
/// 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.
/// </summary>
/// <returns><c>true</c>, if with hints was helpsed, <c>false</c> otherwise.</returns>
/// <param name="autofillHints">Autofill hints.</param>
public bool HelpsWithHints(List<string> 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<string> _allSupportedHints = new HashSet<string>(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<HashSet<string>> partitionsOfCanonicalHints = new List<HashSet<string>>()
{
new HashSet<string>(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<string>(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<string>(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<string, string> hintToCanonicalReplacement = new Dictionary<string, string>(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<string>();
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;
}
/// <summary>
/// transforms hints by replacing some W3cHints by their Android counterparts and transforming everything to lowercase
/// </summary>
public static List<string> ConvertToCanonicalHints(string[] supportedHints)
{
List<string> result = new List<string>();
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<FieldT> FilterForPartition<FieldT>(FilledAutofillFieldCollection<FieldT> autofillFields, int partitionIndex) where FieldT: InputField
{
FilledAutofillFieldCollection<FieldT> filteredCollection =
new FilledAutofillFieldCollection<FieldT> { 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<FieldT> FilterForPartition<FieldT>(FilledAutofillFieldCollection<FieldT> filledAutofillFieldCollection, List<string> 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;
}
}
/// <summary>
/// This enum represents the Android.Text.InputTypes values. For testability, this is duplicated here.
/// </summary>
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<FieldT> 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;
}
/// <summary>
/// returns the autofill hints for the filled field. These are always lowercased for simpler string comparison.
/// </summary>
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<string> hintList = new List<string>();
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<FieldT> that = (FilledAutofillField<FieldT>)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;
}
}
}
/// <summary>
/// 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
/// </summary>
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<TField> where TField : InputField
{
public List<TField> InputFields { get; set; } = new List<TField>();
public string PackageId { get; set; } = null;
public string WebDomain { get; set; } = null;
}
public interface ILogger
{
void Log(string x);
}
public class StructureParserBase<FieldT> where FieldT: InputField
{
private readonly ILogger _log;
private readonly IKp2aDigitalAssetLinksDataSource _digitalAssetLinksDataSource;
private readonly List<string> _autofillHintsForLogin = new List<string>
{
AutofillHintsHelper.AutofillHintPassword,
AutofillHintsHelper.AutofillHintUsername,
AutofillHintsHelper.AutofillHintEmailAddress
};
public string PackageId { get; set; }
public Dictionary<FieldT, string[]> FieldsMappedToHints = new Dictionary<FieldT, string[]>();
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; }
/// <summary>
/// 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.
/// </summary>
public bool IncompatiblePackageAndDomain { get; set; }
public string DomainOrPackage
{
get
{
return WebDomain ?? PackageNameWithPseudoSchema;
}
}
}
public AutofillTargetId ParseForFill(bool isManual, AutofillView<FieldT> autofillView)
{
return Parse(true, isManual, autofillView);
}
public AutofillTargetId ParseForSave(AutofillView<FieldT> autofillView)
{
return Parse(false, true, autofillView);
}
/// <summary>
/// Traverse AssistStructure and add ViewNode metadata to a flat list.
/// </summary>
/// <returns>The parse.</returns>
/// <param name="forFill">If set to <c>true</c> for fill.</param>
/// <param name="isManualRequest"></param>
protected virtual AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<FieldT> 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<string> 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<FieldT> 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<FieldT> 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<string> _passwordHints = new HashSet<string> { "password", "passwort"
/*, "passwordAuto", "pswd"*/ };
private static bool HasPasswordHint(InputField f)
{
return IsAny(f.IdEntry, _passwordHints) ||
IsAny(f.Hint, _passwordHints);
}
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "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<string> 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")
);
}
}
}

View File

@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>
</Project>