diff --git a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs index f3f9a26adba1..b9dfa7934b5f 100644 --- a/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs +++ b/src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs @@ -1,11 +1,12 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.KeyManagement.Models.Api.Request; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Utilities; namespace Bit.Core.Auth.Models.Api.Request.Accounts; using System.ComponentModel.DataAnnotations; +using Bit.Core.KeyManagement.Models.Api.Request; public enum RegisterFinishTokenType : byte { @@ -39,7 +40,12 @@ public class RegisterFinishRequestModel : IValidatableObject // in the MasterPasswordAuthenticationData. public string? UserSymmetricKey { get; set; } - public required KeysRequestModel UserAsymmetricKeys { get; set; } + // TODO Remove property below, deprecated due to new AccountKeys property + // https://bitwarden.atlassian.net/browse/PM-TBD + // Will throw error if both UserAsymmetricKeys and AccountKeys do not exist. + public KeysRequestModel? UserAsymmetricKeys { get; set; } + + public AccountKeysRequestModel? AccountKeys { get; set; } // PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData) public KdfType? Kdf { get; set; } @@ -62,6 +68,8 @@ public class RegisterFinishRequestModel : IValidatableObject public Guid? ProviderUserId { get; set; } + // TODO remove with https://bitwarden.atlassian.net/browse/PM-TBD + [Obsolete("Use ToV2User instead")] public User ToUser() { var user = new User @@ -80,11 +88,70 @@ public User ToUser() Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey ?? throw new BadRequestException("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."), }; - UserAsymmetricKeys.ToUser(user); + user = UserAsymmetricKeys?.ToUser(user) ?? throw new Exception("User's public and private account keys couldn't be found in either AccountKeys or UserAsymmetricKeys"); return user; } + public User ToV2User() + { + return new User + { + Email = Email, + MasterPasswordHint = MasterPasswordHint, + }; + } + + public RegisterFinishData ToData() + { + // TODO clean up flow once old fields are deprecated + // https://bitwarden.atlassian.net/browse/PM-TBD + return new RegisterFinishData + { + MasterPasswordUnlockData = MasterPasswordUnlock?.ToData() ?? + new MasterPasswordUnlockData + { + Kdf = new KdfSettings + { + KdfType = Kdf ?? throw new Exception("KdfType couldn't be found on either the MasterPasswordUnlockData or the Kdf property passed in."), + Iterations = KdfIterations ?? throw new Exception("KdfIterations couldn't be found on either the MasterPasswordUnlockData or the KdfIterations property passed in."), + // KdfMemory and KdfParallelism are optional (only used for Argon2id) + Memory = KdfMemory, + Parallelism = KdfParallelism, + }, + MasterKeyWrappedUserKey = UserSymmetricKey ?? throw new Exception("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."), + // PM-28827 To be added when MasterPasswordSalt is added to the user column + Salt = Email.ToLower().Trim(), + }, + UserAccountKeysData = AccountKeys?.ToAccountKeysData() ?? + UserAsymmetricKeys?.AccountKeys.ToAccountKeysData() ?? + new UserAccountKeysData + { + PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData + ( + UserAsymmetricKeys?.EncryptedPrivateKey ?? + throw new Exception("WrappedPrivateKey couldn't be found in either AccountKeys or UserAsymmetricKeys."), + UserAsymmetricKeys?.PublicKey ?? + throw new Exception("PublicKey couldn't be found in either AccountKeys or UserAsymmetricKeys") + ), + }, + MasterPasswordAuthenticationData = MasterPasswordAuthentication?.ToData() ?? + new MasterPasswordAuthenticationData + { + Kdf = new KdfSettings + { + KdfType = Kdf ?? throw new Exception("KdfType couldn't be found on either the MasterPasswordUnlockData or the Kdf property passed in."), + Iterations = KdfIterations ?? throw new Exception("KdfIterations couldn't be found on either the MasterPasswordUnlockData or the KdfIterations property passed in."), + // KdfMemory and KdfParallelism are optional (only used for Argon2id) + Memory = KdfMemory, + Parallelism = KdfParallelism, + }, + MasterPasswordAuthenticationHash = MasterPasswordHash ?? throw new BadRequestException("MasterPasswordHash couldn't be found on either the MasterPasswordAuthenticationData or the MasterPasswordHash property passed in."), + Salt = Email.ToLower().Trim(), + } + }; + } + public RegisterFinishTokenType GetTokenType() { if (!string.IsNullOrWhiteSpace(EmailVerificationToken)) diff --git a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs index 97c2eabd3c8a..9f8ad616da1a 100644 --- a/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data; using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.UserFeatures.Registration; @@ -31,11 +32,11 @@ public interface IRegisterUserCommand /// If the organization has a 2FA required policy enabled, email verification will be enabled for the user. /// /// The to create - /// The hashed master password the user entered + /// Cryptographic data for finishing user registration /// The org invite token sent to the user via email /// The associated org user guid that was created at the time of invite /// - public Task RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash, string orgInviteToken, Guid? orgUserId); + public Task RegisterUserViaOrganizationInviteToken(User user, RegisterFinishData registerFinishData, string orgInviteToken, Guid? orgUserId); /// /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event. @@ -43,10 +44,10 @@ public interface IRegisterUserCommand /// An error will be thrown if the token is invalid or expired. /// /// The to create - /// The hashed master password the user entered + /// Cryptographic data for finishing user registration /// The email verification token sent to the user via email /// - public Task RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, string emailVerificationToken); + public Task RegisterUserViaEmailVerificationToken(User user, RegisterFinishData registerFinishData, string emailVerificationToken); /// /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event. @@ -54,10 +55,10 @@ public interface IRegisterUserCommand /// If the token is invalid or expired, an error will be thrown. /// /// The to create - /// The hashed master password the user entered + /// Cryptographic data for finishing user registration /// The org sponsored free family plan invite token sent to the user via email /// - public Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken); + public Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, RegisterFinishData registerFinishData, string orgSponsoredFreeFamilyPlanInviteToken); /// /// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event. @@ -65,11 +66,11 @@ public interface IRegisterUserCommand /// If the token is invalid or expired, an error will be thrown. /// /// The to create - /// The hashed master password the user entered + /// Cryptographic data for finishing user registration /// The emergency access invite token sent to the user via email /// The emergency access id (used to validate the token) /// - public Task RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash, + public Task RegisterUserViaAcceptEmergencyAccessInviteToken(User user, RegisterFinishData registerFinishData, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId); /// @@ -78,10 +79,10 @@ public Task RegisterUserViaAcceptEmergencyAccessInviteToken(User /// If the token is invalid or expired, an error will be thrown. /// /// The to create - /// The hashed master password the user entered + /// Cryptographic data for finishing user registration /// The provider invite token sent to the user via email /// The provider user id which is used to validate the invite token /// - public Task RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId); + public Task RegisterUserViaProviderInviteToken(User user, RegisterFinishData registerFinishData, string providerInviteToken, Guid providerUserId); } diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 4a0e9c2cf521..86348e230cf8 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -111,7 +112,7 @@ public async Task RegisterSSOAutoProvisionedUserAsync(User user, return result; } - public async Task RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash, + public async Task RegisterUserViaOrganizationInviteToken(User user, RegisterFinishData registerFinishData, string orgInviteToken, Guid? orgUserId) { TryValidateOrgInviteToken(orgInviteToken, orgUserId, user); @@ -129,7 +130,7 @@ public async Task RegisterUserViaOrganizationInviteToken(User us user.EmailVerified = true; } - var result = await _userService.CreateUserAsync(user, masterPasswordHash); + var result = await _userService.CreateV2UserAsync(user, registerFinishData); var organization = await GetOrganizationUserOrganization(orgUserId ?? Guid.Empty, orgUser); if (result == IdentityResult.Success) { @@ -280,7 +281,7 @@ private async Task SendAppropriateWelcomeEmailAsync(User user, string initiation } } - public async Task RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, + public async Task RegisterUserViaEmailVerificationToken(User user, RegisterFinishData registerFinishData, string emailVerificationToken) { ValidateOpenRegistrationAllowed(); @@ -292,7 +293,7 @@ public async Task RegisterUserViaEmailVerificationToken(User use user.Name = tokenable.Name; user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null. - var result = await _userService.CreateUserAsync(user, masterPasswordHash); + var result = await _userService.CreateV2UserAsync(user, registerFinishData); if (result == IdentityResult.Success) { await SendWelcomeEmailAsync(user); @@ -301,7 +302,7 @@ public async Task RegisterUserViaEmailVerificationToken(User use return result; } - public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash, + public async Task RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, RegisterFinishData registerFinishData, string orgSponsoredFreeFamilyPlanInviteToken) { ValidateOpenRegistrationAllowed(); @@ -311,7 +312,7 @@ public async Task RegisterUserViaOrganizationSponsoredFreeFamily user.EmailVerified = true; user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null. - var result = await _userService.CreateUserAsync(user, masterPasswordHash); + var result = await _userService.CreateV2UserAsync(user, registerFinishData); if (result == IdentityResult.Success) { await SendWelcomeEmailAsync(user); @@ -322,7 +323,7 @@ public async Task RegisterUserViaOrganizationSponsoredFreeFamily // TODO: in future, consider how we can consolidate base registration logic to reduce code duplication - public async Task RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash, + public async Task RegisterUserViaAcceptEmergencyAccessInviteToken(User user, RegisterFinishData registerFinishData, string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId) { ValidateOpenRegistrationAllowed(); @@ -332,7 +333,7 @@ public async Task RegisterUserViaAcceptEmergencyAccessInviteToke user.EmailVerified = true; user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null. - var result = await _userService.CreateUserAsync(user, masterPasswordHash); + var result = await _userService.CreateV2UserAsync(user, registerFinishData); if (result == IdentityResult.Success) { await SendWelcomeEmailAsync(user); @@ -341,7 +342,7 @@ public async Task RegisterUserViaAcceptEmergencyAccessInviteToke return result; } - public async Task RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, + public async Task RegisterUserViaProviderInviteToken(User user, RegisterFinishData registerFinishData, string providerInviteToken, Guid providerUserId) { ValidateOpenRegistrationAllowed(); @@ -351,7 +352,7 @@ public async Task RegisterUserViaProviderInviteToken(User user, user.EmailVerified = true; user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null. - var result = await _userService.CreateUserAsync(user, masterPasswordHash); + var result = await _userService.CreateV2UserAsync(user, registerFinishData); if (result == IdentityResult.Success) { await SendWelcomeEmailAsync(user); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 61c8d7931f61..d17311e935e6 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -213,6 +213,7 @@ public static class FeatureFlagKeys public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit"; public const string DataRecoveryTool = "pm-28813-data-recovery-tool"; public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration"; + public const string EnableAccountEncryptionV2PasswordRegistration = "pm-27278-v2-password-registration"; /* Mobile Team */ public const string AndroidImportLoginsFlow = "import-logins-flow"; diff --git a/src/Core/KeyManagement/Models/Data/RegisterFinishData.cs b/src/Core/KeyManagement/Models/Data/RegisterFinishData.cs new file mode 100644 index 000000000000..edda1f6ed160 --- /dev/null +++ b/src/Core/KeyManagement/Models/Data/RegisterFinishData.cs @@ -0,0 +1,13 @@ +namespace Bit.Core.KeyManagement.Models.Data; + +public class RegisterFinishData +{ + public required MasterPasswordUnlockData MasterPasswordUnlockData { get; set; } + public required UserAccountKeysData UserAccountKeysData { get; set; } + public required MasterPasswordAuthenticationData MasterPasswordAuthenticationData {get; set; } + + public bool IsV2Encryption() + { + return UserAccountKeysData.IsV2Encryption(); + } +} \ No newline at end of file diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 93316d78bdab..883482eb81da 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -74,6 +74,7 @@ Task SetV2AccountCryptographicStateAsync( Task DeleteManyAsync(IEnumerable users); UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey); + UpdateUserData SetRegisterFinishUserData(Guid userId, RegisterFinishData registerFinishData); } public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null, diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index a531883db114..d0ee519e05bb 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -7,6 +7,7 @@ using Bit.Core.Billing.Models.Business; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Business; using Fido2NetLib; using Microsoft.AspNetCore.Identity; @@ -23,6 +24,7 @@ public interface IUserService Task SaveUserAsync(User user, bool push = false); Task CreateUserAsync(User user); Task CreateUserAsync(User user, string masterPasswordHash); + Task CreateV2UserAsync(User user, RegisterFinishData registerFinishData); Task SendMasterPasswordHintAsync(string email); Task StartWebAuthnRegistrationAsync(User user); Task DeleteWebAuthnKeyAsync(User user, int id); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 498721238bf7..7428cc6fe2bf 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -25,6 +25,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.KeyManagement.Models.Data; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; @@ -326,6 +327,24 @@ public async Task CreateUserAsync(User user, string masterPasswo return await CreateAsync(user, masterPasswordHash); } + public async Task CreateV2UserAsync(User user, RegisterFinishData registerFinishData) + { + // TODO remove logic below after a compatibility period - once V2 accounts are fully supported + // https://bitwarden.atlassian.net/browse/PM-TBD + if (!registerFinishData.IsV2Encryption()) + { + return await CreateUserAsync(user, registerFinishData.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash); + } + + var result = await CreateAsync(user, registerFinishData.MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash); + if (result.Succeeded) + { + var setRegisterFinishUserDataTask = _userRepository.SetRegisterFinishUserData(user.Id, registerFinishData); + await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, registerFinishData.UserAccountKeysData, [setRegisterFinishUserDataTask]); + } + return result; + } + public async Task SendMasterPasswordHintAsync(string email) { var user = await _userRepository.GetByEmailAsync(email); diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index 2e42b690dd07..b8f56996ed75 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -141,28 +141,37 @@ public async Task PostRegisterVerificationEmailClicked([FromBody] [HttpPost("register/finish")] public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) { - User user = model.ToUser(); + User user; + + var registerFinishData = model.ToData(); + + // TODO simplify logic below after a compatibility period - once V2 accounts are supported + // https://bitwarden.atlassian.net/browse/PM-TBD + if (registerFinishData.IsV2Encryption()) + { + user = model.ToV2User(); + } + else + { + user = model.ToUser(); + } // Users will either have an emailed token or an email verification token - not both. IdentityResult? identityResult = null; - // PM-28143 - Just use the MasterPasswordAuthenticationData.MasterPasswordAuthenticationHash - string masterPasswordHash = model.MasterPasswordAuthentication?.MasterPasswordAuthenticationHash - ?? model.MasterPasswordHash ?? throw new BadRequestException("MasterPasswordHash couldn't be found on either the MasterPasswordAuthenticationData or the MasterPasswordHash property passed in."); - switch (model.GetTokenType()) { case RegisterFinishTokenType.EmailVerification: identityResult = await _registerUserCommand.RegisterUserViaEmailVerificationToken( user, - masterPasswordHash, + registerFinishData, model.EmailVerificationToken!); return ProcessRegistrationResult(identityResult, user); case RegisterFinishTokenType.OrganizationInvite: identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken( user, - masterPasswordHash, + registerFinishData, model.OrgInviteToken!, model.OrganizationUserId); return ProcessRegistrationResult(identityResult, user); @@ -170,14 +179,14 @@ public async Task PostRegisterFinish([FromBody] Reg case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan: identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken( user, - masterPasswordHash, + registerFinishData, model.OrgSponsoredFreeFamilyPlanToken!); return ProcessRegistrationResult(identityResult, user); case RegisterFinishTokenType.EmergencyAccessInvite: identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken( user, - masterPasswordHash, + registerFinishData, model.AcceptEmergencyAccessInviteToken!, (Guid)model.AcceptEmergencyAccessId!); return ProcessRegistrationResult(identityResult, user); @@ -185,7 +194,7 @@ public async Task PostRegisterFinish([FromBody] Reg case RegisterFinishTokenType.ProviderInvite: identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken( user, - masterPasswordHash, + registerFinishData, model.ProviderInviteToken!, (Guid)model.ProviderUserId!); return ProcessRegistrationResult(identityResult, user); diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 571319e4c7ba..3b11a800b4fe 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -428,6 +428,32 @@ public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWra }; } + public UpdateUserData SetRegisterFinishUserData(Guid userId, RegisterFinishData registerFinishData) + { + return async (connection, transaction) => + { + var timestamp = DateTime.UtcNow; + + await connection!.ExecuteAsync( + "[dbo].[User_SetRegisterFinishUserData]", + new + { + Id = userId, + Kdf = registerFinishData.MasterPasswordUnlockData.Kdf.KdfType, + KdfIterations = registerFinishData.MasterPasswordUnlockData.Kdf.Iterations, + KdfMemory = registerFinishData.MasterPasswordUnlockData.Kdf.Memory, + KdfParallelism = registerFinishData.MasterPasswordUnlockData.Kdf.Parallelism, + Key = registerFinishData.MasterPasswordUnlockData.MasterKeyWrappedUserKey, + PublicKey = registerFinishData.UserAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey, + PrivateKey = registerFinishData.UserAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey, + RevisionDate = timestamp, + AccountRevisionDate = timestamp, + }, + transaction: transaction, + commandType: CommandType.StoredProcedure); + }; + } + private async Task ProtectDataAndSaveAsync(User user, Func saveTask) { if (user == null) diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 56d64094d0db..7439efad0c31 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -510,6 +510,30 @@ public UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWra }; } + public UpdateUserData SetRegisterFinishUserData(Guid userId, RegisterFinishData registerFinishData) + { + return async (_, _) => + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var userEntity = await dbContext.Users.FindAsync(userId) ?? throw new ArgumentException("User not found", nameof(userId)); + var timestamp = DateTime.UtcNow; + + userEntity.Kdf = registerFinishData.MasterPasswordUnlockData.Kdf.KdfType; + userEntity.KdfIterations = registerFinishData.MasterPasswordUnlockData.Kdf.Iterations; + userEntity.KdfMemory = registerFinishData.MasterPasswordUnlockData.Kdf.Memory; + userEntity.KdfParallelism = registerFinishData.MasterPasswordUnlockData.Kdf.Parallelism; + userEntity.Key = registerFinishData.MasterPasswordUnlockData.MasterKeyWrappedUserKey; + userEntity.PublicKey = registerFinishData.UserAccountKeysData.PublicKeyEncryptionKeyPairData.PublicKey; + userEntity.PrivateKey = registerFinishData.UserAccountKeysData.PublicKeyEncryptionKeyPairData.WrappedPrivateKey; + userEntity.RevisionDate = timestamp; + userEntity.AccountRevisionDate = timestamp; + + await dbContext.SaveChangesAsync(); + }; + } + private static void MigrateDefaultUserCollectionsToShared(DatabaseContext dbContext, IEnumerable userIds) { var defaultCollections = (from c in dbContext.Collections diff --git a/src/Sql/dbo/KeyManagement/Stored Procedures/User_SetRegisterFinishUserData.sql b/src/Sql/dbo/KeyManagement/Stored Procedures/User_SetRegisterFinishUserData.sql new file mode 100644 index 000000000000..648e9a66bbf0 --- /dev/null +++ b/src/Sql/dbo/KeyManagement/Stored Procedures/User_SetRegisterFinishUserData.sql @@ -0,0 +1,30 @@ +CREATE PROCEDURE [dbo].[User_SetRegisterFinishUserData] + @Id UNIQUEIDENTIFIER, + @Kdf TINYINT, + @KdfIterations INT, + @KdfMemory INT, + @KdfParallelism INT, + @Key VARCHAR(MAX), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @RevisionDate DATETIME2(7), + @AccountRevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[User] + SET + [Key] = @Key, + [Kdf] = @Kdf, + [KdfIterations] = @KdfIterations, + [KdfMemory] = @KdfMemory, + [KdfParallelism] = @KdfParallelism, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [RevisionDate] = @RevisionDate, + [AccountRevisionDate] = @AccountRevisionDate + WHERE + [Id] = @Id +END