Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Api.Request;
#nullable enable

using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;

namespace Bit.Api.Auth.Models.Request.Accounts;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;

public class KdfRequestModel : IValidatableObject
public class KdfRequestModel
{
[Required]
public required KdfType KdfType { get; init; }
Expand All @@ -24,10 +23,4 @@ public KdfSettings ToData()
Parallelism = Parallelism
};
}

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// Generic per-request KDF validation for any request model embedding KdfRequestModel
return KdfSettingsValidator.Validate(ToData());
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;

/// <summary>
/// Use this datatype when interfacing with requests to create a separation of concern.
/// See <see cref="MasterPasswordAuthenticationData"/> to use for commands, queries, services.
/// </summary>
public class MasterPasswordAuthenticationDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,8 @@
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

namespace Bit.Core.KeyManagement.Models.Api.Request;
namespace Bit.Api.KeyManagement.Models.Requests;

/// <summary>
/// Use this datatype when interfacing with requests to create a separation of concern.
/// See <see cref="MasterPasswordUnlockData"/> to use for commands, queries, services.
/// </summary>
public class MasterPasswordUnlockDataRequestModel
{
public required KdfRequestModel Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Bit.Core.Entities;
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Core.Auth.Models.Api.Request.Accounts;
Expand All @@ -21,32 +21,19 @@ public class RegisterFinishRequestModel : IValidatableObject
public required string Email { get; set; }
public string? EmailVerificationToken { get; set; }

public MasterPasswordAuthenticationDataRequestModel? MasterPasswordAuthentication { get; set; }
public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlock { get; set; }

// PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)
[StringLength(1000)]
// Made optional but there will still be a thrown error if it does not exist either here or
// in the MasterPasswordAuthenticationData.
public string? MasterPasswordHash { get; set; }
public required string MasterPasswordHash { get; set; }

[StringLength(50)]
public string? MasterPasswordHint { get; set; }

// PM-28143 - Remove property below (made optional during migration to MasterPasswordUnlockData)
// Made optional but there will still be a thrown error if it does not exist either here or
// in the MasterPasswordAuthenticationData.
public string? UserSymmetricKey { get; set; }
public required string UserSymmetricKey { get; set; }

public required KeysRequestModel UserAsymmetricKeys { get; set; }

// PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)
public KdfType? Kdf { get; set; }
// PM-28143 - Remove line below (made optional during migration to MasterPasswordUnlockData)
public int? KdfIterations { get; set; }
// PM-28143 - Remove line below
public required KdfType Kdf { get; set; }
public required int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
// PM-28143 - Remove line below
public int? KdfParallelism { get; set; }

public Guid? OrganizationUserId { get; set; }
Expand All @@ -67,14 +54,11 @@ public User ToUser()
{
Email = Email,
MasterPasswordHint = MasterPasswordHint,
Kdf = (KdfType)(MasterPasswordUnlock?.Kdf.KdfType ?? Kdf)!,
KdfIterations = (int)(MasterPasswordUnlock?.Kdf.Iterations ?? KdfIterations)!,
// KdfMemory and KdfParallelism are optional (only used for Argon2id)
KdfMemory = MasterPasswordUnlock?.Kdf.Memory ?? KdfMemory,
KdfParallelism = MasterPasswordUnlock?.Kdf.Parallelism ?? KdfParallelism,
// PM-28827 To be added when MasterPasswordSalt is added to the user column
// MasterPasswordSalt = MasterPasswordUnlock?.Salt ?? Email.ToLower().Trim(),
Key = MasterPasswordUnlock?.MasterKeyWrappedUserKey ?? UserSymmetricKey
Kdf = Kdf,
KdfIterations = KdfIterations,
KdfMemory = KdfMemory,
KdfParallelism = KdfParallelism,
Key = UserSymmetricKey,
};

UserAsymmetricKeys.ToUser(user);
Expand All @@ -88,182 +72,29 @@ public RegisterFinishTokenType GetTokenType()
{
return RegisterFinishTokenType.EmailVerification;
}
if (!string.IsNullOrEmpty(OrgInviteToken)
&& OrganizationUserId.HasValue
&& OrganizationUserId.Value != Guid.Empty)
if (!string.IsNullOrEmpty(OrgInviteToken) && OrganizationUserId.HasValue)
{
return RegisterFinishTokenType.OrganizationInvite;
}
if (!string.IsNullOrWhiteSpace(OrgSponsoredFreeFamilyPlanToken))
{
return RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan;
}
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken)
&& AcceptEmergencyAccessId.HasValue
&& AcceptEmergencyAccessId.Value != Guid.Empty)
if (!string.IsNullOrWhiteSpace(AcceptEmergencyAccessInviteToken) && AcceptEmergencyAccessId.HasValue)
{
return RegisterFinishTokenType.EmergencyAccessInvite;
}
if (!string.IsNullOrWhiteSpace(ProviderInviteToken)
&& ProviderUserId.HasValue
&& ProviderUserId.Value != Guid.Empty)
if (!string.IsNullOrWhiteSpace(ProviderInviteToken) && ProviderUserId.HasValue)
{
return RegisterFinishTokenType.ProviderInvite;
}

