1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-06 21:48:12 -05:00

Innovation/pm 18992/add credential table (#5499)

* feat(OPAQUE-KE): added entity

* innovation(opaque-ke) : inital database changes

* innovation(opaque-ke) : dapper implementation. Key rotation WIP.

* Updating credential repository

* feat : updating service to use repository to save credential

* Fix table creation and make registration work

---------

Co-authored-by: Bernd Schoolmann <mail@quexten.com>
This commit is contained in:
Ike 2025-03-17 08:48:30 -04:00 committed by GitHub
parent d617004435
commit b03e3c3b8c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 554 additions and 117 deletions

View File

@ -1,5 +1,5 @@
using Bit.Api.Auth.Models.Request.Opaque; using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Api.Auth.Models.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;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -12,7 +12,7 @@ namespace Bit.Api.Auth.Controllers;
public class OpaqueKeyExchangeController : Controller public class OpaqueKeyExchangeController : Controller
{ {
private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService; private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService;
IUserService _userService; private readonly IUserService _userService;
public OpaqueKeyExchangeController( public OpaqueKeyExchangeController(
IOpaqueKeyExchangeService opaqueKeyExchangeService, IOpaqueKeyExchangeService opaqueKeyExchangeService,
@ -27,8 +27,8 @@ public class OpaqueKeyExchangeController : Controller
public async Task<OpaqueRegistrationStartResponse> StartRegistrationAsync([FromBody] OpaqueRegistrationStartRequest request) public async Task<OpaqueRegistrationStartResponse> StartRegistrationAsync([FromBody] OpaqueRegistrationStartRequest request)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
var result = await _opaqueKeyExchangeService.StartRegistration(Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration.ToNativeConfiguration()); var result = await _opaqueKeyExchangeService.StartRegistration(Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration);
return new OpaqueRegistrationStartResponse(result.Item1, Convert.ToBase64String(result.Item2)); return result;
} }
@ -36,25 +36,23 @@ public class OpaqueKeyExchangeController : Controller
public async void FinishRegistrationAsync([FromBody] OpaqueRegistrationFinishRequest request) public async void FinishRegistrationAsync([FromBody] OpaqueRegistrationFinishRequest request)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); var user = await _userService.GetUserByPrincipalAsync(User);
_opaqueKeyExchangeService.FinishRegistration(request.SessionId, Convert.FromBase64String(request.RegistrationUpload), user); await _opaqueKeyExchangeService.FinishRegistration(request.SessionId, Convert.FromBase64String(request.RegistrationUpload), user, request.KeySet);
} }
// 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<OpaqueLoginStartResponse> StartLoginAsync([FromBody] OpaqueLoginStartRequest request) public async Task<Models.Response.Opaque.OpaqueLoginStartResponse> StartLoginAsync([FromBody] Models.Request.Opaque.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 OpaqueLoginStartResponse(result.Item1, Convert.ToBase64String(result.Item2)); return new Models.Response.Opaque.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] OpaqueLoginFinishRequest request) public async Task<bool> FinishLoginAsync([FromBody] Models.Request.Opaque.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

