mirror of
https://github.com/bitwarden/mobile.git
synced 2024-11-15 10:25:20 +01:00
[PM-6466] Implement passkeys User Verification (#3044)
* 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
This commit is contained in:
parent
e41abf5003
commit
4292542155
@ -85,6 +85,12 @@ namespace Bit.Droid
|
||||
ServiceContainer.Resolve<IWatchDeviceService>(),
|
||||
ServiceContainer.Resolve<IConditionedAwaiterManager>());
|
||||
ServiceContainer.Register<IAccountsManager>("accountsManager", accountsManager);
|
||||
|
||||
var userPinService = new UserPinService(
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<ICryptoService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
}
|
||||
#if !FDROID
|
||||
if (Build.VERSION.SdkInt <= BuildVersionCodes.Kitkat)
|
||||
@ -160,7 +166,6 @@ namespace Bit.Droid
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var userPinService = new UserPinService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
@ -184,7 +189,6 @@ namespace Bit.Droid
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
|
||||
// Push
|
||||
#if FDROID
|
||||
|
@ -1,9 +1,11 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public struct Fido2GetAssertionUserInterfaceCredential
|
||||
{
|
||||
public string CipherId { get; set; }
|
||||
public bool RequireUserVerification { get; set; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
}
|
||||
|
||||
public interface IFido2GetAssertionUserInterface : IFido2UserInterface
|
||||
|
@ -1,3 +1,5 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public struct Fido2ConfirmNewCredentialParams
|
||||
@ -13,9 +15,9 @@ namespace Bit.Core.Abstractions
|
||||
public string UserName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the user must be verified before completing the operation.
|
||||
/// The preference to whether or not the user must be verified before completing the operation.
|
||||
/// </summary>
|
||||
public bool UserVerification { get; set; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The relying party identifier
|
||||
|
@ -2,6 +2,11 @@ namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IFido2UserInterface
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the vault has been unlocked during this transaction
|
||||
/// </summary>
|
||||
bool HasVaultBeenUnlockedInThisTransaction { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Make sure that the vault is unlocked.
|
||||
/// This should open a window and ask the user to login or unlock the vault if necessary.
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.App.Abstractions
|
||||
{
|
||||
@ -10,5 +9,7 @@ namespace Bit.App.Abstractions
|
||||
Task<bool> PromptAndCheckPasswordIfNeededAsync(CipherRepromptType repromptType = CipherRepromptType.Password);
|
||||
|
||||
Task<(string password, bool valid)> ShowPasswordPromptAndGetItAsync();
|
||||
|
||||
Task<bool> ShouldByPassMasterPasswordRepromptAsync();
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
@ -29,7 +26,7 @@ namespace Bit.Core.Abstractions
|
||||
bool SupportsDuo();
|
||||
Task<bool> SupportsBiometricAsync();
|
||||
Task<bool> IsBiometricIntegrityValidAsync(string bioIntegritySrcKey = null);
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false);
|
||||
Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null, Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false);
|
||||
long GetActiveTime();
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserPinService
|
||||
{
|
||||
Task<bool> IsPinLockEnabledAsync();
|
||||
Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart);
|
||||
Task<bool> VerifyPinAsync(string inputPin);
|
||||
Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType);
|
||||
}
|
||||
}
|
||||
|
14
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
14
src/Core/Abstractions/IUserVerificationMediatorService.cs
Normal file
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserVerificationMediatorService
|
||||
{
|
||||
Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||
Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options);
|
||||
Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options);
|
||||
Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync();
|
||||
Task<(bool canPerformUnlockWithPin, bool pinVerified)> VerifyPinCodeAsync();
|
||||
Task<(bool canPerformUnlockWithMasterPassword, bool mpVerified)> VerifyMasterPasswordAsync();
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Abstractions
|
||||
{
|
||||
public interface IUserVerificationService
|
||||
{
|
||||
Task<bool> VerifyUser(string secret, VerificationType verificationType);
|
||||
Task<bool> VerifyMasterPasswordAsync(string masterPassword);
|
||||
Task<bool> HasMasterPasswordAsync(bool checkMasterKeyHash = false);
|
||||
}
|
||||
}
|
||||
|
@ -80,6 +80,7 @@
|
||||
<Folder Include="Utilities\Fido2\" />
|
||||
<Folder Include="Controls\Picker\" />
|
||||
<Folder Include="Controls\Avatar\" />
|
||||
<Folder Include="Services\UserVerification\" />
|
||||
<Folder Include="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
@ -112,6 +113,7 @@
|
||||
<None Remove="Utilities\Fido2\" />
|
||||
<None Remove="Controls\Picker\" />
|
||||
<None Remove="Controls\Avatar\" />
|
||||
<None Remove="Services\UserVerification\" />
|
||||
<None Remove="Utilities\WebAuthenticatorMAUI\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Domain
|
||||
{
|
||||
@ -9,7 +8,7 @@ namespace Bit.Core.Models.Domain
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new Exception("Must provide key.");
|
||||
throw new ArgumentKeyNullException(nameof(key));
|
||||
}
|
||||
|
||||
if (encType == null)
|
||||
@ -24,7 +23,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unable to determine encType.");
|
||||
throw new InvalidKeyOperationException("Unable to determine encType.");
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +47,7 @@ namespace Bit.Core.Models.Domain
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("Unsupported encType/key length.");
|
||||
throw new InvalidKeyOperationException("Unsupported encType/key length.");
|
||||
}
|
||||
|
||||
if (Key != null)
|
||||
@ -72,6 +71,32 @@ namespace Bit.Core.Models.Domain
|
||||
public string KeyB64 { get; set; }
|
||||
public string EncKeyB64 { get; set; }
|
||||
public string MacKeyB64 { get; set; }
|
||||
|
||||
public class ArgumentKeyNullException : ArgumentNullException
|
||||
{
|
||||
public ArgumentKeyNullException(string paramName) : base(paramName)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgumentKeyNullException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
|
||||
public ArgumentKeyNullException(string paramName, string message) : base(paramName, message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class InvalidKeyOperationException : InvalidOperationException
|
||||
{
|
||||
public InvalidKeyOperationException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidKeyOperationException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class UserKey : SymmetricCryptoKey
|
||||
|
@ -2632,6 +2632,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error creating passkey.
|
||||
/// </summary>
|
||||
public static string ErrorCreatingPasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorCreatingPasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Error reading passkey.
|
||||
/// </summary>
|
||||
public static string ErrorReadingPasskey {
|
||||
get {
|
||||
return ResourceManager.GetString("ErrorReadingPasskey", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to EU.
|
||||
/// </summary>
|
||||
@ -6767,6 +6785,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There was a problem creating a passkey for {0}. Try again later..
|
||||
/// </summary>
|
||||
public static string ThereWasAProblemCreatingAPasskeyForXTryAgainLater {
|
||||
get {
|
||||
return ResourceManager.GetString("ThereWasAProblemCreatingAPasskeyForXTryAgainLater", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to There was a problem reading your passkey for {0}. Try again later..
|
||||
/// </summary>
|
||||
public static string ThereWasAProblemReadingAPasskeyForXTryAgainLater {
|
||||
get {
|
||||
return ResourceManager.GetString("ThereWasAProblemReadingAPasskeyForXTryAgainLater", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to The URI {0} is already blocked.
|
||||
/// </summary>
|
||||
@ -7595,6 +7631,24 @@ namespace Bit.Core.Resources.Localization {
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Verification required by {0}.
|
||||
/// </summary>
|
||||
public static string VerificationRequiredByX {
|
||||
get {
|
||||
return ResourceManager.GetString("VerificationRequiredByX", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Verification required for this action. Set up an unlock method in Bitwarden to continue..
|
||||
/// </summary>
|
||||
public static string VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue {
|
||||
get {
|
||||
return ResourceManager.GetString("VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Verify Face ID.
|
||||
/// </summary>
|
||||
|
@ -2916,4 +2916,24 @@ Do you want to switch to this account?</value>
|
||||
<data name="LaunchDuo" xml:space="preserve">
|
||||
<value>Launch Duo</value>
|
||||
</data>
|
||||
<data name="VerificationRequiredByX" xml:space="preserve">
|
||||
<value>Verification required by {0}</value>
|
||||
</data>
|
||||
<data name="VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue" xml:space="preserve">
|
||||
<value>Verification required for this action. Set up an unlock method in Bitwarden to continue.</value>
|
||||
</data>
|
||||
<data name="ErrorCreatingPasskey" xml:space="preserve">
|
||||
<value>Error creating passkey</value>
|
||||
</data>
|
||||
<data name="ErrorReadingPasskey" xml:space="preserve">
|
||||
<value>Error reading passkey</value>
|
||||
</data>
|
||||
<data name="ThereWasAProblemCreatingAPasskeyForXTryAgainLater" xml:space="preserve">
|
||||
<value>There was a problem creating a passkey for {0}. Try again later.</value>
|
||||
<comment>The parameter is the RpId</comment>
|
||||
</data>
|
||||
<data name="ThereWasAProblemReadingAPasskeyForXTryAgainLater" xml:space="preserve">
|
||||
<value>There was a problem reading your passkey for {0}. Try again later.</value>
|
||||
<comment>The parameter is the RpId</comment>
|
||||
</data>
|
||||
</root>
|
||||
|
@ -16,12 +16,17 @@ namespace Bit.Core.Services
|
||||
private readonly ICipherService _cipherService;
|
||||
private readonly ISyncService _syncService;
|
||||
private readonly ICryptoFunctionService _cryptoFunctionService;
|
||||
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||
|
||||
public Fido2AuthenticatorService(ICipherService cipherService, ISyncService syncService, ICryptoFunctionService cryptoFunctionService)
|
||||
public Fido2AuthenticatorService(ICipherService cipherService,
|
||||
ISyncService syncService,
|
||||
ICryptoFunctionService cryptoFunctionService,
|
||||
IUserVerificationMediatorService userVerificationMediatorService)
|
||||
{
|
||||
_cipherService = cipherService;
|
||||
_syncService = syncService;
|
||||
_cryptoFunctionService = cryptoFunctionService;
|
||||
_userVerificationMediatorService = userVerificationMediatorService;
|
||||
}
|
||||
|
||||
public async Task<Fido2AuthenticatorMakeCredentialResult> MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams, IFido2MakeCredentialUserInterface userInterface)
|
||||
@ -47,7 +52,7 @@ namespace Bit.Core.Services
|
||||
{
|
||||
CredentialName = makeCredentialParams.RpEntity.Name,
|
||||
UserName = makeCredentialParams.UserEntity.Name,
|
||||
UserVerification = makeCredentialParams.RequireUserVerification,
|
||||
UserVerificationPreference = makeCredentialParams.UserVerificationPreference,
|
||||
RpId = makeCredentialParams.RpEntity.Id
|
||||
});
|
||||
|
||||
@ -67,7 +72,12 @@ namespace Bit.Core.Services
|
||||
var encrypted = await _cipherService.GetAsync(cipherId);
|
||||
var cipher = await encrypted.DecryptAsync();
|
||||
|
||||
if (!userVerified && (makeCredentialParams.RequireUserVerification || cipher.Reprompt != CipherRepromptType.None))
|
||||
if (!userVerified
|
||||
&&
|
||||
await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||
cipher.Reprompt != CipherRepromptType.None,
|
||||
makeCredentialParams.UserVerificationPreference,
|
||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||
{
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
@ -133,7 +143,7 @@ namespace Bit.Core.Services
|
||||
cipherOptions.Select((cipher) => new Fido2GetAssertionUserInterfaceCredential
|
||||
{
|
||||
CipherId = cipher.Id,
|
||||
RequireUserVerification = assertionParams.RequireUserVerification || cipher.Reprompt != CipherRepromptType.None
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.GetUserVerificationPreferenceFrom(assertionParams.UserVerificationPreference, cipher.Reprompt)
|
||||
}).ToArray()
|
||||
);
|
||||
var selectedCipherId = response.CipherId;
|
||||
@ -145,7 +155,12 @@ namespace Bit.Core.Services
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
|
||||
if (!userVerified && (assertionParams.RequireUserVerification || selectedCipher.Reprompt != CipherRepromptType.None))
|
||||
if (!userVerified
|
||||
&&
|
||||
await ShouldEnforceRequiredUserVerificationAsync(new Fido2UserVerificationOptions(
|
||||
selectedCipher.Reprompt != CipherRepromptType.None,
|
||||
assertionParams.UserVerificationPreference,
|
||||
userInterface.HasVaultBeenUnlockedInThisTransaction)))
|
||||
{
|
||||
throw new NotAllowedError();
|
||||
}
|
||||
@ -440,6 +455,19 @@ namespace Bit.Core.Services
|
||||
return dsa.SignData(sigBase, HashAlgorithmName.SHA256, DSASignatureFormat.Rfc3279DerSequence);
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldEnforceRequiredUserVerificationAsync(Fido2UserVerificationOptions options)
|
||||
{
|
||||
switch (options.UserVerificationPreference)
|
||||
{
|
||||
case Fido2UserVerificationPreference.Required:
|
||||
return true;
|
||||
case Fido2UserVerificationPreference.Discouraged:
|
||||
return await _userVerificationMediatorService.ShouldPerformMasterPasswordRepromptAsync(options);
|
||||
default:
|
||||
return await _userVerificationMediatorService.CanPerformUserVerificationPreferredAsync(options);
|
||||
}
|
||||
}
|
||||
|
||||
private class PublicKey
|
||||
{
|
||||
private readonly ECDsa _dsa;
|
||||
|
@ -96,10 +96,11 @@ namespace Bit.Core.Services
|
||||
else
|
||||
{
|
||||
// Assign default algorithms
|
||||
credTypesAndPubKeyAlgs = [
|
||||
credTypesAndPubKeyAlgs = new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.ES256, Type = Constants.DefaultFido2CredentialType },
|
||||
new PublicKeyCredentialParameters { Alg = (int) Fido2AlgorithmIdentifier.RS256, Type = Constants.DefaultFido2CredentialType }
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
if (credTypesAndPubKeyAlgs.Length == 0)
|
||||
@ -131,7 +132,7 @@ namespace Bit.Core.Services
|
||||
ClientDataJSON = clientDataJSONBytes,
|
||||
PublicKey = makeCredentialResult.PublicKey,
|
||||
PublicKeyAlgorithm = makeCredentialResult.PublicKeyAlgorithm,
|
||||
Transports = createCredentialParams.Rp.Id == "google.com" ? ["internal", "usb"] : ["internal"] // workaround for a bug on Google's side
|
||||
Transports = createCredentialParams.Rp.Id == "google.com" ? new string[] { "internal", "usb" } : new string[] { "internal" } // workaround for a bug on Google's side
|
||||
};
|
||||
}
|
||||
catch (InvalidStateError)
|
||||
@ -230,14 +231,10 @@ namespace Bit.Core.Services
|
||||
(createCredentialParams.AuthenticatorSelection?.ResidentKey == null &&
|
||||
createCredentialParams.AuthenticatorSelection?.RequireResidentKey == true);
|
||||
|
||||
var requireUserVerification = createCredentialParams.AuthenticatorSelection?.UserVerification == "required" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == "preferred" ||
|
||||
createCredentialParams.AuthenticatorSelection?.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
RequireResidentKey = requireResidentKey,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(createCredentialParams.AuthenticatorSelection?.UserVerification),
|
||||
ExcludeCredentialDescriptorList = createCredentialParams.ExcludeCredentials,
|
||||
CredTypesAndPubKeyAlgs = credTypesAndPubKeyAlgs,
|
||||
Hash = clientDataHash,
|
||||
@ -251,16 +248,11 @@ namespace Bit.Core.Services
|
||||
Fido2ClientAssertCredentialParams assertCredentialParams,
|
||||
byte[] cliendDataHash)
|
||||
{
|
||||
var requireUserVerification = assertCredentialParams.UserVerification == "required" ||
|
||||
assertCredentialParams.UserVerification == "preferred" ||
|
||||
assertCredentialParams.UserVerification == null;
|
||||
|
||||
return new Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
return new Fido2AuthenticatorGetAssertionParams {
|
||||
RpId = assertCredentialParams.RpId,
|
||||
Challenge = assertCredentialParams.Challenge,
|
||||
AllowCredentialDescriptorList = assertCredentialParams.AllowCredentials,
|
||||
RequireUserVerification = requireUserVerification,
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(assertCredentialParams?.UserVerification),
|
||||
Hash = cliendDataHash
|
||||
};
|
||||
}
|
||||
|
@ -23,7 +23,6 @@ namespace Bit.Core.Services
|
||||
// we need to track the error as well
|
||||
Microsoft.AppCenter.Crashes.Crashes.TrackError(ex);
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -57,7 +57,7 @@ namespace Bit.App.Services
|
||||
return passwordValid;
|
||||
}
|
||||
|
||||
private async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
|
||||
public async Task<bool> ShouldByPassMasterPasswordRepromptAsync()
|
||||
{
|
||||
return await _cryptoService.GetMasterKeyHashAsync() is null;
|
||||
}
|
||||
|
@ -1,19 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.App.Models;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Plugin.Fingerprint;
|
||||
using Plugin.Fingerprint.Abstractions;
|
||||
using Microsoft.Maui.ApplicationModel.DataTransfer;
|
||||
using Microsoft.Maui.ApplicationModel;
|
||||
using Microsoft.Maui.Devices;
|
||||
using Microsoft.Maui.Controls;
|
||||
using Microsoft.Maui;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@ -246,24 +239,23 @@ namespace Bit.App.Services
|
||||
}
|
||||
|
||||
public async Task<bool> AuthenticateBiometricAsync(string text = null, string fallbackText = null,
|
||||
Action fallback = null, bool logOutOnTooManyAttempts = false)
|
||||
Action fallback = null, bool logOutOnTooManyAttempts = false, bool allowAlternativeAuthentication = false)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
text = AppResources.BiometricsDirection;
|
||||
// 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)
|
||||
{
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
||||
}
|
||||
#if IOS
|
||||
var supportsFace = await _deviceActionService.SupportsFaceBiometricAsync();
|
||||
text = supportsFace ? AppResources.FaceIDDirection : AppResources.FingerprintDirection;
|
||||
#endif
|
||||
}
|
||||
var biometricRequest = new AuthenticationRequestConfiguration(AppResources.Bitwarden, text)
|
||||
{
|
||||
CancelTitle = AppResources.Cancel,
|
||||
FallbackTitle = fallbackText
|
||||
FallbackTitle = fallbackText,
|
||||
AllowAlternativeAuthentication = allowAlternativeAuthentication
|
||||
};
|
||||
var result = await CrossFingerprint.Current.AuthenticateAsync(biometricRequest);
|
||||
if (result.Authenticated)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.App.Services
|
||||
{
|
||||
@ -7,11 +8,25 @@ namespace Bit.App.Services
|
||||
{
|
||||
private readonly IStateService _stateService;
|
||||
private readonly ICryptoService _cryptoService;
|
||||
private readonly IVaultTimeoutService _vaultTimeoutService;
|
||||
|
||||
public UserPinService(IStateService stateService, ICryptoService cryptoService)
|
||||
public UserPinService(IStateService stateService, ICryptoService cryptoService, IVaultTimeoutService vaultTimeoutService)
|
||||
{
|
||||
_stateService = stateService;
|
||||
_cryptoService = cryptoService;
|
||||
_vaultTimeoutService = vaultTimeoutService;
|
||||
}
|
||||
|
||||
public async Task<bool> IsPinLockEnabledAsync()
|
||||
{
|
||||
var pinLockType = await _vaultTimeoutService.GetPinLockTypeAsync();
|
||||
|
||||
var ephemeralPinSet = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync()
|
||||
?? await _stateService.GetPinProtectedKeyAsync();
|
||||
|
||||
return (pinLockType == PinLockType.Transient && ephemeralPinSet != null)
|
||||
||
|
||||
pinLockType == PinLockType.Persistent;
|
||||
}
|
||||
|
||||
public async Task SetupPinAsync(string pin, bool requireMasterPasswordOnRestart)
|
||||
@ -34,5 +49,59 @@ namespace Bit.App.Services
|
||||
await _stateService.SetPinKeyEncryptedUserKeyAsync(protectedPinKey);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyPinAsync(string inputPin)
|
||||
{
|
||||
var (email, kdfConfig) = await _stateService.GetActiveUserCustomDataAsync(a => a?.Profile is null ? (null, default) : (a.Profile.Email, new KdfConfig(a.Profile)));
|
||||
if (kdfConfig.Type is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await VerifyPinAsync(inputPin, email, kdfConfig, await _vaultTimeoutService.GetPinLockTypeAsync());
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyPinAsync(string inputPin, string email, KdfConfig kdfConfig, PinLockType pinLockType)
|
||||
{
|
||||
EncString userKeyPin = null;
|
||||
EncString oldPinProtected = null;
|
||||
if (pinLockType == PinLockType.Persistent)
|
||||
{
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyAsync();
|
||||
var oldEncryptedKey = await _stateService.GetPinProtectedAsync();
|
||||
oldPinProtected = oldEncryptedKey != null ? new EncString(oldEncryptedKey) : null;
|
||||
}
|
||||
else if (pinLockType == PinLockType.Transient)
|
||||
{
|
||||
userKeyPin = await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
|
||||
oldPinProtected = await _stateService.GetPinProtectedKeyAsync();
|
||||
}
|
||||
|
||||
UserKey userKey;
|
||||
if (oldPinProtected != null)
|
||||
{
|
||||
userKey = await _cryptoService.DecryptAndMigrateOldPinKeyAsync(
|
||||
pinLockType == PinLockType.Transient,
|
||||
inputPin,
|
||||
email,
|
||||
kdfConfig,
|
||||
oldPinProtected
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
userKey = await _cryptoService.DecryptUserKeyWithPinAsync(
|
||||
inputPin,
|
||||
email,
|
||||
kdfConfig,
|
||||
userKeyPin
|
||||
);
|
||||
}
|
||||
|
||||
var protectedPin = await _stateService.GetProtectedPinAsync();
|
||||
var decryptedPin = await _cryptoService.DecryptToUtf8Async(new EncString(protectedPin), userKey);
|
||||
|
||||
return decryptedPin == inputPin;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,33 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services.UserVerification
|
||||
{
|
||||
public class Fido2UserVerificationPreferredServiceStrategy : IUserVerificationServiceStrategy
|
||||
{
|
||||
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||
|
||||
public Fido2UserVerificationPreferredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService)
|
||||
{
|
||||
_userVerificationMediatorService = userVerificationMediatorService;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||
{
|
||||
if (options.HasVaultBeenUnlockedInTransaction)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
options.OnNeedUI?.Invoke();
|
||||
|
||||
var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||
if (canPerformOSUnlock)
|
||||
{
|
||||
return isOSUnlocked;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services.UserVerification
|
||||
{
|
||||
public class Fido2UserVerificationRequiredServiceStrategy : IUserVerificationServiceStrategy
|
||||
{
|
||||
private readonly IUserVerificationMediatorService _userVerificationMediatorService;
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
|
||||
public Fido2UserVerificationRequiredServiceStrategy(IUserVerificationMediatorService userVerificationMediatorService,
|
||||
IPlatformUtilsService platformUtilsService)
|
||||
{
|
||||
_userVerificationMediatorService = userVerificationMediatorService;
|
||||
_platformUtilsService = platformUtilsService;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||
{
|
||||
if (options.HasVaultBeenUnlockedInTransaction)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
options.OnNeedUI?.Invoke();
|
||||
|
||||
var (canPerformOSUnlock, isOSUnlocked) = await _userVerificationMediatorService.PerformOSUnlockAsync();
|
||||
if (canPerformOSUnlock)
|
||||
{
|
||||
return isOSUnlocked;
|
||||
}
|
||||
|
||||
var (canPerformUnlockWithPin, pinVerified) = await _userVerificationMediatorService.VerifyPinCodeAsync();
|
||||
if (canPerformUnlockWithPin)
|
||||
{
|
||||
return pinVerified;
|
||||
}
|
||||
|
||||
var (canPerformUnlockWithMasterPassword, mpVerified) = await _userVerificationMediatorService.VerifyMasterPasswordAsync();
|
||||
if (canPerformUnlockWithMasterPassword)
|
||||
{
|
||||
return mpVerified;
|
||||
}
|
||||
|
||||
// TODO: Setup PIN code. For the sake of simplicity, we're not implementing this step now and just telling the user to do it in the main app.
|
||||
|
||||
await _platformUtilsService.ShowDialogAsync(AppResources.VerificationRequiredForThisActionSetUpAnUnlockMethodInBitwardenToContinue,
|
||||
string.Format(AppResources.VerificationRequiredByX, options.RpId),
|
||||
AppResources.Ok);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
|
||||
namespace Bit.Core.Services.UserVerification
|
||||
{
|
||||
public interface IUserVerificationServiceStrategy
|
||||
{
|
||||
Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options);
|
||||
}
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
using Bit.App.Abstractions;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Plugin.Fingerprint;
|
||||
using FingerprintAvailability = Plugin.Fingerprint.Abstractions.FingerprintAvailability;
|
||||
|
||||
namespace Bit.Core.Services.UserVerification
|
||||
{
|
||||
public class UserVerificationMediatorService : IUserVerificationMediatorService
|
||||
{
|
||||
private readonly IPlatformUtilsService _platformUtilsService;
|
||||
private readonly IPasswordRepromptService _passwordRepromptService;
|
||||
private readonly IUserPinService _userPinService;
|
||||
private readonly IDeviceActionService _deviceActionService;
|
||||
private readonly IUserVerificationService _userVerificationService;
|
||||
|
||||
private readonly Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy> _fido2UserVerificationStrategies = new Dictionary<Fido2UserVerificationPreference, IUserVerificationServiceStrategy>();
|
||||
|
||||
public UserVerificationMediatorService(
|
||||
IPlatformUtilsService platformUtilsService,
|
||||
IPasswordRepromptService passwordRepromptService,
|
||||
IUserPinService userPinService,
|
||||
IDeviceActionService deviceActionService,
|
||||
IUserVerificationService userVerificationService)
|
||||
{
|
||||
_platformUtilsService = platformUtilsService;
|
||||
_passwordRepromptService = passwordRepromptService;
|
||||
_userPinService = userPinService;
|
||||
_deviceActionService = deviceActionService;
|
||||
_userVerificationService = userVerificationService;
|
||||
|
||||
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Required, new Fido2UserVerificationRequiredServiceStrategy(this, _platformUtilsService));
|
||||
_fido2UserVerificationStrategies.Add(Fido2UserVerificationPreference.Preferred, new Fido2UserVerificationPreferredServiceStrategy(this));
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyUserForFido2Async(Fido2UserVerificationOptions options)
|
||||
{
|
||||
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
||||
{
|
||||
options.OnNeedUI?.Invoke();
|
||||
return await _passwordRepromptService.PromptAndCheckPasswordIfNeededAsync(Enums.CipherRepromptType.Password);
|
||||
}
|
||||
|
||||
if (!_fido2UserVerificationStrategies.TryGetValue(options.UserVerificationPreference, out var userVerificationServiceStrategy))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await userVerificationServiceStrategy.VerifyUserForFido2Async(options);
|
||||
}
|
||||
|
||||
public async Task<bool> CanPerformUserVerificationPreferredAsync(Fido2UserVerificationOptions options)
|
||||
{
|
||||
if (await ShouldPerformMasterPasswordRepromptAsync(options))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return options.HasVaultBeenUnlockedInTransaction
|
||||
||
|
||||
await CrossFingerprint.Current.GetAvailabilityAsync() == FingerprintAvailability.Available
|
||||
||
|
||||
await CrossFingerprint.Current.GetAvailabilityAsync(true) == FingerprintAvailability.Available;
|
||||
}
|
||||
|
||||
public async Task<bool> ShouldPerformMasterPasswordRepromptAsync(Fido2UserVerificationOptions options)
|
||||
{
|
||||
return options.ShouldCheckMasterPasswordReprompt && !await _passwordRepromptService.ShouldByPassMasterPasswordRepromptAsync();
|
||||
}
|
||||
|
||||
public async Task<(bool CanPerfom, bool IsUnlocked)> PerformOSUnlockAsync()
|
||||
{
|
||||
var availability = await CrossFingerprint.Current.GetAvailabilityAsync();
|
||||
if (availability == FingerprintAvailability.Available)
|
||||
{
|
||||
var isValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null);
|
||||
return (true, isValid);
|
||||
}
|
||||
|
||||
var alternativeAuthAvailability = await CrossFingerprint.Current.GetAvailabilityAsync(true);
|
||||
if (alternativeAuthAvailability == FingerprintAvailability.Available)
|
||||
{
|
||||
var isNonBioValid = await _platformUtilsService.AuthenticateBiometricAsync(null, DeviceInfo.Platform == DevicePlatform.Android ? "." : null, allowAlternativeAuthentication: true);
|
||||
return (true, isNonBioValid);
|
||||
}
|
||||
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
public async Task<(bool canPerformUnlockWithPin, bool pinVerified)> VerifyPinCodeAsync()
|
||||
{
|
||||
if (!await _userPinService.IsPinLockEnabledAsync())
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
var pin = await _deviceActionService.DisplayPromptAync(AppResources.EnterPIN,
|
||||
AppResources.VerifyPIN, null, AppResources.Ok, AppResources.Cancel, password: true);
|
||||
if (pin is null)
|
||||
{
|
||||
// cancelled by the user
|
||||
return (true, false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var isVerified = await _userPinService.VerifyPinAsync(pin);
|
||||
return (true, isVerified);
|
||||
}
|
||||
catch (SymmetricCryptoKey.ArgumentKeyNullException)
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
catch (SymmetricCryptoKey.InvalidKeyOperationException)
|
||||
{
|
||||
return (true, false);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<(bool canPerformUnlockWithMasterPassword, bool mpVerified)> VerifyMasterPasswordAsync()
|
||||
{
|
||||
if (!await _userVerificationService.HasMasterPasswordAsync(true))
|
||||
{
|
||||
return (false, false);
|
||||
}
|
||||
|
||||
var (_, isValid) = await _platformUtilsService.ShowPasswordDialogAndGetItAsync(AppResources.MasterPassword, string.Empty, _userVerificationService.VerifyMasterPasswordAsync);
|
||||
return (true, isValid);
|
||||
}
|
||||
}
|
||||
}
|
@ -25,7 +25,7 @@ namespace Bit.Core.Services
|
||||
_keyConnectorService = keyConnectorService;
|
||||
}
|
||||
|
||||
async public Task<bool> VerifyUser(string secret, VerificationType verificationType)
|
||||
public async Task<bool> VerifyUser(string secret, VerificationType verificationType)
|
||||
{
|
||||
if (string.IsNullOrEmpty(secret))
|
||||
{
|
||||
@ -61,6 +61,12 @@ namespace Bit.Core.Services
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<bool> VerifyMasterPasswordAsync(string masterPassword)
|
||||
{
|
||||
var masterKey = await _cryptoService.GetOrDeriveMasterKeyAsync(masterPassword);
|
||||
return await _cryptoService.CompareAndUpdateKeyHashAsync(masterPassword, masterKey);
|
||||
}
|
||||
|
||||
async private Task InvalidSecretErrorAsync(VerificationType verificationType)
|
||||
{
|
||||
var errorMessage = verificationType == VerificationType.OTP
|
||||
|
@ -11,9 +11,9 @@
|
||||
public PublicKeyCredentialDescriptor[] AllowCredentialDescriptorList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Instructs the authenticator to require a user-verifying gesture in order to complete the request. Examples of such gestures are fingerprint scan or a PIN.
|
||||
/// Instructs the authenticator the user verification preference in order to complete the request. Examples of UV gestures are fingerprint scan or a PIN.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The challenge to be signed by the authenticator.
|
||||
|
@ -33,9 +33,9 @@ namespace Bit.Core.Utilities.Fido2
|
||||
public bool RequireResidentKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The effective user verification requirement for assertion, a Boolean value provided by the client.
|
||||
/// The effective user verification preference for assertion, provided by the client.
|
||||
/// </summary>
|
||||
public bool RequireUserVerification { get; set; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CTAP2 authenticators support setting this to false, but we only support the WebAuthn authenticator model which does not have that option.
|
||||
|
@ -15,19 +15,27 @@ namespace Bit.Core.Utilities.Fido2
|
||||
private readonly string _cipherId;
|
||||
private readonly bool _userVerified = false;
|
||||
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||
private readonly Func<Task<bool>> _verifyUserCallback;
|
||||
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
|
||||
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
|
||||
|
||||
/// <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>
|
||||
public Fido2GetAssertionUserInterface(string cipherId, bool userVerified, Func<Task> ensureUnlockedVaultCallback, Func<Task<bool>> verifyUserCallback)
|
||||
public Fido2GetAssertionUserInterface(string cipherId,
|
||||
bool userVerified,
|
||||
Func<Task> ensureUnlockedVaultCallback,
|
||||
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
|
||||
{
|
||||
_cipherId = cipherId;
|
||||
_userVerified = userVerified;
|
||||
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
|
||||
_verifyUserCallback = verifyUserCallback;
|
||||
}
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> PickCredentialAsync(Fido2GetAssertionUserInterfaceCredential[] credentials)
|
||||
{
|
||||
if (credentials.Length == 0 || !credentials.Any(c => c.CipherId == _cipherId))
|
||||
{
|
||||
@ -35,18 +43,16 @@ namespace Bit.Core.Utilities.Fido2
|
||||
}
|
||||
|
||||
var credential = credentials.First(c => c.CipherId == _cipherId);
|
||||
var verified = _userVerified;
|
||||
if (credential.RequireUserVerification && !verified)
|
||||
{
|
||||
verified = await _verifyUserCallback();
|
||||
}
|
||||
var verified = _userVerified || await _verifyUserCallback(_cipherId, credential.UserVerificationPreference);
|
||||
|
||||
return (CipherId: _cipherId, UserVerified: verified);
|
||||
}
|
||||
|
||||
public Task EnsureUnlockedVaultAsync()
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
return _ensureUnlockedVaultCallback();
|
||||
await _ensureUnlockedVaultCallback();
|
||||
|
||||
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
24
src/Core/Utilities/Fido2/Fido2UserVerificationOptions.cs
Normal file
24
src/Core/Utilities/Fido2/Fido2UserVerificationOptions.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public readonly struct Fido2UserVerificationOptions
|
||||
{
|
||||
public Fido2UserVerificationOptions(bool shouldCheckMasterPasswordReprompt,
|
||||
Fido2UserVerificationPreference userVerificationPreference,
|
||||
bool hasVaultBeenUnlockedInTransaction,
|
||||
string rpId = null,
|
||||
Action onNeedUI = null)
|
||||
{
|
||||
ShouldCheckMasterPasswordReprompt = shouldCheckMasterPasswordReprompt;
|
||||
UserVerificationPreference = userVerificationPreference;
|
||||
HasVaultBeenUnlockedInTransaction = hasVaultBeenUnlockedInTransaction;
|
||||
RpId = rpId;
|
||||
OnNeedUI = onNeedUI;
|
||||
}
|
||||
|
||||
public bool ShouldCheckMasterPasswordReprompt { get; }
|
||||
public Fido2UserVerificationPreference UserVerificationPreference { get; }
|
||||
public bool HasVaultBeenUnlockedInTransaction { get; }
|
||||
public string RpId { get; }
|
||||
public Action OnNeedUI { get; }
|
||||
}
|
||||
}
|
39
src/Core/Utilities/Fido2/Fido2UserVerificationPreference.cs
Normal file
39
src/Core/Utilities/Fido2/Fido2UserVerificationPreference.cs
Normal file
@ -0,0 +1,39 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Utilities.Fido2
|
||||
{
|
||||
public enum Fido2UserVerificationPreference
|
||||
{
|
||||
Discouraged,
|
||||
Preferred,
|
||||
Required
|
||||
}
|
||||
|
||||
public static class Fido2UserVerificationPreferenceExtensions
|
||||
{
|
||||
public static Fido2UserVerificationPreference ToFido2UserVerificationPreference(string? preference)
|
||||
{
|
||||
switch (preference)
|
||||
{
|
||||
case "required":
|
||||
return Fido2UserVerificationPreference.Required;
|
||||
case "discouraged":
|
||||
return Fido2UserVerificationPreference.Discouraged;
|
||||
default:
|
||||
return Fido2UserVerificationPreference.Preferred;
|
||||
}
|
||||
}
|
||||
|
||||
public static Fido2UserVerificationPreference GetUserVerificationPreferenceFrom(Fido2UserVerificationPreference preference, CipherRepromptType repromptType)
|
||||
{
|
||||
if (repromptType != CipherRepromptType.None)
|
||||
{
|
||||
return Fido2UserVerificationPreference.Required;
|
||||
}
|
||||
|
||||
return preference;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,9 +5,11 @@ 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;
|
||||
@ -19,7 +21,10 @@ 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))
|
||||
@ -27,6 +32,8 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
try
|
||||
{
|
||||
switch (registrationRequest?.Type)
|
||||
@ -68,33 +75,55 @@ namespace Bit.iOS.Autofill
|
||||
var credIdentity = Runtime.GetNSObject<ASPasskeyCredentialIdentity>(passkeyRegistrationRequest.CredentialIdentity.GetHandle());
|
||||
|
||||
_context.UrlString = credIdentity?.RelyingPartyIdentifier;
|
||||
|
||||
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||
|
||||
try
|
||||
{
|
||||
Hash = passkeyRegistrationRequest.ClientDataHash.ToArray(),
|
||||
CredTypesAndPubKeyAlgs = GetCredTypesAndPubKeyAlgs(passkeyRegistrationRequest.SupportedAlgorithms),
|
||||
RequireUserVerification = passkeyRegistrationRequest.UserVerificationPreference == "required",
|
||||
RequireResidentKey = true,
|
||||
RpEntity = new PublicKeyCredentialRpEntity
|
||||
var result = await _fido2AuthService.Value.MakeCredentialAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorMakeCredentialParams
|
||||
{
|
||||
Id = credIdentity.RelyingPartyIdentifier,
|
||||
Name = credIdentity.RelyingPartyIdentifier
|
||||
},
|
||||
UserEntity = new PublicKeyCredentialUserEntity
|
||||
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
|
||||
{
|
||||
Id = credIdentity.UserHandle.ToArray(),
|
||||
Name = credIdentity.UserName,
|
||||
DisplayName = credIdentity.UserName
|
||||
await _platformUtilsService.Value.ShowDialogAsync(
|
||||
string.Format(AppResources.ThereWasAProblemCreatingAPasskeyForXTryAgainLater, credIdentity?.RelyingPartyIdentifier),
|
||||
AppResources.ErrorCreatingPasskey);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
}
|
||||
}, new Fido2MakeCredentialUserInterface(EnsureUnlockedVaultAsync, _context, OnConfirmingNewCredential));
|
||||
|
||||
await ASHelpers.ReplaceAllIdentitiesAsync();
|
||||
|
||||
var expired = await ExtensionContext.CompleteRegistrationRequestAsync(new ASPasskeyRegistrationCredential(
|
||||
credIdentity.RelyingPartyIdentifier,
|
||||
passkeyRegistrationRequest.ClientDataHash,
|
||||
NSData.FromArray(result.CredentialId),
|
||||
NSData.FromArray(result.AttestationObject)));
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private PublicKeyCredentialParameters[] GetCredTypesAndPubKeyAlgs(NSNumber[] supportedAlgorithms)
|
||||
@ -155,12 +184,11 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
try
|
||||
{
|
||||
// TODO: Add user verification and remove hardcoding on the user interface "userVerified"
|
||||
var fido2AssertionResult = await _fido2AuthService.Value.GetAssertionAsync(new Bit.Core.Utilities.Fido2.Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
RpId = rpId,
|
||||
Hash = _context.PasskeyCredentialRequest.ClientDataHash.ToArray(),
|
||||
RequireUserVerification = _context.PasskeyCredentialRequest.UserVerificationPreference == "required",
|
||||
UserVerificationPreference = Fido2UserVerificationPreferenceExtensions.ToFido2UserVerificationPreference(_context.PasskeyCredentialRequest.UserVerificationPreference),
|
||||
AllowCredentialDescriptorList = new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new Bit.Core.Utilities.Fido2.PublicKeyCredentialDescriptor
|
||||
@ -168,29 +196,81 @@ namespace Bit.iOS.Autofill
|
||||
Id = credentialIdData.ToArray()
|
||||
}
|
||||
}
|
||||
}, new Fido2GetAssertionUserInterface(cipherId, true, EnsureUnlockedVaultAsync, () => Task.FromResult(true)));
|
||||
}, new Fido2GetAssertionUserInterface(cipherId, false,
|
||||
EnsureUnlockedVaultAsync,
|
||||
() => _context?.VaultUnlockedDuringThisSession ?? false,
|
||||
VerifyUserAsync));
|
||||
|
||||
var selectedUserHandleData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle)
|
||||
: (NSData)userHandleData;
|
||||
|
||||
var selectedCredentialIdData = fido2AssertionResult.SelectedCredential != null
|
||||
? NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||
: credentialIdData;
|
||||
if (fido2AssertionResult.SelectedCredential is null)
|
||||
{
|
||||
throw new NullReferenceException("SelectedCredential must have a value");
|
||||
}
|
||||
|
||||
await CompleteAssertionRequest(new ASPasskeyAssertionCredential(
|
||||
selectedUserHandleData,
|
||||
NSData.FromArray(fido2AssertionResult.SelectedCredential.UserHandle),
|
||||
rpId,
|
||||
NSData.FromArray(fido2AssertionResult.Signature),
|
||||
_context.PasskeyCredentialRequest.ClientDataHash,
|
||||
NSData.FromArray(fido2AssertionResult.AuthenticatorData),
|
||||
selectedCredentialIdData
|
||||
NSData.FromArray(fido2AssertionResult.SelectedCredential.Id)
|
||||
));
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
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)
|
||||
@ -266,7 +346,7 @@ namespace Bit.iOS.Autofill
|
||||
if (!await IsAuthed() || await IsLocked())
|
||||
{
|
||||
CancelRequest(ASExtensionErrorCode.UserInteractionRequired);
|
||||
throw new InvalidOperationException("Not authed or locked");
|
||||
throw new InvalidOperationNeedsUIException("Not authed or locked");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
Logo.Image = new UIImage(ThemeHelpers.LightTheme ? "logo.png" : "logo_white.png");
|
||||
View.BackgroundColor = ThemeHelpers.SplashBackgroundColor;
|
||||
|
||||
_context = new Context
|
||||
{
|
||||
ExtContext = ExtensionContext
|
||||
@ -109,6 +110,7 @@ namespace Bit.iOS.Autofill
|
||||
try
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
_context.ServiceIdentifiers = serviceIdentifiers;
|
||||
if (serviceIdentifiers.Length > 0)
|
||||
{
|
||||
@ -153,6 +155,9 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
_context.IsExecutingWithoutUserInteraction = true;
|
||||
|
||||
try
|
||||
{
|
||||
switch (credentialRequest?.Type)
|
||||
@ -196,6 +201,8 @@ namespace Bit.iOS.Autofill
|
||||
return;
|
||||
}
|
||||
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
try
|
||||
{
|
||||
switch (credentialRequest?.Type)
|
||||
@ -237,6 +244,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
InitAppIfNeeded();
|
||||
_context.Configuring = true;
|
||||
_context.VaultUnlockedDuringThisSession = false;
|
||||
|
||||
if (!await IsAuthed())
|
||||
{
|
||||
@ -326,7 +334,7 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
if (_context?.IsPasskey == true)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.TrySetCanceled();
|
||||
_context.PickCredentialForFido2CreationTcs?.TrySetCanceled();
|
||||
_context.UnlockVaultTcs?.TrySetCanceled();
|
||||
}
|
||||
|
||||
@ -395,6 +403,8 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
try
|
||||
{
|
||||
_context.VaultUnlockedDuringThisSession = true;
|
||||
|
||||
if (_context.IsCreatingPasskey)
|
||||
{
|
||||
_context.UnlockVaultTcs.SetResult(true);
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
@ -8,30 +9,49 @@ namespace Bit.iOS.Autofill
|
||||
public class Fido2MakeCredentialUserInterface : IFido2MakeCredentialUserInterface
|
||||
{
|
||||
private readonly Func<Task> _ensureUnlockedVaultCallback;
|
||||
private readonly Func<bool> _hasVaultBeenUnlockedInThisTransaction;
|
||||
private readonly Context _context;
|
||||
private readonly Action _onConfirmingNewCredential;
|
||||
private readonly Func<string, Fido2UserVerificationPreference, Task<bool>> _verifyUserCallback;
|
||||
|
||||
public Fido2MakeCredentialUserInterface(Func<Task> ensureUnlockedVaultCallback, Context context, Action onConfirmingNewCredential)
|
||||
public Fido2MakeCredentialUserInterface(Func<Task> ensureUnlockedVaultCallback,
|
||||
Func<bool> hasVaultBeenUnlockedInThisTransaction,
|
||||
Context context,
|
||||
Action onConfirmingNewCredential,
|
||||
Func<string, Fido2UserVerificationPreference, Task<bool>> verifyUserCallback)
|
||||
{
|
||||
_ensureUnlockedVaultCallback = ensureUnlockedVaultCallback;
|
||||
_hasVaultBeenUnlockedInThisTransaction = hasVaultBeenUnlockedInThisTransaction;
|
||||
_context = context;
|
||||
_onConfirmingNewCredential = onConfirmingNewCredential;
|
||||
_verifyUserCallback = verifyUserCallback;
|
||||
}
|
||||
|
||||
public bool HasVaultBeenUnlockedInThisTransaction { get; private set; }
|
||||
|
||||
public async Task<(string CipherId, bool UserVerified)> ConfirmNewCredentialAsync(Fido2ConfirmNewCredentialParams confirmNewCredentialParams)
|
||||
{
|
||||
_context.ConfirmNewCredentialTcs?.SetCanceled();
|
||||
_context.ConfirmNewCredentialTcs = new TaskCompletionSource<(string CipherId, bool UserVerified)>();
|
||||
_context.PickCredentialForFido2CreationTcs?.SetCanceled();
|
||||
_context.PickCredentialForFido2CreationTcs = new TaskCompletionSource<(string, bool?)>();
|
||||
_context.PasskeyCreationParams = confirmNewCredentialParams;
|
||||
|
||||
_onConfirmingNewCredential();
|
||||
|
||||
return await _context.ConfirmNewCredentialTcs.Task;
|
||||
var (cipherId, isUserVerified) = await _context.PickCredentialForFido2CreationTcs.Task;
|
||||
|
||||
var verified = isUserVerified ?? await _verifyUserCallback(cipherId, confirmNewCredentialParams.UserVerificationPreference);
|
||||
|
||||
return (cipherId, verified);
|
||||
}
|
||||
|
||||
// iOS doesn't seem to provide the ExcludeCredentialDescriptorList so nothing to do here currently.
|
||||
public Task InformExcludedCredentialAsync(string[] existingCipherIds) => Task.CompletedTask;
|
||||
|
||||
public Task EnsureUnlockedVaultAsync() => _ensureUnlockedVaultCallback();
|
||||
public async Task EnsureUnlockedVaultAsync()
|
||||
{
|
||||
await _ensureUnlockedVaultCallback();
|
||||
|
||||
HasVaultBeenUnlockedInThisTransaction = _hasVaultBeenUnlockedInThisTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,9 @@ 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.Models;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
using Bit.iOS.Core.Utilities;
|
||||
using Bit.iOS.Core.Views;
|
||||
using Foundation;
|
||||
@ -16,6 +18,7 @@ namespace Bit.iOS.Autofill
|
||||
public partial class LoginAddViewController : Core.Controllers.LoginAddViewController
|
||||
{
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
|
||||
public LoginAddViewController(IntPtr handle)
|
||||
: base(handle)
|
||||
@ -32,11 +35,13 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
private new Context Context => (Context)base.Context;
|
||||
|
||||
private bool? _isUserVerified;
|
||||
|
||||
public override Action<string> Success => cipherId =>
|
||||
{
|
||||
if (IsCreatingPasskey)
|
||||
{
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true));
|
||||
Context.PickCredentialForFido2CreationTcs.TrySetResult((cipherId, _isUserVerified));
|
||||
return;
|
||||
}
|
||||
|
||||
@ -76,10 +81,15 @@ namespace Bit.iOS.Autofill
|
||||
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
_isUserVerified = await VerifyUserAsync();
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
try
|
||||
{
|
||||
@ -99,6 +109,30 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context?.PasskeyCreationParams is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
false,
|
||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
||||
Context.VaultUnlockedDuringThisSession,
|
||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async partial void SaveBarButton_Activated(UIBarButtonItem sender)
|
||||
{
|
||||
await SaveAsync();
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.iOS.Autofill.ListItems;
|
||||
using Bit.iOS.Autofill.Models;
|
||||
using Bit.iOS.Autofill.Utilities;
|
||||
@ -17,7 +18,6 @@ using CoreFoundation;
|
||||
using CoreGraphics;
|
||||
using Foundation;
|
||||
using UIKit;
|
||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||
|
||||
namespace Bit.iOS.Autofill
|
||||
{
|
||||
@ -44,6 +44,7 @@ namespace Bit.iOS.Autofill
|
||||
LazyResolve<ICipherService> _cipherService = new LazyResolve<ICipherService>();
|
||||
LazyResolve<IPlatformUtilsService> _platformUtilsService = new LazyResolve<IPlatformUtilsService>();
|
||||
LazyResolve<ILogger> _logger = new LazyResolve<ILogger>();
|
||||
LazyResolve<IUserVerificationMediatorService> _userVerificationMediatorService = new LazyResolve<IUserVerificationMediatorService>();
|
||||
|
||||
bool _alreadyLoadItemsOnce = false;
|
||||
|
||||
@ -163,24 +164,29 @@ namespace Bit.iOS.Autofill
|
||||
{
|
||||
if (!UIDevice.CurrentDevice.CheckSystemVersion(17, 0))
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login on iOS less than 17."));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context.PasskeyCreationParams is null)
|
||||
{
|
||||
Context?.ConfirmNewCredentialTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params."));
|
||||
Context?.PickCredentialForFido2CreationTcs?.TrySetException(new InvalidOperationException("Trying to save passkey as new login wihout creation params."));
|
||||
return;
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
bool? isUserVerified = null;
|
||||
if (Context?.PasskeyCreationParams?.UserVerificationPreference != Fido2UserVerificationPreference.Discouraged)
|
||||
{
|
||||
isUserVerified = await VerifyUserAsync();
|
||||
}
|
||||
|
||||
var loadingAlert = Dialogs.CreateLoadingAlert(AppResources.Saving);
|
||||
try
|
||||
{
|
||||
PresentViewController(loadingAlert, true, null);
|
||||
|
||||
var cipherId = await _cipherService.Value.CreateNewLoginForPasskeyAsync(Context.PasskeyCreationParams.Value);
|
||||
Context.ConfirmNewCredentialTcs.TrySetResult((cipherId, true));
|
||||
Context.PickCredentialForFido2CreationTcs.TrySetResult((cipherId, isUserVerified));
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -189,6 +195,30 @@ namespace Bit.iOS.Autofill
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Context?.PasskeyCreationParams is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return await _userVerificationMediatorService.Value.VerifyUserForFido2Async(
|
||||
new Fido2UserVerificationOptions(
|
||||
false,
|
||||
Context.PasskeyCreationParams.Value.UserVerificationPreference,
|
||||
Context.VaultUnlockedDuringThisSession,
|
||||
Context.PasskeyCredentialIdentity?.RelyingPartyIdentifier)
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LoggerHelper.LogEvenIfCantBeResolved(ex);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void PrepareForSegue(UIStoryboardSegue segue, NSObject sender)
|
||||
{
|
||||
if (segue.DestinationViewController is UINavigationController navController)
|
||||
|
@ -652,7 +652,7 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="12574"/>
|
||||
<segue reference="12959"/>
|
||||
<segue reference="3731"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
|
@ -15,11 +15,21 @@ namespace Bit.iOS.Autofill.Models
|
||||
public ASPasswordCredentialIdentity PasswordCredentialIdentity { get; set; }
|
||||
public ASPasskeyCredentialRequest PasskeyCredentialRequest { get; set; }
|
||||
public bool Configuring { get; set; }
|
||||
public bool IsExecutingWithoutUserInteraction { get; set; }
|
||||
|
||||
public bool IsCreatingPasskey { get; set; }
|
||||
public Fido2ConfirmNewCredentialParams? PasskeyCreationParams { get; set; }
|
||||
/// <summary>
|
||||
/// This is used to defer the completion until the vault is unlocked.
|
||||
/// </summary>
|
||||
public TaskCompletionSource<bool> UnlockVaultTcs { get; set; }
|
||||
public TaskCompletionSource<(string CipherId, bool UserVerified)> ConfirmNewCredentialTcs { get; set; }
|
||||
/// <summary>
|
||||
/// This is used to defer the completion until a vault item is chosen to add the passkey to.
|
||||
/// Param: cipher ID to add the passkey to.
|
||||
/// Param: isUserVerified if the user was verified. If null then the verification hasn't been done.
|
||||
/// </summary>
|
||||
public TaskCompletionSource<(string cipherId, bool? isUserVerified)> PickCredentialForFido2CreationTcs { get; set; }
|
||||
public bool VaultUnlockedDuringThisSession { get; set; }
|
||||
|
||||
public ASPasskeyCredentialIdentity PasskeyCredentialIdentity
|
||||
{
|
||||
|
@ -78,9 +78,7 @@ namespace Bit.iOS.Autofill.Utilities
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Check user verification
|
||||
|
||||
Context.ConfirmNewCredentialTcs.SetResult((item.Id, true));
|
||||
Context.PickCredentialForFido2CreationTcs.SetResult((item.Id, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
using System;
|
||||
|
||||
namespace Bit.iOS.Autofill.Utilities
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom exception to be thrown when we need UI from the extension.
|
||||
/// This is likely to be thrown when initiating on "...WithoutUserInteraction(...)" and doing some logic that
|
||||
/// requires user interaction.
|
||||
/// </summary>
|
||||
public class InvalidOperationNeedsUIException : InvalidOperationException
|
||||
{
|
||||
public InvalidOperationNeedsUIException()
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidOperationNeedsUIException(string message) : base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public InvalidOperationNeedsUIException(string message, Exception innerException) : base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -92,6 +92,7 @@
|
||||
<Compile Include="Utilities\BaseLoginListTableSource.cs" />
|
||||
<Compile Include="ILoginListViewController.cs" />
|
||||
<Compile Include="Fido2MakeCredentialUserInterface.cs" />
|
||||
<Compile Include="Utilities\InvalidOperationNeedsUIException.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BundleResource Include="Resources\check.png" />
|
||||
|
@ -8,6 +8,7 @@ using Bit.App.Utilities.AccountManagement;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Resources.Localization;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Services.UserVerification;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.iOS.Core.Services;
|
||||
using CoreNFC;
|
||||
@ -161,7 +162,6 @@ namespace Bit.iOS.Core.Utilities
|
||||
var cryptoFunctionService = new PclCryptoFunctionService(cryptoPrimitiveService);
|
||||
var cryptoService = new CryptoService(stateService, cryptoFunctionService, logger);
|
||||
var biometricService = new BiometricService(stateService, cryptoService);
|
||||
var userPinService = new UserPinService(stateService, cryptoService);
|
||||
var passwordRepromptService = new MobilePasswordRepromptService(platformUtilsService, cryptoService, stateService);
|
||||
|
||||
ServiceContainer.Register<ISynchronousStorageService>(preferencesStorage);
|
||||
@ -185,15 +185,29 @@ namespace Bit.iOS.Core.Utilities
|
||||
ServiceContainer.Register<ICryptoService>("cryptoService", cryptoService);
|
||||
ServiceContainer.Register<IPasswordRepromptService>("passwordRepromptService", passwordRepromptService);
|
||||
ServiceContainer.Register<IAvatarImageSourcePool>("avatarImageSourcePool", new AvatarImageSourcePool());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
}
|
||||
|
||||
public static void RegisterFinallyBeforeBootstrap()
|
||||
{
|
||||
var userPinService = new UserPinService(
|
||||
ServiceContainer.Resolve<IStateService>(),
|
||||
ServiceContainer.Resolve<ICryptoService>(),
|
||||
ServiceContainer.Resolve<IVaultTimeoutService>());
|
||||
ServiceContainer.Register<IUserPinService>(userPinService);
|
||||
|
||||
var userVerificationMediatorService = new UserVerificationMediatorService(
|
||||
ServiceContainer.Resolve<IPlatformUtilsService>(),
|
||||
ServiceContainer.Resolve<IPasswordRepromptService>(),
|
||||
userPinService,
|
||||
ServiceContainer.Resolve<IDeviceActionService>(),
|
||||
ServiceContainer.Resolve<IUserVerificationService>());
|
||||
ServiceContainer.Register<IUserVerificationMediatorService>(userVerificationMediatorService);
|
||||
|
||||
ServiceContainer.Register<IFido2AuthenticatorService>(new Fido2AuthenticatorService(
|
||||
ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<ISyncService>(),
|
||||
ServiceContainer.Resolve<ICryptoFunctionService>()));
|
||||
ServiceContainer.Resolve<ICryptoFunctionService>(),
|
||||
userVerificationMediatorService));
|
||||
|
||||
ServiceContainer.Register<IWatchDeviceService>(new WatchDeviceService(ServiceContainer.Resolve<ICipherService>(),
|
||||
ServiceContainer.Resolve<IEnvironmentService>(),
|
||||
|
@ -1,20 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using Bit.Core.Abstractions;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Domain;
|
||||
using Bit.Core.Models.View;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Utilities.Fido2;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Xunit;
|
||||
using Bit.Core.Utilities;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Bit.Core.Test.Services
|
||||
{
|
||||
@ -37,41 +37,49 @@ namespace Bit.Core.Test.Services
|
||||
/// </summary>
|
||||
public Fido2AuthenticatorGetAssertionTests()
|
||||
{
|
||||
_credentialIds = [
|
||||
_credentialIds = new List<string>
|
||||
{
|
||||
"2a346a27-02c5-4967-ae9e-8a090a1a8ef3",
|
||||
"924e812b-540e-445f-a2fc-b392a1bf9f27",
|
||||
"547d7aea-0d0e-493c-bf86-d8587e730dc1",
|
||||
"c07c71c4-030f-4e24-b284-c853aad72e2b"
|
||||
];
|
||||
_rawCredentialIds = [
|
||||
[0x2a, 0x34, 0x6a, 0x27, 0x02, 0xc5, 0x49, 0x67, 0xae, 0x9e, 0x8a, 0x09, 0x0a, 0x1a, 0x8e, 0xf3],
|
||||
[0x92, 0x4e, 0x81, 0x2b, 0x54, 0x0e, 0x44, 0x5f, 0xa2, 0xfc, 0xb3, 0x92, 0xa1, 0xbf, 0x9f, 0x27],
|
||||
[0x54, 0x7d, 0x7a, 0xea, 0x0d, 0x0e, 0x49, 0x3c, 0xbf, 0x86, 0xd8, 0x58, 0x7e, 0x73, 0x0d, 0xc1],
|
||||
[0xc0, 0x7c, 0x71, 0xc4, 0x03, 0x0f, 0x4e, 0x24, 0xb2, 0x84, 0xc8, 0x53, 0xaa, 0xd7, 0x2e, 0x2b]
|
||||
];
|
||||
_ciphers = [
|
||||
};
|
||||
_rawCredentialIds = new List<byte[]>
|
||||
{
|
||||
new byte[] { 0x2a, 0x34, 0x6a, 0x27, 0x02, 0xc5, 0x49, 0x67, 0xae, 0x9e, 0x8a, 0x09, 0x0a, 0x1a, 0x8e, 0xf3 },
|
||||
new byte[] { 0x92, 0x4e, 0x81, 0x2b, 0x54, 0x0e, 0x44, 0x5f, 0xa2, 0xfc, 0xb3, 0x92, 0xa1, 0xbf, 0x9f, 0x27 },
|
||||
new byte[] { 0x54, 0x7d, 0x7a, 0xea, 0x0d, 0x0e, 0x49, 0x3c, 0xbf, 0x86, 0xd8, 0x58, 0x7e, 0x73, 0x0d, 0xc1 },
|
||||
new byte[] { 0xc0, 0x7c, 0x71, 0xc4, 0x03, 0x0f, 0x4e, 0x24, 0xb2, 0x84, 0xc8, 0x53, 0xaa, 0xd7, 0x2e, 0x2b }
|
||||
};
|
||||
_ciphers = new List<CipherView>
|
||||
{
|
||||
CreateCipherView(_credentialIds[0].ToString(), _rpId, false, false),
|
||||
CreateCipherView(_credentialIds[1].ToString(), _rpId, true, true),
|
||||
];
|
||||
};
|
||||
_selectedCipher = _ciphers[0];
|
||||
_selectedCipherCredentialId = _credentialIds[0];
|
||||
_selectedCipherRawCredentialId = _rawCredentialIds[0];
|
||||
_params = CreateParams(
|
||||
rpId: _rpId,
|
||||
allowCredentialDescriptorList: [
|
||||
new PublicKeyCredentialDescriptor {
|
||||
allowCredentialDescriptorList: new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Id = _rawCredentialIds[0],
|
||||
Type = Constants.DefaultFido2CredentialType
|
||||
},
|
||||
new PublicKeyCredentialDescriptor {
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Id = _rawCredentialIds[1],
|
||||
Type = Constants.DefaultFido2CredentialType
|
||||
},
|
||||
],
|
||||
requireUserVerification: false
|
||||
},
|
||||
userVerificationPreference: Fido2UserVerificationPreference.Discouraged
|
||||
);
|
||||
_sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(_ciphers);
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_ciphers[0].Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>().CanPerformUserVerificationPreferredAsync(Arg.Any<Fido2UserVerificationOptions>()).Returns(Task.FromResult(false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>().ShouldPerformMasterPasswordRepromptAsync(Arg.Any<Fido2UserVerificationOptions>()).Returns(Task.FromResult(false));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
@ -109,7 +117,8 @@ namespace Bit.Core.Test.Services
|
||||
public async Task GetAssertionAsync_AsksForAllCredentials_ParamsContainsAllowedCredentialsList()
|
||||
{
|
||||
// Arrange
|
||||
_params.AllowCredentialDescriptorList = [
|
||||
_params.AllowCredentialDescriptorList = new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor {
|
||||
Id = _rawCredentialIds[0],
|
||||
Type = Constants.DefaultFido2CredentialType
|
||||
@ -118,7 +127,7 @@ namespace Bit.Core.Test.Services
|
||||
Id = _rawCredentialIds[1],
|
||||
Type = Constants.DefaultFido2CredentialType
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// Act
|
||||
await _sutProvider.Sut.GetAssertionAsync(_params, _userInterface);
|
||||
@ -148,11 +157,11 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
[Fact]
|
||||
// Spec: Prompt the user to select a public key credential source `selectedCredential` from `credentialOptions`.
|
||||
// If requireUserVerification is true, the authorization gesture MUST include user verification.
|
||||
// If UserVerificationPreference is Required, the authorization gesture MUST include user verification.
|
||||
public async Task GetAssertionAsync_RequestsUserVerification_ParamsRequireUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = true;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Required;
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_ciphers[0].Id, true));
|
||||
|
||||
// Act
|
||||
@ -160,7 +169,30 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().PickCredentialAsync(Arg.Is<Fido2GetAssertionUserInterfaceCredential[]>(
|
||||
(credentials) => credentials.All((c) => c.RequireUserVerification == true)
|
||||
(credentials) => credentials.All((c) => c.UserVerificationPreference == Fido2UserVerificationPreference.Required)
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: Prompt the user to select a public key credential source `selectedCredential` from `credentialOptions`.
|
||||
// If UserVerificationPreference is Preferred and MP reprompt is on then the authorization gesture MUST include user verification.
|
||||
// If MP reprompt is off then the authorization gestue MAY include user verification
|
||||
public async Task GetAssertionAsync_RequestsPreferredUserVerification_ParamsPreferUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Preferred;
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_ciphers[0].Id, true));
|
||||
|
||||
// Act
|
||||
await _sutProvider.Sut.GetAssertionAsync(_params, _userInterface);
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().PickCredentialAsync(Arg.Is<Fido2GetAssertionUserInterfaceCredential[]>(
|
||||
(credentials) => credentials.Any((c) => _ciphers.First(cip => cip.Id == c.CipherId).Reprompt == CipherRepromptType.None && c.UserVerificationPreference == Fido2UserVerificationPreference.Preferred)
|
||||
));
|
||||
|
||||
await _userInterface.Received().PickCredentialAsync(Arg.Is<Fido2GetAssertionUserInterfaceCredential[]>(
|
||||
(credentials) => credentials.Any((c) => _ciphers.First(cip => cip.Id == c.CipherId).Reprompt != CipherRepromptType.None && c.UserVerificationPreference == Fido2UserVerificationPreference.Required)
|
||||
));
|
||||
}
|
||||
|
||||
@ -169,17 +201,18 @@ namespace Bit.Core.Test.Services
|
||||
// If `requireUserPresence` is true, the authorization gesture MUST include a test of user presence.
|
||||
// Comment: User presence is implied by the UI returning a credential.
|
||||
// Extension: UserVerification is required if the cipher requires reprompting.
|
||||
// Deviation: We send the actual preference instead of just a boolean, user presence (not user verification) is therefore required when that value is `discouraged`
|
||||
public async Task GetAssertionAsync_DoesNotRequestUserVerification_ParamsDoNotRequireUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = false;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Discouraged;
|
||||
|
||||
// Act
|
||||
await _sutProvider.Sut.GetAssertionAsync(_params, _userInterface);
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().PickCredentialAsync(Arg.Is<Fido2GetAssertionUserInterfaceCredential[]>(
|
||||
(credentials) => credentials.Select(c => c.RequireUserVerification).SequenceEqual(_ciphers.Select((c) => c.Reprompt == CipherRepromptType.Password))
|
||||
(credentials) => credentials.Select(c => c.UserVerificationPreference == Fido2UserVerificationPreference.Required).SequenceEqual(_ciphers.Select((c) => c.Reprompt == CipherRepromptType.Password))
|
||||
));
|
||||
}
|
||||
|
||||
@ -199,7 +232,7 @@ namespace Bit.Core.Test.Services
|
||||
public async Task GetAssertionAsync_ThrowsNotAllowed_NoUserVerificationWhenRequired()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = true;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Required;
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_selectedCipher.Id, false));
|
||||
|
||||
// Act and assert
|
||||
@ -212,8 +245,27 @@ namespace Bit.Core.Test.Services
|
||||
{
|
||||
// Arrange
|
||||
_selectedCipher.Reprompt = CipherRepromptType.Password;
|
||||
_params.RequireUserVerification = false;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Discouraged;
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_selectedCipher.Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>()
|
||||
.ShouldPerformMasterPasswordRepromptAsync(Arg.Is<Fido2UserVerificationOptions>(opt => opt.ShouldCheckMasterPasswordReprompt))
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => _sutProvider.Sut.GetAssertionAsync(_params, _userInterface));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
// Spec: If the user does not consent, return an error code equivalent to "NotAllowedError" and terminate the operation.
|
||||
public async Task GetAssertionAsync_ThrowsNotAllowed_PreferredUserVerificationPreference_CanPerformUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_selectedCipher.Reprompt = CipherRepromptType.Password;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Preferred;
|
||||
_userInterface.PickCredentialAsync(Arg.Any<Fido2GetAssertionUserInterfaceCredential[]>()).Returns((_selectedCipher.Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>()
|
||||
.CanPerformUserVerificationPreferredAsync(Arg.Any<Fido2UserVerificationOptions>())
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => _sutProvider.Sut.GetAssertionAsync(_params, _userInterface));
|
||||
@ -265,7 +317,7 @@ namespace Bit.Core.Test.Services
|
||||
var keyPair = GenerateKeyPair();
|
||||
var rpIdHashMock = RandomBytes(32);
|
||||
_params.Hash = RandomBytes(32);
|
||||
_params.RequireUserVerification = true;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Required;
|
||||
_selectedCipher.Login.MainFido2Credential.CounterValue = 9000;
|
||||
_selectedCipher.Login.MainFido2Credential.KeyValue = CoreHelpers.Base64UrlEncode(keyPair.ExportPkcs8PrivateKey());
|
||||
_sutProvider.GetDependency<ICryptoFunctionService>().HashAsync(_params.RpId, CryptoHashAlgorithm.Sha256).Returns(rpIdHashMock);
|
||||
@ -339,14 +391,14 @@ namespace Bit.Core.Test.Services
|
||||
};
|
||||
}
|
||||
|
||||
private Fido2AuthenticatorGetAssertionParams CreateParams(string? rpId = null, byte[]? hash = null, PublicKeyCredentialDescriptor[]? allowCredentialDescriptorList = null, bool? requireUserPresence = null, bool? requireUserVerification = null)
|
||||
private Fido2AuthenticatorGetAssertionParams CreateParams(string? rpId = null, byte[]? hash = null, PublicKeyCredentialDescriptor[]? allowCredentialDescriptorList = null, bool? requireUserPresence = null, Fido2UserVerificationPreference? userVerificationPreference = null)
|
||||
{
|
||||
return new Fido2AuthenticatorGetAssertionParams
|
||||
{
|
||||
RpId = rpId ?? "bitwarden.com",
|
||||
Hash = hash ?? RandomBytes(32),
|
||||
AllowCredentialDescriptorList = allowCredentialDescriptorList ?? null,
|
||||
RequireUserVerification = requireUserPresence ?? false
|
||||
UserVerificationPreference = userVerificationPreference ?? Fido2UserVerificationPreference.Preferred
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +34,16 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
public Fido2AuthenticatorMakeCredentialTests() {
|
||||
_credentialIds = new List<string> { "21d6aa04-92bd-4def-bf81-33f046924599", "f70c01ca-d1bf-4704-86e1-b07573aa17fa" };
|
||||
_rawCredentialIds = [
|
||||
[0x21, 0xd6, 0xaa, 0x04, 0x92, 0xbd, 0x4d, 0xef, 0xbf, 0x81, 0x33, 0xf0, 0x46, 0x92, 0x45, 0x99],
|
||||
[0xf7, 0x0c, 0x01, 0xca, 0xd1, 0xbf, 0x47, 0x04, 0x86, 0xe1, 0xb0, 0x75, 0x73, 0xaa, 0x17, 0xfa]
|
||||
];
|
||||
_ciphers = [
|
||||
_rawCredentialIds = new List<byte[]>
|
||||
{
|
||||
new byte[] { 0x21, 0xd6, 0xaa, 0x04, 0x92, 0xbd, 0x4d, 0xef, 0xbf, 0x81, 0x33, 0xf0, 0x46, 0x92, 0x45, 0x99 },
|
||||
new byte[] { 0xf7, 0x0c, 0x01, 0xca, 0xd1, 0xbf, 0x47, 0x04, 0x86, 0xe1, 0xb0, 0x75, 0x73, 0xaa, 0x17, 0xfa }
|
||||
};
|
||||
_ciphers = new List<CipherView>
|
||||
{
|
||||
CreateCipherView(true, _credentialIds[0], "bitwarden.com", false),
|
||||
CreateCipherView(true, _credentialIds[1], "bitwarden.com", true)
|
||||
];
|
||||
};
|
||||
_selectedCipherView = _ciphers[0];
|
||||
_selectedCipherCredentialId = _credentialIds[0];
|
||||
_selectedCipherRawCredentialId = _rawCredentialIds[0];
|
||||
@ -56,14 +58,16 @@ namespace Bit.Core.Test.Services
|
||||
Id = _rpId,
|
||||
Name = "Bitwarden"
|
||||
},
|
||||
CredTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialParameters {
|
||||
CredTypesAndPubKeyAlgs = new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Alg = (int) Fido2AlgorithmIdentifier.ES256
|
||||
}
|
||||
],
|
||||
},
|
||||
RequireResidentKey = false,
|
||||
RequireUserVerification = false,
|
||||
UserVerificationPreference = Fido2UserVerificationPreference.Discouraged,
|
||||
ExcludeCredentialDescriptorList = null
|
||||
};
|
||||
|
||||
@ -71,6 +75,8 @@ namespace Bit.Core.Test.Services
|
||||
_sutProvider.GetDependency<ICipherService>().EncryptAsync(Arg.Any<CipherView>()).Returns(_encryptedSelectedCipher);
|
||||
_sutProvider.GetDependency<ICipherService>().GetAsync(Arg.Is(_encryptedSelectedCipher.Id)).Returns(_encryptedSelectedCipher);
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_selectedCipherView.Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>().CanPerformUserVerificationPreferredAsync(Arg.Any<Fido2UserVerificationOptions>()).Returns(Task.FromResult(false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>().ShouldPerformMasterPasswordRepromptAsync(Arg.Any<Fido2UserVerificationOptions>()).Returns(Task.FromResult(false));
|
||||
|
||||
var cryptoServiceMock = Substitute.For<ICryptoService>();
|
||||
ServiceContainer.Register(typeof(CryptoService), cryptoServiceMock);
|
||||
@ -80,7 +86,7 @@ namespace Bit.Core.Test.Services
|
||||
{
|
||||
ServiceContainer.Reset();
|
||||
}
|
||||
|
||||
|
||||
#region invalid input parameters
|
||||
|
||||
[Fact]
|
||||
@ -88,12 +94,14 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_ThrowsNotSupported_NoSupportedAlgorithm()
|
||||
{
|
||||
// Arrange
|
||||
_params.CredTypesAndPubKeyAlgs = [
|
||||
new PublicKeyCredentialParameters {
|
||||
_params.CredTypesAndPubKeyAlgs = new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Alg = -257 // RS256 which we do not support
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotSupportedError>(() => _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface));
|
||||
@ -109,12 +117,14 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_InformsUser_ExcludedCredentialFound()
|
||||
{
|
||||
// Arrange
|
||||
_params.ExcludeCredentialDescriptorList = [
|
||||
new PublicKeyCredentialDescriptor {
|
||||
_params.ExcludeCredentialDescriptorList = new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Id = _rawCredentialIds[0]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Act
|
||||
try
|
||||
@ -133,12 +143,14 @@ namespace Bit.Core.Test.Services
|
||||
// Spec: return an error code equivalent to "NotAllowedError" and terminate the operation.
|
||||
public async Task MakeCredentialAsync_ThrowsNotAllowed_ExcludedCredentialFound()
|
||||
{
|
||||
_params.ExcludeCredentialDescriptorList = [
|
||||
new PublicKeyCredentialDescriptor {
|
||||
_params.ExcludeCredentialDescriptorList = new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Id = _rawCredentialIds[0]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface));
|
||||
}
|
||||
@ -148,12 +160,14 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_DoesNotInformAboutExcludedCredential_ExcludedCredentialBelongsToOrganization()
|
||||
{
|
||||
_ciphers[0].OrganizationId = "someOrganizationId";
|
||||
_params.ExcludeCredentialDescriptorList = [
|
||||
new PublicKeyCredentialDescriptor {
|
||||
_params.ExcludeCredentialDescriptorList = new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Id = _rawCredentialIds[0]
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
await _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface);
|
||||
|
||||
@ -168,7 +182,7 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_RequestsUserVerification_ParamsRequireUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = true;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Required;
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_selectedCipherView.Id, true));
|
||||
|
||||
// Act
|
||||
@ -176,7 +190,23 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().ConfirmNewCredentialAsync(Arg.Is<Fido2ConfirmNewCredentialParams>(
|
||||
(p) => p.UserVerification == true
|
||||
(p) => p.UserVerificationPreference == Fido2UserVerificationPreference.Required
|
||||
));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MakeCredentialAsync_RequestsUserVerificationPreferred_ParamsPrefersUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Preferred;
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_selectedCipherView.Id, true));
|
||||
|
||||
// Act
|
||||
await _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface);
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().ConfirmNewCredentialAsync(Arg.Is<Fido2ConfirmNewCredentialParams>(
|
||||
(p) => p.UserVerificationPreference == Fido2UserVerificationPreference.Preferred
|
||||
));
|
||||
}
|
||||
|
||||
@ -184,14 +214,14 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_DoesNotRequestUserVerification_ParamsDoNotRequireUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = false;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Discouraged;
|
||||
|
||||
// Act
|
||||
await _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface);
|
||||
|
||||
// Assert
|
||||
await _userInterface.Received().ConfirmNewCredentialAsync(Arg.Is<Fido2ConfirmNewCredentialParams>(
|
||||
(p) => p.UserVerification == false
|
||||
(p) => p.UserVerificationPreference == Fido2UserVerificationPreference.Discouraged
|
||||
));
|
||||
}
|
||||
|
||||
@ -236,7 +266,7 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_ThrowsNotAllowed_NoUserVerificationWhenRequiredByParams()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = true;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Required;
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_encryptedSelectedCipher.Id, false));
|
||||
|
||||
// Act & Assert
|
||||
@ -247,9 +277,27 @@ namespace Bit.Core.Test.Services
|
||||
public async Task MakeCredentialAsync_ThrowsNotAllowed_NoUserVerificationForCipherWithReprompt()
|
||||
{
|
||||
// Arrange
|
||||
_params.RequireUserVerification = false;
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Discouraged;
|
||||
_encryptedSelectedCipher.Reprompt = CipherRepromptType.Password;
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_encryptedSelectedCipher.Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>()
|
||||
.ShouldPerformMasterPasswordRepromptAsync(Arg.Is<Fido2UserVerificationOptions>(opt => opt.ShouldCheckMasterPasswordReprompt))
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MakeCredentialAsync_ThrowsNotAllowed_PreferredUserVerificationPreference_CanPerformUserVerification()
|
||||
{
|
||||
// Arrange
|
||||
_params.UserVerificationPreference = Fido2UserVerificationPreference.Preferred;
|
||||
_encryptedSelectedCipher.Reprompt = CipherRepromptType.Password;
|
||||
_userInterface.ConfirmNewCredentialAsync(Arg.Any<Fido2ConfirmNewCredentialParams>()).Returns((_encryptedSelectedCipher.Id, false));
|
||||
_sutProvider.GetDependency<IUserVerificationMediatorService>()
|
||||
.CanPerformUserVerificationPreferredAsync(Arg.Any<Fido2UserVerificationOptions>())
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => _sutProvider.Sut.MakeCredentialAsync(_params, _userInterface));
|
||||
@ -297,10 +345,10 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
Assert.Equal(71 + 77, authData.Length);
|
||||
Assert.Equal(rpIdHashMock, rpIdHash);
|
||||
Assert.Equal([0b01011001], flags); // UP = true, AD = true, BS = true, BE = true
|
||||
Assert.Equal([0, 0, 0, 0], counter);
|
||||
Assert.Equal(new byte[] { 0b01011001 }, flags); // UP = true, AD = true, BS = true, BE = true
|
||||
Assert.Equal(new byte[] { 0, 0, 0, 0 }, counter);
|
||||
Assert.Equal(Fido2AuthenticatorService.AAGUID, aaguid);
|
||||
Assert.Equal([0, 16], credentialIdLength); // 16 bytes because we're using GUIDs
|
||||
Assert.Equal(new byte[] { 0, 16 }, credentialIdLength); // 16 bytes because we're using GUIDs
|
||||
Assert.Equal(credentialIdBytes, credentialId);
|
||||
}
|
||||
|
||||
@ -339,7 +387,7 @@ namespace Bit.Core.Test.Services
|
||||
Id = Guid.NewGuid().ToString(),
|
||||
Type = CipherType.Login,
|
||||
Key = null,
|
||||
Attachments = [],
|
||||
Attachments = new List<Attachment>(),
|
||||
Login = new Login {},
|
||||
};
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace Bit.Core.Test.Services
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
||||
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_NoCredentialsExist(SutProvider<Fido2AuthenticatorService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([]);
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(Task.FromResult(new List<CipherView>()));
|
||||
|
||||
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
|
||||
|
||||
@ -32,11 +32,12 @@ namespace Bit.Core.Test.Services
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
||||
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_OnlyNonDiscoverableCredentialsExist(SutProvider<Fido2AuthenticatorService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(Task.FromResult(new List<CipherView>
|
||||
{
|
||||
CreateCipherView("bitwarden.com", false),
|
||||
CreateCipherView("bitwarden.com", false),
|
||||
CreateCipherView("bitwarden.com", false)
|
||||
]);
|
||||
}));
|
||||
|
||||
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
|
||||
|
||||
@ -47,10 +48,11 @@ namespace Bit.Core.Test.Services
|
||||
[InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })]
|
||||
public async Task SilentCredentialDiscoveryAsync_ReturnsEmptyArray_NoCredentialsWithMatchingRpIdExist(SutProvider<Fido2AuthenticatorService> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns([
|
||||
sutProvider.GetDependency<ICipherService>().GetAllDecryptedAsync().Returns(Task.FromResult(new List<CipherView>
|
||||
{
|
||||
CreateCipherView("a.bitwarden.com", true),
|
||||
CreateCipherView("example.com", true)
|
||||
]);
|
||||
}));
|
||||
|
||||
var result = await sutProvider.Sut.SilentCredentialDiscoveryAsync("bitwarden.com");
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
@ -27,16 +28,18 @@ namespace Bit.Core.Test.Services
|
||||
Challenge = RandomBytes(32),
|
||||
RpId = "bitwarden.com",
|
||||
UserVerification = "required",
|
||||
AllowCredentials = [
|
||||
new PublicKeyCredentialDescriptor {
|
||||
AllowCredentials = new PublicKeyCredentialDescriptor[]
|
||||
{
|
||||
new PublicKeyCredentialDescriptor
|
||||
{
|
||||
Id = RandomBytes(32),
|
||||
Type = Constants.DefaultFido2CredentialType
|
||||
}
|
||||
],
|
||||
},
|
||||
Timeout = 60000,
|
||||
};
|
||||
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([]);
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>()));
|
||||
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true);
|
||||
}
|
||||
|
||||
@ -100,9 +103,11 @@ namespace Bit.Core.Test.Services
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "https://sub.bitwarden.com";
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>
|
||||
{
|
||||
"sub.bitwarden.com"
|
||||
]);
|
||||
|
||||
}));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.AssertCredentialAsync(_params));
|
||||
@ -195,7 +200,7 @@ namespace Bit.Core.Test.Services
|
||||
.GetAssertionAsync(
|
||||
Arg.Is<Fido2AuthenticatorGetAssertionParams>(x =>
|
||||
x.RpId == _params.RpId &&
|
||||
x.RequireUserVerification == true &&
|
||||
x.UserVerificationPreference == Fido2UserVerificationPreference.Required &&
|
||||
x.AllowCredentialDescriptorList.Length == 1 &&
|
||||
x.AllowCredentialDescriptorList[0].Id == _params.AllowCredentials[0].Id
|
||||
),
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
@ -22,17 +23,20 @@ namespace Bit.Core.Test.Services
|
||||
|
||||
public Fido2ClientCreateCredentialTests()
|
||||
{
|
||||
_params = new Fido2ClientCreateCredentialParams {
|
||||
_params = new Fido2ClientCreateCredentialParams
|
||||
{
|
||||
Origin = "https://bitwarden.com",
|
||||
SameOriginWithAncestors = true,
|
||||
Attestation = "none",
|
||||
Challenge = RandomBytes(32),
|
||||
PubKeyCredParams = [
|
||||
new PublicKeyCredentialParameters {
|
||||
PubKeyCredParams = new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters
|
||||
{
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Alg = (int) Fido2AlgorithmIdentifier.ES256
|
||||
}
|
||||
],
|
||||
},
|
||||
Rp = new PublicKeyCredentialRpEntity {
|
||||
Id = "bitwarden.com",
|
||||
Name = "Bitwarden"
|
||||
@ -44,7 +48,7 @@ namespace Bit.Core.Test.Services
|
||||
}
|
||||
};
|
||||
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([]);
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>()));
|
||||
_sutProvider.GetDependency<IStateService>().IsAuthenticatedAsync().Returns(true);
|
||||
}
|
||||
|
||||
@ -150,9 +154,10 @@ namespace Bit.Core.Test.Services
|
||||
{
|
||||
// Arrange
|
||||
_params.Origin = "https://sub.bitwarden.com";
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns([
|
||||
_sutProvider.GetDependency<IStateService>().GetAutofillBlacklistedUrisAsync().Returns(Task.FromResult(new List<string>
|
||||
{
|
||||
"sub.bitwarden.com"
|
||||
]);
|
||||
}));
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
@ -166,7 +171,8 @@ namespace Bit.Core.Test.Services
|
||||
public async Task CreateCredentialAsync_ThrowsNotSupportedError_CredTypesAndPubKeyAlgsIsEmpty()
|
||||
{
|
||||
// Arrange
|
||||
_params.PubKeyCredParams = [
|
||||
_params.PubKeyCredParams = new PublicKeyCredentialParameters[]
|
||||
{
|
||||
new PublicKeyCredentialParameters {
|
||||
Type = "not-supported",
|
||||
Alg = (int) Fido2AlgorithmIdentifier.ES256
|
||||
@ -175,7 +181,7 @@ namespace Bit.Core.Test.Services
|
||||
Type = Constants.DefaultFido2CredentialType,
|
||||
Alg = -9001
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
// Act
|
||||
var exception = await Assert.ThrowsAsync<Fido2ClientException>(() => _sutProvider.Sut.CreateCredentialAsync(_params));
|
||||
@ -216,7 +222,7 @@ namespace Bit.Core.Test.Services
|
||||
.MakeCredentialAsync(
|
||||
Arg.Is<Fido2AuthenticatorMakeCredentialParams>(x =>
|
||||
x.RequireResidentKey == true &&
|
||||
x.RequireUserVerification == true &&
|
||||
x.UserVerificationPreference == Fido2UserVerificationPreference.Required &&
|
||||
x.RpEntity.Id == _params.Rp.Id &&
|
||||
x.UserEntity.DisplayName == _params.User.DisplayName
|
||||
),
|
||||
@ -227,7 +233,7 @@ namespace Bit.Core.Test.Services
|
||||
Assert.Equal(authenticatorResult.AuthData, result.AuthData);
|
||||
Assert.Equal(authenticatorResult.PublicKey, result.PublicKey);
|
||||
Assert.Equal(authenticatorResult.PublicKeyAlgorithm, result.PublicKeyAlgorithm);
|
||||
Assert.Equal(["internal"], result.Transports);
|
||||
Assert.Equal(new string[] { "internal" }, result.Transports);
|
||||
|
||||
var clientDataJSON = JsonSerializer.Deserialize<JsonObject>(Encoding.UTF8.GetString(result.ClientDataJSON));
|
||||
Assert.Equal("webauthn.create", clientDataJSON["type"].GetValue<string>());
|
||||
|
@ -11,20 +11,24 @@ namespace Bit.Core.Test.Utilities.Fido2
|
||||
public async Task PickCredentialAsync_ThrowsNotAllowed_PrePickedCredentialDoesNotMatch()
|
||||
{
|
||||
// Arrange
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, null);
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, DefaultHasVaultBeenUnlockedInThisTransaction, DefaultVerifyUserAsync);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => userInterface.PickCredentialAsync([CreateCredential("notMatching", false)]));
|
||||
await Assert.ThrowsAsync<NotAllowedError>(() => userInterface.PickCredentialAsync(new Fido2GetAssertionUserInterfaceCredential[] { CreateCredential("notMatching", Fido2UserVerificationPreference.Discouraged) }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PickCredentialAsync_ReturnPrePickedCredential_CredentialsMatch()
|
||||
{
|
||||
// Arrange
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, null);
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, DefaultHasVaultBeenUnlockedInThisTransaction, DefaultVerifyUserAsync);
|
||||
|
||||
// Act
|
||||
var result = await userInterface.PickCredentialAsync([CreateCredential("cipherId", false), CreateCredential("cipherId2", true)]);
|
||||
var result = await userInterface.PickCredentialAsync(new Fido2GetAssertionUserInterfaceCredential[]
|
||||
{
|
||||
CreateCredential("cipherId", Fido2UserVerificationPreference.Discouraged),
|
||||
CreateCredential("cipherId2", Fido2UserVerificationPreference.Required)
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("cipherId", result.CipherId);
|
||||
@ -36,11 +40,18 @@ namespace Bit.Core.Test.Utilities.Fido2
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
var callback = () => { called = true; return Task.FromResult(true); };
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, callback);
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, null, DefaultHasVaultBeenUnlockedInThisTransaction, (_, __) =>
|
||||
{
|
||||
called = true;
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await userInterface.PickCredentialAsync([CreateCredential("cipherId", true), CreateCredential("cipherId2", false)]);
|
||||
var result = await userInterface.PickCredentialAsync(new Fido2GetAssertionUserInterfaceCredential[]
|
||||
{
|
||||
CreateCredential("cipherId", Fido2UserVerificationPreference.Required),
|
||||
CreateCredential("cipherId2", Fido2UserVerificationPreference.Discouraged)
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("cipherId", result.CipherId);
|
||||
@ -53,11 +64,18 @@ namespace Bit.Core.Test.Utilities.Fido2
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
var callback = () => { called = true; return Task.FromResult(true); };
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId2", true, null, callback);
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId2", true, null, DefaultHasVaultBeenUnlockedInThisTransaction, (_, __) =>
|
||||
{
|
||||
called = true;
|
||||
return Task.FromResult(true);
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = await userInterface.PickCredentialAsync([CreateCredential("cipherId", true), CreateCredential("cipherId2", false)]);
|
||||
var result = await userInterface.PickCredentialAsync(new Fido2GetAssertionUserInterfaceCredential[]
|
||||
{
|
||||
CreateCredential("cipherId", Fido2UserVerificationPreference.Required),
|
||||
CreateCredential("cipherId2", Fido2UserVerificationPreference.Discouraged)
|
||||
});
|
||||
|
||||
// Assert
|
||||
Assert.Equal("cipherId2", result.CipherId);
|
||||
@ -65,45 +83,34 @@ namespace Bit.Core.Test.Utilities.Fido2
|
||||
Assert.False(called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PickCredentialAsync_DoesNotCallUserVerificationCallback_UserVerificationIsNotRequired()
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
var callback = () => { called = true; return Task.FromResult(true); };
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId2", false, null, callback);
|
||||
|
||||
// Act
|
||||
var result = await userInterface.PickCredentialAsync([CreateCredential("cipherId", true), CreateCredential("cipherId2", false)]);
|
||||
|
||||
// Assert
|
||||
Assert.Equal("cipherId2", result.CipherId);
|
||||
Assert.False(result.UserVerified);
|
||||
Assert.False(called);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EnsureUnlockedVaultAsync_CallsCallback()
|
||||
{
|
||||
// Arrange
|
||||
var called = false;
|
||||
var callback = () => { called = true; return Task.CompletedTask; };
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, callback, null);
|
||||
var userInterface = new Fido2GetAssertionUserInterface("cipherId", false, callback, DefaultHasVaultBeenUnlockedInThisTransaction, DefaultVerifyUserAsync);
|
||||
|
||||
// Act
|
||||
await userInterface.EnsureUnlockedVaultAsync();
|
||||
|
||||
// Assert
|
||||
Assert.True(called);
|
||||
Assert.True(userInterface.HasVaultBeenUnlockedInThisTransaction);
|
||||
}
|
||||
|
||||
private Fido2GetAssertionUserInterfaceCredential CreateCredential(string cipherId, bool requireUserVerification)
|
||||
private Fido2GetAssertionUserInterfaceCredential CreateCredential(string cipherId, Fido2UserVerificationPreference userVerificationPreference)
|
||||
{
|
||||
return new Fido2GetAssertionUserInterfaceCredential
|
||||
{
|
||||
CipherId = cipherId,
|
||||
RequireUserVerification = requireUserVerification
|
||||
UserVerificationPreference = userVerificationPreference
|
||||
};
|
||||
}
|
||||
|
||||
private bool DefaultHasVaultBeenUnlockedInThisTransaction() => true;
|
||||
|
||||
private Task<bool> DefaultVerifyUserAsync(string _, Fido2UserVerificationPreference __) => Task.FromResult(false);
|
||||
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user