1
0
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:
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.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;
}
}

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;
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; }

View File

@ -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; }
}

View File

@ -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; }
}

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 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);
}

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 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>();
}

View File

@ -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);

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<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();
services.AddSingleton<IOpaqueKeyExchangeCredentialRepository, OpaqueKeyExchangeCredentialRepository>();
if (selfHosted)
{
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]
@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

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