@ -0,0 +1,39 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;
namespace Bit.Core.Auth.Entities;
public class OpaqueKeyExchangeCredential : ITableObject<Guid>
{
/// <summary>
/// Identity column
/// </summary>
public Guid Id { get; set; }
/// <summary>
/// User who owns the credential
/// </summary>
public Guid UserId { get; set; }
/// <summary>
/// This describes the cipher configuration that both the server and client know.
/// This is returned on the /prelogin api call for the user.
/// </summary>
public string CipherConfiguration { get; set; }
/// <summary>
/// This contains Credential specific information. Storing as a blob gives us flexibility for future
/// iterations of the specifics of the OPAQUE implementation.
/// </summary>
public string CredentialBlob { get; set; }
public string EncryptedPublicKey { get; set; }
public string EncryptedPrivateKey { get; set; }
public string EncryptedUserKey { get; set; }
/// <summary>
/// Date credential was created. When we update we are creating a new key set so in effect we are creating a new credential.
/// </summary>
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Auth.Models.Request.Opaque; namespace Bit.Core.Auth.Models.Api.Request.Opaque;
public class OpaqueRegistrationFinishRequest public class OpaqueRegistrationFinishRequest
{ {
@ -9,10 +9,10 @@ public class OpaqueRegistrationFinishRequest
[Required] [Required]
public Guid SessionId { get; set; } public Guid SessionId { get; set; }
public RotateableKeyset KeySet { get; set; } public RotateableOpaqueKeyset KeySet { get; set; }
} }
public class RotateableKeyset public class RotateableOpaqueKeyset
{ {
[Required] [Required]
public string EncryptedUserKey { get; set; } public string EncryptedUserKey { get; set; }

View File

@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Auth.Models.Request.Opaque; namespace Bit.Core.Auth.Models.Api.Request.Opaque;
public class OpaqueRegistrationStartRequest public class OpaqueRegistrationStartRequest
@ -17,6 +17,7 @@ public class CipherConfiguration
[Required] [Required]
public string CipherSuite { get; set; } public string CipherSuite { get; set; }
[Required]
public Argon2KsfParameters Argon2Parameters { get; set; } public Argon2KsfParameters Argon2Parameters { get; set; }
public Bitwarden.OPAQUE.CipherConfiguration ToNativeConfiguration() public Bitwarden.OPAQUE.CipherConfiguration ToNativeConfiguration()
@ -28,7 +29,7 @@ public class CipherConfiguration
OprfCS = Bitwarden.OPAQUE.OprfCS.Ristretto255, OprfCS = Bitwarden.OPAQUE.OprfCS.Ristretto255,
KeGroup = Bitwarden.OPAQUE.KeGroup.Ristretto255, KeGroup = Bitwarden.OPAQUE.KeGroup.Ristretto255,
KeyExchange = Bitwarden.OPAQUE.KeyExchange.TripleDH, KeyExchange = Bitwarden.OPAQUE.KeyExchange.TripleDH,
KSF = new Bitwarden.OPAQUE.Argon2id(Argon2Parameters.iterations, Argon2Parameters.memory, Argon2Parameters.parallelism) KSF = new Bitwarden.OPAQUE.Argon2id(Argon2Parameters.Iterations, Argon2Parameters.Memory, Argon2Parameters.Parallelism)
}; };
} }
else else
@ -42,9 +43,9 @@ public class Argon2KsfParameters
{ {
// Memory in KiB // Memory in KiB
[Required] [Required]
public int memory; public int Memory { get; set; }
[Required] [Required]
public int iterations; public int Iterations { get; set; }
[Required] [Required]
public int parallelism; public int Parallelism { get; set; }
} }

View File

@ -1,6 +1,6 @@
using Bit.Core.Models.Api; using Bit.Core.Models.Api;
namespace Bit.Api.Auth.Models.Response.Opaque; namespace Bit.Core.Auth.Models.Api.Response.Opaque;
public class OpaqueRegistrationStartResponse : ResponseModel public class OpaqueRegistrationStartResponse : ResponseModel
{ {
@ -11,7 +11,7 @@ public class OpaqueRegistrationStartResponse : ResponseModel
SessionId = sessionId; SessionId = sessionId;
} }
public String RegistrationResponse { get; set; } public string RegistrationResponse { get; set; }
public Guid SessionId { get; set; } public Guid SessionId { get; set; }
} }

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Auth.Models.Data;
public class OpaqueKeyExchangeCredentialBlob
{
public byte[] PasswordFile { get; set; }
public byte[] ServerSetup { get; set; }
}

View File

@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Auth.Models.Data;
// TODO implement
public class OpaqueKeyExchangeRotateKeyData
{
[Required]
public Guid Id { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedUserKey { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(2000)]
public string EncryptedPublicKey { get; set; }
}

View File

@ -0,0 +1,15 @@
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Repositories;
#nullable enable
namespace Bit.Core.Auth.Repositories;
public interface IOpaqueKeyExchangeCredentialRepository : IRepository<OpaqueKeyExchangeCredential, Guid>
{
Task<OpaqueKeyExchangeCredential?> GetByUserIdAsync(Guid userId);
//TODO implement rotation
UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<OpaqueKeyExchangeRotateKeyData> credentials);
}

View File

