PM-7623 Fix proper implementation of IFido2GetAssertionUserInterface now that the Fido2ClientService is being used for passkey autofill (#3174)

This commit is contained in:
Federico Maccaroni 2024-04-22 13:36:18 -03:00 committed by GitHub
parent bfa57ad888
commit 1bfe894181
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 99 additions and 53 deletions

View File

@ -13,6 +13,7 @@ using Bit.Core.Resources.Localization;
using Bit.Core.Utilities.Fido2; using Bit.Core.Utilities.Fido2;
using Java.Security; using Java.Security;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.App.Platforms.Android.Autofill;
namespace Bit.Droid.Autofill namespace Bit.Droid.Autofill
{ {
@ -22,6 +23,7 @@ namespace Bit.Droid.Autofill
public class CredentialProviderSelectionActivity : MauiAppCompatActivity public class CredentialProviderSelectionActivity : MauiAppCompatActivity
{ {
private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>(); private LazyResolve<IFido2MediatorService> _fido2MediatorService = new LazyResolve<IFido2MediatorService>();
private LazyResolve<IFido2AndroidGetAssertionUserInterface> _fido2GetAssertionUserInterface = new LazyResolve<IFido2AndroidGetAssertionUserInterface>();
private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>(); private LazyResolve<IVaultTimeoutService> _vaultTimeoutService = new LazyResolve<IVaultTimeoutService>();
private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>(); private LazyResolve<IStateService> _stateService = new LazyResolve<IStateService>();
private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>(); private LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
@ -69,7 +71,7 @@ namespace Bit.Droid.Autofill
var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo); var androidOrigin = AppInfoToOrigin(getRequest?.CallingAppInfo);
var packageName = getRequest?.CallingAppInfo.PackageName; var packageName = getRequest?.CallingAppInfo.PackageName;
var appInfoOrigin = getRequest?.CallingAppInfo.Origin; var appInfoOrigin = getRequest?.CallingAppInfo.Origin;
if (appInfoOrigin is null) if (appInfoOrigin is null)
{ {
await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok); await _deviceActionService.Value.DisplayAlertAsync(AppResources.ErrorReadingPasskey, AppResources.PasskeysNotSupportedForThisApp, AppResources.Ok);
@ -77,12 +79,12 @@ namespace Bit.Droid.Autofill
return; return;
} }
var userInterface = new Fido2GetAssertionUserInterface( _fido2GetAssertionUserInterface.Value.Init(
cipherId: cipherId, cipherId,
userVerified: false, false,
ensureUnlockedVaultCallback: EnsureUnlockedVaultAsync, () => hasVaultBeenUnlockedInThisTransaction,
hasVaultBeenUnlockedInThisTransaction: () => hasVaultBeenUnlockedInThisTransaction, RpId
verifyUserCallback: (cipherId, uvPreference) => VerifyUserAsync(cipherId, uvPreference, RpId, hasVaultBeenUnlockedInThisTransaction)); );
var clientAssertParams = new Fido2ClientAssertCredentialParams var clientAssertParams = new Fido2ClientAssertCredentialParams
{ {
@ -142,38 +144,6 @@ namespace Bit.Droid.Autofill
} }
} }
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) private string AppInfoToOrigin(CallingAppInfo info)
{ {
var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray(); var cert = info.SigningInfo.GetApkContentsSigners()[0].ToByteArray();

View File

@ -1,21 +1,77 @@
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Services;
using Bit.Core.Utilities.Fido2;
namespace Bit.App.Platforms.Android.Autofill namespace Bit.App.Platforms.Android.Autofill
{ {
//TODO: WIP: Temporary Dummy implementation public interface IFido2AndroidGetAssertionUserInterface : IFido2GetAssertionUserInterface
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
{ {
public bool HasVaultBeenUnlockedInThisTransaction => true; void Init(string cipherId,
bool userVerified,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
string rpId);
}
public Task EnsureUnlockedVaultAsync() public class Fido2GetAssertionUserInterface : Core.Utilities.Fido2.Fido2GetAssertionUserInterface, IFido2AndroidGetAssertionUserInterface
{
private readonly IStateService _stateService;
private readonly IVaultTimeoutService _vaultTimeoutService;
private readonly ICipherService _cipherService;
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
public Fido2GetAssertionUserInterface(IStateService stateService,
IVaultTimeoutService vaultTimeoutService,
ICipherService cipherService,
IUserVerificationMediatorService userVerificationMediatorService)
{ {
return Task.FromResult(true); _stateService = stateService;
_vaultTimeoutService = vaultTimeoutService;
_cipherService = cipherService;
_userVerificationMediatorService = userVerificationMediatorService;
} }
public Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials) public void Init(string cipherId,
bool userVerified,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
string rpId)
{ {
var credential = credentials[0]; Init(cipherId,
return Task.FromResult<(string CipherId, bool UserVerified)>((credential.CipherId, true)); userVerified,
EnsureAuthenAndVaultUnlockedAsync,
hasVaultBeenUnlockedInThisTransaction,
(cipherId, userVerificationPreference) => VerifyUserAsync(cipherId, userVerificationPreference, rpId, hasVaultBeenUnlockedInThisTransaction()));
}
public async Task EnsureAuthenAndVaultUnlockedAsync()
{
if (!await _stateService.IsAuthenticatedAsync() || await _vaultTimeoutService.IsLockedAsync())
{
// this should never happen but just in case.
throw new InvalidOperationException("Not authed or vault locked");
}
}
private async Task<bool> VerifyUserAsync(string selectedCipherId, Fido2UserVerificationPreference userVerificationPreference, string rpId, bool vaultUnlockedDuringThisTransaction)
{
try
{
var encrypted = await _cipherService.GetAsync(selectedCipherId);
var cipher = await encrypted.DecryptAsync();
var userVerification = await _userVerificationMediatorService.VerifyUserForFido2Async(
new Fido2UserVerificationOptions(
cipher?.Reprompt == Core.Enums.CipherRepromptType.Password,
userVerificationPreference,
vaultUnlockedDuringThisTransaction,
rpId)
);
return !userVerification.IsCancelled && userVerification.Result;
}
catch (Exception ex)
{
LoggerHelper.LogEvenIfCantBeResolved(ex);
return false;
}
} }
} }
} }

View File

@ -110,6 +110,13 @@ namespace Bit.Droid
userVerificationMediatorService); userVerificationMediatorService);
ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService); ServiceContainer.Register<IFido2AuthenticatorService>(fido2AuthenticatorService);
var fido2GetAssertionUserInterface = new Fido2GetAssertionUserInterface(
ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>(),
ServiceContainer.Resolve<ICipherService>(),
ServiceContainer.Resolve<IUserVerificationMediatorService>());
ServiceContainer.Register<IFido2AndroidGetAssertionUserInterface>(fido2GetAssertionUserInterface);
var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface( var fido2MakeCredentialUserInterface = new Fido2MakeCredentialUserInterface(
ServiceContainer.Resolve<IStateService>(), ServiceContainer.Resolve<IStateService>(),
ServiceContainer.Resolve<IVaultTimeoutService>(), ServiceContainer.Resolve<IVaultTimeoutService>(),
@ -124,7 +131,7 @@ namespace Bit.Droid
ServiceContainer.Resolve<IEnvironmentService>(), ServiceContainer.Resolve<IEnvironmentService>(),
ServiceContainer.Resolve<ICryptoFunctionService>(), ServiceContainer.Resolve<ICryptoFunctionService>(),
ServiceContainer.Resolve<IFido2AuthenticatorService>(), ServiceContainer.Resolve<IFido2AuthenticatorService>(),
new Fido2GetAssertionUserInterface(), fido2GetAssertionUserInterface,
fido2MakeCredentialUserInterface); fido2MakeCredentialUserInterface);
ServiceContainer.Register<IFido2ClientService>(fido2ClientService); ServiceContainer.Register<IFido2ClientService>(fido2ClientService);

