From 0b34f09fc7ea9cd2424e35cfeb426cf6a9e6ea30 Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann <mail@quexten.com>
Date: Fri, 14 Mar 2025 14:16:47 +0100
Subject: [PATCH] Add demo authentication and cleanup controller

---
 .../OpaqueKeyExchangeController.cs            |  29 +++-
 .../Opaque/OpaqueLoginFinishRequest.cs        |  12 ++
 .../Request/Opaque/OpaqueLoginStartRequest.cs |  12 ++
 .../Opaque/OpaqueRegistrationFinishRequest.cs |   8 +-
 .../Opaque/OpaqueLoginStartResponse.cs        |  17 ++
 .../Services/IOpaqueKeyExchangeService.cs     |   6 +-
 .../OpaqueKeyExchangeService.cs               | 158 ++++++++++++++++--
 7 files changed, 216 insertions(+), 26 deletions(-)
 create mode 100644 src/Api/Auth/Models/Request/Opaque/OpaqueLoginFinishRequest.cs
 create mode 100644 src/Api/Auth/Models/Request/Opaque/OpaqueLoginStartRequest.cs
 create mode 100644 src/Api/Auth/Models/Response/Opaque/OpaqueLoginStartResponse.cs

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<OpaqueRegistrationStartResponse> StartRegistration([FromBody] OpaqueRegistrationStartRequest request)
+    public async Task<OpaqueRegistrationStartResponse> 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<String> 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<OpaqueLoginStartResponse> 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<bool> 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<bool> 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<bool> 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<bool> 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<bool> 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<Guid, RegisterSession> RegisterSessions = new Dictionary<Guid, RegisterSession>();
-    public static Dictionary<Guid, byte[]> LoginSessions = new Dictionary<Guid, byte[]>();
+    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>();
 }