1
0
mirror of https://github.com/bitwarden/server synced 2025-12-31 07:33:43 +00:00

[PM-27280] Support v2 encryption on key-connector signups (#6712)

* account v2 registration for key connector

* use new user repository functions

* test coverage

* integration test coverage

* documentation

* code review

* missing test coverage

* fix failing test

* failing test

* incorrect ticket number

* moved back request model to Api, created dedicated data class in Core

* sql stored procedure type mismatch, simplification

* key connector authorization handler
This commit is contained in:
Maciej Zieniuk
2025-12-18 19:43:03 +01:00
committed by GitHub
parent 2b742b0343
commit a92d7ac129
22 changed files with 1283 additions and 50 deletions

View File

@@ -7,6 +7,7 @@ using Bit.Api.KeyManagement.Models.Responses;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
@@ -19,9 +20,11 @@ using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Vault.Enums;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.KeyManagement.Controllers;
@@ -31,6 +34,7 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
private static readonly string _mockEncryptedString =
"2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private static readonly string _mockEncryptedType7String = "7.AOs41Hd8OQiCPXjyJKCiDA==";
private static readonly string _mockEncryptedType7WrappedSigningKey = "7.DRv74Kg1RSlFSam1MNFlGD==";
private readonly HttpClient _client;
private readonly IEmergencyAccessRepository _emergencyAccessRepository;
@@ -47,8 +51,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
public AccountsKeyManagementControllerTests(ApiApplicationFactory factory)
{
_factory = factory;
_factory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"true");
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
.Returns(true);
});
_client = factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
_userRepository = _factory.GetService<IUserRepository>();
@@ -78,8 +85,11 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
{
// Localize factory to inject a false value for the feature flag.
var localFactory = new ApiApplicationFactory();
localFactory.UpdateConfiguration("globalSettings:launchDarkly:flagValues:pm-12241-private-key-regeneration",
"false");
localFactory.SubstituteService<IFeatureService>(featureService =>
{
featureService.IsEnabled(FeatureFlagKeys.PrivateKeyRegeneration, Arg.Any<bool>())
.Returns(false);
});
var localClient = localFactory.CreateClient();
var localEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
var localLoginHelper = new LoginHelper(localFactory, localClient);
@@ -285,21 +295,21 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier,
SetKeyConnectorKeyRequestModel request)
public async Task PostSetKeyConnectorKeyAsync_Success(string organizationSsoIdentifier)
{
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
var ssoUser = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(ssoUser);
request.Keys = new KeysRequestModel
var request = new SetKeyConnectorKeyRequestModel
{
PublicKey = ssoUser.PublicKey,
EncryptedPrivateKey = ssoUser.PrivateKey
Key = _mockEncryptedString,
Keys = new KeysRequestModel { PublicKey = ssoUser.PublicKey, EncryptedPrivateKey = ssoUser.PrivateKey },
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = organizationSsoIdentifier
};
request.Key = _mockEncryptedString;
request.OrgIdentifier = organizationSsoIdentifier;
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
response.EnsureSuccessStatusCode();
@@ -310,12 +320,95 @@ public class AccountsKeyManagementControllerTests : IClassFixture<ApiApplication
Assert.True(user.UsesKeyConnector);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
var ssoOrganizationUser = await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
Assert.NotNull(ssoOrganizationUser);
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
Assert.Null(ssoOrganizationUser.Email);
}
[Fact]
public async Task PostSetKeyConnectorKeyAsync_V2_NotLoggedIn_Unauthorized()
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "publicKey",
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String
},
OrgIdentifier = "test-org"
};
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_Success(string organizationSsoIdentifier)
{
var (ssoUserEmail, organization) = await SetupKeyConnectorTestAsync(OrganizationUserStatusType.Invited, organizationSsoIdentifier);
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _mockEncryptedString,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "publicKey",
UserKeyEncryptedAccountPrivateKey = _mockEncryptedType7String,
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairRequestModel
{
PublicKey = "publicKey",
WrappedPrivateKey = _mockEncryptedType7String,
SignedPublicKey = "signedPublicKey"
},
SignatureKeyPair = new SignatureKeyPairRequestModel
{
SignatureAlgorithm = "ed25519",
WrappedSigningKey = _mockEncryptedType7WrappedSigningKey,
VerifyingKey = "verifyingKey"
},
SecurityState = new SecurityStateModel
{
SecurityVersion = 2,
SecurityState = "v2"
}
},
OrgIdentifier = organizationSsoIdentifier
};
var response = await _client.PostAsJsonAsync("/accounts/set-key-connector-key", request);
response.EnsureSuccessStatusCode();
var user = await _userRepository.GetByEmailAsync(ssoUserEmail);
Assert.NotNull(user);
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, user.Key);
Assert.True(user.UsesKeyConnector);
Assert.Equal(KdfType.Argon2id, user.Kdf);
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, user.KdfIterations);
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, user.KdfMemory);
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, user.KdfParallelism);
Assert.Equal(request.AccountKeys.PublicKeyEncryptionKeyPair!.SignedPublicKey, user.SignedPublicKey);
Assert.Equal(request.AccountKeys.SecurityState!.SecurityState, user.SecurityState);
Assert.Equal(request.AccountKeys.SecurityState.SecurityVersion, user.SecurityVersion);
Assert.Equal(DateTime.UtcNow, user.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, user.AccountRevisionDate, TimeSpan.FromMinutes(1));
var ssoOrganizationUser =
await _organizationUserRepository.GetByOrganizationAsync(organization.Id, user.Id);
Assert.NotNull(ssoOrganizationUser);
Assert.Equal(OrganizationUserStatusType.Accepted, ssoOrganizationUser.Status);
Assert.Equal(user.Id, ssoOrganizationUser.UserId);
Assert.Null(ssoOrganizationUser.Email);
var signatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
Assert.NotNull(signatureKeyPair);
Assert.Equal(SignatureAlgorithm.Ed25519, signatureKeyPair.SignatureAlgorithm);
Assert.Equal(_mockEncryptedType7WrappedSigningKey, signatureKeyPair.WrappedSigningKey);
Assert.Equal("verifyingKey", signatureKeyPair.VerifyingKey);
}
[Fact]