@ -1,14 +1,15 @@
using Bit.Core.Entities; using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bitwarden.OPAQUE; using Bit.Core.Auth.Models.Api.Response.Opaque;
using Bit.Core.Entities;
namespace Bit.Core.Auth.Services; namespace Bit.Core.Auth.Services;
public interface IOpaqueKeyExchangeService public interface IOpaqueKeyExchangeService
{ {
public Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration); public Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration);
public void FinishRegistration(Guid sessionId, byte[] registrationUpload, User user); public Task FinishRegistration(Guid sessionId, byte[] registrationUpload, User user, RotateableOpaqueKeyset keyset);
public Task<(Guid, byte[])> StartLogin(byte[] request, string email); public Task<(Guid, byte[])> StartLogin(byte[] request, string email);
public Task<bool> FinishLogin(Guid sessionId, byte[] finishCredential); public Task<bool> FinishLogin(Guid sessionId, byte[] finishCredential);
public void SetActive(Guid sessionId, User user); public Task SetActive(Guid sessionId, User user);
public void Unenroll(User user); public Task Unenroll(User user);
} }

View File

@ -1,5 +1,14 @@
using Bit.Core.Entities; using System.Text;
using System.Text.Json;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Core.Auth.Models.Api.Response.Opaque;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bitwarden.OPAQUE; using Bitwarden.OPAQUE;
using Microsoft.Extensions.Caching.Distributed;
namespace Bit.Core.Auth.Services; namespace Bit.Core.Auth.Services;
@ -7,36 +16,48 @@ namespace Bit.Core.Auth.Services;
public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
{ {
private readonly BitwardenOpaqueServer _bitwardenOpaque; private readonly BitwardenOpaqueServer _bitwardenOpaque;
private readonly IOpaqueKeyExchangeCredentialRepository _opaqueKeyExchangeCredentialRepository;
private readonly IDistributedCache _distributedCache;
private readonly IUserRepository _userRepository;
public OpaqueKeyExchangeService( public OpaqueKeyExchangeService(
) IOpaqueKeyExchangeCredentialRepository opaqueKeyExchangeCredentialRepository,
IDistributedCache distributedCache,
IUserRepository userRepository
)
{ {
_bitwardenOpaque = new BitwardenOpaqueServer(); _bitwardenOpaque = new BitwardenOpaqueServer();
_opaqueKeyExchangeCredentialRepository = opaqueKeyExchangeCredentialRepository;
_distributedCache = distributedCache;
_userRepository = userRepository;
} }
public async Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, Models.Api.Request.Opaque.CipherConfiguration cipherConfiguration)
public async Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration)
{ {
return await Task.Run(() => return await Task.Run(() =>
{ {
var registrationResponse = _bitwardenOpaque.StartRegistration(cipherConfiguration, null, request, user.Id.ToString()); var registrationRequest = _bitwardenOpaque.StartRegistration(cipherConfiguration.ToNativeConfiguration(), null, request, user.Id.ToString());
var registrationReseponse = registrationRequest.registrationResponse;
var serverSetup = registrationRequest.serverSetup;
// persist server setup
var sessionId = Guid.NewGuid(); var sessionId = Guid.NewGuid();
SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { sessionId = sessionId, serverSetup = registrationResponse.serverSetup, cipherConfiguration = cipherConfiguration, userId = user.Id }); SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { SessionId = sessionId, ServerSetup = serverSetup, CipherConfiguration = cipherConfiguration, UserId = user.Id });
return (sessionId, registrationResponse.registrationResponse); return new OpaqueRegistrationStartResponse(sessionId, Convert.ToBase64String(registrationReseponse));
}); });
} }
public async void FinishRegistration(Guid sessionId, byte[] registrationUpload, User user) public async Task FinishRegistration(Guid sessionId, byte[] registrationUpload, User user, RotateableOpaqueKeyset keyset)
{ {
await Task.Run(() => await Task.Run(() =>
{ {
var cipherConfiguration = SessionStore.RegisterSessions[sessionId].cipherConfiguration; var cipherConfiguration = SessionStore.RegisterSessions[sessionId].CipherConfiguration;
try try
{ {
var registrationFinish = _bitwardenOpaque.FinishRegistration(cipherConfiguration, registrationUpload); var registrationFinish = _bitwardenOpaque.FinishRegistration(cipherConfiguration.ToNativeConfiguration(), registrationUpload);
SessionStore.RegisterSessions[sessionId].serverRegistration = registrationFinish.serverRegistration; SessionStore.RegisterSessions[sessionId].PasswordFile = registrationFinish.serverRegistration;
SessionStore.RegisterSessions[sessionId].KeySet = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(keyset));
} }
catch (Exception e) catch (Exception e)
{ {
@ -48,109 +69,130 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
public async Task<(Guid, byte[])> StartLogin(byte[] request, string email) public async Task<(Guid, byte[])> StartLogin(byte[] request, string email)
{ {
return await Task.Run(() => var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
{ {
var credential = PersistentStore.Credentials.First(x => x.Value.email == email); // todo don't allow user enumeration
if (credential.Value == null) throw new InvalidOperationException("User not found");
{ }
// generate fake credential
throw new InvalidOperationException("User not found");
}
var cipherConfiguration = credential.Value.cipherConfiguration; var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id);
var serverSetup = credential.Value.serverSetup; if (credential == null)
var serverRegistration = credential.Value.serverRegistration; {
// generate fake credential
throw new InvalidOperationException("Credential not found");
}
var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration, serverSetup, serverRegistration, request, credential.Key.ToString()); var cipherConfiguration = JsonSerializer.Deserialize<Models.Api.Request.Opaque.CipherConfiguration>(credential.CipherConfiguration)!;
var sessionId = Guid.NewGuid(); var credentialBlob = JsonSerializer.Deserialize<OpaqueKeyExchangeCredentialBlob>(credential.CredentialBlob)!;
SessionStore.LoginSessions.Add(sessionId, new LoginSession() { userId = credential.Key, loginState = loginResponse.state }); var serverSetup = credentialBlob.ServerSetup;
return (sessionId, loginResponse.credentialResponse); var serverRegistration = credentialBlob.PasswordFile;
});
var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration.ToNativeConfiguration(), serverSetup, serverRegistration, request, user.Id.ToString());
var sessionId = Guid.NewGuid();
SessionStore.LoginSessions.Add(sessionId, new LoginSession() { UserId = user.Id, LoginState = loginResponse.state, CipherConfiguration = cipherConfiguration });
return (sessionId, loginResponse.credentialResponse);
} }
public async Task<bool> FinishLogin(Guid sessionId, byte[] credentialFinalization) public async Task<bool> FinishLogin(Guid sessionId, byte[] credentialFinalization)
{ {
return await Task.Run(() => return await Task.Run(async () =>
{ {
if (!SessionStore.LoginSessions.ContainsKey(sessionId)) if (!SessionStore.LoginSessions.ContainsKey(sessionId))
{ {
throw new InvalidOperationException("Session not found"); throw new InvalidOperationException("Session not found");
} }
var credential = PersistentStore.Credentials[SessionStore.LoginSessions[sessionId].userId];
var userId = SessionStore.LoginSessions[sessionId].UserId;
var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(userId);
if (credential == null) if (credential == null)
{ {
throw new InvalidOperationException("User not found"); throw new InvalidOperationException("Credential not found");
} }
var loginState = SessionStore.LoginSessions[sessionId].loginState; var loginState = SessionStore.LoginSessions[sessionId].LoginState;
var cipherConfiguration = credential.cipherConfiguration; var cipherConfiguration = SessionStore.LoginSessions[sessionId].CipherConfiguration;
SessionStore.LoginSessions.Remove(sessionId);
try try
{ {
var success = _bitwardenOpaque.FinishLogin(cipherConfiguration, loginState, credentialFinalization); var success = _bitwardenOpaque.FinishLogin(cipherConfiguration.ToNativeConfiguration(), loginState, credentialFinalization);
SessionStore.LoginSessions.Remove(sessionId);
return true; return true;
} }
catch (Exception e) catch (Exception e)
{ {
// print // print
Console.WriteLine(e.Message); Console.WriteLine(e.Message);
SessionStore.LoginSessions.Remove(sessionId);
return false; return false;
} }
}); });
} }
public async void SetActive(Guid sessionId, User user) public async Task SetActive(Guid sessionId, User user)
{ {
await Task.Run(() => var session = SessionStore.RegisterSessions[sessionId];
SessionStore.RegisterSessions.Remove(sessionId);
if (session.UserId != user.Id)
{ {
var session = SessionStore.RegisterSessions[sessionId]; throw new InvalidOperationException("Session does not belong to user");
if (session.userId != user.Id) }
{ if (session.PasswordFile == null)
throw new InvalidOperationException("Session does not belong to user"); {
} throw new InvalidOperationException("Session did not complete registration");
if (session.serverRegistration == null) }
{ if (session.KeySet == null)
throw new InvalidOperationException("Session did not complete registration"); {
} throw new InvalidOperationException("Session did not complete registration");
SessionStore.RegisterSessions.Remove(sessionId); }
// to be copied to the persistent DB var keyset = JsonSerializer.Deserialize<RotateableOpaqueKeyset>(Encoding.ASCII.GetString(session.KeySet))!;
var cipherConfiguration = session.cipherConfiguration; var credentialBlob = new OpaqueKeyExchangeCredentialBlob()
var serverRegistration = session.serverRegistration; {
var serverSetup = session.serverSetup; PasswordFile = session.PasswordFile,
ServerSetup = session.ServerSetup
};
if (PersistentStore.Credentials.ContainsKey(user.Id)) var credential = new OpaqueKeyExchangeCredential()
{ {
PersistentStore.Credentials.Remove(user.Id); UserId = user.Id,
} CipherConfiguration = JsonSerializer.Serialize(session.CipherConfiguration),
PersistentStore.Credentials.Add(user.Id, new Credential() { serverRegistration = serverRegistration, serverSetup = serverSetup, cipherConfiguration = cipherConfiguration, email = user.Email }); CredentialBlob = JsonSerializer.Serialize(credentialBlob),
}); EncryptedPrivateKey = keyset.EncryptedPrivateKey,
EncryptedPublicKey = keyset.EncryptedPublicKey,
EncryptedUserKey = keyset.EncryptedUserKey,
CreationDate = DateTime.UtcNow
};
await Unenroll(user);
await _opaqueKeyExchangeCredentialRepository.CreateAsync(credential);
} }
public async void Unenroll(User user) public async Task Unenroll(User user)
{ {
await Task.Run(() => var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id);
if (credential != null)
{ {
PersistentStore.Credentials.Remove(user.Id); await _opaqueKeyExchangeCredentialRepository.DeleteAsync(credential);
}); }
} }
} }
public class RegisterSession public class RegisterSession
{ {
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 CipherConfiguration cipherConfiguration { get; set; } public required Models.Api.Request.Opaque.CipherConfiguration CipherConfiguration { get; set; }
public required Guid userId { get; set; } public required Guid UserId { get; set; }
public byte[]? serverRegistration { get; set; } public byte[]? PasswordFile { get; set; }
public byte[]? KeySet { get; set; }
} }
public class LoginSession public class LoginSession
{ {
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 class SessionStore() public class SessionStore()
@ -158,16 +200,3 @@ public class SessionStore()
public static Dictionary<Guid, RegisterSession> RegisterSessions = new Dictionary<Guid, RegisterSession>(); public static Dictionary<Guid, RegisterSession> RegisterSessions = new Dictionary<Guid, RegisterSession>();
public static Dictionary<Guid, LoginSession> LoginSessions = new Dictionary<Guid, LoginSession>(); public static Dictionary<Guid, LoginSession> LoginSessions = new Dictionary<Guid, LoginSession>();
} }
public class Credential
{
public required byte[] serverRegistration { get; set; }
public required byte[] serverSetup { get; set; }
public required CipherConfiguration cipherConfiguration { get; set; }
public required string email { get; set; }
}
public class PersistentStore()
{
public static Dictionary<Guid, Credential> Credentials = new Dictionary<Guid, Credential>();
}

View File

@ -670,11 +670,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
if (opaqueSessionId != null) if (opaqueSessionId != null)
{ {
_opaqueKeyExchangeService.SetActive((Guid)opaqueSessionId, user); await _opaqueKeyExchangeService.SetActive((Guid)opaqueSessionId, user);
} }
else else
{ {
_opaqueKeyExchangeService.Unenroll(user); await _opaqueKeyExchangeService.Unenroll(user);
} }
await _userRepository.ReplaceAsync(user); await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword); await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
@ -817,7 +817,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.Key = key; user.Key = key;
// TODO: Add support // TODO: Add support
_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());
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword); await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
@ -845,7 +845,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
user.MasterPasswordHint = hint; user.MasterPasswordHint = hint;
// TODO: Add support // TODO: Add support
_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);
await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword); await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);

