bitwarden-mobile/src/iOS.Autofill/CredentialProviderViewContr...

389 lines
16 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<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
private readonly LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
private readonly LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
private readonly LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
[Export("prepareCredentialListForServiceIdentifiers:requestParameters:")]
public override void PrepareCredentialList(ASCredentialServiceIdentifier[] serviceIdentifiers, ASPasskeyCredentialRequestParameters requestParameters)
{
try
{
if (UIDevice.CurrentDevice.CheckSystemVersion(17, 0) && !string.IsNullOrEmpty(requestParameters?.RelyingPartyIdentifier))
{
_context.PasskeyCredentialRequestParameters = requestParameters;
}
PrepareCredentialList(serviceIdentifiers);
}
catch (Exception ex)
{
OnProvidingCredentialException(ex);
}
}
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 (Fido2AuthenticatorException)
{
CancelRequest(ASExtensionErrorCode.Failed);
}
catch (Exception ex)
{
OnProvidingCredentialException(ex);
}
}
private async Task PrepareInterfaceForPasskeyRegistrationAsync(ASPasskeyCredentialRequest passkeyRegistrationRequest)
{
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0) || passkeyRegistrationRequest?.CredentialIdentity is null)
{
return;
}
await InitAppIfNeededAsync();
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 _fido2MediatorService.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)
{
await InitAppIfNeededAsync();
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 _fido2MediatorService.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;
}
}
internal async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference)
{
try
{
var encrypted = await _cipherService.Value.GetAsync(selectedCipherId);
var cipher = await encrypted.DecryptAsync();
var cResult = await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
new Fido2UserVerificationOptions(
cipher?.Reprompt == Bit.Core.Enums.CipherRepromptType.Password,
userVerificationPreference,
_context.VaultUnlockedDuringThisSession,
_context.PasskeyCredentialIdentity?.RelyingPartyIdentifier,
async () =>
{
if (_context.IsExecutingWithoutUserInteraction)
{
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
throw new InvalidOperationNeedsUIException();
}
// HACK: [PM-6685] There are some devices that end up with a race condition when doing biometrics authentication
// that the check is trying to be done before the iOS extension UI is shown, which cause the bio check to fail.
// So a workaround is to show a toast which force the iOS extension UI to be shown and then awaiting for the
// precondition that the view did appear before continuing with the verification.
_platformUtilsService.Value.ShowToast(null, null, AppResources.VerifyingIdentityEllipsis);
await _conditionedAwaiterManager.Value.GetAwaiterForPrecondition(AwaiterPrecondition.AutofillIOSExtensionViewDidAppear);
}
)
);
return !cResult.IsCancelled && cResult.Result;
}
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 void OnConfirmingNewCredential()
{
MainThread.BeginInvokeOnMainThread(() =>
{
try
{
DismissViewController(false, () => PerformSegue(SegueConstants.LOGIN_LIST, this));
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
}
});
}
private async Task EnsureUnlockedVaultAsync()
{
if (_context.IsCreatingPasskey)
{
if (!await IsAuthed()
||
await _vaultTimeoutService.Value.IsLoggedOutByTimeoutAsync()
||
await _vaultTimeoutService.Value.ShouldLogOutByTimeoutAsync())
{
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.HomeLogin);
return;
}
if (!await IsLocked())
{
return;
}
await NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget.Lock);
return;
}
if (!await IsAuthed() || await IsLocked())
{
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
throw new InvalidOperationNeedsUIException("Not authed or locked");
}
}
private async Task NavigateAndWaitForUnlockAsync(Bit.Core.Enums.NavigationTarget navTarget)
{
_context.UnlockVaultTcs?.TrySetCanceled();
_context.UnlockVaultTcs = new TaskCompletionSource<bool>();
await MainThread.InvokeOnMainThreadAsync(() =>
{
DoNavigate(navTarget);
});
await _context.UnlockVaultTcs.Task;
}
}
}