View File

@@ -238,10 +238,13 @@ public class AccountsKeyManagementControllerTests
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_UserNull_Throws(
public async Task PostSetKeyConnectorKeyAsync_V1_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(data));
@@ -252,10 +255,13 @@ public class AccountsKeyManagementControllerTests
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeyFails_ThrowsBadRequestWithErrorResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
@@ -278,17 +284,20 @@ public class AccountsKeyManagementControllerTests
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_SetKeyConnectorKeySucceeds_OkResponse(
public async Task PostSetKeyConnectorKeyAsync_V1_SetKeyConnectorKeySucceeds_OkResponse(
SutProvider<AccountsKeyManagementController> sutProvider,
SetKeyConnectorKeyRequestModel data, User expectedUser)
{
data.KeyConnectorKeyWrappedUserKey = null;
data.AccountKeys = null;
expectedUser.PublicKey = null;
expectedUser.PrivateKey = null;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
@@ -308,11 +317,108 @@ public class AccountsKeyManagementControllerTests
Assert.Equal(data.KdfIterations, user.KdfIterations);
Assert.Equal(data.KdfMemory, user.KdfMemory);
Assert.Equal(data.KdfParallelism, user.KdfParallelism);
Assert.Equal(data.Keys.PublicKey, user.PublicKey);
Assert.Equal(data.Keys.EncryptedPrivateKey, user.PrivateKey);
Assert.Equal(data.Keys!.PublicKey, user.PublicKey);
Assert.Equal(data.Keys!.EncryptedPrivateKey, user.PrivateKey);
}), Arg.Is(data.Key), Arg.Is(data.OrgIdentifier));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_UserNull_Throws(
SutProvider<AccountsKeyManagementController> sutProvider)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsNull();
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().DidNotReceive()
.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>());
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_Success(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
await sutProvider.Sut.PostSetKeyConnectorKeyAsync(request);
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
Arg.Do<KeyConnectorKeysData>(data =>
{
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
}));
}
[Theory]
[BitAutoData]
public async Task PostSetKeyConnectorKeyAsync_V2_CommandThrows_PropagatesException(
SutProvider<AccountsKeyManagementController> sutProvider,
User expectedUser)
{
var request = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "wrapped-user-key",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = "public-key",
UserKeyEncryptedAccountPrivateKey = "encrypted-private-key"
},
OrgIdentifier = "test-org"
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>())
.Returns(expectedUser);
sutProvider.GetDependency<ISetKeyConnectorKeyCommand>()
.When(x => x.SetKeyConnectorKeyForUserAsync(Arg.Any<User>(), Arg.Any<KeyConnectorKeysData>()))
.Do(_ => throw new BadRequestException("Command failed"));
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.PostSetKeyConnectorKeyAsync(request));
Assert.Equal("Command failed", exception.Message);
await sutProvider.GetDependency<ISetKeyConnectorKeyCommand>().Received(1)
.SetKeyConnectorKeyForUserAsync(Arg.Is(expectedUser),
Arg.Do<KeyConnectorKeysData>(data =>
{
Assert.Equal(request.KeyConnectorKeyWrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(request.AccountKeys.AccountPublicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(request.AccountKeys.UserKeyEncryptedAccountPrivateKey,
data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(request.OrgIdentifier, data.OrgIdentifier);
}));
}
[Theory]
[BitAutoData]
public async Task PostConvertToKeyConnectorAsync_UserNull_Throws(

View File

@@ -0,0 +1,333 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;
using Xunit;
namespace Bit.Api.Test.KeyManagement.Models.Request;
public class SetKeyConnectorKeyRequestModelTests
{
private const string _wrappedUserKey = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=";
private const string _publicKey = "public-key";
private const string _privateKey = "private-key";
private const string _userKey = "user-key";
private const string _orgIdentifier = "org-identifier";
[Fact]
public void Validate_V2Registration_Valid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Empty(results);
}
[Fact]
public void Validate_V2Registration_WrappedUserKeyNotEncryptedString_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "not-encrypted-string",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results,
r => r.ErrorMessage == "KeyConnectorKeyWrappedUserKey is not a valid encrypted string.");
}
[Fact]
public void Validate_V1Registration_Valid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Empty(results);
}
[Fact]
public void Validate_V1Registration_MissingKey_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = null,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Key must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKeys_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = null,
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Keys must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKdf_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = null,
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "Kdf must be supplied.");
}
[Fact]
public void Validate_V1Registration_MissingKdfIterations_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = null,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfIterations must be supplied.");
}
[Fact]
public void Validate_V1Registration_Argon2id_MissingKdfMemory_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = null,
KdfParallelism = AuthConstants.ARGON2_PARALLELISM.Default,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfMemory must be supplied when Kdf is Argon2id.");
}
[Fact]
public void Validate_V1Registration_Argon2id_MissingKdfParallelism_Invalid()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
Key = _userKey,
Keys = new KeysRequestModel
{
PublicKey = _publicKey,
EncryptedPrivateKey = _privateKey
},
Kdf = KdfType.Argon2id,
KdfIterations = AuthConstants.ARGON2_ITERATIONS.Default,
KdfMemory = AuthConstants.ARGON2_MEMORY.Default,
KdfParallelism = null,
OrgIdentifier = _orgIdentifier
};
// Act
var results = Validate(model);
// Assert
Assert.Single(results);
Assert.Contains(results, r => r.ErrorMessage == "KdfParallelism must be supplied when Kdf is Argon2id.");
}
[Fact]
public void ToKeyConnectorKeysData_EmptyKeyConnectorKeyWrappedUserKey_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = "",
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_NullKeyConnectorKeyWrappedUserKey_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = null,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_NullAccountKeys_ThrowsException()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = null,
OrgIdentifier = _orgIdentifier
};
// Act
var exception = Assert.Throws<BadRequestException>(() => model.ToKeyConnectorKeysData());
// Assert
Assert.Equal("KeyConnectorKeyWrappedUserKey and AccountKeys must be supplied.", exception.Message);
}
[Fact]
public void ToKeyConnectorKeysData_Valid_Success()
{
// Arrange
var model = new SetKeyConnectorKeyRequestModel
{
KeyConnectorKeyWrappedUserKey = _wrappedUserKey,
AccountKeys = new AccountKeysRequestModel
{
AccountPublicKey = _publicKey,
UserKeyEncryptedAccountPrivateKey = _privateKey
},
OrgIdentifier = _orgIdentifier
};
// Act
var data = model.ToKeyConnectorKeysData();
// Assert
Assert.Equal(_wrappedUserKey, data.KeyConnectorKeyWrappedUserKey);
Assert.Equal(_publicKey, data.AccountKeys.AccountPublicKey);
Assert.Equal(_privateKey, data.AccountKeys.UserKeyEncryptedAccountPrivateKey);
Assert.Equal(_orgIdentifier, data.OrgIdentifier);
}
private static List<ValidationResult> Validate(SetKeyConnectorKeyRequestModel model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);
return results;
}
}

