1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-20 10:58:07 -05:00

Add migration to user encryption v2

This commit is contained in:
Bernd Schoolmann 2025-06-06 19:23:15 +02:00
parent 68237d0b6d
commit ec955565e6
No known key found for this signature in database
8 changed files with 143 additions and 4 deletions

View File

@ -108,6 +108,7 @@ public class AccountsKeyManagementController : Controller
UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey, UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey,
AccountPublicKey = model.AccountKeys.AccountPublicKey, AccountPublicKey = model.AccountKeys.AccountPublicKey,
AccountKeys = model.AccountKeys,
MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(), MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(),
EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData),

View File

@ -1,4 +1,6 @@
#nullable enable #nullable enable
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Models.Data.Models;
using Bit.Core.Utilities; using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests; namespace Bit.Api.KeyManagement.Models.Requests;
@ -7,4 +9,31 @@ public class AccountKeysRequestModel
{ {
[EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; } [EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { get; set; }
public required string AccountPublicKey { get; set; } public required string AccountPublicKey { get; set; }
public PublicKeyEncryptionKeyPairRequestModel? PublicKeyEncryptionKeyPair { get; set; }
public SignatureKeyPairRequestModel? SignatureKeyPair { get; set; }
public UserAccountKeysData ToAccountKeysData()
{
// This will be cleaned up, after a compatibility period, at which point PublicKeyEncryptionKeyPair and SignatureKeyPair will be required.
if (PublicKeyEncryptionKeyPair == null || SignatureKeyPair == null)
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = new PublicKeyEncryptionKeyPairData
(
UserKeyEncryptedAccountPrivateKey,
AccountPublicKey
),
};
}
else
{
return new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = PublicKeyEncryptionKeyPair.ToPublicKeyEncryptionKeyPairData(),
SignatureKeyPairData = SignatureKeyPair.ToSignatureKeyPairData()
};
}
}
} }

View File

@ -0,0 +1,21 @@
#nullable enable
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
public class PrivateEncryptionKeyPairRequestModel
{
[EncryptedString] public required string WrappedPrivateKey { get; set; }
public required string PublicKey { get; set; }
public required string SignedPublicKey { get; set; }
public PublicKeyEncryptionKeyPairData ToPrivateKeyEncryptionKeyPairData()
{
return new PublicKeyEncryptionKeyPairData(
WrappedPrivateKey,
PublicKey,
SignedPublicKey
);
}
}

View File

@ -0,0 +1,21 @@
#nullable enable
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
public class PublicKeyEncryptionKeyPairRequestModel
{
[EncryptedString] public required string WrappedPrivateKey { get; set; }
public required string PublicKey { get; set; }
public required string SignedPublicKey { get; set; }
public PublicKeyEncryptionKeyPairData ToPublicKeyEncryptionKeyPairData()
{
return new PublicKeyEncryptionKeyPairData(
WrappedPrivateKey,
PublicKey,
SignedPublicKey
);
}
}

View File

@ -0,0 +1,22 @@
#nullable enable
using Bit.Core.KeyManagement.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
namespace Bit.Api.KeyManagement.Models.Requests;
public class SignatureKeyPairRequestModel
{
public required SignatureAlgorithm SignatureAlgorithm { get; set; }
[EncryptedString] public required string WrappedSigningKey { get; set; }
public required string VerifyingKey { get; set; }
public SignatureKeyPairData ToSignatureKeyPairData()
{
return new SignatureKeyPairData(
SignatureAlgorithm,
WrappedSigningKey,
VerifyingKey
);
}
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data.Models;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
@ -12,9 +13,13 @@ public class RotateUserAccountKeysData
public string OldMasterKeyAuthenticationHash { get; set; } public string OldMasterKeyAuthenticationHash { get; set; }
// Other keys encrypted by the userkey // Other keys encrypted by the userkey
[Obsolete("Use AccountKeys instead")]
public string UserKeyEncryptedAccountPrivateKey { get; set; } public string UserKeyEncryptedAccountPrivateKey { get; set; }
[Obsolete("Use AccountKeys instead")]
public string AccountPublicKey { get; set; } public string AccountPublicKey { get; set; }
public UserAccountKeysData AccountKeys { get; set; }
// All methods to get to the userkey // All methods to get to the userkey
public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; } public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; }
public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; } public IEnumerable<EmergencyAccess> EmergencyAccesses { get; set; }

View File

