Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-16603] Add userkey rotation v2 #5204

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
5dd74db
Implement userkey rotation v2
quexten Jan 27, 2025
26175b7
Update request models
quexten Jan 27, 2025
ba489db
Cleanup
quexten Jan 27, 2025
ff44902
Update tests
quexten Jan 27, 2025
4b7667a
Improve test
quexten Jan 28, 2025
cd40987
Add tests
quexten Jan 28, 2025
73f6f67
Fix formatting
quexten Jan 28, 2025
a939132
Fix test
quexten Jan 28, 2025
58b984f
Merge branch 'main' into km/userkey-rotation-v2
quexten Jan 28, 2025
2fe3c27
Remove whitespace
quexten Jan 28, 2025
414775c
Fix namespace
quexten Jan 29, 2025
b96bfad
Merge branch 'km/userkey-rotation-v2' of github.com:bitwarden/server โ€ฆ
quexten Jan 29, 2025
e5c4c9e
Enable nullable on models
quexten Jan 29, 2025
fb952da
Fix build
quexten Jan 29, 2025
2495d07
Add tests and enable nullable on masterpasswordunlockdatamodel
quexten Jan 29, 2025
f462ef2
Fix test
quexten Jan 29, 2025
bcfe852
Remove rollback
quexten Jan 29, 2025
0567da7
Add tests
quexten Jan 29, 2025
de5b914
Make masterpassword hint optional
quexten Jan 29, 2025
2820055
Update user query
quexten Jan 29, 2025
0958762
Add EF test
quexten Jan 30, 2025
e95e4a2
Improve test
quexten Jan 30, 2025
5262611
Cleanup
quexten Jan 30, 2025
7cbc7a5
Set masterpassword hint
quexten Jan 30, 2025
8f9252d
Merge branch 'main' into km/userkey-rotation-v2
quexten Jan 30, 2025
a8a59da
Remove connection close
quexten Jan 31, 2025
853e226
Merge branch 'km/userkey-rotation-v2' of github.com:bitwarden/server โ€ฆ
quexten Jan 31, 2025
0ca2caa
Add tests for invalid kdf types
quexten Jan 31, 2025
39bb255
Update test/Core.Test/KeyManagement/UserKey/RotateUserAccountKeysCommโ€ฆ
quexten Jan 31, 2025
159cb21
Merge branch 'main' into km/userkey-rotation-v2
quexten Jan 31, 2025
b8e8d27
Fix formatting
quexten Jan 31, 2025
39201fb
Update src/Api/KeyManagement/Models/Requests/RotateAccountKeysAndDataโ€ฆ
quexten Jan 31, 2025
ab4afa0
Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataMโ€ฆ
quexten Jan 31, 2025
c64b007
Update src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataMโ€ฆ
quexten Jan 31, 2025
3b767d8
Update src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs
quexten Jan 31, 2025
d67b4d6
Fix imports
quexten Jan 31, 2025
49a6a82
Fix tests
quexten Jan 31, 2025
0716858
Merge branch 'main' into km/userkey-rotation-v2
quexten Jan 31, 2025
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
1 change: 1 addition & 0 deletions src/Api/Auth/Controllers/AccountsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ public async Task PostKdf([FromBody] KdfRequestModel model)
throw new BadRequestException(ModelState);
}

[Obsolete("Replaced by the safer rotate-user-account-keys endpoint.")]
[HttpPost("key")]
public async Task PostKey([FromBody] UpdateKeyRequestModel model)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
๏ปฟ#nullable enable

using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;

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

