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.Models.Api.Response.Opaque;
using Bit.Core.Auth.Services; using Bit.Core.Auth.Services;
using Bit.Core.Services; using Bit.Core.Services;
@ -42,15 +44,15 @@ public class OpaqueKeyExchangeController : Controller
// TODO: Remove and move to token endpoint // TODO: Remove and move to token endpoint
[HttpPost("~/opaque/start-login")] [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); 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 // TODO: Remove and move to token endpoint
[HttpPost("~/opaque/finish-login")] [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)); var result = await _opaqueKeyExchangeService.FinishLogin(request.SessionId, Convert.FromBase64String(request.CredentialFinalization));
return result; return result;

View File

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

View File

@ -4,12 +4,57 @@ using Bit.Core.Entities;
namespace Bit.Core.Auth.Services; namespace Bit.Core.Auth.Services;
/// <summary>
/// Service that exposes methods enabling the use of the Opaque Key Exchange extension.
/// </summary>
public interface IOpaqueKeyExchangeService 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); 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); 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); 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); 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); public Task Unenroll(User user);
} }

View File

@ -36,12 +36,12 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
_userRepository = userRepository; _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 registrationRequest = _bitwardenOpaque.StartRegistration(cipherConfiguration.ToNativeConfiguration(), null, request, user.Id.ToString());
var sessionId = Guid.NewGuid(); 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))); await _distributedCache.SetAsync(string.Format(REGISTER_SESSION_KEY, sessionId), Encoding.ASCII.GetBytes(JsonSerializer.Serialize(registerSession)));
return new OpaqueRegistrationStartResponse(sessionId, Convert.ToBase64String(registrationRequest.registrationResponse)); return new OpaqueRegistrationStartResponse(sessionId, Convert.ToBase64String(registrationRequest.registrationResponse));
@ -57,7 +57,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
try 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); var registrationFinish = _bitwardenOpaque.FinishRegistration(registerSession.CipherConfiguration.ToNativeConfiguration(), registrationUpload);
registerSession.PasswordFile = registrationFinish.serverRegistration; registerSession.PasswordFile = registrationFinish.serverRegistration;
registerSession.KeySet = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(keyset)); registerSession.KeySet = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(keyset));
@ -86,14 +86,18 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
throw new InvalidOperationException("Credential not found"); 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 credentialBlob = JsonSerializer.Deserialize<OpaqueKeyExchangeCredentialBlob>(credential.CredentialBlob)!;
var serverSetup = credentialBlob.ServerSetup; var serverSetup = credentialBlob.ServerSetup;
var serverRegistration = credentialBlob.PasswordFile; var serverRegistration = credentialBlob.PasswordFile;
var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration.ToNativeConfiguration(), serverSetup, serverRegistration, request, user.Id.ToString()); var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration.ToNativeConfiguration(), serverSetup, serverRegistration, request, user.Id.ToString());
var sessionId = Guid.NewGuid(); 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))); await _distributedCache.SetAsync(string.Format(LOGIN_SESSION_KEY, sessionId), Encoding.ASCII.GetBytes(JsonSerializer.Serialize(loginSession)));
return (sessionId, loginResponse.credentialResponse); return (sessionId, loginResponse.credentialResponse);
} }
@ -105,7 +109,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
{ {
throw new InvalidOperationException("Session not found"); 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); var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(loginSession.UserId);
if (credential == null) if (credential == null)
@ -137,7 +141,7 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
{ {
throw new InvalidOperationException("Session not found"); 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) 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 Guid SessionId { get; set; }
public required byte[] ServerSetup { 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 required Guid UserId { get; set; }
public byte[]? PasswordFile { get; set; } public byte[]? PasswordFile { get; set; }
public byte[]? KeySet { get; set; } public byte[]? KeySet { get; set; }
} }
public class LoginSession public class OpaqueKeyExchangeLoginSession
{ {
public required Guid UserId { get; set; } public required Guid UserId { get; set; }
public required byte[] LoginState { 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.ForcePasswordReset = true;
user.Key = key; user.Key = key;
// TODO: Add support // TODO: Add Opaque-KE support
await _opaqueKeyExchangeService.Unenroll(user); await _opaqueKeyExchangeService.Unenroll(user);
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName()); await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
@ -844,7 +844,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.Key = key; user.Key = key;
user.MasterPasswordHint = hint; user.MasterPasswordHint = hint;
// TODO: Add support // TODO: Add Opaque-KE support
await _opaqueKeyExchangeService.Unenroll(user); await _opaqueKeyExchangeService.Unenroll(user);
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name); await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name);

View File

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

View File

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

View File

@ -25,15 +25,13 @@ public class OpaqueKeyExchangeCredentialRepository : Repository<OpaqueKeyExchang
public async Task<OpaqueKeyExchangeCredential?> GetByUserIdAsync(Guid userId) 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>(
var results = await connection.QueryAsync<OpaqueKeyExchangeCredential>( $"[{Schema}].[{Table}_ReadByUserId]",
$"[{Schema}].[{Table}_ReadByUserId]", new { UserId = userId },
new { UserId = userId }, commandType: CommandType.StoredProcedure);
commandType: CommandType.StoredProcedure);
return results.FirstOrDefault(); return results.FirstOrDefault();
}
} }
// TODO - How do we want to handle rotation? // TODO - How do we want to handle rotation?

View File

@ -8,7 +8,7 @@ CREATE TABLE [dbo].[OpaqueKeyExchangeCredential]
[EncryptedPrivateKey] VARCHAR(MAX) NOT NULL, [EncryptedPrivateKey] VARCHAR(MAX) NOT NULL,
[EncryptedUserKey] VARCHAR(MAX) NULL, [EncryptedUserKey] VARCHAR(MAX) NULL,
[CreationDate] DATETIME2 (7) NOT 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]) CONSTRAINT [FK_OpaqueKeyExchangeCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
); );