diff --git a/src/Core/Services/Fido2ClientService.cs b/src/Core/Services/Fido2ClientService.cs index 025b28412..1f0bea35e 100644 --- a/src/Core/Services/Fido2ClientService.cs +++ b/src/Core/Services/Fido2ClientService.cs @@ -5,7 +5,25 @@ namespace Bit.Core.Services { public class Fido2ClientService : IFido2ClientService { - public Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) => throw new NotImplementedException(); + public Task CreateCredentialAsync(Fido2ClientCreateCredentialParams createCredentialParams) + { + if (!createCredentialParams.SameOriginWithAncestors) + { + throw new Fido2ClientException( + Fido2ClientException.ErrorCode.NotAllowedError, + "Credential creation is now allowed from embedded contexts with different origins."); + } + + if (createCredentialParams.User.Id.Length < 1 || createCredentialParams.User.Id.Length > 64) + { + // TODO: Should we use ArgumentException here instead? + throw new Fido2ClientException( + Fido2ClientException.ErrorCode.TypeError, + "The length of user.id is not between 1 and 64 bytes (inclusive)."); + } + + throw new NotImplementedException(); + } public Task AssertCredentialAsync(Fido2ClientAssertCredentialParams assertCredentialParams) => throw new NotImplementedException(); } diff --git a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs index 49668ef38..f138f79d5 100644 --- a/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs +++ b/src/Core/Utilities/Fido2/Fido2ClientCreateCredentialParams.cs @@ -12,6 +12,7 @@ namespace Bit.Core.Utilities.Fido2 /// public required string Origin { get; set; } + // TODO: Check if we actually need this /// /// A value which is true if and only if the caller’s environment settings object is same-origin with its ancestors. /// It is false if caller is cross-origin. diff --git a/src/Core/Utilities/Fido2/Fido2ClientException.cs b/src/Core/Utilities/Fido2/Fido2ClientException.cs new file mode 100644 index 000000000..4fcfa52d3 --- /dev/null +++ b/src/Core/Utilities/Fido2/Fido2ClientException.cs @@ -0,0 +1,22 @@ +namespace Bit.Core.Utilities.Fido2 +{ + public class Fido2ClientException : Exception + { + public enum ErrorCode + { + NotAllowedError, + TypeError, + SecurityError, + UnknownError + } + + public readonly ErrorCode Code; + public readonly string Reason; + + public Fido2ClientException(ErrorCode code, string reason) : base($"{code} ({reason})") + { + Code = code; + Reason = reason; + } + } +} diff --git a/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs new file mode 100644 index 000000000..b250234e2 --- /dev/null +++ b/test/Core.Test/Services/Fido2ClientCreateCredentialTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Services; +using Bit.Core.Utilities.Fido2; +using Bit.Test.Common.AutoFixture; +using Xunit; + +namespace Bit.Core.Test.Services +{ + public class Fido2ClientCreateCredentialTests : IDisposable + { + private readonly SutProvider _sutProvider = new SutProvider().Create(); + + private Fido2ClientCreateCredentialParams _params; + + public Fido2ClientCreateCredentialTests() + { + _params = new Fido2ClientCreateCredentialParams { + Origin = "https://bitwarden.com", + SameOriginWithAncestors = true, + Attestation = "none", + Challenge = RandomBytes(32), + PubKeyCredParams = [ + new PublicKeyCredentialParameters { + Type = "public-key", + Alg = -7 + } + ], + Rp = new PublicKeyCredentialRpEntity { + Id = "bitwarden.com", + Name = "Bitwarden" + }, + User = new PublicKeyCredentialUserEntity { + Id = RandomBytes(32), + Name = "user@bitwarden.com", + DisplayName = "User" + } + }; + } + + public void Dispose() + { + } + + [Fact] + // Spec: If sameOriginWithAncestors is false, return a "NotAllowedError" DOMException. + public async Task CreateCredentialAsync_ThrowsNotAllowedError_SameOriginWithAncestorsIsFalse() + { + _params.SameOriginWithAncestors = false; + + var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.CreateCredentialAsync(_params)); + + Assert.Equal(Fido2ClientException.ErrorCode.NotAllowedError, exception.Code); + } + + [Fact] + // Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError. + public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooSmall() + { + _params.User.Id = RandomBytes(0); + + var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.CreateCredentialAsync(_params)); + + Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code); + } + + [Fact] + // Spec: If the length of options.user.id is not between 1 and 64 bytes (inclusive) then return a TypeError. + public async Task CreateCredentialAsync_ThrowsTypeError_UserIdIsTooLarge() + { + _params.User.Id = RandomBytes(65); + + var exception = await Assert.ThrowsAsync(() => _sutProvider.Sut.CreateCredentialAsync(_params)); + + Assert.Equal(Fido2ClientException.ErrorCode.TypeError, exception.Code); + } + + + private byte[] RandomBytes(int length) + { + var bytes = new byte[length]; + new Random().NextBytes(bytes); + return bytes; + } + } +}