From a01d5d9a51d0175e9c3e39fa8271a469df07a105 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Wed, 31 May 2017 09:54:32 -0400 Subject: [PATCH] "user key" schema and api changes --- src/Api/Controllers/AccountsController.cs | 52 +++++++++++-------- .../Api/Request/Accounts/EmailRequestModel.cs | 2 +- .../Request/Accounts/PasswordRequestModel.cs | 2 +- .../Request/Accounts/RegisterRequestModel.cs | 6 +++ .../UpdateKeyRequestModel.cs} | 7 ++- .../Models/Api/Response/KeysResponseModel.cs | 2 + src/Core/Models/Table/User.cs | 1 + src/Core/Repositories/ICipherRepository.cs | 2 +- .../SqlServer/CipherRepository.cs | 10 ++-- src/Core/Services/IUserService.cs | 11 ++-- .../Services/Implementations/UserService.cs | 43 ++++++++++----- src/Sql/Sql.sqlproj | 2 +- src/Sql/dbo/Stored Procedures/User_Create.sql | 3 ++ src/Sql/dbo/Stored Procedures/User_Update.sql | 2 + ...eEmailPassword.sql => User_UpdateKeys.sql} | 10 ++-- src/Sql/dbo/Tables/User.sql | 1 + 16 files changed, 97 insertions(+), 59 deletions(-) rename src/Core/Models/Api/Request/{DataReloadRequestModel.cs => Accounts/UpdateKeyRequestModel.cs} (65%) rename src/Sql/dbo/Stored Procedures/{User_UpdateEmailPassword.sql => User_UpdateKeys.sql} (60%) diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index f3f1c8ea5b..01872caac0 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -78,23 +78,8 @@ namespace Bit.Api.Controllers public async Task PutEmail([FromBody]EmailRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); - - // 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.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, - model.MasterPasswordHash, - model.NewEmail, - model.NewMasterPasswordHash, - model.Token, - ciphers, - folders, - model.Data.PrivateKey); - + var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail, + model.NewMasterPasswordHash, model.Token, model.Key); if(result.Succeeded) { return; @@ -112,22 +97,43 @@ namespace Bit.Api.Controllers [HttpPut("password")] [HttpPost("password")] public async Task PutPassword([FromBody]PasswordRequestModel model) + { + var user = await _userService.GetUserByPrincipalAsync(User); + var result = await _userService.ChangePasswordAsync(user, model.MasterPasswordHash, + model.NewMasterPasswordHash, model.Key); + if(result.Succeeded) + { + return; + } + + foreach(var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + + await Task.Delay(2000); + throw new BadRequestException(ModelState); + } + + [HttpPut("key")] + [HttpPost("key")] + public async Task PutKey([FromBody]UpdateKeyRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); // 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.Data.Ciphers.Select(c => c.ToCipher(user.Id)); - var folders = model.Data.Folders.Select(c => c.ToFolder(user.Id)); + var ciphers = model.Ciphers.Select(c => c.ToCipher(user.Id)); + var folders = model.Folders.Select(c => c.ToFolder(user.Id)); - var result = await _userService.ChangePasswordAsync( + var result = await _userService.UpdateKeyAsync( user, model.MasterPasswordHash, - model.NewMasterPasswordHash, + model.Key, + model.PrivateKey, ciphers, - folders, - model.Data.PrivateKey); + folders); if(result.Succeeded) { diff --git a/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs b/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs index 3ce45d1ca4..ced59336af 100644 --- a/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/EmailRequestModel.cs @@ -18,6 +18,6 @@ namespace Bit.Core.Models.Api [Required] public string Token { get; set; } [Required] - public DataReloadRequestModel Data { get; set; } + public string Key { get; set; } } } diff --git a/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs b/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs index 2dc48786c4..d3d5a72fc9 100644 --- a/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/PasswordRequestModel.cs @@ -12,6 +12,6 @@ namespace Bit.Core.Models.Api [StringLength(300)] public string NewMasterPasswordHash { get; set; } [Required] - public DataReloadRequestModel Data { get; set; } + public string Key { get; set; } } } diff --git a/src/Core/Models/Api/Request/Accounts/RegisterRequestModel.cs b/src/Core/Models/Api/Request/Accounts/RegisterRequestModel.cs index 7168e29116..859a4e7e1d 100644 --- a/src/Core/Models/Api/Request/Accounts/RegisterRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/RegisterRequestModel.cs @@ -16,6 +16,7 @@ namespace Bit.Core.Models.Api public string MasterPasswordHash { get; set; } [StringLength(50)] public string MasterPasswordHint { get; set; } + public string Key { get; set; } public KeysRequestModel Keys { get; set; } public User ToUser() @@ -27,6 +28,11 @@ namespace Bit.Core.Models.Api MasterPasswordHint = MasterPasswordHint }; + if(Key != null) + { + user.Key = Key; + } + if(Keys != null) { Keys.ToUser(user); diff --git a/src/Core/Models/Api/Request/DataReloadRequestModel.cs b/src/Core/Models/Api/Request/Accounts/UpdateKeyRequestModel.cs similarity index 65% rename from src/Core/Models/Api/Request/DataReloadRequestModel.cs rename to src/Core/Models/Api/Request/Accounts/UpdateKeyRequestModel.cs index 6d9af213dc..21787c7a33 100644 --- a/src/Core/Models/Api/Request/DataReloadRequestModel.cs +++ b/src/Core/Models/Api/Request/Accounts/UpdateKeyRequestModel.cs @@ -3,13 +3,18 @@ using System.ComponentModel.DataAnnotations; namespace Bit.Core.Models.Api { - public class DataReloadRequestModel + public class UpdateKeyRequestModel { + [Required] + [StringLength(300)] + public string MasterPasswordHash { get; set; } [Required] public IEnumerable Ciphers { get; set; } [Required] public IEnumerable Folders { get; set; } [Required] public string PrivateKey { get; set; } + [Required] + public string Key { get; set; } } } diff --git a/src/Core/Models/Api/Response/KeysResponseModel.cs b/src/Core/Models/Api/Response/KeysResponseModel.cs index 35b1d22730..c9a9b88c2b 100644 --- a/src/Core/Models/Api/Response/KeysResponseModel.cs +++ b/src/Core/Models/Api/Response/KeysResponseModel.cs @@ -13,10 +13,12 @@ namespace Bit.Core.Models.Api throw new ArgumentNullException(nameof(user)); } + Key = user.Key; PublicKey = user.PublicKey; PrivateKey = user.PrivateKey; } + public string Key { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } } diff --git a/src/Core/Models/Table/User.cs b/src/Core/Models/Table/User.cs index 54ae8c3b6a..eaad10905b 100644 --- a/src/Core/Models/Table/User.cs +++ b/src/Core/Models/Table/User.cs @@ -21,6 +21,7 @@ namespace Bit.Core.Models.Table public string EquivalentDomains { get; set; } public string ExcludedGlobalEquivalentDomains { get; set; } public DateTime AccountRevisionDate { get; internal set; } = DateTime.UtcNow; + public string Key { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Repositories/ICipherRepository.cs b/src/Core/Repositories/ICipherRepository.cs index 361fdab562..cf6f67cc6d 100644 --- a/src/Core/Repositories/ICipherRepository.cs +++ b/src/Core/Repositories/ICipherRepository.cs @@ -19,7 +19,7 @@ namespace Bit.Core.Repositories Task UpsertAsync(CipherDetails cipher); Task ReplaceAsync(Cipher obj, IEnumerable collectionIds); Task UpdatePartialAsync(Guid id, Guid userId, Guid? folderId, bool favorite); - Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders); + Task UpdateUserKeysAndCiphersAsync(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 d65d65060d..6a08b1990d 100644 --- a/src/Core/Repositories/SqlServer/CipherRepository.cs +++ b/src/Core/Repositories/SqlServer/CipherRepository.cs @@ -176,7 +176,7 @@ namespace Bit.Core.Repositories.SqlServer } } - public Task UpdateUserEmailPasswordAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders) + public Task UpdateUserKeysAndCiphersAsync(User user, IEnumerable ciphers, IEnumerable folders) { using(var connection = new SqlConnection(ConnectionString)) { @@ -188,14 +188,13 @@ namespace Bit.Core.Repositories.SqlServer { // 1. Update user. - using(var cmd = new SqlCommand("[dbo].[User_UpdateEmailPassword]", connection, transaction)) + using(var cmd = new SqlCommand("[dbo].[User_UpdateKeys]", connection, transaction)) { cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add("@Id", SqlDbType.UniqueIdentifier).Value = user.Id; - cmd.Parameters.Add("@Email", SqlDbType.NVarChar).Value = user.Email; - 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("@Key", SqlDbType.VarChar).Value = user.Key; + if(string.IsNullOrWhiteSpace(user.PrivateKey)) { cmd.Parameters.Add("@PrivateKey", SqlDbType.VarChar).Value = DBNull.Value; @@ -204,6 +203,7 @@ namespace Bit.Core.Repositories.SqlServer { cmd.Parameters.Add("@PrivateKey", SqlDbType.VarChar).Value = user.PrivateKey; } + cmd.Parameters.Add("@RevisionDate", SqlDbType.DateTime2).Value = user.RevisionDate; cmd.ExecuteNonQuery(); } diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index baecdfaa98..794e97e509 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -18,14 +18,15 @@ 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, IEnumerable folders, string privateKey); - Task ChangePasswordAsync(User user, string currentMasterPasswordHash, string newMasterPasswordHash, - IEnumerable ciphers, IEnumerable folders, string privateKey); + Task ChangeEmailAsync(User user, string masterPassword, string newEmail, string newMasterPassword, + string token, string key); + Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key); + Task UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, + IEnumerable ciphers, IEnumerable folders); Task RefreshSecurityStampAsync(User user, string masterPasswordHash); Task GetTwoFactorAsync(User user, Enums.TwoFactorProviderType provider); Task RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode); - Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); + Task GenerateUserTokenAsync(User user, string tokenProvider, string purpose); Task DeleteAsync(User user); } } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 3dfd7afdfb..9420dcb44c 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -195,7 +195,7 @@ namespace Bit.Core.Services } public async Task ChangeEmailAsync(User user, string masterPassword, string newEmail, - string newMasterPassword, string token, IEnumerable ciphers, IEnumerable folders, string privateKey) + string newMasterPassword, string token, string key) { var verifyPasswordResult = _passwordHasher.VerifyHashedPassword(user, user.MasterPassword, masterPassword); if(verifyPasswordResult == PasswordVerificationResult.Failed) @@ -221,19 +221,11 @@ namespace Bit.Core.Services return result; } + user.Key = key; user.Email = newEmail; user.EmailVerified = true; user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; - user.PrivateKey = privateKey; - - if(ciphers.Any() || folders.Any()) - { - await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers, folders); - } - else - { - await _userRepository.ReplaceAsync(user); - } + await _userRepository.ReplaceAsync(user); return IdentityResult.Success; } @@ -244,7 +236,7 @@ namespace Bit.Core.Services } public async Task ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, - IEnumerable ciphers, IEnumerable folders, string privateKey) + string key) { if(user == null) { @@ -260,10 +252,33 @@ namespace Bit.Core.Services } user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; + user.Key = key; + await _userRepository.ReplaceAsync(user); + + return IdentityResult.Success; + } + + Logger.LogWarning("Change password failed for user {userId}.", user.Id); + return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); + } + + public async Task UpdateKeyAsync(User user, string masterPassword, string key, string privateKey, + IEnumerable ciphers, IEnumerable folders) + { + if(user == null) + { + throw new ArgumentNullException(nameof(user)); + } + + if(await base.CheckPasswordAsync(user, masterPassword)) + { + user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow; + user.SecurityStamp = Guid.NewGuid().ToString(); + user.Key = key; user.PrivateKey = privateKey; if(ciphers.Any() || folders.Any()) { - await _cipherRepository.UpdateUserEmailPasswordAndCiphersAsync(user, ciphers, folders); + await _cipherRepository.UpdateUserKeysAndCiphersAsync(user, ciphers, folders); } else { @@ -273,7 +288,7 @@ namespace Bit.Core.Services return IdentityResult.Success; } - Logger.LogWarning("Change password failed for user {userId}.", user.Id); + Logger.LogWarning("Update key for user {userId}.", user.Id); return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch()); } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 2abbe355cd..2e7d813335 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -148,7 +148,7 @@ - + diff --git a/src/Sql/dbo/Stored Procedures/User_Create.sql b/src/Sql/dbo/Stored Procedures/User_Create.sql index b38b9a3813..e37deb662e 100644 --- a/src/Sql/dbo/Stored Procedures/User_Create.sql +++ b/src/Sql/dbo/Stored Procedures/User_Create.sql @@ -14,6 +14,7 @@ @EquivalentDomains NVARCHAR(MAX), @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), @PublicKey NVARCHAR(MAX), @PrivateKey NVARCHAR(MAX), @CreationDate DATETIME2(7), @@ -39,6 +40,7 @@ BEGIN [EquivalentDomains], [ExcludedGlobalEquivalentDomains], [AccountRevisionDate], + [Key], [PublicKey], [PrivateKey], [CreationDate], @@ -61,6 +63,7 @@ BEGIN @EquivalentDomains, @ExcludedGlobalEquivalentDomains, @AccountRevisionDate, + @Key, @PublicKey, @PrivateKey, @CreationDate, diff --git a/src/Sql/dbo/Stored Procedures/User_Update.sql b/src/Sql/dbo/Stored Procedures/User_Update.sql index a6ce50ce61..5ba2deb7f7 100644 --- a/src/Sql/dbo/Stored Procedures/User_Update.sql +++ b/src/Sql/dbo/Stored Procedures/User_Update.sql @@ -14,6 +14,7 @@ @EquivalentDomains NVARCHAR(MAX), @ExcludedGlobalEquivalentDomains NVARCHAR(MAX), @AccountRevisionDate DATETIME2(7), + @Key NVARCHAR(MAX), @PublicKey NVARCHAR(MAX), @PrivateKey NVARCHAR(MAX), @CreationDate DATETIME2(7), @@ -39,6 +40,7 @@ BEGIN [EquivalentDomains] = @EquivalentDomains, [ExcludedGlobalEquivalentDomains] = @ExcludedGlobalEquivalentDomains, [AccountRevisionDate] = @AccountRevisionDate, + [Key] = @Key, [PublicKey] = @PublicKey, [PrivateKey] = @PrivateKey, [CreationDate] = @CreationDate, diff --git a/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql b/src/Sql/dbo/Stored Procedures/User_UpdateKeys.sql similarity index 60% rename from src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql rename to src/Sql/dbo/Stored Procedures/User_UpdateKeys.sql index 06d16d8d86..8df14d0ab2 100644 --- a/src/Sql/dbo/Stored Procedures/User_UpdateEmailPassword.sql +++ b/src/Sql/dbo/Stored Procedures/User_UpdateKeys.sql @@ -1,9 +1,7 @@ -CREATE PROCEDURE [dbo].[User_UpdateEmailPassword] +CREATE PROCEDURE [dbo].[User_UpdateKeys] @Id UNIQUEIDENTIFIER, - @Email NVARCHAR(50), - @EmailVerified BIT, - @MasterPassword NVARCHAR(300), @SecurityStamp NVARCHAR(50), + @Key NVARCHAR(MAX), @PrivateKey VARCHAR(MAX), @RevisionDate DATETIME2(7) AS @@ -13,10 +11,8 @@ BEGIN UPDATE [dbo].[User] SET - [Email] = @Email, - [EmailVerified] = @EmailVerified, - [MasterPassword] = @MasterPassword, [SecurityStamp] = @SecurityStamp, + [Key] = @Key, [PrivateKey] = @PrivateKey, [RevisionDate] = @RevisionDate, [AccountRevisionDate] = @RevisionDate diff --git a/src/Sql/dbo/Tables/User.sql b/src/Sql/dbo/Tables/User.sql index f55c2f0cf6..b413ed37d8 100644 --- a/src/Sql/dbo/Tables/User.sql +++ b/src/Sql/dbo/Tables/User.sql @@ -14,6 +14,7 @@ [EquivalentDomains] NVARCHAR (MAX) NULL, [ExcludedGlobalEquivalentDomains] NVARCHAR (MAX) NULL, [AccountRevisionDate] DATETIME2 (7) NOT NULL, + [Key] VARCHAR (MAX) NULL, [PublicKey] VARCHAR (MAX) NULL, [PrivateKey] VARCHAR (MAX) NULL, [CreationDate] DATETIME2 (7) NOT NULL,