View File

@ -0,0 +1,73 @@
using System.Data;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.KeyManagement.UserKey;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;
#nullable enable
namespace Bit.Infrastructure.Dapper.Auth.Repositories;
public class OpaqueKeyExchangeCredentialRepository : Repository<OpaqueKeyExchangeCredential, Guid>, IOpaqueKeyExchangeCredentialRepository
{
public OpaqueKeyExchangeCredentialRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ }
public OpaqueKeyExchangeCredentialRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{ }
public async Task<OpaqueKeyExchangeCredential?> GetByUserIdAsync(Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<OpaqueKeyExchangeCredential>(
$"[{Schema}].[{Table}_ReadByUserId]",
new { UserId = userId },
commandType: CommandType.StoredProcedure);
return results.FirstOrDefault();
}
}
// TODO - How do we want to handle rotation?
public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<OpaqueKeyExchangeRotateKeyData> credentials)
{
throw new NotImplementedException();
// return async (SqlConnection connection, SqlTransaction transaction) =>
// {
// const string sql = @"
// UPDATE WC
// SET
// WC.[EncryptedPublicKey] = UW.[encryptedPublicKey],
// WC.[EncryptedUserKey] = UW.[encryptedUserKey]
// FROM
// [dbo].[WebAuthnCredential] WC
// INNER JOIN
// OPENJSON(@JsonCredentials)
// WITH (
// id UNIQUEIDENTIFIER,
// encryptedPublicKey NVARCHAR(MAX),
// encryptedUserKey NVARCHAR(MAX)
// ) UW
// ON UW.id = WC.Id
// WHERE
// WC.[UserId] = @UserId";
// var jsonCredentials = CoreHelpers.ClassToJsonData(credentials);
// await connection.ExecuteAsync(
// sql,
// new { UserId = userId, JsonCredentials = jsonCredentials },
// transaction: transaction,
// commandType: CommandType.Text);
// };
}
}

