Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand All @@ -57,7 +60,8 @@ public AccountsController(
IFeatureService featureService,
IUserAccountKeysQuery userAccountKeysQuery,
ITwoFactorEmailService twoFactorEmailService,
IChangeKdfCommand changeKdfCommand
IChangeKdfCommand changeKdfCommand,
ISetAccountKeysForUserCommand setAccountKeysForUserCommand
)
{
_organizationService = organizationService;
Expand All @@ -72,6 +76,7 @@ IChangeKdfCommand changeKdfCommand
_userAccountKeysQuery = userAccountKeysQuery;
_twoFactorEmailService = twoFactorEmailService;
_changeKdfCommand = changeKdfCommand;
_setAccountKeysForUserCommand = setAccountKeysForUserCommand;
}


Expand Down Expand Up @@ -440,8 +445,23 @@ public async Task<KeysResponseModel> 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")]
Expand All @@ -453,7 +473,8 @@ public async Task<KeysResponseModel> GetKeys()
throw new UnauthorizedAccessException();
}

return new KeysResponseModel(user);
var accountKeys = await _userAccountKeysQuery.Run(user);
return new KeysResponseModel(accountKeys, user.Key);
}

[HttpDelete]
Expand Down
27 changes: 16 additions & 11 deletions src/Api/Models/Response/KeysResponseModel.cs
Original file line number Diff line number Diff line change
@@ -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;

Check warning on line 17 in src/Api/Models/Response/KeysResponseModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysResponseModel.PublicKey' is obsolete: 'Use AccountKeys.PublicKeyEncryptionKeyPair.PublicKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrpXkAe4V7aZRWqjVnb&open=AZrpXkAe4V7aZRWqjVnb&pullRequest=6671
PrivateKey = accountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey;

Check warning on line 18 in src/Api/Models/Response/KeysResponseModel.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysResponseModel.PrivateKey' is obsolete: 'Use AccountKeys.PublicKeyEncryptionKeyPair.WrappedPrivateKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrpXkAe4V7aZRWqjVnc&open=AZrpXkAe4V7aZRWqjVnc&pullRequest=6671
AccountKeys = new PrivateKeysResponseModel(accountKeys);
}

public string Key { get; set; }
/// <summary>
/// The master key wrapped user key. The master key can either be a master-password master key or a
/// key-connector master key.
/// </summary>
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; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

โš ๏ธ MISSING VALIDATION: The AccountKeys property has no validation attributes, while the obsolete properties have [Required].

This creates an ambiguous state where neither old nor new properties might be provided. Consider adding validation:

public class KeysRequestModel : IValidatableObject
{
    // ... existing properties ...
    
    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (AccountKeys == null && (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey)))
        {
            yield return new ValidationResult(
                "Either AccountKeys or both PublicKey and EncryptedPrivateKey must be provided.",
                new[] { nameof(AccountKeys), nameof(PublicKey), nameof(EncryptedPrivateKey) });
        }
    }
}


[Obsolete("Use SetAccountKeysForUserCommand instead")]
public User ToUser(User existingUser)
{
if (string.IsNullOrWhiteSpace(PublicKey) || string.IsNullOrWhiteSpace(EncryptedPrivateKey))
Expand Down
64 changes: 63 additions & 1 deletion test/Api.Test/Auth/Controllers/AccountsControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -38,6 +40,7 @@
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
private readonly ISetAccountKeysForUserCommand _setAccountKeysForUserCommand;

public AccountsControllerTests()
{
Expand All @@ -53,6 +56,7 @@
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_twoFactorEmailService = Substitute.For<ITwoFactorEmailService>();
_changeKdfCommand = Substitute.For<IChangeKdfCommand>();
_setAccountKeysForUserCommand = Substitute.For<ISetAccountKeysForUserCommand>();

_sut = new AccountsController(
_organizationService,
Expand All @@ -66,7 +70,8 @@
_featureService,
_userAccountKeysQuery,
_twoFactorEmailService,
_changeKdfCommand
_changeKdfCommand,
_setAccountKeysForUserCommand
);
}

Expand Down Expand Up @@ -738,5 +743,62 @@
_userService.GetUserByIdAsync(Arg.Any<Guid>())
.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<ClaimsPrincipal>()).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<User>());
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";

Check warning on line 785 in test/Api.Test/Auth/Controllers/AccountsControllerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysRequestModel.PublicKey' is obsolete: 'Use AccountKeys.AccountPublicKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrgDk9pUDTcHg7RcGS0&open=AZrgDk9pUDTcHg7RcGS0&pullRequest=6671
model.EncryptedPrivateKey = "encrypted-private-key";

Check warning on line 786 in test/Api.Test/Auth/Controllers/AccountsControllerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysRequestModel.EncryptedPrivateKey' is obsolete: 'Use AccountKeys.UserKeyEncryptedAccountPrivateKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrgDk9pUDTcHg7RcGS1&open=AZrgDk9pUDTcHg7RcGS1&pullRequest=6671

_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.ReturnErrorOnExistingKeypair).Returns(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

๐Ÿ“ TEST COVERAGE GAP: This test doesn't validate the response construction, which would have caught the null reference bug at line 456 in the controller.

Recommendation: Add assertions to verify the response data:

// Assert
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
Assert.Equal(user.Key, result.Key);
Assert.NotNull(result.AccountKeys);
// Verify response contains data from the saved user, not null model.AccountKeys

You'll also need to mock _userAccountKeysQuery.Run(user) to return the expected data.


// Act
var result = await _sut.PostKeys(model);

// Assert
await _userService.Received(1).SaveUserAsync(Arg.Is<User>(u =>
u.PublicKey == model.PublicKey &&

Check warning on line 796 in test/Api.Test/Auth/Controllers/AccountsControllerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysRequestModel.PublicKey' is obsolete: 'Use AccountKeys.AccountPublicKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrgDk9pUDTcHg7RcGS2&open=AZrgDk9pUDTcHg7RcGS2&pullRequest=6671
u.PrivateKey == model.EncryptedPrivateKey));

Check warning on line 797 in test/Api.Test/Auth/Controllers/AccountsControllerTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

'KeysRequestModel.EncryptedPrivateKey' is obsolete: 'Use AccountKeys.UserKeyEncryptedAccountPrivateKey instead'

See more on https://sonarcloud.io/project/issues?id=bitwarden_server&issues=AZrgDk9pUDTcHg7RcGS3&open=AZrgDk9pUDTcHg7RcGS3&pullRequest=6671
await _setAccountKeysForUserCommand.DidNotReceiveWithAnyArgs()
.SetAccountKeysForUserAsync(Arg.Any<User>(), Arg.Any<AccountKeysRequestModel>());
Assert.NotNull(result);
Assert.Equal("keys", result.Object);
}
}

Loading