mirror of
https://github.com/bitwarden/mobile
synced 2025-12-16 00:03:22 +00:00
* PM-6441 Implement passkeys User Verification * PM-6441 Reorganized UserVerificationMediatorService so everything is not in the same file * PM-6441 Fix Unit tests * PM-6441 Refactor UserVerification on Fido2Authenticator and Client services to be of an enum type so we can see which specific preference the RP sent and to be passed into the user verification mediator service to perform the correct flow depending on that. Also updated Unit tests. * PM-6441 Changed user verification logic a bit so if preference is Preferred and the app has the ability to verify the user then enforce required UV and fix issue on on Discouraged to take into account MP reprompt
354 lines
14 KiB
C#
354 lines
14 KiB
C#
using System;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using AuthenticationServices;
|
|
using Bit.App.Abstractions;
|
|
using Bit.Core.Abstractions;
|
|
using Bit.Core.Models.View;
|
|
using Bit.Core.Resources.Localization;
|
|
using Bit.Core.Services;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Core.Utilities.Fido2;
|
|
using Bit.iOS.Autofill.Utilities;
|
|
using Bit.iOS.Core.Utilities;
|
|
using Foundation;
|
|
using Microsoft.Maui.ApplicationModel;
|
|
using ObjCRuntime;
|
|
using UIKit;
|
|
|
|
namespace Bit.iOS.Autofill
|
|
{
|
|
public partial class CredentialProviderViewController : ASCredentialProviderViewController, IAccountsManagerHost
|
|
{
|
|
private readonly LazyResolve<IFido2AuthenticatorService> _fido2AuthService = new LazyResolve<IFido2AuthenticatorService>();
|
|
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
|
private readonly LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
|
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
|
|
|
public override async void PrepareInterfaceForPasskeyRegistration(IASCredentialRequest registrationRequest)
|
|
{
|
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_context.VaultUnlockedDuringThisSession = false;
|
|
|
|
try
|
|
{
|
|
switch (registrationRequest?.Type)
|
|
{
|
|
case ASCredentialRequestType.PasskeyAssertion:
|
|
var passkeyRegistrationRequest = Runtime.GetNSObject<ASPasskeyCredentialRequest>(registrationRequest.GetHandle());
|
|
await PrepareInterfaceForPasskeyRegistrationAsync(passkeyRegistrationRequest);
|
|
break;
|
|
default:
|
|
CancelRequest(ASExtensionErrorCode.Failed);
|
|
break;
|
|
}
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
OnProvidingCredentialException(ex);
|
|
}
|
|
}
|
|
|
|
private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest)
|
|
{
|
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
InitAppIfNeeded();
|
|
|
|
if (!await IsAuthed())
|
|
{
|
|
await _accountsManager.NavigateOnAccountChangeAsync(false);
|
|
return;
|
|
}
|
|
|
|
_context.PasskeyCredentialRequest = passkeyRegistrationRequest;
|
|
_context.IsCreatingPasskey = true;
|
|
|
|
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
|
|
|
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
|
|
|
try
|
|
{
|
|
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
|
{
|
|
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
|
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(passkeyRegistrationRequest.UserVerificationPreference),
|
|
RequireResidentKey = true,
|
|
RpEntity = new PublicKeyCredentialRpEntity
|
|
{
|
|
Id = credIdentity.RelyingPartyIdentifier,
|
|
Name = credIdentity.RelyingPartyIdentifier
|
|
},
|
|
UserEntity = new PublicKeyCredentialUserEntity
|
|
{
|
|
Id = credIdentity.UserHandle.ToArray(),
|
|
Name = credIdentity.UserName,
|
|
DisplayName = credIdentity.UserName
|
|
}
|
|
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync,
|
|
() => _context.VaultUnlockedDuringThisSession,
|
|
_context,
|
|
OnConfirmingNewCredential,
|
|
VerifyUserAsync));
|
|
|
|
await ASHelpers.ReplaceAllIdentitiesAsync();
|
|
|
|
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
|
credIdentity.RelyingPartyIdentifier,
|
|
passkeyRegistrationRequest.ClientDataHash,
|
|
NSData.FromArray(result.CredentialId),
|
|
NSData.FromArray(result.AttestationObject)));
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
await _platformUtilsService.Value.ShowDialogAsync(
|
|
string.Format(AppResources.ThereWasAProblemCreatingAPasskeyForXTryAgainLater, credIdentity?.RelyingPartyIdentifier),
|
|
AppResources.ErrorCreatingPasskey);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
|
|
{
|
|
if (supportedAlgorithms?.Any() != true)
|
|
{
|
|
return new PublicKeyCredentialParameters[]
|
|
{
|
|
new PublicKeyCredentialParameters
|
|
{
|
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
|
Alg = (int)Fido2AlgorithmIdentifier.ES256
|
|
},
|
|
new PublicKeyCredentialParameters
|
|
{
|
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
|
Alg = (int)Fido2AlgorithmIdentifier.RS256
|
|
}
|
|
};
|
|
}
|
|
|
|
return supportedAlgorithms
|
|
.Where(alg => (int)alg == (int)Fido2AlgorithmIdentifier.ES256)
|
|
.Select(alg => new PublicKeyCredentialParameters
|
|
{
|
|
Type = Bit.Core.Constants.DefaultFido2CredentialType,
|
|
Alg = (int)alg
|
|
}).ToArray();
|
|
}
|
|
|
|
private async Task ProvideCredentialWithoutUserInteractionAsync(ASPasskeyCredentialRequest passkeyCredentialRequest)
|
|
{
|
|
InitAppIfNeeded();
|
|
await _stateService.Value.SetPasswordRepromptAutofillAsync(false);
|
|
await _stateService.Value.SetPasswordVerifiedAutofillAsync(false);
|
|
if (!await IsAuthed() || await IsLocked())
|
|
{
|
|
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
|
return;
|
|
}
|
|
_context.PasskeyCredentialRequest = passkeyCredentialRequest;
|
|
await ProvideCredentialAsync(false);
|
|
}
|
|
|
|
public async Task CompleteAssertionRequestAsync(string rpId, NSData userHandleData, NSData credentialIdData, string cipherId)
|
|
{
|
|
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
|
{
|
|
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request before iOS 17"));
|
|
return;
|
|
}
|
|
|
|
if (_context.PasskeyCredentialRequest is null)
|
|
{
|
|
OnProvidingCredentialException(new InvalidOperationException("Trying to complete assertion request without a PasskeyCredentialRequest"));
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
|
{
|
|
RpId = rpId,
|
|
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
|
|
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(_context.PasskeyCredentialRequest.UserVerificationPreference),
|
|
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
|
|
{
|
|
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
|
|
{
|
|
Id = credentialIdData.ToArray()
|
|
}
|
|
}
|
|
}, new Fido2GetAssertionUserInterface(cipherId, false,
|
|
EnsureUnlockedVaultAsync,
|
|
() => _context?.VaultUnlockedDuringThisSession ?? false,
|
|
VerifyUserAsync));
|
|
|
|
if (fido2AssertionResult.SelectedCredential is null)
|
|
{
|
|
throw new NullReferenceException("SelectedCredential must have a value");
|
|
}
|
|
|
|
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
|
NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle),
|
|
rpId,
|
|
NSData.FromArray(fido2AssertionResult.Signature),
|
|
_context.PasskeyCredentialRequest.ClientDataHash,
|
|
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
|
NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
|
));
|
|
}
|
|
catch (InvalidOperationNeedsUIException)
|
|
{
|
|
return;
|
|
}
|
|
catch
|
|
{
|
|
try
|
|
{
|
|
if (_context?.IsExecutingWithoutUserInteraction == false)
|
|
{
|
|
await _platformUtilsService.Value.ShowDialogAsync(
|
|
string.Format(AppResources.ThereWasAProblemReadingAPasskeyForXTryAgainLater, rpId),
|
|
AppResources.ErrorReadingPasskey);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
}
|
|
|
|
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
|
|
{
|
|
try
|
|
{
|
|
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
|
|
var cipher = await encrypted.DecryptAsync();
|
|
|
|
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
|
new Fido2UserVerificationOptions(
|
|
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
|
|
userVerificationPreference,
|
|
_context.VaultUnlockedDuringThisSession,
|
|
_context.PasskeyCredentialIdentity?.RelyingPartyIdentifier,
|
|
() =>
|
|
{
|
|
if (_context.IsExecutingWithoutUserInteraction)
|
|
{
|
|
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
|
throw new InvalidOperationNeedsUIException();
|
|
}
|
|
})
|
|
);
|
|
}
|
|
catch (InvalidOperationNeedsUIException)
|
|
{
|
|
throw;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public async Task CompleteAssertionRequest(ASPasskeyAssertionCredential assertionCredential)
|
|
{
|
|
try
|
|
{
|
|
if (assertionCredential is null)
|
|
{
|
|
ServiceContainer.Reset();
|
|
CancelRequest(ASExtensionErrorCode.UserCanceled);
|
|
return;
|
|
}
|
|
|
|
ServiceContainer.Reset();
|
|
#pragma warning disable CA1416 // Validate platform compatibility
|
|
var expired = await ExtensionContext.CompleteAssertionRequestAsync(assertionCredential);
|
|
#pragma warning restore CA1416 // Validate platform compatibility
|
|
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
}
|
|
}
|
|
|
|
private bool CanProvideCredentialOnPasskeyRequest(CipherView cipherView)
|
|
{
|
|
return _context.PasskeyCredentialRequest != null && !cipherView.Login.HasFido2Credentials;
|
|
}
|
|
|
|
private void OnConfirmingNewCredential()
|
|
{
|
|
MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
try
|
|
{
|
|
PerformSegue(SegueConstants.LOGIN_LIST, this);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task EnsureUnlockedVaultAsync()
|
|
{
|
|
if (_context.IsCreatingPasskey)
|
|
{
|
|
if (!await IsLocked())
|
|
{
|
|
return;
|
|
}
|
|
|
|
_context.UnlockVaultTcs?.SetCanceled();
|
|
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
|
|
MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
try
|
|
{
|
|
PerformSegue(SegueConstants.LOCK, this);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
|
}
|
|
});
|
|
|
|
await _context.UnlockVaultTcs.Task;
|
|
return;
|
|
}
|
|
|
|
if (!await IsAuthed() || await IsLocked())
|
|
{
|
|
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
|
throw new InvalidOperationNeedsUIException("Not authed or locked");
|
|
}
|
|
}
|
|
}
|
|
}
|