View File

@ -65,7 +65,7 @@ public static class DapperServiceCollectionExtensions
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>(); services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>(); services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>(); services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IOpaqueKeyExchangeCredentialRepository, OpaqueKeyExchangeCredentialRepository>();
if (selfHosted) if (selfHosted)
{ {
services.AddSingleton<IEventRepository, EventRepository>(); services.AddSingleton<IEventRepository, EventRepository>();

View File

@ -0,0 +1,36 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@CipherConfiguration VARCHAR(MAX) NOT NULL,
@CredentialBlob VARCHAR(MAX) NOT NULL,
@EncryptedPublicKey VARCHAR(MAX) NOT NULL,
@EncryptedPrivateKey VARCHAR(MAX) NOT NULL,
@EncryptedUserKey VARCHAR(MAX) NOT NULL,
@CreationDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OpaqueKeyExchangeCredential]
(
[Id],
[UserId],
[CipherConfiguration],
[CredentialBlob],
[EncryptedPublicKey],
[EncryptedPrivateKey],
[EncryptedUserKey],
[CreationDate]
)
VALUES
(
@Id,
@UserId,
@CipherConfiguration,
@CredentialBlob,
@EncryptedPublicKey,
@EncryptedPrivateKey,
@EncryptedUserKey,
@CreationDate
)
END

View File

@ -0,0 +1,10 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_DeleteById]
@Id UNIQUEIDENTIFIER,
AS
BEGIN
DELETE
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[Id] = @Id
END