throw new InvalidOperationException("Invalid token type.");
}


public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
// 1. Authentication data containing hash and hash at root level check
if (MasterPasswordAuthentication != null && MasterPasswordHash != null)
{
if (MasterPasswordAuthentication.MasterPasswordAuthenticationHash != MasterPasswordHash)
{
yield return new ValidationResult(
$"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and root level {nameof(MasterPasswordHash)} provided and are not equal. Only provide one.",
[nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);
}
} // 1.5 if there is no master password hash that is unacceptable even though they are both optional in the model
else if (MasterPasswordAuthentication == null && MasterPasswordHash == null)
{
yield return new ValidationResult(
$"{nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash)} and {nameof(MasterPasswordHash)} not found on request, one needs to be defined.",
[nameof(MasterPasswordAuthentication.MasterPasswordAuthenticationHash), nameof(MasterPasswordHash)]);
}

// 2. Validate kdf settings.
if (MasterPasswordUnlock != null)
{
foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordUnlock.ToData().Kdf))
{
yield return validationResult;
}
}

if (MasterPasswordAuthentication != null)
{
foreach (var validationResult in KdfSettingsValidator.Validate(MasterPasswordAuthentication.ToData().Kdf))
{
yield return validationResult;
}
}

// 3. Validate root kdf values if kdf values are not in the unlock and authentication.
if (MasterPasswordUnlock == null && MasterPasswordAuthentication == null)
{
var hasMissingRequiredKdfInputs = false;
if (Kdf == null)
{
yield return new ValidationResult($"{nameof(Kdf)} not found on RequestModel", [nameof(Kdf)]);
hasMissingRequiredKdfInputs = true;
}
if (KdfIterations == null)
{
yield return new ValidationResult($"{nameof(KdfIterations)} not found on RequestModel", [nameof(KdfIterations)]);
hasMissingRequiredKdfInputs = true;
}

if (!hasMissingRequiredKdfInputs)
{
foreach (var validationResult in KdfSettingsValidator.Validate(
Kdf!.Value,
KdfIterations!.Value,
KdfMemory,
KdfParallelism))
{
yield return validationResult;
}
}
}
else if (MasterPasswordUnlock == null && MasterPasswordAuthentication != null)
{
// Authentication provided but Unlock missing
yield return new ValidationResult($"{nameof(MasterPasswordUnlock)} not found on RequestModel", [nameof(MasterPasswordUnlock)]);
}
else if (MasterPasswordUnlock != null && MasterPasswordAuthentication == null)
{
// Unlock provided but Authentication missing
yield return new ValidationResult($"{nameof(MasterPasswordAuthentication)} not found on RequestModel", [nameof(MasterPasswordAuthentication)]);
}

// 3. Lastly, validate access token type and presence. Must be done last because of yield break.
RegisterFinishTokenType tokenType;
var tokenTypeResolved = true;
try
{
tokenType = GetTokenType();
}
catch (InvalidOperationException)
{
tokenTypeResolved = false;
tokenType = default;
}

if (!tokenTypeResolved)
{
yield return new ValidationResult("No valid registration token provided");
yield break;
}

