1
0
mirror of https://github.com/bitwarden/mobile.git synced 2024-09-27 03:52:57 +02:00

[PM-5153] Android Passkey Implementation (#3020)

* Initial WIP implementation for the app unlock flow when called from Passkey. Still needs code organization and to be finished.
Also added a new Window workaround in App.xaml.cs to allow CredentialProviderSelectionActivity to launch separately.

* Added missing IDeviceActionService.cs implementation for iOS to build.

* Added Async to ReturnToPasskeyAfterUnlockMethod
Changed i18n to AppResource.Unlock
Removed unecessary cast

* minor code change (added comment)

* Added back the case for loading a specific Window for CredentialProviverSelectionActivity

* Added fix for Intent not passing properly to CredentialProviderSelectionActivity
Added Activity cancellation on error during execution of ReturnToPasskeyAfterUnlockAsync()

* Added WIP code for Android passkey implementation. Currently returns a mostly complete response that is missing the ClientDataJson

* Added WIP code for creating passkeys on Android. Still missing unlock flow and response of passkey creation is still not correct.
Removed unused throw NotImplementedException from Fido2ClientService
Added CredentialCreationActivity for passkey creation
Added alternative code on CredentialProviderSelectionActivity to try to debug issue with response not being valid

* Started working on logic to adding unlock flow. It's already handling the unlock but not passing the PendingIntentHandler info for CredentialCreation to CredentialCreationActivity

* Changed "cross-platform" to "platform"

* Created CredentialHelpers.cs class to share code used for Populating Passkeys in Android.

* Added Passkey Credential Creation shared code to CredentialHelpers.
Unlock flow for Passkey creation should now be working also.

* Updated code for checking if the CredentialProviderService has been enabled by the user or not. Still WIP, somes notes in code due to Credential API not being complete.
Also changed the disable code to open the Credential Settings.

* Replaced the AndroidX.Credential helpers with custom JSON creation to fix the response for Credential Creation

* minor code cleanup on CredentialProviderSelectionActivity

* added todo comment

* Feature/maui migraton passkeys android unlock fix andreas (#3077)

* fix: bitwarden providing too many/wrong credentials

* feat: use authenticator instead of client

---------

Co-authored-by: Dinis Vieira <dinisvieira@outlook.com>

* Removed / commented some older Passkey Proof of concept code.
Auth and creation of passkey should still work both when device is unlocked (and not)
Added some initial code in AutofillCiphersPageViewModel and CipherAddEditPageViewModel for handling Passkey creation

* PM-6829 Implemented Fido2...UserInterfaces on Android and necessary logic to get/make a credential with those

* Added IFido2MediatorService registrations
Inverted two IsLockedAsync checks

* Added navigation to autofillCipher when creating passkey

* Updated LockPage to avoid multiple executions of SubmitAsync

* Added new flow for creating new passkey on Android with the Cipher page for editing details

* Changed the Credential Provider Switch to an external link control

* Added i18n for Passkey Settings

* Cleanup of older Credentials code used for Android Fido2 POC.
Removed CredentialCreationActivity as it's no longer needed

* fixed merge conflict/error and added error check to Fido2 navigation in App.xaml.cs

* Removed from MainActivity casts from DeviceActionService
Changed CredentialProviderServiceActivity to handle Fido errors and exceptions gracefully and show the user an error. Still not with the correct messages.

* Added some error messages. Still need to confirm the Text Resource to use and change.

* Changed some messages to use AppResources

* Cleanup of Credential Android code and added exception result if the clientCreateCredentialResult is null

* Updated Add new item button text when creating a new passkey

* Added AccountSwitchedException for the Fido Mediator Service

* Removed TODO that is no longer needed

* Updated some todo messages in Android AutofillHandler

* When authenticating a passkey on Android the "showDialog" callback can be called and there's no MainPage available so it was changed for that specific scenario to use _deviceActionService instead of MainPage.

---------

Co-authored-by: Andreas Coroiu <andreas.coroiu@gmail.com>
Co-authored-by: Federico Maccaroni <fedemkr@gmail.com>
This commit is contained in:
Dinis Vieira 2024-04-09 21:57:31 +01:00 committed by GitHub
parent 8644fe598e
commit ca944025d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 1189 additions and 270 deletions

View File

@ -0,0 +1,159 @@
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.Core.Abstractions;
using Bit.Core.Utilities;
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<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
{
var passkeyEntries = new List<CredentialEntry>();
var requestOptions = new PublicKeyCredentialRequestOptions(option.RequestJson);
var authenticator = Bit.Core.Utilities.ServiceContainer.Resolve<IFido2AuthenticatorService>();
var credentials = await authenticator.SilentCredentialDiscoveryAsync(requestOptions.RpId);
passkeyEntries = credentials.Select(credential => MapCredential(credential, option, context, hasVaultBeenUnlockedInThisTransaction) as CredentialEntry).ToList();
return passkeyEntries;
}
private static PublicKeyCredentialEntry MapCredential(Fido2AuthenticatorDiscoverableCredentialMetadata credential, BeginGetPublicKeyCredentialOption option, Context context, bool hasVaultBeenUnlockedInThisTransaction)
{
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, Bit.Droid.Autofill.CredentialProviderService.UniqueGetRequestCode, 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();
}
public static async Task CreateCipherPasskeyAsync(ProviderCreateCredentialRequest getRequest, Activity activity)
{
var callingRequest = getRequest?.CallingRequest as CreatePublicKeyCredentialRequest;
var origin = callingRequest.Origin;
var credentialCreationOptions = new PublicKeyCredentialCreationOptions(callingRequest.RequestJson);
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<Core.Utilities.Fido2.PublicKeyCredentialParameters>();
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<Core.Utilities.Fido2.PublicKeyCredentialDescriptor>();
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 Bit.Core.Utilities.Fido2.Fido2ClientCreateCredentialParams()
{
Challenge = credentialCreationOptions.GetChallenge(),
Origin = origin,
PubKeyCredParams = pubKeyCredParams.ToArray(),
Rp = rp,
User = user,
Timeout = timeout,
Attestation = credentialCreationOptions.Attestation,
AuthenticatorSelection = authenticatorSelection,
ExcludeCredentials = excludeCredentials.ToArray(),
//Extensions = // Can be improved later to add support for 'credProps'
SameOriginWithAncestors = true
};
var fido2MediatorService = ServiceContainer.Resolve<IFido2MediatorService>();
var clientCreateCredentialResult = await fido2MediatorService.CreateCredentialAsync(credentialCreateParams);
if (clientCreateCredentialResult == null)
{
var resultErrorIntent = new Intent();
PendingIntentHandler.SetCreateCredentialException(resultErrorIntent, new CreateCredentialUnknownException());
activity.SetResult(Result.Ok, resultErrorIntent);
activity.Finish();
return;
}
var transportsArray = new JSONArray();
if (clientCreateCredentialResult.Transports != null)
{
foreach (var transport in clientCreateCredentialResult.Transports)
{
transportsArray.Put(transport);
}
}
var responseInnerAndroidJson = new JSONObject();
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", new JSONObject());
rootAndroidJson.Put("response", responseInnerAndroidJson);
var responseAndroidJson = rootAndroidJson.ToString();
System.Diagnostics.Debug.WriteLine(responseAndroidJson);
var result = new Intent();
var publicKeyResponse = new CreatePublicKeyCredentialResponse(responseAndroidJson);
PendingIntentHandler.SetCreateCredentialResponse(result, publicKeyResponse);
activity.SetResult(Result.Ok, result);
activity.Finish();
}
}
}

View File

@ -1,9 +0,0 @@
namespace Bit.Droid.Autofill
{
public class CredentialProviderConstants
{
public const string CredentialProviderCipherId = "credentialProviderCipherId";
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
public const string CredentialIdIntentExtra = "credId";
}
}

View File

@ -1,12 +1,18 @@
using System.Threading.Tasks;
using Android.App;
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.Abstractions;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using Bit.App.Droid.Utilities;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2;
using Java.Security;
using Bit.Core.Services;
namespace Bit.Droid.Autofill
{
@ -15,6 +21,13 @@ namespace Bit.Droid.Autofill
LaunchMode = LaunchMode.SingleTop)]
public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
private LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
private LazyResolve<IDeviceActionService> _deviceActionService = new LazyResolve<IDeviceActionService>();
protected override void OnCreate(Bundle bundle)
{
Intent?.Validate();
@ -23,43 +36,142 @@ namespace Bit.Droid.Autofill
var cipherId = Intent?.GetStringExtra(CredentialProviderConstants.CredentialProviderCipherId);
if (string.IsNullOrEmpty(cipherId))
{
SetResult(Result.Canceled);
Finish();
return;
}
GetCipherAndPerformPasskeyAuthAsync(cipherId).FireAndForget();
GetCipherAndPerformFido2AuthAsync(cipherId).FireAndForget();
}
private async Task GetCipherAndPerformPasskeyAuthAsync(string cipherId)
//Used to avoid crash on MAUI when doing back
public override void OnBackPressed()
{
// TODO this is a work in progress
// https://developer.android.com/training/sign-in/credential-provider#passkeys-implement
Finish();
}
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
// var publicKeyRequest = getRequest?.CredentialOptions as PublicKeyCredentialRequestOptions;
private async Task GetCipherAndPerformFido2AuthAsync(string cipherId)
{
string RpId = string.Empty;
try
{
var getRequest = PendingIntentHandler.RetrieveProviderGetCredentialRequest(Intent);
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credIdEnc = requestInfo?.GetString(CredentialProviderConstants.CredentialIdIntentExtra);
var credentialOption = getRequest?.CredentialOptions.FirstOrDefault();
var credentialPublic = credentialOption as GetPublicKeyCredentialOption;
var cipherService = ServiceContainer.Resolve<ICipherService>();
var cipher = await cipherService.GetAsync(cipherId);
var decCipher = await cipher.DecryptAsync();
var requestOptions = new PublicKeyCredentialRequestOptions(credentialPublic.RequestJson);
RpId = requestOptions.RpId;
var passkey = decCipher.Login.Fido2Credentials.Find(f => f.CredentialId == credIdEnc);
var requestInfo = Intent.GetBundleExtra(CredentialProviderConstants.CredentialDataIntentExtra);
var credentialId = requestInfo?.GetByteArray(CredentialProviderConstants.CredentialIdIntentExtra);
var hasVaultBeenUnlockedInThisTransaction = Intent.GetBooleanExtra(CredentialProviderConstants.CredentialHasVaultBeenUnlockedInThisTransactionExtra, false);
var credId = Convert.FromBase64String(credIdEnc);
// var privateKey = Convert.FromBase64String(passkey.PrivateKey);
// var uid = Convert.FromBase64String(passkey.uid);
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
var packageName = getRequest?.CallingAppInfo.PackageName;
var origin = getRequest?.CallingAppInfo.Origin;
var packageName = getRequest?.CallingAppInfo.PackageName;
var userInterface = new Fido2GetAssertionUserInterface(
cipherId: cipherId,
userVerified: false,
ensureUnlockedVaultCallback: EnsureUnlockedVaultAsync,
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction,
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction));
// --- continue WIP here (save TOTP copy as last step) ---
var assertParams = new Fido2AuthenticatorGetAssertionParams
{
Challenge = requestOptions.GetChallenge(),
RpId = RpId,
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(requestOptions.UserVerification),
Hash = credentialPublic.GetClientDataHash(),
AllowCredentialDescriptorList = new Core.Utilities.Fido2.PublicKeyCredentialDescriptor[] { new Core.Utilities.Fido2.PublicKeyCredentialDescriptor { Id = credentialId } },
Extensions = new object()
};
// Copy TOTP if needed
var autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
autofillHandler.Autofill(decCipher);
var assertResult = await _fido2MediatorService.Value.GetAssertionAsync(assertParams, userInterface);
var response = new AuthenticatorAssertionResponse(
requestOptions,
assertResult.SelectedCredential.Id,
androidOrigin,
false, // These flags have no effect, we set our own within `SetAuthenticatorData`
false,
false,
false,
assertResult.SelectedCredential.UserHandle,
packageName,
credentialPublic.GetClientDataHash() //clientDataHash
);
response.SetAuthenticatorData(assertResult.AuthenticatorData);
response.SetSignature(assertResult.Signature);
var result = new Intent();
var fidoCredential = new FidoPublicKeyCredential(assertResult.SelectedCredential.Id, response, "platform");
var cred = new PublicKeyCredential(fidoCredential.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);
Finish();
});
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
await MainThread.InvokeOnMainThreadAsync(async() =>
{
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, RpId), AppResources.Ok);
Finish();
});
}
}
private async Task EnsureUnlockedVaultAsync()
{
if (!await _stateService.Value.IsAuthenticatedAsync() || await _vaultTimeoutService.Value.IsLockedAsync())
{
// this should never happen but just in case.
throw new InvalidOperationException("Not authed or vault locked");
}
}
internal async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
{
try
{
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
var cipher = await encrypted.DecryptAsync();
var userVerification = await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
new Fido2UserVerificationOptions(
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
userVerificationPreference,
vaultUnlockedDuringThisTransaction,
rpId)
);
return !userVerification.IsCancelled && userVerification.Result;
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return false;
}
}
private string AppInfoToOrigin(CallingAppInfo info)
{
var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray();
var md = MessageDigest.GetInstance("SHA-256");
var certHash = md.Digest(cert);
return $"android:apk-key-hash:${CoreHelpers.Base64UrlEncode(certHash)}";
}
}
}

View File

