2025-03-25 15:23:01 +01:00
using Bit.Core.Auth.Repositories ;
using Bit.Core.Entities ;
using Bit.Core.KeyManagement.Models.Data ;
using Bit.Core.Platform.Push ;
using Bit.Core.Repositories ;
using Bit.Core.Services ;
using Bit.Core.Tools.Repositories ;
using Bit.Core.Vault.Repositories ;
using Microsoft.AspNetCore.Identity ;
namespace Bit.Core.KeyManagement.UserKey.Implementations ;
/// <inheritdoc />
public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
{
private readonly IUserService _userService ;
private readonly IUserRepository _userRepository ;
private readonly ICipherRepository _cipherRepository ;
private readonly IFolderRepository _folderRepository ;
private readonly ISendRepository _sendRepository ;
private readonly IEmergencyAccessRepository _emergencyAccessRepository ;
private readonly IOrganizationUserRepository _organizationUserRepository ;
2025-04-03 11:30:49 +02:00
private readonly IDeviceRepository _deviceRepository ;
2025-03-25 15:23:01 +01:00
private readonly IPushNotificationService _pushService ;
private readonly IdentityErrorDescriber _identityErrorDescriber ;
private readonly IWebAuthnCredentialRepository _credentialRepository ;
private readonly IPasswordHasher < User > _passwordHasher ;
/// <summary>
/// Instantiates a new <see cref="RotateUserAccountKeysCommand"/>
/// </summary>
/// <param name="userService">Master password hash validation</param>
/// <param name="userRepository">Updates user keys and re-encrypted data if needed</param>
/// <param name="cipherRepository">Provides a method to update re-encrypted cipher data</param>
/// <param name="folderRepository">Provides a method to update re-encrypted folder data</param>
/// <param name="sendRepository">Provides a method to update re-encrypted send data</param>
/// <param name="emergencyAccessRepository">Provides a method to update re-encrypted emergency access data</param>
/// <param name="organizationUserRepository">Provides a method to update re-encrypted organization user data</param>
/// <param name="passwordHasher">Hashes the new master password</param>
/// <param name="pushService">Logs out user from other devices after successful rotation</param>
/// <param name="errors">Provides a password mismatch error if master password hash validation fails</param>
/// <param name="credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param>
public RotateUserAccountKeysCommand ( IUserService userService , IUserRepository userRepository ,
ICipherRepository cipherRepository , IFolderRepository folderRepository , ISendRepository sendRepository ,
IEmergencyAccessRepository emergencyAccessRepository , IOrganizationUserRepository organizationUserRepository ,
2025-04-03 11:30:49 +02:00
IDeviceRepository deviceRepository ,
2025-03-25 15:23:01 +01:00
IPasswordHasher < User > passwordHasher ,
[PM-24233] Use BulkResourceCreationService in CipherRepository (#6201)
* Add constant for CipherRepositoryBulkResourceCreation in FeatureFlagKeys
* Add bulk creation methods for Ciphers, Folders, and CollectionCiphers in BulkResourceCreationService
- Implemented CreateCiphersAsync, CreateFoldersAsync, CreateCollectionCiphersAsync, and CreateTempCiphersAsync methods for bulk insertion.
- Added helper methods to build DataTables for Ciphers, Folders, and CollectionCiphers.
- Enhanced error handling for empty collections during bulk operations.
* Refactor CipherRepository to utilize BulkResourceCreationService
- Introduced IFeatureService to manage feature flag checks for bulk operations.
- Updated methods to conditionally use BulkResourceCreationService for creating Ciphers, Folders, and CollectionCiphers based on feature flag status.
- Enhanced existing bulk copy logic to maintain functionality while integrating feature flag checks.
* Add InlineFeatureService to DatabaseDataAttribute for feature flag management
- Introduced EnabledFeatureFlags property to DatabaseDataAttribute for configuring feature flags.
- Integrated InlineFeatureService to provide feature flag checks within the service collection.
- Enhanced GetData method to utilize feature flags for conditional service registration.
* Add tests for bulk creation of Ciphers in CipherRepositoryTests
- Implemented tests for bulk creation of Ciphers, Folders, and Collections with feature flag checks.
- Added test cases for updating multiple Ciphers to validate bulk update functionality.
- Enhanced existing test structure to ensure comprehensive coverage of bulk operations in the CipherRepository.
* Refactor BulkResourceCreationService to use dynamic types for DataColumns
- Updated DataColumn definitions in BulkResourceCreationService to utilize the actual types of properties from the cipher object instead of hardcoded types.
- Simplified the assignment of nullable properties to directly use their values, improving code readability and maintainability.
* Update BulkResourceCreationService to use specific types for DataColumns
- Changed DataColumn definitions to use specific types (short and string) instead of dynamic types based on cipher properties.
- Improved handling of nullable properties when assigning values to DataTable rows, ensuring proper handling of DBNull for null values.
* Refactor CipherRepositoryTests for improved clarity and consistency
- Renamed test methods to better reflect their purpose and improve readability.
- Updated test data to use more descriptive names for users, folders, and collections.
- Enhanced test structure with clear Arrange, Act, and Assert sections for better understanding of test flow.
- Ensured all tests validate the expected outcomes for bulk operations with feature flag checks.
* Update CipherRepositoryBulkResourceCreation feature flag key
* Refactor DatabaseDataAttribute usage in CipherRepositoryTests to use array syntax for EnabledFeatureFlags
* Update CipherRepositoryTests to use GenerateComb for generating unique IDs
* Refactor CipherRepository methods to accept a boolean parameter for enabling bulk resource creation based on feature flags. Update tests to verify functionality with and without the feature flag enabled.
* Refactor CipherRepository and related services to support new methods for bulk resource creation without boolean parameters.
2025-09-03 14:57:53 +01:00
IPushNotificationService pushService , IdentityErrorDescriber errors , IWebAuthnCredentialRepository credentialRepository ,
IFeatureService featureService )
2025-03-25 15:23:01 +01:00
{
_userService = userService ;
_userRepository = userRepository ;
_cipherRepository = cipherRepository ;
_folderRepository = folderRepository ;
_sendRepository = sendRepository ;
_emergencyAccessRepository = emergencyAccessRepository ;
_organizationUserRepository = organizationUserRepository ;
2025-04-03 11:30:49 +02:00
_deviceRepository = deviceRepository ;
2025-03-25 15:23:01 +01:00
_pushService = pushService ;
_identityErrorDescriber = errors ;
_credentialRepository = credentialRepository ;
_passwordHasher = passwordHasher ;
}
/// <inheritdoc />
public async Task < IdentityResult > RotateUserAccountKeysAsync ( User user , RotateUserAccountKeysData model )
{
if ( user = = null )
{
throw new ArgumentNullException ( nameof ( user ) ) ;
}
if ( ! await _userService . CheckPasswordAsync ( user , model . OldMasterKeyAuthenticationHash ) )
{
return IdentityResult . Failed ( _identityErrorDescriber . PasswordMismatch ( ) ) ;
}
var now = DateTime . UtcNow ;
user . RevisionDate = user . AccountRevisionDate = now ;
user . LastKeyRotationDate = now ;
user . SecurityStamp = Guid . NewGuid ( ) . ToString ( ) ;
if (
! model . MasterPasswordUnlockData . ValidateForUser ( user )
)
{
throw new InvalidOperationException ( "The provided master password unlock data is not valid for this user." ) ;
}
if (
model . AccountPublicKey ! = user . PublicKey
)
{
throw new InvalidOperationException ( "The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation." ) ;
}
user . Key = model . MasterPasswordUnlockData . MasterKeyEncryptedUserKey ;
user . PrivateKey = model . UserKeyEncryptedAccountPrivateKey ;
user . MasterPassword = _passwordHasher . HashPassword ( user , model . MasterPasswordUnlockData . MasterKeyAuthenticationHash ) ;
user . MasterPasswordHint = model . MasterPasswordUnlockData . MasterPasswordHint ;
List < UpdateEncryptedDataForKeyRotation > saveEncryptedDataActions = new ( ) ;
if ( model . Ciphers . Any ( ) )
{
2025-10-08 12:21:02 -05:00
saveEncryptedDataActions . Add ( _cipherRepository . UpdateForKeyRotation ( user . Id , model . Ciphers ) ) ;
2025-03-25 15:23:01 +01:00
}
if ( model . Folders . Any ( ) )
{
saveEncryptedDataActions . Add ( _folderRepository . UpdateForKeyRotation ( user . Id , model . Folders ) ) ;
}
if ( model . Sends . Any ( ) )
{
saveEncryptedDataActions . Add ( _sendRepository . UpdateForKeyRotation ( user . Id , model . Sends ) ) ;
}
if ( model . EmergencyAccesses . Any ( ) )
{
saveEncryptedDataActions . Add (
_emergencyAccessRepository . UpdateForKeyRotation ( user . Id , model . EmergencyAccesses ) ) ;
}
if ( model . OrganizationUsers . Any ( ) )
{
saveEncryptedDataActions . Add (
_organizationUserRepository . UpdateForKeyRotation ( user . Id , model . OrganizationUsers ) ) ;
}
if ( model . WebAuthnKeys . Any ( ) )
{
saveEncryptedDataActions . Add ( _credentialRepository . UpdateKeysForRotationAsync ( user . Id , model . WebAuthnKeys ) ) ;
}
2025-04-03 11:30:49 +02:00
if ( model . DeviceKeys . Any ( ) )
{
saveEncryptedDataActions . Add ( _deviceRepository . UpdateKeysForRotationAsync ( user . Id , model . DeviceKeys ) ) ;
}
2025-03-25 15:23:01 +01:00
await _userRepository . UpdateUserKeyAndEncryptedDataV2Async ( user , saveEncryptedDataActions ) ;
await _pushService . PushLogOutAsync ( user . Id ) ;
return IdentityResult . Success ;
}
}