public class MasterPasswordUnlockDataModel : IValidatableObject
{
public required KdfType KdfType { get; set; }
public required int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
Comment on lines +14 to +15
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add custom validator instead to cover this scenario for Argon2. Example


[StrictEmailAddress]
[StringLength(256)]
public required string Email { get; set; }
quexten marked this conversation as resolved.
Show resolved Hide resolved
[StringLength(300)]
public required string MasterKeyAuthenticationHash { get; set; }
[EncryptedString] public required string MasterKeyEncryptedUserKey { get; set; }
[StringLength(50)]
public string? MasterPasswordHint { get; set; }

public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
Thomas-Avery marked this conversation as resolved.
Show resolved Hide resolved
{
if (KdfType == KdfType.PBKDF2_SHA256)
{
if (KdfMemory.HasValue || KdfParallelism.HasValue)
{
yield return new ValidationResult("KdfMemory and KdfParallelism must be null for PBKDF2_SHA256", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
}
}
else if (KdfType == KdfType.Argon2id)
{
if (!KdfMemory.HasValue || !KdfParallelism.HasValue)
{
yield return new ValidationResult("KdfMemory and KdfParallelism must have values for Argon2id", new[] { nameof(KdfMemory), nameof(KdfParallelism) });
}
}
else
{
yield return new ValidationResult("Invalid KdfType", new[] { nameof(KdfType) });
}
}

public MasterPasswordUnlockData ToUnlockData()
{
var data = new MasterPasswordUnlockData
{
KdfType = KdfType,
KdfIterations = KdfIterations,
KdfMemory = KdfMemory,
KdfParallelism = KdfParallelism,

Email = Email,

MasterKeyAuthenticationHash = MasterKeyAuthenticationHash,
MasterKeyEncryptedUserKey = MasterKeyEncryptedUserKey,
MasterPasswordHint = MasterPasswordHint
};
return data;
}

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
๏ปฟ#nullable enable
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Models.Requests;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
using Bit.Core;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -19,18 +32,45 @@ public class AccountsKeyManagementController : Controller
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IRegenerateUserAsymmetricKeysCommand _regenerateUserAsymmetricKeysCommand;
private readonly IUserService _userService;
private readonly IRotateUserAccountKeysCommand _rotateUserAccountKeysCommand;
private readonly IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> _cipherValidator;
private readonly IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> _folderValidator;
private readonly IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> _sendValidator;
private readonly IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
_emergencyAccessValidator;
private readonly IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>
_organizationUserValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator;

public AccountsKeyManagementController(IUserService userService,
IFeatureService featureService,
IOrganizationUserRepository organizationUserRepository,
IEmergencyAccessRepository emergencyAccessRepository,
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand)
IRegenerateUserAsymmetricKeysCommand regenerateUserAsymmetricKeysCommand,
IRotateUserAccountKeysCommand rotateUserKeyCommandV2,
IRotationValidator<IEnumerable<CipherWithIdRequestModel>, IEnumerable<Cipher>> cipherValidator,
IRotationValidator<IEnumerable<FolderWithIdRequestModel>, IEnumerable<Folder>> folderValidator,
IRotationValidator<IEnumerable<SendWithIdRequestModel>, IReadOnlyList<Send>> sendValidator,
IRotationValidator<IEnumerable<EmergencyAccessWithIdRequestModel>, IEnumerable<EmergencyAccess>>
emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator)
{
_userService = userService;
_featureService = featureService;
_regenerateUserAsymmetricKeysCommand = regenerateUserAsymmetricKeysCommand;
_organizationUserRepository = organizationUserRepository;
_emergencyAccessRepository = emergencyAccessRepository;
_rotateUserAccountKeysCommand = rotateUserKeyCommandV2;
_cipherValidator = cipherValidator;
_folderValidator = folderValidator;
_sendValidator = sendValidator;
_emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator;
}

[HttpPost("regenerate-keys")]
Expand All @@ -47,4 +87,45 @@ public async Task RegenerateKeysAsync([FromBody] KeyRegenerationRequestModel req
await _regenerateUserAsymmetricKeysCommand.RegenerateKeysAsync(request.ToUserAsymmetricKeys(user.Id),
usersOrganizationAccounts, designatedEmergencyAccess);
}


[HttpPost("rotate-user-account-keys")]
public async Task RotateUserAccountKeysAsync([FromBody] RotateUserAccountKeysAndDataRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}

var dataModel = new RotateUserAccountKeysData
{
OldMasterKeyAuthenticationHash = model.OldMasterKeyAuthenticationHash,

UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
AccountPublicKey = model.AccountKeys.AccountPublicKey,

MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),
OrganizationUsers = await _organizationUserValidator.ValidateAsync(user, model.AccountUnlockData.OrganizationAccountRecoveryUnlockData),
WebAuthnKeys = await _webauthnKeyValidator.ValidateAsync(user, model.AccountUnlockData.PasskeyUnlockData),

Ciphers = await _cipherValidator.ValidateAsync(user, model.AccountData.Ciphers),
Folders = await _folderValidator.ValidateAsync(user, model.AccountData.Folders),
Sends = await _sendValidator.ValidateAsync(user, model.AccountData.Sends),
};

var result = await _rotateUserAccountKeysCommand.RotateUserAccountKeysAsync(user, dataModel);
if (result.Succeeded)
{
return;
}

foreach (var error in result.Errors)
{
ModelState.AddModelError(string.Empty, error.Description);
}

throw new BadRequestException(ModelState);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
๏ปฟ#nullable enable
using Bit.Core.Utilities;

namespace Bit.Api.KeyManagement.Models.Requests;

