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