Skip to content

Commit a06b900

Browse files
committed
[PM-27278] add AccountKeysRequestModel to RegisterFinishRequestModel for account encryption v2 support
1 parent a5890d2 commit a06b900

File tree

12 files changed

+227
-33
lines changed

12 files changed

+227
-33
lines changed

src/Core/Auth/Models/Api/Request/Accounts/RegisterFinishRequestModel.cs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using Bit.Core.Entities;
22
using Bit.Core.Enums;
33
using Bit.Core.Exceptions;
4-
using Bit.Core.KeyManagement.Models.Api.Request;
4+
using Bit.Core.KeyManagement.Models.Data;
55
using Bit.Core.Utilities;
66

77
namespace Bit.Core.Auth.Models.Api.Request.Accounts;
88
using System.ComponentModel.DataAnnotations;
9+
using Bit.Core.KeyManagement.Models.Api.Request;
910

1011
public enum RegisterFinishTokenType : byte
1112
{
@@ -39,7 +40,12 @@ public class RegisterFinishRequestModel : IValidatableObject
3940
// in the MasterPasswordAuthenticationData.
4041
public string? UserSymmetricKey { get; set; }
4142

42-
public required KeysRequestModel UserAsymmetricKeys { get; set; }
43+
// TODO Remove property below, deprecated due to new AccountKeys property
44+
// https://bitwarden.atlassian.net/browse/PM-TBD
45+
// Will throw error if both UserAsymmetricKeys and AccountKeys do not exist.
46+
public KeysRequestModel? UserAsymmetricKeys { get; set; }
47+
48+
public AccountKeysRequestModel? AccountKeys { get; set; }
4349

4450
// PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)
4551
public KdfType? Kdf { get; set; }
@@ -62,6 +68,8 @@ public class RegisterFinishRequestModel : IValidatableObject
6268

6369
public Guid? ProviderUserId { get; set; }
6470

71+
// TODO remove with https://bitwarden.atlassian.net/browse/PM-TBD
72+
[Obsolete("Use ToV2User instead")]
6573
public User ToUser()
6674
{
6775
var user = new User
@@ -80,11 +88,57 @@ public User ToUser()
8088
Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey ?? throw new BadRequestException("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."),
8189
};
8290

83-
UserAsymmetricKeys.ToUser(user);
91+
user = UserAsymmetricKeys?.ToUser(user) ?? throw new Exception("User's public and private account keys couldn't be found in either AccountKeys or UserAsymmetricKeys");
8492

8593
return user;
8694
}
8795

96+
public User ToV2User()
97+
{
98+
return new User
99+
{
100+
Email = Email,
101+
MasterPasswordHint = MasterPasswordHint,
102+
};
103+
}
104+
105+
public RegisterFinishData ToData(string masterPasswordAuthenticationHash)
106+
{
107+
// TODO clean up flow once old fields are deprecated
108+
// https://bitwarden.atlassian.net/browse/PM-TBD
109+
return new RegisterFinishData
110+
{
111+
MasterPasswordUnlockData = MasterPasswordUnlock?.ToData() ??
112+
new MasterPasswordUnlockData
113+
{
114+
Kdf = new KdfSettings
115+
{
116+
KdfType = Kdf ?? throw new Exception("KdfType couldn't be found on either the MasterPasswordUnlockData or the Kdf property passed in."),
117+
Iterations = KdfIterations ?? throw new Exception("KdfIterations couldn't be found on either the MasterPasswordUnlockData or the KdfIterations property passed in."),
118+
// KdfMemory and KdfParallelism are optional (only used for Argon2id)
119+
Memory = KdfMemory,
120+
Parallelism = KdfParallelism,
121+
},
122+
MasterKeyWrappedUserKey = UserSymmetricKey ?? throw new Exception("MasterKeyWrappedUserKey couldn't be found on either the MasterPasswordUnlockData or the UserSymmetricKey property passed in."),
123+
// PM-28827 To be added when MasterPasswordSalt is added to the user column
124+
Salt = Email.ToLower().Trim(),
125+
},
126+
UserAccountKeysData = AccountKeys?.ToAccountKeysData() ??
127+
UserAsymmetricKeys?.AccountKeys.ToAccountKeysData() ??
128+
new UserAccountKeysData
129+
{
130+
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData
131+
(
132+
UserAsymmetricKeys?.EncryptedPrivateKey ??
133+
throw new Exception("WrappedPrivateKey couldn't be found in either AccountKeys or UserAsymmetricKeys."),
134+
UserAsymmetricKeys?.PublicKey ??
135+
throw new Exception("PublicKey couldn't be found in either AccountKeys or UserAsymmetricKeys")
136+
),
137+
},
138+
MasterPasswordAuthenticationHash = masterPasswordAuthenticationHash,
139+
};
140+
}
141+
88142
public RegisterFinishTokenType GetTokenType()
89143
{
90144
if (!string.IsNullOrWhiteSpace(EmailVerificationToken))

src/Core/Auth/UserFeatures/Registration/IRegisterUserCommand.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Bit.Core.AdminConsole.Entities;
22
using Bit.Core.Entities;
3+
using Bit.Core.KeyManagement.Models.Data;
34
using Microsoft.AspNetCore.Identity;
45

56
namespace Bit.Core.Auth.UserFeatures.Registration;
@@ -31,45 +32,45 @@ public interface IRegisterUserCommand
3132
/// If the organization has a 2FA required policy enabled, email verification will be enabled for the user.
3233
/// </summary>
3334
/// <param name="user">The <see cref="User"/> to create</param>
34-
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
35+
/// <param name="registerFinishData">Cryptographic data for finishing user registration</param>
3536
/// <param name="orgInviteToken">The org invite token sent to the user via email</param>
3637
/// <param name="orgUserId">The associated org user guid that was created at the time of invite</param>
3738
/// <returns><see cref="IdentityResult"/></returns>
38-
public Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash, string orgInviteToken, Guid? orgUserId);
39+
public Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, RegisterFinishData registerFinishData, string orgInviteToken, Guid? orgUserId);
3940

