mirror of
https://github.com/bitwarden/server.git
synced 2025-06-19 18:38:03 -05:00
[PM-21034] Implement api changes to retreive signing keys (#5932)
* Add signing key repositories, models, and sql migration scripts * Rename UserSigningKeys table to UserSigningKey * Rename signedpublickeyownershipclaim to signedpublickey * Move signedPublicKey to last parameter * Add newline at end of file * Rename to signature key pair * Further rename to signaturekeypair * Rename to UserSignatureKeyPairRepository * Add newline * Rename more instances to UserSignatureKeyPair * Update parameter order * Fix order * Add more renames * Cleanup * Fix sql * Add ef migrations * Fix difference in SQL SP compared to migration SP * Fix difference in SQL SP vs migration * Fix difference in SQL SP vs migration * Attempt to fix sql * Rename migration to start later * Address feedback * Move UserSignatureKeyPair to KM codeownership * Fix build * Fix build * Fix build * Move out entitytypeconfiguration * Use view for reading usersignaturekeypairs * Fix migration script * Fix migration script * Add initial get keys endpoint * Add sync response * Cleanup * Add query and fix types * Add tests and cleanup * Fix test * Drop view if exists * Add km queries * Cleanup * Enable nullable * Cleanup * Cleanup * Enable nullable * Fix incorrect namespace * Remove unused using * Fix test build * Fix build error * Fix build * Attempt to fix tests * Attempt to fix tests * Replace with create or alter view * Attempt to fix tests * Attempt to fix build * Rename to include async suffix * Fix test * Rename repo * Attempt to fix tests * Cleanup * Test * Undo test * Fix tests * Fix test * Switch go generatecomb * Switch to generatecomb * Move signature algorithm * Move useresignaturekeypairentitytypeconfiguration to km ownership * Move userSignatureKeyPair model * Unswap file names * Move sql files to km ownership * Add index on userid for signature keys * Fix wrong filename * Fix build * Remove string length limit * Regenerate EF migrations * Undo changes to program.cs * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update util/Migrator/DbScripts/2025-06-01_00_AddSignatureKeyPairTable.sql Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Rename dbset to plural * Update src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/KeyManagement/Controllers/UsersController.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Cleanup and move query to core * Fix test * Fix build * Fix tests * Update src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Switch away from primary constructor * Use argumentNullException * Add test * Pass user account keys directly to profileresponsemodel * Move registration to core * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Update src/Api/Startup.cs Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com> * Remove empty line * Apply suggestions * Fix tests * Fix tests --------- Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
This commit is contained in:
parent
2b3d92b789
commit
e8d8914fe2
@ -3,6 +3,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.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -59,6 +60,7 @@ public class AccountsController : Controller
|
||||
_organizationUserValidator;
|
||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
_webauthnKeyValidator;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
|
||||
public AccountsController(
|
||||
@ -79,7 +81,8 @@ public class AccountsController : Controller
|
||||
emergencyAccessValidator,
|
||||
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
|
||||
organizationUserValidator,
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
|
||||
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
|
||||
IUserAccountKeysQuery userAccountKeysQuery
|
||||
)
|
||||
{
|
||||
_organizationService = organizationService;
|
||||
@ -98,6 +101,7 @@ public class AccountsController : Controller
|
||||
_emergencyAccessValidator = emergencyAccessValidator;
|
||||
_organizationUserValidator = organizationUserValidator;
|
||||
_webauthnKeyValidator = webAuthnKeyValidator;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
|
||||
@ -397,7 +401,9 @@ public class AccountsController : Controller
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
|
||||
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, accountKeys, organizationUserDetails, providerUserDetails,
|
||||
providerUserOrganizationDetails, twoFactorEnabled,
|
||||
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
@ -430,8 +436,9 @@ public class AccountsController : Controller
|
||||
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
var response = new ProfileResponseModel(user, userAccountKeys, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
}
|
||||
|
||||
@ -449,8 +456,9 @@ public class AccountsController : Controller
|
||||
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
var response = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Response;
|
||||
@ -20,7 +22,8 @@ namespace Bit.Api.Billing.Controllers;
|
||||
[Authorize("Application")]
|
||||
public class AccountsController(
|
||||
IUserService userService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery) : Controller
|
||||
{
|
||||
[HttpPost("premium")]
|
||||
public async Task<PaymentResponseModel> PostPremiumAsync(
|
||||
@ -57,8 +60,9 @@ public class AccountsController(
|
||||
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
|
||||
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
|
||||
var accountKeys = await userAccountKeysQuery.Run(user);
|
||||
|
||||
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
|
||||
var profile = new ProfileResponseModel(user, accountKeys, null, null, null, userTwoFactorEnabled,
|
||||
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
|
||||
return new PaymentResponseModel
|
||||
{
|
||||
|
@ -1,33 +0,0 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("users")]
|
||||
[Authorize("Application")]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public UsersController(
|
||||
IUserRepository userRepository)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/public-key")]
|
||||
public async Task<UserKeyResponseModel> Get(string id)
|
||||
{
|
||||
var guidId = new Guid(id);
|
||||
var key = await _userRepository.GetPublicKeyAsync(guidId);
|
||||
if (key == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new UserKeyResponseModel(guidId, key);
|
||||
}
|
||||
}
|
40
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
40
src/Api/KeyManagement/Controllers/UsersController.cs
Normal file
@ -0,0 +1,40 @@
|
||||
using Bit.Api.KeyManagement.Models.Response;
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using UserKeyResponseModel = Bit.Api.Models.Response.UserKeyResponseModel;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.KeyManagement.Controllers;
|
||||
|
||||
[Route("users")]
|
||||
[Authorize("Application")]
|
||||
public class UsersController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public UsersController(IUserRepository userRepository, IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
[HttpGet("{id}/public-key")]
|
||||
public async Task<UserKeyResponseModel> GetPublicKeyAsync([FromRoute] Guid id)
|
||||
{
|
||||
var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException();
|
||||
return new UserKeyResponseModel(id, key);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/keys")]
|
||||
public async Task<PublicKeysResponseModel> GetAccountKeysAsync([FromRoute] Guid id)
|
||||
{
|
||||
var user = await _userRepository.GetByIdAsync(id) ?? throw new NotFoundException();
|
||||
var accountKeys = await _userAccountKeysQuery.Run(user) ?? throw new NotFoundException("User account keys not found.");
|
||||
return new PublicKeysResponseModel(accountKeys);
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Response;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// This response model is used to return the asymmetric encryption keys,
|
||||
/// and signature keys of an entity. This includes the private keys of the key pairs,
|
||||
/// (private key, signing key), and the public keys of the key pairs (unsigned public key,
|
||||
/// signed public key, verification key).
|
||||
/// </summary>
|
||||
public class PrivateKeysResponseModel : ResponseModel
|
||||
{
|
||||
// Not all accounts have signature keys, but all accounts have public encryption keys.
|
||||
public SignatureKeyPairResponseModel? SignatureKeyPair { get; set; }
|
||||
public required PublicKeyEncryptionKeyPairModel PublicKeyEncryptionKeyPair { get; set; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("privateKeys")
|
||||
{
|
||||
PublicKeyEncryptionKeyPair = new PublicKeyEncryptionKeyPairModel(accountKeys.PublicKeyEncryptionKeyPairData);
|
||||
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||
|
||||
if (accountKeys.SignatureKeyPairData != null)
|
||||
{
|
||||
SignatureKeyPair = new SignatureKeyPairResponseModel(accountKeys.SignatureKeyPairData);
|
||||
}
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PrivateKeysResponseModel(SignatureKeyPairResponseModel? signatureKeyPair, PublicKeyEncryptionKeyPairModel publicKeyEncryptionKeyPair)
|
||||
: base("privateKeys")
|
||||
{
|
||||
SignatureKeyPair = signatureKeyPair;
|
||||
PublicKeyEncryptionKeyPair = publicKeyEncryptionKeyPair ?? throw new ArgumentNullException(nameof(publicKeyEncryptionKeyPair));
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Response;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class PublicKeyEncryptionKeyPairModel : ResponseModel
|
||||
{
|
||||
public required string WrappedPrivateKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeyEncryptionKeyPairModel(PublicKeyEncryptionKeyPairData keyPair)
|
||||
: base("publicKeyEncryptionKeyPair")
|
||||
{
|
||||
WrappedPrivateKey = keyPair.WrappedPrivateKey;
|
||||
PublicKey = keyPair.PublicKey;
|
||||
SignedPublicKey = keyPair.SignedPublicKey;
|
||||
}
|
||||
|
||||
[JsonConstructor]
|
||||
public PublicKeyEncryptionKeyPairModel(string wrappedPrivateKey, string publicKey, string? signedPublicKey)
|
||||
: base("publicKeyEncryptionKeyPair")
|
||||
{
|
||||
WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));
|
||||
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
|
||||
SignedPublicKey = signedPublicKey;
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Response;
|
||||
|
||||
#nullable enable
|
||||
|
||||
/// <summary>
|
||||
/// This response model is used to return the public keys of a user, to any other registered user or entity on the server.
|
||||
/// It can contain public keys (signature/encryption), and proofs between the two. It does not contain (encrypted) private keys.
|
||||
/// </summary>
|
||||
public class PublicKeysResponseModel : ResponseModel
|
||||
{
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeysResponseModel(UserAccountKeysData accountKeys)
|
||||
: base("publicKeys")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(accountKeys);
|
||||
PublicKey = accountKeys.PublicKeyEncryptionKeyPairData.PublicKey;
|
||||
|
||||
if (accountKeys.SignatureKeyPairData != null)
|
||||
{
|
||||
SignedPublicKey = accountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
|
||||
VerifyingKey = accountKeys.SignatureKeyPairData.VerifyingKey;
|
||||
}
|
||||
}
|
||||
|
||||
public string? VerifyingKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Models.Response;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class SignatureKeyPairResponseModel : ResponseModel
|
||||
{
|
||||
public required string WrappedSigningKey;
|
||||
public required string VerifyingKey;
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public SignatureKeyPairResponseModel(SignatureKeyPairData signatureKeyPair)
|
||||
: base("signatureKeyPair")
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(signatureKeyPair);
|
||||
WrappedSigningKey = signatureKeyPair.WrappedSigningKey;
|
||||
VerifyingKey = signatureKeyPair.VerifyingKey;
|
||||
}
|
||||
|
||||
|
||||
[JsonConstructor]
|
||||
public SignatureKeyPairResponseModel(string wrappedSigningKey, string verifyingKey)
|
||||
: base("signatureKeyPair")
|
||||
{
|
||||
WrappedSigningKey = wrappedSigningKey ?? throw new ArgumentNullException(nameof(wrappedSigningKey));
|
||||
VerifyingKey = verifyingKey ?? throw new ArgumentNullException(nameof(verifyingKey));
|
||||
}
|
||||
}
|
@ -1,7 +1,9 @@
|
||||
using Bit.Api.AdminConsole.Models.Response;
|
||||
using Bit.Api.AdminConsole.Models.Response.Providers;
|
||||
using Bit.Api.KeyManagement.Models.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
|
||||
@ -10,6 +12,7 @@ namespace Bit.Api.Models.Response;
|
||||
public class ProfileResponseModel : ResponseModel
|
||||
{
|
||||
public ProfileResponseModel(User user,
|
||||
UserAccountKeysData userAccountKeysData,
|
||||
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
|
||||
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
|
||||
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
|
||||
@ -32,6 +35,7 @@ public class ProfileResponseModel : ResponseModel
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
Key = user.Key;
|
||||
PrivateKey = user.PrivateKey;
|
||||
AccountKeys = new PrivateKeysResponseModel(userAccountKeysData);
|
||||
SecurityStamp = user.SecurityStamp;
|
||||
ForcePasswordReset = user.ForcePasswordReset;
|
||||
UsesKeyConnector = user.UsesKeyConnector;
|
||||
@ -57,7 +61,9 @@ public class ProfileResponseModel : ResponseModel
|
||||
public string Culture { get; set; }
|
||||
public bool TwoFactorEnabled { get; set; }
|
||||
public string Key { get; set; }
|
||||
[Obsolete("Use AccountKeys instead.")]
|
||||
public string PrivateKey { get; set; }
|
||||
public PrivateKeysResponseModel AccountKeys { get; set; }
|
||||
public string SecurityStamp { get; set; }
|
||||
public bool ForcePasswordReset { get; set; }
|
||||
public bool UsesKeyConnector { get; set; }
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@ -39,6 +40,7 @@ public class SyncController : Controller
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public SyncController(
|
||||
IUserService userService,
|
||||
@ -54,7 +56,8 @@ public class SyncController : Controller
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserAccountKeysQuery userAccountKeysQuery)
|
||||
{
|
||||
_userService = userService;
|
||||
_folderRepository = folderRepository;
|
||||
@ -70,6 +73,7 @@ public class SyncController : Controller
|
||||
_featureService = featureService;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_userAccountKeysQuery = userAccountKeysQuery;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -112,8 +116,9 @@ public class SyncController : Controller
|
||||
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
|
||||
|
||||
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var userAccountKeys = await _userAccountKeysQuery.Run(user);
|
||||
|
||||
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
var response = new SyncResponseModel(_globalSettings, user, userAccountKeys, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
|
||||
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
|
||||
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
|
||||
return response;
|
||||
|
@ -4,6 +4,7 @@ using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
@ -20,6 +21,7 @@ public class SyncResponseModel : ResponseModel
|
||||
public SyncResponseModel(
|
||||
GlobalSettings globalSettings,
|
||||
User user,
|
||||
UserAccountKeysData userAccountKeysData,
|
||||
bool userTwoFactorEnabled,
|
||||
bool userHasPremiumFromOrganization,
|
||||
IDictionary<Guid, OrganizationAbility> organizationAbilities,
|
||||
@ -36,7 +38,7 @@ public class SyncResponseModel : ResponseModel
|
||||
IEnumerable<Send> sends)
|
||||
: base("sync")
|
||||
{
|
||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
|
||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||
Folders = folders.Select(f => new FolderResponseModel(f));
|
||||
Ciphers = ciphers.Select(cipher =>
|
||||
|
@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
@ -254,4 +255,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
{
|
||||
return MasterPassword != null;
|
||||
}
|
||||
|
||||
public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(PrivateKey) || string.IsNullOrWhiteSpace(PublicKey))
|
||||
{
|
||||
throw new InvalidOperationException("User public key encryption key pair is not fully initialized.");
|
||||
}
|
||||
|
||||
return new PublicKeyEncryptionKeyPairData(PrivateKey, PublicKey, SignedPublicKey);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
#nullable enable
|
||||
@ -22,4 +23,9 @@ public class UserSignatureKeyPair : ITableObject<Guid>, IRevisable
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public SignatureKeyPairData ToSignatureKeyPairData()
|
||||
{
|
||||
return new SignatureKeyPairData(SignatureAlgorithm, SigningKey, VerifyingKey);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.KeyManagement.Commands;
|
||||
using Bit.Core.KeyManagement.Commands.Interfaces;
|
||||
using Bit.Core.KeyManagement.Queries;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Bit.Core.KeyManagement;
|
||||
@ -9,10 +11,16 @@ public static class KeyManagementServiceCollectionExtensions
|
||||
public static void AddKeyManagementServices(this IServiceCollection services)
|
||||
{
|
||||
services.AddKeyManagementCommands();
|
||||
services.AddKeyManagementQueries();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
|
||||
}
|
||||
|
||||
private static void AddKeyManagementQueries(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class PublicKeyEncryptionKeyPairData
|
||||
{
|
||||
public required string WrappedPrivateKey { get; set; }
|
||||
public string? SignedPublicKey { get; set; }
|
||||
public required string PublicKey { get; set; }
|
||||
|
||||
[JsonConstructor]
|
||||
[System.Diagnostics.CodeAnalysis.SetsRequiredMembersAttribute]
|
||||
public PublicKeyEncryptionKeyPairData(string wrappedPrivateKey, string publicKey, string? signedPublicKey = null)
|
||||
{
|
||||
WrappedPrivateKey = wrappedPrivateKey ?? throw new ArgumentNullException(nameof(wrappedPrivateKey));
|
||||
PublicKey = publicKey ?? throw new ArgumentNullException(nameof(publicKey));
|
||||
SignedPublicKey = signedPublicKey;
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
#nullable enable
|
||||
|
||||
public class UserAccountKeysData
|
||||
{
|
||||
public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }
|
||||
public SignatureKeyPairData? SignatureKeyPairData { get; set; }
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
namespace Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
|
||||
public interface IUserAccountKeysQuery
|
||||
{
|
||||
Task<UserAccountKeysData> Run(User user);
|
||||
}
|
22
src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
22
src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs
Normal file
@ -0,0 +1,22 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Queries;
|
||||
|
||||
|
||||
public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery
|
||||
{
|
||||
public async Task<UserAccountKeysData> Run(User user)
|
||||
{
|
||||
var userAccountKeysData = new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id)
|
||||
};
|
||||
return userAccountKeysData;
|
||||
}
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using AutoMapper;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.KeyManagement.Repositories;
|
||||
@ -22,11 +23,7 @@ public class UserSignatureKeyPairRepository(IServiceScopeFactory serviceScopeFac
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SignatureKeyPairData(
|
||||
signingKeys.SignatureAlgorithm,
|
||||
signingKeys.SigningKey,
|
||||
signingKeys.VerifyingKey
|
||||
);
|
||||
return signingKeys.ToSignatureKeyPairData();
|
||||
}
|
||||
|
||||
public UpdateEncryptedDataForKeyRotation SetUserSignatureKeyPair(Guid userId, SignatureKeyPairData signingKeys)
|
||||
|
@ -4,6 +4,7 @@ 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.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
@ -54,7 +55,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_resetPasswordValidator;
|
||||
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
|
||||
_webauthnKeyRotationValidator;
|
||||
|
||||
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
|
||||
|
||||
public AccountsControllerTests()
|
||||
{
|
||||
@ -79,6 +80,7 @@ public class AccountsControllerTests : IDisposable
|
||||
_resetPasswordValidator = Substitute
|
||||
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
|
||||
IReadOnlyList<OrganizationUser>>>();
|
||||
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
|
||||
|
||||
_sut = new AccountsController(
|
||||
_organizationService,
|
||||
@ -96,7 +98,8 @@ public class AccountsControllerTests : IDisposable
|
||||
_sendValidator,
|
||||
_emergencyAccessValidator,
|
||||
_resetPasswordValidator,
|
||||
_webauthnKeyRotationValidator
|
||||
_webauthnKeyRotationValidator,
|
||||
_userAccountKeysQuery
|
||||
);
|
||||
}
|
||||
|
||||
|
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
112
test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs
Normal file
@ -0,0 +1,112 @@
|
||||
#nullable enable
|
||||
using Bit.Api.KeyManagement.Controllers;
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Enums;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.KeyManagement.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(UsersController))]
|
||||
[SutProviderCustomize]
|
||||
[JsonDocumentCustomize]
|
||||
public class UsersControllerTests
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_NotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetPublicKeyAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetPublicKey_ReturnsUserKeyResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var publicKey = "publicKey";
|
||||
sutProvider.GetDependency<IUserRepository>().GetPublicKeyAsync(userId).Returns(publicKey);
|
||||
|
||||
var result = await sutProvider.Sut.GetPublicKeyAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(userId, result.UserId);
|
||||
Assert.Equal(publicKey, result.PublicKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_UserNotFound_ThrowsNotFoundException(
|
||||
SutProvider<UsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).ReturnsNull();
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetAccountKeysAsync(new Guid()));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = "signedPublicKey",
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", "signedPublicKey"),
|
||||
SignatureKeyPairData = new SignatureKeyPairData(SignatureAlgorithm.Ed25519, "wrappedSigningKey", "verifyingKey"),
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Equal("signedPublicKey", result.SignedPublicKey);
|
||||
Assert.Equal("verifyingKey", result.VerifyingKey);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel_WithNullVerifyingKey(
|
||||
SutProvider<UsersController> sutProvider,
|
||||
Guid userId)
|
||||
{
|
||||
var user = new User
|
||||
{
|
||||
Id = userId,
|
||||
PublicKey = "publicKey",
|
||||
SignedPublicKey = null,
|
||||
};
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(userId).Returns(user);
|
||||
sutProvider.GetDependency<IUserAccountKeysQuery>()
|
||||
.Run(user)
|
||||
.Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData("wrappedPrivateKey", "publicKey", null),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
var result = await sutProvider.Sut.GetAccountKeysAsync(userId);
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal("publicKey", result.PublicKey);
|
||||
Assert.Null(result.SignedPublicKey);
|
||||
Assert.Null(result.VerifyingKey);
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using AutoFixture;
|
||||
using Bit.Api.KeyManagement.Queries.Interfaces;
|
||||
using Bit.Api.Vault.Controllers;
|
||||
using Bit.Api.Vault.Models.Response;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
@ -12,6 +13,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.KeyManagement.Models.Data;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Repositories;
|
||||
@ -74,6 +76,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@ -98,6 +101,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@ -127,7 +135,6 @@ public class SyncControllerTests
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
|
||||
// Asserts
|
||||
// Assert that methods are called
|
||||
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
|
||||
@ -166,6 +173,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@ -189,6 +197,11 @@ public class SyncControllerTests
|
||||
|
||||
// Setup returns
|
||||
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
organizationUserRepository
|
||||
.GetManyDetailsByUserAsync(user.Id, OrganizationUserStatusType.Confirmed).Returns(organizationUserDetails);
|
||||
@ -256,6 +269,7 @@ public class SyncControllerTests
|
||||
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
|
||||
var collectionRepository = sutProvider.GetDependency<ICollectionRepository>();
|
||||
var collectionCipherRepository = sutProvider.GetDependency<ICollectionCipherRepository>();
|
||||
var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
|
||||
|
||||
// Adjust random data to match required formats / test intentions
|
||||
user.EquivalentDomains = JsonSerializer.Serialize(userEquivalentDomains);
|
||||
@ -290,6 +304,12 @@ public class SyncControllerTests
|
||||
twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false);
|
||||
userService.HasPremiumFromOrganization(user).Returns(false);
|
||||
|
||||
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
|
||||
{
|
||||
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
|
||||
SignatureKeyPairData = null,
|
||||
});
|
||||
|
||||
// Execute GET
|
||||
var result = await sutProvider.Sut.Get();
|
||||
|
||||
|
@ -92,6 +92,7 @@ public class EfRepositoryListBuilder<T> : ISpecimenBuilder where T : BaseEntityF
|
||||
cfg.AddProfile<TransactionMapperProfile>();
|
||||
cfg.AddProfile<UserMapperProfile>();
|
||||
cfg.AddProfile<PasswordHealthReportApplicationProfile>();
|
||||
cfg.AddProfile<UserSignatureKeyPairMapperProfile>();
|
||||
})
|
||||
.CreateMapper()));
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user