@ -1,16 +1,15 @@
using Android;
using Android.App;
using Android.Content;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using AndroidX.Credentials.Provider;
using Bit.Core.Abstractions;
using Bit.Core.Utilities;
using AndroidX.Credentials.Exceptions;
using AndroidX.Credentials.WebAuthn;
using Bit.Core.Models.View;
using Resource = Microsoft.Maui.Resource;
using Bit.App.Droid.Utilities;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2;
namespace Bit.Droid.Autofill
{
@ -20,32 +19,53 @@ namespace Bit.Droid.Autofill
[Register("com.x8bit.bitwarden.Autofill.CredentialProviderService")]
public class CredentialProviderService : AndroidX.Credentials.Provider.CredentialProviderService
{
private const string GetPasskeyIntentAction = "PACKAGE_NAME.GET_PASSKEY";
private const int UniqueRequestCode = 94556023;
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 ICipherService _cipherService;
private IUserVerificationService _userVerificationService;
private IVaultTimeoutService _vaultTimeoutService;
private LazyResolve<ILogger> _logger = new LazyResolve<ILogger>("logger");
private readonly LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
private readonly LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
public override async void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
public override void OnBeginCreateCredentialRequest(BeginCreateCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
var response = ProcessCreateCredentialsRequestAsync(request);
if (response != null)
{
callback.OnResult(response);
}
else
{
callback.OnError("Error creating credential");
}
}
public override async void OnBeginGetCredentialRequest(BeginGetCredentialRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
try
{
_vaultTimeoutService ??= ServiceContainer.Resolve<IVaultTimeoutService>();
await _vaultTimeoutService.CheckVaultTimeoutAsync();
var locked = await _vaultTimeoutService.IsLockedAsync();
await _vaultTimeoutService.Value.CheckVaultTimeoutAsync();
var locked = await _vaultTimeoutService.Value.IsLockedAsync();
if (!locked)
{
var response = await ProcessGetCredentialsRequestAsync(request);
callback.OnResult(response);
return;
}
// TODO handle auth/unlock account flow
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<AuthenticationAction>() { unlockAction } )
.Build();
callback.OnResult(unlockResponse);
}
catch (GetCredentialException e)
{
@ -54,28 +74,59 @@ namespace Bit.Droid.Autofill
}
catch (Exception e)
{
_logger.Value.Exception(e);
_logger.Value.Exception(e);
throw;
}
}
private BeginCreateCredentialResponse 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 HandleCreatePasskeyQuery(beginCreatePublicKeyCredentialRequest);
}
return null;
}
private BeginCreateCredentialResponse HandleCreatePasskeyQuery(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));
//TODO: i81n needs to be done
var createEntryBuilder = new CreateEntry.Builder("Bitwarden Vault", pendingIntent)
.SetDescription("Your passkey will be saved securely to the Bitwarden Vault. You can use it from any other device for sign-in in the future.")
.Build();
var createCredentialResponse = new BeginCreateCredentialResponse.Builder()
.AddCreateEntry(createEntryBuilder);
return createCredentialResponse.Build();
}
private async Task<BeginGetCredentialResponse> ProcessGetCredentialsRequestAsync(
BeginGetCredentialRequest request)
{
IList<CredentialEntry> credentialEntries = null;
var credentialEntries = new List<CredentialEntry>();
foreach (var option in request.BeginGetCredentialOptions)
foreach (var option in request.BeginGetCredentialOptions.OfType<BeginGetPublicKeyCredentialOption>())
{
var credentialOption = option as BeginGetPublicKeyCredentialOption;
if (credentialOption != null)
{
credentialEntries ??= new List<CredentialEntry>();
((List<CredentialEntry>)credentialEntries).AddRange(
await PopulatePasskeyDataAsync(request.CallingAppInfo, credentialOption));
}
credentialEntries.AddRange(await Bit.App.Platforms.Android.Autofill.CredentialHelpers.PopulatePasskeyDataAsync(request.CallingAppInfo, option, ApplicationContext, false));
}
if (credentialEntries == null)
if (!credentialEntries.Any())
{
return new BeginGetCredentialResponse();
}
@ -85,63 +136,10 @@ namespace Bit.Droid.Autofill
.Build();
}
private async Task<List<CredentialEntry>> PopulatePasskeyDataAsync(CallingAppInfo callingAppInfo,
BeginGetPublicKeyCredentialOption option)
{
var packageName = callingAppInfo.PackageName;
var origin = callingAppInfo.Origin;
var signingInfo = callingAppInfo.SigningInfo;
var request = new PublicKeyCredentialRequestOptions(option.RequestJson);
var passkeyEntries = new List<CredentialEntry>();
_cipherService ??= ServiceContainer.Resolve<ICipherService>();
var ciphers = await _cipherService.GetAllDecryptedForUrlAsync(origin);
if (ciphers == null)
{
return passkeyEntries;
}
var passkeyCiphers = ciphers.Where(cipher => cipher.HasFido2Credential).ToList();
if (!passkeyCiphers.Any())
{
return passkeyEntries;
}
foreach (var cipher in passkeyCiphers)
{
var passkeyEntry = GetPasskey(cipher, option);
passkeyEntries.Add(passkeyEntry);
}
return passkeyEntries;
}
private PublicKeyCredentialEntry GetPasskey(CipherView cipher, BeginGetPublicKeyCredentialOption option)
{
var credDataBundle = new Bundle();
credDataBundle.PutString(CredentialProviderConstants.CredentialIdIntentExtra,
cipher.Login.MainFido2Credential.CredentialId);
var intent = new Intent(ApplicationContext, typeof(CredentialProviderSelectionActivity))
.SetAction(GetPasskeyIntentAction).SetPackage(Constants.PACKAGE_NAME);
intent.PutExtra(CredentialProviderConstants.CredentialDataIntentExtra, credDataBundle);
intent.PutExtra(CredentialProviderConstants.CredentialProviderCipherId, cipher.Id);
var pendingIntent = PendingIntent.GetActivity(ApplicationContext, UniqueRequestCode, intent,
PendingIntentFlags.Mutable | PendingIntentFlags.UpdateCurrent);
return new PublicKeyCredentialEntry.Builder(
ApplicationContext,
cipher.Login.Username ?? "No username",
pendingIntent,
option)
.SetDisplayName(cipher.Name)
.SetIcon(Icon.CreateWithResource(ApplicationContext, Resource.Drawable.icon))
.Build();
}
public override void OnClearCredentialStateRequest(ProviderClearCredentialStateRequest request,
CancellationSignal cancellationSignal, IOutcomeReceiver callback) => throw new NotImplementedException();
CancellationSignal cancellationSignal, IOutcomeReceiver callback)
{
callback.OnResult(null);
}
}
}

View File

@ -0,0 +1,21 @@
using Bit.Core.Abstractions;
namespace Bit.App.Platforms.Android.Autofill
{
//TODO: WIP: Temporary Dummy implementation
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
{
public bool HasVaultBeenUnlockedInThisTransaction => true;
public Task EnsureUnlockedVaultAsync()
{
return Task.FromResult(true);
}
public Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
{
var credential = credentials[0];
return Task.FromResult<(string CipherId, bool UserVerified)>((credential.CipherId, true));
}
}
}

View File

@ -0,0 +1,151 @@
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 TaskCompletionSource<(string cipherId, bool? userVerified)> _confirmCredentialTcs;
Fido2UserVerificationOptions? _currentDefaultUserVerificationOptions;
public Fido2MakeCredentialUserInterface(IStateService stateService,
IVaultTimeoutService vaultTimeoutService,
ICipherService cipherService,
IUserVerificationMediatorService userVerificationMediatorService,
IDeviceActionService deviceActionService)
{
_stateService = stateService;
_vaultTimeoutService = vaultTimeoutService;
_cipherService = cipherService;
_userVerificationMediatorService = userVerificationMediatorService;
_deviceActionService = deviceActionService;
}
public bool HasVaultBeenUnlockedInThisTransaction => true;
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, true, confirmNewCredentialParams.RpId);
var messagingService = ServiceContainer.Resolve<IMessagingService>("messagingService");
messagingService?.Send("fidoNavigateToAutofillCipher", 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.IsLockedAsync())
{
// this should never happen but just in case.
throw new InvalidOperationException("Not authed or vault locked");
}
}
public Task InformExcludedCredentialAsync(string[] existingCipherIds)
{
// TODO: Show excluded credential to the user in some screen.
return Task.FromResult(true);
}
public void Confirm(string cipherId, bool? userVerified) => _confirmCredentialTcs?.TrySetResult((cipherId, userVerified));
public void Cancel() => _confirmCredentialTcs?.TrySetCanceled();
public void OnConfirmationException(Exception ex) => _confirmCredentialTcs?.TrySetException(ex);
private async Task<CancellableResult<bool>> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId)
{
try
{
if (selectedCipherId is null && userVerificationPreference == Fido2UserVerificationPreference.Discouraged)
{
return new CancellableResult<bool>(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,
true,
rpId)
);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return new CancellableResult<bool>(false);
}
}
public Fido2UserVerificationOptions? GetCurrentUserVerificationOptions() => _currentDefaultUserVerificationOptions;
}
}

