Skip to content
Draft
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
Expand Up @@ -3,6 +3,8 @@
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.KeyManagement.Kdf.Implementations;
using Bit.Core.KeyManagement.MasterPassword;
using Bit.Core.KeyManagement.MasterPassword.Interfaces;
using Bit.Core.KeyManagement.Queries;
using Bit.Core.KeyManagement.Queries.Interfaces;
using Microsoft.AspNetCore.Authorization;
Expand Down Expand Up @@ -30,11 +32,15 @@ private static void AddKeyManagementCommands(this IServiceCollection services)
services.AddScoped<IRegenerateUserAsymmetricKeysCommand, RegenerateUserAsymmetricKeysCommand>();
services.AddScoped<IChangeKdfCommand, ChangeKdfCommand>();
services.AddScoped<ISetKeyConnectorKeyCommand, SetKeyConnectorKeyCommand>();
services.AddScoped<ISetInitialMasterPasswordCommand, SetInitialMasterPasswordCommand>();
services.AddScoped<IUpdateMasterPasswordCommand, UpdateMasterPasswordCommand>();
}

private static void AddKeyManagementQueries(this IServiceCollection services)
{
services.AddScoped<IUserAccountKeysQuery, UserAccountKeysQuery>();
services.AddScoped<IKeyConnectorConfirmationDetailsQuery, KeyConnectorConfirmationDetailsQuery>();
services.AddScoped<ISetInitialMasterPasswordQuery, SetInitialMasterPasswordQuery>();
services.AddScoped<IUpdateMasterPasswordQuery, UpdateMasterPasswordQuery>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.MasterPassword.Interfaces;

/// <summary>
/// Validates the provided data against the user, applies the initial master password state
/// to the user object in memory, and persists the changes to the database.
/// </summary>
/// <remarks>
/// Use <see cref="ISetInitialMasterPasswordQuery"/> for in-memory mutation only (no persistence).
/// </remarks>
public interface ISetInitialMasterPasswordCommand
{
/// <summary>
/// Validates <paramref name="data"/> against <paramref name="user"/>, mutates the user
/// with the initial master password state, and persists the result.
/// </summary>
/// <param name="user">The user to set the initial master password for.</param>
/// <param name="data">The initial master password data to validate and apply.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when the data is not valid for the user (see <see cref="SetInitialMasterPasswordData.ValidateForUser"/>).
/// </exception>
Task RunAsync(User user, SetInitialMasterPasswordData data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.MasterPassword.Interfaces;

/// <summary>
/// Validates the provided data against the user and applies the initial master password state
/// to the user object in memory. Does not persist to the database.
/// </summary>
/// <remarks>
/// Use <see cref="ISetInitialMasterPasswordCommand"/> to also persist the changes.
/// </remarks>
public interface ISetInitialMasterPasswordQuery
{
/// <summary>
/// Validates <paramref name="data"/> against <paramref name="user"/> and mutates the user
/// in memory with the initial master password state.
/// </summary>
/// <param name="user">The user to apply the initial master password to.</param>
/// <param name="data">The initial master password data to validate and apply.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when the data is not valid for the user (see <see cref="SetInitialMasterPasswordData.ValidateForUser"/>).
/// </exception>
Task RunAsync(User user, SetInitialMasterPasswordData data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.MasterPassword.Interfaces;

/// <summary>
/// Validates the provided data against the user, applies the updated master password state
/// to the user object in memory, and persists the changes to the database.
/// </summary>
/// <remarks>
/// KDF settings must remain unchanged. Use <see cref="Bit.Core.KeyManagement.Kdf.IChangeKdfCommand"/>
/// to change KDF settings. Use <see cref="IUpdateMasterPasswordQuery"/> for in-memory mutation only (no persistence).
/// </remarks>
public interface IUpdateMasterPasswordCommand
{
/// <summary>
/// Validates <paramref name="data"/> against <paramref name="user"/>, mutates the user
/// with the updated master password state, and persists the result.
/// </summary>
/// <param name="user">The user to update the master password for.</param>
/// <param name="data">The updated master password data to validate and apply.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when the data is not valid for the user (see <see cref="UpdateMasterPasswordData.ValidateForUser"/>).
/// </exception>
Task RunAsync(User user, UpdateMasterPasswordData data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.MasterPassword.Interfaces;

/// <summary>
/// Validates the provided data against the user and applies the updated master password state
/// to the user object in memory. Does not persist to the database.
/// </summary>
/// <remarks>
/// KDF settings must remain unchanged. Use <see cref="Bit.Core.KeyManagement.Kdf.IChangeKdfCommand"/>
/// to change KDF settings. Use <see cref="IUpdateMasterPasswordCommand"/> to also persist the changes.
/// </remarks>
public interface IUpdateMasterPasswordQuery
{
/// <summary>
/// Validates <paramref name="data"/> against <paramref name="user"/> and mutates the user
/// in memory with the updated master password state.
/// </summary>
/// <param name="user">The user to apply the updated master password to.</param>
/// <param name="data">The updated master password data to validate and apply.</param>
/// <exception cref="InvalidOperationException">
/// Thrown when the data is not valid for the user (see <see cref="UpdateMasterPasswordData.ValidateForUser"/>).
/// </exception>
Task RunAsync(User user, UpdateMasterPasswordData data);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.MasterPassword.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;

namespace Bit.Core.KeyManagement.MasterPassword;

/// <inheritdoc />
public class SetInitialMasterPasswordCommand : ISetInitialMasterPasswordCommand
{
private readonly ISetInitialMasterPasswordQuery _query;
private readonly IUserRepository _userRepository;

public SetInitialMasterPasswordCommand(ISetInitialMasterPasswordQuery query, IUserRepository userRepository)
{
_query = query;
_userRepository = userRepository;
}

/// <inheritdoc />
public async Task RunAsync(User user, SetInitialMasterPasswordData data)
{
await _query.RunAsync(user, data);
await _userRepository.ReplaceAsync(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.MasterPassword.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;

namespace Bit.Core.KeyManagement.MasterPassword;

/// <inheritdoc />
public class SetInitialMasterPasswordQuery : ISetInitialMasterPasswordQuery
{
private readonly IPasswordHasher<User> _passwordHasher;

public SetInitialMasterPasswordQuery(IPasswordHasher<User> passwordHasher)
{
_passwordHasher = passwordHasher;
}

/// <inheritdoc />
public Task RunAsync(User user, SetInitialMasterPasswordData data)
{
data.ValidateForUser(user);

user.MasterPassword = _passwordHasher.HashPassword(user,
data.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);
user.MasterPasswordHint = data.MasterPasswordHint;
user.MasterPasswordSalt = data.MasterPasswordAuthentication.Salt;
user.Key = data.MasterPasswordUnlock.MasterKeyWrappedUserKey;
user.Kdf = data.MasterPasswordAuthentication.Kdf.KdfType;
user.KdfIterations = data.MasterPasswordAuthentication.Kdf.Iterations;
user.KdfMemory = data.MasterPasswordAuthentication.Kdf.Memory;
user.KdfParallelism = data.MasterPasswordAuthentication.Kdf.Parallelism;

var now = DateTime.UtcNow;
user.RevisionDate = now;
user.AccountRevisionDate = now;
user.LastPasswordChangeDate = now;

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.MasterPassword.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Repositories;

namespace Bit.Core.KeyManagement.MasterPassword;

/// <inheritdoc />
public class UpdateMasterPasswordCommand : IUpdateMasterPasswordCommand
{
private readonly IUpdateMasterPasswordQuery _query;
private readonly IUserRepository _userRepository;

public UpdateMasterPasswordCommand(IUpdateMasterPasswordQuery query, IUserRepository userRepository)
{
_query = query;
_userRepository = userRepository;
}

/// <inheritdoc />
public async Task RunAsync(User user, UpdateMasterPasswordData data)
{
await _query.RunAsync(user, data);
await _userRepository.ReplaceAsync(user);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Bit.Core.Entities;
using Bit.Core.KeyManagement.MasterPassword.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;

namespace Bit.Core.KeyManagement.MasterPassword;

/// <inheritdoc />
public class UpdateMasterPasswordQuery : IUpdateMasterPasswordQuery
{
private readonly IPasswordHasher<User> _passwordHasher;

public UpdateMasterPasswordQuery(IPasswordHasher<User> passwordHasher)
{
_passwordHasher = passwordHasher;
}

/// <inheritdoc />
public Task RunAsync(User user, UpdateMasterPasswordData data)
{
data.ValidateForUser(user);

user.MasterPassword = _passwordHasher.HashPassword(user,
data.MasterPasswordAuthentication.MasterPasswordAuthenticationHash);
user.MasterPasswordHint = data.MasterPasswordHint;
user.Key = data.MasterPasswordUnlock.MasterKeyWrappedUserKey;

var now = DateTime.UtcNow;
user.RevisionDate = now;
user.AccountRevisionDate = now;
user.LastPasswordChangeDate = now;

return Task.CompletedTask;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using Bit.Core.Entities;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Data for setting an initial master password on a user account that has no existing master password.
/// See <see cref="UpdateMasterPasswordData"/> for updating an existing master password.
/// </summary>
public class SetInitialMasterPasswordData
{
public required MasterPasswordAuthenticationData MasterPasswordAuthentication { get; init; }
public required MasterPasswordUnlockData MasterPasswordUnlock { get; init; }
public string? MasterPasswordHint { get; init; }

/// <summary>
/// Validates that the provided data is consistent with a user account that has no existing master password.
/// </summary>
/// <remarks>
/// Verifies that the user has no existing master password, user key, or master password salt,
/// and that the provided salts match the user's current salt (email fallback).
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown when the user already has a master password, user key, or master password salt set,
/// or when the provided salt does not match the user's current salt.
/// </exception>
public void ValidateForUser(User user)
{
try
{
if (user.MasterPassword != null)
{
throw new InvalidOperationException("User already has a master password.");
}

if (user.Key != null)
{
throw new InvalidOperationException("User already has a user key.");
}

if (user.MasterPasswordSalt != null)
{
throw new InvalidOperationException("User already has a master password salt.");
}

MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);
MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);
}
catch
{
throw new InvalidOperationException("The provided master password data is not valid for this user.");
}
}
}
39 changes: 39 additions & 0 deletions src/Core/KeyManagement/Models/Data/UpdateMasterPasswordData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using Bit.Core.Entities;

namespace Bit.Core.KeyManagement.Models.Data;

/// <summary>
/// Data for updating a master password on a user account that already has one.
/// KDF settings must remain unchanged — use <see cref="Bit.Core.KeyManagement.Kdf.IChangeKdfCommand"/> to change KDF.
/// See <see cref="SetInitialMasterPasswordData"/> for setting an initial master password.
/// </summary>
public class UpdateMasterPasswordData
{
public required MasterPasswordAuthenticationData MasterPasswordAuthentication { get; init; }
public required MasterPasswordUnlockData MasterPasswordUnlock { get; init; }
public string? MasterPasswordHint { get; init; }

/// <summary>
/// Validates that the provided data is consistent with the user's current KDF and salt configuration.
/// </summary>
/// <remarks>
/// KDF settings and salt must be unchanged, as this operation only updates the master password.
/// </remarks>
/// <exception cref="InvalidOperationException">
/// Thrown when the KDF settings or salt in the provided data do not match the user's current values.
/// </exception>
public void ValidateForUser(User user)
{
try
{
MasterPasswordAuthentication.ValidateSaltUnchangedForUser(user);
MasterPasswordAuthentication.Kdf.ValidateUnchangedForUser(user);
MasterPasswordUnlock.ValidateSaltUnchangedForUser(user);
MasterPasswordUnlock.Kdf.ValidateUnchangedForUser(user);
}
catch
{
throw new InvalidOperationException("The provided master password data is not valid for this user.");
}
}
}
Loading
Loading