diff --git a/.gitignore b/.gitignore
index d5148da27..1e0f2d44b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -148,6 +148,7 @@ publish/
# NuGet Packages
*.nupkg
+!**/Xamarin.AndroidX.Credentials.1.0.0.nupkg
# The packages folder can be ignored because of Package Restore
**/packages/*
# except build/, which is used as an MSBuild target.
diff --git a/lib/android/Xamarin.AndroidX.Credentials/Xamarin.AndroidX.Credentials.1.0.0.nupkg b/lib/android/Xamarin.AndroidX.Credentials/Xamarin.AndroidX.Credentials.1.0.0.nupkg
new file mode 100644
index 000000000..0a8cb5a88
Binary files /dev/null and b/lib/android/Xamarin.AndroidX.Credentials/Xamarin.AndroidX.Credentials.1.0.0.nupkg differ
diff --git a/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.dll b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.dll
new file mode 100644
index 000000000..22434e53d
Binary files /dev/null and b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.dll differ
diff --git a/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.xml b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.xml
new file mode 100644
index 000000000..f7f8c6217
--- /dev/null
+++ b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/Xamarin.AndroidX.Credentials.xml
@@ -0,0 +1,8 @@
+
+
+
+ Xamarin.AndroidX.Credentials
+
+
+
+
diff --git a/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/credentials-1.2.0.aar b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/credentials-1.2.0.aar
new file mode 100644
index 000000000..a3fdcfb52
Binary files /dev/null and b/lib/android/Xamarin.AndroidX.Credentials/net8.0-android/credentials-1.2.0.aar differ
diff --git a/nuget.config b/nuget.config
index 16502a7fd..e18f80b9a 100644
--- a/nuget.config
+++ b/nuget.config
@@ -2,5 +2,6 @@
+
\ No newline at end of file
diff --git a/src/App/App.csproj b/src/App/App.csproj
index e8b607055..3c2bc8c12 100644
--- a/src/App/App.csproj
+++ b/src/App/App.csproj
@@ -117,10 +117,13 @@
+
+
+
@@ -256,8 +259,13 @@
+
+
+
+
+
diff --git a/src/App/Platforms/Android/Autofill/CredentialHelpers.cs b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs
new file mode 100644
index 000000000..6153a8aa4
--- /dev/null
+++ b/src/App/Platforms/Android/Autofill/CredentialHelpers.cs
@@ -0,0 +1,303 @@
+using System.Text.Json.Nodes;
+using Android.App;
+using Android.Content;
+using Android.OS;
+using AndroidX.Credentials;
+using AndroidX.Credentials.Exceptions;
+using AndroidX.Credentials.Provider;
+using AndroidX.Credentials.WebAuthn;
+using Bit.App.Abstractions;
+using Bit.App.Droid.Utilities;
+using Bit.Core.Abstractions;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+using Bit.Core.Utilities.Fido2;
+using Bit.Core.Utilities.Fido2.Extensions;
+using Bit.Droid;
+using Org.Json;
+using Activity = Android.App.Activity;
+using Drawables = Android.Graphics.Drawables;
+
+namespace Bit.App.Platforms.Android.Autofill
+{
+ public static class CredentialHelpers
+ {
+ public static async Task> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
+ BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
+ {
+ var passkeyEntries = new List();
+ var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);
+
+ var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve();
+ var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);
+
+ // We need to change the request code for every pending intent on mapping the credential so the extras are not overriten by the last
+ // credential entry created.
+ int requestCodeAddition = 0;
+ passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode + requestCodeAddition++) as CredentialEntry).ToList();
+
+ return passkeyEntries;
+ }
+
+ private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction, int requestCode)
+ {
+ var credDataBundle = new Bundle();
+ credDataBundle.PutByteArray(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialIdIntentExtra, credential.Id);
+
+ var intent = new Intent(context, typeof(Bit.Droid.Autofill.CredentialProviderSelectionActivity))
+ .SetAction(Bit.Droid.Autofill.CredentialProviderService.GetFido2IntentAction).SetPackage(Constants.PACKAGE_NAME);
+ intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
+ intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialProviderCipherId, credential.CipherId);
+ intent.PutExtra(Bit.Core.Utilities.Fido2.CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, hasVaultBeenUnlockedInThisTransaction);
+ var pendingIntent = PendingIntent.GetActivity(context, requestCode, intent,
+ PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
+
+ return new PublicKeyCredentialEntry.Builder(
+ context,
+ credential.UserName ?? "No username",
+ pendingIntent,
+ option)
+ .SetDisplayName(credential.UserName ?? "No username")
+ .SetIcon(Drawables.Icon.CreateWithResource(context, Microsoft.Maui.Resource.Drawable.icon))
+ .Build();
+ }
+
+ private static PublicKeyCredentialCreationOptions GetPublicKeyCredentialCreationOptionsFromJson(string json)
+ {
+ var request = new PublicKeyCredentialCreationOptions(json);
+ var jsonObj = new JSONObject(json);
+ var authenticatorSelection = jsonObj.GetJSONObject("authenticatorSelection");
+ request.AuthenticatorSelection = new AndroidX.Credentials.WebAuthn.AuthenticatorSelectionCriteria(
+ authenticatorSelection.OptString("authenticatorAttachment", "platform"),
+ authenticatorSelection.OptString("residentKey", null),
+ authenticatorSelection.OptBoolean("requireResidentKey", false),
+ authenticatorSelection.OptString("userVerification", "preferred"));
+
+ return request;
+ }
+
+ public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
+ {
+ var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
+
+ if (callingRequest is null)
+ {
+ if (ServiceContainer.TryResolve(out var deviceActionService))
+ {
+ await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, string.Empty, AppResources.Ok);
+ }
+ FailAndFinish();
+ return;
+ }
+
+ var credentialCreationOptions = GetPublicKeyCredentialCreationOptionsFromJson(callingRequest.RequestJson);
+ var origin = await ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, credentialCreationOptions.Rp.Id);
+
+ if (origin is null)
+ {
+ if (ServiceContainer.TryResolve(out var deviceActionService))
+ {
+ await deviceActionService.DisplayAlertAsync(AppResources.ErrorCreatingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
+ }
+ FailAndFinish();
+ return;
+ }
+
+ var rp = new Core.Utilities.Fido2.PublicKeyCredentialRpEntity()
+ {
+ Id = credentialCreationOptions.Rp.Id,
+ Name = credentialCreationOptions.Rp.Name
+ };
+
+ var user = new Core.Utilities.Fido2.PublicKeyCredentialUserEntity()
+ {
+ Id = credentialCreationOptions.User.GetId(),
+ Name = credentialCreationOptions.User.Name,
+ DisplayName = credentialCreationOptions.User.DisplayName
+ };
+
+ var pubKeyCredParams = new List();
+ foreach (var pubKeyCredParam in credentialCreationOptions.PubKeyCredParams)
+ {
+ pubKeyCredParams.Add(new Core.Utilities.Fido2.PublicKeyCredentialParameters() { Alg = Convert.ToInt32(pubKeyCredParam.Alg), Type = pubKeyCredParam.Type });
+ }
+
+ var excludeCredentials = new List();
+ foreach (var excludeCred in credentialCreationOptions.ExcludeCredentials)
+ {
+ excludeCredentials.Add(new Core.Utilities.Fido2.PublicKeyCredentialDescriptor() { Id = excludeCred.GetId(), Type = excludeCred.Type, Transports = excludeCred.Transports.ToArray() });
+ }
+
+ var authenticatorSelection = new Core.Utilities.Fido2.AuthenticatorSelectionCriteria()
+ {
+ UserVerification = credentialCreationOptions.AuthenticatorSelection.UserVerification,
+ ResidentKey = credentialCreationOptions.AuthenticatorSelection.ResidentKey,
+ RequireResidentKey = credentialCreationOptions.AuthenticatorSelection.RequireResidentKey
+ };
+
+ var timeout = Convert.ToInt32(credentialCreationOptions.Timeout);
+
+ var credentialCreateParams = new Fido2ClientCreateCredentialParams()
+ {
+ Challenge = credentialCreationOptions.GetChallenge(),
+ Origin = origin,
+ PubKeyCredParams = pubKeyCredParams.ToArray(),
+ Rp = rp,
+ User = user,
+ Timeout = timeout,
+ Attestation = credentialCreationOptions.Attestation,
+ AuthenticatorSelection = authenticatorSelection,
+ ExcludeCredentials = excludeCredentials.ToArray(),
+ Extensions = MapExtensionsFromJson(credentialCreationOptions),
+ SameOriginWithAncestors = true
+ };
+
+ var credentialExtraCreateParams = new Fido2ExtraCreateCredentialParams
+ (
+ callingRequest.GetClientDataHash(),
+ getRequest.CallingAppInfo?.PackageName
+ );
+
+ var fido2MediatorService = ServiceContainer.Resolve();
+ var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams, credentialExtraCreateParams);
+ if (clientCreateCredentialResult == null)
+ {
+ FailAndFinish();
+ return;
+ }
+
+ var transportsArray = new JSONArray();
+ if (clientCreateCredentialResult.Transports != null)
+ {
+ foreach (var transport in clientCreateCredentialResult.Transports)
+ {
+ transportsArray.Put(transport);
+ }
+ }
+
+ var responseInnerAndroidJson = new JSONObject();
+ if (clientCreateCredentialResult.ClientDataJSON != null)
+ {
+ responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.ClientDataJSON));
+ }
+ responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AuthData));
+ responseInnerAndroidJson.Put("attestationObject", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.AttestationObject));
+ responseInnerAndroidJson.Put("transports", transportsArray);
+ responseInnerAndroidJson.Put("publicKeyAlgorithm", clientCreateCredentialResult.PublicKeyAlgorithm);
+ responseInnerAndroidJson.Put("publicKey", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.PublicKey));
+
+ var rootAndroidJson = new JSONObject();
+ rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
+ rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(clientCreateCredentialResult.CredentialId));
+ rootAndroidJson.Put("authenticatorAttachment", "platform");
+ rootAndroidJson.Put("type", "public-key");
+ rootAndroidJson.Put("clientExtensionResults", MapExtensionsToJson(clientCreateCredentialResult.Extensions));
+ rootAndroidJson.Put("response", responseInnerAndroidJson);
+
+ var result = new Intent();
+ var publicKeyResponse = new CreatePublicKeyCredentialResponse(rootAndroidJson.ToString());
+ PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
+
+ activity.SetResult(Result.Ok, result);
+ activity.Finish();
+
+ void FailAndFinish()
+ {
+ var result = new Intent();
+ PendingIntentHandler.SetCreateCredentialException(result, new CreateCredentialUnknownException());
+
+ activity.SetResult(Result.Ok, result);
+ activity.Finish();
+ }
+ }
+
+ private static Fido2CreateCredentialExtensionsParams MapExtensionsFromJson(PublicKeyCredentialCreationOptions options)
+ {
+ if (options == null || !options.Json.Has("extensions"))
+ {
+ return null;
+ }
+
+ var extensions = options.Json.GetJSONObject("extensions");
+ return new Fido2CreateCredentialExtensionsParams
+ {
+ CredProps = extensions.Has("credProps") && extensions.GetBoolean("credProps")
+ };
+ }
+
+ private static JSONObject MapExtensionsToJson(Fido2CreateCredentialExtensionsResult extensions)
+ {
+ if (extensions == null)
+ {
+ return null;
+ }
+
+ var extensionsJson = new JSONObject();
+ if (extensions.CredProps != null)
+ {
+ var credPropsJson = new JSONObject();
+ credPropsJson.Put("rk", extensions.CredProps.Rk);
+ extensionsJson.Put("credProps", credPropsJson);
+ }
+
+ return extensionsJson;
+ }
+
+ public static async Task LoadFido2PriviligedAllowedListAsync()
+ {
+ try
+ {
+ using var stream = await FileSystem.OpenAppPackageFileAsync("fido2_priviliged_allow_list.json");
+ using var reader = new StreamReader(stream);
+
+ return reader.ReadToEnd();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public static async Task ValidateCallingAppInfoAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
+ {
+ if (callingAppInfo.Origin is null)
+ {
+ return await ValidateAssetLinksAndGetOriginAsync(callingAppInfo, rpId);
+ }
+
+ var priviligedAllowedList = await LoadFido2PriviligedAllowedListAsync();
+ if (priviligedAllowedList is null)
+ {
+ throw new InvalidOperationException("Could not load Fido2 priviliged allowed list");
+ }
+
+ try
+ {
+ return callingAppInfo.GetOrigin(priviligedAllowedList);
+ }
+ catch (Java.Lang.IllegalStateException)
+ {
+ return null; // not priviliged
+ }
+ catch (Java.Lang.IllegalArgumentException)
+ {
+ return null; // wrong list format
+ }
+ }
+
+ private static async Task ValidateAssetLinksAndGetOriginAsync(CallingAppInfo callingAppInfo, string rpId)
+ {
+ if (!ServiceContainer.TryResolve(out var assetLinksService))
+ {
+ throw new InvalidOperationException("Can't resolve IAssetLinksService");
+ }
+
+ var normalizedFingerprint = callingAppInfo.GetLatestCertificationFingerprint();
+
+ var isValid = await assetLinksService.ValidateAssetLinksAsync(rpId, callingAppInfo.PackageName, normalizedFingerprint);
+
+ return isValid ? callingAppInfo.GetAndroidOrigin() : null;
+ }
+ }
+}
diff --git a/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs
new file mode 100644
index 000000000..7d406939a
--- /dev/null
+++ b/src/App/Platforms/Android/Autofill/CredentialProviderSelectionActivity.cs
@@ -0,0 +1,172 @@
+using Android.App;
+using Android.Content;
+using Android.Content.PM;
+using Android.OS;
+using AndroidX.Credentials;
+using AndroidX.Credentials.Provider;
+using AndroidX.Credentials.WebAuthn;
+using Bit.App.Droid.Utilities;
+using Bit.App.Abstractions;
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Utilities.Fido2;
+using Bit.Core.Services;
+using Bit.App.Platforms.Android.Autofill;
+using AndroidX.Credentials.Exceptions;
+using Org.Json;
+
+namespace Bit.Droid.Autofill
+{
+ [Activity(
+ NoHistory = true,
+ LaunchMode = LaunchMode.SingleTop)]
+ public class CredentialProviderSelectionActivity : MauiAppCompatActivity
+ {
+ private LazyResolve _fido2MediatorService = new LazyResolve();
+ private LazyResolve _fido2GetAssertionUserInterface = new LazyResolve();
+ private LazyResolve _vaultTimeoutService = new LazyResolve();
+ private LazyResolve _stateService = new LazyResolve();
+ private LazyResolve _cipherService = new LazyResolve();
+ private LazyResolve _userVerificationMediatorService = new LazyResolve();
+ private LazyResolve _deviceActionService = new LazyResolve();
+
+ protected override void OnCreate(Bundle bundle)
+ {
+ Intent?.Validate();
+ base.OnCreate(bundle);
+
+ var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
+ if (string.IsNullOrEmpty(cipherId))
+ {
+ Finish();
+ return;
+ }
+
+ GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
+ }
+
+ //Used to avoid crash on MAUI when doing back
+ public override void OnBackPressed()
+ {
+ Finish();
+ }
+
+ private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
+ {
+ string RpId = string.Empty;
+ try
+ {
+ var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
+
+ if (getRequest is null)
+ {
+ FailAndFinish();
+ return;
+ }
+
+ var credentialOption = getRequest.CredentialOptions.FirstOrDefault();
+ var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
+
+ var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
+ RpId = requestOptions.RpId;
+
+ var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
+ var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
+ var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
+
+ var packageName = getRequest.CallingAppInfo.PackageName;
+
+ var origin = await CredentialHelpers.ValidateCallingAppInfoAndGetOriginAsync(getRequest.CallingAppInfo, RpId);
+ if (origin is null)
+ {
+ await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
+ FailAndFinish();
+ return;
+ }
+
+ _fido2GetAssertionUserInterface.Value.Init(
+ cipherId,
+ false,
+ () => hasVaultBeenUnlockedInThisTransaction,
+ RpId
+ );
+
+ var clientAssertParams = new Fido2ClientAssertCredentialParams
+ {
+ Challenge = requestOptions.GetChallenge(),
+ RpId = RpId,
+ AllowCredentials = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
+ Origin = origin,
+ SameOriginWithAncestors = true,
+ UserVerification = requestOptions.UserVerification
+ };
+
+ var extraAssertParams = new Fido2ExtraAssertCredentialParams
+ (
+ getRequest.CallingAppInfo.Origin != null ? credentialPublic.GetClientDataHash() : null,
+ packageName
+ );
+
+ var assertResult = await _fido2MediatorService.Value.AssertCredentialAsync(clientAssertParams, extraAssertParams);
+
+ var result = new Intent();
+
+ var responseInnerAndroidJson = new JSONObject();
+ if (assertResult.ClientDataJSON != null)
+ {
+ responseInnerAndroidJson.Put("clientDataJSON", CoreHelpers.Base64UrlEncode(assertResult.ClientDataJSON));
+ }
+ responseInnerAndroidJson.Put("authenticatorData", CoreHelpers.Base64UrlEncode(assertResult.AuthenticatorData));
+ responseInnerAndroidJson.Put("signature", CoreHelpers.Base64UrlEncode(assertResult.Signature));
+ responseInnerAndroidJson.Put("userHandle", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.UserHandle));
+
+ var rootAndroidJson = new JSONObject();
+ rootAndroidJson.Put("id", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
+ rootAndroidJson.Put("rawId", CoreHelpers.Base64UrlEncode(assertResult.SelectedCredential.Id));
+ rootAndroidJson.Put("authenticatorAttachment", "platform");
+ rootAndroidJson.Put("type", "public-key");
+ rootAndroidJson.Put("clientExtensionResults", new JSONObject());
+ rootAndroidJson.Put("response", responseInnerAndroidJson);
+
+ var json = rootAndroidJson.ToString();
+
+ var cred = new PublicKeyCredential(json);
+ var credResponse = new GetCredentialResponse(cred);
+ PendingIntentHandler.SetGetCredentialResponse(result, credResponse);
+
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ SetResult(Result.Ok, result);
+ Finish();
+ });
+ }
+ catch (NotAllowedError)
+ {
+ await MainThread.InvokeOnMainThreadAsync(async () =>
+ {
+ await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
+ FailAndFinish();
+ });
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
+ await MainThread.InvokeOnMainThreadAsync(async () =>
+ {
+ await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
+ FailAndFinish();
+ });
+ }
+ }
+
+ private void FailAndFinish()
+ {
+ var result = new Intent();
+ PendingIntentHandler.SetGetCredentialException(result, new GetCredentialUnknownException());
+
+ SetResult(Result.Ok, result);
+ Finish();
+ }
+ }
+}
diff --git a/src/App/Platforms/Android/Autofill/CredentialProviderService.cs b/src/App/Platforms/Android/Autofill/CredentialProviderService.cs
new file mode 100644
index 000000000..9be7377f8
--- /dev/null
+++ b/src/App/Platforms/Android/Autofill/CredentialProviderService.cs
@@ -0,0 +1,168 @@
+using Android;
+using Android.App;
+using Android.Content;
+using Android.OS;
+using Android.Runtime;
+using AndroidX.Credentials.Provider;
+using Bit.Core.Abstractions;
+using Bit.Core.Utilities;
+using AndroidX.Credentials.Exceptions;
+using Bit.App.Droid.Utilities;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Droid.Autofill
+{
+ [Service(Permission = Manifest.Permission.BindCredentialProviderService, Label = "Bitwarden", Exported = true)]
+ [IntentFilter(new string[] { "android.service.credentials.CredentialProviderService" })]
+ [MetaData("android.credentials.provider", Resource = "@xml/provider")]
+ [Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")]
+ public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService
+ {
+ public const string GetFido2IntentAction = "PACKAGE_NAME.GET_PASSKEY";
+ public const string CreateFido2IntentAction = "PACKAGE_NAME.CREATE_PASSKEY";
+ public const int UniqueGetRequestCode = 94556023;
+ public const int UniqueCreateRequestCode = 94556024;
+
+ private readonly LazyResolve _vaultTimeoutService = new LazyResolve();
+ private readonly LazyResolve _stateService = new LazyResolve();
+ private readonly LazyResolve _logger = new LazyResolve();
+
+ public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
+ CancellationSignal cancellationSignal, IOutcomeReceiver callback)
+ {
+ try
+ {
+ var response = await ProcessCreateCredentialsRequestAsync(request);
+ if (response != null)
+ {
+ await MainThread.InvokeOnMainThreadAsync(() => callback.OnResult(response));
+ return;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Value.Exception(ex);
+ }
+ MainThread.BeginInvokeOnMainThread(() => callback.OnError(AppResources.ErrorCreatingPasskey));
+ }
+
+ public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
+ CancellationSignal cancellationSignal, IOutcomeReceiver callback)
+ {
+ try
+ {
+ await _vaultTimeoutService.Value.CheckVaultTimeoutAsync();
+ var locked = await _vaultTimeoutService.Value.IsLockedAsync();
+ if (!locked)
+ {
+ var response = await ProcessGetCredentialsRequestAsync(request);
+ callback.OnResult(response);
+ return;
+ }
+
+ var intent = new Intent(ApplicationContext, typeof(MainActivity));
+ intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialGet);
+ var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueGetRequestCode, intent,
+ AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
+
+ var unlockAction = new AuthenticationAction(AppResources.Unlock, pendingIntent);
+
+ var unlockResponse = new BeginGetCredentialResponse.Builder()
+ .SetAuthenticationActions(new List() { unlockAction } )
+ .Build();
+ callback.OnResult(unlockResponse);
+ }
+ catch (GetCredentialException e)
+ {
+ _logger.Value.Exception(e);
+ callback.OnError(e.ErrorMessage ?? AppResources.ErrorReadingPasskey);
+ }
+ catch (Exception e)
+ {
+ _logger.Value.Exception(e);
+ callback.OnError(AppResources.ErrorReadingPasskey);
+ }
+ }
+
+ private async Task ProcessCreateCredentialsRequestAsync(
+ BeginCreateCredentialRequest request)
+ {
+ if (request == null) { return null; }
+
+ if (request is BeginCreatePasswordCredentialRequest beginCreatePasswordCredentialRequest)
+ {
+ //This flow can be used if Password flow needs to be implemented
+ throw new NotImplementedException();
+ //return HandleCreatePasswordQuery(beginCreatePasswordCredentialRequest);
+ }
+ else if (request is BeginCreatePublicKeyCredentialRequest beginCreatePublicKeyCredentialRequest)
+ {
+ return await HandleCreatePasskeyQueryAsync(beginCreatePublicKeyCredentialRequest);
+ }
+
+ return null;
+ }
+
+ private async Task HandleCreatePasskeyQueryAsync(BeginCreatePublicKeyCredentialRequest optionRequest)
+ {
+ var intent = new Intent(ApplicationContext, typeof(MainActivity));
+ intent.PutExtra(CredentialProviderConstants.Fido2CredentialAction, CredentialProviderConstants.Fido2CredentialCreate);
+ var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueCreateRequestCode, intent,
+ AndroidHelpers.AddPendingIntentMutabilityFlag(PendingIntentFlags.UpdateCurrent, true));
+
+ var userEmail = await GetSafeActiveAccountEmailAsync();
+
+ var createEntryBuilder = new CreateEntry.Builder(userEmail ?? AppResources.Bitwarden, pendingIntent)
+ .SetDescription(userEmail != null
+ ? string.Format(AppResources.YourPasskeyWillBeSavedToYourBitwardenVaultForX, userEmail)
+ : AppResources.YourPasskeyWillBeSavedToYourBitwardenVault)
+ .Build();
+
+ var createCredentialResponse = new BeginCreateCredentialResponse.Builder()
+ .AddCreateEntry(createEntryBuilder);
+
+ return createCredentialResponse.Build();
+ }
+
+ private async Task ProcessGetCredentialsRequestAsync(
+ BeginGetCredentialRequest request)
+ {
+ var credentialEntries = new List();
+
+ foreach (var option in request.BeginGetCredentialOptions.OfType())
+ {
+ credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, ApplicationContext, false));
+ }
+
+ if (!credentialEntries.Any())
+ {
+ return new BeginGetCredentialResponse();
+ }
+
+ return new BeginGetCredentialResponse.Builder()
+ .SetCredentialEntries(credentialEntries)
+ .Build();
+ }
+
+ public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request,
+ CancellationSignal cancellationSignal, IOutcomeReceiver callback)
+ {
+ callback.OnResult(null);
+ }
+
+ private async Task GetSafeActiveAccountEmailAsync()
+ {
+ try
+ {
+ return await _stateService.Value.GetEmailAsync();
+ }
+ catch (Exception ex)
+ {
+ // if it throws to get the user's email then we log and continue showing a more generic message
+ _logger.Value.Exception(ex);
+ return null;
+ }
+ }
+ }
+}
diff --git a/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs b/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs
new file mode 100644
index 000000000..9d1e503ec
--- /dev/null
+++ b/src/App/Platforms/Android/Autofill/Fido2GetAssertionUserInterface.cs
@@ -0,0 +1,77 @@
+using Bit.Core.Abstractions;
+using Bit.Core.Services;
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.App.Platforms.Android.Autofill
+{
+ public interface IFido2AndroidGetAssertionUserInterface : IFido2GetAssertionUserInterface
+ {
+ void Init(string cipherId,
+ bool userVerified,
+ Func hasVaultBeenUnlockedInThisTransaction,
+ string rpId);
+ }
+
+ public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface
+ {
+ private readonly IStateService _stateService;
+ private readonly IVaultTimeoutService _vaultTimeoutService;
+ private readonly ICipherService _cipherService;
+ private readonly IUserVerificationMediatorService _userVerificationMediatorService;
+
+ public Fido2GetAssertionUserInterface(IStateService stateService,
+ IVaultTimeoutService vaultTimeoutService,
+ ICipherService cipherService,
+ IUserVerificationMediatorService userVerificationMediatorService)
+ {
+ _stateService = stateService;
+ _vaultTimeoutService = vaultTimeoutService;
+ _cipherService = cipherService;
+ _userVerificationMediatorService = userVerificationMediatorService;
+ }
+
+ public void Init(string cipherId,
+ bool userVerified,
+ Func hasVaultBeenUnlockedInThisTransaction,
+ string rpId)
+ {
+ Init(cipherId,
+ userVerified,
+ EnsureAuthenAndVaultUnlockedAsync,
+ hasVaultBeenUnlockedInThisTransaction,
+ (cipherId, userVerificationPreference) => VerifyUserAsync(cipherId, userVerificationPreference, rpId, hasVaultBeenUnlockedInThisTransaction()));
+ }
+
+ public async Task EnsureAuthenAndVaultUnlockedAsync()
+ {
+ if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
+ {
+ // this should never happen but just in case.
+ throw new InvalidOperationException("Not authed or vault locked");
+ }
+ }
+
+ private async Task VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
+ {
+ try
+ {
+ var encrypted = await _cipherService.GetAsync(selectedCipherId);
+ var cipher = await encrypted.DecryptAsync();
+
+ var userVerification = await _userVerificationMediatorService.VerifyUserForFido2Async(
+ new Fido2UserVerificationOptions(
+ cipher?.Reprompt == Core.Enums.CipherRepromptType.Password,
+ userVerificationPreference,
+ vaultUnlockedDuringThisTransaction,
+ rpId)
+ );
+ return !userVerification.IsCancelled && userVerification.Result;
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
+ return false;
+ }
+ }
+ }
+}
diff --git a/src/App/Platforms/Android/Autofill/Fido2MakeCredentialUserInterface.cs b/src/App/Platforms/Android/Autofill/Fido2MakeCredentialUserInterface.cs
new file mode 100644
index 000000000..f68ecaf7b
--- /dev/null
+++ b/src/App/Platforms/Android/Autofill/Fido2MakeCredentialUserInterface.cs
@@ -0,0 +1,202 @@
+using Bit.App.Abstractions;
+using Bit.Core.Abstractions;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.App.Platforms.Android.Autofill
+{
+ public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialConfirmationUserInterface
+ {
+ private readonly IStateService _stateService;
+ private readonly IVaultTimeoutService _vaultTimeoutService;
+ private readonly ICipherService _cipherService;
+ private readonly IUserVerificationMediatorService _userVerificationMediatorService;
+ private readonly IDeviceActionService _deviceActionService;
+ private readonly IPlatformUtilsService _platformUtilsService;
+ private LazyResolve _messagingService = new LazyResolve();
+
+ private TaskCompletionSource<(string cipherId, bool? userVerified)> _confirmCredentialTcs;
+ private TaskCompletionSource _unlockVaultTcs;
+ private Fido2UserVerificationOptions? _currentDefaultUserVerificationOptions;
+ private Func _checkHasVaultBeenUnlockedInThisTransaction;
+
+ public Fido2MakeCredentialUserInterface(IStateService stateService,
+ IVaultTimeoutService vaultTimeoutService,
+ ICipherService cipherService,
+ IUserVerificationMediatorService userVerificationMediatorService,
+ IDeviceActionService deviceActionService,
+ IPlatformUtilsService platformUtilsService)
+ {
+ _stateService = stateService;
+ _vaultTimeoutService = vaultTimeoutService;
+ _cipherService = cipherService;
+ _userVerificationMediatorService = userVerificationMediatorService;
+ _deviceActionService = deviceActionService;
+ _platformUtilsService = platformUtilsService;
+ }
+
+ public bool HasVaultBeenUnlockedInThisTransaction => _checkHasVaultBeenUnlockedInThisTransaction?.Invoke() == true;
+
+ public bool IsConfirmingNewCredential => _confirmCredentialTcs?.Task != null && !_confirmCredentialTcs.Task.IsCompleted;
+ public bool IsWaitingUnlockVault => _unlockVaultTcs?.Task != null && !_unlockVaultTcs.Task.IsCompleted;
+
+ public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
+ {
+ _confirmCredentialTcs?.TrySetCanceled();
+ _confirmCredentialTcs = null;
+ _confirmCredentialTcs = new TaskCompletionSource<(string cipherId, bool? userVerified)>();
+
+ _currentDefaultUserVerificationOptions = new Fido2UserVerificationOptions(false, confirmNewCredentialParams.UserVerificationPreference, HasVaultBeenUnlockedInThisTransaction, confirmNewCredentialParams.RpId);
+
+ _messagingService.Value.Send(Bit.Core.Constants.CredentialNavigateToAutofillCipherMessageCommand, confirmNewCredentialParams);
+
+ var (cipherId, isUserVerified) = await _confirmCredentialTcs.Task;
+
+ var verified = isUserVerified;
+ if (verified is null)
+ {
+ var userVerification = await VerifyUserAsync(cipherId, confirmNewCredentialParams.UserVerificationPreference, confirmNewCredentialParams.RpId);
+ // TODO: If cancelled then let the user choose another cipher.
+ // I think this can be done by showing a message to the uesr and recursive calling of this method ConfirmNewCredentialAsync
+ verified = !userVerification.IsCancelled && userVerification.Result;
+ }
+
+ if (cipherId is null)
+ {
+ return await CreateNewLoginForFido2CredentialAsync(confirmNewCredentialParams, verified.Value);
+ }
+
+ return (cipherId, verified.Value);
+ }
+
+ private async Task<(string CipherId, bool UserVerified)> CreateNewLoginForFido2CredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams, bool userVerified)
+ {
+ if (!userVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions
+ (
+ false,
+ confirmNewCredentialParams.UserVerificationPreference,
+ true,
+ confirmNewCredentialParams.RpId
+ )))
+ {
+ return (null, false);
+ }
+
+ try
+ {
+ await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
+
+ var cipherId = await _cipherService.CreateNewLoginForPasskeyAsync(confirmNewCredentialParams);
+
+ await _deviceActionService.HideLoadingAsync();
+
+ return (cipherId, userVerified);
+ }
+ catch
+ {
+ await _deviceActionService.HideLoadingAsync();
+ throw;
+ }
+ }
+
+ public async Task EnsureUnlockedVaultAsync()
+ {
+ if (!await _stateService.IsAuthenticatedAsync()
+ ||
+ await _vaultTimeoutService.IsLoggedOutByTimeoutAsync()
+ ||
+ await _vaultTimeoutService.ShouldLogOutByTimeoutAsync())
+ {
+ await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.HomeLogin);
+ return;
+ }
+
+ if (!await _vaultTimeoutService.IsLockedAsync())
+ {
+ return;
+ }
+
+ await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.Lock);
+ }
+
+ private async Task NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget navTarget)
+ {
+ _unlockVaultTcs?.TrySetCanceled();
+ _unlockVaultTcs = new TaskCompletionSource();
+
+ _messagingService.Value.Send(Bit.Core.Constants.NavigateToMessageCommand, navTarget);
+
+ await _unlockVaultTcs.Task;
+ }
+
+ public Task InformExcludedCredentialAsync(string[] existingCipherIds)
+ {
+ // TODO: Show excluded credential to the user in some screen.
+ return Task.FromResult(true);
+ }
+
+ public void SetCheckHasVaultBeenUnlockedInThisTransaction(Func checkHasVaultBeenUnlockedInThisTransaction)
+ {
+ _checkHasVaultBeenUnlockedInThisTransaction = checkHasVaultBeenUnlockedInThisTransaction;
+ }
+
+ public void Confirm(string cipherId, bool? userVerified) => _confirmCredentialTcs?.TrySetResult((cipherId, userVerified));
+ public void ConfirmVaultUnlocked() => _unlockVaultTcs?.TrySetResult(true);
+
+ public async Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified)
+ {
+ if (alreadyHasFido2Credential
+ &&
+ !await _platformUtilsService.ShowDialogAsync(
+ AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
+ AppResources.OverwritePasskey,
+ AppResources.Yes,
+ AppResources.No))
+ {
+ return;
+ }
+
+ Confirm(cipherId, userVerified);
+ }
+
+ public void Cancel() => _confirmCredentialTcs?.TrySetCanceled();
+
+ public void OnConfirmationException(Exception ex) => _confirmCredentialTcs?.TrySetException(ex);
+
+ private async Task> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId)
+ {
+ try
+ {
+ if (selectedCipherId is null && userVerificationPreference == Fido2UserVerificationPreference.Discouraged)
+ {
+ return new CancellableResult(false);
+ }
+
+ var shouldCheckMasterPasswordReprompt = false;
+ if (selectedCipherId != null)
+ {
+ var encrypted = await _cipherService.GetAsync(selectedCipherId);
+ var cipher = await encrypted.DecryptAsync();
+ shouldCheckMasterPasswordReprompt = cipher?.Reprompt == Core.Enums.CipherRepromptType.Password;
+ }
+
+ return await _userVerificationMediatorService.VerifyUserForFido2Async(
+ new Fido2UserVerificationOptions(
+ shouldCheckMasterPasswordReprompt,
+ userVerificationPreference,
+ HasVaultBeenUnlockedInThisTransaction,
+ rpId)
+ );
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
+ return new CancellableResult(false);
+ }
+ }
+
+ public Fido2UserVerificationOptions? GetCurrentUserVerificationOptions() => _currentDefaultUserVerificationOptions;
+ }
+}
diff --git a/src/App/Platforms/Android/MainActivity.cs b/src/App/Platforms/Android/MainActivity.cs
index fe852fc7e..441b8d309 100644
--- a/src/App/Platforms/Android/MainActivity.cs
+++ b/src/App/Platforms/Android/MainActivity.cs
@@ -24,6 +24,7 @@ using Bit.App.Droid.Utilities;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using FileProvider = AndroidX.Core.Content.FileProvider;
+using Bit.Core.Utilities.Fido2;
namespace Bit.Droid
{
@@ -167,6 +168,13 @@ namespace Bit.Droid
base.OnNewIntent(intent);
try
{
+ if (intent?.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction) == CredentialProviderConstants.Fido2CredentialCreate
+ &&
+ _appOptions != null)
+ {
+ _appOptions.HasUnlockedInThisTransaction = false;
+ }
+
if (intent?.GetStringExtra("uri") is string uri)
{
_messagingService.Send(App.App.POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE);
@@ -325,12 +333,15 @@ namespace Bit.Droid
private AppOptions GetOptions()
{
+ var fido2CredentialAction = Intent.GetStringExtra(CredentialProviderConstants.Fido2CredentialAction);
var options = new AppOptions
{
Uri = Intent.GetStringExtra("uri") ?? Intent.GetStringExtra(AutofillConstants.AutofillFrameworkUri),
MyVaultTile = Intent.GetBooleanExtra("myVaultTile", false),
GeneratorTile = Intent.GetBooleanExtra("generatorTile", false),
FromAutofillFramework = Intent.GetBooleanExtra(AutofillConstants.AutofillFramework, false),
+ Fido2CredentialAction = fido2CredentialAction,
+ FromFido2Framework = !string.IsNullOrWhiteSpace(fido2CredentialAction),
CreateSend = GetCreateSendRequest(Intent)
};
var fillType = Intent.GetIntExtra(AutofillConstants.AutofillFrameworkFillType, 0);
diff --git a/src/App/Platforms/Android/MainApplication.cs b/src/App/Platforms/Android/MainApplication.cs
index f23363b42..69b4b01e6 100644
--- a/src/App/Platforms/Android/MainApplication.cs
+++ b/src/App/Platforms/Android/MainApplication.cs
@@ -20,7 +20,10 @@ using Bit.App.Utilities;
using Bit.App.Pages;
using Bit.App.Utilities.AccountManagement;
using Bit.App.Controls;
+using Bit.App.Platforms.Android.Autofill;
using Bit.Core.Enums;
+using Bit.Core.Services.UserVerification;
+
#if !FDROID
using Android.Gms.Security;
#endif
@@ -85,6 +88,57 @@ namespace Bit.Droid
ServiceContainer.Resolve(),
ServiceContainer.Resolve());
ServiceContainer.Register("accountsManager", accountsManager);
+
+ var userPinService = new UserPinService(
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve());
+ ServiceContainer.Register(userPinService);
+
+ var userVerificationMediatorService = new UserVerificationMediatorService(
+ ServiceContainer.Resolve("platformUtilsService"),
+ ServiceContainer.Resolve("passwordRepromptService"),
+ userPinService,
+ deviceActionService,
+ ServiceContainer.Resolve());
+ ServiceContainer.Register(userVerificationMediatorService);
+
+ var fido2AuthenticatorService = new Fido2AuthenticatorService(
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ userVerificationMediatorService);
+ ServiceContainer.Register(fido2AuthenticatorService);
+
+ var fido2GetAssertionUserInterface = new Fido2GetAssertionUserInterface(
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve());
+ ServiceContainer.Register(fido2GetAssertionUserInterface);
+
+ var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve());
+ ServiceContainer.Register(fido2MakeCredentialUserInterface);
+
+ var fido2ClientService = new Fido2ClientService(
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ ServiceContainer.Resolve(),
+ fido2GetAssertionUserInterface,
+ fido2MakeCredentialUserInterface);
+ ServiceContainer.Register(fido2ClientService);
+
+ ServiceContainer.Register(new Fido2MediatorService(
+ fido2AuthenticatorService,
+ fido2ClientService,
+ ServiceContainer.Resolve()));
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
@@ -160,7 +214,6 @@ namespace Bit.Droid
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
var biometricService = new BiometricService(stateService, cryptoService);
- var userPinService = new UserPinService(stateService, cryptoService);
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
ServiceContainer.Register(preferencesStorage);
@@ -184,7 +237,6 @@ namespace Bit.Droid
ServiceContainer.Register("cryptoService", cryptoService);
ServiceContainer.Register("passwordRepromptService", passwordRepromptService);
ServiceContainer.Register("avatarImageSourcePool", new AvatarImageSourcePool());
- ServiceContainer.Register(userPinService);
// Push
#if FDROID
diff --git a/src/App/Platforms/Android/Resources/xml/provider.xml b/src/App/Platforms/Android/Resources/xml/provider.xml
new file mode 100644
index 000000000..eb901638a
--- /dev/null
+++ b/src/App/Platforms/Android/Resources/xml/provider.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/App/Platforms/Android/Services/AutofillHandler.cs b/src/App/Platforms/Android/Services/AutofillHandler.cs
index 12429b841..6b629446c 100644
--- a/src/App/Platforms/Android/Services/AutofillHandler.cs
+++ b/src/App/Platforms/Android/Services/AutofillHandler.cs
@@ -1,11 +1,11 @@
-using System.Linq;
-using System.Threading.Tasks;
-using Android.App;
+using Android.App;
using Android.App.Assist;
using Android.Content;
+using Android.Credentials;
using Android.OS;
using Android.Provider;
using Android.Views.Autofill;
+using Bit.App.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
@@ -37,6 +37,42 @@ namespace Bit.Droid.Services
_eventService = eventService;
}
+ public bool CredentialProviderServiceEnabled()
+ {
+ if (Build.VERSION.SdkInt < BuildVersionCodes.UpsideDownCake)
+ {
+ return false;
+ }
+
+ try
+ {
+ var activity = (MainActivity)Platform.CurrentActivity;
+ if (activity == null)
+ {
+ return false;
+ }
+
+ var credManager = activity.GetSystemService(Java.Lang.Class.FromType(typeof(CredentialManager))) as CredentialManager;
+ if (credManager == null)
+ {
+ return false;
+ }
+
+ var credentialProviderServiceComponentName = new ComponentName(activity, Java.Lang.Class.FromType(typeof(CredentialProviderService)));
+ return credManager.IsEnabledCredentialProviderService(credentialProviderServiceComponentName);
+ }
+ catch (Java.Lang.NullPointerException)
+ {
+ // CredentialManager API is not working fully and may return a NullPointerException even if the CredentialProviderService is working and enabled
+ // Info Here: https://developer.android.com/reference/android/credentials/CredentialManager#isEnabledCredentialProviderService(android.content.ComponentName)
+ return false;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
public bool AutofillServiceEnabled()
{
if (Build.VERSION.SdkInt < BuildVersionCodes.O)
@@ -163,7 +199,17 @@ namespace Bit.Droid.Services
return Accessibility.AccessibilityHelpers.OverlayPermitted();
}
-
+ public void DisableCredentialProviderService()
+ {
+ try
+ {
+ // We should try to find a way to programmatically disable the provider service when the API allows for it.
+ // For now we'll take the user to Credential Settings so they can manually disable it
+ var deviceActionService = ServiceContainer.Resolve();
+ deviceActionService.OpenCredentialProviderSettings();
+ }
+ catch { }
+ }
public void DisableAutofillService()
{
diff --git a/src/App/Platforms/Android/Services/DeviceActionService.cs b/src/App/Platforms/Android/Services/DeviceActionService.cs
index 0a3498114..82174220f 100644
--- a/src/App/Platforms/Android/Services/DeviceActionService.cs
+++ b/src/App/Platforms/Android/Services/DeviceActionService.cs
@@ -1,6 +1,4 @@
-using System;
-using System.Threading.Tasks;
-using Android.App;
+using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.Nfc;
@@ -11,16 +9,20 @@ using Android.Text.Method;
using Android.Views;
using Android.Views.InputMethods;
using Android.Widget;
+using AndroidX.Credentials;
using Bit.App.Abstractions;
using Bit.Core.Resources.Localization;
using Bit.App.Utilities;
using Bit.App.Utilities.Prompts;
using Bit.Core.Abstractions;
-using Bit.Core.Enums;
using Bit.App.Droid.Utilities;
+using Bit.App.Models;
+using Bit.Droid.Autofill;
using Microsoft.Maui.Controls.Compatibility.Platform.Android;
using Resource = Bit.Core.Resource;
using Application = Android.App.Application;
+using Bit.Core.Services;
+using Bit.Core.Utilities.Fido2;
namespace Bit.Droid.Services
{
@@ -203,7 +205,7 @@ namespace Bit.Droid.Services
string text = null, string okButtonText = null, string cancelButtonText = null,
bool numericKeyboard = false, bool autofocus = true, bool password = false)
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return Task.FromResult(null);
@@ -260,7 +262,7 @@ namespace Bit.Droid.Services
public Task DisplayValidatablePromptAsync(ValidatablePromptConfig config)
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return Task.FromResult(null);
@@ -337,7 +339,7 @@ namespace Bit.Droid.Services
public void RateApp()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try
{
var rateIntent = RateIntentForUrl("market://details", activity);
@@ -370,14 +372,14 @@ namespace Bit.Droid.Services
public bool SupportsNfc()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var manager = activity.GetSystemService(Context.NfcService) as NfcManager;
return manager.DefaultAdapter?.IsEnabled ?? false;
}
public bool SupportsCamera()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
return activity.PackageManager.HasSystemFeature(PackageManager.FeatureCamera);
}
@@ -393,7 +395,7 @@ namespace Bit.Droid.Services
public Task DisplayAlertAsync(string title, string message, string cancel, params string[] buttons)
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return Task.FromResult(null);
@@ -474,7 +476,7 @@ namespace Bit.Droid.Services
public void OpenAccessibilityOverlayPermissionSettings()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try
{
var intent = new Intent(Settings.ActionManageOverlayPermission);
@@ -501,11 +503,32 @@ namespace Bit.Droid.Services
}
}
+ public void OpenCredentialProviderSettings()
+ {
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ try
+ {
+ var pendingIntent = ICredentialManager.Create(activity).CreateSettingsPendingIntent();
+ pendingIntent.Send();
+ }
+ catch (ActivityNotFoundException)
+ {
+ var alertBuilder = new AlertDialog.Builder(activity);
+ alertBuilder.SetMessage(AppResources.BitwardenCredentialProviderGoToSettings);
+ alertBuilder.SetCancelable(true);
+ alertBuilder.SetPositiveButton(AppResources.Ok, (sender, args) =>
+ {
+ (sender as AlertDialog)?.Cancel();
+ });
+ alertBuilder.Create().Show();
+ }
+ }
+
public void OpenAccessibilitySettings()
{
try
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var intent = new Intent(Settings.ActionAccessibilitySettings);
activity.StartActivity(intent);
}
@@ -514,7 +537,7 @@ namespace Bit.Droid.Services
public void OpenAutofillSettings()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try
{
var intent = new Intent(Settings.ActionRequestSetAutofillService);
@@ -542,10 +565,92 @@ namespace Bit.Droid.Services
// ref: https://developer.android.com/reference/android/os/SystemClock#elapsedRealtime()
return SystemClock.ElapsedRealtime();
}
+
+ public async Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
+ {
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ if (activity == null || string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
+ {
+ return;
+ }
+
+ if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialGet)
+ {
+ await ExecuteFido2GetCredentialAsync(appOptions);
+ }
+ else if (appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
+ {
+ await ExecuteFido2CreateCredentialAsync();
+ }
+
+ // Clear CredentialAction and FromFido2Framework values to avoid erratic behaviors in subsequent navigation/flows
+ // For Fido2CredentialGet these are no longer needed as a new Activity will be initiated.
+ // For Fido2CredentialCreate the app will rely on IFido2MakeCredentialConfirmationUserInterface.IsConfirmingNewCredential
+ appOptions.Fido2CredentialAction = null;
+ appOptions.FromFido2Framework = false;
+ }
+
+ private async Task ExecuteFido2GetCredentialAsync(AppOptions appOptions)
+ {
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ if (activity == null)
+ {
+ return;
+ }
+
+ try
+ {
+ var request = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveBeginGetCredentialRequest(activity.Intent);
+ var response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse();;
+ var credentialEntries = new List();
+ foreach (var option in request.BeginGetCredentialOptions.OfType())
+ {
+ credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, activity, appOptions.HasUnlockedInThisTransaction));
+ }
+
+ if (credentialEntries.Any())
+ {
+ response = new AndroidX.Credentials.Provider.BeginGetCredentialResponse.Builder()
+ .SetCredentialEntries(credentialEntries)
+ .Build();
+ }
+
+ var result = new Android.Content.Intent();
+ AndroidX.Credentials.Provider.PendingIntentHandler.SetBeginGetCredentialResponse(result, response);
+ activity.SetResult(Result.Ok, result);
+ activity.Finish();
+ }
+ catch (Exception ex)
+ {
+ Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
+
+ activity.SetResult(Result.Canceled);
+ activity.Finish();
+ }
+ }
+
+ private async Task ExecuteFido2CreateCredentialAsync()
+ {
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ if (activity == null) { return; }
+
+ try
+ {
+ var getRequest = AndroidX.Credentials.Provider.PendingIntentHandler.RetrieveProviderCreateCredentialRequest(activity.Intent);
+ await Bit.App.Platforms.Android.Autofill.CredentialHelpers.CreateCipherPasskeyAsync(getRequest, activity);
+ }
+ catch (Exception ex)
+ {
+ Bit.Core.Services.LoggerHelper.LogEvenIfCantBeResolved(ex);
+
+ activity.SetResult(Result.Canceled);
+ activity.Finish();
+ }
+ }
public void CloseMainApp()
{
- var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return;
@@ -559,6 +664,8 @@ namespace Bit.Droid.Services
return true;
}
+ public bool SupportsCredentialProviderService() => Build.VERSION.SdkInt >= BuildVersionCodes.UpsideDownCake;
+
public bool SupportsAutofillServices() => Build.VERSION.SdkInt >= BuildVersionCodes.O;
public bool SupportsInlineAutofill() => Build.VERSION.SdkInt >= BuildVersionCodes.R;
@@ -584,7 +691,7 @@ namespace Bit.Droid.Services
public float GetSystemFontSizeScale()
{
- var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity as MainActivity;
+ var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
return activity?.Resources?.Configuration?.FontScale ?? 1;
}
diff --git a/src/App/Platforms/Android/Utilities/CallingAppInfoExtensions.cs b/src/App/Platforms/Android/Utilities/CallingAppInfoExtensions.cs
new file mode 100644
index 000000000..1d5cd8654
--- /dev/null
+++ b/src/App/Platforms/Android/Utilities/CallingAppInfoExtensions.cs
@@ -0,0 +1,37 @@
+using Android.OS;
+using AndroidX.Credentials.Provider;
+using Bit.Core.Utilities;
+using Java.Security;
+
+namespace Bit.App.Droid.Utilities
+{
+ public static class CallingAppInfoExtensions
+ {
+ public static string GetAndroidOrigin(this CallingAppInfo callingAppInfo)
+ {
+ if (Build.VERSION.SdkInt < BuildVersionCodes.P || callingAppInfo?.SigningInfo?.GetApkContentsSigners().Any() != true)
+ {
+ return null;
+ }
+
+ var cert = callingAppInfo.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
+ var md = MessageDigest.GetInstance("SHA-256");
+ var certHash = md.Digest(cert);
+ return $"android:apk-key-hash:{CoreHelpers.Base64UrlEncode(certHash)}";
+ }
+
+ public static string GetLatestCertificationFingerprint(this CallingAppInfo callingAppInfo)
+ {
+ if (callingAppInfo.SigningInfo.HasMultipleSigners)
+ {
+ return null;
+ }
+
+ var signature = callingAppInfo.SigningInfo.GetSigningCertificateHistory()[0].ToByteArray();
+ var md = MessageDigest.GetInstance("SHA-256");
+ var digestedSignature = md.Digest(signature);
+ var normalizedFingerprint = string.Join(":", digestedSignature.Select(b => b.ToString("X2")));
+ return normalizedFingerprint;
+ }
+ }
+}
diff --git a/src/App/Platforms/iOS/AppDelegate.cs b/src/App/Platforms/iOS/AppDelegate.cs
index b9f648805..066eb54bc 100644
--- a/src/App/Platforms/iOS/AppDelegate.cs
+++ b/src/App/Platforms/iOS/AppDelegate.cs
@@ -88,7 +88,7 @@ namespace Bit.iOS
Core.Constants.AutofillNeedsIdentityReplacementKey);
if (needsAutofillReplacement.GetValueOrDefault())
{
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
else if (message.Command == "showAppExtension")
@@ -102,7 +102,7 @@ namespace Bit.iOS
var success = value as bool?;
if (success.GetValueOrDefault() && _deviceActionService.SystemMajorVersion() >= 12)
{
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
}
@@ -114,22 +114,21 @@ namespace Bit.iOS
return;
}
- if (await ASHelpers.IdentitiesCanIncremental())
+ if (await ASHelpers.IdentitiesSupportIncrementalAsync())
{
var cipherId = message.Data as string;
if (message.Command == "addedCipher" && !string.IsNullOrWhiteSpace(cipherId))
{
- var identity = await ASHelpers.GetCipherIdentityAsync(cipherId);
+ var identity = await ASHelpers.GetCipherPasswordIdentityAsync(cipherId);
if (identity == null)
{
return;
}
- await ASCredentialIdentityStore.SharedStore?.SaveCredentialIdentitiesAsync(
- new ASPasswordCredentialIdentity[] { identity });
+ await ASCredentialIdentityStoreExtensions.SaveCredentialIdentitiesAsync(identity);
return;
}
}
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
else if (message.Command == "deletedCipher" || message.Command == "softDeletedCipher")
{
@@ -138,28 +137,27 @@ namespace Bit.iOS
return;
}
- if (await ASHelpers.IdentitiesCanIncremental())
+ if (await ASHelpers.IdentitiesSupportIncrementalAsync())
{
- var identity = ASHelpers.ToCredentialIdentity(
+ var identity = ASHelpers.ToPasswordCredentialIdentity(
message.Data as Bit.Core.Models.View.CipherView);
if (identity == null)
{
return;
}
- await ASCredentialIdentityStore.SharedStore?.RemoveCredentialIdentitiesAsync(
- new ASPasswordCredentialIdentity[] { identity });
+ await ASCredentialIdentityStoreExtensions.RemoveCredentialIdentitiesAsync(identity);
return;
}
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
else if (message.Command == "logout" && UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
- await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
+ await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
}
else if ((message.Command == "softDeletedCipher" || message.Command == "restoredCipher")
&& UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
else if (message.Command == AppHelpers.VAULT_TIMEOUT_ACTION_CHANGED_MESSAGE_COMMAND)
{
@@ -168,12 +166,12 @@ namespace Bit.iOS
{
if (UIDevice.CurrentDevice.CheckSystemVersion(12, 0))
{
- await ASCredentialIdentityStore.SharedStore?.RemoveAllCredentialIdentitiesAsync();
+ await ASCredentialIdentityStore.SharedStore.RemoveAllCredentialIdentitiesAsync();
}
}
else
{
- await ASHelpers.ReplaceAllIdentities();
+ await ASHelpers.ReplaceAllIdentitiesAsync();
}
}
}
diff --git a/src/App/Resources/Raw/fido2_priviliged_allow_list.json b/src/App/Resources/Raw/fido2_priviliged_allow_list.json
new file mode 100644
index 000000000..dd23740e0
--- /dev/null
+++ b/src/App/Resources/Raw/fido2_priviliged_allow_list.json
@@ -0,0 +1,481 @@
+{
+ "apps": [
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.android.chrome",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "DA:63:3D:34:B6:9E:63:AE:21:03:B4:9D:53:CE:05:2F:C5:F7:F3:C5:3A:AB:94:FD:C2:A2:08:BD:FD:14:24:9C"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.dev",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "90:44:EE:5F:EE:4B:BC:5E:21:DD:44:66:54:31:C4:EB:1F:1F:71:A3:27:16:A0:BC:92:7B:CB:B3:92:33:CA:BF"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:7A:12:23:01:9A:A3:9D:9E:A0:E3:43:6A:B7:C0:89:6B:FB:4F:B6:79:F4:DE:5F:E7:C2:3F:32:6C:8F:99:4A"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.chrome.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "20:19:DF:A1:FB:23:EF:BF:70:C5:BC:D1:44:3C:5B:EA:B0:4F:3F:2F:F4:36:6E:9A:C1:E3:45:76:39:A2:4C:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.chromium.chrome",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
+ },
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.google.android.apps.chrome",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fennec_webauthndebug",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "BD:AE:82:02:80:D2:AF:B7:74:94:EF:22:58:AA:78:A9:AE:A1:36:41:7E:8B:C2:3D:C9:87:75:2E:6F:48:E8:48"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.firefox",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.firefox_beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "A7:8B:62:A5:16:5B:44:94:B2:FE:AD:9E:76:A2:80:D2:2D:93:7F:EE:62:51:AE:CE:59:94:46:B2:EA:31:9B:04"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.focus",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "62:03:A4:73:BE:36:D6:4E:E3:7F:87:FA:50:0E:DB:C7:9E:AB:93:06:10:AB:9B:9F:A4:CA:7D:5C:1F:1B:4F:FC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.fennec_aurora",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "BC:04:88:83:8D:06:F4:CA:6B:F3:23:86:DA:AB:0D:D8:EB:CF:3E:77:30:78:74:59:F6:2F:B3:CD:14:A1:BA:AA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "org.mozilla.rocket",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "86:3A:46:F0:97:39:32:B7:D0:19:9B:54:91:12:74:1C:2D:27:31:AC:72:EA:11:B7:52:3A:A9:0A:11:BF:56:91"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.dev",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "01:E1:99:97:10:A8:2C:27:49:B4:D5:0C:44:5D:C8:5D:67:0B:61:36:08:9D:0A:76:6A:73:82:7C:82:A1:EA:C9"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.rolling",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.microsoft.emmx.local",
+ "signatures": [
+ {
+ "build": "userdebug",
+ "cert_fingerprint_sha256": "32:A2:FC:74:D7:31:10:58:59:E5:A8:5D:F1:6D:95:F1:02:D8:5B:22:09:9B:80:64:C5:D8:91:5C:61:DA:D1:E0"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser_beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.brave.browser_nightly",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "9C:2D:B7:05:13:51:5F:DB:FB:BC:58:5B:3E:DF:3D:71:23:D4:DC:67:C9:4F:FD:30:63:61:C1:D7:9B:BF:18:AC"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "app.vanadium.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C6:AD:B8:B8:3C:6D:4C:17:D2:92:AF:DE:56:FD:48:8A:51:D3:16:FF:8F:2C:11:C5:41:02:23:BF:F8:A7:DB:B3"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser.snapshot",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.vivaldi.browser.sopranos",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "E8:A7:85:44:65:5B:A8:C0:98:17:F7:32:76:8F:56:89:B1:66:2E:C4:B2:BC:5A:0B:C0:EC:13:8D:33:CA:3D:1E"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.citrix.Receiver",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "3D:D1:12:67:10:69:AB:36:4E:F9:BE:73:9A:B7:B5:EE:15:E1:CD:E9:D8:75:7B:1B:F0:64:F5:0C:55:68:9A:49"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "CE:B2:23:D7:77:09:F2:B6:BC:0B:3A:78:36:F5:A5:AF:4C:E1:D3:55:F4:A7:28:86:F7:9D:F8:0D:C9:D6:12:2E"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AA:D0:D4:57:E6:33:C3:78:25:77:30:5B:C1:B2:D9:E3:81:41:C7:21:DF:0D:AA:6E:29:07:2F:C4:1D:34:F0:AB"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.android.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C9:00:9D:01:EB:F9:F5:D0:30:2B:C7:1B:2F:E9:AA:9A:47:A4:32:BB:A1:73:08:A3:11:1B:75:D7:B2:14:90:25"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.sec.android.app.sbrowser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.sec.android.app.sbrowser.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "C8:A2:E9:BC:CF:59:7C:2F:B6:DC:66:BE:E2:93:FC:13:F2:FC:47:EC:77:BC:6B:2B:0D:52:C1:1F:51:19:2A:B8"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "34:DF:0E:7A:9F:1C:F1:89:2E:45:C0:56:B4:97:3C:D8:1C:CF:14:8A:40:50:D1:1A:EA:4A:C5:A6:5F:90:0A:42"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.google.android.gms",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "7C:E8:3C:1B:71:F3:D5:72:FE:D0:4C:8D:40:C5:CB:10:FF:75:E6:D8:7D:9D:F6:FB:D5:3F:04:68:C2:90:50:53"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "D2:2C:C5:00:29:9F:B2:28:73:A0:1A:01:0D:E1:C8:2F:BE:4D:06:11:19:B9:48:14:DD:30:1D:AB:50:CB:76:78"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "F0:FD:6C:5B:41:0F:25:CB:25:C3:B5:33:46:C8:97:2F:AE:30:F8:EE:74:11:DF:91:04:80:AD:6B:2D:60:DB:83"
+ },
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "19:75:B2:F1:71:77:BC:89:A5:DF:F3:1F:9E:64:A6:CA:E2:81:A5:3D:C1:D1:D5:9B:1D:14:7F:E1:C8:2A:FA:00"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.beta",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.alpha",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.corp",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "AC:A4:05:DE:D8:B2:5C:B2:E8:C6:DA:69:42:5D:2B:43:07:D0:87:C1:27:6F:C0:6A:D5:94:27:31:CC:C5:1D:BA"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.canary",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
+ }
+ ]
+ }
+ },
+ {
+ "type": "android",
+ "info": {
+ "package_name": "com.yandex.browser.broteam",
+ "signatures": [
+ {
+ "build": "release",
+ "cert_fingerprint_sha256": "1D:A9:CB:AE:2D:CC:C6:A5:8D:6C:94:7B:E9:4C:DB:B7:33:D6:5D:A4:D1:77:0F:A1:4A:53:64:CB:4A:28:EB:49"
+ }
+ ]
+ }
+ }
+ ]
+ }
+
\ No newline at end of file
diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs
index b60d90267..bbd3b9357 100644
--- a/src/Core/Abstractions/IApiService.cs
+++ b/src/Core/Abstractions/IApiService.cs
@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
@@ -100,5 +95,6 @@ namespace Bit.Core.Abstractions
Task GetDevicesExistenceByTypes(DeviceType[] deviceTypes);
Task GetConfigsAsync();
Task GetFastmailAccountIdAsync(string apiKey);
+ Task> GetDigitalAssetLinksForRpAsync(string rpId);
}
}
diff --git a/src/Core/Abstractions/IAssetLinksService.cs b/src/Core/Abstractions/IAssetLinksService.cs
new file mode 100644
index 000000000..c9dc5ca85
--- /dev/null
+++ b/src/Core/Abstractions/IAssetLinksService.cs
@@ -0,0 +1,7 @@
+namespace Bit.Core.Services
+{
+ public interface IAssetLinksService
+ {
+ Task ValidateAssetLinksAsync(string rpId, string packageName, string normalizedFingerprint);
+ }
+}
diff --git a/src/Core/Abstractions/IAutofillHandler.cs b/src/Core/Abstractions/IAutofillHandler.cs
index 84c9489b9..81a8016f8 100644
--- a/src/Core/Abstractions/IAutofillHandler.cs
+++ b/src/Core/Abstractions/IAutofillHandler.cs
@@ -4,6 +4,7 @@ namespace Bit.Core.Abstractions
{
public interface IAutofillHandler
{
+ bool CredentialProviderServiceEnabled();
bool AutofillServicesEnabled();
bool SupportsAutofillService();
void Autofill(CipherView cipher);
@@ -11,6 +12,7 @@ namespace Bit.Core.Abstractions
bool AutofillAccessibilityServiceRunning();
bool AutofillAccessibilityOverlayPermitted();
bool AutofillServiceEnabled();
+ void DisableCredentialProviderService();
void DisableAutofillService();
}
}
diff --git a/src/Core/Abstractions/ICipherService.cs b/src/Core/Abstractions/ICipherService.cs
index 91b93e9ce..59c395b44 100644
--- a/src/Core/Abstractions/ICipherService.cs
+++ b/src/Core/Abstractions/ICipherService.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Domain;
using Bit.Core.Models.View;
@@ -37,6 +34,8 @@ namespace Bit.Core.Abstractions
Task DownloadAndDecryptAttachmentAsync(string cipherId, AttachmentView attachment, string organizationId);
Task SoftDeleteWithServerAsync(string id);
Task RestoreWithServerAsync(string id);
+ Task CreateNewLoginForPasskeyAsync(Fido2ConfirmNewCredentialParams newPasskeyParams);
+ Task CopyTotpCodeIfNeededAsync(CipherView cipher);
Task VerifyOrganizationHasUnassignedItemsAsync();
}
}
diff --git a/src/Core/Abstractions/IConditionedAwaiterManager.cs b/src/Core/Abstractions/IConditionedAwaiterManager.cs
index 6eb4df5dc..3a6a0ec0f 100644
--- a/src/Core/Abstractions/IConditionedAwaiterManager.cs
+++ b/src/Core/Abstractions/IConditionedAwaiterManager.cs
@@ -1,12 +1,10 @@
-using System;
-using System.Threading.Tasks;
-
-namespace Bit.Core.Abstractions
+namespace Bit.Core.Abstractions
{
public enum AwaiterPrecondition
{
EnvironmentUrlsInited,
- AndroidWindowCreated
+ AndroidWindowCreated,
+ AutofillIOSExtensionViewDidAppear
}
public interface IConditionedAwaiterManager
@@ -14,5 +12,6 @@ namespace Bit.Core.Abstractions
Task GetAwaiterForPrecondition(AwaiterPrecondition awaiterPrecondition);
void SetAsCompleted(AwaiterPrecondition awaiterPrecondition);
void SetException(AwaiterPrecondition awaiterPrecondition, Exception ex);
+ void Recreate(AwaiterPrecondition awaiterPrecondition);
}
}
diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs
index 39b6ba6a1..630dc1b96 100644
--- a/src/Core/Abstractions/ICryptoFunctionService.cs
+++ b/src/Core/Abstractions/ICryptoFunctionService.cs
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
+using Bit.Core.Models.Domain;
namespace Bit.Core.Abstractions
{
diff --git a/src/Core/Abstractions/IDeviceActionService.cs b/src/Core/Abstractions/IDeviceActionService.cs
index 3a15fde80..c6188b6b4 100644
--- a/src/Core/Abstractions/IDeviceActionService.cs
+++ b/src/Core/Abstractions/IDeviceActionService.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using Bit.App.Models;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums;
using Bit.Core.Models;
@@ -28,6 +29,7 @@ namespace Bit.App.Abstractions
bool SupportsNfc();
bool SupportsCamera();
bool SupportsFido2();
+ bool SupportsCredentialProviderService();
bool SupportsAutofillServices();
bool SupportsInlineAutofill();
bool SupportsDrawOver();
@@ -36,8 +38,10 @@ namespace Bit.App.Abstractions
void RateApp();
void OpenAccessibilitySettings();
void OpenAccessibilityOverlayPermissionSettings();
+ void OpenCredentialProviderSettings();
void OpenAutofillSettings();
long GetActiveTime();
+ Task ExecuteFido2CredentialActionAsync(AppOptions appOptions);
void CloseMainApp();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();
diff --git a/src/Core/Abstractions/IFido2AuthenticatorService.cs b/src/Core/Abstractions/IFido2AuthenticatorService.cs
new file mode 100644
index 000000000..32ec5c0b8
--- /dev/null
+++ b/src/Core/Abstractions/IFido2AuthenticatorService.cs
@@ -0,0 +1,12 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public interface IFido2AuthenticatorService
+ {
+ Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
+ Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
+ // TODO: Should this return a List? Or maybe IEnumerable?
+ Task SilentCredentialDiscoveryAsync(string rpId);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2ClientService.cs b/src/Core/Abstractions/IFido2ClientService.cs
new file mode 100644
index 000000000..8684ff4e1
--- /dev/null
+++ b/src/Core/Abstractions/IFido2ClientService.cs
@@ -0,0 +1,37 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ ///
+ /// This class represents an abstraction of the WebAuthn Client as described by W3C:
+ /// https://www.w3.org/TR/webauthn-3/#webauthn-client
+ ///
+ /// The WebAuthn Client is an intermediary entity typically implemented in the user agent
+ /// (in whole, or in part). Conceptually, it underlies the Web Authentication API and embodies
+ /// the implementation of the Web Authentication API's operations.
+ ///
+ /// It is responsible for both marshalling the inputs for the underlying authenticator operations,
+ /// and for returning the results of the latter operations to the Web Authentication API's callers.
+ ///
+ public interface IFido2ClientService
+ {
+ ///
+ /// Allows WebAuthn Relying Party scripts to request the creation of a new public key credential source.
+ /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-createCredential
+ ///
+ /// The parameters for the credential creation operation
+ /// Extra parameters for the credential creation operation
+ /// The new credential
+ Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
+
+ ///
+ /// Allows WebAuthn Relying Party scripts to discover and use an existing public key credential, with the user’s consent.
+ /// Relying Party script can optionally specify some criteria to indicate what credential sources are acceptable to it.
+ /// For more information please see: https://www.w3.org/TR/webauthn-3/#sctn-getAssertion
+ ///
+ /// The parameters for the credential assertion operation
+ /// Extra parameters for the credential assertion operation
+ /// The asserted credential
+ Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2GetAssertionUserInterface.cs b/src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
new file mode 100644
index 000000000..b0633a4d3
--- /dev/null
+++ b/src/Core/Abstractions/IFido2GetAssertionUserInterface.cs
@@ -0,0 +1,20 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public struct Fido2GetAssertionUserInterfaceCredential
+ {
+ public string CipherId { get; set; }
+ public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
+ }
+
+ public interface IFido2GetAssertionUserInterface : IFido2UserInterface
+ {
+ ///
+ /// Ask the user to pick a credential from a list of existing credentials.
+ ///
+ /// The credentials that the user can pick from, and if the user must be verified before completing the operation
+ /// The ID of the cipher that contains the credentials the user picked, and if the user was verified before completing the operation
+ Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2MakeCredentialConfirmationUserInterface.cs b/src/Core/Abstractions/IFido2MakeCredentialConfirmationUserInterface.cs
new file mode 100644
index 000000000..e2ec22614
--- /dev/null
+++ b/src/Core/Abstractions/IFido2MakeCredentialConfirmationUserInterface.cs
@@ -0,0 +1,66 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public interface IFido2MakeCredentialConfirmationUserInterface : IFido2MakeCredentialUserInterface
+ {
+ ///
+ /// Call this method after the user chose where to save the new Fido2 credential.
+ ///
+ ///
+ /// Cipher ID where to save the new credential.
+ /// If null a new default passkey cipher item will be created
+ ///
+ ///
+ /// Whether the user has been verified or not.
+ /// If null verification has not taken place yet.
+ ///
+ void Confirm(string cipherId, bool? userVerified);
+
+ ///
+ /// Call this method after the user chose where to save the new Fido2 credential.
+ ///
+ ///
+ /// Cipher ID where to save the new credential.
+ /// If null a new default passkey cipher item will be created
+ ///
+ ///
+ /// If the cipher corresponding to the already has a Fido2 credential.
+ ///
+ ///
+ /// Whether the user has been verified or not.
+ /// If null verification has not taken place yet.
+ ///
+ Task ConfirmAsync(string cipherId, bool alreadyHasFido2Credential, bool? userVerified);
+
+ ///
+ /// Cancels the current flow to make a credential
+ ///
+ void Cancel();
+
+ ///
+ /// Call this if an exception needs to happen on the credential making process
+ ///
+ void OnConfirmationException(Exception ex);
+
+
+ ///
+ /// True if we are already confirming a new credential.
+ ///
+ bool IsConfirmingNewCredential { get; }
+
+ ///
+ /// Call this after the vault was unlocked so that Fido2 credential creation can proceed.
+ ///
+ void ConfirmVaultUnlocked();
+
+ ///
+ /// True if we are waiting for the vault to be unlocked.
+ ///
+ bool IsWaitingUnlockVault { get; }
+
+ Fido2UserVerificationOptions? GetCurrentUserVerificationOptions();
+
+ void SetCheckHasVaultBeenUnlockedInThisTransaction(Func checkHasVaultBeenUnlockedInThisTransaction);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs b/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
new file mode 100644
index 000000000..90fc9f3f7
--- /dev/null
+++ b/src/Core/Abstractions/IFido2MakeCredentialUserInterface.cs
@@ -0,0 +1,44 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public struct Fido2ConfirmNewCredentialParams
+ {
+ ///
+ /// The name of the credential.
+ ///
+ public string CredentialName { get; set; }
+
+ ///
+ /// The name of the user.
+ ///
+ public string UserName { get; set; }
+
+ ///
+ /// The preference to whether or not the user must be verified before completing the operation.
+ ///
+ public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
+
+ ///
+ /// The relying party identifier
+ ///
+ public string RpId { get; set; }
+ }
+
+ public interface IFido2MakeCredentialUserInterface : IFido2UserInterface
+ {
+ ///
+ /// Inform the user that the operation was cancelled because their vault contains excluded credentials.
+ ///
+ /// The IDs of the excluded credentials.
+ /// When user has confirmed the message
+ Task InformExcludedCredentialAsync(string[] existingCipherIds);
+
+ ///
+ /// Ask the user to confirm the creation of a new credential.
+ ///
+ /// The parameters to use when asking the user to confirm the creation of a new credential.
+ /// The ID of the cipher where the new credential should be saved, and if the user was verified before completing the operation
+ Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2MediatorService.cs b/src/Core/Abstractions/IFido2MediatorService.cs
new file mode 100644
index 000000000..a177d1e80
--- /dev/null
+++ b/src/Core/Abstractions/IFido2MediatorService.cs
@@ -0,0 +1,14 @@
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public interface IFido2MediatorService
+ {
+ Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams, Fido2ExtraCreateCredentialParams extraParams);
+ Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams, Fido2ExtraAssertCredentialParams extraParams);
+
+ Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface);
+ Task GetAssertionAsync(Fido2AuthenticatorGetAssertionParams assertionParams, IFido2GetAssertionUserInterface userInterface);
+ Task SilentCredentialDiscoveryAsync(string rpId);
+ }
+}
diff --git a/src/Core/Abstractions/IFido2UserInterface.cs b/src/Core/Abstractions/IFido2UserInterface.cs
new file mode 100644
index 000000000..d2ff9ff50
--- /dev/null
+++ b/src/Core/Abstractions/IFido2UserInterface.cs
@@ -0,0 +1,17 @@
+namespace Bit.Core.Abstractions
+{
+ public interface IFido2UserInterface
+ {
+ ///
+ /// Whether the vault has been unlocked during this transaction
+ ///
+ bool HasVaultBeenUnlockedInThisTransaction { get; }
+
+ ///
+ /// Make sure that the vault is unlocked.
+ /// This should open a window and ask the user to login or unlock the vault if necessary.
+ ///
+ /// When vault has been unlocked.
+ Task EnsureUnlockedVaultAsync();
+ }
+}
diff --git a/src/Core/Abstractions/IPasswordRepromptService.cs b/src/Core/Abstractions/IPasswordRepromptService.cs
index 2490271c2..7660c766f 100644
--- a/src/Core/Abstractions/IPasswordRepromptService.cs
+++ b/src/Core/Abstractions/IPasswordRepromptService.cs
@@ -1,5 +1,4 @@
-using System.Threading.Tasks;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
namespace Bit.App.Abstractions
{
@@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
Task PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
+
+ Task ShouldByPassMasterPasswordRepromptAsync();
}
}
diff --git a/src/Core/Abstractions/IPlatformUtilsService.cs b/src/Core/Abstractions/IPlatformUtilsService.cs
index 2462e29a8..2d09952f6 100644
--- a/src/Core/Abstractions/IPlatformUtilsService.cs
+++ b/src/Core/Abstractions/IPlatformUtilsService.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Threading.Tasks;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
namespace Bit.Core.Abstractions
{
@@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
bool SupportsDuo();
Task SupportsBiometricAsync();
Task IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
- Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false);
+ Task AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
long GetActiveTime();
}
}
diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs
index 2d8391cfa..0afb34da6 100644
--- a/src/Core/Abstractions/IStateService.cs
+++ b/src/Core/Abstractions/IStateService.cs
@@ -186,6 +186,7 @@ namespace Bit.Core.Abstractions
Task GetActiveUserRegionAsync();
Task GetPreAuthRegionAsync();
Task SetPreAuthRegionAsync(BwRegion value);
+ Task ReloadStateAsync();
Task GetShouldCheckOrganizationUnassignedItemsAsync(string userId = null);
Task SetShouldCheckOrganizationUnassignedItemsAsync(bool shouldCheck, string userId = null);
[Obsolete("Use GetPinKeyEncryptedUserKeyAsync instead, left for migration purposes")]
diff --git a/src/Core/Abstractions/IUserPinService.cs b/src/Core/Abstractions/IUserPinService.cs
index 1a8b69d6e..270da5e92 100644
--- a/src/Core/Abstractions/IUserPinService.cs
+++ b/src/Core/Abstractions/IUserPinService.cs
@@ -1,9 +1,12 @@
-using System.Threading.Tasks;
+using Bit.Core.Services;
namespace Bit.Core.Abstractions
{
public interface IUserPinService
{
+ Task IsPinLockEnabledAsync();
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
+ Task VerifyPinAsync(string inputPin);
+ Task VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType);
}
}
diff --git a/src/Core/Abstractions/IUserVerificationMediatorService.cs b/src/Core/Abstractions/IUserVerificationMediatorService.cs
new file mode 100644
index 000000000..2382873da
--- /dev/null
+++ b/src/Core/Abstractions/IUserVerificationMediatorService.cs
@@ -0,0 +1,28 @@
+using Bit.Core.Utilities;
+using Bit.Core.Utilities.Fido2;
+
+namespace Bit.Core.Abstractions
+{
+ public interface IUserVerificationMediatorService
+ {
+ Task> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
+ Task CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
+ Task ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
+ Task ShouldEnforceFido2RequiredUserVerificationAsync(Fido2UserVerificationOptions options);
+ Task> PerformOSUnlockAsync();
+ Task> VerifyPinCodeAsync();
+ Task> VerifyMasterPasswordAsync(bool isMasterPasswordReprompt);
+
+ public struct UVResult
+ {
+ public UVResult(bool canPerform, bool isVerified)
+ {
+ CanPerform = canPerform;
+ IsVerified = isVerified;
+ }
+
+ public bool CanPerform { get; set; }
+ public bool IsVerified { get; set; }
+ }
+ }
+}
diff --git a/src/Core/Abstractions/IUserVerificationService.cs b/src/Core/Abstractions/IUserVerificationService.cs
index 8a20595ed..e6ee1bfe8 100644
--- a/src/Core/Abstractions/IUserVerificationService.cs
+++ b/src/Core/Abstractions/IUserVerificationService.cs
@@ -1,11 +1,11 @@
-using System.Threading.Tasks;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
namespace Bit.Core.Abstractions
{
public interface IUserVerificationService
{
Task VerifyUser(string secret, VerificationType verificationType);
+ Task VerifyMasterPasswordAsync(string masterPassword);
Task HasMasterPasswordAsync(bool checkMasterKeyHash = false);
}
}
diff --git a/src/Core/App.xaml.cs b/src/Core/App.xaml.cs
index 31a3ba8ca..a213cb02b 100644
--- a/src/Core/App.xaml.cs
+++ b/src/Core/App.xaml.cs
@@ -14,6 +14,7 @@ using Bit.Core.Models.Response;
using Bit.Core.Pages;
using Bit.Core.Services;
using Bit.Core.Utilities;
+using Bit.Core.Utilities.Fido2;
[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace Bit.App
@@ -37,6 +38,9 @@ namespace Bit.App
private readonly IPushNotificationService _pushNotificationService;
private readonly IConfigService _configService;
private readonly ILogger _logger;
+#if ANDROID
+ private LazyResolve _fido2MakeCredentialConfirmationUserInterface = new LazyResolve();
+#endif
private static bool _isResumed;
// these variables are static because the app is launching new activities on notification click, creating new instances of App.
@@ -104,7 +108,10 @@ namespace Bit.App
Options.MyVaultTile = appOptions.MyVaultTile;
Options.GeneratorTile = appOptions.GeneratorTile;
Options.FromAutofillFramework = appOptions.FromAutofillFramework;
+ Options.FromFido2Framework = appOptions.FromFido2Framework;
+ Options.Fido2CredentialAction = appOptions.Fido2CredentialAction;
Options.CreateSend = appOptions.CreateSend;
+ Options.HasUnlockedInThisTransaction = appOptions.HasUnlockedInThisTransaction;
}
}
@@ -120,6 +127,15 @@ namespace Bit.App
return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
}
+ //When executing from CredentialProviderSelectionActivity we don't have "Options" so we need to filter "manually"
+ //In the CredentialProviderSelectionActivity we don't need to show any Page, so we just create a "dummy" Window with a NavigationPage to avoid crashing.
+ if (activationState != null
+ && activationState.State.ContainsKey("CREDENTIAL_DATA")
+ && activationState.State.ContainsKey("credentialProviderCipherId"))
+ {
+ return new Window(new NavigationPage()); //No actual page needed. Only used for auto-filling the fields directly (externally)
+ }
+
_isResumed = true;
return new ResumeWindow(new NavigationPage(new AndroidNavigationRedirectPage(Options)));
}
@@ -182,7 +198,6 @@ namespace Bit.App
{
var details = message.Data as DialogDetails;
ArgumentNullException.ThrowIfNull(details);
- ArgumentNullException.ThrowIfNull(MainPage);
var confirmed = true;
var confirmText = string.IsNullOrWhiteSpace(details.ConfirmText) ?
@@ -192,12 +207,14 @@ namespace Bit.App
{
if (!string.IsNullOrWhiteSpace(details.CancelText))
{
+ ArgumentNullException.ThrowIfNull(MainPage);
+
confirmed = await MainPage.DisplayAlert(details.Title, details.Text, confirmText,
details.CancelText);
}
else
{
- await MainPage.DisplayAlert(details.Title, details.Text, confirmText);
+ await _deviceActionService.DisplayAlertAsync(details.Title, details.Text, confirmText);
}
_messagingService.Send("showDialogResolve", new Tuple(details.DialogId, confirmed));
}
@@ -218,17 +235,17 @@ namespace Bit.App
await _accountsManager.NavigateOnAccountChangeAsync();
}
else if (message.Command == POP_ALL_AND_GO_TO_TAB_GENERATOR_MESSAGE ||
- message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
- message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
- message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
- message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
+ message.Command == POP_ALL_AND_GO_TO_TAB_MYVAULT_MESSAGE ||
+ message.Command == POP_ALL_AND_GO_TO_TAB_SEND_MESSAGE ||
+ message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE ||
+ message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{
if (message.Command == DeepLinkContext.NEW_OTP_MESSAGE)
{
Options.OtpData = new OtpData((string)message.Data);
}
- await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
+ await MainThread.InvokeOnMainThreadAsync(ExecuteNavigationAction);
async Task ExecuteNavigationAction()
{
if (MainPage is TabsPage tabsPage)
@@ -239,6 +256,7 @@ namespace Bit.App
{
await tabsPage.Navigation.PopModalAsync(false);
}
+
if (message.Command == POP_ALL_AND_GO_TO_AUTOFILL_CIPHERS_MESSAGE)
{
MainPage = new NavigationPage(new CipherSelectionPage(Options));
@@ -266,6 +284,19 @@ namespace Bit.App
}
}
}
+ else if (message.Command == Constants.CredentialNavigateToAutofillCipherMessageCommand && message.Data is Fido2ConfirmNewCredentialParams createParams)
+ {
+ ArgumentNullException.ThrowIfNull(MainPage);
+ ArgumentNullException.ThrowIfNull(Options);
+ await MainThread.InvokeOnMainThreadAsync(NavigateToCipherSelectionPageAction);
+ void NavigateToCipherSelectionPageAction()
+ {
+ Options.Uri = createParams.RpId;
+ Options.SaveUsername = createParams.UserName;
+ Options.SaveName = createParams.CredentialName;
+ MainPage = new NavigationPage(new CipherSelectionPage(Options));
+ }
+ }
else if (message.Command == "convertAccountToKeyConnector")
{
ArgumentNullException.ThrowIfNull(MainPage);
@@ -304,12 +335,26 @@ namespace Bit.App
|| message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
+#if ANDROID
+ if (message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ _fido2MakeCredentialConfirmationUserInterface.Value.OnConfirmationException(new AccountSwitchedException());
+ }
+#endif
+
lock (_processingLoginRequestLock)
{
// lock doesn't allow for async execution
CheckPasswordlessLoginRequestsAsync().Wait();
}
}
+ else if (message.Command == Constants.NavigateToMessageCommand && message.Data is NavigationTarget navigationTarget)
+ {
+ await MainThread.InvokeOnMainThreadAsync(() =>
+ {
+ Navigate(navigationTarget, null);
+ });
+ }
}
catch (Exception ex)
{
@@ -680,6 +725,15 @@ namespace Bit.App
// If we are in background we add the Navigation Actions to a queue to execute when the app resumes.
// Links: https://github.com/dotnet/maui/issues/11501 and https://bitwarden.atlassian.net/wiki/spaces/NMME/pages/664862722/MainPage+Assignments+not+working+on+Android+on+Background+or+App+resume
#if ANDROID
+ if (_fido2MakeCredentialConfirmationUserInterface != null && _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ // if it's creating passkey
+ // and we have an active pending TaskCompletionSource
+ // then we let the Fido2 Authenticator flow manage the navigation to avoid issues
+ // like duplicated navigation to lock page.
+ return;
+ }
+
if (!_isResumed)
{
_onResumeActions.Enqueue(() => NavigateImpl(navTarget, navParams));
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index a6ca528e4..f24e87016 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -49,6 +49,8 @@ namespace Bit.Core
public const string UnassignedItemsBannerFlag = "unassigned-items-banner";
public const string RegionEnvironment = "regionEnvironment";
public const string DuoCallback = "bitwarden://duo-callback";
+ public const string NavigateToMessageCommand = "navigateTo";
+ public const string CredentialNavigateToAutofillCipherMessageCommand = "credentialNavigateToAutofillCipher";
///
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in
diff --git a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml
index bbfdd41ee..4a8ebaeba 100644
--- a/src/Core/Controls/CipherViewCell/CipherViewCell.xaml
+++ b/src/Core/Controls/CipherViewCell/CipherViewCell.xaml
@@ -50,7 +50,7 @@
HorizontalOptions="Center"
VerticalOptions="Center"
StyleClass="list-icon, list-icon-platform"
- Text="{Binding Cipher, Converter={StaticResource iconGlyphConverter}}"
+ Text="{Binding ., Converter={StaticResource iconGlyphConverter}}"
ShouldUpdateFontSizeDynamicallyForAccesibility="True"
AutomationProperties.IsInAccessibleTree="False"
AutomationId="CipherTypeIcon" />
diff --git a/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml b/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
new file mode 100644
index 000000000..bacbe0360
--- /dev/null
+++ b/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml.cs b/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml.cs
new file mode 100644
index 000000000..edecd21b0
--- /dev/null
+++ b/src/Core/Controls/Settings/ExternalLinkSubtitleItemView.xaml.cs
@@ -0,0 +1,26 @@
+using System.Windows.Input;
+
+namespace Bit.App.Controls
+{
+ public partial class ExternalLinkSubtitleItemView : BaseSettingItemView
+ {
+ public static readonly BindableProperty GoToLinkCommandProperty = BindableProperty.Create(
+ nameof(GoToLinkCommand), typeof(ICommand), typeof(ExternalLinkSubtitleItemView));
+
+ public ExternalLinkSubtitleItemView()
+ {
+ InitializeComponent();
+ }
+
+ public ICommand GoToLinkCommand
+ {
+ get => GetValue(GoToLinkCommandProperty) as ICommand;
+ set => SetValue(GoToLinkCommandProperty, value);
+ }
+
+ void ContentView_Tapped(System.Object sender, System.EventArgs e)
+ {
+ GoToLinkCommand?.Execute(null);
+ }
+ }
+}
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index aa4fba0f9..3f880f911 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -34,6 +34,7 @@
+
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -52,6 +53,7 @@
+
@@ -75,14 +77,14 @@
+
+
+
-
- 168,208
-
@@ -91,6 +93,9 @@
AppResources.Designer.cs
PublicResXFileCodeGenerator
+
+ ExternalLinkSubtitleItemView.xaml
+
AndroidNavigationRedirectPage.xaml
@@ -101,13 +106,25 @@
+
+ MSBuild:Compile
+
MSBuild:Compile
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Core/Models/Api/Fido2CredentialApi.cs b/src/Core/Models/Api/Fido2CredentialApi.cs
index 7953e06a1..672a7ec47 100644
--- a/src/Core/Models/Api/Fido2CredentialApi.cs
+++ b/src/Core/Models/Api/Fido2CredentialApi.cs
@@ -1,5 +1,4 @@
-using System;
-using Bit.Core.Models.Domain;
+using Bit.Core.Models.Domain;
namespace Bit.Core.Models.Api
{
@@ -21,6 +20,7 @@ namespace Bit.Core.Models.Api
RpName = fido2Key.RpName?.EncryptedString;
UserHandle = fido2Key.UserHandle?.EncryptedString;
UserName = fido2Key.UserName?.EncryptedString;
+ UserDisplayName = fido2Key.UserDisplayName?.EncryptedString;
Counter = fido2Key.Counter?.EncryptedString;
CreationDate = fido2Key.CreationDate;
}
@@ -35,6 +35,7 @@ namespace Bit.Core.Models.Api
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
+ public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
}
diff --git a/src/Core/Models/AppOptions.cs b/src/Core/Models/AppOptions.cs
index 4d5939e51..6f99f40b9 100644
--- a/src/Core/Models/AppOptions.cs
+++ b/src/Core/Models/AppOptions.cs
@@ -1,5 +1,4 @@
-using System;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.App.Models
@@ -9,6 +8,8 @@ namespace Bit.App.Models
public bool MyVaultTile { get; set; }
public bool GeneratorTile { get; set; }
public bool FromAutofillFramework { get; set; }
+ public bool FromFido2Framework { get; set; }
+ public string Fido2CredentialAction { get; set; }
public CipherType? FillType { get; set; }
public string Uri { get; set; }
public CipherType? SaveType { get; set; }
@@ -25,6 +26,7 @@ namespace Bit.App.Models
public bool CopyInsteadOfShareAfterSaving { get; set; }
public bool HideAccountSwitcher { get; set; }
public OtpData? OtpData { get; set; }
+ public bool HasUnlockedInThisTransaction { get; set; }
public bool HasJustLoggedInOrUnlocked { get; set; }
public void SetAllFrom(AppOptions o)
@@ -36,6 +38,7 @@ namespace Bit.App.Models
MyVaultTile = o.MyVaultTile;
GeneratorTile = o.GeneratorTile;
FromAutofillFramework = o.FromAutofillFramework;
+ Fido2CredentialAction = o.Fido2CredentialAction;
FillType = o.FillType;
Uri = o.Uri;
SaveType = o.SaveType;
@@ -52,6 +55,7 @@ namespace Bit.App.Models
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
HideAccountSwitcher = o.HideAccountSwitcher;
OtpData = o.OtpData;
+ HasUnlockedInThisTransaction = o.HasUnlockedInThisTransaction;
}
}
}
diff --git a/src/Core/Models/Data/Fido2CredentialData.cs b/src/Core/Models/Data/Fido2CredentialData.cs
index 846df59f4..103d03cbf 100644
--- a/src/Core/Models/Data/Fido2CredentialData.cs
+++ b/src/Core/Models/Data/Fido2CredentialData.cs
@@ -19,6 +19,7 @@ namespace Bit.Core.Models.Data
RpName = apiData.RpName;
UserHandle = apiData.UserHandle;
UserName = apiData.UserName;
+ UserDisplayName = apiData.UserDisplayName;
Counter = apiData.Counter;
CreationDate = apiData.CreationDate;
}
@@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
+ public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
}
diff --git a/src/Core/Models/Domain/Fido2Credential.cs b/src/Core/Models/Domain/Fido2Credential.cs
index 7c6928204..313ff2c8f 100644
--- a/src/Core/Models/Domain/Fido2Credential.cs
+++ b/src/Core/Models/Domain/Fido2Credential.cs
@@ -1,8 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Threading.Tasks;
-using Bit.Core.Models.Data;
+using Bit.Core.Models.Data;
using Bit.Core.Models.View;
namespace Bit.Core.Models.Domain
@@ -21,6 +17,7 @@ namespace Bit.Core.Models.Domain
nameof(RpName),
nameof(UserHandle),
nameof(UserName),
+ nameof(UserDisplayName),
nameof(Counter)
};
@@ -48,6 +45,7 @@ namespace Bit.Core.Models.Domain
public EncString RpName { get; set; }
public EncString UserHandle { get; set; }
public EncString UserName { get; set; }
+ public EncString UserDisplayName { get; set; }
public EncString Counter { get; set; }
public DateTime CreationDate { get; set; }
diff --git a/src/Core/Models/Domain/SymmetricCryptoKey.cs b/src/Core/Models/Domain/SymmetricCryptoKey.cs
index 09d0e9268..7d248a6e7 100644
--- a/src/Core/Models/Domain/SymmetricCryptoKey.cs
+++ b/src/Core/Models/Domain/SymmetricCryptoKey.cs
@@ -1,5 +1,4 @@
-using System;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
namespace Bit.Core.Models.Domain
{
@@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
{
if (key == null)
{
- throw new Exception("Must provide key.");
+ throw new ArgumentKeyNullException(nameof(key));
}
if (encType == null)
@@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
}
else
{
- throw new Exception("Unable to determine encType.");
+ throw new InvalidKeyOperationException("Unable to determine encType.");
}
}
@@ -48,7 +47,7 @@ namespace Bit.Core.Models.Domain
}
else
{
- throw new Exception("Unsupported encType/key length.");
+ throw new InvalidKeyOperationException("Unsupported encType/key length.");
}
if (Key != null)
@@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
public string KeyB64 { get; set; }
public string EncKeyB64 { get; set; }
public string MacKeyB64 { get; set; }
+
+ public class ArgumentKeyNullException : ArgumentNullException
+ {
+ public ArgumentKeyNullException(string paramName) : base(paramName)
+ {
+ }
+
+ public ArgumentKeyNullException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ public ArgumentKeyNullException(string paramName, string message) : base(paramName, message)
+ {
+ }
+ }
+
+ public class InvalidKeyOperationException : InvalidOperationException
+ {
+ public InvalidKeyOperationException(string message) : base(message)
+ {
+ }
+
+ public InvalidKeyOperationException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
}
public class UserKey : SymmetricCryptoKey
diff --git a/src/Core/Models/View/CipherView.cs b/src/Core/Models/View/CipherView.cs
index df8a47eb4..6919e3854 100644
--- a/src/Core/Models/View/CipherView.cs
+++ b/src/Core/Models/View/CipherView.cs
@@ -1,8 +1,7 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
using Bit.Core.Models.Domain;
+using Bit.Core.Resources.Localization;
+using Bit.Core.Utilities;
namespace Bit.Core.Models.View
{
@@ -52,7 +51,7 @@ namespace Bit.Core.Models.View
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public CipherKey Key { get; set; }
-
+
public ItemView Item
{
get
@@ -122,5 +121,14 @@ namespace Bit.Core.Models.View
public bool IsClonable => OrganizationId is null;
public bool HasFido2Credential => Type == CipherType.Login && Login?.HasFido2Credentials == true;
+
+ public string GetMainFido2CredentialUsername()
+ {
+ return Login?.MainFido2Credential?.UserName
+ .FallbackOnNullOrWhiteSpace(Login?.MainFido2Credential?.UserDisplayName)
+ .FallbackOnNullOrWhiteSpace(Login?.Username)
+ .FallbackOnNullOrWhiteSpace(Name)
+ .FallbackOnNullOrWhiteSpace(AppResources.UnknownAccount);
+ }
}
}
diff --git a/src/Core/Models/View/Fido2CredentialView.cs b/src/Core/Models/View/Fido2CredentialView.cs
index 049d82047..89058fc64 100644
--- a/src/Core/Models/View/Fido2CredentialView.cs
+++ b/src/Core/Models/View/Fido2CredentialView.cs
@@ -1,7 +1,7 @@
-using System;
-using System.Collections.Generic;
+using System.Text.Json.Serialization;
using Bit.Core.Enums;
using Bit.Core.Models.Domain;
+using Bit.Core.Utilities;
namespace Bit.Core.Models.View
{
@@ -26,13 +26,42 @@ namespace Bit.Core.Models.View
public string RpName { get; set; }
public string UserHandle { get; set; }
public string UserName { get; set; }
+ public string UserDisplayName { get; set; }
public string Counter { get; set; }
public DateTime CreationDate { get; set; }
+ [JsonIgnore]
+ public int CounterValue {
+ get => int.TryParse(Counter, out var counter) ? counter : 0;
+ set => Counter = value.ToString();
+ }
+
+ [JsonIgnore]
+ public byte[] UserHandleValue {
+ get => UserHandle == null ? null : CoreHelpers.Base64UrlDecode(UserHandle);
+ set => UserHandle = value == null ? null : CoreHelpers.Base64UrlEncode(value);
+ }
+
+ [JsonIgnore]
+ public byte[] KeyBytes {
+ get => KeyValue == null ? null : CoreHelpers.Base64UrlDecode(KeyValue);
+ set => KeyValue = value == null ? null : CoreHelpers.Base64UrlEncode(value);
+ }
+
+ [JsonIgnore]
+ public bool DiscoverableValue {
+ get => bool.TryParse(Discoverable, out var discoverable) && discoverable;
+ set => Discoverable = value.ToString().ToLower();
+ }
+
+ [JsonIgnore]
public override string SubTitle => UserName;
+
public override List> LinkedFieldOptions => new List>();
- public bool IsDiscoverable => !string.IsNullOrWhiteSpace(Discoverable);
+
+ [JsonIgnore]
public bool CanLaunch => !string.IsNullOrEmpty(RpId);
+ [JsonIgnore]
public string LaunchUri => $"https://{RpId}";
public bool IsUniqueAgainst(Fido2CredentialView fido2View) => fido2View?.RpId != RpId || fido2View?.UserName != UserName;
diff --git a/src/Core/Models/View/LoginView.cs b/src/Core/Models/View/LoginView.cs
index 9993c2f11..528026dc6 100644
--- a/src/Core/Models/View/LoginView.cs
+++ b/src/Core/Models/View/LoginView.cs
@@ -1,7 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using Bit.Core.Enums;
+using Bit.Core.Enums;
using Bit.Core.Models.Domain;
namespace Bit.Core.Models.View
diff --git a/src/Core/Pages/Accounts/LockPage.xaml.cs b/src/Core/Pages/Accounts/LockPage.xaml.cs
index 77d499d8a..b90b2c04e 100644
--- a/src/Core/Pages/Accounts/LockPage.xaml.cs
+++ b/src/Core/Pages/Accounts/LockPage.xaml.cs
@@ -168,7 +168,7 @@ namespace Bit.App.Pages
var tasks = Task.Run(async () =>
{
await Task.Delay(50);
- MainThread.BeginInvokeOnMainThread(async () => await _vm.SubmitAsync());
+ _vm.SubmitCommand.Execute(null);
});
}
}
diff --git a/src/Core/Pages/Accounts/LockPageViewModel.cs b/src/Core/Pages/Accounts/LockPageViewModel.cs
index b3bd61015..3838df361 100644
--- a/src/Core/Pages/Accounts/LockPageViewModel.cs
+++ b/src/Core/Pages/Accounts/LockPageViewModel.cs
@@ -1,5 +1,6 @@
using System;
using System.Threading.Tasks;
+using System.Windows.Input;
using Bit.App.Abstractions;
using Bit.App.Controls;
using Bit.Core.Resources.Localization;
@@ -73,7 +74,7 @@ namespace Bit.App.Pages
PageTitle = AppResources.VerifyMasterPassword;
TogglePasswordCommand = new Command(TogglePassword);
- SubmitCommand = new Command(async () => await SubmitAsync());
+ SubmitCommand = CreateDefaultAsyncRelayCommand(SubmitAsync, onException: _logger.Exception, allowsMultipleExecutions: false);
AccountSwitchingOverlayViewModel =
new AccountSwitchingOverlayViewModel(_stateService, _messagingService, _logger)
@@ -157,7 +158,7 @@ namespace Bit.App.Pages
public AccountSwitchingOverlayViewModel AccountSwitchingOverlayViewModel { get; }
- public Command SubmitCommand { get; }
+ public ICommand SubmitCommand { get; }
public Command TogglePasswordCommand { get; }
public string ShowPasswordIcon => ShowPassword ? BitwardenIcons.EyeSlash : BitwardenIcons.Eye;
@@ -233,8 +234,8 @@ namespace Bit.App.Pages
}
BiometricButtonVisible = true;
BiometricButtonText = AppResources.UseBiometricsToUnlock;
- // TODO Xamarin.Forms.Device.RuntimePlatform is no longer supported. Use Microsoft.Maui.Devices.DeviceInfo.Platform instead. For more details see https://learn.microsoft.com/en-us/dotnet/maui/migration/forms-projects#device-changes
- if (Device.RuntimePlatform == Device.iOS)
+
+ if (DeviceInfo.Platform == DevicePlatform.iOS)
{
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
BiometricButtonText = supportsFace ? AppResources.UseFaceIDToUnlock :
@@ -330,6 +331,7 @@ namespace Bit.App.Pages
Pin = string.Empty;
await AppHelpers.ResetInvalidUnlockAttemptsAsync();
await SetUserKeyAndContinueAsync(userKey);
+ await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
}
}
catch (LegacyUserException)
@@ -418,6 +420,7 @@ namespace Bit.App.Pages
var userKey = await _cryptoService.DecryptUserKeyWithMasterKeyAsync(masterKey);
await _cryptoService.SetMasterKeyAsync(masterKey);
await SetUserKeyAndContinueAsync(userKey);
+ await Task.Delay(150); //Workaround Delay to avoid "duplicate" execution of SubmitAsync on Android when invoked from the ReturnCommand
// Re-enable biometrics
if (BiometricEnabled & !BiometricIntegrityValid)
@@ -515,7 +518,7 @@ namespace Bit.App.Pages
var success = await _platformUtilsService.AuthenticateBiometricAsync(null,
PinEnabled ? AppResources.PIN : AppResources.MasterPassword,
() => _secretEntryFocusWeakEventManager.RaiseEvent((int?)null, nameof(FocusSecretEntry)),
- !PinEnabled && !HasMasterPassword);
+ !PinEnabled && !HasMasterPassword) ?? false;
await _stateService.SetBiometricLockedAsync(!success);
if (success)
diff --git a/src/Core/Pages/Settings/AutofillPage.xaml b/src/Core/Pages/Settings/AutofillPage.xaml
index 89635d69d..48cc988aa 100644
--- a/src/Core/Pages/Settings/AutofillPage.xaml
+++ b/src/Core/Pages/Settings/AutofillPage.xaml
@@ -5,7 +5,7 @@
x:Class="Bit.App.Pages.AutofillPage"
xmlns:pages="clr-namespace:Bit.App.Pages"
xmlns:u="clr-namespace:Bit.App.Utilities"
- Title="{u:I18n PasswordAutofill}">
+ Title="{u:I18n SetUpAutofill}">
@@ -15,26 +15,22 @@
-
-
-
-
-
-
-
+
+
DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsCredentialProviderService();
+
public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
public bool UseAutofillServices
@@ -90,6 +92,7 @@ namespace Bit.App.Pages
public AsyncRelayCommand ToggleUseDrawOverCommand { get; private set; }
public AsyncRelayCommand ToggleAskToAddLoginCommand { get; private set; }
public ICommand GoToBlockAutofillUrisCommand { get; private set; }
+ public ICommand GoToCredentialProviderSettingsCommand { get; private set; }
private void InitAndroidCommands()
{
@@ -99,6 +102,7 @@ namespace Bit.App.Pages
ToggleUseDrawOverCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleDrawOver()), () => _inited, allowsMultipleExecutions: false);
ToggleAskToAddLoginCommand = CreateDefaultAsyncRelayCommand(ToggleAskToAddLoginAsync, () => _inited, allowsMultipleExecutions: false);
GoToBlockAutofillUrisCommand = CreateDefaultAsyncRelayCommand(() => Page.Navigation.PushAsync(new BlockAutofillUrisPage()), allowsMultipleExecutions: false);
+ GoToCredentialProviderSettingsCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => GoToCredentialProviderSettings()), () => _inited, allowsMultipleExecutions: false);
}
private async Task InitAndroidAutofillSettingsAsync()
@@ -130,6 +134,17 @@ namespace Bit.App.Pages
});
}
+ private async Task GoToCredentialProviderSettings()
+ {
+ var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.SetBitwardenAsPasskeyManagerDescription, AppResources.ContinueToDeviceSettings,
+ AppResources.Continue,
+ AppResources.Cancel);
+ if (confirmed)
+ {
+ _deviceActionService.OpenCredentialProviderSettings();
+ }
+ }
+
private void ToggleUseAutofillServices()
{
if (UseAutofillServices)
diff --git a/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs b/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
index bc247295b..81c0fb60f 100644
--- a/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
+++ b/src/Core/Pages/Settings/SecuritySettingsPageViewModel.cs
@@ -370,7 +370,7 @@ namespace Bit.App.Pages
if (!_supportsBiometric
||
- !await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null))
+ await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null) != true)
{
_canUnlockWithBiometrics = false;
MainThread.BeginInvokeOnMainThread(() => TriggerPropertyChanged(nameof(CanUnlockWithBiometrics)));
diff --git a/src/Core/Pages/Vault/AutofillCiphersPageViewModel.cs b/src/Core/Pages/Vault/AutofillCiphersPageViewModel.cs
index a13d61f32..38d2bfa8a 100644
--- a/src/Core/Pages/Vault/AutofillCiphersPageViewModel.cs
+++ b/src/Core/Pages/Vault/AutofillCiphersPageViewModel.cs
@@ -1,6 +1,7 @@
using Bit.App.Models;
using Bit.App.Utilities;
using Bit.Core;
+using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.View;
@@ -12,6 +13,8 @@ namespace Bit.App.Pages
public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel
{
private CipherType? _fillType;
+ private AppOptions _appOptions;
+ private readonly LazyResolve _fido2MakeCredentialConfirmationUserInterface = new LazyResolve();
public string Uri { get; set; }
@@ -19,6 +22,7 @@ namespace Bit.App.Pages
{
Uri = appOptions?.Uri;
_fillType = appOptions.FillType;
+ _appOptions = appOptions;
string name = null;
if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
@@ -36,6 +40,7 @@ namespace Bit.App.Pages
Name = name;
PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--");
NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
+ AddNewItemText = _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential ? AppResources.SavePasskeyAsNewLogin : AppResources.AddAnItem;
}
protected override async Task> LoadGroupedItemsAsync()
@@ -43,7 +48,11 @@ namespace Bit.App.Pages
var groupedItems = new List();
var ciphers = await _cipherService.GetAllDecryptedByUrlAsync(Uri, null);
- var matching = ciphers.Item1?.Select(c => new CipherItemViewModel(c, WebsiteIconsEnabled)).ToList();
+ var matching = ciphers.Item1?.Select(c => new CipherItemViewModel(c, WebsiteIconsEnabled)
+ {
+ UsePasskeyIconAsPlaceholderFallback = _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential
+ }).ToList();
+
var hasMatching = matching?.Any() ?? false;
if (matching?.Any() ?? false)
{
@@ -78,6 +87,12 @@ namespace Bit.App.Pages
return;
}
+ if (_fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ await _fido2MakeCredentialConfirmationUserInterface.Value.ConfirmAsync(cipher.Id, cipher.Login.HasFido2Credentials, null);
+ return;
+ }
+
if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
return;
@@ -130,8 +145,30 @@ namespace Bit.App.Pages
}
}
+ protected override async Task AddFabCipherAsync()
+ {
+ //Scenario for creating a new Fido2 credential on Android but showing the Cipher Page
+ if (_fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ var pageForOther = new CipherAddEditPage(null, CipherType.Login, appOptions: _appOptions);
+ await Page.Navigation.PushModalAsync(new NavigationPage(pageForOther));
+ return;
+ }
+ else
+ {
+ await AddCipherAsync();
+ }
+ }
+
protected override async Task AddCipherAsync()
{
+ //Scenario for creating a new Fido2 credential on Android
+ if (_fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ _fido2MakeCredentialConfirmationUserInterface.Value.Confirm(null, null);
+ return;
+ }
+
if (_fillType.HasValue && _fillType != CipherType.Login)
{
var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true);
@@ -143,5 +180,13 @@ namespace Bit.App.Pages
fromAutofill: true);
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
}
+
+ public void Cancel()
+ {
+ if (_fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential)
+ {
+ _fido2MakeCredentialConfirmationUserInterface.Value.Cancel();
+ }
+ }
}
}
diff --git a/src/Core/Pages/Vault/CipherAddEditPage.xaml b/src/Core/Pages/Vault/CipherAddEditPage.xaml
index b1c2e747e..ce2ea1ea1 100644
--- a/src/Core/Pages/Vault/CipherAddEditPage.xaml
+++ b/src/Core/Pages/Vault/CipherAddEditPage.xaml
@@ -112,7 +112,7 @@
StyleClass="box-header, box-header-platform" />
+ IsVisible="{Binding TypeEditMode, Converter={StaticResource inverseBool}}">
@@ -649,9 +649,11 @@
Grid.Row="1"
Grid.Column="0"
SemanticProperties.Description="{u:I18n URI}"
+ IsEnabled="{Binding BindingContext.IsFromFido2Framework, Source={x:Reference _page}, Converter={StaticResource inverseBool}}"
AutomationId="LoginUriEntry" />
diff --git a/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs b/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs
index 2aa659d1b..2d16e8656 100644
--- a/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs
+++ b/src/Core/Pages/Vault/CipherAddEditPage.xaml.cs
@@ -19,6 +19,9 @@ namespace Bit.App.Pages
private readonly IAutofillHandler _autofillHandler;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly IUserVerificationService _userVerificationService;
+#if ANDROID
+ private readonly LazyResolve _fido2MakeCredentialConfirmationUserInterface = new LazyResolve();
+#endif
private CipherAddEditPageViewModel _vm;
private bool _fromAutofill;
@@ -45,6 +48,9 @@ namespace Bit.App.Pages
_appOptions = appOptions;
_fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
+#if ANDROID
+ FromAndroidFido2Framework = _fido2MakeCredentialConfirmationUserInterface.Value.IsConfirmingNewCredential;
+#endif
InitializeComponent();
_vm = BindingContext as CipherAddEditPageViewModel;
_vm.Page = this;
@@ -144,6 +150,7 @@ namespace Bit.App.Pages
}
public bool FromAutofillFramework { get; set; }
+ public bool FromAndroidFido2Framework { get; set; }
public CipherAddEditPageViewModel ViewModel => _vm;
protected override async void OnAppearing()
diff --git a/src/Core/Pages/Vault/CipherAddEditPageViewModel.cs b/src/Core/Pages/Vault/CipherAddEditPageViewModel.cs
index c4e679df3..65a0c3979 100644
--- a/src/Core/Pages/Vault/CipherAddEditPageViewModel.cs
+++ b/src/Core/Pages/Vault/CipherAddEditPageViewModel.cs
@@ -17,6 +17,8 @@ using Microsoft.Maui.Controls;
using Microsoft.Maui;
using Bit.App.Utilities;
using CommunityToolkit.Mvvm.Input;
+using Bit.Core.Utilities.Fido2;
+using Bit.Core.Services;
#nullable enable
@@ -37,7 +39,9 @@ namespace Bit.App.Pages
private readonly IAutofillHandler _autofillHandler;
private readonly IWatchDeviceService _watchDeviceService;
private readonly IAccountsManager _accountsManager;
-
+ private readonly IFido2MakeCredentialConfirmationUserInterface _fido2MakeCredentialConfirmationUserInterface;
+ private readonly IUserVerificationMediatorService _userVerificationMediatorService;
+
private bool _showNotesSeparator;
private bool _showPassword;
private bool _showCardNumber;
@@ -92,6 +96,11 @@ namespace Bit.App.Pages
_autofillHandler = ServiceContainer.Resolve();
_watchDeviceService = ServiceContainer.Resolve();
_accountsManager = ServiceContainer.Resolve();
+ if (ServiceContainer.TryResolve(out var fido2MakeService))
+ {
+ _fido2MakeCredentialConfirmationUserInterface = fido2MakeService;
+ }
+ _userVerificationMediatorService = ServiceContainer.Resolve();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@@ -292,7 +301,9 @@ namespace Bit.App.Pages
});
}
public bool ShowCollections => (!EditMode || CloneMode) && Cipher?.OrganizationId != null;
+ public bool IsFromFido2Framework { get; set; }
public bool EditMode => !string.IsNullOrWhiteSpace(CipherId);
+ public bool TypeEditMode => !string.IsNullOrWhiteSpace(CipherId) || IsFromFido2Framework;
public bool ShowOwnershipOptions => !EditMode || CloneMode;
public bool OwnershipPolicyInEffect => ShowOwnershipOptions && !AllowPersonal;
public bool CloneMode { get; set; }
@@ -324,6 +335,7 @@ namespace Bit.App.Pages
public async Task LoadAsync(AppOptions appOptions = null)
{
_fromOtp = appOptions?.OtpData != null;
+ IsFromFido2Framework = _fido2MakeCredentialConfirmationUserInterface?.IsConfirmingNewCredential == true;
var myEmail = await _stateService.GetEmailAsync();
OwnershipOptions.Add(new KeyValuePair(myEmail, null));
@@ -536,6 +548,26 @@ namespace Bit.App.Pages
}
try
{
+ bool isFido2UserVerified = false;
+ if (IsFromFido2Framework)
+ {
+ // Verify the user and prevent saving cipher if enforcing is needed and it's not verified.
+ var userVerification = await VerifyUserAsync();
+ if (userVerification.IsCancelled)
+ {
+ return false;
+ }
+ isFido2UserVerified = userVerification.Result;
+
+ var options = _fido2MakeCredentialConfirmationUserInterface.GetCurrentUserVerificationOptions();
+
+ if (!isFido2UserVerified && await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(options.Value))
+ {
+ await _platformUtilsService.ShowDialogAsync(AppResources.ErrorCreatingPasskey, AppResources.SavePasskey);
+ return false;
+ }
+ }
+
await _deviceActionService.ShowLoadingAsync(AppResources.Saving);
await _cipherService.SaveWithServerAsync(cipher);
@@ -554,6 +586,11 @@ namespace Bit.App.Pages
// Close and go back to app
_autofillHandler.CloseAutofill();
}
+ else if (IsFromFido2Framework)
+ {
+ _fido2MakeCredentialConfirmationUserInterface.Confirm(cipher.Id, isFido2UserVerified);
+ return true;
+ }
else if (_fromOtp)
{
await _accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null);
@@ -589,6 +626,27 @@ namespace Bit.App.Pages
return false;
}
+ private async Task> VerifyUserAsync()
+ {
+ try
+ {
+ var options = _fido2MakeCredentialConfirmationUserInterface.GetCurrentUserVerificationOptions();
+ ArgumentNullException.ThrowIfNull(options);
+
+ if (options.Value.UserVerificationPreference == Fido2UserVerificationPreference.Discouraged)
+ {
+ return new CancellableResult(false);
+ }
+
+ return await _userVerificationMediatorService.VerifyUserForFido2Async(options.Value);
+ }
+ catch (Exception ex)
+ {
+ LoggerHelper.LogEvenIfCantBeResolved(ex);
+ return new CancellableResult(false);
+ }
+ }
+
public async Task DeleteAsync()
{
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)
diff --git a/src/Core/Pages/Vault/CipherItemViewModel.cs b/src/Core/Pages/Vault/CipherItemViewModel.cs
index 28dee6780..a5f094d9d 100644
--- a/src/Core/Pages/Vault/CipherItemViewModel.cs
+++ b/src/Core/Pages/Vault/CipherItemViewModel.cs
@@ -44,5 +44,7 @@ namespace Bit.App.Pages
/// This is useful to check when the cell is being reused.
///
public bool IconImageSuccesfullyLoaded { get; set; }
+
+ public bool UsePasskeyIconAsPlaceholderFallback { get; set; }
}
}
diff --git a/src/Core/Pages/Vault/CipherSelectionPage.xaml b/src/Core/Pages/Vault/CipherSelectionPage.xaml
index 77cb06bdd..17ae849bb 100644
--- a/src/Core/Pages/Vault/CipherSelectionPage.xaml
+++ b/src/Core/Pages/Vault/CipherSelectionPage.xaml
@@ -78,12 +78,13 @@
Spacing="20"
IsVisible="{Binding ShowNoData}">
@@ -133,7 +134,7 @@