diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index ecf49c18c8c9..ea07b0ebfba5 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -17,7 +17,9 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Models.Api.Response; using Bit.Core.Repositories; @@ -44,6 +46,7 @@ public class AccountsController : Controller private readonly IUserAccountKeysQuery _userAccountKeysQuery; private readonly ITwoFactorEmailService _twoFactorEmailService; private readonly IChangeKdfCommand _changeKdfCommand; + private readonly ISetAccountKeysForUserCommand _setAccountKeysForUserCommand; public AccountsController( IOrganizationService organizationService, @@ -57,7 +60,8 @@ public AccountsController( IFeatureService featureService, IUserAccountKeysQuery userAccountKeysQuery, ITwoFactorEmailService twoFactorEmailService, - IChangeKdfCommand changeKdfCommand + IChangeKdfCommand changeKdfCommand, + ISetAccountKeysForUserCommand setAccountKeysForUserCommand ) { _organizationService = organizationService; @@ -72,6 +76,7 @@ IChangeKdfCommand changeKdfCommand _userAccountKeysQuery = userAccountKeysQuery; _twoFactorEmailService = twoFactorEmailService; _changeKdfCommand = changeKdfCommand; + _setAccountKeysForUserCommand = setAccountKeysForUserCommand; } @@ -440,8 +445,23 @@ public async Task PostKeys([FromBody] KeysRequestModel model) } } - await _userService.SaveUserAsync(model.ToUser(user)); - return new KeysResponseModel(user); + if (model.AccountKeys != null) + { + await _setAccountKeysForUserCommand.SetAccountKeysForUserAsync(user, model.AccountKeys); + return new KeysResponseModel(model.AccountKeys?.ToAccountKeysData(), user.Key); + } + else + { + await _userService.SaveUserAsync(model.ToUser(user)); + return new KeysResponseModel(new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData( + user.PrivateKey, + user.PublicKey + ) + }, user.Key); + } + } [HttpGet("keys")] @@ -453,7 +473,8 @@ public async Task GetKeys() throw new UnauthorizedAccessException(); } - return new KeysResponseModel(user); + var accountKeys = await _userAccountKeysQuery.Run(user); + return new KeysResponseModel(accountKeys, user.Key); } [HttpDelete] diff --git a/src/Api/Models/Response/KeysResponseModel.cs b/src/Api/Models/Response/KeysResponseModel.cs index cfc1a6a0a165..4c877e0bfc46 100644 --- a/src/Api/Models/Response/KeysResponseModel.cs +++ b/src/Api/Models/Response/KeysResponseModel.cs @@ -1,27 +1,32 @@ -// FIXME: Update this file to be null safe and then delete the line below -#nullable disable - -using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Api.Response; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; public class KeysResponseModel : ResponseModel { - public KeysResponseModel(User user) + public KeysResponseModel(UserAccountKeysData accountKeys, string? masterKeyWrappedUserKey) : base("keys") { - if (user == null) + if (masterKeyWrappedUserKey != null) { - throw new ArgumentNullException(nameof(user)); + Key = masterKeyWrappedUserKey; } - Key = user.Key; - PublicKey = user.PublicKey; - PrivateKey = user.PrivateKey; + PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey; + PrivateKey = accountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey; + AccountKeys = new PrivateKeysResponseModel(accountKeys); } - public string Key { get; set; } + /// + /// The master key wrapped user key. The master key can either be a master-password master key or a + /// key-connector master key. + /// + public string? Key { get; set; } + [Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.PublicKey instead")] public string PublicKey { get; set; } + [Obsolete("Use AccountKeys.PublicKeyEncryptionKeyPair.WrappedPrivateKey instead")] public string PrivateKey { get; set; } + public PrivateKeysResponseModel AccountKeys { get; set; } } diff --git a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs index f89b67f3c560..85ddef44ce45 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/KeysRequestModel.cs @@ -3,17 +3,22 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Api.Request; using Bit.Core.Utilities; namespace Bit.Core.Auth.Models.Api.Request.Accounts; public class KeysRequestModel { + [Obsolete("Use AccountKeys.AccountPublicKey instead")] [Required] public string PublicKey { get; set; } + [Obsolete("Use AccountKeys.UserKeyEncryptedAccountPrivateKey instead")] [Required] public string EncryptedPrivateKey { get; set; } + public AccountKeysRequestModel AccountKeys { get; set; } + [Obsolete("Use SetAccountKeysForUserCommand instead")] public User ToUser(User existingUser) { if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey)) diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index f1aa11d068c7..457e9ef6c272 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -10,7 +10,9 @@ using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Commands.Interfaces; using Bit.Core.KeyManagement.Kdf; +using Bit.Core.KeyManagement.Models.Api.Request; using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Queries.Interfaces; using Bit.Core.Repositories; @@ -38,6 +40,7 @@ public class AccountsControllerTests : IDisposable private readonly IUserAccountKeysQuery _userAccountKeysQuery; private readonly ITwoFactorEmailService _twoFactorEmailService; private readonly IChangeKdfCommand _changeKdfCommand; + private readonly ISetAccountKeysForUserCommand _setAccountKeysForUserCommand; public AccountsControllerTests() { @@ -53,6 +56,7 @@ public AccountsControllerTests() _userAccountKeysQuery = Substitute.For(); _twoFactorEmailService = Substitute.For(); _changeKdfCommand = Substitute.For(); + _setAccountKeysForUserCommand = Substitute.For(); _sut = new AccountsController( _organizationService, @@ -66,7 +70,8 @@ public AccountsControllerTests() _featureService, _userAccountKeysQuery, _twoFactorEmailService, - _changeKdfCommand + _changeKdfCommand, + _setAccountKeysForUserCommand ); } @@ -738,5 +743,62 @@ private void ConfigureUserServiceToReturnNullUserId() _userService.GetUserByIdAsync(Arg.Any()) .Returns(Task.FromResult((User)null)); } + + [Theory, BitAutoData] + public async Task PostKeys_WithAccountKeys_CallsSetAccountKeysCommand( + User user, + KeysRequestModel model) + { + // Arrange + user.PublicKey = null; + user.PrivateKey = null; + model.AccountKeys = new AccountKeysRequestModel + { + UserKeyEncryptedAccountPrivateKey = "wrapped-private-key", + AccountPublicKey = "public-key" + }; + + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); + _featureService.IsEnabled(Bit.Core.FeatureFlagKeys.ReturnErrorOnExistingKeypair).Returns(false); + + // Act + var result = await _sut.PostKeys(model); + + // Assert + await _setAccountKeysForUserCommand.Received(1).SetAccountKeysForUserAsync( + user, + model.AccountKeys); + await _userService.DidNotReceiveWithAnyArgs().SaveUserAsync(Arg.Any()); + Assert.NotNull(result); + Assert.Equal("keys", result.Object); + } + + [Theory, BitAutoData] + public async Task PostKeys_WithoutAccountKeys_CallsSaveUser( + User user, + KeysRequestModel model) + { + // Arrange + user.PublicKey = null; + user.PrivateKey = null; + model.AccountKeys = null; + model.PublicKey = "public-key"; + model.EncryptedPrivateKey = "encrypted-private-key"; + + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); + _featureService.IsEnabled(Bit.Core.FeatureFlagKeys.ReturnErrorOnExistingKeypair).Returns(false); + + // Act + var result = await _sut.PostKeys(model); + + // Assert + await _userService.Received(1).SaveUserAsync(Arg.Is(u => + u.PublicKey == model.PublicKey && + u.PrivateKey == model.EncryptedPrivateKey)); + await _setAccountKeysForUserCommand.DidNotReceiveWithAnyArgs() + .SetAccountKeysForUserAsync(Arg.Any(), Arg.Any()); + Assert.NotNull(result); + Assert.Equal("keys", result.Object); + } }