Merge pull request #2254 from PhilippC/PhilippC-autofill-testing-and-improvements

Autofill testing and improvements
This commit is contained in:
PhilippC
2023-03-06 08:28:42 +01:00
committed by GitHub
28 changed files with 3273 additions and 1100 deletions

View File

@@ -3,109 +3,111 @@ name: Build keepass2android app
on: [push, pull_request]
jobs:
macos:
# macos:
# Disabled. Does not work, maybe due to nuget version, see https://github.com/PhilippC/keepass2android/actions/runs/4297640426/jobs/7490853348
# should work again when the Project solution is converted to sdk style .csproj files.
runs-on: macos-12
# runs-on: macos-12
steps:
- uses: actions/checkout@v3
# steps:
# - uses: actions/checkout@v3
- name: Fetch submodules
run: git submodule init && git submodule update
# - name: Fetch submodules
# run: git submodule init && git submodule update
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
# - name: Setup Gradle
# uses: gradle/gradle-build-action@v2
- name: Cache NuGet packages
uses: actions/cache@v3
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
restore-keys: |
${{ runner.os }}-nuget-
# - name: Cache NuGet packages
# uses: actions/cache@v3
# with:
# path: ~/.nuget/packages
# key: ${{ runner.os }}-nuget-${{ hashFiles('src/**/*.csproj', 'src/**/packages.config') }}
# restore-keys: |
# ${{ runner.os }}-nuget-
# As per https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#visual-studio-for-mac
- name: Switch to Visual Studio 2019
if: ${{ false }} # Not needed. We stay with the default 'Visual Studio 2022' of macos-12 runner.
run: |
mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app"
mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app"
# # As per https://github.com/actions/runner-images/blob/main/images/macos/macos-12-Readme.md#visual-studio-for-mac
# - name: Switch to Visual Studio 2019
# if: ${{ false }} # Not needed. We stay with the default 'Visual Studio 2022' of macos-12 runner.
# run: |
# mv "/Applications/Visual Studio.app" "/Applications/Visual Studio 2022.app"
# mv "/Applications/Visual Studio 2019.app" "/Applications/Visual Studio.app"
# As of 2022-12-02, keepass2android doesn't build with Xamarin >= 12.1 because there is some issue with SamsungPass. Removing SamsungPass would make the build succeed.
- name: Set default Xamarin SDK versions
run: |
# If using the github runner 'macos-12'
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.3
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.1 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException : Index 4 out of bounds for length 4
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.2 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException : Index 4 out of bounds for length 4
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.3 # Build fails in this case, as of 2022-12-02
$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=13.1
# # As of 2022-12-02, keepass2android doesn't build with Xamarin >= 12.1 because there is some issue with SamsungPass. Removing SamsungPass would make the build succeed.
# - name: Set default Xamarin SDK versions
# run: |
# # If using the github runner 'macos-12'
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.3
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.1 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException : Index 4 out of bounds for length 4
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.2 # Build fails in this case, as of 2022-12-02 : Xamarin/Android/Xamarin.Android.D8.targets(79,5): error : java.lang.ArrayIndexOutOfBoundsException : Index 4 out of bounds for length 4
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.3 # Build fails in this case, as of 2022-12-02
# $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=13.1
# If using the github runner 'macos-11'
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.0
#$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0
# # If using the github runner 'macos-11'
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.0
# #$VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=12.0
# If using the github runner 'macos-10.15'
# $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
# # If using the github runner 'macos-10.15'
# # $VM_ASSETS/select-xamarin-sdk-v2.sh --mono=6.12 --android=11.2
- name: Switch to JDK-11
uses: actions/setup-java@v3
with:
java-version: '11'
distribution: 'temurin'
# - name: Switch to JDK-11
# uses: actions/setup-java@v3
# with:
# java-version: '11'
# distribution: 'temurin'
- name: Display java version
run: java -version
# - name: Display java version
# run: java -version
# Some components of Keepass2Android currently target android API 26 which are not available on the runner
- name: Download android-26 API
run: $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-26"
# # Some components of Keepass2Android currently target android API 26 which are not available on the runner
# - name: Download android-26 API
# run: $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "platforms;android-26"
- name: Build native dependencies
run: make native
# - name: Build native dependencies
# run: make native
- name: Build java dependencies
run: make java
# - name: Build java dependencies
# run: make java
- name: Install NuGet dependencies (net)
run: make nuget Flavor=Net
# - name: Install NuGet dependencies (net)
# run: make nuget Flavor=Net
- name: Build keepass2android (net)
run: |
make msbuild Flavor=Net
# - name: Build keepass2android (net)
# run: |
# make msbuild Flavor=Net
- name: Build APK (net)
run: |
make apk Flavor=Net
# - name: Build APK (net)
# run: |
# make apk Flavor=Net
- name: Archive production artifacts (net)
uses: actions/upload-artifact@v3
with:
name: signed APK ('net' built on ${{ github.job }})
path: |
src/keepass2android/bin/*/*-Signed.apk
# - name: Archive production artifacts (net)
# uses: actions/upload-artifact@v3
# with:
# name: signed APK ('net' built on ${{ github.job }})
# path: |
# src/keepass2android/bin/*/*-Signed.apk
- name: Install NuGet dependencies (nonet)
run: make nuget Flavor=NoNet
# - name: Install NuGet dependencies (nonet)
# run: make nuget Flavor=NoNet
- name: Build keepass2android (nonet)
run: |
make msbuild Flavor=NoNet
# - name: Build keepass2android (nonet)
# run: |
# make msbuild Flavor=NoNet
- name: Build APK (nonet)
run: |
make apk Flavor=NoNet
# - name: Build APK (nonet)
# run: |
# make apk Flavor=NoNet
- name: Archive production artifacts (nonet)
uses: actions/upload-artifact@v3
with:
name: signed APK ('nonet' built on ${{ github.job }})
path: |
src/keepass2android/bin/*/*-Signed.apk
# - name: Archive production artifacts (nonet)
# uses: actions/upload-artifact@v3
# with:
# name: signed APK ('nonet' built on ${{ github.job }})
# path: |
# src/keepass2android/bin/*/*-Signed.apk
- name: Perform "make distclean"
run: make distclean
# - name: Perform "make distclean"
# run: make distclean
# linux:
# disabled.
@@ -330,6 +332,9 @@ jobs:
- name: Build keepass2android (nonet)
run: |
make msbuild Flavor=NoNet
- name: Test Autofill
working-directory: ./src/Kp2aAutofillParserTest
run: dotnet test
- name: Build APK (nonet)
run: |

