implement icon management and update client app state

This commit is contained in:
2025-12-19 14:22:28 -06:00
parent 3bb3ccf18c
commit f49aa97b0f
8 changed files with 218 additions and 66 deletions

View File

@@ -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<Email, UserStateCodeRegister>,
user_login: HashMap<Email, UserStateCodeLogin>
code_reg: Option<UserStateCodeRegister>,
key_login: Option<UserStateKey<KeyLogin>>,
code_login: Option<UserStateCodeLogin>,
_state: PhantomData<State>
}
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<NextState>(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<Vec<Icon>, 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<ClientApp<'a, NewUserRegisterCode, R, U, C>, String> {
let key_register = UserStateKey::<KeyRegister>::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}
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<IconID>) -> Result<Vec<Vec<Icon>>, 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<usize>) -> 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::<NewUserRegisterCode>())
}
}
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::<KeyLogin>::new(email, secret_key);
self.key_login = Some(key_login);
let key_log_in = self.into_state::<UserKeyLogin>();
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<Vec<Icon>, 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<IconID>) -> Result<ClientApp<'a, NewUserRegisterCode, R, U, C>, 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<usize>) -> 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(())
// }

View File

@@ -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<Vec<Icon>, 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(

View File

@@ -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<UserStateCodeLogin, String> {
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,

View File

@@ -85,3 +85,4 @@ where
)
}
}

View File

@@ -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<Mutex<HashMap<Uuid, KeyLoggedInSession>>>,
code_session: Arc<Mutex<HashMap<Uuid, CodeLoggedInSession>>>,
code_data: Arc<Mutex<HashMap<Email, CodeLoginData>>>,
code_data_store: Arc<Mutex<HashMap<Email, CodeLoginData>>>,
owned_icons: Arc<Mutex<HashMap<IconID, Icon>>>,
icon_pool: Arc<Mutex<Vec<Icon>>>
}
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<CodeLoginData, String> {
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<Vec<Icon>, String> {
todo!()
async fn get_new_icons(&self, n: usize) -> Result<Vec<Icon>, 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<Email>) -> Result<CodeLoginData, String> {
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<Email>) -> Result<bool, String> {
todo!()
let _ = self.get_login_data(session).await?;
Ok(true)
}
async fn set_code_login_data(&self, session: SignedSessionData<CodeLoginData>) -> Result<(), String> {
todo!()
async fn set_code_login_data(&self, session: SignedSessionData<RegisterCodeData>) -> 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<Keypad>) -> Result<(), String> {
todo!()
async fn update_keypad(&self, session: SignedSessionData<Keypad>) -> 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<Vec<Icon>, String> {
let owned_icons = self.owned_icons.lock().await;
let mut user_icons = Vec::<Icon>::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<Vec<Icon>>) -> 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<Icon>) -> Result<(), String> {
self.icon_pool.lock().await.extend(icons);
Ok(())
}
}

View File

@@ -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<ICON_ID_SIZE> 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<u64>,
#[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<Icon>,
}
#[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<KeyLoggedInSession, String>;
async fn login_code(&self, email: &Email, passcode: &[u64], key_login_session: &KeyLoggedInSession, keypad: Keypad) -> Result<CodeLoggedInSession, String>;

View File

@@ -41,7 +41,6 @@ impl<T> SignedSessionData<T>
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,

View File

@@ -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<Vec<Icon>, String>;
async fn get_new_icons(&self, n: usize) -> Result<Vec<Icon>, String>;
async fn get_login_data(&self, session: SignedSessionData<Email>) -> Result<CodeLoginData, String>;
async fn is_code_registered(&self, session: SignedSessionData<Email>) -> Result<bool, String>;
async fn set_code_login_data(&self, session: SignedSessionData<CodeLoginData>) -> Result<(), String>;
async fn update_keypad(&self, keypad: SignedSessionData<Keypad>) -> Result<(), String>;
async fn set_code_login_data(&self, session: SignedSessionData<RegisterCodeData>) -> Result<(), String>;
async fn update_keypad(&self, session: SignedSessionData<Keypad>) -> Result<(), String>;
async fn get_icons(&self, icon_ids: &[IconID]) -> Result<Vec<Icon>, String>;
async fn set_icons(&self, session: SignedSessionData<Vec<Icon>>) -> Result<(), String>;
}
#[async_trait]
pub trait IconPoolAPI: Sync + Send {
async fn add_icons(&self, icons: Vec<Icon>) -> Result<(), String>;
}