1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

feat : fix database script; add comments.

This commit is contained in:
Ike Kottlowski 2025-03-19 22:54:23 -04:00
parent 11fcb26778
commit 9848d53683
No known key found for this signature in database
GPG Key ID: C86308E3DCA6D76F
9 changed files with 103 additions and 46 deletions

View File

@ -1,4 +1,6 @@
using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Api.Auth.Models.Request.Opaque;
using Bit.Api.Auth.Models.Response.Opaque;
using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Core.Auth.Models.Api.Response.Opaque;
using Bit.Core.Auth.Services;
using Bit.Core.Services;
@ -42,15 +44,15 @@ public class OpaqueKeyExchangeController : Controller
// TODO: Remove and move to token endpoint
[HttpPost("~/opaque/start-login")]
public async Task<Models.Response.Opaque.OpaqueLoginStartResponse> StartLoginAsync([FromBody] Models.Request.Opaque.OpaqueLoginStartRequest request)
public async Task<OpaqueLoginStartResponse> StartLoginAsync([FromBody] OpaqueLoginStartRequest request)
{
var result = await _opaqueKeyExchangeService.StartLogin(Convert.FromBase64String(request.CredentialRequest), request.Email);
return new Models.Response.Opaque.OpaqueLoginStartResponse(result.Item1, Convert.ToBase64String(result.Item2));
return new OpaqueLoginStartResponse(result.Item1, Convert.ToBase64String(result.Item2));
}
// TODO: Remove and move to token endpoint
[HttpPost("~/opaque/finish-login")]
public async Task<bool> FinishLoginAsync([FromBody] Models.Request.Opaque.OpaqueLoginFinishRequest request)
public async Task<bool> FinishLoginAsync([FromBody] OpaqueLoginFinishRequest request)
{
var result = await _opaqueKeyExchangeService.FinishLogin(request.SessionId, Convert.FromBase64String(request.CredentialFinalization));
return result;

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using Bitwarden.Opaque;
namespace Bit.Core.Auth.Models.Api.Request.Opaque;
@ -8,10 +9,10 @@ public class OpaqueRegistrationStartRequest
[Required]
public string RegistrationRequest { get; set; }
[Required]
public CipherConfiguration CipherConfiguration { get; set; }
public OpaqueKeyExchangeCipherConfiguration CipherConfiguration { get; set; }
}
public class CipherConfiguration
public class OpaqueKeyExchangeCipherConfiguration
{
static string OpaqueKe3Ristretto3DHArgonSuite = "OPAQUE_3_RISTRETTO255_OPRF_RISTRETTO255_KEGROUP_3DH_KEX_ARGON2ID13_KSF";
@ -20,20 +21,20 @@ public class CipherConfiguration
[Required]
public Argon2KsfParameters Argon2Parameters { get; set; }
public Bitwarden.Opaque.CipherConfiguration ToNativeConfiguration()
public CipherConfiguration ToNativeConfiguration()
{
if (CipherSuite == OpaqueKe3Ristretto3DHArgonSuite)
{
return new Bitwarden.Opaque.CipherConfiguration
return new CipherConfiguration
{
OpaqueVersion = 3,
OprfCs = Bitwarden.Opaque.OprfCs.Ristretto255,
KeGroup = Bitwarden.Opaque.KeGroup.Ristretto255,
KeyExchange = Bitwarden.Opaque.KeyExchange.TripleDH,
Ksf = new Bitwarden.Opaque.Ksf
OprfCs = OprfCs.Ristretto255,
KeGroup = KeGroup.Ristretto255,
KeyExchange = KeyExchange.TripleDH,
Ksf = new Ksf
{
Algorithm = Bitwarden.Opaque.KsfAlgorithm.Argon2id,
Parameters = new Bitwarden.Opaque.KsfParameters
Algorithm = KsfAlgorithm.Argon2id,
Parameters = new KsfParameters
{
Iterations = Argon2Parameters.Iterations,
Memory = Argon2Parameters.Memory,

View File

@ -4,12 +4,57 @@ using Bit.Core.Entities;
namespace Bit.Core.Auth.Services;
/// <summary>
/// Service that exposes methods enabling the use of the Opaque Key Exchange extension.
/// </summary>
public interface IOpaqueKeyExchangeService
{
public Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration);
/// <summary>
/// Begin registering a user's Opaque Key Exchange Credential. We write to the distributed cache so since there is some back and forth between the client and server.
/// </summary>
/// <param name="request">unsure what this byte array is for.</param>
/// <param name="user">user being acted on</param>
/// <param name="cipherConfiguration">configuration shared between the client and server to ensure the proper crypto-algorithms are being utilized.</param>
/// <returns>void</returns>
public Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, OpaqueKeyExchangeCipherConfiguration cipherConfiguration);
/// <summary>
/// This doesn't actually finish registration. It updates the cache with the server setup and cipher configuration so that the clearly named "SetActive" method can finish registration.
/// </summary>
/// <param name="sessionId">Cache Id</param>
/// <param name="registrationUpload">Byte Array for Rust Magic</param>
/// <param name="user">User being acted on</param>
/// <param name="keyset">Key Pair that can be used for vault decryption</param>
/// <returns>void</returns>
public Task FinishRegistration(Guid sessionId, byte[] registrationUpload, User user, RotateableOpaqueKeyset keyset);
/// <summary>
/// Returns server crypto material for the client to consume and reply with a login request to the identity/token endpoint.
/// To protect against account enumeration we will always return a deterministic response based on the user's email.
/// </summary>
/// <param name="request">client crypto material</param>
/// <param name="email">user email trying to login</param>
/// <returns>tuple(login SessionId for cache lookup, Server crypto material)</returns>
public Task<(Guid, byte[])> StartLogin(byte[] request, string email);
/// <summary>
/// Accepts the client's login request and validates it against the server's crypto material. If successful then the user is logged in.
/// If using a fake account we will return a standard failed login. If the account does have a legitimate credential but is still invalid
/// we will return a failed login.
/// </summary>
/// <param name="sessionId"></param>
/// <param name="finishCredential"></param>
/// <returns></returns>
public Task<bool> FinishLogin(Guid sessionId, byte[] finishCredential);
/// <summary>
/// This is where registration really finishes. This method writes the Credential to the database. If a credential already exists then it will be removed before the new one is added.
/// A user can only have one credential.
/// </summary>
/// <param name="sessionId">cache value</param>
/// <param name="user">user being acted on</param>
/// <returns>void</returns>
public Task SetActive(Guid sessionId, User user);
/// <summary>
/// Removes the credential for the user.
/// </summary>
/// <param name="user">user being acted on</param>
/// <returns>void</returns>
public Task Unenroll(User user);
}

View File

@ -36,12 +36,12 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
_userRepository = userRepository;
}
public async Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, Models.Api.Request.Opaque.CipherConfiguration cipherConfiguration)
public async Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, OpaqueKeyExchangeCipherConfiguration cipherConfiguration)
{
var registrationRequest = _bitwardenOpaque.StartRegistration(cipherConfiguration.ToNativeConfiguration(), null, request, user.Id.ToString());
var sessionId = Guid.NewGuid();
var registerSession = new RegisterSession() { SessionId = sessionId, ServerSetup = registrationRequest.serverSetup, CipherConfiguration = cipherConfiguration, UserId = user.Id };
var registerSession = new OpaqueKeyExchangeRegisterSession() { SessionId = sessionId, ServerSetup = registrationRequest.serverSetup, CipherConfiguration = cipherConfiguration, UserId = user.Id };
await _distributedCache.SetAsync(string.Format(REGISTER_SESSION_KEY, sessionId), Encoding.ASCII.GetBytes(JsonSerializer.Serialize(registerSession)));
return new OpaqueRegistrationStartResponse(sessionId, Convert.ToBase64String(registrationRequest.registrationResponse));
@ -57,7 +57,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
try
{
var registerSession = JsonSerializer.Deserialize<RegisterSession>(Encoding.ASCII.GetString(serializedRegisterSession))!;
var registerSession = JsonSerializer.Deserialize<OpaqueKeyExchangeRegisterSession>(Encoding.ASCII.GetString(serializedRegisterSession))!;
var registrationFinish = _bitwardenOpaque.FinishRegistration(registerSession.CipherConfiguration.ToNativeConfiguration(), registrationUpload);
registerSession.PasswordFile = registrationFinish.serverRegistration;
registerSession.KeySet = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(keyset));
@ -86,14 +86,18 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
throw new InvalidOperationException("Credential not found");
}
var cipherConfiguration = JsonSerializer.Deserialize<Models.Api.Request.Opaque.CipherConfiguration>(credential.CipherConfiguration)!;
var cipherConfiguration = JsonSerializer.Deserialize<OpaqueKeyExchangeCipherConfiguration>(credential.CipherConfiguration)!;
var credentialBlob = JsonSerializer.Deserialize<OpaqueKeyExchangeCredentialBlob>(credential.CredentialBlob)!;
var serverSetup = credentialBlob.ServerSetup;
var serverRegistration = credentialBlob.PasswordFile;
var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration.ToNativeConfiguration(), serverSetup, serverRegistration, request, user.Id.ToString());
var sessionId = Guid.NewGuid();
var loginSession = new LoginSession() { UserId = user.Id, LoginState = loginResponse.state, CipherConfiguration = cipherConfiguration };
var loginSession = new OpaqueKeyExchangeLoginSession() {
UserId = user.Id,
LoginState = loginResponse.state,
CipherConfiguration = cipherConfiguration
};
await _distributedCache.SetAsync(string.Format(LOGIN_SESSION_KEY, sessionId), Encoding.ASCII.GetBytes(JsonSerializer.Serialize(loginSession)));
return (sessionId, loginResponse.credentialResponse);
}
@ -105,7 +109,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
{
throw new InvalidOperationException("Session not found");
}
var loginSession = JsonSerializer.Deserialize<LoginSession>(Encoding.ASCII.GetString(serializedLoginSession))!;
var loginSession = JsonSerializer.Deserialize<OpaqueKeyExchangeLoginSession>(Encoding.ASCII.GetString(serializedLoginSession))!;
var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(loginSession.UserId);
if (credential == null)
@ -137,7 +141,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
{
throw new InvalidOperationException("Session not found");
}
var session = JsonSerializer.Deserialize<RegisterSession>(Encoding.ASCII.GetString(serializedRegisterSession))!;
var session = JsonSerializer.Deserialize<OpaqueKeyExchangeRegisterSession>(Encoding.ASCII.GetString(serializedRegisterSession))!;
if (session.UserId != user.Id)
{
@ -184,19 +188,19 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
}
}
public class RegisterSession
public class OpaqueKeyExchangeRegisterSession
{
public required Guid SessionId { get; set; }
public required byte[] ServerSetup { get; set; }
public required Models.Api.Request.Opaque.CipherConfiguration CipherConfiguration { get; set; }
public required OpaqueKeyExchangeCipherConfiguration CipherConfiguration { get; set; }
public required Guid UserId { get; set; }
public byte[]? PasswordFile { get; set; }
public byte[]? KeySet { get; set; }
}
public class LoginSession
public class OpaqueKeyExchangeLoginSession
{
public required Guid UserId { get; set; }
public required byte[] LoginState { get; set; }
public required Models.Api.Request.Opaque.CipherConfiguration CipherConfiguration { get; set; }
public required OpaqueKeyExchangeCipherConfiguration CipherConfiguration { get; set; }
}

