implement server app

This commit is contained in:
2025-12-17 13:26:40 -06:00
parent 3029a41386
commit ac2ddf86df
5 changed files with 104 additions and 91 deletions

View File

@@ -1,3 +1,4 @@
use async_trait::async_trait;
use uuid::Uuid; use uuid::Uuid;
use opaque_ke::{CredentialFinalization, CredentialRequest, RegistrationRequest, ServerLogin, ServerLoginParameters, ServerRegistration}; use opaque_ke::{CredentialFinalization, CredentialRequest, RegistrationRequest, ServerLogin, ServerLoginParameters, ServerRegistration};
use opaque_ke::argon2::password_hash::rand_core::OsRng; use opaque_ke::argon2::password_hash::rand_core::OsRng;
@@ -26,7 +27,7 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
identifier: &[u8], identifier: &[u8],
request: RegistrationRequest<NKodeCipherSuite>, request: RegistrationRequest<NKodeCipherSuite>,
) -> Result<OpaqueRegisterSession, String> { ) -> Result<OpaqueRegisterSession, String> {
K::prereq_for_register(&self.opaque_db, identifier) K::prereq_for_register(&self.opaque_db, identifier).await
.map_err(|e| format!("registration prereq failed: {e:?}"))?; .map_err(|e| format!("registration prereq failed: {e:?}"))?;
let start = ServerRegistration::<NKodeCipherSuite>::start( let start = ServerRegistration::<NKodeCipherSuite>::start(
&self.server_setup, &self.server_setup,
@@ -34,7 +35,7 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
identifier, identifier,
).map_err(|e| format!("opaque reg start: {e:?}"))?; ).map_err(|e| format!("opaque reg start: {e:?}"))?;
let cache = self.opaque_sess let cache = self.opaque_sess
.new_reg_session(identifier) .new_reg_session(identifier).await
.map_err(|e| format!("reg cache: {e}"))?; .map_err(|e| format!("reg cache: {e}"))?;
Ok(OpaqueRegisterSession { session_id: cache.session_id, response: start.message }) Ok(OpaqueRegisterSession { session_id: cache.session_id, response: start.message })
@@ -46,14 +47,14 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
password_file: PasswordFile, password_file: PasswordFile,
) -> Result<(), String> { ) -> Result<(), String> {
let sess = self.opaque_sess let sess = self.opaque_sess
.get_reg_session(session_id) .get_reg_session(session_id).await
.map_err(|e| format!("get reg session: {e}"))?; .map_err(|e| format!("get reg session: {e}"))?;
K::prereq_for_register(&self.opaque_db, sess.identifier.as_slice()) K::prereq_for_register(&self.opaque_db, sess.identifier.as_slice()).await
.map_err(|e| format!("registration prereq failed: {e:?}"))?; .map_err(|e| format!("registration prereq failed: {e:?}"))?;
K::set_password_file(&mut self.opaque_db, sess.identifier.as_slice(), password_file) K::set_password_file(&mut self.opaque_db, sess.identifier.as_slice(), password_file).await
.map_err(|e| format!("repo write: {e:?}"))?; .map_err(|e| format!("repo write: {e:?}"))?;
self.opaque_sess self.opaque_sess
.clear_reg_session(session_id) .clear_reg_session(session_id).await
.map_err(|e| format!("clear reg session: {e}")) .map_err(|e| format!("clear reg session: {e}"))
} }
@@ -62,7 +63,7 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
identifier: &[u8], identifier: &[u8],
request: CredentialRequest<NKodeCipherSuite>, request: CredentialRequest<NKodeCipherSuite>,
) -> Result<OpaqueLoginSession, String> { ) -> Result<OpaqueLoginSession, String> {
let password_file = K::get_password_file(&self.opaque_db, identifier) let password_file = K::get_password_file(&self.opaque_db, identifier).await
.map_err(|e| format!("repo read: {e:?}"))?; .map_err(|e| format!("repo read: {e:?}"))?;
let password_file = let password_file =
@@ -78,7 +79,7 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
ServerLoginParameters::default(), ServerLoginParameters::default(),
).map_err(|e| format!("opaque login start: {e:?}"))?; ).map_err(|e| format!("opaque login start: {e:?}"))?;
let cache = self.opaque_sess let cache = self.opaque_sess
.new_login_session(identifier, start.state) .new_login_session(identifier, start.state).await
.map_err(|e| format!("login cache: {e}"))?; .map_err(|e| format!("login cache: {e}"))?;
Ok(OpaqueLoginSession { session_id: cache.session_id, response: start.message }) Ok(OpaqueLoginSession { session_id: cache.session_id, response: start.message })
} }
@@ -89,15 +90,14 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
finalize: CredentialFinalization<NKodeCipherSuite>, finalize: CredentialFinalization<NKodeCipherSuite>,
) -> Result<LoggedInSession, String> { ) -> Result<LoggedInSession, String> {
let cache = self.opaque_sess let cache = self.opaque_sess
.get_login_session(session_id) .get_login_session(session_id).await
.map_err(|e| format!("get login session: {e}"))?; .map_err(|e| format!("get login session: {e}"))?;
let finish = cache.server_login let finish = cache.server_login
.finish(finalize, ServerLoginParameters::default()) .finish(finalize, ServerLoginParameters::default())
.map_err(|e| format!("opaque login finish: {e:?}"))?; .map_err(|e| format!("opaque login finish: {e:?}"))?;
self.opaque_sess self.opaque_sess
.clear_login_session(session_id) .clear_login_session(session_id).await
.map_err(|e| format!("clear login session: {e}"))?; .map_err(|e| format!("clear login session: {e}"))?;
let session_key = OpaqueSessionKey::from_bytes(&finish.session_key.to_vec()).unwrap(); let session_key = OpaqueSessionKey::from_bytes(&finish.session_key.to_vec()).unwrap();
Ok(LoggedInSession{ Ok(LoggedInSession{
@@ -144,36 +144,38 @@ impl<R: OpaqueDatabaseRepo, S: OpaqueSessionRepo, U: UserRepo> ServerApp<R, S, U
} }
} }
#[derive(Clone)] #[derive(Debug,Clone )]
pub struct Key; pub struct Key;
#[derive(Clone)] #[derive(Debug,Clone)]
pub struct Code; pub struct Code;
#[async_trait]
impl CredKind for Key { impl CredKind for Key {
fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool { async fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool {
repo.has_key(id) repo.has_key(id).await
} }
fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError> { async fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError> {
repo.get_key_passcode_file(id) repo.get_key_passcode_file(id).await
} }
fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError> { async fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError> {
repo.new_key(id, pf) repo.new_key(id, pf).await
} }
} }
#[async_trait]
impl CredKind for Code { impl CredKind for Code {
fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool { async fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool {
repo.has_code(id) repo.has_code(id).await
} }
fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError> { async fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError> {
repo.get_code_passcode_file(id) repo.get_code_passcode_file(id).await
} }
fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError> { async fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError> {
repo.new_code(id, pf) repo.new_code(id, pf).await
} }
fn prereq_for_register<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<(), AuthRepoError> { async fn prereq_for_register<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<(), AuthRepoError> {
if repo.has_key(id) { if repo.has_key(id).await {
Ok(()) Ok(())
} else { } else {
Err(AuthRepoError::KeyNotRegistered) Err(AuthRepoError::KeyNotRegistered)

View File

@@ -1,3 +1,4 @@
use async_trait::async_trait;
use uuid::Uuid; use uuid::Uuid;
use opaque_ke::ServerLogin; use opaque_ke::ServerLogin;
use crate::server::repository::opaque_repo::{AuthRepoError, OpaqueDatabaseRepo}; use crate::server::repository::opaque_repo::{AuthRepoError, OpaqueDatabaseRepo};
@@ -14,11 +15,12 @@ pub struct LoginCache {
pub server_login: ServerLogin<NKodeCipherSuite>, pub server_login: ServerLogin<NKodeCipherSuite>,
} }
#[async_trait]
pub trait CredKind { pub trait CredKind {
fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool; async fn has<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> bool;
fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError>; async fn get_password_file<R: OpaqueDatabaseRepo>(repo: &R, id: &[u8]) -> Result<PasswordFile, AuthRepoError>;
fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError>; async fn set_password_file<R: OpaqueDatabaseRepo>(repo: &mut R, id: &[u8], pf: PasswordFile) -> Result<(), AuthRepoError>;
fn prereq_for_register<R: OpaqueDatabaseRepo>(_repo: &R, _id: &[u8]) -> Result<(), AuthRepoError> { async fn prereq_for_register<R: OpaqueDatabaseRepo>(_repo: &R, _id: &[u8]) -> Result<(), AuthRepoError> {
Ok(()) Ok(())
} }
} }

View File

@@ -1,11 +1,14 @@
use std::collections::HashMap; use std::collections::HashMap;
use crate::shared::models::opaque::PasswordFile; use crate::shared::models::opaque::PasswordFile;
use crate::server::repository::opaque_repo::{OpaqueDatabaseRepo, AuthRepoError}; use crate::server::repository::opaque_repo::{OpaqueDatabaseRepo, AuthRepoError};
use tokio::sync::Mutex;
use std::sync::Arc;
use async_trait::async_trait;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct InMemoryOpaqueDB { pub struct InMemoryOpaqueDB {
key_entries: HashMap<KeyID, PasswordFile>, key_entries: Arc<Mutex<HashMap<KeyID, PasswordFile>>>,
code_entries: HashMap<CodeID, PasswordFile>, code_entries: Arc<Mutex<HashMap<CodeID, PasswordFile>>>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
@@ -19,66 +22,67 @@ impl InMemoryOpaqueDB {
Self::default() Self::default()
} }
fn code_exists(&self, identifier: &CodeID) -> bool { async fn code_exists(&self, identifier: &CodeID) -> bool {
self.code_entries.contains_key(identifier) self.code_entries.lock().await.contains_key(identifier)
} }
fn key_exists(&self, identifier: &KeyID) -> bool { async fn key_exists(&self, identifier: &KeyID) -> bool {
self.key_entries.contains_key(identifier) self.key_entries.lock().await.contains_key(identifier)
} }
} }
#[async_trait]
impl OpaqueDatabaseRepo for InMemoryOpaqueDB { impl OpaqueDatabaseRepo for InMemoryOpaqueDB {
fn new_key( async fn new_key(
&mut self, &self,
identifier: &[u8], identifier: &[u8],
password_file: PasswordFile, password_file: PasswordFile,
) -> Result<(), AuthRepoError> { ) -> Result<(), AuthRepoError> {
if self.key_exists(&KeyID(identifier.to_vec())) { if self.key_exists(&KeyID(identifier.to_vec())).await {
return Err(AuthRepoError::UserExists); return Err(AuthRepoError::UserExists);
} }
self.key_entries.lock().await
self.key_entries
.insert(KeyID(identifier.to_vec()), password_file); .insert(KeyID(identifier.to_vec()), password_file);
Ok(()) Ok(())
} }
fn new_code( async fn new_code(
&mut self, &self,
identifier: &[u8], identifier: &[u8],
password_file: PasswordFile, password_file: PasswordFile,
) -> Result<(), AuthRepoError> { ) -> Result<(), AuthRepoError> {
if !self.has_key(identifier) { if !self.has_key(identifier).await {
return Err(AuthRepoError::KeyNotRegistered); return Err(AuthRepoError::KeyNotRegistered);
} }
if self.code_exists(&CodeID(identifier.to_vec())) { if self.code_exists(&CodeID(identifier.to_vec())).await {
return Err(AuthRepoError::UserExists); return Err(AuthRepoError::UserExists);
} }
self.code_entries self.code_entries.lock().await
.insert(CodeID(identifier.to_vec()), password_file); .insert(CodeID(identifier.to_vec()), password_file);
Ok(()) Ok(())
} }
fn has_code(&self, identifier: &[u8]) -> bool { async fn has_code(&self, identifier: &[u8]) -> bool {
self.code_entries self.code_entries.lock().await
.contains_key(&CodeID(identifier.to_vec())) .contains_key(&CodeID(identifier.to_vec()))
} }
fn has_key(&self, identifier: &[u8]) -> bool { async fn has_key(&self, identifier: &[u8]) -> bool {
self.key_entries self.key_entries.lock().await
.contains_key(&KeyID(identifier.to_vec())) .contains_key(&KeyID(identifier.to_vec()))
} }
fn get_key_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError> { async fn get_key_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError> {
self.key_entries self.key_entries.lock().await
.get(&KeyID(identifier.to_vec())) .get(&KeyID(identifier.to_vec()))
.cloned() .cloned()
.ok_or(AuthRepoError::KeyNotRegistered) .ok_or(AuthRepoError::KeyNotRegistered)
} }
fn get_code_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError> { async fn get_code_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError> {
self.code_entries self.code_entries.lock().await
.get(&CodeID(identifier.to_vec())) .get(&CodeID(identifier.to_vec()))
.cloned() .cloned()
.ok_or(AuthRepoError::CodeNotRegistered) .ok_or(AuthRepoError::CodeNotRegistered)

View File

@@ -1,5 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use async_trait::async_trait;
use opaque_ke::ServerLogin; use opaque_ke::ServerLogin;
use tokio::sync::Mutex;
use uuid::Uuid; use uuid::Uuid;
use crate::server::models::{LoginCache, RegCache}; use crate::server::models::{LoginCache, RegCache};
use crate::shared::models::opaque::NKodeCipherSuite; use crate::shared::models::opaque::NKodeCipherSuite;
@@ -7,8 +11,8 @@ use crate::server::repository::opaque_repo::OpaqueSessionRepo;
#[derive(Default)] #[derive(Default)]
pub struct InMemoryOpaqueSession { pub struct InMemoryOpaqueSession {
reg_sessions: HashMap<Uuid, RegCache>, reg_sessions: Arc<Mutex<HashMap<Uuid, RegCache>>>,
login_sessions: HashMap<Uuid, LoginCache>, login_sessions: Arc<Mutex<HashMap<Uuid, LoginCache>>>,
} }
impl InMemoryOpaqueSession { impl InMemoryOpaqueSession {
@@ -17,28 +21,27 @@ impl InMemoryOpaqueSession {
} }
} }
#[async_trait]
impl OpaqueSessionRepo for InMemoryOpaqueSession { impl OpaqueSessionRepo for InMemoryOpaqueSession {
fn new_reg_session(&mut self, identifier: &[u8]) -> Result<RegCache, String> { async fn new_reg_session(&self, identifier: &[u8]) -> Result<RegCache, String> {
let cache = RegCache { let cache = RegCache {
session_id: Uuid::new_v4(), session_id: Uuid::new_v4(),
identifier: identifier.to_vec(), identifier: identifier.to_vec(),
}; };
if self.reg_sessions.lock().await.contains_key(&cache.session_id) {
// Extremely unlikely collision, but keep the invariant anyway.
if self.reg_sessions.contains_key(&cache.session_id) {
return Err("session_id collision".to_string()); return Err("session_id collision".to_string());
} }
self.reg_sessions.lock().await.insert(cache.session_id, RegCache {
self.reg_sessions.insert(cache.session_id, RegCache {
session_id: cache.session_id, session_id: cache.session_id,
identifier: cache.identifier.clone(), identifier: cache.identifier.clone(),
}); });
Ok(cache) Ok(cache)
} }
fn get_reg_session(&self, session_id: &Uuid) -> Result<RegCache, String> { async fn get_reg_session(&self, session_id: &Uuid) -> Result<RegCache, String> {
self.reg_sessions self.reg_sessions
.lock()
.await
.get(session_id) .get(session_id)
.map(|c| RegCache { .map(|c| RegCache {
session_id: c.session_id, session_id: c.session_id,
@@ -47,15 +50,17 @@ impl OpaqueSessionRepo for InMemoryOpaqueSession {
.ok_or_else(|| "registration session not found".to_string()) .ok_or_else(|| "registration session not found".to_string())
} }
fn clear_reg_session(&mut self, session_id: &Uuid) -> Result<(), String> { async fn clear_reg_session(&self, session_id: &Uuid) -> Result<(), String> {
self.reg_sessions self.reg_sessions
.lock()
.await
.remove(session_id) .remove(session_id)
.map(|_| ()) .map(|_| ())
.ok_or_else(|| "registration session not found".to_string()) .ok_or_else(|| "registration session not found".to_string())
} }
fn new_login_session( async fn new_login_session(
&mut self, &self,
identifier: &[u8], identifier: &[u8],
server_login: ServerLogin<NKodeCipherSuite>, server_login: ServerLogin<NKodeCipherSuite>,
) -> Result<LoginCache, String> { ) -> Result<LoginCache, String> {
@@ -64,12 +69,10 @@ impl OpaqueSessionRepo for InMemoryOpaqueSession {
identifiers: identifier.to_vec(), identifiers: identifier.to_vec(),
server_login, server_login,
}; };
if self.login_sessions.lock().await.contains_key(&cache.session_id) {
if self.login_sessions.contains_key(&cache.session_id) {
return Err("session_id collision".to_string()); return Err("session_id collision".to_string());
} }
self.login_sessions.lock().await.insert(
self.login_sessions.insert(
cache.session_id, cache.session_id,
LoginCache { LoginCache {
session_id: cache.session_id, session_id: cache.session_id,
@@ -78,12 +81,11 @@ impl OpaqueSessionRepo for InMemoryOpaqueSession {
server_login: cache.server_login.clone(), server_login: cache.server_login.clone(),
}, },
); );
Ok(cache) Ok(cache)
} }
fn get_login_session(&self, session_id: &Uuid) -> Result<LoginCache, String> { async fn get_login_session(&self, session_id: &Uuid) -> Result<LoginCache, String> {
self.login_sessions self.login_sessions.lock().await
.get(session_id) .get(session_id)
.map(|c| LoginCache { .map(|c| LoginCache {
session_id: c.session_id, session_id: c.session_id,
@@ -93,8 +95,8 @@ impl OpaqueSessionRepo for InMemoryOpaqueSession {
.ok_or_else(|| "login session not found".to_string()) .ok_or_else(|| "login session not found".to_string())
} }
fn clear_login_session(&mut self, session_id: &Uuid) -> Result<(), String> { async fn clear_login_session(&self, session_id: &Uuid) -> Result<(), String> {
self.login_sessions self.login_sessions.lock().await
.remove(session_id) .remove(session_id)
.map(|_| ()) .map(|_| ())
.ok_or_else(|| "login session not found".to_string()) .ok_or_else(|| "login session not found".to_string())

View File

@@ -1,3 +1,4 @@
use async_trait::async_trait;
use uuid::Uuid; use uuid::Uuid;
use opaque_ke::ServerLogin; use opaque_ke::ServerLogin;
use crate::server::models::{LoginCache, RegCache}; use crate::server::models::{LoginCache, RegCache};
@@ -9,27 +10,29 @@ pub enum AuthRepoError {
CodeNotRegistered, CodeNotRegistered,
} }
pub trait OpaqueDatabaseRepo { #[async_trait]
fn new_key(&mut self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>; pub trait OpaqueDatabaseRepo: Send + Sync {
fn new_code(&mut self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>; async fn new_key(&self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>;
async fn new_code(&self, identifier: &[u8], password_file: PasswordFile) -> Result<(), AuthRepoError>;
fn has_code(&self, identifier: &[u8]) -> bool; async fn has_code(&self, identifier: &[u8]) -> bool;
fn has_key(&self, identifier: &[u8]) -> bool; async fn has_key(&self, identifier: &[u8]) -> bool;
fn get_key_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError>; async fn get_key_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError>;
fn get_code_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError>; async fn get_code_passcode_file(&self, identifier: &[u8]) -> Result<PasswordFile, AuthRepoError>;
} }
#[async_trait]
pub trait OpaqueSessionRepo { pub trait OpaqueSessionRepo {
fn new_reg_session(&mut self, identifier: &[u8]) -> Result<RegCache, String>; async fn new_reg_session(&self, identifier: &[u8]) -> Result<RegCache, String>;
fn get_reg_session(&self, session_id: &Uuid) -> Result<RegCache, String>; async fn get_reg_session(&self, session_id: &Uuid) -> Result<RegCache, String>;
fn clear_reg_session(&mut self, session_id: &Uuid) -> Result<(), String>; async fn clear_reg_session(&self, session_id: &Uuid) -> Result<(), String>;
fn new_login_session( async fn new_login_session(
&mut self, &self,
identifier: &[u8], identifier: &[u8],
server_login: ServerLogin<NKodeCipherSuite>, server_login: ServerLogin<NKodeCipherSuite>,
) -> Result<LoginCache, String>; ) -> Result<LoginCache, String>;
fn get_login_session(&self, session_id: &Uuid) -> Result<LoginCache, String>; async fn get_login_session(&self, session_id: &Uuid) -> Result<LoginCache, String>;
fn clear_login_session(&mut self, session_id: &Uuid) -> Result<(), String>; async fn clear_login_session(&self, session_id: &Uuid) -> Result<(), String>;
} }