From e0f79344a60fa9a9b914957b7def0451ed22ee78 Mon Sep 17 00:00:00 2001 From: Donovan Date: Sat, 13 Dec 2025 16:55:17 -0600 Subject: [PATCH] test client server opaque --- Cargo.lock | 91 +++++++++++------ Cargo.toml | 4 +- src/client.rs | 57 ++--------- src/in_memory_auth_repo.rs | 87 ++++++++++++++++ src/in_memory_auth_session.rs | 101 +++++++++++++++++++ src/in_memory_transport.rs | 163 ++++++++++++++++++++++++++++++ src/lib.rs | 9 +- src/models.rs | 20 ++-- src/server.rs | 182 ++++++++++------------------------ tests/in_memory_test.rs | 82 +++++++++++++++ 10 files changed, 572 insertions(+), 224 deletions(-) create mode 100644 src/in_memory_auth_repo.rs create mode 100644 src/in_memory_auth_session.rs create mode 100644 src/in_memory_transport.rs create mode 100644 tests/in_memory_test.rs diff --git a/Cargo.lock b/Cargo.lock index 26b0c64..17897fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -273,15 +273,15 @@ dependencies = [ ] [[package]] -name = "getset" -version = "0.1.6" +name = "getrandom" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf0fc11e47561d47397154977bc219f4cf809b2974facc3ccb3b89e2436f912" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", + "cfg-if", + "libc", + "r-efi", + "wasip2", ] [[package]] @@ -334,10 +334,10 @@ name = "nkode-protocol" version = "0.1.0" dependencies = [ "async-trait", - "getset", "opaque-ke", "rand", "sha2", + "tokio", "uuid", ] @@ -362,7 +362,7 @@ dependencies = [ "ed25519-dalek", "elliptic-curve", "generic-array", - "getrandom", + "getrandom 0.2.16", "hkdf", "hmac", "rand", @@ -383,6 +383,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + [[package]] name = "pkcs8" version = "0.10.2" @@ -402,28 +408,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.103" @@ -442,6 +426,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.8.5" @@ -469,7 +459,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom", + "getrandom 0.2.16", ] [[package]] @@ -596,6 +586,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "pin-project-lite", + "tokio-macros", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "typenum" version = "1.19.0" @@ -614,6 +625,7 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ + "getrandom 0.3.4", "js-sys", "wasm-bindgen", ] @@ -649,6 +661,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.105" @@ -694,6 +715,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + [[package]] name = "zerocopy" version = "0.8.31" diff --git a/Cargo.toml b/Cargo.toml index 36bebf5..b9a3560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2024" opaque-ke = { version = "4.0.1",default-features = true, features = [ "std","argon2"] } rand = { version = "0.8.5", features = ["std"] } sha2 = "0.10.9" -uuid = "1.19.0" -getset = "0.1.6" async-trait = "0.1.89" +uuid = { version = "1.19.0", features = ["v4"] } +tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] } diff --git a/src/client.rs b/src/client.rs index 4867a8d..df89314 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,16 +5,13 @@ use opaque_ke::rand::rngs::OsRng; use opaque_ke::{ ClientLogin, ClientLoginFinishParameters, ClientRegistration, ClientRegistrationFinishParameters, - CredentialFinalization, CredentialRequest, CredentialResponse, - RegistrationRequest, RegistrationResponse, + CredentialFinalization, CredentialRequest, + RegistrationRequest, }; -use crate::models::{ - NKodeCipherSuite, - PasswordFile, - KeyLoginSession, - KeyRegisterSession, -}; +use crate::models::{RegisterSession, LoginSession, NKodeCipherSuite, PasswordFile}; + + #[derive(Debug)] pub enum ClientAuthError { @@ -22,8 +19,6 @@ pub enum ClientAuthError { Transport(String), } -// --- Normalize auth inputs to (identifier, secret-bytes) --- - pub struct AuthenticationData { pub identifier: Vec, pub secret: Vec, @@ -50,40 +45,13 @@ impl AuthenticationData { } } -// --- Small adapter traits so server can return any “session wrapper” type --- - -pub trait RegStartSession { - fn session_id(&self) -> &Uuid; - fn response(&self) -> &RegistrationResponse; -} - -pub trait LoginStartSession { - fn session_id(&self) -> &Uuid; - fn response(&self) -> &CredentialResponse; -} - -// If your protocol types already have these methods, just implement the traits: -impl RegStartSession for KeyRegisterSession { - fn session_id(&self) -> &Uuid { self.session_id() } - fn response(&self) -> &RegistrationResponse { self.response() } -} - -impl LoginStartSession for KeyLoginSession { - fn session_id(&self) -> &Uuid { self.session_id() } - fn response(&self) -> &CredentialResponse { self.response() } -} - -// --- Server connection traits: generic over returned session wrapper types --- - #[async_trait] pub trait ServerConnectionRegister { - type Start: RegStartSession + Send; - async fn start( &mut self, identifier: &[u8], message: &RegistrationRequest, - ) -> Result; + ) -> Result; async fn finish( &mut self, @@ -94,13 +62,11 @@ pub trait ServerConnectionRegister { #[async_trait] pub trait ServerConnectionLogin { - type Start: LoginStartSession + Send; - async fn start( &mut self, identifier: &[u8], request: &CredentialRequest, - ) -> Result; + ) -> Result; async fn finish( &mut self, @@ -125,7 +91,7 @@ impl OpaqueAuthentication { .start(&auth.identifier, &start.message) .await .map_err(|e| ClientAuthError::Transport(format!("server reg start: {e:?}")))?; - let server_msg = server_start.response().clone(); + let server_msg = server_start.response; let finish = start .state .finish( @@ -135,10 +101,9 @@ impl OpaqueAuthentication { ClientRegistrationFinishParameters::default(), ) .map_err(|e| ClientAuthError::Opaque(format!("client reg finish: {e:?}")))?; - // Assuming PasswordFile is Vec (serialized server-side password file) let password_file: PasswordFile = finish.message.serialize(); server - .finish(server_start.session_id(), password_file) + .finish(&server_start.session_id, password_file) .await .map_err(|e| ClientAuthError::Transport(format!("server reg finish: {e:?}")))?; Ok(()) @@ -155,7 +120,7 @@ impl OpaqueAuthentication { .start(&auth.identifier, &start.message) .await .map_err(|e| ClientAuthError::Transport(format!("server login start: {e:?}")))?; - let server_msg = server_start.response().clone(); + let server_msg = server_start.response.clone(); let finish = start .state .finish( @@ -166,7 +131,7 @@ impl OpaqueAuthentication { ) .map_err(|e| ClientAuthError::Opaque(format!("client login finish: {e:?}")))?; server - .finish(server_start.session_id(), &finish.message) + .finish(&server_start.session_id, &finish.message) .await .map_err(|e| ClientAuthError::Transport(format!("server login finish: {e:?}")))?; Ok(finish.session_key.to_vec()) diff --git a/src/in_memory_auth_repo.rs b/src/in_memory_auth_repo.rs new file mode 100644 index 0000000..5da79c8 --- /dev/null +++ b/src/in_memory_auth_repo.rs @@ -0,0 +1,87 @@ +use std::collections::HashMap; + +use crate::models::PasswordFile; +use crate::server::{AuthRepo, AuthRepoError}; + +#[derive(Debug, Default)] +pub struct InMemoryAuthRepo { + key_entries: HashMap, + code_entries: HashMap, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct KeyID(Vec); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct CodeID(Vec); + +impl InMemoryAuthRepo { + pub fn new() -> Self { + Self::default() + } + + fn code_exists(&self, identifier: &CodeID) -> bool { + self.code_entries.contains_key(identifier) + } + + fn key_exists(&self, identifier: &KeyID) -> bool { + self.key_entries.contains_key(identifier) + } +} + +impl AuthRepo for InMemoryAuthRepo { + fn new_key( + &mut self, + identifier: &[u8], + password_file: PasswordFile, + ) -> Result<(), AuthRepoError> { + if self.key_exists(&KeyID(identifier.to_vec())) { + return Err(AuthRepoError::UserExists); + } + + self.key_entries + .insert(KeyID(identifier.to_vec()), password_file); + Ok(()) + } + + fn new_code( + &mut self, + identifier: &[u8], + password_file: PasswordFile, + ) -> Result<(), AuthRepoError> { + if !self.has_key(identifier) { + return Err(AuthRepoError::KeyNotRegistered); + } + if self.code_exists(&CodeID(identifier.to_vec())) { + return Err(AuthRepoError::UserExists); + } + + self.code_entries + .insert(CodeID(identifier.to_vec()), password_file); + Ok(()) + } + + fn has_code(&self, identifier: &[u8]) -> bool { + self.code_entries + .contains_key(&CodeID(identifier.to_vec())) + } + + fn has_key(&self, identifier: &[u8]) -> bool { + self.key_entries + .contains_key(&KeyID(identifier.to_vec())) + } + + fn get_key_passcode_file(&self, identifier: &[u8]) -> Result { + self.key_entries + .get(&KeyID(identifier.to_vec())) + .cloned() + .ok_or(AuthRepoError::KeyNotRegistered) + } + + fn get_code_passcode_file(&self, identifier: &[u8]) -> Result { + self.code_entries + .get(&CodeID(identifier.to_vec())) + .cloned() + .ok_or(AuthRepoError::CodeNotRegistered) + } +} diff --git a/src/in_memory_auth_session.rs b/src/in_memory_auth_session.rs new file mode 100644 index 0000000..56b7b7b --- /dev/null +++ b/src/in_memory_auth_session.rs @@ -0,0 +1,101 @@ +use std::collections::HashMap; +use crate::server::{RegCache, LoginCache, AuthSession}; +use opaque_ke::{ServerLogin}; +use crate::models::NKodeCipherSuite; +use uuid::Uuid; + +#[derive(Default)] +pub struct InMemoryAuthSession { + reg_sessions: HashMap, + login_sessions: HashMap, +} + +impl InMemoryAuthSession { + pub fn new() -> Self { + Self::default() + } +} + +impl AuthSession for InMemoryAuthSession { + fn new_reg_session(&mut self, identifier: &[u8]) -> Result { + let cache = RegCache { + session_id: Uuid::new_v4(), + identifier: identifier.to_vec(), + }; + + // Extremely unlikely collision, but keep the invariant anyway. + if self.reg_sessions.contains_key(&cache.session_id) { + return Err("session_id collision".to_string()); + } + + self.reg_sessions.insert(cache.session_id, RegCache { + session_id: cache.session_id, + identifier: cache.identifier.clone(), + }); + + Ok(cache) + } + + fn get_reg_session(&self, session_id: &Uuid) -> Result { + self.reg_sessions + .get(session_id) + .map(|c| RegCache { + session_id: c.session_id, + identifier: c.identifier.clone(), + }) + .ok_or_else(|| "registration session not found".to_string()) + } + + fn clear_reg_session(&mut self, session_id: &Uuid) -> Result<(), String> { + self.reg_sessions + .remove(session_id) + .map(|_| ()) + .ok_or_else(|| "registration session not found".to_string()) + } + + fn new_login_session( + &mut self, + identifier: &[u8], + server_login: ServerLogin, + ) -> Result { + let cache = LoginCache { + session_id: Uuid::new_v4(), + identifiers: identifier.to_vec(), + server_login, + }; + + if self.login_sessions.contains_key(&cache.session_id) { + return Err("session_id collision".to_string()); + } + + self.login_sessions.insert( + cache.session_id, + LoginCache { + session_id: cache.session_id, + identifiers: cache.identifiers.clone(), + // move is fine; we already moved into cache, so clone to keep both: + server_login: cache.server_login.clone(), + }, + ); + + Ok(cache) + } + + fn get_login_session(&self, session_id: &Uuid) -> Result { + self.login_sessions + .get(session_id) + .map(|c| LoginCache { + session_id: c.session_id, + identifiers: c.identifiers.clone(), + server_login: c.server_login.clone(), + }) + .ok_or_else(|| "login session not found".to_string()) + } + + fn clear_login_session(&mut self, session_id: &Uuid) -> Result<(), String> { + self.login_sessions + .remove(session_id) + .map(|_| ()) + .ok_or_else(|| "login session not found".to_string()) + } +} \ No newline at end of file diff --git a/src/in_memory_transport.rs b/src/in_memory_transport.rs new file mode 100644 index 0000000..764e538 --- /dev/null +++ b/src/in_memory_transport.rs @@ -0,0 +1,163 @@ +use async_trait::async_trait; +use std::marker::PhantomData; +use tokio::sync::Mutex; +use std::sync::Arc; +use uuid::Uuid; +use opaque_ke::{CredentialFinalization, CredentialRequest, RegistrationRequest}; +use crate::client::{ClientAuthError, ServerConnectionLogin, ServerConnectionRegister}; +use crate::models::{LoginSession, RegisterSession, NKodeCipherSuite, NKodeServerSetup, PasswordFile}; +use crate::server::{OpaqueAuth, CredKind, Key, Code}; +use crate::in_memory_auth_repo::InMemoryAuthRepo; +use crate::in_memory_auth_session::InMemoryAuthSession; + +pub struct InMemoryServer { + auth: OpaqueAuth, + _kind: PhantomData, +} + +impl InMemoryServer { + pub fn new(server_setup: NKodeServerSetup) -> Self { + Self { + auth: OpaqueAuth::new(server_setup, InMemoryAuthRepo::new(), InMemoryAuthSession::new()), + _kind: PhantomData, + } + } +} + +/// Convenience aliases +pub type InMemoryKeyServer = InMemoryServer; +pub type InMemoryCodeServer = InMemoryServer; + +#[async_trait] +impl ServerConnectionRegister for InMemoryServer +where + K: CredKind + Send + Sync, +{ + async fn start( + &mut self, + identifier: &[u8], + message: &RegistrationRequest, + ) -> Result { + // Server API takes ownership; client trait gives us a reference. + // opaque-ke request types are typically Clone; if not, you'll need to adjust signatures. + self.auth + .reg_start::(identifier, message.clone()) + .await + .map_err(|e| ClientAuthError::Transport(e)) + } + + async fn finish( + &mut self, + session_id: &Uuid, + password_file: PasswordFile, + ) -> Result<(), ClientAuthError> { + self.auth + .reg_finish::(session_id, password_file) + .await + .map_err(|e| ClientAuthError::Transport(e)) + } +} + +#[async_trait] +impl ServerConnectionLogin for InMemoryServer +where + K: CredKind + Send + Sync, +{ + async fn start( + &mut self, + identifier: &[u8], + request: &CredentialRequest, + ) -> Result { + self.auth + .login_start::(identifier, request.clone()) + .await + .map_err(|e| ClientAuthError::Transport(e)) + } + + async fn finish( + &mut self, + session_id: &Uuid, + message: &CredentialFinalization, + ) -> Result<(), ClientAuthError> { + // Server computes its own session key too; we just need it to validate and complete. + let _server_session_key = self + .auth + .login_finish::(session_id, message.clone()) + .await + .map_err(|e| ClientAuthError::Transport(e))?; + + Ok(()) + } +} + +pub struct SharedServer { + inner: Arc>>, + _k: PhantomData, +} + +impl SharedServer { + pub fn new(inner: Arc>>) -> Self { + Self { inner, _k: PhantomData } + } +} + +#[async_trait::async_trait] +impl ServerConnectionRegister for SharedServer +where + K: CredKind + Send + Sync, +{ + async fn start( + &mut self, + identifier: &[u8], + message: &RegistrationRequest, + ) -> Result { + let mut guard = self.inner.lock().await; + guard + .reg_start::(identifier, message.clone()) + .await + .map_err(ClientAuthError::Transport) + } + + async fn finish( + &mut self, + session_id: &Uuid, + password_file: PasswordFile, + ) -> Result<(), ClientAuthError> { + let mut guard = self.inner.lock().await; + guard + .reg_finish::(session_id, password_file) + .await + .map_err(ClientAuthError::Transport) + } +} + +#[async_trait::async_trait] +impl ServerConnectionLogin for SharedServer +where + K: CredKind + Send + Sync, +{ + async fn start( + &mut self, + identifier: &[u8], + request: &CredentialRequest, + ) -> Result { + let mut guard = self.inner.lock().await; + guard + .login_start::(identifier, request.clone()) + .await + .map_err(ClientAuthError::Transport) + } + + async fn finish( + &mut self, + session_id: &Uuid, + message: &CredentialFinalization, + ) -> Result<(), ClientAuthError> { + let mut guard = self.inner.lock().await; + let _ = guard + .login_finish::(session_id, message.clone()) + .await + .map_err(ClientAuthError::Transport)?; + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 42b4fa4..efa9c92 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ -mod models; -mod client; -mod server; +pub mod models; +pub mod client; +pub mod server; +pub mod in_memory_auth_repo; +pub mod in_memory_auth_session; +pub mod in_memory_transport; diff --git a/src/models.rs b/src/models.rs index 11b4a1d..5c672cb 100644 --- a/src/models.rs +++ b/src/models.rs @@ -1,11 +1,10 @@ -use opaque_ke::{RegistrationResponse, Ristretto255, TripleDh, ServerSetup, CredentialResponse, RegistrationUploadLen}; +use opaque_ke::{Ristretto255, TripleDh, ServerSetup, CredentialResponse, RegistrationUploadLen, RegistrationResponse}; use opaque_ke::keypair::{OprfSeed, PrivateKey}; use sha2::Sha512; use opaque_ke::CipherSuite; use opaque_ke::argon2::Argon2; use opaque_ke::generic_array::GenericArray; use uuid::Uuid; -use getset::Getters; pub const NONCE_SIZE: usize = 12; pub const SESSION_KEY_SIZE: usize = 32; @@ -22,14 +21,6 @@ impl CipherSuite for NKodeCipherSuite { pub type NKodeServerSetup = ServerSetup, OprfSeed>; -#[derive(Debug, Clone, PartialEq, Eq, Getters)] -pub struct RegisterSession { - #[get = "pub"] - response: RegistrationResponse, - #[get = "pub"] - session_id: Uuid -} - pub type PasswordFile = GenericArray>; #[derive(Debug, Clone, PartialEq, Eq)] @@ -38,7 +29,8 @@ pub struct LoginSession { pub session_id: Uuid } -pub type KeyRegisterSession = RegisterSession; - -pub type KeyLoginSession = LoginSession; - +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RegisterSession { + pub response: RegistrationResponse, + pub session_id: Uuid +} diff --git a/src/server.rs b/src/server.rs index a7d34fb..8697176 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1,30 +1,19 @@ -//! Single-file example: remove Key-vs-Code duplication by introducing a CredKind trait -//! and implementing the OPAQUE flows once for Registration and Login states. - -use std::marker::PhantomData; - use opaque_ke::{ rand::rngs::OsRng, CredentialFinalization, CredentialRequest, - RegistrationRequest, RegistrationResponse, ServerLogin, ServerLoginParameters, + RegistrationRequest, ServerLogin, ServerLoginParameters, ServerRegistration, }; use uuid::Uuid; - -// --- Your crate types (as referenced in your snippet) --- -use crate::models::{LoginSession, NKodeCipherSuite, NKodeServerSetup, PasswordFile}; - -// ---------------- Errors ---------------- +use crate::models::{RegisterSession, LoginSession, NKodeCipherSuite, NKodeServerSetup, PasswordFile}; #[derive(Debug)] -enum AuthRepoError { +pub enum AuthRepoError { UserExists, KeyNotRegistered, CodeNotRegistered, } -// ---------------- Repo abstraction ---------------- - -trait AuthRepo { +pub trait AuthRepo { fn new_key(&mut self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>; fn new_code(&mut self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>; @@ -35,21 +24,18 @@ trait AuthRepo { fn get_code_passcode_file(&self, identifier: &[u8]) -> Result; } -// ---------------- Session abstraction ---------------- - -#[derive(Clone)] -struct RegCache { - session_id: Uuid, - identifier: Vec, +pub struct RegCache { + pub session_id: Uuid, + pub identifier: Vec, } -struct LoginCache { - session_id: Uuid, - identifiers: Vec, - server_login: ServerLogin, +pub struct LoginCache { + pub session_id: Uuid, + pub identifiers: Vec, + pub server_login: ServerLogin, } -trait AuthSession { +pub trait AuthSession { fn new_reg_session(&mut self, identifier: &[u8]) -> Result; fn get_reg_session(&self, session_id: &Uuid) -> Result; fn clear_reg_session(&mut self, session_id: &Uuid) -> Result<(), String>; @@ -63,40 +49,18 @@ trait AuthSession { fn clear_login_session(&mut self, session_id: &Uuid) -> Result<(), String>; } -// ---------------- Core OpaqueAuth struct ---------------- -struct OpaqueAuth { - server_setup: NKodeServerSetup, - user_repo: R, - session: S, - _state: PhantomData, -} - -impl OpaqueAuth -where - R: AuthRepo, - S: AuthSession, -{ - pub fn new(server_setup: NKodeServerSetup, user_repo: R, session: S) -> Self { - Self { - server_setup, - user_repo, - session, - _state: PhantomData, - } - } -} - -// ---------------- “Kind” trait: Key vs Code differences live here ---------------- - -trait CredKind { +pub trait CredKind { fn has(repo: &R, id: &[u8]) -> bool; fn get_pf(repo: &R, id: &[u8]) -> Result; fn put_pf(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError>; + fn prereq_for_register(_repo: &R, _id: &[u8]) -> Result<(), AuthRepoError> { + Ok(()) + } } -struct Key; -struct Code; +pub struct Key; +pub struct Code; impl CredKind for Key { fn has(repo: &R, id: &[u8]) -> bool { @@ -120,106 +84,74 @@ impl CredKind for Code { fn put_pf(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError> { repo.new_code(id, pf) } + fn prereq_for_register(repo: &R, id: &[u8]) -> Result<(), AuthRepoError> { + if repo.has_key(id) { + Ok(()) + } else { + Err(AuthRepoError::KeyNotRegistered) + } + } } -// ---------------- State markers ---------------- - -struct Registration(PhantomData); -struct Login(PhantomData); - -// Optional: aliases to make call sites read nicely -type KeyAuthRegistration = OpaqueAuth, R, S>; -type CodeAuthRegistration = OpaqueAuth, R, S>; -type KeyAuthLogin = OpaqueAuth, R, S>; -type CodeAuthLogin = OpaqueAuth, R, S>; - -// ---------------- Return types ---------------- - -struct RegSession { - session_id: Uuid, - response: RegistrationResponse, +pub struct OpaqueAuth { + server_setup: NKodeServerSetup, + user_repo: R, + session: S, } -// NOTE: you already have crate::protocol::LoginSession, so we use that. -// If you want it here, uncomment below and remove crate import. -// struct LoginSession { -// session_id: Uuid, -// response: CredentialResponse, -// } +impl OpaqueAuth { + pub fn new(server_setup: NKodeServerSetup, user_repo: R, session: S) -> Self { + Self { server_setup, user_repo, session } + } -// ---------------- Shared registration flow ---------------- - -impl OpaqueAuth, R, S> -where - K: CredKind, - R: AuthRepo, - S: AuthSession, -{ - pub async fn start( + pub async fn reg_start( &mut self, identifier: &[u8], request: RegistrationRequest, - ) -> Result { + ) -> Result { + K::prereq_for_register(&self.user_repo, identifier) + .map_err(|e| format!("registration prereq failed: {e:?}"))?; let start = ServerRegistration::::start( &self.server_setup, request, identifier, - ) - .map_err(|e| format!("opaque reg start: {e:?}"))?; - - let cache = self - .session + ).map_err(|e| format!("opaque reg start: {e:?}"))?; + let cache = self.session .new_reg_session(identifier) .map_err(|e| format!("reg cache: {e}"))?; - Ok(RegSession { - session_id: cache.session_id, - response: start.message, - }) + Ok(RegisterSession { session_id: cache.session_id, response: start.message }) } - pub async fn finish( + pub async fn reg_finish( &mut self, session_id: &Uuid, password_file: PasswordFile, ) -> Result<(), String> { - let sess = self - .session + let sess = self.session .get_reg_session(session_id) .map_err(|e| format!("get reg session: {e}"))?; - + K::prereq_for_register(&self.user_repo, sess.identifier.as_slice()) + .map_err(|e| format!("registration prereq failed: {e:?}"))?; K::put_pf(&mut self.user_repo, sess.identifier.as_slice(), password_file) .map_err(|e| format!("repo write: {e:?}"))?; - self.session .clear_reg_session(session_id) .map_err(|e| format!("clear reg session: {e}")) } -} -// ---------------- Shared login flow ---------------- - -impl OpaqueAuth, R, S> -where - K: CredKind, - R: AuthRepo, - S: AuthSession, -{ - pub async fn start( + pub async fn login_start( &mut self, identifier: &[u8], request: CredentialRequest, ) -> Result { - // Lookup password file for K (Key vs Code) let password_file = K::get_pf(&self.user_repo, identifier) .map_err(|e| format!("repo read: {e:?}"))?; - // Deserialize into OPAQUE password file type let password_file = ServerRegistration::::deserialize(password_file.as_slice()) .map_err(|e| format!("pf deserialize: {e:?}"))?; - // OPAQUE login start let mut server_rng = OsRng; let start = ServerLogin::start( &mut server_rng, @@ -228,36 +160,32 @@ where request, identifier, ServerLoginParameters::default(), - ) - .map_err(|e| format!("opaque login start: {e:?}"))?; + ).map_err(|e| format!("opaque login start: {e:?}"))?; - // Cache server state - let cache = self - .session + let cache = self.session .new_login_session(identifier, start.state) .map_err(|e| format!("login cache: {e}"))?; - Ok(LoginSession { - session_id: cache.session_id, - response: start.message, - }) + Ok(LoginSession { session_id: cache.session_id, response: start.message }) } - pub async fn finish( + pub async fn login_finish( &mut self, session_id: &Uuid, finalize: CredentialFinalization, ) -> Result, String> { - let cache = self - .session + let cache = self.session .get_login_session(session_id) .map_err(|e| format!("get login session: {e}"))?; - let finish = cache - .server_login + let finish = cache.server_login .finish(finalize, ServerLoginParameters::default()) .map_err(|e| format!("opaque login finish: {e:?}"))?; + self.session + .clear_login_session(session_id) + .map_err(|e| format!("clear login session: {e}"))?; + Ok(finish.session_key.to_vec()) } } diff --git a/tests/in_memory_test.rs b/tests/in_memory_test.rs new file mode 100644 index 0000000..b47f2e8 --- /dev/null +++ b/tests/in_memory_test.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; +use opaque_ke::rand::rngs::OsRng; +use tokio::sync::Mutex; +use nkode_protocol::client::{AuthenticationData, OpaqueAuthentication, ClientAuthError}; +use nkode_protocol::in_memory_auth_repo::InMemoryAuthRepo; +use nkode_protocol::in_memory_auth_session::InMemoryAuthSession; +use nkode_protocol::in_memory_transport::{InMemoryKeyServer, InMemoryCodeServer, SharedServer}; +use nkode_protocol::models::NKodeServerSetup; +use nkode_protocol::server::{Code, Key, OpaqueAuth}; + +#[tokio::test] +async fn opaque_key_registration_and_login_roundtrip() { + let mut rng = OsRng; + let server_setup = NKodeServerSetup::new(&mut rng); + let mut server = InMemoryKeyServer::new(server_setup); + let auth = AuthenticationData::from_secret_key("a@b.com", b"supersecret16bytes"); + OpaqueAuthentication::register(&auth, &mut server) + .await + .expect("registration should succeed"); + let session_key = OpaqueAuthentication::login(&auth, &mut server) + .await + .expect("login should succeed"); + assert!(!session_key.is_empty()); +} + +#[tokio::test] +async fn opaque_code_registration_and_login_roundtrip() { + let mut rng = OsRng; + let server_setup = NKodeServerSetup::new(&mut rng); + let shared = Arc::new(Mutex::new(OpaqueAuth::new( + server_setup, + InMemoryAuthRepo::new(), + InMemoryAuthSession::new(), + ))); + let mut key_server = SharedServer::::new(shared.clone()); + let mut code_server = SharedServer::::new(shared.clone()); + let email = "c@d.com"; + let key_auth = AuthenticationData::from_secret_key(email, b"supersecret16bytes"); + OpaqueAuthentication::register(&key_auth, &mut key_server) + .await + .expect("key registration should succeed"); + let code = vec![1usize, 2, 3, 4, 5, 6]; + let code_auth = AuthenticationData::from_code(email, &code); + OpaqueAuthentication::register(&code_auth, &mut code_server) + .await + .expect("code registration should succeed after key exists"); + let session_key = OpaqueAuthentication::login(&code_auth, &mut code_server) + .await + .expect("login should succeed"); + assert!(!session_key.is_empty()); +} +#[tokio::test] +async fn opaque_login_fails_if_not_registered() { + let mut rng = OsRng; + let server_setup = NKodeServerSetup::new(&mut rng); + let mut server = InMemoryKeyServer::new(server_setup); + let auth = AuthenticationData::from_secret_key("nope@nope.com", b"supersecret16bytes"); + let err = OpaqueAuthentication::login(&auth, &mut server) + .await + .expect_err("login should fail if user not registered"); + match err { + ClientAuthError::Transport(_) => {} + other => panic!("unexpected error: {other:?}"), + } +} + +#[tokio::test] +async fn cannot_register_code_before_key() { + let mut rng = OsRng; + let server_setup = NKodeServerSetup::new(&mut rng); + let mut server = InMemoryCodeServer::new(server_setup); + let auth = AuthenticationData::from_code("x@y.com", &[1usize,2,3,4]); + let err = OpaqueAuthentication::register(&auth, &mut server) + .await + .expect_err("should fail because key is not registered"); + match err { + ClientAuthError::Transport(msg) => { + assert!(msg.contains("KeyNotRegistered"), "msg was: {msg}"); + } + other => panic!("unexpected error: {other:?}"), + } +}