View File

@@ -25,6 +25,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PCloudBindings", "PCloudBin
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "keepass2android-app", "keepass2android\keepass2android-app.csproj", "{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kp2aAutofillParser", "Kp2aAutofillParser\Kp2aAutofillParser.csproj", "{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kp2aAutofillParserTest", "Kp2aAutofillParserTest\Kp2aAutofillParserTest.csproj", "{3D1560FF-86BB-4CB4-8367-80BA13B81C38}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -283,6 +287,54 @@ Global
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.Build.0 = Release|Any CPU
{D4C32E0A-0193-4496-9DB4-02CC126FD9F3}.ReleaseNoNet|x64.Deploy.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Win32.ActiveCfg = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|Win32.Build.0 = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|x64.ActiveCfg = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Debug|x64.Build.0 = Debug|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Any CPU.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Win32.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|Win32.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|x64.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.Release|x64.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU
{39B12571-BAFE-4D3A-AEE2-4D74F14DFD96}.ReleaseNoNet|Win32.ActiveCfg = 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.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.ActiveCfg = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|Win32.Build.0 = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.ActiveCfg = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Debug|x64.Build.0 = Debug|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Any CPU.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|Win32.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.Release|x64.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Any CPU.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Mixed Platforms.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|Win32.Build.0 = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.ActiveCfg = Release|Any CPU
{3D1560FF-86BB-4CB4-8367-80BA13B81C38}.ReleaseNoNet|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,963 @@
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 (or could be) an 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; }
}
/// <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 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...");
if (LogAutofillView)
{
string debugInfo = JsonConvert.SerializeObject(autofillView, Newtonsoft.Json.Formatting.Indented);
_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;
}
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)
{
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>

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 ?? new string[]{})}");
_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

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="189"
android:versionName="1.09e-r1"
android:versionCode="190"
android:versionName="1.09e-r2"
package="keepass2android.keepass2android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
android:versionCode="189"
android:versionName="1.09e-r1"
android:versionCode="190"
android:versionName="1.09e-r2"
package="keepass2android.keepass2android_nonet"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="auto">

View File

@@ -1944,6 +1944,10 @@
<Project>{545b4a6b-8bba-4fbe-92fc-4ac060122a54}</Project>
<Name>KeePassLib2Android</Name>
</ProjectReference>
<ProjectReference Include="..\Kp2aAutofillParser\Kp2aAutofillParser.csproj">
<Project>{39b12571-bafe-4d3a-aee2-4d74f14dfd96}</Project>
<Name>Kp2aAutofillParser</Name>
</ProjectReference>
<ProjectReference Include="..\Kp2aBusinessLogic\Kp2aBusinessLogic.csproj">
<Project>{53a9cb7f-6553-4bc0-b56b-9410bb2e59aa}</Project>
<Name>Kp2aBusinessLogic</Name>

View File

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

View File

