From 88656fefac18b17a687012096b1b83c116e6cec8 Mon Sep 17 00:00:00 2001 From: Donovan Date: Thu, 18 Dec 2025 15:11:42 -0600 Subject: [PATCH] implement signed session data --- Cargo.lock | 160 ++++++++++++++++++ Cargo.toml | 7 +- src/client/app.rs | 21 ++- src/client/client_auth_api.rs | 52 +++--- src/client/opaque.rs | 6 +- src/client/states.rs | 8 +- src/server/app.rs | 10 +- src/server/models.rs | 4 +- .../repository/UserRepo.rs} | 4 +- .../in_memory/in_memory_opaque_db.rs | 2 +- .../in_memory/in_memory_opaque_session.rs | 2 +- .../in_memory/in_memory_transport.rs | 18 +- .../repository/in_memory/in_memory_user_db.rs | 6 +- src/server/repository/mod.rs | 1 + src/server/repository/opaque_repo.rs | 2 +- src/shared/{models => }/email.rs | 3 +- src/shared/mod.rs | 6 +- src/shared/models/app.rs | 8 +- src/shared/models/mod.rs | 3 - src/shared/{models => }/opaque.rs | 6 + src/shared/signed_session_data.rs | 110 ++++++++++++ src/shared/user_api.rs | 13 ++ tests/in_memory_test.rs | 8 +- 23 files changed, 384 insertions(+), 76 deletions(-) rename src/{shared/models/store.rs => server/repository/UserRepo.rs} (91%) rename src/shared/{models => }/email.rs (91%) rename src/shared/{models => }/opaque.rs (95%) create mode 100644 src/shared/signed_session_data.rs create mode 100644 src/shared/user_api.rs diff --git a/Cargo.lock b/Cargo.lock index 8a8ae36..d067b16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,15 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "base16ct" version = "0.2.0" @@ -74,6 +83,26 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "bincode_derive", + "serde", + "unty", +] + +[[package]] +name = "bincode_derive" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf95709a440f45e986983918d0e8a1f30a9b1df04918fc828670606804ac3c09" +dependencies = [ + "virtue", +] + [[package]] name = "blake2" version = "0.10.6" @@ -153,6 +182,15 @@ dependencies = [ "inout", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -174,6 +212,12 @@ dependencies = [ "libc", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crypto-bigint" version = "0.5.5" @@ -332,6 +376,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "ff" version = "0.13.1" @@ -414,6 +470,29 @@ dependencies = [ "subtle", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin", + "stable_deref_trait", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -457,20 +536,34 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + [[package]] name = "nkode-protocol" version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "bincode", "blake3", "email_address", "getset", + "hkdf", + "hmac", "nkode-rs", "opaque-ke", + "postcard", "rand 0.8.5", "serde", "sha2", + "thiserror", "tokio", "uuid", "zeroize", @@ -548,6 +641,19 @@ dependencies = [ "spki", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "serde", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -677,6 +783,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "sec1" version = "0.7.3" @@ -765,6 +877,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.7.3" @@ -775,6 +896,12 @@ dependencies = [ "der", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "subtle" version = "2.6.1" @@ -792,6 +919,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio" version = "1.48.0" @@ -825,6 +972,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + [[package]] name = "uuid" version = "1.19.0" @@ -833,6 +986,7 @@ checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", + "serde_core", "wasm-bindgen", ] @@ -842,6 +996,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "virtue" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "051eb1abcf10076295e815102942cc58f9d5e3b4560e46e53c21e8ff6f3af7b1" + [[package]] name = "voprf" version = "0.5.0" diff --git a/Cargo.toml b/Cargo.toml index cdc726e..e980737 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ opaque-ke = { version = "4.0.1",default-features = true, features = [ "std","arg rand = { version = "0.8.5", features = ["std"] } sha2 = "0.10.9" async-trait = "0.1.89" -uuid = { version = "1.19.0", features = ["v4"] } +uuid = { version = "1.19.0", features = ["serde","v4"] } tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread", "sync"] } nkode-rs = { path = "nkode-rs" } zeroize = "1.8.2" @@ -17,5 +17,10 @@ email_address = "0.2.9" anyhow = "1.0.100" serde = { version = "1.0.228", features = ["derive"] } getset = "0.1.6" +hmac = "0.12.1" +hkdf = "0.12.4" +thiserror = "2.0.17" +bincode = "2.0.1" +postcard = { version = "1.1.3", features = ["use-std"] } diff --git a/src/client/app.rs b/src/client/app.rs index 5e90741..c6507cd 100644 --- a/src/client/app.rs +++ b/src/client/app.rs @@ -1,24 +1,29 @@ use async_trait::async_trait; +use crate::client::client_auth_api::ClientAuth; +use crate::client::opaque::{ServerConnectionLogin, ServerConnectionRegister}; use crate::shared::models::app::AuthAPI; -use crate::shared::models::email::Email; -use crate::shared::models::opaque::UserSecretKey; +use crate::shared::email::Email; +use crate::shared::opaque::UserSecretKey; +use crate::shared::user_api::UserAPI; //https://chatgpt.com/c/69441737-c990-8333-9737-7ac75232da1d -struct ClientApp +struct ClientApp<'a, R, U, C> where - K: AuthAPI, + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, C: ClientRepo { - auth_api: K, + auth_api: ClientAuth<'a, R, U>, client_repo: C, } -impl ClientApp +impl <'a, R, U, C>ClientApp<'a, R, U, C> where - K: AuthAPI, + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, C: ClientRepo { - fn new(auth_api: K, client_repo: C) -> Self { + fn new(auth_api: ClientAuth<'a, R, U>, client_repo: C) -> Self { Self { auth_api, client_repo diff --git a/src/client/client_auth_api.rs b/src/client/client_auth_api.rs index 86d7630..74e2ef8 100644 --- a/src/client/client_auth_api.rs +++ b/src/client/client_auth_api.rs @@ -1,38 +1,45 @@ use crate::shared::models::app::{AuthAPI, CodeLoggedInSession, CodeLoginData, Icon, KeyLoggedInSession}; -use crate::shared::models::email::Email; -use crate::shared::models::opaque::UserSecretKey; +use crate::shared::email::Email; +use crate::shared::opaque::UserSecretKey; use anyhow::Result; use async_trait::async_trait; use nkode_rs::nkode_core::policy::{NKodePolicy, DEFAULT_POLICY}; use crate::client::opaque::{OpaqueAuthData, OpaqueAuth, ServerConnectionLogin, ServerConnectionRegister}; -use crate::shared::models::store::UserAuthStore; +use crate::shared::signed_session_data::SignedSessionData; +use crate::shared::user_api::UserAPI; pub struct ClientAuth<'a, R, U> where - R: ServerConnectionRegister + ServerConnectionLogin + Clone, - U: UserAuthStore + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI { opaque_key_register: OpaqueAuth<'a, R>, opaque_key_login: OpaqueAuth<'a, R>, opaque_code_register: OpaqueAuth<'a, R>, opaque_code_login: OpaqueAuth<'a, R>, - user_store: U + user_api: U } #[async_trait] impl<'a, R, U> AuthAPI for ClientAuth<'a, R, U> where - R: ServerConnectionRegister + ServerConnectionLogin + Clone + Sync + Send, - U: UserAuthStore + Sync + Send, + R: ServerConnectionRegister + ServerConnectionLogin, + U: UserAPI, { async fn register_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result<(), String> { let auth_data = OpaqueAuthData::from_secret_key(email.as_str(), secret_key.as_slice()); 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, data: CodeLoginData) -> 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)) + 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, + &key_login_session.0.session_key + ).map_err(|e| format!("error: {e:?}"))?; + self.user_api.set_code_login_data(signed_session).await } async fn login_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result { @@ -49,28 +56,29 @@ where async fn get_new_icons( &self, - key_login_session: &KeyLoggedInSession, ) -> Result, String> { - // self.nkode_api - // .get_new_icons(key_login_session) - // .await - // - todo!() + self.user_api.get_new_icons().await } async fn get_login_data( &self, key_login_session: &KeyLoggedInSession, ) -> Result { - // self.nkode_api - // .get_login_data(key_login_session) - // .await - todo!() + let session = SignedSessionData::new( + key_login_session.0.session_id, + key_login_session.0.email.clone(), + &key_login_session.0.session_key + ).map_err(|e| format!("error: {e:?}"))?; + self.user_api.get_login_data(session).await } async fn is_code_registered(&self, key_login_session: &KeyLoggedInSession) -> Result { - // self.nkode_api.is_code_registered(key_login_session).await - todo!() + let session = SignedSessionData::new( + key_login_session.0.session_id, + key_login_session.0.email.clone(), + &key_login_session.0.session_key + ).map_err(|e| format!("error: {e:?}"))?; + self.user_api.is_code_registered(session).await } async fn get_policy(&self) -> Result { diff --git a/src/client/opaque.rs b/src/client/opaque.rs index de0ebbf..a1c5ba6 100644 --- a/src/client/opaque.rs +++ b/src/client/opaque.rs @@ -9,7 +9,7 @@ use opaque_ke::{ RegistrationRequest, }; use crate::shared::models::app::LoggedInSession; -use crate::shared::models::opaque::{OpaqueRegisterSession, OpaqueLoginSession, NKodeCipherSuite, PasswordFile}; +use crate::shared::opaque::{OpaqueRegisterSession, OpaqueLoginSession, NKodeCipherSuite, PasswordFile}; #[derive(Debug)] pub enum ClientAuthError { @@ -55,7 +55,7 @@ impl OpaqueAuthData { } #[async_trait] -pub trait ServerConnectionRegister { +pub trait ServerConnectionRegister: Send + Sync { async fn start( &self, identifier: &[u8], @@ -70,7 +70,7 @@ pub trait ServerConnectionRegister { } #[async_trait] -pub trait ServerConnectionLogin { +pub trait ServerConnectionLogin: Send + Sync { async fn start( &self, identifier: &[u8], diff --git a/src/client/states.rs b/src/client/states.rs index 1d68370..bf4d12d 100644 --- a/src/client/states.rs +++ b/src/client/states.rs @@ -4,8 +4,8 @@ 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::email::Email; -use crate::shared::models::opaque::UserSecretKey; +use crate::shared::email::Email; +use crate::shared::opaque::UserSecretKey; struct KeyLogin; @@ -68,7 +68,7 @@ impl UserStateKeyLoggedIn { async fn get_icons(&self, icon_nonce: &Nonce) -> Result, String> { let chacha20_key = self.user_secret_key.chacha20_secret_key(); let mut chacha_cipher = nkode_rs::nkode_core::chacha20prng::ChaCha20PRNG::new(&chacha20_key, icon_nonce); - let mut icons = self.api.get_new_icons(&self.key_login).await?; + let mut icons = self.api.get_new_icons().await?; for icon in &mut icons { let bytes = chacha_cipher.read_u8(ICON_ID_SIZE)?; let new_id = IconID::from_bytes(bytes.as_slice()).unwrap(); @@ -123,7 +123,7 @@ impl UserStateCodeRegister { icon_nonce: self.icon_nonce, keypad, }; - self.api.register_code(&self.email, &ciphered_nkode.passcode, &self.key_login, &data).await?; + self.api.register_code(&self.email, &ciphered_nkode.passcode, &self.key_login, data.clone()).await?; Ok(UserStateCodeLogin { api: self.api, mask: data.mask, diff --git a/src/server/app.rs b/src/server/app.rs index 5e20f17..149829f 100644 --- a/src/server/app.rs +++ b/src/server/app.rs @@ -5,20 +5,20 @@ use opaque_ke::argon2::password_hash::rand_core::OsRng; use nkode_rs::from_bytes::FromBytes; use crate::server::models::CredKind; use crate::server::repository::opaque_repo::{AuthRepoError, OpaqueDatabaseRepo, OpaqueSessionRepo}; -use crate::shared::models::store::UserAuthStore; +use crate::server::repository::UserRepo::UserRepo; use crate::shared::models::app::{CodeLoggedInSession, CodeLoginData, KeyLoggedInSession, LoggedInSession}; -use crate::shared::models::email::Email; -use crate::shared::models::opaque::{NKodeCipherSuite, NKodeServerSetup, OpaqueLoginSession, OpaqueRegisterSession, OpaqueSessionKey, PasswordFile}; +use crate::shared::email::Email; +use crate::shared::opaque::{NKodeCipherSuite, NKodeServerSetup, OpaqueLoginSession, OpaqueRegisterSession, OpaqueSessionKey, PasswordFile}; -pub struct ServerApp { +pub struct ServerApp { server_setup: NKodeServerSetup, opaque_db: R, opaque_sess: S, user_db: U, } -impl ServerApp { +impl ServerApp { pub fn new(server_setup: NKodeServerSetup, opaque_db: R, opaque_sess: S, user_db: U) -> Self { Self { server_setup, opaque_db, opaque_sess, user_db} } diff --git a/src/server/models.rs b/src/server/models.rs index f56badb..525ea43 100644 --- a/src/server/models.rs +++ b/src/server/models.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use uuid::Uuid; use opaque_ke::ServerLogin; use crate::server::repository::opaque_repo::{AuthRepoError, OpaqueDatabaseRepo}; -use crate::shared::models::opaque::{NKodeCipherSuite, PasswordFile}; +use crate::shared::opaque::{NKodeCipherSuite, PasswordFile}; pub struct RegCache { pub session_id: Uuid, @@ -16,7 +16,7 @@ pub struct LoginCache { } #[async_trait] -pub trait CredKind { +pub trait CredKind: Send + Sync { async fn has(repo: &R, id: &[u8]) -> bool; async fn get_password_file(repo: &R, id: &[u8]) -> Result; async fn set_password_file(repo: &R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError>; diff --git a/src/shared/models/store.rs b/src/server/repository/UserRepo.rs similarity index 91% rename from src/shared/models/store.rs rename to src/server/repository/UserRepo.rs index 48e7d06..5135e26 100644 --- a/src/shared/models/store.rs +++ b/src/server/repository/UserRepo.rs @@ -1,10 +1,10 @@ use async_trait::async_trait; use uuid::Uuid; use crate::shared::models::app::{CodeLoggedInSession, CodeLoginData, KeyLoggedInSession}; -use crate::shared::models::email::Email; +use crate::shared::email::Email; #[async_trait] -pub trait UserAuthStore { +pub trait UserRepo: Send + Sync { async fn get_key_session(&self, session_id: &Uuid) -> Result; async fn get_code_session(&self, session_id: &Uuid) -> Result; diff --git a/src/server/repository/in_memory/in_memory_opaque_db.rs b/src/server/repository/in_memory/in_memory_opaque_db.rs index 0ef1a5e..fcba944 100644 --- a/src/server/repository/in_memory/in_memory_opaque_db.rs +++ b/src/server/repository/in_memory/in_memory_opaque_db.rs @@ -1,5 +1,5 @@ use std::collections::HashMap; -use crate::shared::models::opaque::PasswordFile; +use crate::shared::opaque::PasswordFile; use crate::server::repository::opaque_repo::{OpaqueDatabaseRepo, AuthRepoError}; use tokio::sync::Mutex; use std::sync::Arc; diff --git a/src/server/repository/in_memory/in_memory_opaque_session.rs b/src/server/repository/in_memory/in_memory_opaque_session.rs index 5663e84..02b87d8 100644 --- a/src/server/repository/in_memory/in_memory_opaque_session.rs +++ b/src/server/repository/in_memory/in_memory_opaque_session.rs @@ -5,7 +5,7 @@ use opaque_ke::ServerLogin; use tokio::sync::Mutex; use uuid::Uuid; use crate::server::models::{LoginCache, RegCache}; -use crate::shared::models::opaque::NKodeCipherSuite; +use crate::shared::opaque::NKodeCipherSuite; use crate::server::repository::opaque_repo::OpaqueSessionRepo; #[derive(Default)] diff --git a/src/server/repository/in_memory/in_memory_transport.rs b/src/server/repository/in_memory/in_memory_transport.rs index 4acd737..bcc8fc6 100644 --- a/src/server/repository/in_memory/in_memory_transport.rs +++ b/src/server/repository/in_memory/in_memory_transport.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use std::marker::PhantomData; use uuid::Uuid; use opaque_ke::{CredentialFinalization, CredentialRequest, RegistrationRequest}; -use crate::shared::models::opaque::{NKodeCipherSuite, OpaqueLoginSession, OpaqueRegisterSession, PasswordFile}; +use crate::shared::opaque::{NKodeCipherSuite, OpaqueLoginSession, OpaqueRegisterSession, PasswordFile}; use crate::client::opaque::{ClientAuthError, ServerConnectionLogin, ServerConnectionRegister}; use crate::server::app::{Code, Key}; use crate::server::app::ServerApp; @@ -12,15 +12,15 @@ use crate::server::repository::in_memory::in_memory_opaque_session::InMemoryOpaq use crate::server::repository::in_memory::in_memory_user_db::InMemoryUserDB; use crate::shared::models::app::LoggedInSession; -pub type InMemoryKeyServer<'a> = InMemoryServer<'a, Key>; -pub type InMemoryCodeServer<'a> = InMemoryServer<'a, Code>; +pub type InMemoryKeyServer<'a> = InMemoryServerTransport<'a, Key>; +pub type InMemoryCodeServer<'a> = InMemoryServerTransport<'a, Code>; -pub struct InMemoryServer<'a, K: CredKind> { +pub struct InMemoryServerTransport<'a, K: CredKind> { auth_db: &'a ServerApp, _kind: PhantomData, } -impl<'a, K: CredKind> InMemoryServer<'a, K> { +impl<'a, K: CredKind> InMemoryServerTransport<'a, K> { pub fn new(server_app: &'a ServerApp) -> Self { Self { auth_db: server_app, @@ -30,17 +30,15 @@ impl<'a, K: CredKind> InMemoryServer<'a, K> { } #[async_trait] -impl<'a, K> ServerConnectionRegister for InMemoryServer<'a, K> +impl<'a, K> ServerConnectionRegister for InMemoryServerTransport<'a, K> where - K: CredKind + Sync, + K: CredKind, { async fn start( &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_db .reg_start::(identifier, message.clone()) .await @@ -59,7 +57,7 @@ where } #[async_trait] -impl<'a, K> ServerConnectionLogin for InMemoryServer<'a,K> +impl<'a, K> ServerConnectionLogin for InMemoryServerTransport<'a,K> where K: CredKind + Send + Sync, { 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 6a0213a..080f1bb 100644 --- a/src/server/repository/in_memory/in_memory_user_db.rs +++ b/src/server/repository/in_memory/in_memory_user_db.rs @@ -3,9 +3,9 @@ use std::sync::Arc; use async_trait::async_trait; use tokio::sync::Mutex; use uuid::Uuid; -use crate::shared::models::store::UserAuthStore; +use crate::server::repository::UserRepo::UserRepo; use crate::shared::models::app::{CodeLoggedInSession, CodeLoginData, KeyLoggedInSession}; -use crate::shared::models::email::Email; +use crate::shared::email::Email; pub struct InMemoryUserDB { key_session: Arc>>, @@ -30,7 +30,7 @@ impl Default for InMemoryUserDB { } #[async_trait] -impl UserAuthStore for InMemoryUserDB { +impl UserRepo for InMemoryUserDB { async fn get_key_session(&self, session_id: &Uuid) -> Result { self.key_session.lock().await .get(&session_id) diff --git a/src/server/repository/mod.rs b/src/server/repository/mod.rs index c57c477..c632124 100644 --- a/src/server/repository/mod.rs +++ b/src/server/repository/mod.rs @@ -1,2 +1,3 @@ pub mod in_memory; pub mod opaque_repo; +pub mod UserRepo; diff --git a/src/server/repository/opaque_repo.rs b/src/server/repository/opaque_repo.rs index b365928..c4afd72 100644 --- a/src/server/repository/opaque_repo.rs +++ b/src/server/repository/opaque_repo.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use uuid::Uuid; use opaque_ke::ServerLogin; use crate::server::models::{LoginCache, RegCache}; -use crate::shared::models::opaque::{NKodeCipherSuite, PasswordFile}; +use crate::shared::opaque::{NKodeCipherSuite, PasswordFile}; #[derive(Debug)] pub enum AuthRepoError { UserExists, diff --git a/src/shared/models/email.rs b/src/shared/email.rs similarity index 91% rename from src/shared/models/email.rs rename to src/shared/email.rs index d70331e..14a4835 100644 --- a/src/shared/models/email.rs +++ b/src/shared/email.rs @@ -1,7 +1,8 @@ use std::fmt; use email_address::{EmailAddress, Options}; +use serde::{Deserialize, Serialize}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct Email(String); #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/src/shared/mod.rs b/src/shared/mod.rs index ff92946..cd6a41e 100644 --- a/src/shared/mod.rs +++ b/src/shared/mod.rs @@ -1 +1,5 @@ -pub mod models; \ No newline at end of file +pub mod models; +pub mod email; +pub mod opaque; +pub mod signed_session_data; +pub mod user_api; \ No newline at end of file diff --git a/src/shared/models/app.rs b/src/shared/models/app.rs index 249f6ba..72d2cdf 100644 --- a/src/shared/models/app.rs +++ b/src/shared/models/app.rs @@ -5,8 +5,8 @@ use getset::Getters; use nkode_rs::from_bytes::FromBytes; use nkode_rs::nkode_core::policy::NKodePolicy; use uuid::Uuid; -use crate::shared::models::email::Email; -use crate::shared::models::opaque::{OpaqueSessionKey, UserSecretKey}; +use crate::shared::email::Email; +use crate::shared::opaque::{OpaqueSessionKey, UserSecretKey}; #[derive(Debug, Clone)] pub struct LoggedInSession { @@ -60,11 +60,11 @@ pub struct CodeLoginData { #[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, data: CodeLoginData) -> Result<(), String>; async fn login_key(&self, email: &Email, secret_key: &UserSecretKey) -> Result; async fn login_code(&self, email: &Email, passcode: &[u64]) -> Result; - async fn get_new_icons(&self, key_login_session: &KeyLoggedInSession) -> Result, String>; + async fn get_new_icons(&self) -> Result, String>; async fn get_login_data(&self, key_login_session: &KeyLoggedInSession) -> Result; async fn is_code_registered(&self, key_login_session: &KeyLoggedInSession) -> Result; async fn get_policy(&self) -> Result; diff --git a/src/shared/models/mod.rs b/src/shared/models/mod.rs index d5a32d5..309be62 100644 --- a/src/shared/models/mod.rs +++ b/src/shared/models/mod.rs @@ -1,4 +1 @@ -pub mod opaque; pub mod app; -pub mod email; -pub mod store; \ No newline at end of file diff --git a/src/shared/models/opaque.rs b/src/shared/opaque.rs similarity index 95% rename from src/shared/models/opaque.rs rename to src/shared/opaque.rs index c92a57d..727563b 100644 --- a/src/shared/models/opaque.rs +++ b/src/shared/opaque.rs @@ -52,6 +52,12 @@ impl FromBytes for OpaqueSessionKey { } } +impl OpaqueSessionKey { + pub fn as_bytes(&self) -> &[u8] { + self.0.as_slice() + } +} + pub struct NKodeCipherSuite; impl CipherSuite for NKodeCipherSuite { diff --git a/src/shared/signed_session_data.rs b/src/shared/signed_session_data.rs new file mode 100644 index 0000000..cbfc894 --- /dev/null +++ b/src/shared/signed_session_data.rs @@ -0,0 +1,110 @@ +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use uuid::Uuid; + +use hmac::{Hmac, Mac}; +use hkdf::Hkdf; +use sha2::Sha256; + +use crate::shared::opaque::OpaqueSessionKey; + +type HmacSha256 = Hmac; + +pub const SIGNATURE_SIZE: usize = 32; + +/// Domain separation + versioning so you can safely change formats later. +const SIGNING_DOMAIN: &[u8] = b"nkode-protocol:signed-session-data:v1"; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Signature(pub [u8; SIGNATURE_SIZE]); + +// TODO: This there's should be two signature types +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(bound( + serialize = "T: Serialize", + deserialize = "T: DeserializeOwned", +))] +pub struct SignedSessionData { + pub session_id: Uuid, + pub data: T, + pub signature: Signature, +} + +#[derive(Debug, thiserror::Error)] +pub enum SignedSessionError { + #[error("failed to serialize data for signing: {0}")] + Serialize(#[from] postcard::Error), + #[error("invalid signature")] + InvalidSignature, +} + +impl SignedSessionData +where + T: Serialize + DeserializeOwned, +{ + /// Create a signed envelope around (session_id, issued/expires, data). + pub fn new( + session_id: Uuid, + data: T, + session_key: &OpaqueSessionKey, + ) -> Result { + let signature = Self::compute_signature(session_id, &data, session_key)?; + Ok(Self { + session_id, + // issued_at_unix, + // expires_at_unix, + data, + signature, + }) + } + + /// Verify signature + time bounds. + pub fn verify(&self, session_key: &OpaqueSessionKey) -> Result<(), SignedSessionError> { + let expected = Self::compute_signature( + self.session_id, + &self.data, + session_key, + )?; + if !constant_time_eq(&self.signature.0, &expected.0) { + return Err(SignedSessionError::InvalidSignature); + } + Ok(()) + } + + fn compute_signature( + session_id: Uuid, + data: &T, + session_key: &OpaqueSessionKey, + ) -> Result { + let auth_key = derive_auth_key(session_key.as_bytes()); + let mut msg = Vec::with_capacity(128); + msg.extend_from_slice(SIGNING_DOMAIN); + msg.extend_from_slice(session_id.as_bytes()); + let data_bytes = postcard::to_stdvec(data)?; + msg.extend_from_slice(&data_bytes); + let mut mac = HmacSha256::new_from_slice(&auth_key).expect("HMAC can take any key size"); + mac.update(&msg); + let tag = mac.finalize().into_bytes(); + let mut out = [0u8; SIGNATURE_SIZE]; + out.copy_from_slice(&tag[..SIGNATURE_SIZE]); + Ok(Signature(out)) + } +} + +fn derive_auth_key(session_key: &[u8]) -> [u8; 32] { + let hk = Hkdf::::new(None, session_key); + let mut out = [0u8; 32]; + hk.expand(b"myapp:hkdf:api-auth-key:v1", &mut out) + .expect("HKDF expand"); + out +} + +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; + } + let mut r = 0u8; + for i in 0..a.len() { + r |= a[i] ^ b[i]; + } + r == 0 +} diff --git a/src/shared/user_api.rs b/src/shared/user_api.rs new file mode 100644 index 0000000..f3f7c45 --- /dev/null +++ b/src/shared/user_api.rs @@ -0,0 +1,13 @@ +use async_trait::async_trait; +use crate::shared::email::Email; +use crate::shared::models::app::{CodeLoginData, Icon}; +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_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>; +} \ No newline at end of file diff --git a/tests/in_memory_test.rs b/tests/in_memory_test.rs index 8d5af40..28ab98c 100644 --- a/tests/in_memory_test.rs +++ b/tests/in_memory_test.rs @@ -3,8 +3,8 @@ use nkode_protocol::client::opaque::{OpaqueAuthData, ClientAuthError, OpaqueAuth use nkode_protocol::server::app::{ServerApp}; use nkode_protocol::server::repository::in_memory::in_memory_opaque_db::InMemoryOpaqueDB; use nkode_protocol::server::repository::in_memory::in_memory_opaque_session::InMemoryOpaqueSession; -use nkode_protocol::shared::models::opaque::NKodeServerSetup; -use nkode_protocol::server::repository::in_memory::in_memory_transport::{InMemoryCodeServer, InMemoryKeyServer, InMemoryServer}; +use nkode_protocol::shared::opaque::NKodeServerSetup; +use nkode_protocol::server::repository::in_memory::in_memory_transport::{InMemoryCodeServer, InMemoryKeyServer, InMemoryServerTransport}; use nkode_protocol::server::repository::in_memory::in_memory_user_db::InMemoryUserDB; #[tokio::test] @@ -17,7 +17,7 @@ async fn opaque_key_registration_and_login_roundtrip() { InMemoryOpaqueSession::new(), InMemoryUserDB::new() ); - let key_server: InMemoryKeyServer = InMemoryServer::new(&server); + let key_server: InMemoryKeyServer = InMemoryServerTransport::new(&server); let auth = OpaqueAuth::new(&key_server); let auth_data = OpaqueAuthData::from_secret_key("a@b.com", b"supersecret16bytes"); auth.register(&auth_data).await.expect("registration should succeed"); @@ -58,7 +58,7 @@ async fn cannot_register_code_before_key() { InMemoryOpaqueSession::new(), InMemoryUserDB::new() ); - let key_server: InMemoryCodeServer = InMemoryServer::new(&server); + let key_server: InMemoryCodeServer = InMemoryServerTransport::new(&server); let auth = OpaqueAuth::new(&key_server); let auth_data = OpaqueAuthData::from_code("x@y.com", &[1u64,2,3,4]); let err = auth.register(&auth_data)