4041
/// <summary>
4142
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
4243
/// If a valid email verification token is provided, the user will be created with their email verified.
4344
/// An error will be thrown if the token is invalid or expired.
4445
/// </summary>
4546
/// <param name="user">The <see cref="User"/> to create</param>
46-
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
47+
/// <param name="registerFinishData">Cryptographic data for finishing user registration</param>
4748
/// <param name="emailVerificationToken">The email verification token sent to the user via email</param>
4849
/// <returns><see cref="IdentityResult"/></returns>
49-
public Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash, string emailVerificationToken);
50+
public Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, RegisterFinishData registerFinishData, string emailVerificationToken);
5051

5152
/// <summary>
5253
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
5354
/// If a valid org sponsored free family plan invite token is provided, the user will be created with their email verified.
5455
/// If the token is invalid or expired, an error will be thrown.
5556
/// </summary>
5657
/// <param name="user">The <see cref="User"/> to create</param>
57-
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
58+
/// <param name="registerFinishData">Cryptographic data for finishing user registration</param>
5859
/// <param name="orgSponsoredFreeFamilyPlanInviteToken">The org sponsored free family plan invite token sent to the user via email</param>
5960
/// <returns><see cref="IdentityResult"/></returns>
60-
public Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash, string orgSponsoredFreeFamilyPlanInviteToken);
61+
public Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, RegisterFinishData registerFinishData, string orgSponsoredFreeFamilyPlanInviteToken);
6162

6263
/// <summary>
6364
/// Creates a new user with a given master password hash, sends a welcome email, and raises the signup reference event.
6465
/// If a valid token is provided, the user will be created with their email verified.
6566
/// If the token is invalid or expired, an error will be thrown.
6667
/// </summary>
6768
/// <param name="user">The <see cref="User"/> to create</param>
68-
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
69+
/// <param name="registerFinishData">Cryptographic data for finishing user registration</param>
6970
/// <param name="acceptEmergencyAccessInviteToken">The emergency access invite token sent to the user via email</param>
7071
/// <param name="acceptEmergencyAccessId">The emergency access id (used to validate the token)</param>
7172
/// <returns><see cref="IdentityResult"/></returns>
72-
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
73+
public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, RegisterFinishData registerFinishData,
7374
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId);
7475

7576
/// <summary>
@@ -78,10 +79,10 @@ public Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User
7879
/// If the token is invalid or expired, an error will be thrown.
7980
/// </summary>
8081
/// <param name="user">The <see cref="User"/> to create</param>
81-
/// <param name="masterPasswordHash">The hashed master password the user entered</param>
82+
/// <param name="registerFinishData">Cryptographic data for finishing user registration</param>
8283
/// <param name="providerInviteToken">The provider invite token sent to the user via email</param>
8384
/// <param name="providerUserId">The provider user id which is used to validate the invite token</param>
8485
/// <returns><see cref="IdentityResult"/></returns>
85-
public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash, string providerInviteToken, Guid providerUserId);
86+
public Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, RegisterFinishData registerFinishData, string providerInviteToken, Guid providerUserId);
8687

