diff --git a/Cargo.lock b/Cargo.lock index 5f35049..d1030d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95" +dependencies = [ + "generic-array", +] + [[package]] name = "buf_redux" version = "0.8.4" @@ -92,6 +101,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crypto-common" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0" +dependencies = [ + "generic-array", +] + [[package]] name = "digest" version = "0.9.0" @@ -101,6 +119,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "digest" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b" +dependencies = [ + "block-buffer 0.10.0", + "crypto-common", + "generic-array", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -403,7 +432,9 @@ name = "kampanja-kontrolada-servilo" version = "0.1.0" dependencies = [ "anyhow", + "rand 0.8.4", "rusqlite", + "sha2", "tempfile", "thiserror", "tokio", @@ -833,13 +864,24 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ - "block-buffer", + "block-buffer 0.9.0", "cfg-if", "cpufeatures", - "digest", + "digest 0.9.0", "opaque-debug", ] +[[package]] +name = "sha2" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.1", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" diff --git a/servilo/Cargo.toml b/servilo/Cargo.toml index 89bc321..d008cf8 100644 --- a/servilo/Cargo.toml +++ b/servilo/Cargo.toml @@ -7,7 +7,9 @@ edition = "2018" [dependencies] anyhow = { version = "1" } +rand = { version = "0.8" } rusqlite = { version = "0.26" } +sha2 = { version = "0.10" } thiserror = { version = "1" } tokio = { version = "1", features = ["full"] } uuid = { version = "0.8", features = ["v4"] } diff --git a/servilo/src/aŭtentigo.rs b/servilo/src/aŭtentigo.rs index ccd1a1c..838b3fe 100644 --- a/servilo/src/aŭtentigo.rs +++ b/servilo/src/aŭtentigo.rs @@ -1,5 +1,11 @@ use crate::datumbazo::Datumbazo; -use rusqlite::OptionalExtension; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use rusqlite::{ + params, + types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}, + OptionalExtension, ToSql, +}; +use sha2::{Digest, Sha256}; use std::{ collections::HashMap, convert::Infallible, @@ -10,24 +16,24 @@ use uuid::{adapter::Hyphenated, Uuid}; use warp::{reject, reject::Reject, Filter, Rejection}; #[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct Ĵetono(String); +pub struct SesiaĴetono(String); -impl From<&str> for Ĵetono { +impl From<&str> for SesiaĴetono { fn from(s: &str) -> Self { - Ĵetono(s.to_owned()) + SesiaĴetono(s.to_owned()) } } -impl FromStr for Ĵetono { +impl FromStr for SesiaĴetono { type Err = Infallible; fn from_str(s: &str) -> Result { - Ok(Ĵetono(s.to_owned())) + Ok(SesiaĴetono(s.to_owned())) } } -impl From<Ĵetono> for String { - fn from(s: Ĵetono) -> Self { +impl From for String { + fn from(s: SesiaĴetono) -> Self { s.0.clone() } } @@ -47,6 +53,17 @@ impl From for String { } } +impl FromSql for UzantIdentigilo { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Text(t) => Ok(UzantIdentigilo::from( + String::from_utf8(Vec::from(t)).unwrap().as_ref(), + )), + _ => Err(FromSqlError::InvalidType), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct Uzantnomo(String); @@ -56,47 +73,177 @@ impl From<&str> for Uzantnomo { } } +impl From<&Uzantnomo> for String { + fn from(s: &Uzantnomo) -> Self { + s.0.clone() + } +} + impl From for String { fn from(s: Uzantnomo) -> Self { s.0.clone() } } -pub trait AŭtentigoDB: Send { - fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)>; +impl FromSql for Uzantnomo { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Text(t) => Ok(Uzantnomo::from( + String::from_utf8(Vec::from(t)).unwrap().as_ref(), + )), + _ => Err(FromSqlError::InvalidType), + } + } +} - fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono; +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Pasvorto(String); + +impl Pasvorto { + fn haku(&self, salo: &Salo) -> HakitaPasvorto { + let mut haketilo = Sha256::new(); + haketilo.update(String::from(self).as_bytes()); + haketilo.update(salo); + HakitaPasvorto(Vec::from(haketilo.finalize().as_slice())) + } +} + +impl From<&str> for Pasvorto { + fn from(s: &str) -> Self { + Pasvorto(s.to_owned()) + } +} + +impl From<&Pasvorto> for String { + fn from(s: &Pasvorto) -> Self { + s.0.clone() + } +} + +impl From for String { + fn from(s: Pasvorto) -> Self { + s.0.clone() + } +} + +#[derive(PartialEq)] +struct HakitaPasvorto(Vec); + +impl ToSql for HakitaPasvorto { + fn to_sql(&self) -> Result, rusqlite::Error> { + Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref()))) + } +} + +impl FromSql for HakitaPasvorto { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Blob(t) => Ok(HakitaPasvorto(Vec::from(t))), + _ => Err(FromSqlError::InvalidType), + } + } +} + +struct Salo(Vec); + +impl AsRef<[u8]> for Salo { + fn as_ref(&self) -> &[u8] { + self.0.as_ref() + } +} + +impl ToSql for Salo { + fn to_sql(&self) -> Result, rusqlite::Error> { + Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref()))) + } +} + +impl FromSql for Salo { + fn column_result(val: ValueRef<'_>) -> FromSqlResult { + match val { + ValueRef::Blob(t) => Ok(Salo(Vec::from(t))), + _ => Err(FromSqlError::InvalidType), + } + } +} + +pub trait AŭtentigoDB: Send { + fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo; + + fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono; + + fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto); + + fn aŭtentigu_per_pasvorto( + &self, + uzantnomo: &Uzantnomo, + pasvorto: &Pasvorto, + ) -> Option; + + fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)>; } pub struct MemAŭtentigo { - sesioj: HashMap<Ĵetono, UzantIdentigilo>, - uzantoj: HashMap, inversa_uzantoj: HashMap, + pasvortoj: HashMap, + sesioj: HashMap, + uzantoj: HashMap, } impl MemAŭtentigo { pub fn new() -> Self { Self { + inversa_uzantoj: HashMap::new(), + pasvortoj: HashMap::new(), sesioj: HashMap::new(), uzantoj: HashMap::new(), - inversa_uzantoj: HashMap::new(), } } } impl AŭtentigoDB for MemAŭtentigo { - fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> { + fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo { + let uzant_id = + UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.uzantoj.insert(uzant_id.clone(), uzantnomo.clone()); + self.pasvortoj.insert(uzant_id.clone(), pasvorto); + self.inversa_uzantoj.insert(uzantnomo, uzant_id.clone()); + uzant_id + } + + fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono { + let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap(); + let ĵetono = + SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + self.sesioj.insert(ĵetono.clone(), identigilo); + ĵetono + } + + fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) { + self.pasvortoj.insert(identigilo.clone(), pasvorto); + } + + fn aŭtentigu_per_pasvorto( + &self, + uzantnomo: &Uzantnomo, + pasvorto: &Pasvorto, + ) -> Option { + self.inversa_uzantoj.get(&uzantnomo).and_then(|id| { + self.pasvortoj.get(id).and_then(|kandidato| { + if *pasvorto == *kandidato { + Some(id.clone()) + } else { + None + } + }) + }) + } + + fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> { let identigilo = self.sesioj.get(&ĵetono).cloned()?; let uzantnomo = self.uzantoj.get(&identigilo).cloned()?; Some((identigilo, uzantnomo)) } - - fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono { - let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap(); - let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); - self.sesioj.insert(ĵetono.clone(), identigilo); - ĵetono - } } pub struct DBAŭtentigo { @@ -110,26 +257,50 @@ impl DBAŭtentigo { } impl AŭtentigoDB for DBAŭtentigo { - fn aŭtentigu(&self, ĵetono: Ĵetono) -> Option<(UzantIdentigilo, Uzantnomo)> { - let konekto = self.pool.konektu().unwrap(); - konekto + fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo { + let mut konekto = self.pool.konektu().unwrap(); + let tr = konekto.transaction().unwrap(); + + let identigilo = + UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + + let cnt: usize = tr .query_row( - "SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?", - [String::from(ĵetono)], - |row| { - let identigilo = row.get("id") - .map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap(); - let nomo = row.get("nomo") - .map(|s: String| Uzantnomo::from(s.as_str())).unwrap(); - Ok((identigilo, nomo)) - }, + "SELECT count(*) from uzantoj WHERE nomo = ?", + params![String::from(uzantnomo.clone())], + |row| row.get("count(*)"), ) - .optional() - .unwrap() + .unwrap(); + if cnt > 0 { + panic!("uzanto jam ekzistas"); + } else { + let salo = Salo( + thread_rng() + .sample_iter(&Alphanumeric) + .take(10) + .collect::>(), + ); + let pasvorto = pasvorto.haku(&salo); + tr.execute( + "INSERT INTO uzantoj VALUES(?, ?, ?, ?)", + params![ + String::from(identigilo.clone()), + String::from(uzantnomo), + pasvorto, + salo + ], + ) + .unwrap(); + } + + tr.commit().unwrap(); + + identigilo } - fn kreu_ĵetono(&mut self, uzantnomo: Uzantnomo) -> Ĵetono { - let ĵetono = Ĵetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); + fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono { + let ĵetono = + SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str()); let mut konekto = self.pool.konektu().unwrap(); let tr = konekto.transaction().unwrap(); let uzanta_id: Option = tr @@ -153,6 +324,68 @@ impl AŭtentigoDB for DBAŭtentigo { tr.commit().unwrap(); ĵetono } + + fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) {} + + fn aŭtentigu_per_pasvorto( + &self, + uzantnomo: &Uzantnomo, + pasvorto: &Pasvorto, + ) -> Option { + let konekto = self.pool.konektu().unwrap(); + let mut demando = konekto + .prepare_cached("SELECT * FROM uzantoj WHERE nomo = ?") + .unwrap(); + let rezultoj = demando.query_map(params![String::from(uzantnomo)], |row| { + let id = row.get("id")?; + let nomo = row.get("nomo")?; + let pasvorto = row.get("pasvorto")?; + let salo = row.get("pasvortsalo")?; + Ok((id, nomo, pasvorto, salo)) + }); + + let rows: Vec<(UzantIdentigilo, Uzantnomo, HakitaPasvorto, Salo)> = match rezultoj { + Ok(r) => { + let mut rows = Vec::new(); + for row in r { + let (id, nomo, pasvorto, salo) = row.unwrap(); + rows.push((id, nomo, pasvorto, salo)); + } + rows + } + Err(_) => panic!("eraro en datumbazo"), + }; + + match rows.len() { + 1 => { + let (ref identigilo, _, ref hakita_pasvorto, ref salo) = rows[0]; + if pasvorto.haku(&salo) == *hakita_pasvorto { + Some(identigilo.clone()) + } else { + None + } + } + 0 => None, + _ => panic!("pli ol unu kongruo trovis por uzantnomo"), + } + } + + fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> { + let konekto = self.pool.konektu().unwrap(); + konekto.query_row( + "SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?", + params![String::from(ĵetono.clone())], + |row| { + let identigilo = row.get("id") + .map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap(); + let nomo = row.get("nomo") + .map(|s: String| Uzantnomo::from(s.as_str())).unwrap(); + Ok((identigilo, nomo)) + }, + ) + .optional() + .unwrap() + } } #[derive(Debug)] @@ -166,10 +399,10 @@ pub fn kun_aŭtentigo( let auth_ctx = auth_ctx.clone(); warp::header("authentication").and_then({ let auth_ctx = auth_ctx.clone(); - move |text| { + move |text: SesiaĴetono| { let auth_ctx = auth_ctx.clone(); async move { - match auth_ctx.read().unwrap().aŭtentigu(text) { + match auth_ctx.read().unwrap().aŭtentigu_per_sesio(&text) { Some(salutiloj) => Ok(salutiloj), None => Err(reject::custom(AŭtentigoPostulas)), } @@ -177,3 +410,22 @@ pub fn kun_aŭtentigo( } }) } + +#[cfg(test)] +mod test { + use super::*; + use tempfile::NamedTempFile; + + #[test] + fn ĝi_povas_krei_uzanto() { + let vojo = NamedTempFile::new().unwrap().into_temp_path(); + let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap(); + let mut aŭtentigo = DBAŭtentigo::kreu(datumbazo); + let identigilo = + aŭtentigo.kreu_uzanton(Uzantnomo::from("savanni"), Pasvorto::from("abcdefg")); + + let aŭtentita_identigilo = aŭtentigo + .aŭtentigu_per_pasvorto(&Uzantnomo::from("savanni"), &Pasvorto::from("abcdefg")); + assert_eq!(aŭtentita_identigilo, Some(identigilo)); + } +} diff --git a/servilo/src/datumbazo.rs b/servilo/src/datumbazo.rs index 7386364..63e0d2e 100644 --- a/servilo/src/datumbazo.rs +++ b/servilo/src/datumbazo.rs @@ -25,7 +25,7 @@ impl Datumbazo { println!("versio: {}", versio); if versio == 0 { tx.execute_batch( - "CREATE TABLE uzantoj (id string primary key, nomo text); + "CREATE TABLE uzantoj (id string primary key, nomo text, pasvorto text, pasvortsalo text); CREATE TABLE rajtoj (uzanto string, rajto string, foreign key(uzanto) references uzanto(id)); CREATE TABLE sesioj (id string primary key not null, uzanto string, foreign key(uzanto) references uzantoj(id)); PRAGMA user_version = 1;", @@ -97,20 +97,20 @@ mod test { let mut konekto = datumbazo.konektu().unwrap(); let tr = konekto.transaction().unwrap(); tr.execute( - "INSERT INTO uzantoj VALUES(?, ?)", - params![1, String::from("mia-uzantnomo")], + "INSERT INTO uzantoj VALUES(?, ?, ?, ?)", + params!["abcdfeg", "mia-uzantnomo", "pasvorto", "abcdefg"], ) .unwrap(); tr.commit().unwrap(); let konekto = datumbazo.konektu().unwrap(); - let id: Option = konekto + let id: Option = konekto .query_row( "SELECT id FROM uzantoj WHERE nomo = ?", [String::from("mia-uzantnomo")], |row| row.get("id"), ) .unwrap(); - assert_eq!(id, Some(1)); + assert_eq!(id, Some(String::from("abcdfeg"))); } } diff --git a/servilo/src/main.rs b/servilo/src/main.rs index c484e6e..b5bc010 100644 --- a/servilo/src/main.rs +++ b/servilo/src/main.rs @@ -54,7 +54,7 @@ pub async fn main() { let ĵetono = auth_ctx .write() .unwrap() - .kreu_ĵetono(Uzantnomo::from("savanni")); + .kreu_sesion(&Uzantnomo::from("savanni")); println!("ĵetono: {}", String::from(ĵetono)); let rajtigo = Arc::new(RwLock::new(DBRajtigo::kreu(db)));