View File

@ -816,7 +816,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.ForcePasswordReset = true;
user.Key = key;
// TODO: Add support
// TODO: Add Opaque-KE support
await _opaqueKeyExchangeService.Unenroll(user);
await _userRepository.ReplaceAsync(user);
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
@ -844,7 +844,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.Key = key;
user.MasterPasswordHint = hint;
// TODO: Add support
// TODO: Add Opaque-KE support
await _opaqueKeyExchangeService.Unenroll(user);
await _userRepository.ReplaceAsync(user);
await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name);

View File

@ -1,5 +1,7 @@
using System.Diagnostics;
using System.Text;
using Bit.Api.Auth.Models.Request.Opaque;
using Bit.Api.Auth.Models.Response.Opaque;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Auth.Enums;
@ -48,6 +50,7 @@ public class AccountsController : Controller
private readonly IReferenceEventService _referenceEventService;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService;
private readonly IOpaqueKeyExchangeCredentialRepository _opaqueKeyExchangeCredentialRepository;
private readonly byte[] _defaultKdfHmacKey = null;
@ -98,6 +101,7 @@ public class AccountsController : Controller
IFeatureService featureService,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
IOpaqueKeyExchangeCredentialRepository opaqueKeyExchangeCredentialRepository,
IOpaqueKeyExchangeService opaqueKeyExchangeService,
GlobalSettings globalSettings
)
{
@ -112,6 +116,7 @@ public class AccountsController : Controller
_referenceEventService = referenceEventService;
_featureService = featureService;
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
_opaqueKeyExchangeService = opaqueKeyExchangeService;
_opaqueKeyExchangeCredentialRepository = opaqueKeyExchangeCredentialRepository;
if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey))
@ -204,33 +209,27 @@ public class AccountsController : Controller
model.EmailVerificationToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.OrganizationInvite:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationInviteToken(user, model.MasterPasswordHash,
model.OrgInviteToken, model.OrganizationUserId);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.OrgSponsoredFreeFamilyPlan:
identityResult = await _registerUserCommand.RegisterUserViaOrganizationSponsoredFreeFamilyPlanInviteToken(user, model.MasterPasswordHash, model.OrgSponsoredFreeFamilyPlanToken);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.EmergencyAccessInvite:
Debug.Assert(model.AcceptEmergencyAccessId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaAcceptEmergencyAccessInviteToken(user, model.MasterPasswordHash,
model.AcceptEmergencyAccessInviteToken, model.AcceptEmergencyAccessId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
case RegisterFinishTokenType.ProviderInvite:
Debug.Assert(model.ProviderUserId.HasValue);
identityResult = await _registerUserCommand.RegisterUserViaProviderInviteToken(user, model.MasterPasswordHash,
model.ProviderInviteToken, model.ProviderUserId.Value);
return await ProcessRegistrationResult(identityResult, user, delaysEnabled);
break;
default:
throw new BadRequestException("Invalid registration finish request");
}
@ -270,7 +269,7 @@ public class AccountsController : Controller
var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id);
if (credential != null)
{
return new PreloginResponseModel(kdfInformation, JsonSerializer.Deserialize<CipherConfiguration>(credential.CipherConfiguration)!);
return new PreloginResponseModel(kdfInformation, JsonSerializer.Deserialize<OpaqueKeyExchangeCipherConfiguration>(credential.CipherConfiguration)!);
}
else
{
@ -293,6 +292,14 @@ public class AccountsController : Controller
};
}
[HttpPost("opaque-ke/start-login")]
[RequireFeature(FeatureFlagKeys.OpaqueKeyExchange)]
public async Task<OpaqueLoginStartResponse> GetOpaqueKeyExchangeStartLoginMaterial([FromBody] OpaqueLoginStartRequest request)
{
var result = await _opaqueKeyExchangeService.StartLogin(Convert.FromBase64String(request.CredentialRequest), request.Email);
return new OpaqueLoginStartResponse(result.Item1, Convert.ToBase64String(result.Item2));
}
private UserKdfInformation GetDefaultKdf(string email)
{
if (_defaultKdfHmacKey == null)

View File

@ -6,7 +6,7 @@ namespace Bit.Identity.Models.Response.Accounts;
public class PreloginResponseModel
{
public PreloginResponseModel(UserKdfInformation kdfInformation, CipherConfiguration opaqueConfiguration)
public PreloginResponseModel(UserKdfInformation kdfInformation, OpaqueKeyExchangeCipherConfiguration opaqueConfiguration)
{
Kdf = kdfInformation.Kdf;
KdfIterations = kdfInformation.KdfIterations;
@ -19,5 +19,5 @@ public class PreloginResponseModel
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
public CipherConfiguration OpaqueConfiguration { get; set; }
public OpaqueKeyExchangeCipherConfiguration OpaqueConfiguration { get; set; }
}

View File

@ -25,8 +25,7 @@ public class OpaqueKeyExchangeCredentialRepository : Repository<OpaqueKeyExchang
public async Task<OpaqueKeyExchangeCredential?> GetByUserIdAsync(Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
using var connection = new SqlConnection(ConnectionString);
var results = await connection.QueryAsync<OpaqueKeyExchangeCredential>(
$"[{Schema}].[{Table}_ReadByUserId]",
new { UserId = userId },
@ -34,7 +33,6 @@ public class OpaqueKeyExchangeCredentialRepository : Repository<OpaqueKeyExchang
return results.FirstOrDefault();
}
}
// TODO - How do we want to handle rotation?
public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<OpaqueKeyExchangeRotateKeyData> credentials)

View File

@ -8,7 +8,7 @@ CREATE TABLE [dbo].[OpaqueKeyExchangeCredential]
[EncryptedPrivateKey] VARCHAR(MAX) NOT NULL,
[EncryptedUserKey] VARCHAR(MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([UserId]), -- using this as the primary key ensure users only have one credential
CONSTRAINT [FK_OpaqueKeyExchangeCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
);