1
0
mirror of https://github.com/bitwarden/server synced 2026-03-01 19:01:14 +00:00

feat(auth-validator): [Auth/PM-22975] Client Version Validator (#6588)

* feat(auth-validator): [PM-22975] Client Version Validator - Implementation.

* test(auth-validator): [PM-22975] Client Version Validator - Added tests.
This commit is contained in:
Patrick-Pimentel-Bitwarden
2026-02-23 10:00:10 -05:00
committed by GitHub
parent b5554c6030
commit 3dbd17f61d
27 changed files with 732 additions and 83 deletions

View File

@@ -32,7 +32,7 @@ public interface ICurrentContext
Guid? OrganizationId { get; set; }
IdentityClientType IdentityClientType { get; set; }
string ClientId { get; set; }
Version ClientVersion { get; set; }
Version? ClientVersion { get; set; }
bool ClientVersionIsPrerelease { get; set; }
Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings);

View File

@@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Utilities;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
@@ -216,6 +217,42 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
return SecurityVersion ?? 1;
}
/// <summary>
/// Evaluates user state to determine if they are currently in a v2 encryption state.
/// </summary>
/// <returns>If the shape of their private key is v2 as well as has the proper security version then true, otherwise false</returns>
public bool HasV2Encryption()
{
return HasV2KeyShape() && IsSecurityVersionTwo();
}
private bool HasV2KeyShape()
{
if (string.IsNullOrEmpty(PrivateKey))
{
return false;
}
try
{
return EncryptionParsing.GetEncryptionType(PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
}
catch (ArgumentException)
{
// Invalid encryption string format - treat as not v2
return false;
}
}
/// <summary>
/// This technically is correct but all versions after 1 are considered v2 encryption. Leaving for now with
/// KM's blessing that when a new version comes along they will handle migration.
/// </summary>
private bool IsSecurityVersionTwo()
{
return SecurityVersion == 2;
}
/// <summary>
/// Serializes the C# object to the User.TwoFactorProviders property in JSON format.
/// </summary>

View File

@@ -11,8 +11,11 @@ public enum EncryptionType : byte
XChaCha20Poly1305_B64 = 7,
// asymmetric
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
Rsa2048_OaepSha256_B64 = 3,
Rsa2048_OaepSha1_B64 = 4,
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
Rsa2048_OaepSha256_HmacSha256_B64 = 5,
[Obsolete("PM-29656 - Should probably be removed as it is not known to exist in the real world")]
Rsa2048_OaepSha1_HmacSha256_B64 = 6
}

View File

@@ -0,0 +1,6 @@
namespace Bit.Core.KeyManagement;
public static class Constants
{
public static readonly Version MinimumClientVersionForV2Encryption = new("2025.11.0");
}

View File

@@ -6,6 +6,7 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.KeyManagement.Utilities;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -159,7 +160,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
}
else
{
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.AesCbc256_HmacSha256_B64)
{
throw new InvalidOperationException("The provided account private key was not wrapped with AES-256-CBC-HMAC");
}
@@ -231,7 +232,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
{
// Returns whether the user is a V2 user based on the private key's encryption type.
ArgumentNullException.ThrowIfNull(user);
var isPrivateKeyEncryptionV2 = GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
var isPrivateKeyEncryptionV2 = EncryptionParsing.GetEncryptionType(user.PrivateKey) == EncryptionType.XChaCha20Poly1305_B64;
return isPrivateKeyEncryptionV2;
}
@@ -259,7 +260,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
{
throw new InvalidOperationException("Signature key pair data is required for V2 encryption.");
}
if (GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.SignatureKeyPairData.WrappedSigningKey) != EncryptionType.XChaCha20Poly1305_B64)
{
throw new InvalidOperationException("The provided signing key data is not wrapped with XChaCha20-Poly1305.");
}
@@ -268,7 +269,7 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
throw new InvalidOperationException("The provided signature key pair data does not contain a valid verifying key.");
}
if (GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
if (EncryptionParsing.GetEncryptionType(model.AccountKeys.PublicKeyEncryptionKeyPairData.WrappedPrivateKey) != EncryptionType.XChaCha20Poly1305_B64)
{
throw new InvalidOperationException("The provided private key encryption key is not wrapped with XChaCha20-Poly1305.");
}
@@ -281,24 +282,4 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
throw new InvalidOperationException("No signed security state provider for V2 user");
}
}
/// <summary>
/// Helper method to convert an encryption type string to an enum value.
/// </summary>
private static EncryptionType GetEncryptionType(string encString)
{
var parts = encString.Split('.');
if (parts.Length == 1)
{
throw new ArgumentException("Invalid encryption type string.");
}
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
{
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
{
return (EncryptionType)encryptionTypeNumber;
}
}
throw new ArgumentException("Invalid encryption type string.");
}
}

View File

@@ -0,0 +1,28 @@
using Bit.Core.Enums;
namespace Bit.Core.KeyManagement.Utilities;
public static class EncryptionParsing
{
/// <summary>
/// Helper method to convert an encryption type string to an enum value.
/// </summary>
public static EncryptionType GetEncryptionType(string? encString)
{
ArgumentNullException.ThrowIfNull(encString);
var parts = encString.Split('.');
if (parts.Length == 1)
{
throw new ArgumentException("Invalid encryption type string.");
}
if (byte.TryParse(parts[0], out var encryptionTypeNumber))
{
if (Enum.IsDefined(typeof(EncryptionType), encryptionTypeNumber))
{
return (EncryptionType)encryptionTypeNumber;
}
}
throw new ArgumentException("Invalid encryption type string.");
}
}