1
0
mirror of https://github.com/bitwarden/server synced 2026-02-25 17:03:22 +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:
Maciej Zieniuk
2026-02-20 20:19:14 +01:00
committed by GitHub
parent a961626957
commit 6a7b8f5a89
35 changed files with 12395 additions and 242 deletions

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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; }

View 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;
}
}
}

View File

@@ -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;
}