View File

@ -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
{
@ -325,12 +326,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);

View File

@ -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
@ -91,6 +94,43 @@ namespace Bit.Droid
ServiceContainer.Resolve<ICryptoService>(),
ServiceContainer.Resolve<IVaultTimeoutService>());
ServiceContainer.Register<IUserPinService>(userPinService);
var userVerificationMediatorService = new UserVerificationMediatorService(
ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"),
ServiceContainer.Resolve<IPasswordRepromptService>("passwordRepromptService"),
userPinService,
deviceActionService,
ServiceContainer.Resolve<IUserVerificationService>());
ServiceContainer.Register<IUserVerificationMediatorService>(userVerificationMediatorService);
var fido2AuthenticatorService = new Fido2AuthenticatorService(
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<ISyncService>(),
ServiceContainer.Resolve<ICryptoFunctionService>(),
userVerificationMediatorService);
ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService);
var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>(),
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IUserVerificationMediatorService>(),
ServiceContainer.Resolve<IDeviceActionService>());
ServiceContainer.Register<IFido2MakeCredentialConfirmationUserInterface>(fido2MakeCredentialUserInterface);
var fido2ClientService = new Fido2ClientService(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<ICryptoFunctionService>(),
ServiceContainer.Resolve<IFido2AuthenticatorService>(),
new Fido2GetAssertionUserInterface(),
fido2MakeCredentialUserInterface);
ServiceContainer.Register<IFido2ClientService>(fido2ClientService);
ServiceContainer.Register<IFido2MediatorService>(new Fido2MediatorService(
fido2AuthenticatorService,
fido2ClientService,
ServiceContainer.Resolve<ICipherService>()));
}
#if !FDROID
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)

View File

@ -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;
@ -43,9 +43,28 @@ namespace Bit.Droid.Services
{
return false;
}
try
{
// TODO - find a way to programmatically check if the credential provider service is enabled
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
@ -184,7 +203,10 @@ namespace Bit.Droid.Services
{
try
{
// TODO - find a way to programmatically disable the provider service, or take the user to the settings page where they can do it
// 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<IDeviceActionService>();
deviceActionService.OpenCredentialProviderSettings();
}
catch { }
}

View File

@ -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;
@ -17,11 +15,14 @@ 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
{
@ -204,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<string>(null);
@ -261,7 +262,7 @@ namespace Bit.Droid.Services
public Task<ValidatablePromptResponse?> DisplayValidatablePromptAsync(ValidatablePromptConfig config)
{
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
if (activity == null)
{
return Task.FromResult<ValidatablePromptResponse?>(null);
@ -338,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);
@ -371,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);
}
@ -394,7 +395,7 @@ namespace Bit.Droid.Services
public Task<string> 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<string>(null);
@ -475,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);
@ -504,10 +505,10 @@ namespace Bit.Droid.Services
public void OpenCredentialProviderSettings()
{
var activity = (MainActivity)Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
var activity = Microsoft.Maui.ApplicationModel.Platform.CurrentActivity;
try
{
var pendingIntent = CredentialManager.Create(activity).CreateSettingsPendingIntent();
var pendingIntent = ICredentialManager.Create(activity).CreateSettingsPendingIntent();
pendingIntent.Send();
}
catch (ActivityNotFoundException)
@ -527,7 +528,7 @@ namespace Bit.Droid.Services
{
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);
}
@ -536,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);
@ -564,10 +565,88 @@ 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();
}
appOptions.Fido2CredentialAction = null; //Clear CredentialAction Value
}
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<AndroidX.Credentials.Provider.CredentialEntry>();
foreach (var option in request.BeginGetCredentialOptions.OfType<AndroidX.Credentials.Provider.BeginGetPublicKeyCredentialOption>())
{
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;
@ -608,7 +687,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;
}

View File

@ -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;
@ -40,6 +41,7 @@ namespace Bit.App.Abstractions
void OpenCredentialProviderSettings();
void OpenAutofillSettings();
long GetActiveTime();
Task ExecuteFido2CredentialActionAsync(AppOptions appOptions);
void CloseMainApp();
float GetSystemFontSizeScale();
Task OnAccountSwitchCompleteAsync();

View File

@ -0,0 +1,32 @@
using Bit.Core.Utilities.Fido2;
namespace Bit.Core.Abstractions
{
public interface IFido2MakeCredentialConfirmationUserInterface : IFido2MakeCredentialUserInterface
{
/// <summary>
/// Call this method after the use chose where to save the new Fido2 credential.
/// </summary>
/// <param name="cipherId">
/// Cipher ID where to save the new credential.
/// If <c>null</c> a new default passkey cipher item will be created
/// </param>
/// <param name="userVerified">
/// Whether the user has been verified or not.
/// If <c>null</c> verification has not taken place yet.
/// </param>
void Confirm(string cipherId, bool? userVerified);
/// <summary>
/// Cancels the current flow to make a credential
/// </summary>
void Cancel();
/// <summary>
/// Call this if an exception needs to happen on the credential making process
/// </summary>
void OnConfirmationException(Exception ex);
Fido2UserVerificationOptions? GetCurrentUserVerificationOptions();
}
}

View File

@ -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
@ -104,6 +105,8 @@ 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;
}
}
@ -120,6 +123,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 +194,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 +203,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<int, bool>(details.DialogId, confirmed));
}
@ -218,17 +231,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 +252,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 +280,19 @@ namespace Bit.App
}
}
}
else if (message.Command == "fidoNavigateToAutofillCipher" && 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,6 +331,12 @@ namespace Bit.App
|| message.Command == "unlocked"
|| message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
if (message.Command == AccountsManagerMessageCommands.ACCOUNT_SWITCH_COMPLETED)
{
var userVerificationMediatorService = ServiceContainer.Resolve<IFido2MakeCredentialConfirmationUserInterface>();
userVerificationMediatorService?.OnConfirmationException(new AccountSwitchedException());
}
lock (_processingLoginRequestLock)
{
// lock doesn't allow for async execution

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" ?>
<controls:BaseSettingItemView
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Bit.App.Controls"
xmlns:core="clr-namespace:Bit.Core"
x:Class="Bit.App.Controls.ExternalLinkSubtitleItemView"
x:Name="_contentView"
ControlTemplate="{StaticResource SettingControlTemplate}">
<controls:BaseSettingItemView.GestureRecognizers>
<TapGestureRecognizer Tapped="ContentView_Tapped" />
</controls:BaseSettingItemView.GestureRecognizers>
<controls:IconLabel
Text="{Binding Source={x:Static core:BitwardenIcons.ExternalLink}}"
TextColor="{DynamicResource TextColor}"
FontSize="25"
Margin="6,0,7,0"
HorizontalOptions="End"
VerticalOptions="Center"
SemanticProperties.Description="{Binding Title, Mode=OneWay, Source={x:Reference _contentView}}" />
</controls:BaseSettingItemView>

View File

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

View File

@ -95,6 +95,9 @@
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
<Generator>PublicResXFileCodeGenerator</Generator>
</EmbeddedResource>
<Compile Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml.cs">
<DependentUpon>ExternalLinkSubtitleItemView.xaml</DependentUpon>
</Compile>
<Compile Update="Pages\AndroidNavigationRedirectPage.xaml.cs">
<DependentUpon>AndroidNavigationRedirectPage.xaml</DependentUpon>
</Compile>
@ -105,6 +108,9 @@
</Compile>
</ItemGroup>
<ItemGroup>
<MauiXaml Update="Controls\Settings\ExternalLinkSubtitleItemView.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>
<MauiXaml Update="Pages\AndroidNavigationRedirectPage.xaml">
<Generator>MSBuild:Compile</Generator>
</MauiXaml>

View File

@ -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 void SetAllFrom(AppOptions o)
{
@ -35,6 +37,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;
@ -51,6 +54,7 @@ namespace Bit.App.Models
CopyInsteadOfShareAfterSaving = o.CopyInsteadOfShareAfterSaving;
HideAccountSwitcher = o.HideAccountSwitcher;
OtpData = o.OtpData;
HasUnlockedInThisTransaction = o.HasUnlockedInThisTransaction;
}
}
}