View File

@@ -0,0 +1,151 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Authorization;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Authorization;
[SutProviderCustomize]
public class KeyConnectorAuthorizationHandlerTests
{
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserCanUseKeyConnector_Success(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserAlreadyUsesKeyConnector_Fails(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsOwner_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Owner }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsAdmin_Fails(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.Admin }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UserIsRegularMember_Success(
User user,
Guid organizationId,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
var organizations = new List<CurrentContextOrganization>
{
new() { Id = organizationId, Type = OrganizationUserType.User }
};
sutProvider.GetDependency<ICurrentContext>().Organizations.Returns(organizations);
var requirement = KeyConnectorOperations.Use;
var context = new AuthorizationHandlerContext([requirement], claimsPrincipal, user);
// Act
await sutProvider.Sut.HandleAsync(context);
// Assert
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData]
public async Task HandleRequirementAsync_UnsupportedRequirement_ThrowsArgumentException(
User user,
ClaimsPrincipal claimsPrincipal,
SutProvider<KeyConnectorAuthorizationHandler> sutProvider)
{
// Arrange
user.UsesKeyConnector = false;
sutProvider.GetDependency<ICurrentContext>().Organizations
.Returns(new List<CurrentContextOrganization>());
var unsupportedRequirement = new KeyConnectorOperationsRequirement("UnsupportedOperation");
var context = new AuthorizationHandlerContext([unsupportedRequirement], claimsPrincipal, user);
// Act & Assert
await Assert.ThrowsAsync<ArgumentException>(() => sutProvider.Sut.HandleAsync(context));
}
}

View File

@@ -0,0 +1,125 @@
using System.Security.Claims;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.KeyManagement.Commands;
[SutProviderCustomize]
public class SetKeyConnectorKeyCommandTests
{
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_Success_SetsAccountKeys(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Set up valid V2 encryption data
if (data.AccountKeys!.SignatureKeyPair != null)
{
data.AccountKeys.SignatureKeyPair.SignatureAlgorithm = "ed25519";
}
var expectedAccountKeysData = data.AccountKeys.ToAccountKeysData();
// Arrange
user.UsesKeyConnector = false;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Success());
var userRepository = sutProvider.GetDependency<IUserRepository>();
var mockUpdateUserData = Substitute.For<UpdateUserData>();
userRepository.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey!)
.Returns(mockUpdateUserData);
// Act
await sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data);
// Assert
userRepository
.Received(1)
.SetKeyConnectorUserKey(user.Id, data.KeyConnectorKeyWrappedUserKey);
await userRepository
.Received(1)
.SetV2AccountCryptographicStateAsync(
user.Id,
Arg.Is<UserAccountKeysData>(data =>
data.PublicKeyEncryptionKeyPairData.PublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey &&
data.PublicKeyEncryptionKeyPairData.WrappedPrivateKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey &&
data.PublicKeyEncryptionKeyPairData.SignedPublicKey == expectedAccountKeysData.PublicKeyEncryptionKeyPairData.SignedPublicKey &&
data.SignatureKeyPairData!.SignatureAlgorithm == expectedAccountKeysData.SignatureKeyPairData!.SignatureAlgorithm &&
data.SignatureKeyPairData.WrappedSigningKey == expectedAccountKeysData.SignatureKeyPairData.WrappedSigningKey &&
data.SignatureKeyPairData.VerifyingKey == expectedAccountKeysData.SignatureKeyPairData.VerifyingKey &&
data.SecurityStateData!.SecurityState == expectedAccountKeysData.SecurityStateData!.SecurityState &&
data.SecurityStateData.SecurityVersion == expectedAccountKeysData.SecurityStateData.SecurityVersion),
Arg.Is<IEnumerable<UpdateUserData>>(actions =>
actions.Count() == 1 && actions.First() == mockUpdateUserData));
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.Received(1)
.AcceptOrgUserByOrgSsoIdAsync(data.OrgIdentifier, user, sutProvider.GetDependency<IUserService>());
}
[Theory, BitAutoData]
public async Task SetKeyConnectorKeyForUserAsync_UserCantUseKeyConnector_ThrowsException(
User user,
KeyConnectorKeysData data,
SutProvider<SetKeyConnectorKeyCommand> sutProvider)
{
// Arrange
user.UsesKeyConnector = true;
var currentContext = sutProvider.GetDependency<ICurrentContext>();
var httpContext = Substitute.For<HttpContext>();
httpContext.User.Returns(new ClaimsPrincipal());
currentContext.HttpContext.Returns(httpContext);
sutProvider.GetDependency<IAuthorizationService>()
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), user, Arg.Any<IEnumerable<IAuthorizationRequirement>>())
.Returns(AuthorizationResult.Failed());
// Act & Assert
await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SetKeyConnectorKeyForUserAsync(user, data));
sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetKeyConnectorUserKey(Arg.Any<Guid>(), Arg.Any<string>());
await sutProvider.GetDependency<IUserRepository>()
.DidNotReceiveWithAnyArgs()
.SetV2AccountCryptographicStateAsync(Arg.Any<Guid>(), Arg.Any<UserAccountKeysData>(), Arg.Any<IEnumerable<UpdateUserData>>());
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogUserEventAsync(Arg.Any<Guid>(), Arg.Any<EventType>());
await sutProvider.GetDependency<IAcceptOrgUserCommand>()
.DidNotReceiveWithAnyArgs()
.AcceptOrgUserByOrgSsoIdAsync(Arg.Any<string>(), Arg.Any<User>(), Arg.Any<IUserService>());
}
}