View File

@ -12,11 +12,15 @@ namespace Bit.Core.Utilities.Fido2
/// </summary> /// </summary>
public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface public class Fido2GetAssertionUserInterface : IFido2GetAssertionUserInterface
{ {
private readonly string _cipherId; protected string _cipherId;
private readonly bool _userVerified = false; protected bool _userVerified = false;
private readonly Func<Task> _ensureUnlockedVaultCallback; protected Func<Task> _ensureUnlockedVaultCallback;
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction; protected Func<bool> _hasVaultBeenUnlockedInThisTransaction;
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback; protected Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
public Fido2GetAssertionUserInterface()
{
}
/// <param name="cipherId">The cipherId for the credential that the user has already picker</param> /// <param name="cipherId">The cipherId for the credential that the user has already picker</param>
/// <param name="userVerified">True if the user has already been verified by the operating system</param> /// <param name="userVerified">True if the user has already been verified by the operating system</param>
@ -25,6 +29,15 @@ namespace Bit.Core.Utilities.Fido2
Func<Task> ensureUnlockedVaultCallback, Func<Task> ensureUnlockedVaultCallback,
Func<bool> hasVaultBeenUnlockedInThisTransaction, Func<bool> hasVaultBeenUnlockedInThisTransaction,
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback) Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
{
Init(cipherId, userVerified, ensureUnlockedVaultCallback, hasVaultBeenUnlockedInThisTransaction, verifyUserCallback);
}
protected void Init(string cipherId,
bool userVerified,
Func<Task> ensureUnlockedVaultCallback,
Func<bool> hasVaultBeenUnlockedInThisTransaction,
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
{ {
_cipherId = cipherId; _cipherId = cipherId;
_userVerified = userVerified; _userVerified = userVerified;