View File

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

View File

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

View File

@ -19,15 +19,6 @@
Text="{u:I18n Autofill}"
StyleClass="settings-header" />
<controls:SwitchItemView
Title="{u:I18n CredentialProviderService}"
Subtitle="{u:I18n CredentialProviderServiceExplanationLong}"
IsVisible="{Binding SupportsCredentialProviderService}"
IsToggled="{Binding UseCredentialProviderService}"
AutomationId="CredentialProviderServiceSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n AutofillServices}"
Subtitle="{u:I18n AutofillServicesExplanationLong}"
@ -47,6 +38,15 @@
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:ExternalLinkSubtitleItemView
Title="{u:I18n PasskeyManagement}"
Subtitle="{u:I18n PasskeyManagementExplanationLong}"
IsVisible="{Binding SupportsCredentialProviderService}"
GoToLinkCommand="{Binding GoToCredentialProviderSettingsCommand}"
AutomationId="CredentialProviderServiceSwitch"
StyleClass="settings-item-view"
HorizontalOptions="FillAndExpand" />
<controls:SwitchItemView
Title="{u:I18n Accessibility}"
Subtitle="{Binding UseAccessibilityDescription}"

View File

@ -6,7 +6,6 @@ namespace Bit.App.Pages
{
public partial class AutofillSettingsPageViewModel
{
private bool _useCredentialProviderService;
private bool _useAutofillServices;
private bool _useInlineAutofill;
private bool _useAccessibility;
@ -15,18 +14,6 @@ namespace Bit.App.Pages
public bool SupportsCredentialProviderService => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsCredentialProviderService();
public bool UseCredentialProviderService
{
get => _useCredentialProviderService;
set
{
if (SetProperty(ref _useCredentialProviderService, value))
{
((ICommand)ToggleUseCredentialProviderServiceCommand).Execute(null);
}
}
}
public bool SupportsAndroidAutofillServices => DeviceInfo.Platform == DevicePlatform.Android && _deviceActionService.SupportsAutofillServices();
public bool UseAutofillServices
@ -99,23 +86,23 @@ namespace Bit.App.Pages
}
}
public AsyncRelayCommand ToggleUseCredentialProviderServiceCommand { get; private set; }
public AsyncRelayCommand ToggleUseAutofillServicesCommand { get; private set; }
public AsyncRelayCommand ToggleUseInlineAutofillCommand { get; private set; }
public AsyncRelayCommand ToggleUseAccessibilityCommand { get; private set; }
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()
{
ToggleUseCredentialProviderServiceCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseCredentialProviderService()), () => _inited, allowsMultipleExecutions: false);
ToggleUseAutofillServicesCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseAutofillServices()), () => _inited, allowsMultipleExecutions: false);
ToggleUseInlineAutofillCommand = CreateDefaultAsyncRelayCommand(() => MainThread.InvokeOnMainThreadAsync(() => ToggleUseInlineAutofillEnabledAsync()), () => _inited, allowsMultipleExecutions: false);
ToggleUseAccessibilityCommand = CreateDefaultAsyncRelayCommand(ToggleUseAccessibilityAsync, () => _inited, allowsMultipleExecutions: false);
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()
@ -132,9 +119,6 @@ namespace Bit.App.Pages
private async Task UpdateAndroidAutofillSettingsAsync()
{
// TODO - uncomment once _autofillHandler.CredentialProviderServiceEnabled() returns a real value
// _useCredentialProviderService =
// SupportsCredentialProviderService && _autofillHandler.CredentialProviderServiceEnabled();
_useAutofillServices =
_autofillHandler.SupportsAutofillService() && _autofillHandler.AutofillServiceEnabled();
_useAccessibility = _autofillHandler.AutofillAccessibilityServiceRunning();
@ -143,7 +127,6 @@ namespace Bit.App.Pages
await MainThread.InvokeOnMainThreadAsync(() =>
{
TriggerPropertyChanged(nameof(UseCredentialProviderService));
TriggerPropertyChanged(nameof(UseAutofillServices));
TriggerPropertyChanged(nameof(UseAccessibility));
TriggerPropertyChanged(nameof(UseDrawOver));
@ -151,16 +134,15 @@ namespace Bit.App.Pages
});
}
private void ToggleUseCredentialProviderService()
private async Task GoToCredentialProviderSettings()
{
if (UseCredentialProviderService)
var confirmed = await _platformUtilsService.ShowDialogAsync(AppResources.SetBitwardenAsPasskeyManagerDescription, AppResources.ContinueToDeviceSettings,
AppResources.Continue,
AppResources.Cancel);
if (confirmed)
{
_deviceActionService.OpenCredentialProviderSettings();
}
else
{
_autofillHandler.DisableCredentialProviderService();
}
}
private void ToggleUseAutofillServices()

View File

@ -1,17 +1,23 @@
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;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
using Bit.Core.Utilities.Fido2;
namespace Bit.App.Pages
{
public class AutofillCiphersPageViewModel : CipherSelectionPageViewModel
{
private CipherType? _fillType;
private bool _isAndroidFido2CredentialCreation;
private AppOptions _appOptions;
private readonly LazyResolve<IFido2MakeCredentialConfirmationUserInterface> _fido2MakeCredentialConfirmationUserInterface = new LazyResolve<IFido2MakeCredentialConfirmationUserInterface>();
public string Uri { get; set; }
@ -19,6 +25,8 @@ namespace Bit.App.Pages
{
Uri = appOptions?.Uri;
_fillType = appOptions.FillType;
_isAndroidFido2CredentialCreation = appOptions.FromFido2Framework;
_appOptions = appOptions;
string name = null;
if (Uri?.StartsWith(Constants.AndroidAppProtocol) ?? false)
@ -36,6 +44,7 @@ namespace Bit.App.Pages
Name = name;
PageTitle = string.Format(AppResources.ItemsForUri, Name ?? "--");
NoDataText = string.Format(AppResources.NoItemsForUri, Name ?? "--");
AddNewItemText = appOptions.FromFido2Framework ? AppResources.SavePasskeyAsNewLogin : AppResources.AddAnItem;
}
protected override async Task<List<GroupingsPageListGroup>> LoadGroupedItemsAsync()
@ -78,6 +87,15 @@ namespace Bit.App.Pages
return;
}
if (_appOptions.FromFido2Framework)
{
if (_appOptions.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
{
await CreateFido2CredentialIntoAsync(cipher);
}
return;
}
if (!await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(cipher.Reprompt))
{
return;
@ -130,8 +148,46 @@ namespace Bit.App.Pages
}
}
private async Task CreateFido2CredentialIntoAsync(CipherView cipher)
{
if (cipher.Login.HasFido2Credentials
&&
!await _platformUtilsService.ShowDialogAsync(
AppResources.ThisItemAlreadyContainsAPasskeyAreYouSureYouWantToOverwriteTheCurrentPasskey,
AppResources.OverwritePasskey,
AppResources.Yes,
AppResources.No))
{
return;
}
_fido2MakeCredentialConfirmationUserInterface.Value.Confirm(cipher.Id, null);
}
protected override async Task AddFabCipherAsync()
{
//Scenario for creating a new Fido2 credential on Android but showing the Cipher Page
if (_isAndroidFido2CredentialCreation)
{
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 (_isAndroidFido2CredentialCreation)
{
_fido2MakeCredentialConfirmationUserInterface.Value.Confirm(null, null);
return;
}
if (_fillType.HasValue && _fillType != CipherType.Login)
{
var pageForOther = new CipherAddEditPage(type: _fillType, fromAutofill: true);
@ -143,5 +199,15 @@ namespace Bit.App.Pages
fromAutofill: true);
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
}
public void Cancel()
{
if (_appOptions?.FromFido2Framework == true
&&
_appOptions?.Fido2CredentialAction == CredentialProviderConstants.Fido2CredentialCreate)
{
_fido2MakeCredentialConfirmationUserInterface.Value.Cancel();
}
}
}
}