@ -1,4 +1,4 @@
namespace Bit.Core.KeyManagement.Models.Data; namespace Bit.Core.KeyManagement.Models.Data.Models;
#nullable enable #nullable enable

View File

@ -1,6 +1,7 @@
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data; using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Platform.Push; using Bit.Core.Platform.Push;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
@ -26,6 +27,7 @@ namespace Bit.Core.KeyManagement.UserKey.Implementations;
/// <param name="_pushService">Logs out user from other devices after successful rotation</param> /// <param name="_pushService">Logs out user from other devices after successful rotation</param>
/// <param name="_identityErrorDescriber">Provides a password mismatch error if master password hash validation fails</param> /// <param name="_identityErrorDescriber">Provides a password mismatch error if master password hash validation fails</param>
/// <param name="_credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param> /// <param name="_credentialRepository">Provides a method to update re-encrypted WebAuthn keys</param>
/// <param name="_userSignatureKeyPairRepository">Provides a method to update re-encrypted signature keys</param>
public class RotateUserAccountKeysCommand( public class RotateUserAccountKeysCommand(
IUserService _userService, IUserService _userService,
IUserRepository _userRepository, IUserRepository _userRepository,
@ -38,7 +40,8 @@ public class RotateUserAccountKeysCommand(
IPasswordHasher<User> _passwordHasher, IPasswordHasher<User> _passwordHasher,
IPushNotificationService _pushService, IPushNotificationService _pushService,
IdentityErrorDescriber _identityErrorDescriber, IdentityErrorDescriber _identityErrorDescriber,
IWebAuthnCredentialRepository _credentialRepository IWebAuthnCredentialRepository _credentialRepository,
IUserSignatureKeyPairRepository _userSignatureKeyPairRepository
) : IRotateUserAccountKeysCommand ) : IRotateUserAccountKeysCommand
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -58,17 +61,54 @@ public class RotateUserAccountKeysCommand(
user.RevisionDate = user.AccountRevisionDate = now; user.RevisionDate = user.AccountRevisionDate = now;
user.LastKeyRotationDate = now; user.LastKeyRotationDate = now;
user.SecurityStamp = Guid.NewGuid().ToString(); user.SecurityStamp = Guid.NewGuid().ToString();
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = new(); var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
if (!model.MasterPasswordUnlockData.ValidateForUser(user)) if (!model.MasterPasswordUnlockData.ValidateForUser(user))
{ {
throw new InvalidOperationException("The provided master password unlock data is not valid for this user."); throw new InvalidOperationException("The provided master password unlock data is not valid for this user.");
} }
if (model.AccountPublicKey != user.PublicKey) if (model.AccountKeys.PublicKeyEncryptionKeyPairData.PublicKey != user.PublicKey)
{ {
throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation."); throw new InvalidOperationException("The provided account public key does not match the user's current public key, and changing the account asymmetric keypair is currently not supported during key rotation.");
} }
if (currentSignatureKeyPair != null)
{
// user is already v2 user
if (model.AccountKeys.SignatureKeyPairData == null)
{
throw new InvalidOperationException("The provided signing key data is null, but the user already has signing keys.");
}
if (model.AccountKeys.SignatureKeyPairData.VerifyingKey != currentSignatureKeyPair.VerifyingKey)
{
throw new InvalidOperationException("The provided signing key data does not match the user's current signing key data.");
}
if (string.IsNullOrEmpty(model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey))
{
throw new InvalidOperationException("No signed public key provided, but the user already has a signature key pair.");
}
}
else
{
if (model.AccountKeys.SignatureKeyPairData != null)
{
// user is upgrading
if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.VerifyingKey))
{
throw new InvalidOperationException("The provided signing key data does not contain a valid verifying key.");
}
if (string.IsNullOrEmpty(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey))
{
throw new InvalidOperationException("The provided signing key data does not contain a valid wrapped signing key.");
}
saveEncryptedDataActions.Add(_userSignatureKeyPairRepository.SetUserSignatureKeyPair(user.Id, model.AccountKeys.SignatureKeyPairData));
user.SignedPublicKey = model.AccountKeys.PublicKeyEncryptionKeyPairData.SignedPublicKey;
}
}
user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey; user.Key = model.MasterPasswordUnlockData.MasterKeyEncryptedUserKey;
user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey; user.PrivateKey = model.UserKeyEncryptedAccountPrivateKey;
user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash); user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);