@@ -7,11 +7,12 @@ using Android.Runtime;
using Android.Service.Autofill;
using Android.Util;
using Android.Views;
using Android.Views.Autofill;
using Android.Widget;
using Android.Widget.Inline;
using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
@@ -93,13 +94,9 @@ namespace keepass2android.services.AutofillBase
/// Wraps autofill data in a LoginCredential Dataset object which can then be sent back to the
/// client View.
/// </summary>
/// <returns>The dataset.</returns>
/// <param name="context">Context.</param>
/// <param name="autofillFields">Autofill fields.</param>
/// <param name="filledAutofillFieldCollection">Filled autofill field collection.</param>
public static Dataset NewDataset(Context context,
AutofillFieldMetadataCollection autofillFields,
FilledAutofillFieldCollection filledAutofillFieldCollection,
FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
IAutofillIntentBuilder intentBuilder,
Android.Widget.Inline.InlinePresentationSpec inlinePresentationSpec)
{
@@ -108,21 +105,115 @@ namespace keepass2android.services.AutofillBase
var datasetBuilder = new Dataset.Builder(NewRemoteViews(context.PackageName, datasetName, intentBuilder.AppIconResource));
datasetBuilder.SetId(datasetName);
var setValueAtLeastOnce = filledAutofillFieldCollection.ApplyToFields(autofillFields, datasetBuilder);
var setValueAtLeastOnce = ApplyToFields(filledAutofillFieldCollection, autofillFields, datasetBuilder);
AddInlinePresentation(context, inlinePresentationSpec, datasetName, datasetBuilder, intentBuilder.AppIconResource, null);
if (setValueAtLeastOnce)
{
return datasetBuilder.Build();
}
else
/*else
{
Kp2aLog.Log("Failed to set at least one value. #fields=" + autofillFields.GetAutofillIds().Length + " " + autofillFields.FocusedAutofillCanonicalHints);
}
}*/
return null;
}
/// <summary>
/// Populates a Dataset.Builder with appropriate values for each AutofillId
/// in a AutofillFieldMetadataCollection.
///
/// In other words, it constructs an autofill Dataset.Builder
/// by applying saved values (from this FilledAutofillFieldCollection)
/// to Views specified in a AutofillFieldMetadataCollection, which represents the current
/// page the user is on.
/// </summary>
/// <returns><c>true</c>, if to fields was applyed, <c>false</c> otherwise.</returns>
/// <param name="filledAutofillFieldCollection"></param>
/// <param name="autofillFieldMetadataCollection">Autofill field metadata collection.</param>
/// <param name="datasetBuilder">Dataset builder.</param>
public static bool ApplyToFields(FilledAutofillFieldCollection<ViewNodeInputField> filledAutofillFieldCollection,
AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
{
bool setValueAtLeastOnce = false;
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
{
FilledAutofillField<ViewNodeInputField> filledAutofillField;
if (!filledAutofillFieldCollection.HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
continue;
}
var autofillId = autofillFieldMetadata.AutofillId;
var autofillType = autofillFieldMetadata.AutofillType;
switch (autofillType)
{
case AutofillType.List:
var listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
if (listValue != -1)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Date:
var dateValue = filledAutofillField.DateValue;
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate((long)dateValue));
setValueAtLeastOnce = true;
break;
case AutofillType.Text:
var textValue = filledAutofillField.TextValue;
if (textValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Toggle:
var toggleValue = filledAutofillField.ToggleValue;
if (toggleValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
setValueAtLeastOnce = true;
}
break;
default:
Log.Warn(CommonUtil.Tag, "Invalid autofill type - " + autofillType);
break;
}
}
}
/*
if (!setValueAtLeastOnce)
{
Kp2aLog.Log("No value set. Hint keys : " + string.Join(",", HintMap.Keys));
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
Kp2aLog.Log("No value set. Hint = " + hint);
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection
.GetFieldsForHint(hint))
{
Kp2aLog.Log("No value set. fieldForHint = " + autofillFieldMetadata.AutofillId.ToString());
FilledAutofillField filledAutofillField;
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
Kp2aLog.Log("No value set. Hint map does not contain value, " +
(filledAutofillField == null));
continue;
}
Kp2aLog.Log("autofill type=" + autofillFieldMetadata.AutofillType);
}
}
}*/
return setValueAtLeastOnce;
}
public static void AddInlinePresentation(Context context, InlinePresentationSpec inlinePresentationSpec,
string datasetName, Dataset.Builder datasetBuilder, int iconId, PendingIntent pendingIntent)
{

View File

@@ -14,256 +14,5 @@ using keepass2android.services.AutofillBase.model;
namespace keepass2android.services.AutofillBase
{
class AutofillHintsHelper
{
private static readonly HashSet<string> _allSupportedHints = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
{
View.AutofillHintCreditCardExpirationDate,
View.AutofillHintCreditCardExpirationDay,
View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear,
View.AutofillHintCreditCardNumber,
View.AutofillHintCreditCardSecurityCode,
View.AutofillHintEmailAddress,
View.AutofillHintPhone,
View.AutofillHintName,
View.AutofillHintPassword,
View.AutofillHintPostalAddress,
View.AutofillHintPostalCode,
View.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)
{
View.AutofillHintEmailAddress,
View.AutofillHintPhone,
View.AutofillHintName,
View.AutofillHintPassword,
View.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)
{
View.AutofillHintPostalAddress,
View.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)
{
View.AutofillHintCreditCardExpirationDate,
View.AutofillHintCreditCardExpirationDay,
View.AutofillHintCreditCardExpirationMonth,
View.AutofillHintCreditCardExpirationYear,
View.AutofillHintCreditCardNumber,
View.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, View.AutofillHintEmailAddress},
{W3cHints.USERNAME, View.AutofillHintUsername},
{W3cHints.CURRENT_PASSWORD, View.AutofillHintPassword},
{W3cHints.NEW_PASSWORD, View.AutofillHintPassword},
{W3cHints.CC_EXPIRATION_MONTH, View.AutofillHintCreditCardExpirationMonth },
{W3cHints.CC_EXPIRATION_YEAR, View.AutofillHintCreditCardExpirationYear },
{W3cHints.CC_EXPIRATION, View.AutofillHintCreditCardExpirationDate },
{W3cHints.CC_NUMBER, View.AutofillHintCreditCardNumber },
{W3cHints.CC_CSC, View.AutofillHintCreditCardSecurityCode },
{W3cHints.POSTAL_CODE, View.AutofillHintPostalCode },
};
public static bool IsSupportedHint(string hint)
{
return _allSupportedHints.Contains(hint);
}
public static string[] FilterForSupportedHints(string[] hints)
{
var filteredHints = new string[hints.Length];
int i = 0;
foreach (var hint in hints)
{
if (IsSupportedHint(hint))
{
filteredHints[i++] = hint;
}
else
{
CommonUtil.logd("Invalid autofill hint: " + 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 FilterForPartition(FilledAutofillFieldCollection autofillFields, int partitionIndex)
{
FilledAutofillFieldCollection filteredCollection =
new FilledAutofillFieldCollection {DatasetName = autofillFields.DatasetName};
if (partitionIndex == -1)
return filteredCollection;
foreach (var field in autofillFields.HintMap.Values.Distinct())
{
foreach (var hint in field.AutofillHints)
{
if (GetPartitionIndex(hint) == partitionIndex)
{
filteredCollection.Add(field);
break;
}
}
}
return filteredCollection;
}
public static FilledAutofillFieldCollection FilterForPartition(FilledAutofillFieldCollection filledAutofillFieldCollection, List<string> autofillFieldsFocusedAutofillCanonicalHints)
{
//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;
}
}
}

View File

@@ -20,6 +20,7 @@ using AndroidX.AutoFill.Inline;
using AndroidX.AutoFill.Inline.V1;
using Java.Util.Concurrent.Atomic;
using keepass2android.services.AutofillBase.model;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
@@ -137,7 +138,7 @@ namespace keepass2android.services.AutofillBase
return;
}
AutofillFieldMetadataCollection autofillFields = parser.AutofillFields;
InlineSuggestionsRequest inlineSuggestionsRequest = null;
IList<InlinePresentationSpec> inlinePresentationSpecs = null;
if (((int) Build.VERSION.SdkInt >= 30)
@@ -149,7 +150,7 @@ namespace keepass2android.services.AutofillBase
}
var autofillIds = autofillFields.GetAutofillIds();
var autofillIds = parser.AutofillFields.GetAutofillIds();
if (autofillIds.Length != 0 && CanAutofill(query, isManual))
{
var responseBuilder = new FillResponse.Builder();
@@ -255,7 +256,7 @@ namespace keepass2android.services.AutofillBase
if (warning == DisplayWarning.None)
{
FilledAutofillFieldCollection partitionData =
FilledAutofillFieldCollection<ViewNodeInputField> partitionData =
AutofillHintsHelper.FilterForPartition(filledAutofillFieldCollection, parser.AutofillFields.FocusedAutofillCanonicalHints);
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
{

View File

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

View File

@@ -2,11 +2,13 @@
using System.Linq;
using Android.Content;
using Android.Preferences;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase
{
internal class Kp2aDigitalAssetLinksDataSource
internal class Kp2aDigitalAssetLinksDataSource : IKp2aDigitalAssetLinksDataSource
{
private const string Autofilltrustedapps = "AutoFillTrustedApps";
@@ -37,6 +39,11 @@ namespace keepass2android.services.AutofillBase
return trustedLinks.Contains(BuildLink(domain, targetPackage));
}
public bool IsEnabled()
{
return !PreferenceManager.GetDefaultSharedPreferences(_ctx).GetBoolean(_ctx.GetString(Resource.String.NoDalVerification_key), false);
}
public void RememberAsTrustedApp(string packageName)
{
var prefs = PreferenceManager.GetDefaultSharedPreferences(_ctx);

View File

@@ -1,334 +1,199 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Android.App.Assist;
using Android.Content;
using Android.Preferences;
using Android.Text;
using Android.Util;
using Android.Views;
using Android.Views.Autofill;
using Android.Views.InputMethods;
using DomainNameParser;
using keepass2android.services.AutofillBase.model;
using FilledAutofillFieldCollection = keepass2android.services.AutofillBase.model.FilledAutofillFieldCollection;
using Kp2aAutofillParser;
using Newtonsoft.Json;
namespace keepass2android.services.AutofillBase
{
public class ViewNodeInputField : Kp2aAutofillParser.InputField
{
public ViewNodeInputField(AssistStructure.ViewNode viewNode)
{
ViewNode = viewNode;
IdEntry = viewNode.IdEntry;
Hint = viewNode.Hint;
ClassName = viewNode.ClassName;
AutofillHints = viewNode.GetAutofillHints();
IsFocused = viewNode.IsFocused;
InputType = (Kp2aAutofillParser.InputTypes) ((int)viewNode.InputType);
HtmlInfoTag = viewNode.HtmlInfo?.Tag;
HtmlInfoTypeAttribute = viewNode.HtmlInfo?.Attributes?.FirstOrDefault(p => p.First?.ToString() == "type")?.Second?.ToString();
}
[JsonIgnore]
public AssistStructure.ViewNode ViewNode { get; set; }
public void FillFilledAutofillValue(FilledAutofillField<ViewNodeInputField> filledField)
{
AutofillValue autofillValue = ViewNode.AutofillValue;
if (autofillValue != null)
{
if (autofillValue.IsList)
{
string[] autofillOptions = ViewNode.GetAutofillOptions();
int index = autofillValue.ListValue;
if (autofillOptions != null && autofillOptions.Length > 0)
{
filledField.TextValue = autofillOptions[index];
}
}
else if (autofillValue.IsDate)
{
filledField.DateValue = autofillValue.DateValue;
}
else if (autofillValue.IsText)
{
filledField.TextValue = autofillValue.TextValue;
}
}
}
}
/// <summary>
/// Converts an AssistStructure into a list of InputFields
/// </summary>
class AutofillViewFromAssistStructureFinder
{
private readonly Context _context;
private readonly AssistStructure _structure;
private PublicSuffixRuleCache domainSuffixParserCache;
public AutofillViewFromAssistStructureFinder(Context context, AssistStructure structure)
{
_context = context;
_structure = structure;
domainSuffixParserCache = new PublicSuffixRuleCache(context);
}
public AutofillView<ViewNodeInputField> GetAutofillView(bool isManualRequest)
{
AutofillView<ViewNodeInputField> autofillView = new AutofillView<ViewNodeInputField>();
int nodeCount = _structure.WindowNodeCount;
for (int i = 0; i < nodeCount; i++)
{
var node = _structure.GetWindowNodeAt(i);
var view = node.RootViewNode;
ParseRecursive(autofillView, view, isManualRequest);
}
return autofillView;
}
void ParseRecursive(AutofillView<ViewNodeInputField> autofillView, AssistStructure.ViewNode viewNode, bool isManualRequest)
{
String webDomain = viewNode.WebDomain;
if ((autofillView.PackageId == null) && (!string.IsNullOrWhiteSpace(viewNode.IdPackage)) &&
(viewNode.IdPackage != "android"))
{
autofillView.PackageId = viewNode.IdPackage;
}
DomainName outDomain;
if (DomainName.TryParse(webDomain, domainSuffixParserCache, out outDomain))
{
webDomain = outDomain.RawDomainName;
}
if (webDomain != null)
{
if (!string.IsNullOrEmpty(autofillView.WebDomain))
{
if (webDomain != autofillView.WebDomain)
{
throw new Java.Lang.SecurityException($"Found multiple web domains: valid= {autofillView.WebDomain}, child={webDomain}");
}
}
else
{
autofillView.WebDomain = webDomain;
}
}
autofillView.InputFields.Add(new ViewNodeInputField(viewNode));
var childrenSize = viewNode.ChildCount;
if (childrenSize > 0)
{
for (int i = 0; i < childrenSize; i++)
{
ParseRecursive(autofillView, viewNode.GetChildAt(i), isManualRequest);
}
}
}
}
/// <summary>
/// Parser for an AssistStructure object. This is invoked when the Autofill Service receives an
/// 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.
/// </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; }
AssistStructure Structure;
private List<AssistStructure.ViewNode> _editTextsWithoutHint = new List<AssistStructure.ViewNode>();
private PublicSuffixRuleCache domainSuffixParserCache;
public FilledAutofillFieldCollection ClientFormData { get; set; }
public FilledAutofillFieldCollection<ViewNodeInputField> ClientFormData { get; set; }
public string PackageId { get; set; }
public StructureParser(Context context, AssistStructure structure)
: base(new Kp2aLogger(), new Kp2aDigitalAssetLinksDataSource(context))
{
kp2aDigitalAssetLinksDataSource = new Kp2aDigitalAssetLinksDataSource(context);
mContext = context;
Structure = structure;
AutofillFields = new AutofillFieldMetadataCollection();
domainSuffixParserCache = new PublicSuffixRuleCache(context);
}
public class AutofillTargetId
{
public string PackageName { get; set; }
public string PackageNameWithPseudoSchema
{
get { return KeePass.AndroidAppScheme + PackageName; }
}
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)
{
return Parse(true, isManual);
}
public AutofillTargetId ParseForSave()
{
return Parse(false, true);
}
/// <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>
AutofillTargetId Parse(bool forFill, bool isManualRequest)
{
AutofillTargetId result = new AutofillTargetId();
CommonUtil.logd("Parsing structure for " + Structure.ActivityComponent);
var nodes = Structure.WindowNodeCount;
ClientFormData = new FilledAutofillFieldCollection();
String webDomain = null;
_editTextsWithoutHint.Clear();
for (int i = 0; i < nodes; i++)
{
var node = Structure.GetWindowNodeAt(i);
var view = node.RootViewNode;
ParseLocked(forFill, isManualRequest, view, ref webDomain);
}
List<AssistStructure.ViewNode> passwordFields = new List<AssistStructure.ViewNode>();
List<AssistStructure.ViewNode> usernameFields = new List<AssistStructure.ViewNode>();
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.AutofillId != passwordField.AutofillId).LastOrDefault();
if (usernameField != null)
{
usernameFields.Add(usernameField);
}
}
}
if (usernameFields.Any() == false)
{
//for some pages with two-step login, we don't see a password field and don't display the autofill for non-manual requests. But if the user forces autofill,
//let's assume it is a username field:
if (isManualRequest && !passwordFields.Any() && _editTextsWithoutHint.Count == 1)
{
usernameFields.Add(_editTextsWithoutHint.First());
}
}
}
//force focused fields to be included in autofill fields when request was triggered manually. This allows to fill fields which are "off" or don't have a hint (in case there are hints)
if (isManualRequest)
{
foreach (AssistStructure.ViewNode editText in _editTextsWithoutHint)
{
if (editText.IsFocused)
{
if (IsPassword(editText) || HasPasswordHint(editText))
passwordFields.Add(editText);
else
usernameFields.Add(editText);
break;
}
}
}
if (forFill)
{
foreach (var uf in usernameFields)
AutofillFields.Add(new AutofillFieldMetadata(uf, new[] { View.AutofillHintUsername }));
foreach (var pf in passwordFields)
AutofillFields.Add(new AutofillFieldMetadata(pf, new[] { View.AutofillHintPassword }));
}
else
{
foreach (var uf in usernameFields)
ClientFormData.Add(new FilledAutofillField(uf, new[] { View.AutofillHintUsername }));
foreach (var pf in passwordFields)
ClientFormData.Add(new FilledAutofillField(pf, new[] { View.AutofillHintPassword }));
}
result.WebDomain = webDomain;
result.PackageName = Structure.ActivityComponent.PackageName;
if (!string.IsNullOrEmpty(webDomain) && !PreferenceManager.GetDefaultSharedPreferences(mContext).GetBoolean(mContext.GetString(Resource.String.NoDalVerification_key), false))
{
result.IncompatiblePackageAndDomain = !kp2aDigitalAssetLinksDataSource.IsTrustedLink(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" };
private static bool HasPasswordHint(AssistStructure.ViewNode f)
{
return ContainsAny(f.IdEntry, _passwordHints) ||
ContainsAny(f.Hint, _passwordHints);
}
private static readonly HashSet<string> _usernameHints = new HashSet<string> { "email","e-mail","username" };
private Kp2aDigitalAssetLinksDataSource kp2aDigitalAssetLinksDataSource;
private static bool HasUsernameHint(AssistStructure.ViewNode 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;
_context = context;
_structure = structure;
AutofillFields = new AutofillFieldMetadataCollection();
}
}
private static bool IsPassword(AssistStructure.ViewNode 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.HtmlInfo?.Attributes.Any(p => p.First.ToString() == "type" && p.Second.ToString() == "password") ?? false)
);
}
protected override AutofillTargetId Parse(bool forFill, bool isManualRequest, AutofillView<ViewNodeInputField> autofillView)
{
var result = base.Parse(forFill, isManualRequest, autofillView);
void ParseLocked(bool forFill, bool isManualRequest, AssistStructure.ViewNode viewNode, ref string validWebdomain)
{
String webDomain = viewNode.WebDomain;
if ((PackageId == null) && (!string.IsNullOrWhiteSpace(viewNode.IdPackage)) &&
(viewNode.IdPackage != "android"))
if (forFill)
{
PackageId = viewNode.IdPackage;
foreach (var p in FieldsMappedToHints)
AutofillFields.Add(new AutofillFieldMetadata(p.Key.ViewNode, p.Value));
}
DomainName outDomain;
if (DomainName.TryParse(webDomain, domainSuffixParserCache, out outDomain))
{
webDomain = outDomain.RawDomainName;
}
if (webDomain != null)
{
if (!string.IsNullOrEmpty(validWebdomain))
{
if (webDomain != validWebdomain)
{
throw new Java.Lang.SecurityException($"Found multiple web domains: valid= {validWebdomain}, child={webDomain}");
}
}
else
{
validWebdomain = webDomain;
}
}
string[] viewHints = viewNode.GetAutofillHints();
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 (viewHints != null && viewHints.Length > 0 && viewHints.First() != "on" /*if hint is "on", treat as if there is no hint*/)
{
if (forFill)
{
AutofillFields.Add(new AutofillFieldMetadata(viewNode));
}
else
{
FilledAutofillField filledAutofillField = new FilledAutofillField(viewNode);
ClientFormData.Add(filledAutofillField);
}
}
else
{
if (viewNode.ClassName == "android.widget.EditText"
|| viewNode.ClassName == "android.widget.AutoCompleteTextView"
|| viewNode?.HtmlInfo?.Tag == "input")
{
_editTextsWithoutHint.Add(viewNode);
}
foreach (var p in FieldsMappedToHints)
ClientFormData.Add(new FilledAutofillField<ViewNodeInputField>(p.Key, p.Value));
}
var childrenSize = viewNode.ChildCount;
if (childrenSize > 0)
{
for (int i = 0; i < childrenSize; i++)
{
ParseLocked(forFill, isManualRequest, viewNode.GetChildAt(i), ref validWebdomain);
}
}
}
}
return result;
}
public AutofillTargetId ParseForSave()
{
var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(true);
return Parse(false, true, autofillView);
}
public StructureParserBase<ViewNodeInputField>.AutofillTargetId ParseForFill(bool isManual)
{
var autofillView = new AutofillViewFromAssistStructureFinder(_context, _structure).GetAutofillView(isManual);
return Parse(true, isManual, autofillView);
}
}
public class Kp2aLogger : ILogger
{
public void Log(string x)
{
Kp2aLog.Log(x);
}
}
}

View File

@@ -2,154 +2,9 @@
using Android.App.Assist;
using Android.Views.Autofill;
using KeePassLib.Utility;
using Kp2aAutofillParser;
namespace keepass2android.services.AutofillBase.model
{
public class FilledAutofillField
{
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(AssistStructure.ViewNode viewNode)
: this(viewNode, viewNode.GetAutofillHints())
{
}
public FilledAutofillField(AssistStructure.ViewNode viewNode, 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];
CommonUtil.logd($"Hint is a W3C section prefix; using {hint} instead");
if (i < rawHints.Length - 1)
{
nextHint = rawHints[i + 1];
}
}
if (W3cHints.isW3cTypePrefix(hint) && nextHint != null && W3cHints.isW3cTypeHint(nextHint))
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C type prefix; using {hint} instead");
}
if (W3cHints.isW3cAddressType(hint) && nextHint != null)
{
hint = nextHint;
i++;
CommonUtil.logd($"Hint is a W3C address prefix; using {hint} instead");
}
// Then check if the "actual" hint is supported.
if (AutofillHintsHelper.IsSupportedHint(hint))
{
hintList.Add(hint);
}
else
{
CommonUtil.loge($"Invalid hint: {rawHints[i]}");
}
}
AutofillHints = AutofillHintsHelper.ConvertToCanonicalHints(hintList.ToArray()).ToArray();
AutofillValue autofillValue = viewNode.AutofillValue;
if (autofillValue != null)
{
if (autofillValue.IsList)
{
string[] autofillOptions = viewNode.GetAutofillOptions();
int index = autofillValue.ListValue;
if (autofillOptions != null && autofillOptions.Length > 0)
{
TextValue = autofillOptions[index];
}
}
else if (autofillValue.IsDate)
{
DateValue = autofillValue.DateValue;
}
else if (autofillValue.IsText)
{
TextValue = autofillValue.TextValue;
}
}
}
public bool IsNull()
{
return TextValue == null && DateValue == null && ToggleValue == null;
}
public override bool Equals(object obj)
{
if (this == obj) return true;
if (obj == null || GetType() != obj.GetType()) return false;
FilledAutofillField that = (FilledAutofillField)obj;
if (!TextValue?.Equals(that.TextValue) ?? that.TextValue != null)
return false;
if (DateValue != null ? !DateValue.Equals(that.DateValue) : that.DateValue != null)
return false;
return ToggleValue != null ? ToggleValue.Equals(that.ToggleValue) : that.ToggleValue == null;
}
public override int GetHashCode()
{
unchecked
{
var result = TextValue != null ? TextValue.GetHashCode() : 0;
result = 31 * result + (DateValue != null ? DateValue.GetHashCode() : 0);
result = 31 * result + (ToggleValue != null ? ToggleValue.GetHashCode() : 0);
return result;
}
}
}
}