View File

@ -112,7 +112,7 @@
StyleClass="box-header, box-header-platform" />
</StackLayout>
<StackLayout StyleClass="box-row, box-row-input"
IsVisible="{Binding EditMode, Converter={StaticResource inverseBool}}">
IsVisible="{Binding TypeEditMode, Converter={StaticResource inverseBool}}">
<Label
Text="{u:I18n Type}"
StyleClass="box-label" />
@ -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" />
<controls:IconButton
StyleClass="box-row-button, box-row-button-platform"
IsVisible="{Binding BindingContext.IsFromFido2Framework, Source={x:Reference _page}, Converter={StaticResource inverseBool}}"
Text="{Binding Source={x:Static core:BitwardenIcons.Cog}}"
Command="{Binding BindingContext.UriOptionsCommand, Source={x:Reference _page}}"
CommandParameter="{Binding .}"
@ -665,6 +667,7 @@
</BindableLayout.ItemTemplate>
</StackLayout>
<Button Text="{u:I18n NewUri}" StyleClass="box-button-row"
IsVisible="{Binding IsFromFido2Framework, Converter={StaticResource inverseBool}}"
Clicked="NewUri_Clicked"
AutomationId="LoginAddNewUriButton"></Button>
</StackLayout>

View File

@ -45,6 +45,7 @@ namespace Bit.App.Pages
_appOptions = appOptions;
_fromAutofill = fromAutofill;
FromAutofillFramework = _appOptions?.FromAutofillFramework ?? false;
FromAndroidFido2Framework = _appOptions?.FromFido2Framework ?? false;
InitializeComponent();
_vm = BindingContext as CipherAddEditPageViewModel;
_vm.Page = this;
@ -144,6 +145,7 @@ namespace Bit.App.Pages
}
public bool FromAutofillFramework { get; set; }
public bool FromAndroidFido2Framework { get; set; }
public CipherAddEditPageViewModel ViewModel => _vm;
protected override async void OnAppearing()

View File

@ -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,8 @@ namespace Bit.App.Pages
_autofillHandler = ServiceContainer.Resolve<IAutofillHandler>();
_watchDeviceService = ServiceContainer.Resolve<IWatchDeviceService>();
_accountsManager = ServiceContainer.Resolve<IAccountsManager>();
_fido2MakeCredentialConfirmationUserInterface = ServiceContainer.Resolve<IFido2MakeCredentialConfirmationUserInterface>();
_userVerificationMediatorService = ServiceContainer.Resolve<IUserVerificationMediatorService>();
GeneratePasswordCommand = new Command(GeneratePassword);
TogglePasswordCommand = new Command(TogglePassword);
@ -292,7 +298,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 +332,7 @@ namespace Bit.App.Pages
public async Task<bool> LoadAsync(AppOptions appOptions = null)
{
_fromOtp = appOptions?.OtpData != null;
IsFromFido2Framework = appOptions?.FromFido2Framework ?? false;
var myEmail = await _stateService.GetEmailAsync();
OwnershipOptions.Add(new KeyValuePair<string, string>(myEmail, null));
@ -536,6 +545,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 +583,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 +623,27 @@ namespace Bit.App.Pages
return false;
}
private async Task<CancellableResult<bool>> VerifyUserAsync()
{
try
{
var options = _fido2MakeCredentialConfirmationUserInterface.GetCurrentUserVerificationOptions();
ArgumentNullException.ThrowIfNull(options);
if (options.Value.UserVerificationPreference == Fido2UserVerificationPreference.Discouraged)
{
return new CancellableResult<bool>(false);
}
return await _userVerificationMediatorService.VerifyUserForFido2Async(options.Value);
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return new CancellableResult<bool>(false);
}
}
public async Task<bool> DeleteAsync()
{
if (Microsoft.Maui.Networking.Connectivity.NetworkAccess == Microsoft.Maui.Networking.NetworkAccess.None)

View File

@ -83,7 +83,7 @@
Text="{Binding NoDataText}"
HorizontalTextAlignment="Center"></Label>
<Button
Text="{u:I18n AddAnItem}"
Text="{Binding AddNewItemText}"
Command="{Binding AddCipherCommand}" />
</StackLayout>
@ -133,7 +133,7 @@
<Button
x:Name="_fab"
ImageSource="plus.png"
Command="{Binding AddCipherCommand}"
Command="{Binding AddFabCipherCommand}"
Style="{StaticResource btn-fab}"
IsVisible="{OnPlatform iOS=false, Android=true}"
AbsoluteLayout.LayoutFlags="PositionProportional"

View File

@ -127,6 +127,11 @@ namespace Bit.App.Pages
#if ANDROID
_appOptions.Uri = null;
if (BindingContext is AutofillCiphersPageViewModel autofillVM)
{
autofillVM.Cancel();
}
#endif
return base.OnBackButtonPressed();
}
@ -175,6 +180,11 @@ namespace Bit.App.Pages
if (DoOnce())
{
_accountsManager.StartDefaultNavigationFlowAsync(op => op.OtpData = null).FireAndForget();
if (BindingContext is AutofillCiphersPageViewModel autofillVM)
{
autofillVM.Cancel();
}
}
}
}

View File