View File

@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadByUserId]
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[UserId] = @UserId
END

View File

@ -0,0 +1,26 @@
-- Used for Key Rotation and Password Update
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@CipherConfiguration VARCHAR(MAX),
@CredentialBlob VARCHAR(MAX),
@EncryptedPublicKey VARCHAR(MAX),
@EncryptedPrivateKey VARCHAR(MAX),
@EncryptedUserKey VARCHAR(MAX),
@CreationDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OpaqueKeyExchangeCredential]
SET
[CipherConfiguration] = @CipherConfiguration,
[CredentialBlob] = @CredentialBlob,
[EncryptedPublicKey] = @EncryptedPublicKey,
[EncryptedPrivateKey] = @EncryptedPrivateKey,
[EncryptedUserKey] = @EncryptedUserKey,
[CreationDate] = @CreationDate
WHERE
[Id] = @Id AND [UserId] = @UserId
END

View File

@ -0,0 +1,18 @@
CREATE TABLE [dbo].[OpaqueKeyExchangeCredential]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL,
[CipherConfiguration] VARCHAR(MAX) NOT NULL,
[CredentialBlob] VARCHAR(MAX) NOT NULL,
[EncryptedPublicKey] VARCHAR(MAX) NOT NULL,
[EncryptedPrivateKey] VARCHAR(MAX) NOT NULL,
[EncryptedUserKey] VARCHAR(MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OpaqueKeyExchangeCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_OpaqueKeyExchangeCredential_UserId]
ON [dbo].[OpaqueKeyExchangeCredential]([UserId] ASC);

