mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -05:00
[PM-5963] Fix tde offboarding vault corruption (#4144)
* Attempt to fix tde to mp flow * Move tde offboarding to dedicated flag * Add tde offboarding password request * Validate tde offboarding input * Correctly check whether tde is active when building trusted device options * Refactor Tde offboarding into a separate command * Add unit tests for tde offboarding * Update tde offboarding request model * Fix tests * Fix further tests * Fix documentation * Add validation for updatetdepasswordasync key/newmasterpassword * Add comment explaining test * Remove unrelated changes
This commit is contained in:
@ -17,6 +17,7 @@ using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
|
||||
using Bit.Core.Billing.Models;
|
||||
@ -53,6 +54,7 @@ public class AccountsController : Controller
|
||||
private readonly IUserService _userService;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand;
|
||||
private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand;
|
||||
private readonly IRotateUserKeyCommand _rotateUserKeyCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
@ -83,6 +85,7 @@ public class AccountsController : Controller
|
||||
IUserService userService,
|
||||
IPolicyService policyService,
|
||||
ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand,
|
||||
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
|
||||
IRotateUserKeyCommand rotateUserKeyCommand,
|
||||
IFeatureService featureService,
|
||||
ISubscriberService subscriberService,
|
||||
@ -106,6 +109,7 @@ public class AccountsController : Controller
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
_setInitialMasterPasswordCommand = setInitialMasterPasswordCommand;
|
||||
_tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand;
|
||||
_rotateUserKeyCommand = rotateUserKeyCommand;
|
||||
_featureService = featureService;
|
||||
_subscriberService = subscriberService;
|
||||
@ -877,6 +881,29 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPut("update-tde-offboarding-password")]
|
||||
public async Task PutUpdateTdePasswordAsync([FromBody] UpdateTdeOffboardingPasswordRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _tdeOffboardingPasswordCommand.UpdateTdeOffboardingPasswordAsync(user, model.NewMasterPasswordHash, model.Key, model.MasterPasswordHint);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var error in result.Errors)
|
||||
{
|
||||
ModelState.AddModelError(string.Empty, error.Description);
|
||||
}
|
||||
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("request-otp")]
|
||||
public async Task PostRequestOTP()
|
||||
{
|
||||
|
@ -0,0 +1,14 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
public class UpdateTdeOffboardingPasswordRequestModel
|
||||
{
|
||||
[Required]
|
||||
[StringLength(300)]
|
||||
public string NewMasterPasswordHash { get; set; }
|
||||
[Required]
|
||||
public string Key { get; set; }
|
||||
[StringLength(50)]
|
||||
public string MasterPasswordHint { get; set; }
|
||||
}
|
@ -54,18 +54,21 @@ public class TrustedDeviceUserDecryptionOption
|
||||
public bool HasAdminApproval { get; }
|
||||
public bool HasLoginApprovingDevice { get; }
|
||||
public bool HasManageResetPasswordPermission { get; }
|
||||
public bool IsTdeOffboarding { get; }
|
||||
public string? EncryptedPrivateKey { get; }
|
||||
public string? EncryptedUserKey { get; }
|
||||
|
||||
public TrustedDeviceUserDecryptionOption(bool hasAdminApproval,
|
||||
bool hasLoginApprovingDevice,
|
||||
bool hasManageResetPasswordPermission,
|
||||
bool isTdeOffboarding,
|
||||
string? encryptedPrivateKey,
|
||||
string? encryptedUserKey)
|
||||
{
|
||||
HasAdminApproval = hasAdminApproval;
|
||||
HasLoginApprovingDevice = hasLoginApprovingDevice;
|
||||
HasManageResetPasswordPermission = hasManageResetPasswordPermission;
|
||||
IsTdeOffboarding = isTdeOffboarding;
|
||||
EncryptedPrivateKey = encryptedPrivateKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
}
|
||||
|
@ -0,0 +1,14 @@
|
||||
using Bit.Core.Entities;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// <para>Manages the setting of the master password for JIT provisioned TDE <see cref="User"/> in an organization, after the organization disabled TDE.</para>
|
||||
/// <para>This command is invoked, when the user first logs in after the organization has switched from TDE to master password based decryption.</para>
|
||||
/// </summary>
|
||||
public interface ITdeOffboardingPasswordCommand
|
||||
{
|
||||
public Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string masterPassword, string key,
|
||||
string orgSsoIdentifier);
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
namespace Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
|
||||
public class TdeOffboardingPasswordCommand : ITdeOffboardingPasswordCommand
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly ISsoUserRepository _ssoUserRepository;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IPushNotificationService _pushService;
|
||||
|
||||
|
||||
public TdeOffboardingPasswordCommand(
|
||||
IUserService userService,
|
||||
IUserRepository userRepository,
|
||||
IEventService eventService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ISsoUserRepository ssoUserRepository,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IPushNotificationService pushService)
|
||||
{
|
||||
_userService = userService;
|
||||
_userRepository = userRepository;
|
||||
_eventService = eventService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_ssoUserRepository = ssoUserRepository;
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
_pushService = pushService;
|
||||
}
|
||||
|
||||
public async Task<IdentityResult> UpdateTdeOffboardingPasswordAsync(User user, string newMasterPassword, string key, string hint)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(newMasterPassword))
|
||||
{
|
||||
throw new BadRequestException("Master password is required.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
throw new BadRequestException("Key is required.");
|
||||
}
|
||||
|
||||
if (user.HasMasterPassword())
|
||||
{
|
||||
throw new BadRequestException("User already has a master password.");
|
||||
}
|
||||
var orgUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(user.Id);
|
||||
orgUserDetails = orgUserDetails.Where(x => x.UseSso).ToList();
|
||||
if (orgUserDetails.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("User is not part of any organization that has SSO enabled.");
|
||||
}
|
||||
|
||||
var orgSSOUsers = await Task.WhenAll(orgUserDetails.Select(async x => await _ssoUserRepository.GetByUserIdOrganizationIdAsync(x.OrganizationId, user.Id)));
|
||||
if (orgSSOUsers.Length != 1)
|
||||
{
|
||||
throw new BadRequestException("User is part of no or multiple SSO configurations.");
|
||||
}
|
||||
|
||||
var orgUser = orgUserDetails.First();
|
||||
var orgSSOConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgUser.OrganizationId);
|
||||
if (orgSSOConfig == null)
|
||||
{
|
||||
throw new BadRequestException("Organization SSO configuration not found.");
|
||||
}
|
||||
else if (orgSSOConfig.GetData().MemberDecryptionType != Enums.MemberDecryptionType.MasterPassword)
|
||||
{
|
||||
throw new BadRequestException("Organization SSO Member Decryption Type is not Master Password.");
|
||||
}
|
||||
|
||||
var result = await _userService.UpdatePasswordHash(user, newMasterPassword);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
|
||||
user.ForcePasswordReset = false;
|
||||
user.Key = key;
|
||||
user.MasterPasswordHint = hint;
|
||||
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
|
||||
using Bit.Core.Auth.UserFeatures.Registration;
|
||||
using Bit.Core.Auth.UserFeatures.Registration.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey;
|
||||
using Bit.Core.Auth.UserFeatures.UserKey.Implementations;
|
||||
using Bit.Core.Auth.UserFeatures.UserMasterPassword;
|
||||
@ -22,6 +23,7 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddUserPasswordCommands();
|
||||
services.AddUserRegistrationCommands();
|
||||
services.AddWebAuthnLoginCommands();
|
||||
services.AddTdeOffboardingPasswordCommands();
|
||||
}
|
||||
|
||||
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
|
||||
@ -34,6 +36,11 @@ public static class UserServiceCollectionExtensions
|
||||
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddTdeOffboardingPasswordCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ITdeOffboardingPasswordCommand, TdeOffboardingPasswordCommand>();
|
||||
}
|
||||
|
||||
private static void AddUserRegistrationCommands(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISendVerificationEmailForRegistrationCommand, SendVerificationEmailForRegistrationCommand>();
|
||||
|
@ -14,6 +14,7 @@ public enum EventType : int
|
||||
User_UpdatedTempPassword = 1008,
|
||||
User_MigratedKeyToKeyConnector = 1009,
|
||||
User_RequestedDeviceApproval = 1010,
|
||||
User_TdeOffboardingPasswordSet = 1011,
|
||||
|
||||
Cipher_Created = 1100,
|
||||
Cipher_Updated = 1101,
|
||||
|
@ -95,8 +95,9 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
return;
|
||||
}
|
||||
|
||||
var ssoConfigurationData = _ssoConfig.GetData();
|
||||
if (ssoConfigurationData is not { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption })
|
||||
var isTdeActive = _ssoConfig.GetData() is { MemberDecryptionType: MemberDecryptionType.TrustedDeviceEncryption };
|
||||
var isTdeOffboarding = _user != null && !_user.HasMasterPassword() && _device != null && _device.IsTrusted() && !isTdeActive;
|
||||
if (!isTdeActive && !isTdeOffboarding)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -144,6 +145,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder
|
||||
hasAdminApproval,
|
||||
hasLoginApprovingDevice,
|
||||
hasManageResetPasswordPermission,
|
||||
isTdeOffboarding,
|
||||
encryptedPrivateKey,
|
||||
encryptedUserKey);
|
||||
}
|
||||
|
Reference in New Issue
Block a user