View File

@@ -1,9 +1,11 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Infrastructure.IntegrationTest.AdminConsole;
using Microsoft.Data.SqlClient;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Repositories;
@@ -500,4 +502,54 @@ public class UserRepositoryTests
// Assert
Assert.Empty(results);
}
[Theory, DatabaseData]
public async Task SetKeyConnectorUserKey_UpdatesUserKey(IUserRepository userRepository, Database database)
{
var user = await userRepository.CreateTestUserAsync();
const string keyConnectorWrappedKey = "key-connector-wrapped-user-key";
var setKeyConnectorUserKeyDelegate = userRepository.SetKeyConnectorUserKey(user.Id, keyConnectorWrappedKey);
await RunUpdateUserDataAsync(setKeyConnectorUserKeyDelegate, database);
var updatedUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(updatedUser);
Assert.Equal(keyConnectorWrappedKey, updatedUser.Key);
Assert.True(updatedUser.UsesKeyConnector);
Assert.Equal(KdfType.Argon2id, updatedUser.Kdf);
Assert.Equal(AuthConstants.ARGON2_ITERATIONS.Default, updatedUser.KdfIterations);
Assert.Equal(AuthConstants.ARGON2_MEMORY.Default, updatedUser.KdfMemory);
Assert.Equal(AuthConstants.ARGON2_PARALLELISM.Default, updatedUser.KdfParallelism);
Assert.Equal(DateTime.UtcNow, updatedUser.RevisionDate, TimeSpan.FromMinutes(1));
Assert.Equal(DateTime.UtcNow, updatedUser.AccountRevisionDate, TimeSpan.FromMinutes(1));
}
private static async Task RunUpdateUserDataAsync(UpdateUserData task, Database database)
{
if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf)
{
await using var connection = new SqlConnection(database.ConnectionString);
connection.Open();
await using var transaction = connection.BeginTransaction();
try
{
await task(connection, transaction);
transaction.Commit();
}
catch
{
transaction.Rollback();
throw;
}
}
else
{
await task();
}
}
}