View File

@ -1,6 +1,7 @@
CREATE PROCEDURE [dbo].[User_DeleteById] CREATE PROCEDURE [dbo].[User_DeleteById]
@Id UNIQUEIDENTIFIER @Id UNIQUEIDENTIFIER
WITH RECOMPILE WITH
RECOMPILE
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -23,6 +24,12 @@ BEGIN
END END
BEGIN TRANSACTION User_DeleteById BEGIN TRANSACTION User_DeleteById
-- Delete OpaqueKeyExchangeCredentials
DELETE
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[UserId] = @UserId
-- Delete WebAuthnCredentials -- Delete WebAuthnCredentials
DELETE DELETE
@ -42,7 +49,7 @@ BEGIN
DELETE DELETE
FROM FROM
[dbo].[AuthRequest] [dbo].[AuthRequest]
WHERE WHERE
[UserId] = @Id [UserId] = @Id
-- Delete devices -- Delete devices
@ -116,7 +123,7 @@ BEGIN
DELETE DELETE
FROM FROM
[dbo].[Send] [dbo].[Send]
WHERE WHERE
[UserId] = @Id [UserId] = @Id
-- Delete Notification Status -- Delete Notification Status
@ -132,7 +139,7 @@ BEGIN
[dbo].[Notification] [dbo].[Notification]
WHERE WHERE
[UserId] = @Id [UserId] = @Id
-- Finally, delete the user -- Finally, delete the user
DELETE DELETE
FROM FROM

View File

@ -0,0 +1,130 @@
CREATE TABLE [dbo].[OpaqueKeyExchangeCredential]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL,
[CipherConfiguration] VARCHAR(MAX) NOT NULL,
[CredentialBlob] VARCHAR(MAX) NOT NULL,
[EncryptedPublicKey] VARCHAR(MAX) NOT NULL,
[EncryptedPrivateKey] VARCHAR(MAX) NOT NULL,
[EncryptedUserKey] VARCHAR(MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OpaqueKeyExchangeCredential] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OpaqueKeyExchangeCredential_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_OpaqueKeyExchangeCredential_UserId]
ON [dbo].[OpaqueKeyExchangeCredential]([UserId] ASC);
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@CipherConfiguration VARCHAR(MAX),
@CredentialBlob VARCHAR(MAX),
@EncryptedPublicKey VARCHAR(MAX),
@EncryptedPrivateKey TINYINT,
@EncryptedUserKey VARCHAR(MAX),
@CreationDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[OpaqueKeyExchangeCredential]
(
[Id],
[UserId],
[CipherConfiguration],
[CredentialBlob],
[EncryptedPublicKey],
[EncryptedPrivateKey],
[EncryptedUserKey],
[CreationDate]
)
VALUES
(
@Id,
@UserId,
@CipherConfiguration,
@CredentialBlob,
@EncryptedPublicKey,
@EncryptedPrivateKey,
@EncryptedUserKey,
@CreationDate
)
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@CipherConfiguration VARCHAR(MAX),
@CredentialBlob VARCHAR(MAX),
@EncryptedPublicKey VARCHAR(MAX),
@EncryptedPrivateKey VARCHAR(MAX),
@EncryptedUserKey VARCHAR(MAX),
@CreationDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[OpaqueKeyExchangeCredential]
SET
[CipherConfiguration] = @CipherConfiguration,
[CredentialBlob] = @CredentialBlob,
[EncryptedPublicKey] = @EncryptedPublicKey,
[EncryptedPrivateKey] = @EncryptedPrivateKey,
[EncryptedUserKey] = @EncryptedUserKey,
[CreationDate] = @CreationDate
WHERE
[Id] = @Id AND [UserId] = @UserId
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
DELETE
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[Id] = @Id
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadByUserId]
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[UserId] = @UserId
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
*
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[Id] = @Id
END
GO