Browse Source
* layout new key rotation methods - add endpoint with request model - add command with data model - add repository method * layout new key rotation methods - add endpoint with request model - add command with data model - add repository method * formatting * rename account recovery to reset password * fix tests * remove extra endpoint * rename account recovery to reset password * fix tests and formatting * register db calls in command, removing list from user repo * formattingpull/3441/head
12 changed files with 336 additions and 34 deletions
@ -0,0 +1,19 @@ |
|||||||
|
using Bit.Core.Auth.Entities; |
||||||
|
using Bit.Core.Entities; |
||||||
|
using Bit.Core.Tools.Entities; |
||||||
|
using Bit.Core.Vault.Entities; |
||||||
|
|
||||||
|
namespace Bit.Core.Auth.Models.Data; |
||||||
|
|
||||||
|
public class RotateUserKeyData |
||||||
|
{ |
||||||
|
public User User { get; set; } |
||||||
|
public string MasterPasswordHash { get; set; } |
||||||
|
public string Key { get; set; } |
||||||
|
public string PrivateKey { get; set; } |
||||||
|
public IEnumerable<Cipher> Ciphers { get; set; } |
||||||
|
public IEnumerable<Folder> Folders { get; set; } |
||||||
|
public IEnumerable<Send> Sends { get; set; } |
||||||
|
public IEnumerable<EmergencyAccess> EmergencyAccessKeys { get; set; } |
||||||
|
public IEnumerable<OrganizationUser> ResetPasswordKeys { get; set; } |
||||||
|
} |
||||||
@ -0,0 +1,18 @@ |
|||||||
|
using Bit.Core.Auth.Models.Data; |
||||||
|
using Microsoft.AspNetCore.Identity; |
||||||
|
using Microsoft.Data.SqlClient; |
||||||
|
|
||||||
|
namespace Bit.Core.Auth.UserFeatures.UserKey; |
||||||
|
|
||||||
|
public interface IRotateUserKeyCommand |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Sets a new user key and updates all encrypted data. |
||||||
|
/// </summary> |
||||||
|
/// <param name="model">All necessary information for rotation. Warning: Any encrypted data not included will be lost.</param> |
||||||
|
/// <returns>An IdentityResult for verification of the master password hash</returns> |
||||||
|
/// <exception cref="ArgumentNullException">User must be provided.</exception> |
||||||
|
Task<IdentityResult> RotateUserKeyAsync(RotateUserKeyData model); |
||||||
|
} |
||||||
|
|
||||||
|
public delegate Task UpdateEncryptedDataForKeyRotation(SqlTransaction transaction = null); |
||||||
@ -0,0 +1,61 @@ |
|||||||
|
using Bit.Core.Auth.Models.Data; |
||||||
|
using Bit.Core.Repositories; |
||||||
|
using Bit.Core.Services; |
||||||
|
using Microsoft.AspNetCore.Identity; |
||||||
|
|
||||||
|
namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations; |
||||||
|
|
||||||
|
public class RotateUserKeyCommand : IRotateUserKeyCommand |
||||||
|
{ |
||||||
|
private readonly IUserService _userService; |
||||||
|
private readonly IUserRepository _userRepository; |
||||||
|
private readonly IPushNotificationService _pushService; |
||||||
|
private readonly IdentityErrorDescriber _identityErrorDescriber; |
||||||
|
|
||||||
|
public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, |
||||||
|
IPushNotificationService pushService, IdentityErrorDescriber errors) |
||||||
|
{ |
||||||
|
_userService = userService; |
||||||
|
_userRepository = userRepository; |
||||||
|
_pushService = pushService; |
||||||
|
_identityErrorDescriber = errors; |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public async Task<IdentityResult> RotateUserKeyAsync(RotateUserKeyData model) |
||||||
|
{ |
||||||
|
if (model.User == null) |
||||||
|
{ |
||||||
|
throw new ArgumentNullException(nameof(model.User)); |
||||||
|
} |
||||||
|
|
||||||
|
if (!await _userService.CheckPasswordAsync(model.User, model.MasterPasswordHash)) |
||||||
|
{ |
||||||
|
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); |
||||||
|
} |
||||||
|
|
||||||
|
var now = DateTime.UtcNow; |
||||||
|
model.User.RevisionDate = model.User.AccountRevisionDate = now; |
||||||
|
model.User.LastKeyRotationDate = now; |
||||||
|
model.User.SecurityStamp = Guid.NewGuid().ToString(); |
||||||
|
model.User.Key = model.Key; |
||||||
|
model.User.PrivateKey = model.PrivateKey; |
||||||
|
if (model.Ciphers.Any() || model.Folders.Any() || model.Sends.Any() || model.EmergencyAccessKeys.Any() || |
||||||
|
model.ResetPasswordKeys.Any()) |
||||||
|
{ |
||||||
|
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new(); |
||||||
|
// if (model.Ciphers.Any()) |
||||||
|
// { |
||||||
|
// saveEncryptedDataActions.Add(_cipherRepository.SaveRotatedData); |
||||||
|
// } |
||||||
|
await _userRepository.UpdateUserKeyAndEncryptedDataAsync(model.User, saveEncryptedDataActions); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
await _userRepository.ReplaceAsync(model.User); |
||||||
|
} |
||||||
|
|
||||||
|
await _pushService.PushLogOutAsync(model.User.Id, excludeCurrentContextFromPush: true); |
||||||
|
return IdentityResult.Success; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,50 @@ |
|||||||
|
using Bit.Core.Auth.Models.Data; |
||||||
|
using Bit.Core.Auth.UserFeatures.UserKey.Implementations; |
||||||
|
using Bit.Core.Services; |
||||||
|
using Bit.Test.Common.AutoFixture; |
||||||
|
using Bit.Test.Common.AutoFixture.Attributes; |
||||||
|
using Microsoft.AspNetCore.Identity; |
||||||
|
using NSubstitute; |
||||||
|
using Xunit; |
||||||
|
|
||||||
|
namespace Bit.Core.Test.Auth.UserFeatures.UserKey; |
||||||
|
|
||||||
|
[SutProviderCustomize] |
||||||
|
public class RotateUserKeyCommandTests |
||||||
|
{ |
||||||
|
[Theory, BitAutoData] |
||||||
|
public async Task RotateUserKeyAsync_Success(SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||||
|
.Returns(true); |
||||||
|
|
||||||
|
var result = await sutProvider.Sut.RotateUserKeyAsync(model); |
||||||
|
|
||||||
|
Assert.Equal(IdentityResult.Success, result); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory, BitAutoData] |
||||||
|
public async Task RotateUserKeyAsync_InvalidMasterPasswordHash_ReturnsFailedIdentityResult( |
||||||
|
SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||||
|
.Returns(false); |
||||||
|
|
||||||
|
var result = await sutProvider.Sut.RotateUserKeyAsync(model); |
||||||
|
|
||||||
|
Assert.False(result.Succeeded); |
||||||
|
} |
||||||
|
|
||||||
|
[Theory, BitAutoData] |
||||||
|
public async Task RotateUserKeyAsync_LogsOutUser( |
||||||
|
SutProvider<RotateUserKeyCommand> sutProvider, RotateUserKeyData model) |
||||||
|
{ |
||||||
|
sutProvider.GetDependency<IUserService>().CheckPasswordAsync(model.User, model.MasterPasswordHash) |
||||||
|
.Returns(true); |
||||||
|
|
||||||
|
await sutProvider.Sut.RotateUserKeyAsync(model); |
||||||
|
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().ReceivedWithAnyArgs() |
||||||
|
.PushLogOutAsync(default, default); |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue