1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-20 02:48: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:
Bernd Schoolmann 2025-06-19 18:09:14 +02:00 committed by GitHub
parent 2b3d92b789
commit e8d8914fe2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 439 additions and 52 deletions

View File

@ -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;
}

View File

@ -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
{

View File

@ -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);
}
}

View 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);
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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));
}
}

View File

@ -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; }

View File

@ -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;

View File

@ -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 =>

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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>();
}
}

View File

@ -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;
}
}

View File

@ -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; }
}

View File

@ -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);
}

View 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;
}
}

View File

@ -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)

View File

@ -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
);
}

View 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);
}
}

View File

@ -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();

View File

@ -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()));