1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-20 10:58:07 -05:00

Add sync response

This commit is contained in:
Bernd Schoolmann 2025-06-05 14:13:59 +02:00
parent 83c84a7cc0
commit 90ef67b05c
No known key found for this signature in database
11 changed files with 102 additions and 40 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;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Queries;
using Bit.Api.KeyManagement.Validators; using Bit.Api.KeyManagement.Validators;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
@ -59,6 +60,7 @@ public class AccountsController : Controller
_organizationUserValidator; _organizationUserValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator; _webauthnKeyValidator;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
public AccountsController( public AccountsController(
@ -79,7 +81,8 @@ public class AccountsController : Controller
emergencyAccessValidator, emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>> IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator, organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
IUserAccountKeysQuery userAccountKeysQuery
) )
{ {
_organizationService = organizationService; _organizationService = organizationService;
@ -98,6 +101,7 @@ public class AccountsController : Controller
_emergencyAccessValidator = emergencyAccessValidator; _emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator; _organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator; _webauthnKeyValidator = webAuthnKeyValidator;
_userAccountKeysQuery = userAccountKeysQuery;
} }
@ -397,7 +401,9 @@ public class AccountsController : Controller
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); 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, providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg, organizationIdsClaimingActiveUser); hasPremiumFromOrg, organizationIdsClaimingActiveUser);
return response; return response;
@ -430,8 +436,9 @@ public class AccountsController : Controller
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); 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; return response;
} }
@ -449,8 +456,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); 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; return response;
} }

View File

@ -1,4 +1,5 @@
#nullable enable #nullable enable
using Bit.Api.KeyManagement.Queries;
using Bit.Api.Models.Request; using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response; using Bit.Api.Models.Response;
@ -20,7 +21,8 @@ namespace Bit.Api.Billing.Controllers;
[Authorize("Application")] [Authorize("Application")]
public class AccountsController( public class AccountsController(
IUserService userService, IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery) : Controller
{ {
[HttpPost("premium")] [HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync( public async Task<PaymentResponseModel> PostPremiumAsync(
@ -57,8 +59,9 @@ public class AccountsController(
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); 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); userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
return new PaymentResponseModel return new PaymentResponseModel
{ {

View File

@ -3,28 +3,36 @@ using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response; namespace Bit.Api.Models.Response;
#nullable enable
/// <summary> /// <summary>
/// This response model is used to return keys of a user - downstream of the user key - to the client. /// This response model is used to return the the asymmetric encryption keys,
/// This includes the private keys (signature/encryption), and proof tying one to another. This could /// and signature keys of an entity. This includes the private keys of the key pairs,
/// also be used to contain further user-owned keys in the future (per-vault keys, etc). This should /// (private key, signing key), and the public keys of the key pairs (unsigned public key,
/// not be used to contain keys not just owned by the user (e.g. organization keys). /// signed public key, verification key).
/// </summary> /// </summary>
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; PublicKeyEncryptionKeyPair = accountKeys.PublicKeyEncryptionKeyPairData;
} }
public PrivateAccountKeysResponseModel() : base("accountKeys") public PrivateKeysResponseModel() : base("privateKeys")
{ {
} }
public SignatureKeyPairData SignatureKeyPair { get; set; } // Not all accounts have signature keys, but all accounts have public encryption keys.
public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; } public SignatureKeyPairData? SignatureKeyPair { get; set; }
public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; }
} }

View File

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

View File

@ -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<UserAccountKeysData> Run(User user);
}
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

