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,6 +1,7 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Enums;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Api.KeyManagement.Models.Responses;
using Bit.Api.KeyManagement.Validators;
Expand Down Expand Up @@ -140,6 +141,34 @@
throw new BadRequestException(ModelState);
}

[HttpPost("key-management/rotate-user-keys")]
public async Task RotateUserKeysAsync([FromBody] RotateUserKeysRequestModel request)
Comment thread Dismissed
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}

switch (request.UnlockMethodData.UnlockMethod)
{
case UnlockMethod.MasterPassword:
var dataModel = new MasterPasswordRotateUserAccountKeysData
{
MasterPasswordUnlockData = request.UnlockMethodData.MasterPasswordUnlockData!.ToData(),
BaseData = await ToBaseDataModelAsync(request, user),
};
await _rotateUserAccountKeysCommand.MasterPasswordRotateUserAccountKeysAsync(user, dataModel);
break;
case UnlockMethod.Tde:
throw new BadRequestException("TDE not implemented");
case UnlockMethod.KeyConnector:
throw new BadRequestException("Key connector not implemented");
default:
throw new ArgumentOutOfRangeException(nameof(request.UnlockMethodData.UnlockMethod), "Unrecognized unlock method");
}
}

[HttpPost("set-key-connector-key")]
public async Task PostSetKeyConnectorKeyAsync([FromBody] SetKeyConnectorKeyRequestModel model)
{
Expand Down Expand Up @@ -231,4 +260,24 @@
var details = await _keyConnectorConfirmationDetailsQuery.Run(orgSsoIdentifier, user.Id);
return new KeyConnectorConfirmationDetailsResponseModel(details);
}

private async Task<BaseRotateUserAccountKeysData> ToBaseDataModelAsync(RotateUserKeysRequestModel request, User user)
{
return new BaseRotateUserAccountKeysData
{
AccountKeys = request.WrappedAccountCryptographicState.ToAccountKeysData(),
EmergencyAccesses =
await _emergencyAccessValidator.ValidateAsync(user, request.UnlockData.EmergencyAccessUnlockData),
OrganizationUsers =
await _organizationUserValidator.ValidateAsync(user,
request.UnlockData.OrganizationAccountRecoveryUnlockData),
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, request.UnlockData.PasskeyUnlockData),
DeviceKeys = await _deviceValidator.ValidateAsync(user, request.UnlockData.DeviceKeyUnlockData),
V2UpgradeToken = request.UnlockData.V2UpgradeToken?.ToData(),

Ciphers = await _cipherValidator.ValidateAsync(user, request.AccountData.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, request.AccountData.Folders),
Sends = await _sendValidator.ValidateAsync(user, request.AccountData.Sends),
};
}
}
8 changes: 8 additions & 0 deletions src/Api/KeyManagement/Enums/UnlockMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Bit.Api.KeyManagement.Enums;

