From 77206b12a9f7b9a8f99b4c8c7673ac780521ebab Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 21 Mar 2025 13:49:34 +0100 Subject: [PATCH] Fake responses for non-existent users (#5538) --- .../Opaque/OpaqueRegistrationStartRequest.cs | 2 +- .../OpaqueKeyExchangeService.cs | 59 ++++++++++++++----- 2 files changed, 45 insertions(+), 16 deletions(-) diff --git a/src/Core/Auth/Models/Api/Request/Opaque/OpaqueRegistrationStartRequest.cs b/src/Core/Auth/Models/Api/Request/Opaque/OpaqueRegistrationStartRequest.cs index a3dd805eda..6aaae1f9b7 100644 --- a/src/Core/Auth/Models/Api/Request/Opaque/OpaqueRegistrationStartRequest.cs +++ b/src/Core/Auth/Models/Api/Request/Opaque/OpaqueRegistrationStartRequest.cs @@ -14,7 +14,7 @@ public class OpaqueRegistrationStartRequest public class OpaqueKeyExchangeCipherConfiguration { - static string OpaqueKe3Ristretto3DHArgonSuite = "OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF"; + public static string OpaqueKe3Ristretto3DHArgonSuite = "OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF"; [Required] public string CipherSuite { get; set; } diff --git a/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs b/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs index 9490e715da..d6e5ffc915 100644 --- a/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs +++ b/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs @@ -8,6 +8,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Utilities; using Bit.Core.Entities; using Bit.Core.Repositories; +using Bit.Core.Settings; using Bit.Core.Utilities; using Bitwarden.Opaque; using Microsoft.Extensions.Caching.Distributed; @@ -25,6 +26,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService private readonly IUserRepository _userRepository; private readonly ILogger _logger; private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; + private readonly byte[] _defaultKdfHmacKey = null; const string REGISTRATION_SESSION_KEY = "opaque_register_session_{0}"; const string LOGIN_SESSION_KEY = "opaque_login_session_{0}"; @@ -33,7 +35,8 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService IOpaqueKeyExchangeCredentialRepository opaqueKeyExchangeCredentialRepository, IDistributedCache distributedCache, IUserRepository userRepository, - ILogger logger + ILogger logger, + GlobalSettings globalSettings ) { _bitwardenOpaque = new BitwardenOpaqueServer(); @@ -45,6 +48,14 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService { AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1) }; + if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey)) + { + _defaultKdfHmacKey = Encoding.UTF8.GetBytes(globalSettings.KdfDefaultHashKey); + } + else + { + _defaultKdfHmacKey = new byte[32]; + } } public async Task StartRegistration( @@ -110,18 +121,39 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService { try { - // todo: don't allow user enumeration - var user = await _userRepository.GetByEmailAsync(email) - ?? throw new InvalidOperationException("User not found"); + var user = await _userRepository.GetByEmailAsync(email); + // Fake user to prevent user enumeration + user ??= new User() { Id = Guid.Empty }; - // todo: generate fake credential - var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id) - ?? throw new InvalidOperationException("Credential not found"); - - var cipherConfiguration = JsonSerializer.Deserialize(credential.CipherConfiguration)!; - var credentialBlob = JsonSerializer.Deserialize(credential.CredentialBlob)!; - var serverSetup = credentialBlob.ServerSetup; - var serverRegistration = credentialBlob.PasswordFile; + byte[] serverSetup = null; + byte[] serverRegistration = null; + OpaqueKeyExchangeCipherConfiguration cipherConfiguration; + var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id); + if (credential != null) + { + cipherConfiguration = JsonSerializer.Deserialize(credential.CipherConfiguration)!; + var credentialBlob = JsonSerializer.Deserialize(credential.CredentialBlob)!; + serverSetup = credentialBlob.ServerSetup; + serverRegistration = credentialBlob.PasswordFile; + } + else + { + // Generate a fake registration for non-existent users + cipherConfiguration = new OpaqueKeyExchangeCipherConfiguration() + { + CipherSuite = OpaqueKeyExchangeCipherConfiguration.OpaqueKe3Ristretto3DHArgonSuite, + Argon2Parameters = new Argon2KsfParameters() + { + Memory = 0, + Iterations = 0, + Parallelism = 0 + } + }; + var hmacMessage = Encoding.UTF8.GetBytes(email.Trim().ToLowerInvariant()); + using var hmac = new System.Security.Cryptography.HMACSHA256(_defaultKdfHmacKey); + var hmacHash = hmac.ComputeHash(hmacMessage); + (serverSetup, serverRegistration) = _bitwardenOpaque.SeededFakeRegistration(hmacHash); + } var loginResponse = _bitwardenOpaque.StartLogin( cipherConfiguration.ToNativeConfiguration(), serverSetup, serverRegistration, request, user.Id.ToString()); @@ -156,9 +188,6 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService ?? throw new InvalidOperationException("Session not found"); var loginSession = JsonSerializer.Deserialize(Encoding.ASCII.GetString(serializedLoginSession))!; - var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(loginSession.UserId) - ?? throw new InvalidOperationException("Credential not found"); - var loginState = loginSession.LoginState; var cipherConfiguration = loginSession.CipherConfiguration; await ClearAuthenticationSession(sessionId);