From 4b2bd6cee674317c74019d86cc8f8ac7e08f42ee Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 6 Dec 2023 08:46:36 -0500 Subject: [PATCH] [PM-3797 Part 3] Add vault domains to key rotation (#3436) ## Type of change ``` - [ ] Bug fix - [ ] New feature development - [x] Tech debt (refactoring, code cleanup, dependency upgrades, etc) - [ ] Build/deploy pipeline (DevOps) - [ ] Other ``` ## Objective Previous PR: #3434 Adds ciphers and folders to the new key rotation. ## Code changes * **file.ext:** Description of what was changed and why ## Before you submit - Please check for formatting errors (`dotnet format --verify-no-changes`) (required) - If making database changes - make sure you also update Entity Framework queries and/or migrations - Please add **unit tests** where it makes sense to do so (encouraged but not required) - If this change requires a **documentation update** - notify the documentation team - If this change has particular **deployment requirements** - notify the DevOps team --- .../Auth/Controllers/AccountsController.cs | 11 +++- src/Api/Startup.cs | 9 +++ .../Validators/CipherRotationValidator.cs | 56 ++++++++++++++++++ .../Validators/FolderRotationValidator.cs | 44 ++++++++++++++ .../Implementations/RotateUserKeyCommand.cs | 20 +++++-- .../Vault/Repositories/ICipherRepository.cs | 11 +++- .../Vault/Repositories/IFolderRepository.cs | 11 +++- .../Vault/Helpers/CipherHelpers.cs | 33 +++++++++++ .../Vault/Helpers/FolderHelpers.cs | 25 ++++++++ .../Vault/Repositories/CipherRepository.cs | 59 +++++++++++++++++++ .../Vault/Repositories/FolderRepository.cs | 58 ++++++++++++++++++ .../Repositories/UserRepository.cs | 2 + .../Vault/Repositories/CipherRepository.cs | 30 ++++++++++ .../Vault/Repositories/FolderRepository.cs | 26 ++++++++ .../Controllers/AccountsControllerTests.cs | 11 ++++ .../CipherRotationValidatorTests.cs | 45 ++++++++++++++ .../FolderRotationValidatorTests.cs | 42 +++++++++++++ 17 files changed, 485 insertions(+), 8 deletions(-) create mode 100644 src/Api/Vault/Validators/CipherRotationValidator.cs create mode 100644 src/Api/Vault/Validators/FolderRotationValidator.cs create mode 100644 src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs create mode 100644 src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs create mode 100644 test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs create mode 100644 test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index bf9294709..e49ce9dd2 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -6,6 +6,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; +using Bit.Api.Vault.Models.Request; using Bit.Core; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -65,6 +66,8 @@ public class AccountsController : Controller private bool UseFlexibleCollections => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + private readonly IRotationValidator, IEnumerable> _cipherValidator; + private readonly IRotationValidator, IEnumerable> _folderValidator; private readonly IRotationValidator, IEnumerable> _emergencyAccessValidator; @@ -87,6 +90,8 @@ public class AccountsController : Controller IRotateUserKeyCommand rotateUserKeyCommand, IFeatureService featureService, ICurrentContext currentContext, + IRotationValidator, IEnumerable> cipherValidator, + IRotationValidator, IEnumerable> folderValidator, IRotationValidator, IEnumerable> emergencyAccessValidator ) @@ -108,6 +113,8 @@ public class AccountsController : Controller _rotateUserKeyCommand = rotateUserKeyCommand; _featureService = featureService; _currentContext = currentContext; + _cipherValidator = cipherValidator; + _folderValidator = folderValidator; _emergencyAccessValidator = emergencyAccessValidator; } @@ -414,8 +421,8 @@ public class AccountsController : Controller MasterPasswordHash = model.MasterPasswordHash, Key = model.Key, PrivateKey = model.PrivateKey, - Ciphers = new List(), - Folders = new List(), + Ciphers = await _cipherValidator.ValidateAsync(user, model.Ciphers), + Folders = await _folderValidator.ValidateAsync(user, model.Folders), Sends = new List(), EmergencyAccessKeys = await _emergencyAccessValidator.ValidateAsync(user, model.EmergencyAccessKeys), ResetPasswordKeys = new List(), diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 20301bdb2..1b8505e47 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -9,6 +9,8 @@ using IdentityModel; using System.Globalization; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Validators; +using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Validators; using Bit.Core.Auth.Entities; using Bit.Core.IdentityServer; using Bit.SharedWeb.Health; @@ -21,6 +23,7 @@ using Bit.Core.Auth.Identity; using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Auth.UserFeatures.UserKey.Implementations; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; +using Bit.Core.Vault.Entities; #if !OSS using Bit.Commercial.Core.SecretsManager; @@ -141,6 +144,12 @@ public class Startup services .AddScoped, IEnumerable>, EmergencyAccessRotationValidator>(); + services + .AddScoped, IEnumerable>, + CipherRotationValidator>(); + services + .AddScoped, IEnumerable>, + FolderRotationValidator>(); // Services services.AddBaseServices(globalSettings); diff --git a/src/Api/Vault/Validators/CipherRotationValidator.cs b/src/Api/Vault/Validators/CipherRotationValidator.cs new file mode 100644 index 000000000..9259235d8 --- /dev/null +++ b/src/Api/Vault/Validators/CipherRotationValidator.cs @@ -0,0 +1,56 @@ +using Bit.Api.Auth.Validators; +using Bit.Api.Vault.Models.Request; +using Bit.Core; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Services; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; + +namespace Bit.Api.Vault.Validators; + +public class CipherRotationValidator : IRotationValidator, IEnumerable> +{ + private readonly ICipherRepository _cipherRepository; + private readonly ICurrentContext _currentContext; + private readonly IFeatureService _featureService; + + private bool UseFlexibleCollections => + _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext); + + public CipherRotationValidator(ICipherRepository cipherRepository, ICurrentContext currentContext, + IFeatureService featureService) + { + _cipherRepository = cipherRepository; + _currentContext = currentContext; + _featureService = featureService; + + } + + public async Task> ValidateAsync(User user, IEnumerable ciphers) + { + var result = new List(); + if (ciphers == null || !ciphers.Any()) + { + return result; + } + + var existingCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections); + if (existingCiphers == null || !existingCiphers.Any()) + { + return result; + } + + foreach (var existing in existingCiphers) + { + var cipher = ciphers.FirstOrDefault(c => c.Id == existing.Id); + if (cipher == null) + { + throw new BadRequestException("All existing ciphers must be included in the rotation."); + } + result.Add(cipher.ToCipher(existing)); + } + return result; + } +} diff --git a/src/Api/Vault/Validators/FolderRotationValidator.cs b/src/Api/Vault/Validators/FolderRotationValidator.cs new file mode 100644 index 000000000..b79c38e7a --- /dev/null +++ b/src/Api/Vault/Validators/FolderRotationValidator.cs @@ -0,0 +1,44 @@ +using Bit.Api.Auth.Validators; +using Bit.Api.Vault.Models.Request; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; + +namespace Bit.Api.Vault.Validators; + +public class FolderRotationValidator : IRotationValidator, IEnumerable> +{ + private readonly IFolderRepository _folderRepository; + + public FolderRotationValidator(IFolderRepository folderRepository) + { + _folderRepository = folderRepository; + } + + public async Task> ValidateAsync(User user, IEnumerable folders) + { + var result = new List(); + if (folders == null || !folders.Any()) + { + return result; + } + + var existingFolders = await _folderRepository.GetManyByUserIdAsync(user.Id); + if (existingFolders == null || !existingFolders.Any()) + { + return result; + } + + foreach (var existing in existingFolders) + { + var folder = folders.FirstOrDefault(c => c.Id == existing.Id); + if (folder == null) + { + throw new BadRequestException("All existing folders must be included in the rotation."); + } + result.Add(folder.ToFolder(existing)); + } + return result; + } +} diff --git a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs index f1ac72b17..34b29a246 100644 --- a/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs +++ b/src/Core/Auth/UserFeatures/UserKey/Implementations/RotateUserKeyCommand.cs @@ -2,6 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Vault.Repositories; using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.UserFeatures.UserKey.Implementations; @@ -10,16 +11,21 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand { private readonly IUserService _userService; private readonly IUserRepository _userRepository; + private readonly ICipherRepository _cipherRepository; + private readonly IFolderRepository _folderRepository; private readonly IEmergencyAccessRepository _emergencyAccessRepository; private readonly IPushNotificationService _pushService; private readonly IdentityErrorDescriber _identityErrorDescriber; public RotateUserKeyCommand(IUserService userService, IUserRepository userRepository, + ICipherRepository cipherRepository, IFolderRepository folderRepository, IEmergencyAccessRepository emergencyAccessRepository, IPushNotificationService pushService, IdentityErrorDescriber errors) { _userService = userService; _userRepository = userRepository; + _cipherRepository = cipherRepository; + _folderRepository = folderRepository; _emergencyAccessRepository = emergencyAccessRepository; _pushService = pushService; _identityErrorDescriber = errors; @@ -48,10 +54,16 @@ public class RotateUserKeyCommand : IRotateUserKeyCommand model.ResetPasswordKeys.Any()) { List saveEncryptedDataActions = new(); - // if (model.Ciphers.Any()) - // { - // saveEncryptedDataActions.Add(_cipherRepository.SaveRotatedData); - // } + + if (model.Ciphers.Any()) + { + saveEncryptedDataActions.Add(_cipherRepository.UpdateForKeyRotation(user.Id, model.Ciphers)); + } + + if (model.Folders.Any()) + { + saveEncryptedDataActions.Add(_folderRepository.UpdateForKeyRotation(user.Id, model.Folders)); + } if (model.EmergencyAccessKeys.Any()) { saveEncryptedDataActions.Add( diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index 811e48a17..ae3b72ad0 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -38,4 +39,12 @@ public interface ICipherRepository : IRepository Task RestoreAsync(IEnumerable ids, Guid userId, bool useFlexibleCollections); Task RestoreByIdsOrganizationIdAsync(IEnumerable ids, Guid organizationId); Task DeleteDeletedAsync(DateTime deletedDateBefore); + + /// + /// Updates encrypted data for ciphers during a key rotation + /// + /// The user that initiated the key rotation + /// A list of ciphers with updated data + UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + IEnumerable ciphers); } diff --git a/src/Core/Vault/Repositories/IFolderRepository.cs b/src/Core/Vault/Repositories/IFolderRepository.cs index 3b7c2f77f..f19243761 100644 --- a/src/Core/Vault/Repositories/IFolderRepository.cs +++ b/src/Core/Vault/Repositories/IFolderRepository.cs @@ -1,4 +1,5 @@ -using Bit.Core.Repositories; +using Bit.Core.Auth.UserFeatures.UserKey; +using Bit.Core.Repositories; using Bit.Core.Vault.Entities; namespace Bit.Core.Vault.Repositories; @@ -7,4 +8,12 @@ public interface IFolderRepository : IRepository { Task GetByIdAsync(Guid id, Guid userId); Task> GetManyByUserIdAsync(Guid userId); + + /// + /// Updates encrypted data for folders during a key rotation + /// + /// The user that initiated the key rotation + /// A list of folders with updated data + UpdateEncryptedDataForKeyRotation UpdateForKeyRotation(Guid userId, + IEnumerable folders); } diff --git a/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs b/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs new file mode 100644 index 000000000..8f60d81d9 --- /dev/null +++ b/src/Infrastructure.Dapper/Vault/Helpers/CipherHelpers.cs @@ -0,0 +1,33 @@ +using System.Data; +using Bit.Core.Vault.Entities; +using Dapper; + +namespace Bit.Infrastructure.Dapper.Vault.Helpers; + +public static class CipherHelpers +{ + public static DataTable ToDataTable(this IEnumerable ciphers) + { + var ciphersTable = new DataTable(); + ciphersTable.SetTypeName("[dbo].[Cipher]"); + + var columnData = new List<(string name, Type type, Func getter)> + { + (nameof(Cipher.Id), typeof(Guid), c => c.Id), + (nameof(Cipher.UserId), typeof(Guid), c => c.UserId), + (nameof(Cipher.OrganizationId), typeof(Guid), c => c.OrganizationId), + (nameof(Cipher.Type), typeof(short), c => c.Type), + (nameof(Cipher.Data), typeof(string), c => c.Data), + (nameof(Cipher.Favorites), typeof(string), c => c.Favorites), + (nameof(Cipher.Folders), typeof(string), c => c.Folders), + (nameof(Cipher.Attachments), typeof(string), c => c.Attachments), + (nameof(Cipher.CreationDate), typeof(DateTime), c => c.CreationDate), + (nameof(Cipher.RevisionDate), typeof(DateTime), c => c.RevisionDate), + (nameof(Cipher.DeletedDate), typeof(DateTime), c => c.DeletedDate), + (nameof(Cipher.Reprompt), typeof(short), c => c.Reprompt), + (nameof(Cipher.Key), typeof(string), c => c.Key), + }; + + return ciphers.BuildTable(ciphersTable, columnData); + } +} diff --git a/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs b/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs new file mode 100644 index 000000000..4428316de --- /dev/null +++ b/src/Infrastructure.Dapper/Vault/Helpers/FolderHelpers.cs @@ -0,0 +1,25 @@ +using System.Data; +using Bit.Core.Vault.Entities; +using Dapper; + +namespace Bit.Infrastructure.Dapper.Vault.Helpers; + +public static class FolderHelpers +{ + public static DataTable ToDataTable(this IEnumerable folders) + { + var foldersTable = new DataTable(); + foldersTable.SetTypeName("[dbo].[Folder]"); + + var columnData = new List<(string name, Type type, Func getter)> + { + (nameof(Folder.Id), typeof(Guid), c => c.Id), + (nameof(Folder.UserId), typeof(Guid), c => c.UserId), + (nameof(Folder.Name), typeof(string), c => c.Name), + (nameof(Folder.CreationDate), typeof(DateTime), c => c.CreationDate), + (nameof(Folder.RevisionDate), typeof(DateTime), c => c.RevisionDate), + }; + + return folders.BuildTable(foldersTable, columnData); + } +} diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index b1e4043a9..f0bc4c176 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -1,5 +1,6 @@ using System.Data; using System.Text.Json; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Entities; using Bit.Core.Settings; using Bit.Core.Tools.Entities; @@ -7,6 +8,7 @@ using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.Repositories; +using Bit.Infrastructure.Dapper.Vault.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -308,6 +310,63 @@ public class CipherRepository : Repository, ICipherRepository } } + /// + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + Guid userId, IEnumerable ciphers) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + // Create temp table + var sqlCreateTemp = @" + SELECT TOP 0 * + INTO #TempCipher + FROM [dbo].[Cipher]"; + + await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) + { + cmd.ExecuteNonQuery(); + } + + // Bulk copy data into temp table + using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "#TempCipher"; + var ciphersTable = ciphers.ToDataTable(); + foreach (DataColumn col in ciphersTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + ciphersTable.PrimaryKey = new DataColumn[] { ciphersTable.Columns[0] }; + await bulkCopy.WriteToServerAsync(ciphersTable); + } + + // Update cipher table from temp table + var sql = @" + UPDATE + [dbo].[Cipher] + SET + [Data] = TC.[Data], + [Attachments] = TC.[Attachments], + [RevisionDate] = TC.[RevisionDate], + [Key] = TC.[Key] + FROM + [dbo].[Cipher] C + INNER JOIN + #TempCipher TC ON C.Id = TC.Id + WHERE + C.[UserId] = @UserId + + DROP TABLE #TempCipher"; + + await using (var cmd = new SqlCommand(sql, connection, transaction)) + { + cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; + cmd.ExecuteNonQuery(); + } + }; + } + public Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders, IEnumerable sends) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs index 1703b969a..bf1548b24 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/FolderRepository.cs @@ -1,8 +1,10 @@ using System.Data; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Settings; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.Dapper.Repositories; +using Bit.Infrastructure.Dapper.Vault.Helpers; using Dapper; using Microsoft.Data.SqlClient; @@ -41,4 +43,60 @@ public class FolderRepository : Repository, IFolderRepository return results.ToList(); } } + + /// + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + Guid userId, IEnumerable folders) + { + return async (SqlConnection connection, SqlTransaction transaction) => + { + // Create temp table + var sqlCreateTemp = @" + SELECT TOP 0 * + INTO #TempFolder + FROM [dbo].[Folder]"; + + await using (var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) + { + cmd.ExecuteNonQuery(); + } + + // Bulk copy data into temp table + using (var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "#TempFolder"; + var foldersTable = folders.ToDataTable(); + foreach (DataColumn col in foldersTable.Columns) + { + bulkCopy.ColumnMappings.Add(col.ColumnName, col.ColumnName); + } + + foldersTable.PrimaryKey = new DataColumn[] { foldersTable.Columns[0] }; + await bulkCopy.WriteToServerAsync(foldersTable); + } + + // Update folder table from temp table + var sql = @" + UPDATE + [dbo].[Folder] + SET + [Name] = TF.[Name], + [RevisionDate] = TF.[RevisionDate] + FROM + [dbo].[Folder] F + INNER JOIN + #TempFolder TF ON F.Id = TF.Id + WHERE + F.[UserId] = @UserId; + + DROP TABLE #TempFolder"; + + await using (var cmd = new SqlCommand(sql, connection, transaction)) + { + cmd.Parameters.Add("@UserId", SqlDbType.UniqueIdentifier).Value = userId; + cmd.ExecuteNonQuery(); + } + }; + } + } diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 8ccf6dab2..0e2ecd145 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -161,6 +161,8 @@ public class UserRepository : Repository, IUserR entity.AccountRevisionDate = user.AccountRevisionDate; entity.RevisionDate = user.RevisionDate; + await dbContext.SaveChangesAsync(); + // Update re-encrypted data foreach (var action in updateDataActions) { diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 418938aab..f24fa29e6 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -1,6 +1,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using AutoMapper; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Core.Vault.Enums; @@ -13,6 +14,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Vault.Queries; using Bit.Infrastructure.EntityFramework.Vault.Models; using Bit.Infrastructure.EntityFramework.Vault.Repositories.Queries; using LinqToDB.EntityFrameworkCore; +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using NS = Newtonsoft.Json; @@ -825,6 +827,34 @@ public class CipherRepository : Repository + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + Guid userId, IEnumerable ciphers) + { + return async (SqlConnection _, SqlTransaction _) => + { + var newCiphers = ciphers.ToList(); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var userCiphers = await GetDbSet(dbContext) + .Where(c => c.UserId == userId) + .ToListAsync(); + var validCiphers = userCiphers + .Where(cipher => newCiphers.Any(newCipher => newCipher.Id == cipher.Id)); + foreach (var cipher in validCiphers) + { + var updateCipher = newCiphers.First(newCipher => newCipher.Id == cipher.Id); + cipher.Data = updateCipher.Data; + cipher.Attachments = updateCipher.Attachments; + cipher.RevisionDate = updateCipher.RevisionDate; + cipher.Key = updateCipher.Key; + } + + await dbContext.SaveChangesAsync(); + }; + } + + public async Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders, IEnumerable sends) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs index 42b79a45e..0bab189de 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/FolderRepository.cs @@ -1,7 +1,9 @@ using AutoMapper; +using Bit.Core.Auth.UserFeatures.UserKey; using Bit.Core.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Models; +using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -36,4 +38,28 @@ public class FolderRepository : Repository>(folders); } } + + /// + public UpdateEncryptedDataForKeyRotation UpdateForKeyRotation( + Guid userId, IEnumerable folders) + { + return async (SqlConnection _, SqlTransaction _) => + { + var newFolders = folders.ToList(); + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var userFolders = await GetDbSet(dbContext) + .Where(f => f.UserId == userId) + .ToListAsync(); + var validFolders = userFolders + .Where(folder => newFolders.Any(newFolder => newFolder.Id == folder.Id)); + foreach (var folder in validFolders) + { + var updateFolder = newFolders.First(newFolder => newFolder.Id == folder.Id); + folder.Name = updateFolder.Name; + } + + await dbContext.SaveChangesAsync(); + }; + } } diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index 99d3ac77b..b0a996400 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -3,6 +3,7 @@ using Bit.Api.Auth.Controllers; using Bit.Api.Auth.Models.Request; using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Auth.Validators; +using Bit.Api.Vault.Models.Request; using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; @@ -21,6 +22,7 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Services; +using Bit.Core.Vault.Entities; using Bit.Core.Vault.Repositories; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Identity; @@ -51,6 +53,9 @@ public class AccountsControllerTests : IDisposable private readonly IFeatureService _featureService; private readonly ICurrentContext _currentContext; + + private readonly IRotationValidator, IEnumerable> _cipherValidator; + private readonly IRotationValidator, IEnumerable> _folderValidator; private readonly IRotationValidator, IEnumerable> _emergencyAccessValidator; @@ -74,6 +79,10 @@ public class AccountsControllerTests : IDisposable _rotateUserKeyCommand = Substitute.For(); _featureService = Substitute.For(); _currentContext = Substitute.For(); + _cipherValidator = + Substitute.For, IEnumerable>>(); + _folderValidator = + Substitute.For, IEnumerable>>(); _emergencyAccessValidator = Substitute.For, IEnumerable>>(); @@ -95,6 +104,8 @@ public class AccountsControllerTests : IDisposable _rotateUserKeyCommand, _featureService, _currentContext, + _cipherValidator, + _folderValidator, _emergencyAccessValidator ); } diff --git a/test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs b/test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs new file mode 100644 index 000000000..50e5879dc --- /dev/null +++ b/test/Api.Test/Vault/Validators/CipherRotationValidatorTests.cs @@ -0,0 +1,45 @@ +using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Validators; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Vault.Validators; + +[SutProviderCustomize] +public class CipherRotationValidatorTests +{ + [Theory, BitAutoData] + public async Task ValidateAsync_MissingCipher_Throws(SutProvider sutProvider, User user, + IEnumerable ciphers) + { + var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type }) + .ToList(); + userCiphers.Add(new CipherDetails { Id = Guid.NewGuid(), Type = Core.Vault.Enums.CipherType.Login }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id, Arg.Any()) + .Returns(userCiphers); + + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, ciphers)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_CipherDoesNotBelongToUser_NotIncluded( + SutProvider sutProvider, User user, IEnumerable ciphers) + { + var userCiphers = ciphers.Select(c => new CipherDetails { Id = c.Id.GetValueOrDefault(), Type = c.Type }) + .ToList(); + userCiphers.RemoveAt(0); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id, Arg.Any()) + .Returns(userCiphers); + + var result = await sutProvider.Sut.ValidateAsync(user, ciphers); + + Assert.DoesNotContain(result, c => c.Id == ciphers.First().Id); + } +} diff --git a/test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs b/test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs new file mode 100644 index 000000000..acf987862 --- /dev/null +++ b/test/Api.Test/Vault/Validators/FolderRotationValidatorTests.cs @@ -0,0 +1,42 @@ +using Bit.Api.Vault.Models.Request; +using Bit.Api.Vault.Validators; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Vault.Entities; +using Bit.Core.Vault.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Vault.Validators; + +[SutProviderCustomize] +public class FolderRotationValidatorTests +{ + [Theory] + [BitAutoData] + public async Task ValidateAsync_MissingFolder_Throws(SutProvider sutProvider, User user, + IEnumerable folders) + { + var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList(); + userFolders.Add(new Folder { Id = Guid.NewGuid(), Name = "Missing Folder" }); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(userFolders); + + await Assert.ThrowsAsync(async () => await sutProvider.Sut.ValidateAsync(user, folders)); + } + + [Theory] + [BitAutoData] + public async Task ValidateAsync_FolderDoesNotBelongToUser_NotReturned( + SutProvider sutProvider, User user, IEnumerable folders) + { + var userFolders = folders.Select(f => f.ToFolder(new Folder())).ToList(); + userFolders.RemoveAt(0); + sutProvider.GetDependency().GetManyByUserIdAsync(user.Id).Returns(userFolders); + + var result = await sutProvider.Sut.ValidateAsync(user, folders); + + Assert.DoesNotContain(result, c => c.Id == folders.First().Id); + } +}