From 3ad4bc1cab0ec4c6f517e490d12dd5ac55e63136 Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Mon, 17 Jun 2024 20:46:57 +0200 Subject: [PATCH] [PM-4371] Implement PRF key rotation (#4157) * Send rotateable keyset on list webauthn keys * Implement basic prf key rotation * Add validator for webauthn rotation * Fix accounts controller tests * Add webauthn rotation validator tests * Introduce separate request model * Fix tests * Remove extra empty line * Remove filtering in validator * Don't send encrypted private key * Fix tests * Implement delegated webauthn db transactions * Add backward compatibility * Fix query not working * Update migration sql * Update dapper query * Remove unused helper * Rename webauthn to WebAuthnLogin * Fix linter errors * Fix tests * Fix tests --- .../Auth/Controllers/AccountsController.cs | 10 +- .../Auth/Controllers/WebAuthnController.cs | 2 +- .../Request/Accounts/UpdateKeyRequestModel.cs | 2 + ...AuthnLoginCredentialCreatelRequestModel.cs | 2 +- ...bAuthnLoginCredentialUpdateRequestModel.cs | 2 +- .../WebAuthnLoginRotateKeyRequestModel.cs | 32 +++++++ .../WebAuthnCredentialResponseModel.cs | 9 ++ .../WebAuthnLoginKeyRotationValidator.cs | 55 +++++++++++ src/Api/Startup.cs | 6 +- .../Auth/Models/Data/RotateUserKeyData.cs | 1 + .../Models/Data/WebAuthnLoginRotateKeyData.cs | 21 +++++ .../IWebAuthnCredentialRepository.cs | 3 + .../Implementations/RotateUserKeyCommand.cs | 12 ++- .../WebAuthnCredentialRepository.cs | 36 +++++++ .../WebAuthnCredentialRepository.cs | 28 ++++++ .../Controllers/AccountsControllerTests.cs | 8 +- .../Controllers/WebAuthnControllerTests.cs | 2 +- .../WebauthnLoginKeyRotationValidatorTests.cs | 93 +++++++++++++++++++ .../UserKey/RotateUserKeyCommandTests.cs | 34 ++++++- 19 files changed, 347 insertions(+), 11 deletions(-) create mode 100644 src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs create mode 100644 src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs create mode 100644 src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs create mode 100644 test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 8acdbb7e87..f1b1cf6299 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -2,6 +2,7 @@ using Bit.Api.AdminConsole.Models.Response; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Validators; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; @@ -81,6 +82,8 @@ public class AccountsController : Controller private readonly IRotationValidator, IReadOnlyList> _organizationUserValidator; + private readonly IRotationValidator, IEnumerable> + _webauthnKeyValidator; public AccountsController( @@ -109,7 +112,8 @@ public class AccountsController : Controller IRotationValidator, IEnumerable> emergencyAccessValidator, IRotationValidator, IReadOnlyList> - organizationUserValidator + organizationUserValidator, + IRotationValidator, IEnumerable> webAuthnKeyValidator ) { _cipherRepository = cipherRepository; @@ -136,6 +140,7 @@ public class AccountsController : Controller _sendValidator = sendValidator; _emergencyAccessValidator = emergencyAccessValidator; _organizationUserValidator = organizationUserValidator; + _webauthnKeyValidator = webAuthnKeyValidator; } #region DEPRECATED (Moved to Identity Service) @@ -442,7 +447,8 @@ public class AccountsController : Controller Folders = await _folderValidator.ValidateAsync(user, model.Folders), Sends = await _sendValidator.ValidateAsync(user, model.Sends), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), - OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys) + OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.ResetPasswordKeys), + WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.WebAuthnKeys) }; var result = await _rotateUserKeyCommand.RotateUserKeyAsync(user, dataModel); diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs index 437c1ba20d..a66055b97a 100644 --- a/src/Api/Auth/Controllers/WebAuthnController.cs +++ b/src/Api/Auth/Controllers/WebAuthnController.cs @@ -1,5 +1,5 @@ using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.Webauthn; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Response.WebAuthn; using Bit.Api.Models.Response; using Bit.Core; diff --git a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs index cfeaec3248..d3cb5c2442 100644 --- a/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs +++ b/src/Api/Auth/Models/Request/Accounts/UpdateKeyRequestModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; @@ -19,5 +20,6 @@ public class UpdateKeyRequestModel public IEnumerable Sends { get; set; } public IEnumerable EmergencyAccessKeys { get; set; } public IEnumerable ResetPasswordKeys { get; set; } + public IEnumerable WebAuthnKeys { get; set; } } diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs index 2a3aa1dde9..8c6acbc8d4 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialCreatelRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Utilities; using Fido2NetLib; -namespace Bit.Api.Auth.Models.Request.Webauthn; +namespace Bit.Api.Auth.Models.Request.WebAuthn; public class WebAuthnLoginCredentialCreateRequestModel { diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs index 1d2e0813ef..54244c2dbd 100644 --- a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginCredentialUpdateRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Utilities; using Fido2NetLib; -namespace Bit.Api.Auth.Models.Request.Webauthn; +namespace Bit.Api.Auth.Models.Request.WebAuthn; public class WebAuthnLoginCredentialUpdateRequestModel { diff --git a/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs new file mode 100644 index 0000000000..7e161cfbea --- /dev/null +++ b/src/Api/Auth/Models/Request/WebAuthn/WebAuthnLoginRotateKeyRequestModel.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Utilities; + +namespace Bit.Api.Auth.Models.Request.WebAuthn; + +public class WebAuthnLoginRotateKeyRequestModel +{ + [Required] + public Guid Id { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } + + public WebAuthnLoginRotateKeyData ToWebAuthnRotateKeyData() + { + return new WebAuthnLoginRotateKeyData + { + Id = Id, + EncryptedUserKey = EncryptedUserKey, + EncryptedPublicKey = EncryptedPublicKey + }; + } + +} diff --git a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs index 01cf2559a6..3199dccd02 100644 --- a/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs +++ b/src/Api/Auth/Models/Response/WebAuthn/WebAuthnCredentialResponseModel.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Models.Api; +using Bit.Core.Utilities; namespace Bit.Api.Auth.Models.Response.WebAuthn; @@ -13,9 +14,17 @@ public class WebAuthnCredentialResponseModel : ResponseModel Id = credential.Id.ToString(); Name = credential.Name; PrfStatus = credential.GetPrfStatus(); + EncryptedUserKey = credential.EncryptedUserKey; + EncryptedPublicKey = credential.EncryptedPublicKey; } public string Id { get; set; } public string Name { get; set; } public WebAuthnPrfStatus PrfStatus { get; set; } + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } } diff --git a/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs b/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs new file mode 100644 index 0000000000..5c4d0ef302 --- /dev/null +++ b/src/Api/Auth/Validators/WebAuthnLoginKeyRotationValidator.cs @@ -0,0 +1,55 @@ +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Exceptions; + +namespace Bit.Api.Auth.Validators; + +public class WebAuthnLoginKeyRotationValidator : IRotationValidator, IEnumerable> +{ + private readonly IWebAuthnCredentialRepository _webAuthnCredentialRepository; + + public WebAuthnLoginKeyRotationValidator(IWebAuthnCredentialRepository webAuthnCredentialRepository) + { + _webAuthnCredentialRepository = webAuthnCredentialRepository; + } + + public async Task> ValidateAsync(User user, IEnumerable keysToRotate) + { + // 2024-06: Remove after 3 releases, for backward compatibility + if (keysToRotate == null) + { + return new List(); + } + + var result = new List(); + var existing = await _webAuthnCredentialRepository.GetManyByUserIdAsync(user.Id); + if (existing == null || !existing.Any()) + { + return result; + } + + foreach (var ea in existing) + { + var keyToRotate = keysToRotate.FirstOrDefault(c => c.Id == ea.Id); + if (keyToRotate == null) + { + throw new BadRequestException("All existing webauthn prf keys must be included in the rotation."); + } + + if (keyToRotate.EncryptedUserKey == null) + { + throw new BadRequestException("WebAuthn prf keys must have user-key during rotation."); + } + if (keyToRotate.EncryptedPublicKey == null) + { + throw new BadRequestException("WebAuthn prf keys must have public-key during rotation."); + } + + result.Add(keyToRotate.ToWebAuthnRotateKeyData()); + } + + return result; + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 63b1a3c3cd..fd2a4dbe6f 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -30,6 +30,8 @@ using Bit.Core.Billing.Extensions; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Core.Auth.Models.Data; #if !OSS using Bit.Commercial.Core.SecretsManager; @@ -163,7 +165,9 @@ public class Startup .AddScoped, IReadOnlyList> , OrganizationUserRotationValidator>(); - + services + .AddScoped, IEnumerable>, + WebAuthnLoginKeyRotationValidator>(); // Services services.AddBaseServices(globalSettings); diff --git a/src/Core/Auth/Models/Data/RotateUserKeyData.cs b/src/Core/Auth/Models/Data/RotateUserKeyData.cs index 52c6514770..f361c2a2cc 100644 --- a/src/Core/Auth/Models/Data/RotateUserKeyData.cs +++ b/src/Core/Auth/Models/Data/RotateUserKeyData.cs @@ -15,4 +15,5 @@ public class RotateUserKeyData public IReadOnlyList Sends { get; set; } public IEnumerable EmergencyAccesses { get; set; } public IReadOnlyList OrganizationUsers { get; set; } + public IEnumerable WebAuthnKeys { get; set; } } diff --git a/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs new file mode 100644 index 0000000000..40a096c474 --- /dev/null +++ b/src/Core/Auth/Models/Data/WebAuthnLoginRotateKeyData.cs @@ -0,0 +1,21 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; + +namespace Bit.Core.Auth.Models.Data; + +public class WebAuthnLoginRotateKeyData +{ + [Required] + public Guid Id { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedUserKey { get; set; } + + [Required] + [EncryptedString] + [EncryptedStringLength(2000)] + public string EncryptedPublicKey { get; set; } + +} diff --git a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs index 50f03744c5..1fab56d07a 100644 --- a/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs +++ b/src/Core/Auth/Repositories/IWebAuthnCredentialRepository.cs @@ -1,4 +1,6 @@ using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Repositories; namespace Bit.Core.Auth.Repositories; @@ -8,4 +10,5 @@ public interface IWebAuthnCredentialRepository : IRepository GetByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); Task UpdateAsync(WebAuthnCredential credential); + UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials); } diff --git a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs index a580629864..4c7ca20737 100644 --- a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs +++ b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; @@ -20,6 +21,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; + private readonly IWebAuthnCredentialRepository _credentialRepository; /// /// Instantiates a new @@ -35,7 +37,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, ICipherRepository cipherRepository, IFolderRepository folderRepository, ISendRepository sendRepository, IEmergencyAccessRepository emergencyAccessRepository, IOrganizationUserRepository organizationUserRepository, - IPushNotificationService pushService, IdentityErrorDescriber errors) + IPushNotificationService pushService, IdentityErrorDescriber errors, IWebAuthnCredentialRepository credentialRepository) { _userService = userService; _userRepository = userRepository; @@ -46,6 +48,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand _organizationUserRepository = organizationUserRepository; _pushService = pushService; _identityErrorDescriber = errors; + _credentialRepository = credentialRepository; } /// @@ -68,7 +71,7 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand user.Key = model.Key; user.PrivateKey = model.PrivateKey; if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccesses.Any() || - model.OrganizationUsers.Any()) + model.OrganizationUsers.Any() || model.WebAuthnKeys.Any()) { List saveEncryptedDataActions = new(); @@ -99,6 +102,11 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand _organizationUserRepository.UpdateForKeyRotation(user.Id, model.OrganizationUsers)); } + if (model.WebAuthnKeys.Any()) + { + saveEncryptedDataActions.Add(_credentialRepository.UpdateKeysForRotationAsync(user.Id, model.WebAuthnKeys)); + } + await _userRepository.UpdateUserKeyAndEncryptedDataAsync(user, saveEncryptedDataActions); } else diff --git a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs index d159157c0e..85a7cc64ef 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,7 +1,10 @@ using System.Data; using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Infrastructure.Dapper.Repositories; using Dapper; using Microsoft.Data.SqlClient; @@ -55,4 +58,37 @@ public class WebAuthnCredentialRepository : Repository return affectedRows > 0; } + + public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable credentials) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + const string sql = @" + UPDATE WC + SET + WC.[EncryptedPublicKey] = UW.[encryptedPublicKey], + WC.[EncryptedUserKey] = UW.[encryptedUserKey] + FROM + [dbo].[WebAuthnCredential] WC + INNER JOIN + OPENJSON(@JsonCredentials) + WITH ( + id UNIQUEIDENTIFIER, + encryptedPublicKey NVARCHAR(MAX), + encryptedUserKey NVARCHAR(MAX) + ) UW + ON UW.id = WC.Id + WHERE + WC.[UserId] = @UserId"; + + var jsonCredentials = CoreHelpers.ClassToJsonData(credentials); + + await connection.ExecuteAsync( + sql, + new { UserId = userId, JsonCredentials = jsonCredentials }, + transaction: transaction, + commandType: CommandType.Text); + }; + } + } diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs index cd3751a6d0..1499811880 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/WebAuthnCredentialRepository.cs @@ -1,5 +1,7 @@ using AutoMapper; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Infrastructure.EntityFramework.Auth.Models; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; @@ -56,4 +58,30 @@ public class WebAuthnCredentialRepository : Repository credentials) + { + return async (_, _) => + { + var newCreds = credentials.ToList(); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var userWebauthnCredentials = await GetDbSet(dbContext) + .Where(wc => wc.Id == wc.Id) + .ToListAsync(); + var validUserWebauthnCredentials = userWebauthnCredentials + .Where(wc => newCreds.Any(nwc => nwc.Id == wc.Id)) + .Where(wc => wc.UserId == userId); + + foreach (var wc in validUserWebauthnCredentials) + { + var nwc = newCreds.First(eak => eak.Id == wc.Id); + wc.EncryptedPublicKey = nwc.EncryptedPublicKey; + wc.EncryptedUserKey = nwc.EncryptedUserKey; + } + + await dbContext.SaveChangesAsync(); + }; + } + } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 9b6566bf64..1dd8fe064d 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; @@ -11,6 +12,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; @@ -67,6 +69,8 @@ public class AccountsControllerTests : IDisposable private readonly IRotationValidator, IReadOnlyList> _resetPasswordValidator; + private readonly IRotationValidator, IEnumerable> + _webauthnKeyRotationValidator; public AccountsControllerTests() @@ -97,6 +101,7 @@ public class AccountsControllerTests : IDisposable _sendValidator = Substitute.For, IReadOnlyList>>(); _emergencyAccessValidator = Substitute.For, IEnumerable>>(); + _webauthnKeyRotationValidator = Substitute.For, IEnumerable>>(); _resetPasswordValidator = Substitute .For, IReadOnlyList>>(); @@ -125,7 +130,8 @@ public class AccountsControllerTests : IDisposable _folderValidator, _sendValidator, _emergencyAccessValidator, - _resetPasswordValidator + _resetPasswordValidator, + _webauthnKeyRotationValidator ); } diff --git a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs index 85b0b9cab7..702fc7764a 100644 --- a/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/WebAuthnControllerTests.cs @@ -1,6 +1,6 @@ using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request.Accounts; -using Bit.Api.Auth.Models.Request.Webauthn; +using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; diff --git a/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs b/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs new file mode 100644 index 0000000000..97eadcbdc3 --- /dev/null +++ b/test/Api.Test/Auth/Validators/WebauthnLoginKeyRotationValidatorTests.cs @@ -0,0 +1,93 @@ +using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Api.Auth.Validators; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Auth.Validators; + +[SutProviderCustomize] +public class WebAuthnLoginKeyRotationValidatorTests +{ + [Theory] + [BitAutoData] + public async Task ValidateAsync_WrongWebAuthnKeys_Throws( + SutProvider sutProvider, User user, + IEnumerable webauthnRotateCredentialData) + { + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000001"), + EncryptedPublicKey = e.EncryptedPublicKey, + EncryptedUserKey = e.EncryptedUserKey + }).ToList(); + + var data = new WebAuthnCredential + { + Id = Guid.Parse("00000000-0000-0000-0000-000000000002"), + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_NullUserKey_Throws( + SutProvider sutProvider, User user, + IEnumerable webauthnRotateCredentialData) + { + var guid = Guid.NewGuid(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedPublicKey = e.EncryptedPublicKey, + }).ToList(); + + var data = new WebAuthnCredential + { + Id = guid, + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + + + [Theory] + [BitAutoData] + public async Task ValidateAsync_NullPublicKey_Throws( + SutProvider sutProvider, User user, + IEnumerable webauthnRotateCredentialData) + { + var guid = Guid.NewGuid(); + var webauthnKeysToRotate = webauthnRotateCredentialData.Select(e => new WebAuthnLoginRotateKeyRequestModel + { + Id = guid, + EncryptedUserKey = e.EncryptedUserKey, + }).ToList(); + + var data = new WebAuthnCredential + { + Id = guid, + EncryptedPublicKey = "TestKey", + EncryptedUserKey = "Test" + }; + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(new List { data }); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidateAsync(user, webauthnKeysToRotate)); + } + +} diff --git a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs b/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs index f97e55a674..41c78f4272 100644 --- a/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/UserKey/RotateUserKeyCommandTests.cs @@ -1,4 +1,6 @@ -using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.Entities; using Bit.Core.Services; @@ -19,6 +21,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(true); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); @@ -31,6 +43,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(false); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } var result = await sutProvider.Sut.RotateUserKeyAsync(user, model); @@ -43,6 +65,16 @@ public class RotateUserKeyCommandTests { sutProvider.GetDependency().CheckPasswordAsync(user, model.MasterPasswordHash) .Returns(true); + foreach (var webauthnCred in model.WebAuthnKeys) + { + var dbWebauthnCred = new WebAuthnCredential + { + EncryptedPublicKey = "encryptedPublicKey", + EncryptedUserKey = "encryptedUserKey" + }; + sutProvider.GetDependency().GetByIdAsync(webauthnCred.Id, user.Id) + .Returns(dbWebauthnCred); + } await sutProvider.Sut.RotateUserKeyAsync(user, model);