mirror of
https://github.com/bitwarden/server
synced 2025-12-22 11:13:27 +00:00
[PM-23242] Added UserDecryption with MasterPasswordUnlock as part of /sync response (#6102)
* Added MasterPasswordUnlock to UserDecryptionOptions as part of identity response * Added UserDecryption with MasterPasswordUnlock as part of /sync response
This commit is contained in:
@@ -8,10 +8,10 @@ using Bit.Core.Models.Api;
|
|||||||
|
|
||||||
namespace Bit.Api.Models.Response;
|
namespace Bit.Api.Models.Response;
|
||||||
|
|
||||||
public class DomainsResponseModel : ResponseModel
|
public class DomainsResponseModel() : ResponseModel("domains")
|
||||||
{
|
{
|
||||||
public DomainsResponseModel(User user, bool excluded = true)
|
public DomainsResponseModel(User user, bool excluded = true)
|
||||||
: base("domains")
|
: this()
|
||||||
{
|
{
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
@@ -38,13 +38,13 @@ public class DomainsResponseModel : ResponseModel
|
|||||||
public IEnumerable<GlobalDomains> GlobalEquivalentDomains { get; set; }
|
public IEnumerable<GlobalDomains> GlobalEquivalentDomains { get; set; }
|
||||||
|
|
||||||
|
|
||||||
public class GlobalDomains
|
public class GlobalDomains()
|
||||||
{
|
{
|
||||||
public GlobalDomains(
|
public GlobalDomains(
|
||||||
GlobalEquivalentDomainsType globalDomain,
|
GlobalEquivalentDomainsType globalDomain,
|
||||||
IEnumerable<string> domains,
|
IEnumerable<string> domains,
|
||||||
IEnumerable<GlobalEquivalentDomainsType> excludedDomains,
|
IEnumerable<GlobalEquivalentDomainsType> excludedDomains,
|
||||||
bool excluded)
|
bool excluded) : this()
|
||||||
{
|
{
|
||||||
Type = (byte)globalDomain;
|
Type = (byte)globalDomain;
|
||||||
Domains = domains;
|
Domains = domains;
|
||||||
|
|||||||
@@ -7,6 +7,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.Response;
|
||||||
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;
|
||||||
@@ -18,7 +19,7 @@ using Bit.Core.Vault.Models.Data;
|
|||||||
|
|
||||||
namespace Bit.Api.Vault.Models.Response;
|
namespace Bit.Api.Vault.Models.Response;
|
||||||
|
|
||||||
public class SyncResponseModel : ResponseModel
|
public class SyncResponseModel() : ResponseModel("sync")
|
||||||
{
|
{
|
||||||
public SyncResponseModel(
|
public SyncResponseModel(
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
@@ -37,7 +38,7 @@ public class SyncResponseModel : ResponseModel
|
|||||||
bool excludeDomains,
|
bool excludeDomains,
|
||||||
IEnumerable<Policy> policies,
|
IEnumerable<Policy> policies,
|
||||||
IEnumerable<Send> sends)
|
IEnumerable<Send> sends)
|
||||||
: base("sync")
|
: this()
|
||||||
{
|
{
|
||||||
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
|
||||||
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
|
||||||
@@ -54,6 +55,23 @@ public class SyncResponseModel : ResponseModel
|
|||||||
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
|
Domains = excludeDomains ? null : new DomainsResponseModel(user, false);
|
||||||
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
|
Policies = policies?.Select(p => new PolicyResponseModel(p)) ?? new List<PolicyResponseModel>();
|
||||||
Sends = sends.Select(s => new SendResponseModel(s, globalSettings));
|
Sends = sends.Select(s => new SendResponseModel(s, globalSettings));
|
||||||
|
UserDecryption = new UserDecryptionResponseModel
|
||||||
|
{
|
||||||
|
MasterPasswordUnlock = user.HasMasterPassword()
|
||||||
|
? new MasterPasswordUnlockResponseModel
|
||||||
|
{
|
||||||
|
Kdf = new MasterPasswordUnlockKdfResponseModel
|
||||||
|
{
|
||||||
|
KdfType = user.Kdf,
|
||||||
|
Iterations = user.KdfIterations,
|
||||||
|
Memory = user.KdfMemory,
|
||||||
|
Parallelism = user.KdfParallelism
|
||||||
|
},
|
||||||
|
MasterKeyEncryptedUserKey = user.Key!,
|
||||||
|
Salt = user.Email.ToLowerInvariant()
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProfileResponseModel Profile { get; set; }
|
public ProfileResponseModel Profile { get; set; }
|
||||||
@@ -63,4 +81,5 @@ public class SyncResponseModel : ResponseModel
|
|||||||
public DomainsResponseModel Domains { get; set; }
|
public DomainsResponseModel Domains { get; set; }
|
||||||
public IEnumerable<PolicyResponseModel> Policies { get; set; }
|
public IEnumerable<PolicyResponseModel> Policies { get; set; }
|
||||||
public IEnumerable<SendResponseModel> Sends { get; set; }
|
public IEnumerable<SendResponseModel> Sends { get; set; }
|
||||||
|
public UserDecryptionResponseModel UserDecryption { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace Bit.Core.KeyManagement.Models.Response;
|
||||||
|
|
||||||
|
public class UserDecryptionResponseModel
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the unlock data when the user has a master password that can be used to decrypt their vault.
|
||||||
|
/// </summary>
|
||||||
|
public MasterPasswordUnlockResponseModel? MasterPasswordUnlock { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
using Bit.Api.IntegrationTest.Factories;
|
||||||
|
using Bit.Api.IntegrationTest.Helpers;
|
||||||
|
using Bit.Api.Vault.Models.Response;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Api.IntegrationTest.Vault.Controllers;
|
||||||
|
|
||||||
|
public class SyncControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly ApiApplicationFactory _factory;
|
||||||
|
|
||||||
|
private readonly LoginHelper _loginHelper;
|
||||||
|
|
||||||
|
private readonly IUserRepository _userRepository;
|
||||||
|
private string _ownerEmail = null!;
|
||||||
|
|
||||||
|
public SyncControllerTests(ApiApplicationFactory factory)
|
||||||
|
{
|
||||||
|
_factory = factory;
|
||||||
|
_client = factory.CreateClient();
|
||||||
|
_loginHelper = new LoginHelper(_factory, _client);
|
||||||
|
_userRepository = _factory.GetService<IUserRepository>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_ownerEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
// [BitAutoData]
|
||||||
|
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull()
|
||||||
|
{
|
||||||
|
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(tempEmail);
|
||||||
|
await _loginHelper.LoginAsync(tempEmail);
|
||||||
|
|
||||||
|
// Remove user's password.
|
||||||
|
var user = await _userRepository.GetByEmailAsync(tempEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
user.MasterPassword = null;
|
||||||
|
await _userRepository.UpsertAsync(user);
|
||||||
|
|
||||||
|
var response = await _client.GetAsync("/sync");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
|
||||||
|
|
||||||
|
Assert.NotNull(syncResponseModel);
|
||||||
|
Assert.NotNull(syncResponseModel.UserDecryption);
|
||||||
|
Assert.Null(syncResponseModel.UserDecryption.MasterPasswordUnlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
|
||||||
|
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
|
||||||
|
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
|
||||||
|
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
|
||||||
|
{
|
||||||
|
var tempEmail = $"integration-test{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
await _factory.LoginWithNewAccount(tempEmail);
|
||||||
|
await _loginHelper.LoginAsync(tempEmail);
|
||||||
|
|
||||||
|
// Change KDF settings
|
||||||
|
var user = await _userRepository.GetByEmailAsync(tempEmail);
|
||||||
|
Assert.NotNull(user);
|
||||||
|
user.Kdf = kdfType;
|
||||||
|
user.KdfIterations = kdfIterations;
|
||||||
|
user.KdfMemory = kdfMemory;
|
||||||
|
user.KdfParallelism = kdfParallelism;
|
||||||
|
await _userRepository.UpsertAsync(user);
|
||||||
|
|
||||||
|
var response = await _client.GetAsync("/sync");
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var syncResponseModel = await response.Content.ReadFromJsonAsync<SyncResponseModel>();
|
||||||
|
|
||||||
|
Assert.NotNull(syncResponseModel);
|
||||||
|
Assert.NotNull(syncResponseModel.UserDecryption);
|
||||||
|
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock);
|
||||||
|
Assert.NotNull(syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf);
|
||||||
|
Assert.Equal(kdfType, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
|
||||||
|
Assert.Equal(kdfIterations, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
|
||||||
|
Assert.Equal(kdfMemory, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
|
||||||
|
Assert.Equal(kdfParallelism, syncResponseModel.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
|
||||||
|
Assert.Equal(user.Key, syncResponseModel.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
|
||||||
|
Assert.Equal(user.Email.ToLower(), syncResponseModel.UserDecryption.MasterPasswordUnlock.Salt);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -317,6 +317,55 @@ public class SyncControllerTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task Get_HaveNoMasterPassword_UserDecryptionMasterPasswordUnlockIsNull(
|
||||||
|
User user, SutProvider<SyncController> sutProvider)
|
||||||
|
{
|
||||||
|
user.EquivalentDomains = null;
|
||||||
|
user.ExcludedGlobalEquivalentDomains = null;
|
||||||
|
|
||||||
|
user.MasterPassword = null;
|
||||||
|
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.Get();
|
||||||
|
|
||||||
|
Assert.Null(result.UserDecryption.MasterPasswordUnlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(KdfType.PBKDF2_SHA256, 654_321, null, null)]
|
||||||
|
[BitAutoData(KdfType.Argon2id, 11, 128, 5)]
|
||||||
|
public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNull(
|
||||||
|
KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism,
|
||||||
|
User user, SutProvider<SyncController> sutProvider)
|
||||||
|
{
|
||||||
|
user.EquivalentDomains = null;
|
||||||
|
user.ExcludedGlobalEquivalentDomains = null;
|
||||||
|
|
||||||
|
user.Key = "test-key";
|
||||||
|
user.MasterPassword = "test-master-password";
|
||||||
|
user.Kdf = kdfType;
|
||||||
|
user.KdfIterations = kdfIterations;
|
||||||
|
user.KdfMemory = kdfMemory;
|
||||||
|
user.KdfParallelism = kdfParallelism;
|
||||||
|
|
||||||
|
var userService = sutProvider.GetDependency<IUserService>();
|
||||||
|
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.Get();
|
||||||
|
|
||||||
|
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock);
|
||||||
|
Assert.NotNull(result.UserDecryption.MasterPasswordUnlock.Kdf);
|
||||||
|
Assert.Equal(kdfType, result.UserDecryption.MasterPasswordUnlock.Kdf.KdfType);
|
||||||
|
Assert.Equal(kdfIterations, result.UserDecryption.MasterPasswordUnlock.Kdf.Iterations);
|
||||||
|
Assert.Equal(kdfMemory, result.UserDecryption.MasterPasswordUnlock.Kdf.Memory);
|
||||||
|
Assert.Equal(kdfParallelism, result.UserDecryption.MasterPasswordUnlock.Kdf.Parallelism);
|
||||||
|
Assert.Equal(user.Key, result.UserDecryption.MasterPasswordUnlock.MasterKeyEncryptedUserKey);
|
||||||
|
Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AssertMethodsCalledAsync(IUserService userService,
|
private async Task AssertMethodsCalledAsync(IUserService userService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
|||||||
Reference in New Issue
Block a user