mirror of
https://github.com/bitwarden/server
synced 2025-12-31 23:53:17 +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:
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user