8788
}

src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using Bit.Core.Billing.Extensions;
99
using Bit.Core.Entities;
1010
using Bit.Core.Exceptions;
11+
using Bit.Core.KeyManagement.Models.Data;
1112
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
1213
using Bit.Core.Repositories;
1314
using Bit.Core.Services;
@@ -111,7 +112,7 @@ public async Task<IdentityResult> RegisterSSOAutoProvisionedUserAsync(User user,
111112
return result;
112113
}
113114

114-
public async Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, string masterPasswordHash,
115+
public async Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User user, RegisterFinishData registerFinishData,
115116
string orgInviteToken, Guid? orgUserId)
116117
{
117118
TryValidateOrgInviteToken(orgInviteToken, orgUserId, user);
@@ -129,7 +130,7 @@ public async Task<IdentityResult> RegisterUserViaOrganizationInviteToken(User us
129130
user.EmailVerified = true;
130131
}
131132

132-
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
133+
var result = await _userService.CreateV2UserAsync(user, registerFinishData);
133134
var organization = await GetOrganizationUserOrganization(orgUserId ?? Guid.Empty, orgUser);
134135
if (result == IdentityResult.Success)
135136
{
@@ -280,7 +281,7 @@ private async Task SendAppropriateWelcomeEmailAsync(User user, string initiation
280281
}
281282
}
282283

283-
public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, string masterPasswordHash,
284+
public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User user, RegisterFinishData registerFinishData,
284285
string emailVerificationToken)
285286
{
286287
ValidateOpenRegistrationAllowed();
@@ -292,7 +293,7 @@ public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User use
292293
user.Name = tokenable.Name;
293294
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
294295

295-
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
296+
var result = await _userService.CreateV2UserAsync(user, registerFinishData);
296297
if (result == IdentityResult.Success)
297298
{
298299
await SendWelcomeEmailAsync(user);
@@ -301,7 +302,7 @@ public async Task<IdentityResult> RegisterUserViaEmailVerificationToken(User use
301302
return result;
302303
}
303304

304-
public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, string masterPasswordHash,
305+
public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(User user, RegisterFinishData registerFinishData,
305306
string orgSponsoredFreeFamilyPlanInviteToken)
306307
{
307308
ValidateOpenRegistrationAllowed();
@@ -311,7 +312,7 @@ public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamily
311312
user.EmailVerified = true;
312313
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
313314

314-
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
315+
var result = await _userService.CreateV2UserAsync(user, registerFinishData);
315316
if (result == IdentityResult.Success)
316317
{
317318
await SendWelcomeEmailAsync(user);
@@ -322,7 +323,7 @@ public async Task<IdentityResult> RegisterUserViaOrganizationSponsoredFreeFamily
322323

323324

324325
// TODO: in future, consider how we can consolidate base registration logic to reduce code duplication
325-
public async Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, string masterPasswordHash,
326+
public async Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToken(User user, RegisterFinishData registerFinishData,
326327
string acceptEmergencyAccessInviteToken, Guid acceptEmergencyAccessId)
327328
{
328329
ValidateOpenRegistrationAllowed();
@@ -332,7 +333,7 @@ public async Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToke
332333
user.EmailVerified = true;
333334
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
334335

335-
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
336+
var result = await _userService.CreateV2UserAsync(user, registerFinishData);
336337
if (result == IdentityResult.Success)
337338
{
338339
await SendWelcomeEmailAsync(user);
@@ -341,7 +342,7 @@ public async Task<IdentityResult> RegisterUserViaAcceptEmergencyAccessInviteToke
341342
return result;
342343
}
343344

344-
public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, string masterPasswordHash,
345+
public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user, RegisterFinishData registerFinishData,
345346
string providerInviteToken, Guid providerUserId)
346347
{
347348
ValidateOpenRegistrationAllowed();
@@ -351,7 +352,7 @@ public async Task<IdentityResult> RegisterUserViaProviderInviteToken(User user,
351352
user.EmailVerified = true;
352353
user.ApiKey = CoreHelpers.SecureRandomString(30); // API key can't be null.
353354

354-
var result = await _userService.CreateUserAsync(user, masterPasswordHash);
355+
var result = await _userService.CreateV2UserAsync(user, registerFinishData);
355356
if (result == IdentityResult.Success)
356357
{
357358
await SendWelcomeEmailAsync(user);

src/Core/Constants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ public static class FeatureFlagKeys
212212
public const string V2RegistrationTDEJIT = "pm-27279-v2-registration-tde-jit";
213213
public const string DataRecoveryTool = "pm-28813-data-recovery-tool";
214214
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
215+
public const string EnableAccountEncryptionV2PasswordRegistration = "pm-27278-v2-password-registration";
215216

216217
/* Mobile Team */
217218
public const string AndroidImportLoginsFlow = "import-logins-flow";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Bit.Core.KeyManagement.Models.Data;
2+
3+
public class RegisterFinishData
4+
{
5+
public required MasterPasswordUnlockData MasterPasswordUnlockData { get; set; }
6+
public required UserAccountKeysData UserAccountKeysData { get; set; }
7+
public required string MasterPasswordAuthenticationHash { get; init; }
8+
9+
public bool IsV2Encryption()
10+
{
11+
return UserAccountKeysData.IsV2Encryption();
12+
}
13+
}

src/Core/Repositories/IUserRepository.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Task SetV2AccountCryptographicStateAsync(
7474
Task DeleteManyAsync(IEnumerable<User> users);
7575

7676
UpdateUserData SetKeyConnectorUserKey(Guid userId, string keyConnectorWrappedUserKey);
77+
UpdateUserData SetRegisterFinishUserData(Guid userId, RegisterFinishData registerFinishData);
7778
}
7879

7980
public delegate Task UpdateUserData(Microsoft.Data.SqlClient.SqlConnection? connection = null,

src/Core/Services/IUserService.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Bit.Core.Billing.Models.Business;
88
using Bit.Core.Entities;
99
using Bit.Core.Enums;
10+
using Bit.Core.KeyManagement.Models.Data;
1011
using Bit.Core.Models.Business;
1112
using Fido2NetLib;
1213
using Microsoft.AspNetCore.Identity;
@@ -23,6 +24,7 @@ public interface IUserService
2324
Task SaveUserAsync(User user, bool push = false);
2425
Task<IdentityResult> CreateUserAsync(User user);
2526
Task<IdentityResult> CreateUserAsync(User user, string masterPasswordHash);
27+
Task<IdentityResult> CreateV2UserAsync(User user, RegisterFinishData registerFinishData);
2628
Task SendMasterPasswordHintAsync(string email);
2729
Task<CredentialCreateOptions> StartWebAuthnRegistrationAsync(User user);
2830
Task<bool> DeleteWebAuthnKeyAsync(User user, int id);

src/Core/Services/Implementations/UserService.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
using Bit.Core.Entities;
2626
using Bit.Core.Enums;
2727
using Bit.Core.Exceptions;
28+
using Bit.Core.KeyManagement.Models.Data;
2829
using Bit.Core.Models.Business;
2930
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
3031
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
@@ -326,6 +327,27 @@ public async Task<IdentityResult> CreateUserAsync(User user, string masterPasswo
326327
return await CreateAsync(user, masterPasswordHash);
327328
}
328329

330+
public async Task<IdentityResult> CreateV2UserAsync(User user, RegisterFinishData registerFinishData)
331+
{
332+
// TODO remove logic below after a compatibility period - once V2 accounts are fully supported
333+
// https://bitwarden.atlassian.net/browse/PM-TBD
334+
if (!registerFinishData.IsV2Encryption())
335+
{
336+
return await CreateUserAsync(user, registerFinishData.MasterPasswordAuthenticationHash);
337+
}
338+
339+
var result = await CreateAsync(user, registerFinishData.MasterPasswordAuthenticationHash);
340+
if (result.Succeeded)
341+
{
342+
// call new task for setting remaining user data
343+
var setRegisterFinishUserDataTask = _userRepository.SetRegisterFinishUserData(user.Id, registerFinishData);
344+
// call task as part of v2 account state set call
345+
await _userRepository.SetV2AccountCryptographicStateAsync(user.Id, registerFinishData.UserAccountKeysData, [setRegisterFinishUserDataTask]);
346+
// if this call fails, we need to delete the user we created.
347+
}
348+
return result;
349+
}
350+
329351
public async Task SendMasterPasswordHintAsync(string email)
330352
{
331353
var user = await _userRepository.GetByEmailAsync(email);

0 commit comments

Comments
 (0)