public enum UnlockMethod
{
Tde,
MasterPassword,
KeyConnector,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Core.Auth.Models.Api.Request;

namespace Bit.Api.KeyManagement.Models.Requests;

public class CommonUnlockDataRequestModel
{
public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; }
public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }
public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }
public required IEnumerable<OtherDeviceKeysUpdateRequestModel> DeviceKeyUnlockData { get; set; }
public V2UpgradeTokenRequestModel? V2UpgradeToken { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Bit.Api.KeyManagement.Models.Requests;

public class RotateUserKeysRequestModel
{
public required WrappedAccountCryptographicStateRequestModel WrappedAccountCryptographicState { get; set; }
public required CommonUnlockDataRequestModel UnlockData { get; set; }
public required AccountDataRequestModel AccountData { get; set; }
public required UnlockMethodRequestModel UnlockMethodData { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;

public class UnlockMethodRequestModel : IValidatableObject
{
[Required]
public required UnlockMethod UnlockMethod { get; init; }

// Master password user
public MasterPasswordUnlockDataRequestModel? MasterPasswordUnlockData { get; init; }

// Key Connector user.
[EncryptedString]
public string? KeyConnectorKeyWrappedUserKey { get; init; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
switch (UnlockMethod)
{
case UnlockMethod.MasterPassword:
if (MasterPasswordUnlockData == null || KeyConnectorKeyWrappedUserKey != null)
{
yield return new ValidationResult("Invalid MasterPassword unlock method request, MasterPasswordUnlockData must be provided and KeyConnectorKeyWrappedUserKey must be null");
}
break;
case UnlockMethod.Tde:
if (MasterPasswordUnlockData != null || KeyConnectorKeyWrappedUserKey != null)
{
yield return new ValidationResult("Invalid Tde unlock method request, MasterPasswordUnlockData must be null and KeyConnectorKeyWrappedUserKey must be null");
}
break;
case UnlockMethod.KeyConnector:
if (KeyConnectorKeyWrappedUserKey == null || MasterPasswordUnlockData != null)
{
yield return new ValidationResult("Invalid KeyConnector unlock method request, KeyConnectorKeyWrappedUserKey must be provided and MasterPasswordUnlockData must be null");
}
break;
default:
yield return new ValidationResult("Unrecognized unlock method");
break;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Bit.Core.KeyManagement.Models.Api.Request;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Api.KeyManagement.Models.Requests;

// This request model is meant to be used when the user will be submitting a v2 encryption WrappedAccountCryptographicState payload.
public class WrappedAccountCryptographicStateRequestModel
Comment thread
Thomas-Avery marked this conversation as resolved.
{
public required PublicKeyEncryptionKeyPairRequestModel PublicKeyEncryptionKeyPair { get; set; }
public required SignatureKeyPairRequestModel SignatureKeyPair { get; set; }
public required SecurityStateModel SecurityState { get; set; }

public UserAccountKeysData ToAccountKeysData()
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData(),
SecurityStateData = SecurityState.ToSecurityState()
};
}
}
11 changes: 11 additions & 0 deletions src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#nullable disable

using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.UserKey.Models.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.Data.SqlClient;
Expand All @@ -21,6 +22,16 @@ public interface IRotateUserAccountKeysCommand
/// <exception cref="ArgumentNullException">User must be provided.</exception>
/// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception>
Task<IdentityResult> PasswordChangeAndRotateUserAccountKeysAsync(User user, PasswordChangeAndRotateUserAccountKeysData model);

/// <summary>
/// For a master password user, rotates the user key and updates all encrypted data without changing the master password.
/// </summary>
/// <param name="model">Rotation data. All encrypted data must be included or the request will be rejected.</param>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="user"/> is null.</exception>
/// <exception cref="BadRequestException">Thrown when <paramref name="user"/> is not a master password user.</exception>
/// <exception cref="BadRequestException">Thrown when <paramref name="user"/> salt does not match <paramref name="model"/> MasterPasswordUnlockData.</exception>
/// <exception cref="ArgumentException">Thrown when <paramref name="user"/> KDF settings do not match <paramref name="model"/> MasterPasswordUnlockData.</exception>
Task MasterPasswordRotateUserAccountKeysAsync(User user, MasterPasswordRotateUserAccountKeysData model);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,23 @@ public async Task<IdentityResult> PasswordChangeAndRotateUserAccountKeysAsync(Us
return IdentityResult.Success;
}

/// <inheritdoc />
public async Task MasterPasswordRotateUserAccountKeysAsync(User user, MasterPasswordRotateUserAccountKeysData model)
{
ArgumentNullException.ThrowIfNull(user);

model.ValidateForUser(user);

List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
var shouldPersistV2UpgradeToken =
await BaseRotateUserAccountKeysAsync(model.BaseData, user, saveEncryptedDataActions);
user.Key = model.MasterPasswordUnlockData.MasterKeyWrappedUserKey;

await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);

await HandlePushNotificationAsync(shouldPersistV2UpgradeToken, user);
}

private async Task RotateV2AccountKeysAsync(BaseRotateUserAccountKeysData model, User user, List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions)
{
ValidateV2Encryption(model);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;

namespace Bit.Core.KeyManagement.UserKey.Models.Data;

public class MasterPasswordRotateUserAccountKeysData
{
public required MasterPasswordUnlockData MasterPasswordUnlockData { get; init; }
public required BaseRotateUserAccountKeysData BaseData { get; init; }

public void ValidateForUser(User user)
{
var isMasterPasswordUser = user is { Key: not null, MasterPassword: not null };
if (!isMasterPasswordUser)
{
throw new BadRequestException("User is in an invalid state for master password key rotation.");
}

MasterPasswordUnlockData.ValidateSaltUnchangedForUser(user);
MasterPasswordUnlockData.Kdf.ValidateUnchangedForUser(user);
}
}
Loading
Loading