diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index b7e9c5bb8b11..2102756e6e63 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -68,7 +68,7 @@ public async Task Post([FromBody] WebAuthnCredentialRequestModel model) throw new BadRequestException("The token associated with your request is expired. A valid token is required to continue."); } - var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse); + var success = await _userService.CompleteWebAuthLoginRegistrationAsync(user, model.Name, tokenable.Options, model.DeviceResponse, model.SupportsPrf, model.EncryptedUserKey, model.EncryptedPublicKey, model.EncryptedPrivateKey); if (!success) { throw new BadRequestException("Unable to complete WebAuthn registration."); diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnCredentialRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnCredentialRequestModel.cs index 8f16fe7f5065..43eae3a805c0 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnCredentialRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnCredentialRequestModel.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; using Fido2NetLib; namespace Bit.Api.Auth.Models.Request.Webauthn; @@ -13,5 +14,20 @@ public class WebAuthnCredentialRequestModel [Required] public string Token { get; set; } + + [Required] + public bool SupportsPrf { get; set; } + + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } + + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPrivateKey { get; set; } } diff --git a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs index 0e358c751d32..01cf2559a6e5 100644 --- a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs +++ b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs @@ -1,4 +1,5 @@ using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; using Bit.Core.Models.Api; namespace Bit.Api.Auth.Models.Response.WebAuthn; @@ -11,10 +12,10 @@ public WebAuthnCredentialResponseModel(WebAuthnCredential credential) : base(Res { Id = credential.Id.ToString(); Name = credential.Name; - PrfSupport = false; + PrfStatus = credential.GetPrfStatus(); } public string Id { get; set; } public string Name { get; set; } - public bool PrfSupport { get; set; } + public WebAuthnPrfStatus PrfStatus { get; set; } } diff --git a/src/Core/Auth/Entities/WebAuthnCredential.cs b/src/Core/Auth/Entities/WebAuthnCredential.cs index b4b80ff65481..486fd41e3f3b 100644 --- a/src/Core/Auth/Entities/WebAuthnCredential.cs +++ b/src/Core/Auth/Entities/WebAuthnCredential.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using Bit.Core.Auth.Enums; using Bit.Core.Entities; using Bit.Core.Utilities; @@ -18,8 +19,11 @@ public class WebAuthnCredential : ITableObject [MaxLength(20)] public string Type { get; set; } public Guid AaGuid { get; set; } + [MaxLength(2000)] public string EncryptedUserKey { get; set; } + [MaxLength(2000)] public string EncryptedPrivateKey { get; set; } + [MaxLength(2000)] public string EncryptedPublicKey { get; set; } public bool SupportsPrf { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; @@ -29,4 +33,19 @@ public void SetNewId() { Id = CoreHelpers.GenerateComb(); } + + public WebAuthnPrfStatus GetPrfStatus() + { + if (!SupportsPrf) + { + return WebAuthnPrfStatus.Unsupported; + } + + if (EncryptedUserKey != null && EncryptedPrivateKey != null && EncryptedPublicKey != null) + { + return WebAuthnPrfStatus.Enabled; + } + + return WebAuthnPrfStatus.Supported; + } } diff --git a/src/Core/Auth/Enums/WebAuthnPrfStatus.cs b/src/Core/Auth/Enums/WebAuthnPrfStatus.cs new file mode 100644 index 000000000000..4977aacf71ac --- /dev/null +++ b/src/Core/Auth/Enums/WebAuthnPrfStatus.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Auth.Enums; + +public enum WebAuthnPrfStatus +{ + Enabled = 0, + Supported = 1, + Unsupported = 2 +} diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index e27668946638..736c730e6053 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -28,7 +28,7 @@ public interface IUserService Task DeleteWebAuthnKeyAsync(User user, int id); Task CompleteWebAuthRegistrationAsync(User user, int value, string name, AuthenticatorAttestationRawResponse attestationResponse); Task StartWebAuthnLoginRegistrationAsync(User user); - Task CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse); + Task CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null); Task StartWebAuthnLoginAssertionAsync(User user); Task CompleteWebAuthLoginAssertionAsync(AuthenticatorAssertionRawResponse assertionResponse, User user); Task SendEmailVerificationAsync(User user); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 3f29d14afb0f..e0d2e6aad5a0 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -543,9 +543,9 @@ public async Task StartWebAuthnLoginRegistrationAsync(U return options; } - public async Task CompleteWebAuthLoginRegistrationAsync(User user, string name, - CredentialCreateOptions options, - AuthenticatorAttestationRawResponse attestationResponse) + public async Task CompleteWebAuthLoginRegistrationAsync(User user, string name, CredentialCreateOptions options, + AuthenticatorAttestationRawResponse attestationResponse, bool supportsPrf, + string encryptedUserKey = null, string encryptedPublicKey = null, string encryptedPrivateKey = null) { var existingCredentials = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); if (existingCredentials.Count >= 5) @@ -566,7 +566,11 @@ public async Task CompleteWebAuthLoginRegistrationAsync(User user, string Type = success.Result.CredType, AaGuid = success.Result.Aaguid, Counter = (int)success.Result.Counter, - UserId = user.Id + UserId = user.Id, + SupportsPrf = supportsPrf, + EncryptedUserKey = encryptedUserKey, + EncryptedPublicKey = encryptedPublicKey, + EncryptedPrivateKey = encryptedPrivateKey }; await _webAuthnCredentialRepository.CreateAsync(credential); diff --git a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs index 32f2d5d49177..c9a7c0049d33 100644 --- a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs @@ -22,7 +22,7 @@ public class WebAuthnControllerTests [Theory, BitAutoData] public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -35,7 +35,7 @@ public async Task Get_UserNotFound_ThrowsUnauthorizedAccessException(SutProvider [Theory, BitAutoData] public async Task PostOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -62,7 +62,7 @@ public async Task PostOptions_UserVerificationFailed_ThrowsBadRequestException(S [Theory, BitAutoData] public async Task Post_UserNotFound_ThrowsUnauthorizedAccessException(WebAuthnCredentialRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act @@ -100,7 +100,7 @@ public async Task Post_ValidInput_Returns(WebAuthnCredentialRequestModel request .GetUserByPrincipalAsync(default) .ReturnsForAnyArgs(user); sutProvider.GetDependency() - .CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any()) + .CompleteWebAuthLoginRegistrationAsync(user, requestModel.Name, createOptions, Arg.Any(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey) .Returns(true); sutProvider.GetDependency>() .Unprotect(requestModel.Token) @@ -116,7 +116,7 @@ public async Task Post_ValidInput_Returns(WebAuthnCredentialRequestModel request [Theory, BitAutoData] public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider sutProvider) { - // Arrange + // Arrange sutProvider.GetDependency().GetUserByPrincipalAsync(default).ReturnsNullForAnyArgs(); // Act diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 7df36855a7ca..1438272e4a2c 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -193,7 +193,7 @@ public async void CompleteWebAuthLoginRegistrationAsync_ExceedsExistingCredentia sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(existingCredentials); // Act - var result = await sutProvider.Sut.CompleteWebAuthLoginRegistrationAsync(user, "name", options, response); + var result = await sutProvider.Sut.CompleteWebAuthLoginRegistrationAsync(user, "name", options, response, false, null, null, null); // Assert Assert.False(result);