mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -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:
parent
d617004435
commit
b03e3c3b8c
@ -1,5 +1,5 @@
|
||||
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;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@ -12,7 +12,7 @@ namespace Bit.Api.Auth.Controllers;
|
||||
public class OpaqueKeyExchangeController : Controller
|
||||
{
|
||||
private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService;
|
||||
IUserService _userService;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public OpaqueKeyExchangeController(
|
||||
IOpaqueKeyExchangeService opaqueKeyExchangeService,
|
||||
@ -27,8 +27,8 @@ public class OpaqueKeyExchangeController : Controller
|
||||
public async Task<OpaqueRegistrationStartResponse> StartRegistrationAsync([FromBody] OpaqueRegistrationStartRequest request)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var result = await _opaqueKeyExchangeService.StartRegistration(Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration.ToNativeConfiguration());
|
||||
return new OpaqueRegistrationStartResponse(result.Item1, Convert.ToBase64String(result.Item2));
|
||||
var result = await _opaqueKeyExchangeService.StartRegistration(Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@ -36,25 +36,23 @@ public class OpaqueKeyExchangeController : Controller
|
||||
public async void FinishRegistrationAsync([FromBody] OpaqueRegistrationFinishRequest request)
|
||||
{
|
||||
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
|
||||
[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);
|
||||
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
|
||||
[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));
|
||||
return result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
39
src/Core/Auth/Entities/OpaqueKeyExchangeCredential.cs
Normal file
39
src/Core/Auth/Entities/OpaqueKeyExchangeCredential.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Opaque;
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Opaque;
|
||||
|
||||
public class OpaqueRegistrationFinishRequest
|
||||
{
|
||||
@ -9,10 +9,10 @@ public class OpaqueRegistrationFinishRequest
|
||||
[Required]
|
||||
public Guid SessionId { get; set; }
|
||||
|
||||
public RotateableKeyset KeySet { get; set; }
|
||||
public RotateableOpaqueKeyset KeySet { get; set; }
|
||||
}
|
||||
|
||||
public class RotateableKeyset
|
||||
public class RotateableOpaqueKeyset
|
||||
{
|
||||
[Required]
|
||||
public string EncryptedUserKey { get; set; }
|
@ -1,6 +1,6 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Request.Opaque;
|
||||
namespace Bit.Core.Auth.Models.Api.Request.Opaque;
|
||||
|
||||
|
||||
public class OpaqueRegistrationStartRequest
|
||||
@ -17,6 +17,7 @@ public class CipherConfiguration
|
||||
|
||||
[Required]
|
||||
public string CipherSuite { get; set; }
|
||||
[Required]
|
||||
public Argon2KsfParameters Argon2Parameters { get; set; }
|
||||
|
||||
public Bitwarden.OPAQUE.CipherConfiguration ToNativeConfiguration()
|
||||
@ -28,7 +29,7 @@ public class CipherConfiguration
|
||||
OprfCS = Bitwarden.OPAQUE.OprfCS.Ristretto255,
|
||||
KeGroup = Bitwarden.OPAQUE.KeGroup.Ristretto255,
|
||||
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
|
||||
@ -42,9 +43,9 @@ public class Argon2KsfParameters
|
||||
{
|
||||
// Memory in KiB
|
||||
[Required]
|
||||
public int memory;
|
||||
public int Memory { get; set; }
|
||||
[Required]
|
||||
public int iterations;
|
||||
public int Iterations { get; set; }
|
||||
[Required]
|
||||
public int parallelism;
|
||||
public int Parallelism { get; set; }
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
namespace Bit.Api.Auth.Models.Response.Opaque;
|
||||
namespace Bit.Core.Auth.Models.Api.Response.Opaque;
|
||||
|
||||
public class OpaqueRegistrationStartResponse : ResponseModel
|
||||
{
|
||||
@ -11,7 +11,7 @@ public class OpaqueRegistrationStartResponse : ResponseModel
|
||||
SessionId = sessionId;
|
||||
}
|
||||
|
||||
public String RegistrationResponse { get; set; }
|
||||
public string RegistrationResponse { get; set; }
|
||||
public Guid SessionId { get; set; }
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
public class OpaqueKeyExchangeCredentialBlob
|
||||
{
|
||||
public byte[] PasswordFile { get; set; }
|
||||
public byte[] ServerSetup { get; set; }
|
||||
}
|
22
src/Core/Auth/Models/Data/OpaqueKeyExchangeRotateKeyData.cs
Normal file
22
src/Core/Auth/Models/Data/OpaqueKeyExchangeRotateKeyData.cs
Normal 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; }
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
@ -1,14 +1,15 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bitwarden.OPAQUE;
|
||||
using Bit.Core.Auth.Models.Api.Request.Opaque;
|
||||
using Bit.Core.Auth.Models.Api.Response.Opaque;
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
public interface IOpaqueKeyExchangeService
|
||||
{
|
||||
public Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration);
|
||||
public void FinishRegistration(Guid sessionId, byte[] registrationUpload, User user);
|
||||
public Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration);
|
||||
public Task FinishRegistration(Guid sessionId, byte[] registrationUpload, User user, RotateableOpaqueKeyset keyset);
|
||||
public Task<(Guid, byte[])> StartLogin(byte[] request, string email);
|
||||
public Task<bool> FinishLogin(Guid sessionId, byte[] finishCredential);
|
||||
public void SetActive(Guid sessionId, User user);
|
||||
public void Unenroll(User user);
|
||||
public Task SetActive(Guid sessionId, User user);
|
||||
public Task Unenroll(User user);
|
||||
}
|
||||
|
@ -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 Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
namespace Bit.Core.Auth.Services;
|
||||
|
||||
@ -7,36 +16,48 @@ namespace Bit.Core.Auth.Services;
|
||||
|
||||
public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
|
||||
{
|
||||
|
||||
private readonly BitwardenOpaqueServer _bitwardenOpaque;
|
||||
private readonly IOpaqueKeyExchangeCredentialRepository _opaqueKeyExchangeCredentialRepository;
|
||||
private readonly IDistributedCache _distributedCache;
|
||||
private readonly IUserRepository _userRepository;
|
||||
|
||||
public OpaqueKeyExchangeService(
|
||||
)
|
||||
IOpaqueKeyExchangeCredentialRepository opaqueKeyExchangeCredentialRepository,
|
||||
IDistributedCache distributedCache,
|
||||
IUserRepository userRepository
|
||||
)
|
||||
{
|
||||
_bitwardenOpaque = new BitwardenOpaqueServer();
|
||||
_opaqueKeyExchangeCredentialRepository = opaqueKeyExchangeCredentialRepository;
|
||||
_distributedCache = distributedCache;
|
||||
_userRepository = userRepository;
|
||||
}
|
||||
|
||||
|
||||
public async Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration)
|
||||
public async Task<OpaqueRegistrationStartResponse> StartRegistration(byte[] request, User user, Models.Api.Request.Opaque.CipherConfiguration cipherConfiguration)
|
||||
{
|
||||
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();
|
||||
SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { sessionId = sessionId, serverSetup = registrationResponse.serverSetup, cipherConfiguration = cipherConfiguration, userId = user.Id });
|
||||
return (sessionId, registrationResponse.registrationResponse);
|
||||
SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { SessionId = sessionId, ServerSetup = serverSetup, CipherConfiguration = cipherConfiguration, UserId = user.Id });
|
||||
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(() =>
|
||||
{
|
||||
var cipherConfiguration = SessionStore.RegisterSessions[sessionId].cipherConfiguration;
|
||||
var cipherConfiguration = SessionStore.RegisterSessions[sessionId].CipherConfiguration;
|
||||
try
|
||||
{
|
||||
var registrationFinish = _bitwardenOpaque.FinishRegistration(cipherConfiguration, registrationUpload);
|
||||
SessionStore.RegisterSessions[sessionId].serverRegistration = registrationFinish.serverRegistration;
|
||||
var registrationFinish = _bitwardenOpaque.FinishRegistration(cipherConfiguration.ToNativeConfiguration(), registrationUpload);
|
||||
SessionStore.RegisterSessions[sessionId].PasswordFile = registrationFinish.serverRegistration;
|
||||
|
||||
SessionStore.RegisterSessions[sessionId].KeySet = Encoding.ASCII.GetBytes(JsonSerializer.Serialize(keyset));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -48,109 +69,130 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService
|
||||
|
||||
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);
|
||||
if (credential.Value == null)
|
||||
{
|
||||
// generate fake credential
|
||||
throw new InvalidOperationException("User not found");
|
||||
}
|
||||
// todo don't allow user enumeration
|
||||
throw new InvalidOperationException("User not found");
|
||||
}
|
||||
|
||||
var cipherConfiguration = credential.Value.cipherConfiguration;
|
||||
var serverSetup = credential.Value.serverSetup;
|
||||
var serverRegistration = credential.Value.serverRegistration;
|
||||
var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id);
|
||||
if (credential == null)
|
||||
{
|
||||
// generate fake credential
|
||||
throw new InvalidOperationException("Credential not found");
|
||||
}
|
||||
|
||||
var loginResponse = _bitwardenOpaque.StartLogin(cipherConfiguration, serverSetup, serverRegistration, request, credential.Key.ToString());
|
||||
var sessionId = Guid.NewGuid();
|
||||
SessionStore.LoginSessions.Add(sessionId, new LoginSession() { userId = credential.Key, loginState = loginResponse.state });
|
||||
return (sessionId, loginResponse.credentialResponse);
|
||||
});
|
||||
var cipherConfiguration = JsonSerializer.Deserialize<Models.Api.Request.Opaque.CipherConfiguration>(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();
|
||||
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)
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
return await Task.Run(async () =>
|
||||
{
|
||||
if (!SessionStore.LoginSessions.ContainsKey(sessionId))
|
||||
{
|
||||
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)
|
||||
{
|
||||
throw new InvalidOperationException("User not found");
|
||||
throw new InvalidOperationException("Credential not found");
|
||||
}
|
||||
|
||||
var loginState = SessionStore.LoginSessions[sessionId].loginState;
|
||||
var cipherConfiguration = credential.cipherConfiguration;
|
||||
var loginState = SessionStore.LoginSessions[sessionId].LoginState;
|
||||
var cipherConfiguration = SessionStore.LoginSessions[sessionId].CipherConfiguration;
|
||||
SessionStore.LoginSessions.Remove(sessionId);
|
||||
|
||||
try
|
||||
{
|
||||
var success = _bitwardenOpaque.FinishLogin(cipherConfiguration, loginState, credentialFinalization);
|
||||
SessionStore.LoginSessions.Remove(sessionId);
|
||||
var success = _bitwardenOpaque.FinishLogin(cipherConfiguration.ToNativeConfiguration(), loginState, credentialFinalization);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// print
|
||||
Console.WriteLine(e.Message);
|
||||
SessionStore.LoginSessions.Remove(sessionId);
|
||||
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];
|
||||
if (session.userId != user.Id)
|
||||
{
|
||||
throw new InvalidOperationException("Session does not belong to user");
|
||||
}
|
||||
if (session.serverRegistration == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session did not complete registration");
|
||||
}
|
||||
SessionStore.RegisterSessions.Remove(sessionId);
|
||||
throw new InvalidOperationException("Session does not belong to user");
|
||||
}
|
||||
if (session.PasswordFile == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session did not complete registration");
|
||||
}
|
||||
if (session.KeySet == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session did not complete registration");
|
||||
}
|
||||
|
||||
// to be copied to the persistent DB
|
||||
var cipherConfiguration = session.cipherConfiguration;
|
||||
var serverRegistration = session.serverRegistration;
|
||||
var serverSetup = session.serverSetup;
|
||||
var keyset = JsonSerializer.Deserialize<RotateableOpaqueKeyset>(Encoding.ASCII.GetString(session.KeySet))!;
|
||||
var credentialBlob = new OpaqueKeyExchangeCredentialBlob()
|
||||
{
|
||||
PasswordFile = session.PasswordFile,
|
||||
ServerSetup = session.ServerSetup
|
||||
};
|
||||
|
||||
if (PersistentStore.Credentials.ContainsKey(user.Id))
|
||||
{
|
||||
PersistentStore.Credentials.Remove(user.Id);
|
||||
}
|
||||
PersistentStore.Credentials.Add(user.Id, new Credential() { serverRegistration = serverRegistration, serverSetup = serverSetup, cipherConfiguration = cipherConfiguration, email = user.Email });
|
||||
});
|
||||
var credential = new OpaqueKeyExchangeCredential()
|
||||
{
|
||||
UserId = user.Id,
|
||||
CipherConfiguration = JsonSerializer.Serialize(session.CipherConfiguration),
|
||||
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 required Guid sessionId { get; set; }
|
||||
public required byte[] serverSetup { get; set; }
|
||||
public required CipherConfiguration cipherConfiguration { get; set; }
|
||||
public required Guid userId { get; set; }
|
||||
public byte[]? serverRegistration { get; set; }
|
||||
public required Guid SessionId { get; set; }
|
||||
public required byte[] ServerSetup { get; set; }
|
||||
public required Models.Api.Request.Opaque.CipherConfiguration CipherConfiguration { get; set; }
|
||||
public required Guid UserId { get; set; }
|
||||
public byte[]? PasswordFile { get; set; }
|
||||
public byte[]? KeySet { get; set; }
|
||||
}
|
||||
|
||||
public class LoginSession
|
||||
{
|
||||
public required Guid userId { get; set; }
|
||||
public required byte[] loginState { get; set; }
|
||||
public required Guid UserId { get; set; }
|
||||
public required byte[] LoginState { get; set; }
|
||||
public required Models.Api.Request.Opaque.CipherConfiguration CipherConfiguration { get; set; }
|
||||
}
|
||||
|
||||
public class SessionStore()
|
||||
@ -158,16 +200,3 @@ public class SessionStore()
|
||||
public static Dictionary<Guid, RegisterSession> RegisterSessions = new Dictionary<Guid, RegisterSession>();
|
||||
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>();
|
||||
}
|
||||
|
@ -670,11 +670,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
|
||||
if (opaqueSessionId != null)
|
||||
{
|
||||
_opaqueKeyExchangeService.SetActive((Guid)opaqueSessionId, user);
|
||||
await _opaqueKeyExchangeService.SetActive((Guid)opaqueSessionId, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
_opaqueKeyExchangeService.Unenroll(user);
|
||||
await _opaqueKeyExchangeService.Unenroll(user);
|
||||
}
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
|
||||
@ -817,7 +817,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
user.Key = key;
|
||||
|
||||
// TODO: Add support
|
||||
_opaqueKeyExchangeService.Unenroll(user);
|
||||
await _opaqueKeyExchangeService.Unenroll(user);
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _mailService.SendAdminResetPasswordEmailAsync(user.Email, user.Name, org.DisplayName());
|
||||
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_AdminResetPassword);
|
||||
@ -845,7 +845,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
user.MasterPasswordHint = hint;
|
||||
|
||||
// TODO: Add support
|
||||
_opaqueKeyExchangeService.Unenroll(user);
|
||||
await _opaqueKeyExchangeService.Unenroll(user);
|
||||
await _userRepository.ReplaceAsync(user);
|
||||
await _mailService.SendUpdatedTempPasswordEmailAsync(user.Email, user.Name);
|
||||
await _eventService.LogUserEventAsync(user.Id, EventType.User_UpdatedTempPassword);
|
||||
|
@ -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);
|
||||
// };
|
||||
}
|
||||
|
||||
}
|
@ -65,7 +65,7 @@ public static class DapperServiceCollectionExtensions
|
||||
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
|
||||
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
|
||||
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
|
||||
|
||||
services.AddSingleton<IOpaqueKeyExchangeCredentialRepository, OpaqueKeyExchangeCredentialRepository>();
|
||||
if (selfHosted)
|
||||
{
|
||||
services.AddSingleton<IEventRepository, EventRepository>();
|
||||
|
@ -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
|
@ -0,0 +1,10 @@
|
||||
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
AS
|
||||
BEGIN
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[OpaqueKeyExchangeCredential]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OpaqueKeyExchangeCredential]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
@ -0,0 +1,13 @@
|
||||
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_ReadByUserId]
|
||||
@UserId UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[OpaqueKeyExchangeCredential]
|
||||
WHERE
|
||||
[UserId] = @UserId
|
||||
END
|
@ -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
|
18
src/Sql/Auth/dbo/Tables/OpaqueKeyExchangeCredential.sql
Normal file
18
src/Sql/Auth/dbo/Tables/OpaqueKeyExchangeCredential.sql
Normal 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);
|
@ -1,6 +1,7 @@
|
||||
CREATE PROCEDURE [dbo].[User_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
WITH RECOMPILE
|
||||
WITH
|
||||
RECOMPILE
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -23,6 +24,12 @@ BEGIN
|
||||
END
|
||||
|
||||
BEGIN TRANSACTION User_DeleteById
|
||||
-- Delete OpaqueKeyExchangeCredentials
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[OpaqueKeyExchangeCredential]
|
||||
WHERE
|
||||
[UserId] = @UserId
|
||||
|
||||
-- Delete WebAuthnCredentials
|
||||
DELETE
|
||||
@ -42,7 +49,7 @@ BEGIN
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[AuthRequest]
|
||||
WHERE
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete devices
|
||||
@ -116,7 +123,7 @@ BEGIN
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[Send]
|
||||
WHERE
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
-- Delete Notification Status
|
||||
@ -132,7 +139,7 @@ BEGIN
|
||||
[dbo].[Notification]
|
||||
WHERE
|
||||
[UserId] = @Id
|
||||
|
||||
|
||||
-- Finally, delete the user
|
||||
DELETE
|
||||
FROM
|
||||
|
@ -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
|
Loading…
x
Reference in New Issue
Block a user