From 90ef67b05c03bc7e16bd962cbfaf3ecfa7277beb Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Thu, 5 Jun 2025 14:13:59 +0200 Subject: [PATCH] Add sync response --- .../Auth/Controllers/AccountsController.cs | 16 +++++++--- .../Billing/Controllers/AccountsController.cs | 7 +++-- .../Response/PrivateKeysResponseModel.cs | 30 ++++++++++++------- .../Models/Response/UserKeyResponseModel.cs | 16 ---------- .../Queries/UserAccountKeysQuery.cs | 25 ++++++++++++++++ .../Models/Response/ProfileResponseModel.cs | 5 ++++ src/Api/Vault/Controllers/SyncController.cs | 15 ++++++++-- .../Models/Response/SyncResponseModel.cs | 4 ++- src/Core/Entities/User.cs | 11 +++++++ .../Models/Data/UserAccountKeysData.cs | 6 ++-- .../Controllers/AccountsControllerTests.cs | 7 +++-- 11 files changed, 102 insertions(+), 40 deletions(-) delete mode 100644 src/Api/KeyManagement/Models/Response/UserKeyResponseModel.cs create mode 100644 src/Api/KeyManagement/Queries/UserAccountKeysQuery.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2499b269f5..1094c2f61b 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; 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 userAccountKeysData = await _userAccountKeysQuery.Run(user); + + var response = new ProfileResponseModel(user, userAccountKeysData, 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 userAccountKeysData = await _userAccountKeysQuery.Run(user); - var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser); + var response = new ProfileResponseModel(user, userAccountKeysData, 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 userAccountKeysData = await _userAccountKeysQuery.Run(user); - var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); + var response = new ProfileResponseModel(user, userAccountKeysData, 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..b67a9d1b81 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -1,4 +1,5 @@ #nullable enable +using Bit.Api.KeyManagement.Queries; using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; @@ -20,7 +21,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 +59,9 @@ public class AccountsController( var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); + var userAccountKeysData = await userAccountKeysQuery.Run(user); - var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, + var profile = new ProfileResponseModel(user, userAccountKeysData, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser); return new PaymentResponseModel { diff --git a/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs b/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs index 58555ea50f..78620ccdc1 100644 --- a/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs +++ b/src/Api/KeyManagement/Models/Response/PrivateKeysResponseModel.cs @@ -3,28 +3,36 @@ using Bit.Core.Models.Api; namespace Bit.Api.Models.Response; +#nullable enable + /// -/// This response model is used to return keys of a user - downstream of the user key - to the client. -/// This includes the private keys (signature/encryption), and proof tying one to another. This could -/// also be used to contain further user-owned keys in the future (per-vault keys, etc). This should -/// not be used to contain keys not just owned by the user (e.g. organization keys). +/// This response model is used to return the 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 PrivateAccountKeysResponseModel : ResponseModel +public class PrivateKeysResponseModel : ResponseModel { - public PrivateAccountKeysResponseModel(UserAccountKeysData accountKeys) : base("accountKeys") + public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("accountKeys") { - if (accountKeys != null) + if (accountKeys == null) { - SignatureKeyPair = accountKeys.signatureKeyPairData; + throw new ArgumentNullException(nameof(accountKeys)); + } + + if (accountKeys.SignatureKeyPairData != null) + { + SignatureKeyPair = accountKeys.SignatureKeyPairData; } PublicKeyEncryptionKeyPair = accountKeys.PublicKeyEncryptionKeyPairData; } - public PrivateAccountKeysResponseModel() : base("accountKeys") + public PrivateKeysResponseModel() : base("privateKeys") { } - public SignatureKeyPairData SignatureKeyPair { get; set; } - public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; } + // Not all accounts have signature keys, but all accounts have public encryption keys. + public SignatureKeyPairData? SignatureKeyPair { get; set; } + public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; } } diff --git a/src/Api/KeyManagement/Models/Response/UserKeyResponseModel.cs b/src/Api/KeyManagement/Models/Response/UserKeyResponseModel.cs deleted file mode 100644 index 95bcb82bd7..0000000000 --- a/src/Api/KeyManagement/Models/Response/UserKeyResponseModel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Models.Api; - -namespace Bit.Api.KeyManagement.Models.Response; - -public class UserKeyResponseModel : ResponseModel -{ - public UserKeyResponseModel(Guid id, string key) - : base("userKey") - { - UserId = id; - PublicKey = key; - } - - public Guid UserId { get; set; } - public string PublicKey { get; set; } -} diff --git a/src/Api/KeyManagement/Queries/UserAccountKeysQuery.cs b/src/Api/KeyManagement/Queries/UserAccountKeysQuery.cs new file mode 100644 index 0000000000..44ccdcdcf2 --- /dev/null +++ b/src/Api/KeyManagement/Queries/UserAccountKeysQuery.cs @@ -0,0 +1,25 @@ +#nullable enable + +using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; + +namespace Bit.Api.KeyManagement.Queries; + +public interface IUserAccountKeysQuery +{ + Task Run(User user); +} + +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/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index 246b3c3227..64ce16c1b2 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -2,6 +2,7 @@ using Bit.Api.AdminConsole.Models.Response.Providers; 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 +11,7 @@ namespace Bit.Api.Models.Response; public class ProfileResponseModel : ResponseModel { public ProfileResponseModel(User user, + UserAccountKeysData userAccountKeysData, IEnumerable organizationsUserDetails, IEnumerable providerUserDetails, IEnumerable providerUserOrganizationDetails, @@ -32,6 +34,7 @@ public class ProfileResponseModel : ResponseModel TwoFactorEnabled = twoFactorEnabled; Key = user.Key; PrivateKey = user.PrivateKey; + AccountKeys = userAccountKeysData; SecurityStamp = user.SecurityStamp; ForcePasswordReset = user.ForcePasswordReset; UsesKeyConnector = user.UsesKeyConnector; @@ -57,7 +60,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 UserAccountKeysData 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..37d99db059 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -8,6 +8,8 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Core.Services; @@ -39,6 +41,7 @@ public class SyncController : Controller private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository; public SyncController( IUserService userService, @@ -54,7 +57,8 @@ public class SyncController : Controller ICurrentContext currentContext, IFeatureService featureService, IApplicationCacheService applicationCacheService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IUserSignatureKeyPairRepository userSignatureKeyPairRepository) { _userService = userService; _folderRepository = folderRepository; @@ -70,6 +74,7 @@ public class SyncController : Controller _featureService = featureService; _applicationCacheService = applicationCacheService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _userSignatureKeyPairRepository = userSignatureKeyPairRepository; } [HttpGet("")] @@ -112,8 +117,14 @@ public class SyncController : Controller var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); + var signingKeys = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id); + var userAccountKeysData = new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(), + SignatureKeyPairData = signingKeys, + }; - var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities, + var response = new SyncResponseModel(_globalSettings, user, userAccountKeysData, 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..ad201ac624 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() + { + return new PublicKeyEncryptionKeyPairData + { + WrappedPrivateKey = PrivateKey, + SignedPublicKey = SignedPublicKey, + PublicKey = PublicKey + }; + } } diff --git a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs index 95fa221510..bfd33e690b 100644 --- a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs @@ -1,7 +1,9 @@ namespace Bit.Core.KeyManagement.Models.Data; +#nullable enable + public class UserAccountKeysData { - public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; } - public SignatureKeyPairData signatureKeyPairData { get; set; } + public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; } + public SignatureKeyPairData? SignatureKeyPairData { get; set; } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 581a7e8f04..9d76f0f2b6 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; 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 ); }