View File

@@ -7,164 +7,5 @@ using Android.Views.Autofill;
namespace keepass2android.services.AutofillBase.model
{
/// <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
{
public Dictionary<string, FilledAutofillField> HintMap { get; }
public string DatasetName { get; set; }
public FilledAutofillFieldCollection(Dictionary<string, FilledAutofillField> 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> BuildHintMap()
{
return new Dictionary<string, FilledAutofillField>(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 filledAutofillField)
{
foreach (string hint in filledAutofillField.AutofillHints)
{
if (AutofillHintsHelper.IsSupportedHint(hint))
{
HintMap.TryAdd(hint, filledAutofillField);
}
else
{
CommonUtil.loge($"Invalid hint: {hint}");
}
}
}
/// <summary>
/// Populates a Dataset.Builder with appropriate values for each AutofillId
/// in a AutofillFieldMetadataCollection.
///
/// In other words, it constructs an autofill Dataset.Builder
/// by applying saved values (from this FilledAutofillFieldCollection)
/// to Views specified in a AutofillFieldMetadataCollection, which represents the current
/// page the user is on.
/// </summary>
/// <returns><c>true</c>, if to fields was applyed, <c>false</c> otherwise.</returns>
/// <param name="autofillFieldMetadataCollection">Autofill field metadata collection.</param>
/// <param name="datasetBuilder">Dataset builder.</param>
public bool ApplyToFields(AutofillFieldMetadataCollection autofillFieldMetadataCollection, Dataset.Builder datasetBuilder)
{
bool setValueAtLeastOnce = false;
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection.GetFieldsForHint(hint))
{
FilledAutofillField filledAutofillField;
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
continue;
}
var autofillId = autofillFieldMetadata.AutofillId;
var autofillType = autofillFieldMetadata.AutofillType;
switch (autofillType)
{
case AutofillType.List:
var listValue = autofillFieldMetadata.GetAutofillOptionIndex(filledAutofillField.TextValue);
if (listValue != -1)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForList(listValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Date:
var dateValue = filledAutofillField.DateValue;
datasetBuilder.SetValue(autofillId, AutofillValue.ForDate((long)dateValue));
setValueAtLeastOnce = true;
break;
case AutofillType.Text:
var textValue = filledAutofillField.TextValue;
if (textValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForText(textValue));
setValueAtLeastOnce = true;
}
break;
case AutofillType.Toggle:
var toggleValue = filledAutofillField.ToggleValue;
if (toggleValue != null)
{
datasetBuilder.SetValue(autofillId, AutofillValue.ForToggle(toggleValue.Value));
setValueAtLeastOnce = true;
}
break;
default:
Log.Warn(CommonUtil.Tag, "Invalid autofill type - " + autofillType);
break;
}
}
}
/*
if (!setValueAtLeastOnce)
{
Kp2aLog.Log("No value set. Hint keys : " + string.Join(",", HintMap.Keys));
foreach (string hint in autofillFieldMetadataCollection.AllAutofillCanonicalHints)
{
Kp2aLog.Log("No value set. Hint = " + hint);
foreach (AutofillFieldMetadata autofillFieldMetadata in autofillFieldMetadataCollection
.GetFieldsForHint(hint))
{
Kp2aLog.Log("No value set. fieldForHint = " + autofillFieldMetadata.AutofillId.ToString());
FilledAutofillField filledAutofillField;
if (!HintMap.TryGetValue(hint, out filledAutofillField) || (filledAutofillField == null))
{
Kp2aLog.Log("No value set. Hint map does not contain value, " +
(filledAutofillField == null));
continue;
}
Kp2aLog.Log("autofill type=" + autofillFieldMetadata.AutofillType);
}
}
}*/
return setValueAtLeastOnce;
}
/// <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;
}
}
}