@ -2,6 +2,7 @@
using Bit.Api.AdminConsole.Models.Response.Providers; using Bit.Api.AdminConsole.Models.Response.Providers;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -10,6 +11,7 @@ namespace Bit.Api.Models.Response;
public class ProfileResponseModel : ResponseModel public class ProfileResponseModel : ResponseModel
{ {
public ProfileResponseModel(User user, public ProfileResponseModel(User user,
UserAccountKeysData userAccountKeysData,
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails, IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails, IEnumerable<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails, IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
@ -32,6 +34,7 @@ public class ProfileResponseModel : ResponseModel
TwoFactorEnabled = twoFactorEnabled; TwoFactorEnabled = twoFactorEnabled;
Key = user.Key; Key = user.Key;
PrivateKey = user.PrivateKey; PrivateKey = user.PrivateKey;
AccountKeys = userAccountKeysData;
SecurityStamp = user.SecurityStamp; SecurityStamp = user.SecurityStamp;
ForcePasswordReset = user.ForcePasswordReset; ForcePasswordReset = user.ForcePasswordReset;
UsesKeyConnector = user.UsesKeyConnector; UsesKeyConnector = user.UsesKeyConnector;
@ -57,7 +60,9 @@ public class ProfileResponseModel : ResponseModel
public string Culture { get; set; } public string Culture { get; set; }
public bool TwoFactorEnabled { get; set; } public bool TwoFactorEnabled { get; set; }
public string Key { get; set; } public string Key { get; set; }
[Obsolete("Use AccountKeys instead.")]
public string PrivateKey { get; set; } public string PrivateKey { get; set; }
public UserAccountKeysData AccountKeys { get; set; }
public string SecurityStamp { get; set; } public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; } public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; } public bool UsesKeyConnector { get; set; }

View File

@ -8,6 +8,8 @@ using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -39,6 +41,7 @@ public class SyncController : Controller
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService; private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
public SyncController( public SyncController(
IUserService userService, IUserService userService,
@ -54,7 +57,8 @@ public class SyncController : Controller
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService, IFeatureService featureService,
IApplicationCacheService applicationCacheService, IApplicationCacheService applicationCacheService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{ {
_userService = userService; _userService = userService;
_folderRepository = folderRepository; _folderRepository = folderRepository;
@ -70,6 +74,7 @@ public class SyncController : Controller
_featureService = featureService; _featureService = featureService;
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
} }
[HttpGet("")] [HttpGet("")]
@ -112,8 +117,14 @@ public class SyncController : Controller
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync(); 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, organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response; return response;

View File

@ -4,6 +4,7 @@ using Bit.Api.Tools.Models.Response;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations; using Bit.Core.Models.Data.Organizations;
@ -20,6 +21,7 @@ public class SyncResponseModel : ResponseModel
public SyncResponseModel( public SyncResponseModel(
GlobalSettings globalSettings, GlobalSettings globalSettings,
User user, User user,
UserAccountKeysData userAccountKeysData,
bool userTwoFactorEnabled, bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization, bool userHasPremiumFromOrganization,
IDictionary<Guid, OrganizationAbility> organizationAbilities, IDictionary<Guid, OrganizationAbility> organizationAbilities,
@ -36,7 +38,7 @@ public class SyncResponseModel : ResponseModel
IEnumerable<Send> sends) IEnumerable<Send> sends)
: base("sync") : base("sync")
{ {
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails, Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser); providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
Folders = folders.Select(f => new FolderResponseModel(f)); Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(cipher => Ciphers = ciphers.Select(cipher =>

View File

@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models; using Bit.Core.Auth.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
@ -254,4 +255,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
{ {
return MasterPassword != null; return MasterPassword != null;
} }
public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()
{
return new PublicKeyEncryptionKeyPairData
{
WrappedPrivateKey = PrivateKey,
SignedPublicKey = SignedPublicKey,
PublicKey = PublicKey
};
}
} }

View File

@ -1,7 +1,9 @@
namespace Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.KeyManagement.Models.Data;
#nullable enable
public class UserAccountKeysData public class UserAccountKeysData
{ {
public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; } public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }
public SignatureKeyPairData signatureKeyPairData { get; set; } public SignatureKeyPairData? SignatureKeyPairData { get; set; }
} }

View File

@ -4,6 +4,7 @@ using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Queries;
using Bit.Api.KeyManagement.Validators; using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request; using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request; using Bit.Api.Vault.Models.Request;
@ -54,7 +55,7 @@ public class AccountsControllerTests : IDisposable
_resetPasswordValidator; _resetPasswordValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyRotationValidator; _webauthnKeyRotationValidator;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
public AccountsControllerTests() public AccountsControllerTests()
{ {
@ -79,6 +80,7 @@ public class AccountsControllerTests : IDisposable
_resetPasswordValidator = Substitute _resetPasswordValidator = Substitute
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, .For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>>(); IReadOnlyList<OrganizationUser>>>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_sut = new AccountsController( _sut = new AccountsController(
_organizationService, _organizationService,
@ -96,7 +98,8 @@ public class AccountsControllerTests : IDisposable
_sendValidator, _sendValidator,
_emergencyAccessValidator, _emergencyAccessValidator,
_resetPasswordValidator, _resetPasswordValidator,
_webauthnKeyRotationValidator _webauthnKeyRotationValidator,
_userAccountKeysQuery
); );
} }