diff --git a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs index 9fc0e9a75a..6bd1695b22 100644 --- a/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs +++ b/src/Api/KeyManagement/Controllers/AccountsKeyManagementController.cs @@ -108,6 +108,7 @@ public class AccountsKeyManagementController : Controller UserKeyEncryptedAccountPrivateKey = model.AccountKeys.UserKeyEncryptedAccountPrivateKey, AccountPublicKey = model.AccountKeys.AccountPublicKey, + AccountKeys = model.AccountKeys, MasterPasswordUnlockData = model.AccountUnlockData.MasterPasswordUnlockData.ToUnlockData(), EmergencyAccesses = await _emergencyAccessValidator.ValidateAsync(user, model.AccountUnlockData.EmergencyAccessUnlockData), diff --git a/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs index 7c7de4d210..f1c5bda3ff 100644 --- a/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs +++ b/src/Api/KeyManagement/Models/Requests/AccountKeysRequestModel.cs @@ -1,4 +1,6 @@ #nullable enable +using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Models.Data.Models; using Bit.Core.Utilities; namespace Bit.Api.KeyManagement.Models.Requests; @@ -7,4 +9,31 @@ public class AccountKeysRequestModel { [EncryptedString] public required string UserKeyEncryptedAccountPrivateKey { 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() + }; + } + } } diff --git a/src/Api/KeyManagement/Models/Requests/PrivateEncryptionKeyPairRequestModel.cs b/src/Api/KeyManagement/Models/Requests/PrivateEncryptionKeyPairRequestModel.cs new file mode 100644 index 0000000000..533a4e4a35 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/PrivateEncryptionKeyPairRequestModel.cs @@ -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 + ); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs b/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs new file mode 100644 index 0000000000..e95421b9f1 --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/PublicKeyEncryptionKeyPairRequestModel.cs @@ -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 + ); + } +} diff --git a/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs b/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs new file mode 100644 index 0000000000..ce2d73624b --- /dev/null +++ b/src/Api/KeyManagement/Models/Requests/SignatureKeyPairRequestModel.cs @@ -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 + ); + } +} diff --git a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs index f81baf6fab..b6730a62ff 100644 --- a/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/RotateUserAccountKeysData.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; +using Bit.Core.KeyManagement.Models.Data.Models; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; @@ -12,9 +13,13 @@ public class RotateUserAccountKeysData public string OldMasterKeyAuthenticationHash { get; set; } // Other keys encrypted by the userkey + [Obsolete("Use AccountKeys instead")] public string UserKeyEncryptedAccountPrivateKey { get; set; } + [Obsolete("Use AccountKeys instead")] public string AccountPublicKey { get; set; } + public UserAccountKeysData AccountKeys { get; set; } + // All methods to get to the userkey public MasterPasswordUnlockData MasterPasswordUnlockData { get; set; } public IEnumerable EmergencyAccesses { get; set; } diff --git a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs index bfd33e690b..7ac0e4a7dc 100644 --- a/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs +++ b/src/Core/KeyManagement/Models/Data/UserAccountKeysData.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.KeyManagement.Models.Data; +namespace Bit.Core.KeyManagement.Models.Data.Models; #nullable enable diff --git a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs index 57aa8e02d4..fdc5c9286c 100644 --- a/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs +++ b/src/Core/KeyManagement/UserKey/Implementations/RotateUserAccountkeysCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.KeyManagement.Models.Data; +using Bit.Core.KeyManagement.Repositories; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Services; @@ -26,6 +27,7 @@ namespace Bit.Core.KeyManagement.UserKey.Implementations; /// Logs out user from other devices after successful rotation /// Provides a password mismatch error if master password hash validation fails /// Provides a method to update re-encrypted WebAuthn keys +/// Provides a method to update re-encrypted signature keys public class RotateUserAccountKeysCommand( IUserService _userService, IUserRepository _userRepository, @@ -38,7 +40,8 @@ public class RotateUserAccountKeysCommand( IPasswordHasher _passwordHasher, IPushNotificationService _pushService, IdentityErrorDescriber _identityErrorDescriber, - IWebAuthnCredentialRepository _credentialRepository + IWebAuthnCredentialRepository _credentialRepository, + IUserSignatureKeyPairRepository _userSignatureKeyPairRepository ) : IRotateUserAccountKeysCommand { /// @@ -58,17 +61,54 @@ public class RotateUserAccountKeysCommand( user.RevisionDate = user.AccountRevisionDate = now; user.LastKeyRotationDate = now; user.SecurityStamp = Guid.NewGuid().ToString(); - List saveEncryptedDataActions = new(); + var currentSignatureKeyPair = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id); + List saveEncryptedDataActions = []; if (!model.MasterPasswordUnlockData.ValidateForUser(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."); } + 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.PrivateKey = model.UserKeyEncryptedAccountPrivateKey; user.MasterPassword = _passwordHasher.HashPassword(user, model.MasterPasswordUnlockData.MasterKeyAuthenticationHash);