switch (tokenType)
{
case RegisterFinishTokenType.EmailVerification:
if (string.IsNullOrEmpty(EmailVerificationToken))
{
yield return new ValidationResult(
$"{nameof(EmailVerificationToken)} absent when processing register/finish.",
[nameof(EmailVerificationToken)]);
}
break;
case RegisterFinishTokenType.OrganizationInvite:
if (string.IsNullOrEmpty(OrgInviteToken))
{
yield return new ValidationResult(
$"{nameof(OrgInviteToken)} absent when processing register/finish.",
[nameof(OrgInviteToken)]);
}
break;
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
if (string.IsNullOrEmpty(OrgSponsoredFreeFamilyPlanToken))
{
yield return new ValidationResult(
$"{nameof(OrgSponsoredFreeFamilyPlanToken)} absent when processing register/finish.",
[nameof(OrgSponsoredFreeFamilyPlanToken)]);
}
break;
case RegisterFinishTokenType.EmergencyAccessInvite:
if (string.IsNullOrEmpty(AcceptEmergencyAccessInviteToken))
{
yield return new ValidationResult(
$"{nameof(AcceptEmergencyAccessInviteToken)} absent when processing register/finish.",
[nameof(AcceptEmergencyAccessInviteToken)]);
}
if (!AcceptEmergencyAccessId.HasValue || AcceptEmergencyAccessId.Value == Guid.Empty)
{
yield return new ValidationResult(
$"{nameof(AcceptEmergencyAccessId)} absent when processing register/finish.",
[nameof(AcceptEmergencyAccessId)]);
}
break;
case RegisterFinishTokenType.ProviderInvite:
if (string.IsNullOrEmpty(ProviderInviteToken))
{
yield return new ValidationResult(
$"{nameof(ProviderInviteToken)} absent when processing register/finish.",
[nameof(ProviderInviteToken)]);
}
if (!ProviderUserId.HasValue || ProviderUserId.Value == Guid.Empty)
{
yield return new ValidationResult(
$"{nameof(ProviderUserId)} absent when processing register/finish.",
[nameof(ProviderUserId)]);
}
break;
default:
yield return new ValidationResult("Invalid registration finish request");
break;
}
return KdfSettingsValidator.Validate(Kdf, KdfIterations, KdfMemory, KdfParallelism);
}
}
6 changes: 3 additions & 3 deletions src/Core/Entities/User.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;

#nullable enable

namespace Bit.Core.Entities;

public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFactorProvidersUser
Expand Down Expand Up @@ -49,7 +51,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public string? Key { get; set; }
/// <summary>
/// The raw public key, without a signature from the user's signature key.
/// </summary>
/// </summary>
public string? PublicKey { get; set; }
/// <summary>
/// User key wrapped private key.
Expand Down Expand Up @@ -105,8 +107,6 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
// PM-28827 Uncomment below line.
// public string? MasterPasswordSalt { get; set; }

public string GetMasterPasswordSalt()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.
/// See <see cref="MasterPasswordAuthenticationDataRequestModel"/> to use for requests.
/// </summary>
public class MasterPasswordAuthenticationData
{
public required KdfSettings Kdf { get; init; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Bit.Core.Entities;
#nullable enable
using Bit.Core.Entities;
using Bit.Core.Enums;

namespace Bit.Core.KeyManagement.Models.Data;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Api.Request;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Use this datatype when interfacing with commands, queries, services to create a separation of concern.
/// See <see cref="MasterPasswordUnlockDataRequestModel"/> to use for requests.
/// </summary>
public class MasterPasswordUnlockData
{
public required KdfSettings Kdf { get; init; }
Expand Down
1 change: 0 additions & 1 deletion src/Core/Utilities/KdfSettingsValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ namespace Bit.Core.Utilities;

public static class KdfSettingsValidator
{
// PM-28143 - Remove below when fixing ticket
public static IEnumerable<ValidationResult> Validate(KdfType kdfType, int kdfIterations, int? kdfMemory, int? kdfParallelism)
{
switch (kdfType)
Expand Down
Loading
Loading