@ -4,6 +4,7 @@ using Bit.App.Controls;
using Bit.App.Utilities;
using Bit.Core.Abstractions;
using Bit.Core.Models.View;
using Bit.Core.Resources.Localization;
using Bit.Core.Utilities;
namespace Bit.App.Pages
@ -22,6 +23,7 @@ namespace Bit.App.Pages
protected bool _showNoData;
protected bool _showList;
protected string _noDataText;
protected string _addNewItemText;
protected bool _websiteIconsEnabled;
public CipherSelectionPageViewModel()
@ -42,6 +44,9 @@ namespace Bit.App.Pages
SelectCipherCommand = CreateDefaultAsyncRelayCommand<IGroupingsPageListItem>(SelectCipherAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
AddFabCipherCommand = CreateDefaultAsyncRelayCommand(AddFabCipherAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
AddCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
@ -50,6 +55,8 @@ namespace Bit.App.Pages
{
AllowAddAccountRow = false
};
AddNewItemText = AppResources.AddAnItem;
}
public string Name { get; set; }
@ -60,6 +67,7 @@ namespace Bit.App.Pages
public ICommand CipherOptionsCommand { get; set; }
public ICommand SelectCipherCommand { get; set; }
public ICommand AddCipherCommand { get; set; }
public ICommand AddFabCipherCommand { get; set; }
public bool ShowNoData
{
@ -79,6 +87,12 @@ namespace Bit.App.Pages
set => SetProperty(ref _noDataText, value);
}
public string AddNewItemText
{
get => _addNewItemText;
set => SetProperty(ref _addNewItemText, value);
}
public bool WebsiteIconsEnabled
{
get => _websiteIconsEnabled;
@ -153,5 +167,6 @@ namespace Bit.App.Pages
protected abstract Task SelectCipherAsync(IGroupingsPageListItem item);
protected abstract Task AddCipherAsync();
protected abstract Task AddFabCipherAsync();
}
}

View File

@ -46,6 +46,9 @@ namespace Bit.App.Pages
CipherOptionsCommand = CreateDefaultAsyncRelayCommand<CipherView>(cipher => Utilities.AppHelpers.CipherListOptions(Page, cipher, _passwordRepromptService),
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
AddFabCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
AddCipherCommand = CreateDefaultAsyncRelayCommand(AddCipherAsync,
onException: ex => HandleException(ex),
allowsMultipleExecutions: false);
@ -53,6 +56,7 @@ namespace Bit.App.Pages
public ICommand CipherOptionsCommand { get; }
public ICommand AddCipherCommand { get; }
public ICommand AddFabCipherCommand { get; }
public ExtendedObservableCollection<CipherItemViewModel> Ciphers { get; set; }
public Func<CipherView, bool> Filter { get; set; }
public string AutofillUrl { get; set; }

View File

@ -70,5 +70,10 @@ namespace Bit.App.Pages
var pageForLogin = new CipherAddEditPage(null, CipherType.Login, name: Name, appOptions: _appOptions);
await Page.Navigation.PushModalAsync(new NavigationPage(pageForLogin));
}
protected override async Task AddFabCipherAsync()
{
await AddCipherAsync();
}
}
}

View File

