From f49aa97b0ffce0deb5856181f29d0c0884c75cb4 Mon Sep 17 00:00:00 2001 From: Donovan Date: Fri, 19 Dec 2025 14:22:28 -0600 Subject: [PATCH] implement icon management and update client app state --- src/client/app.rs | 112 +++++++++++++----- src/client/client_auth_api.rs | 9 +- src/client/states.rs | 14 ++- .../in_memory/in_memory_transport.rs | 1 + .../repository/in_memory/in_memory_user_db.rs | 102 +++++++++++++--- src/shared/models/app.rs | 27 +++-- src/shared/signed_session_data.rs | 2 - src/shared/user_api.rs | 17 ++- 8 files changed, 218 insertions(+), 66 deletions(-) diff --git a/src/client/app.rs b/src/client/app.rs index b154cf1..c365d18 100644 --- a/src/client/app.rs +++ b/src/client/app.rs @@ -1,14 +1,15 @@ use std::collections::HashMap; +use std::marker::PhantomData; use crate::client::client_auth_api::ClientAuth; use crate::client::opaque::{ServerConnectionLogin, ServerConnectionRegister}; use crate::client::repo::ClientRepo; -use crate::client::states::{KeyRegister, UserStateCodeLogin, UserStateCodeRegister, UserStateKey}; +use crate::client::states::{KeyLogin, KeyRegister, UserStateCodeLogin, UserStateCodeRegister, UserStateKey}; use crate::shared::models::app::{Icon, IconID}; use crate::shared::email::Email; use crate::shared::opaque::UserSecretKey; use crate::shared::user_api::UserAPI; -struct ClientApp<'a, R, U, C> +struct ClientApp<'a, State, R, U, C> where R: ServerConnectionRegister + ServerConnectionLogin, U: UserAPI, @@ -16,11 +17,15 @@ where { auth_api: ClientAuth<'a, R, U>, client_repo: C, - new_user: HashMap, - user_login: HashMap + code_reg: Option, + key_login: Option>, + code_login: Option, + _state: PhantomData } -impl <'a, R, U, C>ClientApp<'a, R, U, C> + + +impl <'a,State, R, U, C>ClientApp<'a,State, R, U, C> where R: ServerConnectionRegister + ServerConnectionLogin, U: UserAPI, @@ -30,16 +35,36 @@ where Self { auth_api, client_repo, - new_user: HashMap::new(), - user_login: HashMap::new() + code_reg: None, + key_login: None, + code_login: None, + _state: PhantomData } } + fn into_state(self) -> ClientApp<'a, NextState, R, U, C> { + ClientApp { + auth_api: self.auth_api, + client_repo: self.client_repo, + code_reg: self.code_reg, + key_login: self.key_login, + code_login: self.code_login, + _state: PhantomData, + } + } +} - async fn new_user(&mut self, email: Email, secret_key: UserSecretKey) -> Result, String> { +pub struct NewUserRegisterKey; +impl <'a, R, U, C>ClientApp<'a, NewUserRegisterKey, R, U, C> +where + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, + C: ClientRepo, +{ + async fn new_user(mut self, email: Email, secret_key: UserSecretKey) -> Result, String> { let key_register = UserStateKey::::new(email.clone(), secret_key.clone()); self.client_repo.add_secret_key(email.clone(), secret_key.clone()).await?; - let key_login = match key_register.register(&self.auth_api).await.map_err(|e| format!("error: {e:?}")) { - Ok(s) => {s} + let key_login = match key_register.register(&self.auth_api).await.map_err(|e| format!("error: {e:?}")) { + Ok(s) => s, Err(e) => { self.client_repo.remove_secret_key(&email).await.expect("couldn't delete"); return Err(format!("error registering key {}", e)) @@ -47,23 +72,54 @@ where }; let key_logged_in = key_login.login(&self.auth_api).await.map_err(|e| format!("error: {e:?}"))?; let code_register = key_logged_in.register_code(&self.auth_api).await?; - let icons = code_register.get_icons(); - self.new_user.insert(email, code_register); - Ok(icons) - } - - async fn new_user_register_code(&mut self, email: Email, selected_icons: Vec) -> Result>, String> { - let code_register = self.new_user.remove(&email).unwrap(); // Todo should be an error - let code_login = code_register.register(&self.auth_api,selected_icons).await?; - let keypad = code_login.get_keypad().await; - self.user_login.insert(email, code_login); - Ok(keypad) - } - - async fn user_login(&mut self, email: Email, selected_keys: &Vec) -> Result<(), String> { - let mut code_login = self.user_login.remove(&email).unwrap(); // Todo should be an error - let code_logged_in = code_login.login(&self.auth_api, &selected_keys).await?; - self.client_repo.add_code_logged_in(&email, code_logged_in).await?; - Ok(()) + self.code_reg = Some(code_register); + Ok(self.into_state::()) } } + +pub struct UserKeyLogin; +impl <'a, R, U, C>ClientApp<'a, UserKeyLogin, R, U, C> +where + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, + C: ClientRepo, +{ + async fn login(mut self, email: Email, secret_key: UserSecretKey) -> Result<(), String> { + let key_login = UserStateKey::::new(email, secret_key); + self.key_login = Some(key_login); + let key_log_in = self.into_state::(); + todo!() + } +} + +pub struct NewUserRegisterCode; +impl <'a, R, U, C>ClientApp<'a, NewUserRegisterCode, R, U, C> +where + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, + C: ClientRepo, +{ + async fn get_new_user_icons(&self) -> Result, String> { + Ok( + self.code_reg + .clone() + .ok_or("no code register".to_string())? + .get_icons() + ) + } + + async fn new_user_register_code(mut self, email: Email, selected_icons: Vec) -> Result, String> { + let code_register = self.code_reg.ok_or("no code register".to_string())?; + let code_login = code_register.register(&self.auth_api,selected_icons).await?; + todo!() + // Ok() + } +} + + +// async fn user_login(&mut self, email: Email, selected_keys: &Vec) -> Result<(), String> { +// let mut code_login = self.user_login.remove(&email).unwrap(); // Todo should be an error +// let code_logged_in = code_login.login(&self.auth_api, &selected_keys).await?; +// self.client_repo.add_code_logged_in(&email, code_logged_in).await?; +// Ok(()) +// } diff --git a/src/client/client_auth_api.rs b/src/client/client_auth_api.rs index 0cf4039..a8216d5 100644 --- a/src/client/client_auth_api.rs +++ b/src/client/client_auth_api.rs @@ -1,4 +1,4 @@ -use crate::shared::models::app::{AuthAPI, CodeLoggedInSession, CodeLoginData, Icon, KeyLoggedInSession}; +use crate::shared::models::app::{AuthAPI, CodeLoggedInSession, CodeLoginData, Icon, KeyLoggedInSession, RegisterCodeData}; use crate::shared::email::Email; use crate::shared::opaque::UserSecretKey; use anyhow::Result; @@ -32,12 +32,12 @@ where self.opaque_key_register.register(&auth_data).await.map_err(|e| format!("error: {}", e)) } - async fn register_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, data: CodeLoginData) -> Result<(), String> { + async fn register_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, register_code_data: RegisterCodeData) -> Result<(), String> { let auth_data = OpaqueAuthData::from_code(email.as_str(), passcode); self.opaque_code_register.register(&auth_data).await.map_err(|e| format!("error: {}", e))?; let signed_session = SignedSessionData::new( key_login_session.0.session_id, - data, + register_code_data, &key_login_session.0.session_key ).map_err(|e| format!("error: {e:?}"))?; self.user_api.set_code_login_data(signed_session).await @@ -64,7 +64,8 @@ where async fn get_new_icons( &self, ) -> Result, String> { - self.user_api.get_new_icons().await + let total_props = self.get_policy().await?.keypad_dimension().total_props(); + self.user_api.get_new_icons(total_props).await } async fn get_login_data( diff --git a/src/client/states.rs b/src/client/states.rs index 884124b..c578ac3 100644 --- a/src/client/states.rs +++ b/src/client/states.rs @@ -3,7 +3,7 @@ use nkode_rs::nkode_core::chacha20prng::Nonce; use nkode_rs::nkode_core::keypad::Keypad; use nkode_rs::nkode_core::nkode_cipher::NKodeCipher; use nkode_rs::from_bytes::FromBytes; -use crate::shared::models::app::{AuthAPI, CodeLoggedInSession, CodeLoginData, Icon, IconID, KeyLoggedInSession, ICON_ID_SIZE}; +use crate::shared::models::app::{AuthAPI, CodeLoggedInSession, CodeLoginData, Icon, IconID, KeyLoggedInSession, RegisterCodeData, ICON_ID_SIZE}; use crate::shared::email::Email; use crate::shared::opaque::UserSecretKey; @@ -79,10 +79,10 @@ impl UserStateKeyLoggedIn { pub async fn login_code(self, api: &dyn AuthAPI) -> Result { let login_data = api.get_login_data(&self.key_login).await?; - let icons = self.get_icons(api, &login_data.icon_nonce()).await?; + let icons = self.get_icons(api, &login_data.icon_nonce).await?; let policy = api.get_policy().await?; let nkode_secret_key = self.user_secret_key.chacha20_secret_key(); - let cipher = NKodeCipher::from_nonce(policy, &nkode_secret_key, login_data.icon_nonce())?; + let cipher = NKodeCipher::from_nonce(policy, &nkode_secret_key, &login_data.icon_nonce)?; Ok( UserStateCodeLogin { email: self.email, @@ -96,6 +96,7 @@ impl UserStateKeyLoggedIn { } } +#[derive(Clone)] pub struct UserStateCodeRegister { email: Email, user_secret_key: UserSecretKey, @@ -121,8 +122,13 @@ impl UserStateCodeRegister { cipher_nonce, icon_nonce: self.icon_nonce, keypad, + email: self.email.clone() }; - api.register_code(&self.email, &ciphered_nkode.passcode, &self.key_login, data.clone()).await?; + let register_data = RegisterCodeData { + code_login_data: data.clone(), + icons: self.icons.clone(), + }; + api.register_code(&self.email, &ciphered_nkode.passcode, &self.key_login, register_data).await?; Ok(UserStateCodeLogin { mask: data.mask, email: self.email, diff --git a/src/server/repository/in_memory/in_memory_transport.rs b/src/server/repository/in_memory/in_memory_transport.rs index bcc8fc6..20c785b 100644 --- a/src/server/repository/in_memory/in_memory_transport.rs +++ b/src/server/repository/in_memory/in_memory_transport.rs @@ -85,3 +85,4 @@ where ) } } + diff --git a/src/server/repository/in_memory/in_memory_user_db.rs b/src/server/repository/in_memory/in_memory_user_db.rs index 9d3f353..832192c 100644 --- a/src/server/repository/in_memory/in_memory_user_db.rs +++ b/src/server/repository/in_memory/in_memory_user_db.rs @@ -2,18 +2,22 @@ use std::collections::HashMap; use std::sync::Arc; use async_trait::async_trait; use nkode_rs::nkode_core::keypad::Keypad; +use rand::Rng; +use rand::rngs::ThreadRng; use tokio::sync::Mutex; use uuid::Uuid; use crate::server::repository::user_repo::UserRepo; -use crate::shared::models::app::{CodeLoggedInSession, CodeLoginData, Icon, KeyLoggedInSession}; +use crate::shared::models::app::{CodeLoggedInSession, CodeLoginData, Icon, IconID, KeyLoggedInSession, RegisterCodeData}; use crate::shared::email::Email; use crate::shared::signed_session_data::SignedSessionData; -use crate::shared::user_api::UserAPI; +use crate::shared::user_api::{IconPoolAPI, UserAPI}; pub struct InMemoryUserDB { key_session: Arc>>, code_session: Arc>>, - code_data: Arc>>, + code_data_store: Arc>>, + owned_icons: Arc>>, + icon_pool: Arc>> } impl InMemoryUserDB { @@ -21,7 +25,9 @@ impl InMemoryUserDB { Self { key_session: Arc::new(Mutex::new(HashMap::new())), code_session: Arc::new(Mutex::new(HashMap::new())), - code_data: Arc::new(Mutex::new(HashMap::new())), + code_data_store: Arc::new(Mutex::new(HashMap::new())), + owned_icons: Arc::new(Mutex::new(HashMap::new())), + icon_pool: Arc::new(Mutex::new(Vec::new())), } } } @@ -49,22 +55,22 @@ impl UserRepo for InMemoryUserDB { } async fn set_key_session(&self, session: KeyLoggedInSession) -> Result<(), String> { - self.key_session.lock().await.insert(session.0.session_id, session); + self.key_session.lock().await.insert(session.0.session_id, session).ok_or("couldn't set key session".to_string())?; Ok(()) } async fn set_code_session(&self, session: CodeLoggedInSession) -> Result<(), String> { - self.code_session.lock().await.insert(session.0.session_id, session); + self.code_session.lock().await.insert(session.0.session_id, session).ok_or("couldn't set code session".to_string())?; Ok(()) } async fn set_code_login_data(&self, email: Email, data: CodeLoginData) -> Result<(), String> { - self.code_data.lock().await.insert(email, data); + self.code_data_store.lock().await.insert(email, data).ok_or("couldn't set code login data")?; Ok(()) } async fn get_code_login_data(&self, email: &Email) -> Result { - self.code_data.lock().await + self.code_data_store.lock().await .get(email) .cloned() .ok_or_else(|| "code login data not found for email".to_string()) @@ -73,23 +79,87 @@ impl UserRepo for InMemoryUserDB { #[async_trait] impl UserAPI for InMemoryUserDB { - async fn get_new_icons(&self) -> Result, String> { - todo!() + async fn get_new_icons(&self, n: usize) -> Result, String> { + let mut pool = self.icon_pool.lock().await; + if n > pool.len() { + return Err(format!( + "not enough icons in pool: requested {}, have {}", + n, + pool.len() + )); + } + let mut rng: ThreadRng = rand::thread_rng(); + let mut out = Vec::with_capacity(n); + for _ in 0..n { + let idx = rng.gen_range(0..pool.len()); + out.push(pool.swap_remove(idx)); + } + Ok(out) } async fn get_login_data(&self, session: SignedSessionData) -> Result { - todo!() + let key_session = self.get_key_session(&session.session_id).await?; + if key_session.0.email != session.data { + return Err("email does not match session email".to_string()); + } + session.verify(&key_session.0.session_key).map_err(|e| format!("session error: {e:?}"))?; + self.get_code_login_data(&session.data).await } async fn is_code_registered(&self, session: SignedSessionData) -> Result { - todo!() + let _ = self.get_login_data(session).await?; + Ok(true) } - async fn set_code_login_data(&self, session: SignedSessionData) -> Result<(), String> { - todo!() + async fn set_code_login_data(&self, session: SignedSessionData) -> Result<(), String> { + let key_session = self.get_key_session(&session.session_id).await?; + session.verify(&key_session.0.session_key).map_err(|e| format!("session error: {e:?}"))?; + self.code_data_store.lock().await.insert(session.data.code_login_data.email.clone(), session.data.code_login_data).ok_or("couldn't set code data".to_string())?; + let mut owned_icons = self.owned_icons.lock().await; + for icon in session.data.icons { + let icon_id = icon.id().clone(); + owned_icons.insert(icon_id.clone(), icon).ok_or(format!("error inserting icon into owned icons {}", icon_id))?; + } + Ok(()) } - async fn update_keypad(&self, keypad: SignedSessionData) -> Result<(), String> { - todo!() + async fn update_keypad(&self, session: SignedSessionData) -> Result<(), String> { + let key_session = self.get_key_session(&session.session_id).await?; + session.verify(&key_session.0.session_key).map_err(|e| format!("session error: {e:?}"))?; + let email = key_session.0.email; + let mut code_data_store = self.code_data_store.lock().await; + let mut code_data = code_data_store.remove(&email).ok_or("user doesn't have code data".to_string())?; + code_data.keypad = session.data; + code_data_store.insert(email, code_data).ok_or("couldn't update keypad")?; + Ok(()) + } + + async fn get_icons(&self, icon_ids: &[IconID]) -> Result, String> { + let owned_icons = self.owned_icons.lock().await; + let mut user_icons = Vec::::with_capacity(icon_ids.len()); + for id in icon_ids { + let icon = owned_icons.get(id).cloned().ok_or(format!("icon: {} dne", id))?; + user_icons.push(icon); + } + Ok(user_icons) + } + + async fn set_icons(&self, session: SignedSessionData>) -> Result<(), String> { + let key_session = self.get_key_session(&session.session_id).await?; + session.verify(&key_session.0.session_key).map_err(|e| format!("session error: {e:?}"))?; + let mut owned_icons = self.owned_icons.lock().await; + for icon in session.data { + let icon_id = icon.id().clone(); + owned_icons.insert(icon_id.clone(), icon).ok_or(format!("error inserting icon into owned icons {}", icon_id))?; + } + Ok(()) + } +} + +#[async_trait] +impl IconPoolAPI for InMemoryUserDB { + async fn add_icons(&self, icons: Vec) -> Result<(), String> { + self.icon_pool.lock().await.extend(icons); + Ok(()) } } diff --git a/src/shared/models/app.rs b/src/shared/models/app.rs index cb04ddc..cf145b5 100644 --- a/src/shared/models/app.rs +++ b/src/shared/models/app.rs @@ -1,3 +1,4 @@ +use std::fmt; use async_trait::async_trait; use nkode_rs::nkode_core::chacha20prng::Nonce; use nkode_rs::nkode_core::keypad::Keypad; use serde::{Deserialize, Serialize}; @@ -22,9 +23,18 @@ pub struct KeyLoggedInSession(pub(crate) LoggedInSession); pub struct CodeLoggedInSession(pub(crate) LoggedInSession); pub const ICON_ID_SIZE: usize = 32; -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq,Hash, Serialize, Deserialize)] pub struct IconID([u8; 32]); +impl fmt::Display for IconID { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + for b in &self.0 { + write!(f, "{:02x}", b)?; + } + Ok(()) + } +} + impl FromBytes for IconID { fn from_array(arr: [u8; ICON_ID_SIZE]) -> Self { Self(arr) @@ -45,22 +55,25 @@ impl Icon { } } -#[derive(Debug, Clone, Getters, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct CodeLoginData { - #[get = "pub"] + pub(crate) email: Email, pub(crate) mask: Vec, - #[get = "pub"] pub(crate) cipher_nonce: Nonce, - #[get = "pub"] pub(crate) icon_nonce: Nonce, - #[get = "pub"] pub(crate) keypad: Keypad, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RegisterCodeData { + pub(crate) code_login_data: CodeLoginData, + pub(crate) icons: Vec, +} + #[async_trait] pub trait AuthAPI { async fn register_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result<(), String>; - async fn register_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, data: CodeLoginData) -> Result<(), String>; + async fn register_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, register_data: RegisterCodeData) -> Result<(), String>; async fn login_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result; async fn login_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, keypad: Keypad) -> Result; diff --git a/src/shared/signed_session_data.rs b/src/shared/signed_session_data.rs index cbfc894..9f7b22b 100644 --- a/src/shared/signed_session_data.rs +++ b/src/shared/signed_session_data.rs @@ -41,7 +41,6 @@ impl SignedSessionData where T: Serialize + DeserializeOwned, { - /// Create a signed envelope around (session_id, issued/expires, data). pub fn new( session_id: Uuid, data: T, @@ -57,7 +56,6 @@ where }) } - /// Verify signature + time bounds. pub fn verify(&self, session_key: &OpaqueSessionKey) -> Result<(), SignedSessionError> { let expected = Self::compute_signature( self.session_id, diff --git a/src/shared/user_api.rs b/src/shared/user_api.rs index 561fbee..7b256b0 100644 --- a/src/shared/user_api.rs +++ b/src/shared/user_api.rs @@ -1,15 +1,22 @@ use async_trait::async_trait; use nkode_rs::nkode_core::keypad::Keypad; use crate::shared::email::Email; -use crate::shared::models::app::{CodeLoginData, Icon}; +use crate::shared::models::app::{CodeLoginData, Icon, IconID, RegisterCodeData}; use crate::shared::signed_session_data::SignedSessionData; #[async_trait] pub trait UserAPI: Sync + Send { // TODO: this should have a session - async fn get_new_icons(&self) -> Result, String>; + async fn get_new_icons(&self, n: usize) -> Result, String>; async fn get_login_data(&self, session: SignedSessionData) -> Result; async fn is_code_registered(&self, session: SignedSessionData) -> Result; - async fn set_code_login_data(&self, session: SignedSessionData) -> Result<(), String>; - async fn update_keypad(&self, keypad: SignedSessionData) -> Result<(), String>; -} \ No newline at end of file + async fn set_code_login_data(&self, session: SignedSessionData) -> Result<(), String>; + async fn update_keypad(&self, session: SignedSessionData) -> Result<(), String>; + async fn get_icons(&self, icon_ids: &[IconID]) -> Result, String>; + async fn set_icons(&self, session: SignedSessionData>) -> Result<(), String>; +} + +#[async_trait] +pub trait IconPoolAPI: Sync + Send { + async fn add_icons(&self, icons: Vec) -> Result<(), String>; +}