diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2499b269f5..f671411f3d 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -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> _webauthnKeyValidator; + private readonly IUserAccountKeysQuery _userAccountKeysQuery; public AccountsController( @@ -79,7 +81,8 @@ public class AccountsController : Controller emergencyAccessValidator, IRotationValidator, IReadOnlyList> organizationUserValidator, - IRotationValidator, IEnumerable> webAuthnKeyValidator + IRotationValidator, IEnumerable> 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; } diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index 10d386641d..270aa754e6 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -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 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 { diff --git a/src/Api/Controllers/UsersController.cs b/src/Api/Controllers/UsersController.cs deleted file mode 100644 index 4dfd047d37..0000000000 --- a/src/Api/Controllers/UsersController.cs +++ /dev/null @@ -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 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); - } -} diff --git a/src/Api/KeyManagement/Controllers/UsersController.cs b/src/Api/KeyManagement/Controllers/UsersController.cs new file mode 100644 index 0000000000..5972d62b8d --- /dev/null +++ b/src/Api/KeyManagement/Controllers/UsersController.cs @@ -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 GetPublicKeyAsync([FromRoute] Guid id) + { + var key = await _userRepository.GetPublicKeyAsync(id) ?? throw new NotFoundException(); + return new UserKeyResponseModel(id, key); + } + + [HttpGet("{id}/keys")] + public async Task 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); + } +} diff --git a/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs b/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs new file mode 100644 index 0000000000..10465931db --- /dev/null +++ b/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs @@ -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 + +/// +/// 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). +/// +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)); + } +} diff --git a/src/Api/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs b/src/Api/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs new file mode 100644 index 0000000000..8d62dbcdc1 --- /dev/null +++ b/src/Api/KeyManagement/Models/Response/PublicKeyEncryptionKeyPairResponseModel.cs @@ -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; + } +} diff --git a/src/Api/KeyManagement/Models/Response/PublicKeysResponseModel.cs b/src/Api/KeyManagement/Models/Response/PublicKeysResponseModel.cs new file mode 100644 index 0000000000..cc527ccd8d --- /dev/null +++ b/src/Api/KeyManagement/Models/Response/PublicKeysResponseModel.cs @@ -0,0 +1,31 @@ +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.Models.Api; + +namespace Bit.Api.KeyManagement.Models.Response; + +#nullable enable + +/// +/// 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. +/// +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; } +} diff --git a/src/Api/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs b/src/Api/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs new file mode 100644 index 0000000000..395f481abd --- /dev/null +++ b/src/Api/KeyManagement/Models/Response/SignatureKeyPairResponseModel.cs @@ -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)); + } +} diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index 246b3c3227..358739c386 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -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 organizationsUserDetails, IEnumerable providerUserDetails, IEnumerable 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; } diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 568c05d651..724d3f83ce 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -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; diff --git a/src/Api/Vault/Models/Response/SyncResponseModel.cs b/src/Api/Vault/Models/Response/SyncResponseModel.cs index b9da786567..485b59a1d1 100644 --- a/src/Api/Vault/Models/Response/SyncResponseModel.cs +++ b/src/Api/Vault/Models/Response/SyncResponseModel.cs @@ -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 organizationAbilities, @@ -36,7 +38,7 @@ public class SyncResponseModel : ResponseModel IEnumerable 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 => diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index fa66b72241..8df4765ecf 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -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, 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); + } } diff --git a/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs index c2b2c1c538..332fca3608 100644 --- a/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs +++ b/src/Core/KeyManagement/Entities/UserSignatureKeyPair.cs @@ -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, IRevisable { Id = CoreHelpers.GenerateComb(); } + + public SignatureKeyPairData ToSignatureKeyPairData() + { + return new SignatureKeyPairData(SignatureAlgorithm, SigningKey, VerifyingKey); + } } diff --git a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs index 102630c7e6..f53ded94dc 100644 --- a/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs +++ b/src/Core/KeyManagement/KeyManagementServiceCollectionExtensions.cs @@ -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(); } + + private static void AddKeyManagementQueries(this IServiceCollection services) + { + services.AddScoped(); + } } diff --git a/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs b/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs new file mode 100644 index 0000000000..76c43c666e --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/PublicKeyEncryptionKeyPairData.cs @@ -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; + } +} diff --git a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs new file mode 100644 index 0000000000..bfd33e690b --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs @@ -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; } +} diff --git a/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs b/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs new file mode 100644 index 0000000000..d58154e6e6 --- /dev/null +++ b/src/Core/KeyManagement/Queries/Interfaces/IUserAcountKeysQuery.cs @@ -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 Run(User user); +} diff --git a/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs b/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs new file mode 100644 index 0000000000..8c8eeed0d6 --- /dev/null +++ b/src/Core/KeyManagement/Queries/UserAccountKeysQuery.cs @@ -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 Run(User user) + { + var userAccountKeysData = new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id) + }; + return userAccountKeysData; + } +} diff --git a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs index ae2f8ec6f8..57df01cde9 100644 --- a/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs +++ b/src/Infrastructure.EntityFramework/KeyManagement/Repositories/UserSignatureKeyPairRepository.cs @@ -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) diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 581a7e8f04..bcbf1cabb3 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -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> _webauthnKeyRotationValidator; - + private readonly IUserAccountKeysQuery _userAccountKeysQuery; public AccountsControllerTests() { @@ -79,6 +80,7 @@ public class AccountsControllerTests : IDisposable _resetPasswordValidator = Substitute .For, IReadOnlyList>>(); + _userAccountKeysQuery = Substitute.For(); _sut = new AccountsController( _organizationService, @@ -96,7 +98,8 @@ public class AccountsControllerTests : IDisposable _sendValidator, _emergencyAccessValidator, _resetPasswordValidator, - _webauthnKeyRotationValidator + _webauthnKeyRotationValidator, + _userAccountKeysQuery ); } diff --git a/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs b/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs new file mode 100644 index 0000000000..7f6c72a62f --- /dev/null +++ b/test/Api.Test/KeyManagement/Controllers/UsersControllerTests.cs @@ -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 sutProvider) + { + sutProvider.GetDependency().GetPublicKeyAsync(Arg.Any()).ReturnsNull(); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetPublicKeyAsync(new Guid())); + } + + [Theory] + [BitAutoData] + public async Task GetPublicKey_ReturnsUserKeyResponseModel( + SutProvider sutProvider, + Guid userId) + { + var publicKey = "publicKey"; + sutProvider.GetDependency().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 sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).ReturnsNull(); + await Assert.ThrowsAsync(() => sutProvider.Sut.GetAccountKeysAsync(new Guid())); + } + + [Theory] + [BitAutoData] + public async Task GetAccountKeys_ReturnsPublicUserKeysResponseModel( + SutProvider sutProvider, + Guid userId) + { + var user = new User + { + Id = userId, + PublicKey = "publicKey", + SignedPublicKey = "signedPublicKey", + }; + + sutProvider.GetDependency().GetByIdAsync(userId).Returns(user); + sutProvider.GetDependency() + .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 sutProvider, + Guid userId) + { + var user = new User + { + Id = userId, + PublicKey = "publicKey", + SignedPublicKey = null, + }; + + sutProvider.GetDependency().GetByIdAsync(userId).Returns(user); + sutProvider.GetDependency() + .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); + } +} diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index ebbfc2a2ba..2b32ece33a 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -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(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // 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()).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(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // 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()).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(); var collectionRepository = sutProvider.GetDependency(); var collectionCipherRepository = sutProvider.GetDependency(); + var userAccountKeysQuery = sutProvider.GetDependency(); // 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(); diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 4a56d2cb22..0184d3702a 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -92,6 +92,7 @@ public class EfRepositoryListBuilder : ISpecimenBuilder where T : BaseEntityF cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); }) .CreateMapper()));