diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index e8b6d9559d..3b291d38f8 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -82,7 +82,8 @@ namespace Bit.Api.Controllers // NOTE: It is assumed that the eventual repository call will make sure the updated // ciphers belong to user making this call. Therefore, no check is done here. - var ciphers = model.Ciphers.Select(c => c.ToCipher(user.Id)); + var ciphers = model.Data.Ciphers.Select(c => c.ToCipher(user.Id)); + var folders = model.Data.Folders.Select(c => c.ToFolder(user.Id)); var result = await _userService.ChangeEmailAsync( user, @@ -90,7 +91,9 @@ namespace Bit.Api.Controllers model.NewEmail, model.NewMasterPasswordHash, model.Token, - ciphers); + ciphers, + folders, + model.Data.PrivateKey); if(result.Succeeded) { @@ -114,13 +117,16 @@ namespace Bit.Api.Controllers // NOTE: It is assumed that the eventual repository call will make sure the updated // ciphers belong to user making this call. Therefore, no check is done here. - var ciphers = model.Ciphers.Select(c => c.ToCipher(user.Id)); + var ciphers = model.Data.Ciphers.Select(c => c.ToCipher(user.Id)); + var folders = model.Data.Folders.Select(c => c.ToFolder(user.Id)); var result = await _userService.ChangePasswordAsync( user, model.MasterPasswordHash, model.NewMasterPasswordHash, - ciphers); + ciphers, + folders, + model.Data.PrivateKey); if(result.Succeeded) { diff --git a/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs b/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs index 7d7bc3df02..000c3a50c5 100644 --- a/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs @@ -17,6 +17,6 @@ namespace Bit.Core.Models.Api [Required] public string Token { get; set; } [Required] - public CipherRequestModel[] Ciphers { get; set; } + public DataReloadRequestModel Data { get; set; } } } diff --git a/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs b/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs index 6233ddee08..1b524863dc 100644 --- a/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs @@ -11,6 +11,6 @@ namespace Bit.Core.Models.Api [StringLength(300)] public string NewMasterPasswordHash { get; set; } [Required] - public CipherRequestModel[] Ciphers { get; set; } + public DataReloadRequestModel Data { get; set; } } } diff --git a/src/Core/Models/Api/Request/CipherRequestModel.cs b/src/Core/Models/Api/Request/CipherRequestModel.cs index e3008b55f5..a6bad7e17b 100644 --- a/src/Core/Models/Api/Request/CipherRequestModel.cs +++ b/src/Core/Models/Api/Request/CipherRequestModel.cs @@ -18,8 +18,6 @@ namespace Bit.Core.Models.Api public string Id { get; set; } [StringLength(36)] public string OrganizationId { get; set; } - [StringLength(36)] - public string FolderId { get; set; } [Required] [EncryptedString] [StringLength(300)] diff --git a/src/Core/Models/Api/Request/DataReloadRequestModel.cs b/src/Core/Models/Api/Request/DataReloadRequestModel.cs new file mode 100644 index 0000000000..6d9af213dc --- /dev/null +++ b/src/Core/Models/Api/Request/DataReloadRequestModel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class DataReloadRequestModel + { + [Required] + public IEnumerable Ciphers { get; set; } + [Required] + public IEnumerable Folders { get; set; } + [Required] + public string PrivateKey { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/FolderRequestModel.cs b/src/Core/Models/Api/Request/FolderRequestModel.cs index 5ba828999e..74729285c8 100644 --- a/src/Core/Models/Api/Request/FolderRequestModel.cs +++ b/src/Core/Models/Api/Request/FolderRequestModel.cs @@ -27,4 +27,18 @@ namespace Bit.Core.Models.Api return existingFolder; } } + + public class FolderWithIdRequestModel : FolderRequestModel + { + public Guid Id { get; set; } + + public new Folder ToFolder(Guid userId) + { + return ToFolder(new Folder + { + UserId = userId, + Id = Id + }); + } + } } diff --git a/src/Core/Models/Api/Request/LoginRequestModel.cs b/src/Core/Models/Api/Request/LoginRequestModel.cs index fd79f4fa60..f511805c8e 100644 --- a/src/Core/Models/Api/Request/LoginRequestModel.cs +++ b/src/Core/Models/Api/Request/LoginRequestModel.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; using Newtonsoft.Json; using Core.Models.Data; +using Bit.Core.Models.Table; namespace Bit.Core.Models.Api { @@ -48,4 +49,18 @@ namespace Bit.Core.Models.Api return existingLogin; } } + + public class LoginWithIdRequestModel : LoginRequestModel + { + public Guid Id { get; set; } + + public Cipher ToCipher(Guid userId) + { + return ToCipherDetails(new CipherDetails + { + UserId = userId, + Id = Id + }); + } + } } diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 4652167610..585ef80dad 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -18,7 +18,7 @@ namespace Bit.Core.Repositories Task UpsertAsync(CipherDetails cipher); Task ReplaceAsync(Cipher obj, IEnumerable subvaultIds); Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); - Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers); + Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable folders); } } diff --git a/src/Core/Repositories/SqlServer/CipherRepository.cs b/src/Core/Repositories/SqlServer/CipherRepository.cs index 5a9731f38d..8f342a6a5e 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -154,7 +154,7 @@ namespace Bit.Core.Repositories.SqlServer } } - public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers) + public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders) { if(ciphers.Count() == 0) { @@ -179,6 +179,7 @@ namespace Bit.Core.Repositories.SqlServer cmd.Parameters.Add("@EmailVerified", SqlDbType.NVarChar).Value = user.EmailVerified; cmd.Parameters.Add("@MasterPassword", SqlDbType.NVarChar).Value = user.MasterPassword; cmd.Parameters.Add("@SecurityStamp", SqlDbType.NVarChar).Value = user.SecurityStamp; + cmd.Parameters.Add("@PrivateKey", SqlDbType.VarChar).Value = user.PrivateKey; cmd.Parameters.Add("@RevisionDate", SqlDbType.DateTime2).Value = user.RevisionDate; cmd.ExecuteNonQuery(); } @@ -188,7 +189,11 @@ namespace Bit.Core.Repositories.SqlServer var sqlCreateTemp = @" SELECT TOP 0 * INTO #TempCipher - FROM [dbo].[Cipher]"; + FROM [dbo].[Cipher] + + SELECT TOP 0 * + INTO #TempFolder + FROM [dbo].[Folder]"; using(var cmd = new SqlCommand(sqlCreateTemp, connection, transaction)) { @@ -204,6 +209,13 @@ namespace Bit.Core.Repositories.SqlServer bulkCopy.WriteToServer(dataTable); } + using(var bulkCopy = new SqlBulkCopy(connection, SqlBulkCopyOptions.KeepIdentity, transaction)) + { + bulkCopy.DestinationTableName = "#TempFolder"; + var dataTable = BuildFoldersTable(folders); + bulkCopy.WriteToServer(dataTable); + } + // 4. Insert into real tables from temp tables and clean up. var sqlUpdate = @" @@ -219,7 +231,20 @@ namespace Bit.Core.Repositories.SqlServer WHERE C.[UserId] = @UserId - DROP TABLE #TempCipher"; + 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 #TempCipher + DROP TABLE #TempFolder"; using(var cmd = new SqlCommand(sqlUpdate, connection, transaction)) { diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 288285d985..baecdfaa98 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -18,8 +18,10 @@ namespace Bit.Core.Services Task RegisterUserAsync(User user, string masterPassword); Task SendMasterPasswordHintAsync(string email); Task InitiateEmailChangeAsync(User user, string newEmail); - Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, string token, IEnumerable ciphers); - Task ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, IEnumerable ciphers); + Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, + string token, IEnumerable ciphers, IEnumerable folders, string privateKey); + Task ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, + IEnumerable ciphers, IEnumerable folders, string privateKey); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider); Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 688f1c328d..338c79ae9a 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -169,8 +169,8 @@ namespace Bit.Core.Services await _mailService.SendChangeEmailEmailAsync(newEmail, token); } - public async Task ChangeEmailAsync(User user, string masterPassword, string newEmail, - string newMasterPassword, string token, IEnumerable ciphers) + public async Task ChangeEmailAsync(User user, string masterPassword, string newEmail, + string newMasterPassword, string token, IEnumerable ciphers, IEnumerable folders, string privateKey) { var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword); if(verifyPasswordResult == PasswordVerificationResult.Failed) @@ -199,10 +199,11 @@ namespace Bit.Core.Services user.Email = newEmail; user.EmailVerified = true; user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; + user.PrivateKey = privateKey; if(ciphers.Any()) { - await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers); + await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers, folders); } else { @@ -218,7 +219,7 @@ namespace Bit.Core.Services } public async Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, - IEnumerable ciphers) + IEnumerable ciphers, IEnumerable folders, string privateKey) { if(user == null) { @@ -234,9 +235,10 @@ namespace Bit.Core.Services } user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; + user.PrivateKey = privateKey; if(ciphers.Any()) { - await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers); + await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers, folders); } else { diff --git a/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql b/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql index 501052ef38..fa368af81a 100644 --- a/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql +++ b/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql @@ -4,6 +4,7 @@ @EmailVerified BIT, @MasterPassword NVARCHAR(300), @SecurityStamp NVARCHAR(50), + @PrivateKey VARCHAR(MAX), @RevisionDate DATETIME2(7) AS BEGIN @@ -16,6 +17,7 @@ BEGIN [EmailVerified] = @EmailVerified, [MasterPassword] = @MasterPassword, [SecurityStamp] = @SecurityStamp, + [PrivateKey] = @PrivateKey, [RevisionDate] = @RevisionDate, [AccountRevisionDate] = @RevisionDate WHERE