mirror of
https://github.com/bitwarden/server
synced 2026-02-27 01:43:46 +00:00
[PM-31052][PM-32469] Add V2UpgradeToken for key rotation without logout (#6995)
* User V2UpgradeToken for key rotation without logout * reset old v2 upgrade token on manual key rotation * sql migration fix * missing table column * missing view update * tests for V2UpgradeToken clearing on manual key rotation * V2 to V2 rotation causes logout. Updated wrapped key 1 to be a valid V2 encrypted string in tests. * integration tests failures - increase assert recent for date time type from 2 to 5 seconds (usually for UpdatedAt assertions) * repository test coverage * migration script update * new EF migration scripts * broken EF migration scripts fixed * refresh views due to User table alternation
This commit is contained in:
@@ -210,6 +210,7 @@ public static class FeatureFlagKeys
|
||||
public const string EnableAccountEncryptionV2KeyConnectorRegistration = "enable-account-encryption-v2-key-connector-registration";
|
||||
public const string SdkKeyRotation = "pm-30144-sdk-key-rotation";
|
||||
public const string UnlockViaSdk = "unlock-via-sdk";
|
||||
public const string NoLogoutOnKeyUpgradeRotation = "pm-31050-no-logout-key-upgrade-rotation";
|
||||
public const string EnableAccountEncryptionV2JitPasswordRegistration = "enable-account-encryption-v2-jit-password-registration";
|
||||
|
||||
/* Mobile Team */
|
||||
|
||||
@@ -105,6 +105,11 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
public DateTime? LastKeyRotationDate { get; set; }
|
||||
public DateTime? LastEmailChangeDate { get; set; }
|
||||
public bool VerifyDevices { get; set; } = true;
|
||||
/// <summary>
|
||||
/// V2 upgrade token stored as JSON containing two wrapped user keys.
|
||||
/// Allows clients to unlock vault after V1 to V2 key rotation without logout.
|
||||
/// </summary>
|
||||
public string? V2UpgradeToken { get; set; }
|
||||
// PM-28827 Uncomment below line.
|
||||
// public string? MasterPasswordSalt { get; set; }
|
||||
|
||||
|
||||
@@ -2,5 +2,6 @@
|
||||
|
||||
public enum PushNotificationLogOutReason : byte
|
||||
{
|
||||
KdfChange = 0
|
||||
KdfChange = 0,
|
||||
KeyRotation = 1
|
||||
}
|
||||
|
||||
@@ -15,4 +15,10 @@ public class UserDecryptionResponseModel
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// V2 upgrade token returned when available, allowing unlock after V1→V2 upgrade.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public V2UpgradeTokenResponseModel? V2UpgradeToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class V2UpgradeTokenResponseModel
|
||||
{
|
||||
public required string WrappedUserKey1 { get; set; }
|
||||
public required string WrappedUserKey2 { get; set; }
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class RotateUserAccountKeysData
|
||||
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
public required IEnumerable<Device> DeviceKeys { get; set; }
|
||||
public V2UpgradeTokenData? V2UpgradeToken { get; set; }
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
public required IEnumerable<Cipher> Ciphers { get; set; }
|
||||
|
||||
31
src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs
Normal file
31
src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class V2UpgradeTokenData
|
||||
{
|
||||
public required string WrappedUserKey1 { get; init; }
|
||||
public required string WrappedUserKey2 { get; init; }
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
public static V2UpgradeTokenData? FromJson(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<V2UpgradeTokenData>(json);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,19 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
var now = DateTime.UtcNow;
|
||||
user.RevisionDate = user.AccountRevisionDate = now;
|
||||
user.LastKeyRotationDate = now;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
// V2UpgradeToken is only valid for V1 users transitioning to V2.
|
||||
// For V2 users the token is semantically invalid — discard it and perform a full logout.
|
||||
var shouldPersistV2UpgradeToken = model.V2UpgradeToken != null && !IsV2EncryptionUserAsync(user);
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
user.V2UpgradeToken = model.V2UpgradeToken!.ToJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.V2UpgradeToken = null;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
|
||||
|
||||
@@ -99,7 +111,17 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id,
|
||||
reason: PushNotificationLogOutReason.KeyRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user