diff --git a/src/Api/Auth/Controllers/OpaqueKeyExchangeController.cs b/src/Api/Auth/Controllers/OpaqueKeyExchangeController.cs index 692e68db68..cb0768f385 100644 --- a/src/Api/Auth/Controllers/OpaqueKeyExchangeController.cs +++ b/src/Api/Auth/Controllers/OpaqueKeyExchangeController.cs @@ -24,19 +24,36 @@ public class OpaqueKeyExchangeController : Controller } [HttpPost("~/opaque/start-registration")] - public async Task StartRegistration([FromBody] OpaqueRegistrationStartRequest request) + public async Task StartRegistrationAsync([FromBody] OpaqueRegistrationStartRequest request) { var user = await _userService.GetUserByPrincipalAsync(User); - var result = await _opaqueKeyExchangeService.StartRegistration(System.Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration); - return new OpaqueRegistrationStartResponse(result.Item1, System.Convert.ToBase64String(result.Item2)); + var result = await _opaqueKeyExchangeService.StartRegistration(Convert.FromBase64String(request.RegistrationRequest), user, request.CipherConfiguration); + return new OpaqueRegistrationStartResponse(result.Item1, Convert.ToBase64String(result.Item2)); } [HttpPost("~/opaque/finish-registration")] - public async Task FinishRegistration([FromBody] OpaqueRegistrationFinishRequest request) + public async void FinishRegistrationAsync([FromBody] OpaqueRegistrationFinishRequest request) { - await Task.Run(() => { }); - return ""; + var user = await _userService.GetUserByPrincipalAsync(User); + _opaqueKeyExchangeService.FinishRegistration(request.SessionId, Convert.FromBase64String(request.RegistrationUpload), user); + } + + + // TODO: Remove and move to token endpoint + [HttpPost("~/opaque/start-login")] + public async Task StartLoginAsync([FromBody] OpaqueLoginStartRequest request) + { + var result = await _opaqueKeyExchangeService.StartLogin(Convert.FromBase64String(request.CredentialRequest), request.Email); + return new OpaqueLoginStartResponse(result.Item1, Convert.ToBase64String(result.Item2)); + } + + // TODO: Remove and move to token endpoint + [HttpPost("~/opaque/finish-login")] + public async Task FinishLoginAsync([FromBody] OpaqueLoginFinishRequest request) + { + var result = await _opaqueKeyExchangeService.FinishLogin(request.SessionId, Convert.FromBase64String(request.CredentialFinalization)); + return result; } } diff --git a/src/Api/Auth/Models/Request/Opaque/OpaqueLoginFinishRequest.cs b/src/Api/Auth/Models/Request/Opaque/OpaqueLoginFinishRequest.cs new file mode 100644 index 0000000000..c2061f92f1 --- /dev/null +++ b/src/Api/Auth/Models/Request/Opaque/OpaqueLoginFinishRequest.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Auth.Models.Request.Opaque; + +public class OpaqueLoginFinishRequest +{ + [Required] + public string CredentialFinalization { get; set; } + [Required] + public Guid SessionId { get; set; } + +} diff --git a/src/Api/Auth/Models/Request/Opaque/OpaqueLoginStartRequest.cs b/src/Api/Auth/Models/Request/Opaque/OpaqueLoginStartRequest.cs new file mode 100644 index 0000000000..72c835dda6 --- /dev/null +++ b/src/Api/Auth/Models/Request/Opaque/OpaqueLoginStartRequest.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Api.Auth.Models.Request.Opaque; + +public class OpaqueLoginStartRequest +{ + [Required] + public string Email { get; set; } + [Required] + public string CredentialRequest { get; set; } + +} diff --git a/src/Api/Auth/Models/Request/Opaque/OpaqueRegistrationFinishRequest.cs b/src/Api/Auth/Models/Request/Opaque/OpaqueRegistrationFinishRequest.cs index 4a3c1fa36a..cb15e7c37b 100644 --- a/src/Api/Auth/Models/Request/Opaque/OpaqueRegistrationFinishRequest.cs +++ b/src/Api/Auth/Models/Request/Opaque/OpaqueRegistrationFinishRequest.cs @@ -5,7 +5,7 @@ namespace Bit.Api.Auth.Models.Request.Opaque; public class OpaqueRegistrationFinishRequest { [Required] - public String RegistrationUpload { get; set; } + public string RegistrationUpload { get; set; } [Required] public Guid SessionId { get; set; } @@ -15,9 +15,9 @@ public class OpaqueRegistrationFinishRequest public class RotateableKeyset { [Required] - public String EncryptedUserKey { get; set; } + public string EncryptedUserKey { get; set; } [Required] - public String EncryptedPublicKey { get; set; } + public string EncryptedPublicKey { get; set; } [Required] - public String EncryptedPrivateKey { get; set; } + public string EncryptedPrivateKey { get; set; } } diff --git a/src/Api/Auth/Models/Response/Opaque/OpaqueLoginStartResponse.cs b/src/Api/Auth/Models/Response/Opaque/OpaqueLoginStartResponse.cs new file mode 100644 index 0000000000..2488da4b02 --- /dev/null +++ b/src/Api/Auth/Models/Response/Opaque/OpaqueLoginStartResponse.cs @@ -0,0 +1,17 @@ +using Bit.Core.Models.Api; + +namespace Bit.Api.Auth.Models.Response.Opaque; + +public class OpaqueLoginStartResponse : ResponseModel +{ + public OpaqueLoginStartResponse(Guid sessionId, string credentialResponse, string obj = "login-start-response") + : base(obj) + { + CredentialResponse = credentialResponse; + SessionId = sessionId; + } + + public string CredentialResponse { get; set; } + public Guid SessionId { get; set; } +} + diff --git a/src/Core/Auth/Services/IOpaqueKeyExchangeService.cs b/src/Core/Auth/Services/IOpaqueKeyExchangeService.cs index 3ff4af8a1a..d5930141d3 100644 --- a/src/Core/Auth/Services/IOpaqueKeyExchangeService.cs +++ b/src/Core/Auth/Services/IOpaqueKeyExchangeService.cs @@ -6,5 +6,9 @@ namespace Bit.Core.Auth.Services; public interface IOpaqueKeyExchangeService { public Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration); - public Task FinishRegistration(Guid sessionId, byte[] request, User user); + public void FinishRegistration(Guid sessionId, byte[] registrationUpload, User user); + public Task<(Guid, byte[])> StartLogin(byte[] request, string email); + public Task FinishLogin(Guid sessionId, byte[] finishCredential); + public void SetActive(Guid sessionId, User user); + public void Unenroll(User user); } diff --git a/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs b/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs index bca890ee9d..edce12169d 100644 --- a/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs +++ b/src/Core/Auth/Services/Implementations/OpaqueKeyExchangeService.cs @@ -3,6 +3,8 @@ using Bitwarden.OPAQUE; namespace Bit.Core.Auth.Services; +#nullable enable + public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService { @@ -17,32 +19,158 @@ public class OpaqueKeyExchangeService : IOpaqueKeyExchangeService public async Task<(Guid, byte[])> StartRegistration(byte[] request, User user, CipherConfiguration cipherConfiguration) { - var registrationRequest = _bitwardenOpaque.StartRegistration(cipherConfiguration, null, request, user.Id.ToString()); - var message = registrationRequest.registrationResponse; - var serverSetup = registrationRequest.serverSetup; - // persist server setup - var sessionId = Guid.NewGuid(); - SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { SessionId = sessionId, ServerSetup = serverSetup, cipherConfiguration = cipherConfiguration }); - await Task.Run(() => { }); - return (sessionId, message); + return await Task.Run(() => + { + var registrationResponse = _bitwardenOpaque.StartRegistration(cipherConfiguration, null, request, user.Id.ToString()); + var sessionId = Guid.NewGuid(); + SessionStore.RegisterSessions.Add(sessionId, new RegisterSession() { sessionId = sessionId, serverSetup = registrationResponse.serverSetup, cipherConfiguration = cipherConfiguration, userId = user.Id }); + return (sessionId, registrationResponse.registrationResponse); + }); } - public async Task FinishRegistration(Guid sessionId, byte[] request, User user) + public async void FinishRegistration(Guid sessionId, byte[] registrationUpload, User user) { - await Task.Run(() => { }); - return true; + await Task.Run(() => + { + var cipherConfiguration = SessionStore.RegisterSessions[sessionId].cipherConfiguration; + try + { + var registrationFinish = _bitwardenOpaque.FinishRegistration(cipherConfiguration, registrationUpload); + SessionStore.RegisterSessions[sessionId].serverRegistration = registrationFinish.serverRegistration; + + // todo move to changepassword + SetActive(sessionId, user); + } + catch (Exception e) + { + SessionStore.RegisterSessions.Remove(sessionId); + throw new Exception(e.Message); + } + }); + } + + public async Task<(Guid, byte[])> StartLogin(byte[] request, string email) + { + return await Task.Run(() => + { + var credential = PersistentStore.Credentials.First(x => x.Value.email == email); + if (credential.Value == null) + { + // generate fake credential + throw new InvalidOperationException("User not found"); + } + + var cipherConfiguration = credential.Value.cipherConfiguration; + var serverSetup = credential.Value.serverSetup; + var serverRegistration = credential.Value.serverRegistration; + + 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); + }); + } + + public async Task FinishLogin(Guid sessionId, byte[] credentialFinalization) + { + return await Task.Run(() => + { + if (!SessionStore.LoginSessions.ContainsKey(sessionId)) + { + throw new InvalidOperationException("Session not found"); + } + var credential = PersistentStore.Credentials[SessionStore.LoginSessions[sessionId].userId]; + if (credential == null) + { + throw new InvalidOperationException("User not found"); + } + + var loginState = SessionStore.LoginSessions[sessionId].loginState; + var cipherConfiguration = credential.cipherConfiguration; + + try + { + var success = _bitwardenOpaque.FinishLogin(cipherConfiguration, loginState, credentialFinalization); + SessionStore.LoginSessions.Remove(sessionId); + return true; + } + catch (Exception e) + { + // print + Console.WriteLine(e.Message); + SessionStore.LoginSessions.Remove(sessionId); + return false; + } + }); + } + + public async void SetActive(Guid sessionId, User user) + { + await Task.Run(() => + { + 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); + + // to be copied to the persistent DB + var cipherConfiguration = session.cipherConfiguration; + var serverRegistration = session.serverRegistration; + var 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 }); + }); + } + + public async void Unenroll(User user) + { + await Task.Run(() => + { + PersistentStore.Credentials.Remove(user.Id); + }); } } public class RegisterSession { - public Guid SessionId { get; set; } - public byte[] ServerSetup { get; set; } - public CipherConfiguration cipherConfiguration { get; set; } + 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 class LoginSession +{ + public required Guid userId { get; set; } + public required byte[] loginState { get; set; } } public class SessionStore() { public static Dictionary RegisterSessions = new Dictionary(); - public static Dictionary LoginSessions = new Dictionary(); + public static Dictionary LoginSessions = new Dictionary(); +} + +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 Credentials = new Dictionary(); }