View File

@@ -2,126 +2,5 @@
namespace keepass2android.services.AutofillBase.model
{
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;
}
Log.Warn(CommonUtil.Tag, "Invalid W3C type hint: " + hint);
return false;
}
}
}

View File

@@ -241,6 +241,7 @@ namespace keepass2android
.SetWhen(Java.Lang.JavaSystem.CurrentTimeMillis())
.SetTicker(entryName + ": " + desc)
.SetVisibility((int)Android.App.NotificationVisibility.Secret)
.SetAutoCancel(true)
.SetContentIntent(pending);
if (entryIcon != null)
builder.SetLargeIcon(entryIcon);
@@ -951,7 +952,9 @@ namespace keepass2android
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, username, false);
}
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CopyPassword))
{
@@ -960,7 +963,7 @@ namespace keepass2android
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, password, true);
}
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CopyTotp))
{
@@ -969,7 +972,7 @@ namespace keepass2android
{
CopyToClipboardService.CopyValueToClipboardWithTimeout(context, totp, true);
}
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
CloseNotificationDrawer(context);
}
else if (action.Equals(Intents.CheckKeyboard))
{
@@ -977,6 +980,11 @@ namespace keepass2android
}
}
private static void CloseNotificationDrawer(Context context)
{
if ((int)Build.VERSION.SdkInt < 31) //sending this intent is no longer allowed since Android 31
context.SendBroadcast(new Intent(Intent.ActionCloseSystemDialogs)); //close notification drawer
}
};
}

View File

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

View File

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