public class AccountKeysRequestModel
{
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
public required string AccountPublicKey { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
๏ปฟ#nullable enable
using System.ComponentModel.DataAnnotations;

namespace Bit.Api.KeyManagement.Models.Requests;

public class RotateUserAccountKeysAndDataRequestModel
{
[StringLength(300)]
public required string OldMasterKeyAuthenticationHash { get; set; }
quexten marked this conversation as resolved.
Show resolved Hide resolved
public required UnlockDataRequestModel AccountUnlockData { get; set; }
public required AccountKeysRequestModel AccountKeys { get; set; }
public required AccountDataRequestModel AccountData { get; set; }
}
16 changes: 16 additions & 0 deletions src/Api/KeyManagement/Models/Requests/UnlockDataRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
๏ปฟ#nullable enable
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;

namespace Bit.Api.KeyManagement.Models.Requests;

public class UnlockDataRequestModel
{
// All methods to get to the userkey
public required MasterPasswordUnlockDataModel MasterPasswordUnlockData { get; set; }
public required IEnumerable<EmergencyAccessWithIdRequestModel> EmergencyAccessUnlockData { get; set; }
public required IEnumerable<ResetPasswordWithOrgIdRequestModel> OrganizationAccountRecoveryUnlockData { get; set; }
public required IEnumerable<WebAuthnLoginRotateKeyRequestModel> PasskeyUnlockData { get; set; }
}
12 changes: 12 additions & 0 deletions src/Api/KeyManagement/Models/Requests/UserDataRequestModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
๏ปฟ#nullable enable
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;

namespace Bit.Api.KeyManagement.Models.Requests;

public class AccountDataRequestModel
{
public required IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }
public required IEnumerable<FolderWithIdRequestModel> Folders { get; set; }
public required IEnumerable<SendWithIdRequestModel> Sends { get; set; }
Thomas-Avery marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public static void AddUserServices(this IServiceCollection services, IGlobalSett
public static void AddUserKeyCommands(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<IRotateUserKeyCommand, RotateUserKeyCommand>();
services.AddScoped<IRotateUserAccountKeysCommand, RotateUserAccountKeysCommand>();
}

private static void AddUserPasswordCommands(this IServiceCollection services)
Expand Down
1 change: 1 addition & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ public static class FeatureFlagKeys
public const string Argon2Default = "argon2-default";
public const string UsePricingService = "use-pricing-service";
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
public const string UserkeyRotationV2 = "userkey-rotation-v2";
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
Expand Down
34 changes: 34 additions & 0 deletions src/Core/KeyManagement/Models/Data/MasterPasswordUnlockData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
๏ปฟ#nullable enable
using Bit.Core.Entities;
using Bit.Core.Enums;

namespace Bit.Core.KeyManagement.Models.Data;

public class MasterPasswordUnlockData
{
public KdfType KdfType { get; set; }
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }

public required string Email { get; set; }
public required string MasterKeyAuthenticationHash { get; set; }
public required string MasterKeyEncryptedUserKey { get; set; }
public string? MasterPasswordHint { get; set; }

public bool ValidateForUser(User user)
{
if (KdfType != user.Kdf || KdfMemory != user.KdfMemory || KdfParallelism != user.KdfParallelism || KdfIterations != user.KdfIterations)
{
return false;
}
else if (Email != user.Email)
{
return false;
}
else
{
return true;
}
}
}
28 changes: 28 additions & 0 deletions src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
๏ปฟusing Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;

namespace Bit.Core.KeyManagement.Models.Data;

public class RotateUserAccountKeysData
{
// Authentication for this requests
public string OldMasterKeyAuthenticationHash { get; set; }

// Other keys encrypted by the userkey
public string UserKeyEncryptedAccountPrivateKey { get; set; }
public string AccountPublicKey { get; set; }

// All methods to get to the userkey
public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }
public IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
public IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }

// User vault data encrypted by the userkey
public IEnumerable<Cipher> Ciphers { get; set; }
public IEnumerable<Folder> Folders { get; set; }
public IReadOnlyList<Send> Sends { get; set; }
}
20 changes: 20 additions & 0 deletions src/Core/KeyManagement/UserKey/IRotateUserAccountKeysCommand.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
๏ปฟusing Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Microsoft.AspNetCore.Identity;

namespace Bit.Core.KeyManagement.UserKey;

/// <summary>
/// Responsible for rotation of a user key and updating database with re-encrypted data
/// </summary>
public interface IRotateUserAccountKeysCommand
{
/// <summary>
/// Sets a new user key and updates all encrypted data.
/// </summary>
/// <param name="model">All necessary information for rotation. If data is not included, this will lead to the change being rejected.</param>
/// <returns>An IdentityResult for verification of the master password hash</returns>
/// <exception cref="ArgumentNullException">User must be provided.</exception>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should also mention that InvalidOperationException is possible

/// <exception cref="InvalidOperationException">User KDF settings and email must match the model provided settings.</exception>
Task<IdentityResult> RotateUserAccountKeysAsync(User user, RotateUserAccountKeysData model);
}
Loading
Loading