diff --git a/src/Core/Abstractions/IFido2UserInterface.cs b/src/Core/Abstractions/IFido2UserInterface.cs index b01b52d23..f3476cff3 100644 --- a/src/Core/Abstractions/IFido2UserInterface.cs +++ b/src/Core/Abstractions/IFido2UserInterface.cs @@ -42,5 +42,12 @@ namespace Bit.Core.Abstractions /// The parameters to use when asking the user to pick a credential. /// The ID of the cipher that contains the credentials the user picked. Task PickCredentialAsync(Fido2PickCredentialParams pickCredentialParams); + + /// + /// Inform the user that the operation was cancelled because their vault contains excluded credentials. + /// + /// The IDs of the excluded credentials. + /// When user has confirmed the message + Task InformExcludedCredential(string[] existingCipherIds); } } diff --git a/src/Core/Services/Fido2AuthenticatorService.cs b/src/Core/Services/Fido2AuthenticatorService.cs index 3490084c7..f2deb4376 100644 --- a/src/Core/Services/Fido2AuthenticatorService.cs +++ b/src/Core/Services/Fido2AuthenticatorService.cs @@ -23,7 +23,7 @@ namespace Bit.Core.Services _userInterface = userInterface; } - public Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams) + public async Task MakeCredentialAsync(Fido2AuthenticatorMakeCredentialParams makeCredentialParams) { if (makeCredentialParams.CredTypesAndPubKeyAlgs.All((p) => p.Algorithm != (int) Fido2AlgorithmIdentifier.ES256)) { @@ -34,6 +34,20 @@ namespace Bit.Core.Services throw new NotSupportedError(); } + // await _userInterface.EnsureUnlockedVault(); + await _syncService.FullSyncAsync(false); + + var existingCipherIds = await FindExcludedCredentials( + makeCredentialParams.ExcludeCredentialDescriptorList + ); + if (existingCipherIds.Length > 0) { + _logService.Info( + "[Fido2Authenticator] Aborting due to excluded credential found in vault." + ); + await _userInterface.InformExcludedCredential(existingCipherIds); + throw new NotAllowedError(); + } + throw new NotImplementedException(); } @@ -130,6 +144,40 @@ namespace Bit.Core.Services } } + /// + /// Finds existing crendetials and returns the `CipherId` for each one + /// + private async Task FindExcludedCredentials( + PublicKeyCredentialDescriptor[] credentials + ) { + var ids = new List(); + + foreach (var credential in credentials) + { + try + { + ids.Add(GuidToStandardFormat(credential.Id)); + } catch {} + } + + if (ids.Count == 0) { + return []; + } + + var ciphers = await _cipherService.GetAllDecryptedAsync(); + return ciphers + .FindAll( + (cipher) => + !cipher.IsDeleted && + cipher.OrganizationId == null && + cipher.Type == CipherType.Login && + cipher.Login.HasFido2Credentials && + ids.Contains(cipher.Login.MainFido2Credential.CredentialId) + ) + .Select((cipher) => cipher.Id) + .ToArray(); + } + private async Task> FindCredentialsById(PublicKeyCredentialDescriptor[] credentials, string rpId) { var ids = new List(); diff --git a/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs b/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs index 2e0ab09f3..f2b1e5186 100644 --- a/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs +++ b/src/Core/Utilities/Fido2/Fido2AuthenticatorMakeCredentialParams.cs @@ -22,6 +22,11 @@ namespace Bit.Core.Utilities.Fido2 /// public PublicKeyCredentialAlgorithmDescriptor[] CredTypesAndPubKeyAlgs { get; set; } + /// + ///An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials. + /// + public PublicKeyCredentialDescriptor[] ExcludeCredentialDescriptorList { get; set; } + /// /// The effective resident key requirement for credential creation, a Boolean value determined by the client. Resident is synonymous with discoverable. */ /// diff --git a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs index 51d9c604d..08d6d46f0 100644 --- a/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs +++ b/test/Core.Test/Services/Fido2AuthenticatorMakeCredentialTests.cs @@ -21,12 +21,12 @@ namespace Bit.Core.Test.Services { public class Fido2AuthenticatorMakeCredentialTests { - #region missing non-discoverable credential + #region invalid input parameters - // Spec: If credentialOptions is now empty, return an error code equivalent to "NotAllowedError" and terminate the operation. + // Spec: Check if at least one of the specified combinations of PublicKeyCredentialType and cryptographic parameters in credTypesAndPubKeyAlgs is supported. If not, return an error code equivalent to "NotSupportedError" and terminate the operation. [Theory] [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] - public async Task GetAssertionAsync_ThrowsNotSupported_NoSupportedAlgorithm(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams) + public async Task MakeCredentialAsync_ThrowsNotSupported_NoSupportedAlgorithm(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams) { mParams.CredTypesAndPubKeyAlgs = [ new PublicKeyCredentialAlgorithmDescriptor { @@ -38,24 +38,111 @@ namespace Bit.Core.Test.Services await Assert.ThrowsAsync(() => sutProvider.Sut.MakeCredentialAsync(mParams)); } - // [Theory] - // [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] - // public async Task GetAssertionAsync_Throws_CredentialExistsButRpIdDoesNotMatch(SutProvider sutProvider, Fido2AuthenticatorGetAssertionParams aParams) - // { - // var credentialId = Guid.NewGuid(); - // aParams.RpId = "bitwarden.com"; - // aParams.AllowCredentialDescriptorList = [ - // new PublicKeyCredentialDescriptor { - // Id = credentialId.ToByteArray(), - // Type = "public-key" - // } - // ]; - // sutProvider.GetDependency().GetAllDecryptedAsync().Returns([ - // CreateCipherView(credentialId.ToString(), "mismatch-rpid", false), - // ]); + #endregion - // await Assert.ThrowsAsync(() => sutProvider.Sut.GetAssertionAsync(aParams)); - // } + #region vault contains excluded credential + + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + // Spec: collect an authorization gesture confirming user consent for creating a new credential. + // Deviation: Consent is not asked and the user is simply informed of the situation. + public async Task MakeCredentialAsync_InformsUser_ExcludedCredentialFound(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams) + { + var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + List ciphers = [ + CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + ]; + mParams.CredTypesAndPubKeyAlgs = [ + new PublicKeyCredentialAlgorithmDescriptor { + Type = "public-key", + Algorithm = -7 // ES256 + } + ]; + mParams.RpId = "bitwarden.com"; + mParams.RequireUserVerification = false; + mParams.ExcludeCredentialDescriptorList = [ + new PublicKeyCredentialDescriptor { + Type = "public-key", + Id = credentialIds[0].ToByteArray() + } + ]; + sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); + + try + { + await sutProvider.Sut.MakeCredentialAsync(mParams); + } + catch {} + + await sutProvider.GetDependency().Received().InformExcludedCredential(Arg.Is( + (c) => c.SequenceEqual(new string[] { ciphers[0].Id }) + )); + } + + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + // Spec: return an error code equivalent to "NotAllowedError" and terminate the operation. + public async Task MakeCredentialAsync_ThrowsNotAllowed_ExcludedCredentialFound(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams) + { + var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + List ciphers = [ + CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + ]; + mParams.CredTypesAndPubKeyAlgs = [ + new PublicKeyCredentialAlgorithmDescriptor { + Type = "public-key", + Algorithm = -7 // ES256 + } + ]; + mParams.RpId = "bitwarden.com"; + mParams.RequireUserVerification = false; + mParams.ExcludeCredentialDescriptorList = [ + new PublicKeyCredentialDescriptor { + Type = "public-key", + Id = credentialIds[0].ToByteArray() + } + ]; + sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); + + await Assert.ThrowsAsync(() => sutProvider.Sut.MakeCredentialAsync(mParams)); + } + + [Theory] + [InlineCustomAutoData(new[] { typeof(SutProviderCustomization) })] + // Deviation: Organization ciphers are not checked against excluded credentials, even if the user has access to them. + public async Task MakeCredentialAsync_DoesNotInformAboutExcludedCredential_ExcludedCredentialBelongsToOrganization(SutProvider sutProvider, Fido2AuthenticatorMakeCredentialParams mParams) + { + var credentialIds = new[] { Guid.NewGuid(), Guid.NewGuid() }; + List ciphers = [ + CreateCipherView(credentialIds[0].ToString(), "bitwarden.com", false), + CreateCipherView(credentialIds[1].ToString(), "bitwarden.com", true) + ]; + ciphers[0].OrganizationId = "someOrganizationId"; + mParams.CredTypesAndPubKeyAlgs = [ + new PublicKeyCredentialAlgorithmDescriptor { + Type = "public-key", + Algorithm = -7 // ES256 + } + ]; + mParams.RpId = "bitwarden.com"; + mParams.RequireUserVerification = false; + mParams.ExcludeCredentialDescriptorList = [ + new PublicKeyCredentialDescriptor { + Type = "public-key", + Id = credentialIds[0].ToByteArray() + } + ]; + sutProvider.GetDependency().GetAllDecryptedAsync().Returns(ciphers); + + try + { + await sutProvider.Sut.MakeCredentialAsync(mParams); + } catch {} + + await sutProvider.GetDependency().DidNotReceive().InformExcludedCredential(Arg.Any()); + } #endregion