@ -1,6 +1,7 @@
//------------------------------------------------------------------------------
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
@ -13,10 +14,12 @@ namespace Bit.Core.Resources.Localization {
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// This class was generated by MSBuild using the GenerateResource task.
/// To add or remove a member, edit your .resx file then rerun MSBuild.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Build.Tasks.StronglyTypedResourceBuilder", "15.1.0.0")]
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
public class AppResources {
@ -1723,6 +1726,15 @@ namespace Bit.Core.Resources.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to Continue to device Settings?.
/// </summary>
public static string ContinueToDeviceSettings {
get {
return ResourceManager.GetString("ContinueToDeviceSettings", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Continue to Help center?.
/// </summary>
@ -1912,24 +1924,6 @@ namespace Bit.Core.Resources.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to Credential Provider service.
/// </summary>
public static string CredentialProviderService {
get {
return ResourceManager.GetString("CredentialProviderService", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device..
/// </summary>
public static string CredentialProviderServiceExplanationLong {
get {
return ResourceManager.GetString("CredentialProviderServiceExplanationLong", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Credits.
/// </summary>
@ -5227,6 +5221,24 @@ namespace Bit.Core.Resources.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to Passkey management.
/// </summary>
public static string PasskeyManagement {
get {
return ResourceManager.GetString("PasskeyManagement", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Use Bitwarden to save new passkeys and log in with passkeys stored in your vault..
/// </summary>
public static string PasskeyManagementExplanationLong {
get {
return ResourceManager.GetString("PasskeyManagementExplanationLong", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Passkeys.
/// </summary>
@ -6353,6 +6365,15 @@ namespace Bit.Core.Resources.Localization {
}
}
/// <summary>
/// Looks up a localized string similar to Set Bitwarden as your passkey provider in device settings..
/// </summary>
public static string SetBitwardenAsPasskeyManagerDescription {
get {
return ResourceManager.GetString("SetBitwardenAsPasskeyManagerDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Set master password.
/// </summary>

View File

@ -421,6 +421,9 @@
<data name="AutofillService" xml:space="preserve">
<value>Auto-fill service</value>
</data>
<data name="SetBitwardenAsPasskeyManagerDescription" xml:space="preserve">
<value>Set Bitwarden as your passkey provider in device settings.</value>
</data>
<data name="AvoidAmbiguousCharacters" xml:space="preserve">
<value>Avoid ambiguous characters</value>
</data>
@ -1819,8 +1822,8 @@ Scanning will happen automatically.</value>
<data name="AccessibilityDrawOverPermissionAlert" xml:space="preserve">
<value>Bitwarden needs attention - Turn on "Draw-Over" in "Auto-fill Services" from Bitwarden Settings</value>
</data>
<data name="CredentialProviderService" xml:space="preserve">
<value>Credential Provider service</value>
<data name="PasskeyManagement" xml:space="preserve">
<value>Passkey management</value>
</data>
<data name="AutofillServices" xml:space="preserve">
<value>Auto-fill services</value>
@ -2805,8 +2808,8 @@ Do you want to switch to this account?</value>
<data name="XHours" xml:space="preserve">
<value>{0} hours</value>
</data>
<data name="CredentialProviderServiceExplanationLong" xml:space="preserve">
<value>The Android Credential Provider is used for managing passkeys for use with websites and other apps on your device.</value>
<data name="PasskeyManagementExplanationLong" xml:space="preserve">
<value>Use Bitwarden to save new passkeys and log in with passkeys stored in your vault.</value>
</data>
<data name="AutofillServicesExplanationLong" xml:space="preserve">
<value>The Android Autofill Framework is used to assist in filling login information into other apps on your device.</value>
@ -2836,6 +2839,9 @@ Do you want to switch to this account?</value>
<data name="ContinueToAppStore" xml:space="preserve">
<value>Continue to app store?</value>
</data>
<data name="ContinueToDeviceSettings" xml:space="preserve">
<value>Continue to device Settings?</value>
</data>
<data name="TwoStepLoginDescriptionLong" xml:space="preserve">
<value>Make your account more secure by setting up two-step login in the Bitwarden web app.</value>
</data>

View File

@ -87,11 +87,11 @@ namespace Bit.Core.Services
var cipher = await encrypted.DecryptAsync();
if (!userVerified
&&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
cipher.Reprompt != CipherRepromptType.None,
makeCredentialParams.UserVerificationPreference,
userInterface.HasVaultBeenUnlockedInThisTransaction)))
&&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
cipher.Reprompt != CipherRepromptType.None,
makeCredentialParams.UserVerificationPreference,
userInterface.HasVaultBeenUnlockedInThisTransaction)))
{
throw new NotAllowedError();
}
@ -184,7 +184,7 @@ namespace Bit.Core.Services
{
throw new NotAllowedError();
}
if (!userVerified
&&
await _userVerificationMediatorService.ShouldEnforceFido2RequiredUserVerificationAsync(new Fido2UserVerificationOptions(
@ -249,7 +249,8 @@ namespace Bit.Core.Services
Id = cipher.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
RpId = cipher.Login.MainFido2Credential.RpId,
UserHandle = cipher.Login.MainFido2Credential.UserHandleValue,
UserName = cipher.Login.MainFido2Credential.UserName
UserName = cipher.Login.MainFido2Credential.UserName,
CipherId = cipher.Id,
}).ToArray();
return credentials;

View File

@ -232,8 +232,6 @@ namespace Bit.Core.Services
{
throw new Fido2ClientException(Fido2ClientException.ErrorCode.UnknownError, $"An unknown error occurred");
}
throw new NotImplementedException();
}
private Fido2AuthenticatorMakeCredentialParams MapToMakeCredentialParams(

View File

@ -4,63 +4,72 @@
//#if IOS
//using UIKit;
//#elif ANDROID
//using Android.Content;
//#endif
//namespace Bit.Core.Services
//{
// /// <summary>
// /// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet
// /// so we can use this that copies the logs to the clipboard so one
// /// can paste them and analyze its output.
// /// </summary>
// public class ClipLogger : ILogger
// {
// private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder();
// /// <summary>
// /// This logger can be used to help debug iOS extensions where we cannot use the .NET debugger yet
// /// so we can use this that copies the logs to the clipboard so one
// /// can paste them and analyze its output.
// /// </summary>
// public class ClipLogger : ILogger
// {
// private static readonly StringBuilder _currentBreadcrumbs = new StringBuilder();
// static ILogger _instance;
// public static ILogger Instance
// {
// get
// {
// if (_instance is null)
// {
// _instance = new ClipLogger();
// }
// return _instance;
// }
// }
// static ILogger _instance;
// public static ILogger Instance
// {
// get
// {
// if (_instance is null)
// {
// _instance = new ClipLogger();
// }
// return _instance;
// }
// }
// protected ClipLogger()
// {
// }
// protected ClipLogger()
// {
// }
// public static void Log(string breadcrumb)
// {
// var formattedText = $"{DateTime.Now.ToShortTimeString()}: {breadcrumb}";
// _currentBreadcrumbs.AppendLine(formattedText);
// public static void Log(string breadcrumb)
// {
// _currentBreadcrumbs.AppendLine($"{DateTime.Now.ToShortTimeString()}: {breadcrumb}");
//#if IOS
// MainThread.BeginInvokeOnMainThread(() => UIPasteboard.General.String = _currentBreadcrumbs.ToString());
//#elif ANDROID
// var clipboardManager = Android.App.Application.Context.GetSystemService(Context.ClipboardService) as ClipboardManager;
// var clipData = ClipData.NewPlainText("bitwarden", _currentBreadcrumbs.ToString());
// clipboardManager.PrimaryClip = clipData;
// MainThread.BeginInvokeOnMainThread(() => UIPasteboard.General.String = _currentBreadcrumbs.ToString());
//#endif
// }
// public void Error(string message, IDictionary<string, string> extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
// {
// var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}";
// var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}";
// var properties = new Dictionary<string, string>
// {
// ["File"] = filePathAndLineNumber,
// ["Method"] = memberName
// };
// public void Error(string message, IDictionary<string, string> extraData = null, [CallerMemberName] string memberName = "", [CallerFilePath] string sourceFilePath = "", [CallerLineNumber] int sourceLineNumber = 0)
// {
// var classAndMethod = $"{Path.GetFileNameWithoutExtension(sourceFilePath)}.{memberName}";
// var filePathAndLineNumber = $"{Path.GetFileName(sourceFilePath)}:{sourceLineNumber}";
// var properties = new Dictionary<string, string>
// {
// ["File"] = filePathAndLineNumber,
// ["Method"] = memberName
// };
// Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
// }
// Log(message ?? $"Error found in: {classAndMethod}, {filePathAndLineNumber}");
// }
// public void Exception(Exception ex) => Log(ex?.ToString());
// public void Exception(Exception ex) => Log(ex?.ToString());
// public Task InitAsync() => Task.CompletedTask;
// public Task InitAsync() => Task.CompletedTask;
// public Task<bool> IsEnabled() => Task.FromResult(true);
// public Task<bool> IsEnabled() => Task.FromResult(true);
// public Task SetEnabled(bool value) => Task.CompletedTask;
// }
// public Task SetEnabled(bool value) => Task.CompletedTask;
// }
//}

View File

@ -100,6 +100,11 @@ namespace Bit.App.Utilities.AccountManagement
{
_accountsManagerHost.Navigate(NavigationTarget.AddEditCipher);
}
else if (Options.FromFido2Framework)
{
var deviceActionService = Bit.Core.Utilities.ServiceContainer.Resolve<IDeviceActionService>();
deviceActionService.ExecuteFido2CredentialActionAsync(Options).FireAndForget();
}
else if (Options.Uri != null)
{
_accountsManagerHost.Navigate(NavigationTarget.AutofillCiphers);

View File

@ -429,11 +429,20 @@ namespace Bit.App.Utilities
{
if (appOptions != null)
{
// this is called after login in or unlocking so we can assume the vault has been unlocked in this transaction here.
appOptions.HasUnlockedInThisTransaction = true;
if (appOptions.FromAutofillFramework && appOptions.SaveType.HasValue)
{
App.MainPage = new NavigationPage(new CipherAddEditPage(appOptions: appOptions));
return true;
}
if (appOptions.FromFido2Framework && !string.IsNullOrWhiteSpace(appOptions.Fido2CredentialAction))
{
var deviceActionService = Bit.Core.Utilities.ServiceContainer.Resolve<IDeviceActionService>();
deviceActionService.ExecuteFido2CredentialActionAsync(appOptions).FireAndForget();
return true;
}
if (appOptions.Uri != null
||
appOptions.OtpData != null)

View File

@ -0,0 +1,13 @@
namespace Bit.Core.Utilities.Fido2
{
public class CredentialProviderConstants
{
public const string Fido2CredentialCreate = "fido2CredentialCreate";
public const string Fido2CredentialGet = "fido2CredentialGet";
public const string Fido2CredentialAction = "fido2CredentialAction";
public const string CredentialProviderCipherId = "credentialProviderCipherId";
public const string CredentialDataIntentExtra = "CREDENTIAL_DATA";
public const string CredentialIdIntentExtra = "credId";
public const string CredentialHasVaultBeenUnlockedInThisTransactionExtra = "hasVaultBeenUnlockedInThisTransaction";
}
}

View File

@ -13,4 +13,6 @@ public class Fido2AuthenticatorDiscoverableCredentialMetadata
public byte[] UserHandle { get; set; }
public string UserName { get; set; }
public string CipherId { get; set; }
}

View File

@ -2,6 +2,7 @@
using System.Linq;
using System.Threading.Tasks;
using Bit.App.Abstractions;
using Bit.App.Models;
using Bit.Core.Resources.Localization;
using Bit.App.Utilities.Prompts;
using Bit.Core.Enums;
@ -315,6 +316,12 @@ namespace Bit.iOS.Core.Services
return iOSHelpers.GetSystemUpTimeMilliseconds() ?? DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
}
public Task ExecuteFido2CredentialActionAsync(AppOptions appOptions)
{
// only used by Android
throw new NotImplementedException();
}
public void CloseMainApp()
{
throw new NotImplementedException();

View File

@ -82,7 +82,8 @@ namespace Bit.Core.Test.Services
Id = c.Login.MainFido2Credential.CredentialId.GuidToRawFormat(),
RpId = "bitwarden.com",
UserHandle = c.Login.MainFido2Credential.UserHandleValue,
UserName = c.Login.MainFido2Credential.UserName
UserName = c.Login.MainFido2Credential.UserName,
CipherId = c.Id,
}), new MetadataComparer())
);
}