aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoris2023-08-12 20:05:09 +0200
committerJoris2023-08-12 20:05:09 +0200
commit8c689db1c8fa06ddb9119e626e7b1149f3493905 (patch)
treecb4029776162387a03a7a131ceee3628ed1ba4ef
parent459016e70dd4933a8082d27748097de81a3e53ff (diff)
downloadbudget-8c689db1c8fa06ddb9119e626e7b1149f3493905.tar.gz
budget-8c689db1c8fa06ddb9119e626e7b1149f3493905.tar.bz2
budget-8c689db1c8fa06ddb9119e626e7b1149f3493905.zip
Sign cookie with secret key
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml2
-rw-r--r--config.json3
-rw-r--r--src/controller/error.rs6
-rw-r--r--src/controller/login.rs77
-rw-r--r--src/controller/payments.rs5
-rw-r--r--src/controller/utils.rs80
-rw-r--r--src/crypto/mod.rs1
-rw-r--r--src/crypto/signed.rs71
-rw-r--r--src/main.rs1
-rw-r--r--src/model/config.rs1
-rw-r--r--src/routes.rs14
-rw-r--r--src/utils/cookie.rs35
-rw-r--r--src/utils/mod.rs1
14 files changed, 215 insertions, 98 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 8346520..8fc70be 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -157,6 +157,8 @@ dependencies = [
"chrono",
"clap",
"env_logger",
+ "hex",
+ "hmac",
"hyper",
"log",
"serde",
@@ -418,12 +420,13 @@ checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
[[package]]
name = "digest"
-version = "0.10.6"
+version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
+ "subtle",
]
[[package]]
@@ -682,6 +685,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
name = "http"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index ee27cd9..c9a2db0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,6 +9,8 @@ bcrypt = "0.14"
chrono = "0.4"
clap = { version = "4.2", features = ["derive"] }
env_logger = "0.10"
+hmac = "0.12"
+hex = "0.4"
hyper = { version = "0.14", features = ["full"] }
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
diff --git a/config.json b/config.json
index 488b2f9..6be6ab2 100644
--- a/config.json
+++ b/config.json
@@ -1,4 +1,5 @@
{
"secure_cookies": false,
- "mock_mails": true
+ "mock_mails": true,
+ "auth_secret": "1QAZa8RSgogsakXSa0hgEeyA7xqnvo91"
}
diff --git a/src/controller/error.rs b/src/controller/error.rs
index 8dad16b..2a84255 100644
--- a/src/controller/error.rs
+++ b/src/controller/error.rs
@@ -6,13 +6,13 @@ use tera::{Context, Tera};
use crate::controller::utils;
use crate::controller::wallet::Wallet;
+// TODO error code
pub fn error(wallet: &Wallet, title: &str, message: &str) -> Response<Body> {
- utils::with_header(
+ utils::with_headers(
Response::new(
template(&wallet.assets, &wallet.templates, title, message).into(),
),
- CACHE_CONTROL,
- "no-cache",
+ vec![(CACHE_CONTROL, "no-cache")],
)
}
diff --git a/src/controller/login.rs b/src/controller/login.rs
index 09a2786..9757c25 100644
--- a/src/controller/login.rs
+++ b/src/controller/login.rs
@@ -1,21 +1,24 @@
use bcrypt;
+use hyper::header::SET_COOKIE;
use hyper::{Body, Response};
use sqlx::sqlite::SqlitePool;
use std::collections::HashMap;
use tera::{Context, Tera};
use uuid::Uuid;
+use crate::controller::utils::with_headers;
use crate::controller::wallet::Wallet;
use crate::controller::{error, utils};
use crate::db;
use crate::model::config::Config;
use crate::model::user::User;
+use crate::utils::cookie;
use crate::validation;
pub async fn page(
assets: &HashMap<String, String>,
templates: &Tera,
- error: Option<String>,
+ error: Option<&str>,
) -> Response<Body> {
let connected_user: Option<User> = None;
@@ -26,28 +29,23 @@ pub async fn page(
utils::template(assets, templates, "login.html", context)
}
+// TODO rewrite
pub async fn login(
- config: Config,
+ config: &Config,
assets: &HashMap<String, String>,
templates: &Tera,
form: HashMap<String, String>,
pool: SqlitePool,
) -> Response<Body> {
- let not_authorized = page(
- assets,
- templates,
- Some("Vous n’êtes pas autorisé à vous connecter.".to_string()),
- )
- .await;
- let server_error =
- page(assets, templates, Some("Erreur serveur.".to_string())).await;
match validation::login::login(&form) {
Some(login) => {
match db::users::get_password_hash(&pool, login.email.clone()).await
{
Some(hash) => match bcrypt::verify(login.password, &hash) {
Ok(true) => {
+ // TODO generate truly random instead of uuid
let login_token = Uuid::new_v4();
+
if db::users::set_login_token(
&pool,
login.email,
@@ -55,31 +53,66 @@ pub async fn login(
)
.await
{
- utils::with_login_cookie(
- config,
- login_token,
- utils::redirect("/"),
- )
+ match cookie::login(config, login_token) {
+ Ok(str) => with_headers(
+ utils::redirect("/"),
+ vec![(SET_COOKIE, &str)],
+ ),
+ Err(msg) => {
+ server_error(
+ assets,
+ templates,
+ &format!(
+ "Error generating cookie: {msg}"
+ ),
+ )
+ .await
+ }
+ }
} else {
- server_error
+ server_error(assets, templates, "Erreur server")
+ .await
}
}
- Ok(false) => not_authorized,
+ Ok(false) => not_authorized(assets, templates).await,
Err(err) => {
error!("Error verifying bcrypt password: {:?}", err);
- server_error
+ server_error(assets, templates, "Erreur serveur").await
}
},
- None => not_authorized,
+ None => not_authorized(assets, templates).await,
}
}
- None => not_authorized,
+ None => not_authorized(assets, templates).await,
}
}
-pub async fn logout(config: Config, wallet: &Wallet) -> Response<Body> {
+async fn server_error(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ msg: &str,
+) -> Response<Body> {
+ page(assets, templates, Some(msg)).await
+}
+
+async fn not_authorized(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+) -> Response<Body> {
+ page(
+ assets,
+ templates,
+ Some("Vous n’êtes pas autorisé à vous connecter."),
+ )
+ .await
+}
+
+pub async fn logout(config: &Config, wallet: &Wallet) -> Response<Body> {
if db::users::remove_login_token(&wallet.pool, wallet.user.id).await {
- utils::with_logout_cookie(config, utils::redirect("/"))
+ with_headers(
+ utils::redirect("/"),
+ vec![(SET_COOKIE, &cookie::logout(config))],
+ )
} else {
error::error(wallet, "Erreur serveur", "Erreur serveur")
}
diff --git a/src/controller/payments.rs b/src/controller/payments.rs
index 42d3e3c..2663fa7 100644
--- a/src/controller/payments.rs
+++ b/src/controller/payments.rs
@@ -232,10 +232,9 @@ pub async fn search_category(
) -> Response<Body> {
match db::payments::search_category(&wallet.pool, query.payment_name).await
{
- Some(category_id) => utils::with_header(
+ Some(category_id) => utils::with_headers(
Response::new(format!("{}", category_id).into()),
- CONTENT_TYPE,
- "application/json",
+ vec![(CONTENT_TYPE, "application/json")],
),
None => utils::not_found(),
}
diff --git a/src/controller/utils.rs b/src/controller/utils.rs
index 26db765..fb1d9ad 100644
--- a/src/controller/utils.rs
+++ b/src/controller/utils.rs
@@ -1,23 +1,13 @@
use hyper::header::{
- HeaderName, HeaderValue, CACHE_CONTROL, CONTENT_TYPE, LOCATION, SET_COOKIE,
+ HeaderName, HeaderValue, CACHE_CONTROL, CONTENT_TYPE, LOCATION,
};
use hyper::{Body, Response, StatusCode};
use std::collections::HashMap;
use tera::{Context, Tera};
use tokio::fs::File;
use tokio_util::codec::{BytesCodec, FramedRead};
-use uuid::Uuid;
use crate::controller::error;
-use crate::model::config::Config;
-
-pub fn with_header(
- response: Response<Body>,
- name: HeaderName,
- value: &str,
-) -> Response<Body> {
- with_headers(response, vec![(name, value)])
-}
pub fn with_headers(
response: Response<Body>,
@@ -31,40 +21,6 @@ pub fn with_headers(
response
}
-pub fn with_login_cookie(
- config: Config,
- login_token: Uuid,
- response: Response<Body>,
-) -> Response<Body> {
- let cookie = format!(
- "TOKEN={}; SameSite=Strict; HttpOnly; Max-Age=86400{}",
- login_token,
- if config.secure_cookies {
- "; Secure"
- } else {
- ""
- }
- );
-
- with_header(response, SET_COOKIE, &cookie)
-}
-
-pub fn with_logout_cookie(
- config: Config,
- response: Response<Body>,
-) -> Response<Body> {
- let cookie = format!(
- "TOKEN=; SameSite=Strict; HttpOnly; Max-Age=0{}",
- if config.secure_cookies {
- "; Secure"
- } else {
- ""
- }
- );
-
- with_header(response, SET_COOKIE, &cookie)
-}
-
pub fn template(
assets: &HashMap<String, String>,
templates: &Tera,
@@ -74,24 +30,28 @@ pub fn template(
let mut context = context;
context.insert("assets", assets);
- let response = match templates.render(path, &context) {
- Ok(template) => Response::new(template.into()),
- Err(err) => Response::new(
- error::template(
- assets,
- templates,
- "Erreur serveur",
- &format!(
- "Erreur lors de la préparation de la page : {:?}",
- err
- ),
- )
- .into(),
+ match templates.render(path, &context) {
+ Ok(template) => with_headers(
+ Response::new(template.into()),
+ vec![(CONTENT_TYPE, "text/html"), (CACHE_CONTROL, "no-cache")],
),
- };
+ Err(err) => server_error(
+ assets,
+ templates,
+ &format!("Erreur lors de la préparation de la page : {:?}", err),
+ ),
+ }
+}
+fn server_error(
+ assets: &HashMap<String, String>,
+ templates: &Tera,
+ msg: &str,
+) -> Response<Body> {
with_headers(
- response,
+ Response::new(
+ error::template(assets, templates, "Erreur serveur", msg).into(),
+ ),
vec![(CONTENT_TYPE, "text/html"), (CACHE_CONTROL, "no-cache")],
)
}
diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs
new file mode 100644
index 0000000..41e9259
--- /dev/null
+++ b/src/crypto/mod.rs
@@ -0,0 +1 @@
+pub mod signed;
diff --git a/src/crypto/signed.rs b/src/crypto/signed.rs
new file mode 100644
index 0000000..436f3d1
--- /dev/null
+++ b/src/crypto/signed.rs
@@ -0,0 +1,71 @@
+use hex;
+use hmac::{Hmac, Mac};
+use sha2::Sha256;
+use std::str;
+use std::time::{SystemTime, UNIX_EPOCH};
+
+const SEP: &str = "-";
+
+pub fn sign(key: &str, raw: &str) -> Result<String, String> {
+ let nonce = get_nonce()?;
+ let joined = format!("{nonce}{SEP}{raw}");
+ let signature = get_signature(key, &joined)?;
+ Ok(format!("{signature}{SEP}{joined}"))
+}
+
+pub fn verify(key: &str, signed: &str) -> Result<String, String> {
+ let mut iter = signed.split(SEP);
+ match (iter.next(), iter.next()) {
+ (Some(signature), Some(nonce)) => {
+ let raw = iter.collect::<Vec<&str>>().join(SEP);
+ if signature == get_signature(key, &format!("{nonce}{SEP}{raw}"))? {
+ Ok(raw)
+ } else {
+ Err("Signature does not match".to_string())
+ }
+ }
+ _ => Err("Malformed signed".to_string()),
+ }
+}
+
+fn get_signature(key: &str, message: &str) -> Result<String, String> {
+ let mut mac = Hmac::<Sha256>::new_from_slice(key.as_bytes())
+ .map_err(|e| format!("Error initializing MAC: {e}"))?;
+ mac.update(message.as_bytes());
+ let result = mac.finalize();
+ Ok(hex::encode(result.into_bytes()))
+}
+
+fn get_nonce() -> Result<String, String> {
+ Ok(SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .map_err(|e| format!("Failure getting unix expoch: {e}"))?
+ .as_millis()
+ .to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn sign_and_validate() {
+ let key = "xagrlBUobnTj32Rm8tvmsZ6mh8qLfip5".to_string();
+ assert_eq!(verify(&key, &sign(&key, "").unwrap()), Ok("".to_string()));
+ assert_eq!(
+ verify(&key, &sign(&key, "hello").unwrap()),
+ Ok("hello".to_string())
+ );
+ assert_eq!(
+ verify(&key, &sign(&key, "with-sep").unwrap()),
+ Ok("with-sep".to_string())
+ );
+ }
+
+ #[test]
+ fn fail_when_key_mismatch() {
+ let key1 = "xagrlBUobnTj32Rm8tvmsZ6mh8qLfip5".to_string();
+ let key2 = "8KJBK6axEr9wQ390GgdWA8Pjn8FwILDa".to_string();
+ assert!(verify(&key1, &sign(&key2, "hello").unwrap()).is_err());
+ }
+}
diff --git a/src/main.rs b/src/main.rs
index ed54a7b..74917a9 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,6 +10,7 @@ extern crate log;
mod assets;
mod controller;
+mod crypto;
mod db;
mod jobs;
mod mail;
diff --git a/src/model/config.rs b/src/model/config.rs
index cebe1a6..e69b4c4 100644
--- a/src/model/config.rs
+++ b/src/model/config.rs
@@ -4,4 +4,5 @@ use serde::Deserialize;
pub struct Config {
pub secure_cookies: bool,
pub mock_mails: bool,
+ pub auth_secret: String,
}
diff --git a/src/routes.rs b/src/routes.rs
index b9e137e..ef63c8e 100644
--- a/src/routes.rs
+++ b/src/routes.rs
@@ -12,6 +12,7 @@ use crate::controller::wallet::Wallet;
use crate::db;
use crate::model::config::Config;
use crate::model::user::User;
+use crate::utils::cookie;
pub async fn routes(
config: Config,
@@ -32,7 +33,7 @@ pub async fn routes(
}
(&Method::POST, ["login"]) => {
controller::login::login(
- config,
+ &config,
&assets,
&templates,
body_form(request).await,
@@ -47,7 +48,7 @@ pub async fn routes(
"icon.png" => file("assets/icon.png", "image/png").await,
_ => controller::utils::not_found(),
},
- _ => match connected_user(&pool, &request).await {
+ _ => match connected_user(&config, &pool, &request).await {
Some(user) => {
let wallet = Wallet {
pool,
@@ -55,7 +56,7 @@ pub async fn routes(
templates,
user,
};
- authenticated_routes(config, wallet, request).await
+ authenticated_routes(&config, wallet, request).await
}
None => controller::utils::redirect("/login"),
},
@@ -65,18 +66,17 @@ pub async fn routes(
}
async fn connected_user(
+ config: &Config,
pool: &SqlitePool,
request: &Request<Body>,
) -> Option<User> {
let cookie = request.headers().get("COOKIE")?.to_str().ok()?;
- let mut xs = cookie.split('=');
- xs.next();
- let login_token = xs.next()?;
+ let login_token = cookie::extract_token(config, cookie).ok()?;
db::users::get_by_login_token(pool, login_token.to_string()).await
}
async fn authenticated_routes(
- config: Config,
+ config: &Config,
wallet: Wallet,
request: Request<Body>,
) -> Response<Body> {
diff --git a/src/utils/cookie.rs b/src/utils/cookie.rs
new file mode 100644
index 0000000..c716936
--- /dev/null
+++ b/src/utils/cookie.rs
@@ -0,0 +1,35 @@
+use uuid::Uuid;
+
+use crate::crypto::signed;
+use crate::model::config::Config;
+
+pub fn login(config: &Config, token: Uuid) -> Result<String, String> {
+ let signed_token = signed::sign(&config.auth_secret, &token.to_string())?;
+ Ok(cookie(config, &signed_token, 24 * 60 * 60))
+}
+
+pub fn logout(config: &Config) -> String {
+ cookie(config, "", 0)
+}
+
+pub fn extract_token(config: &Config, cookie: &str) -> Result<String, String> {
+ let mut xs = cookie.split('=');
+ xs.next();
+ let signed_cookie = xs.next().ok_or("Error extracting cookie")?;
+ signed::verify(&config.auth_secret, signed_cookie)
+}
+
+fn cookie(config: &Config, token: &str, max_age_seconds: i32) -> String {
+ let mut xs = vec![
+ format!("TOKEN={token}"),
+ "SameSite=Strict".to_string(),
+ "HttpOnly".to_string(),
+ format!("Max-Age={}", max_age_seconds),
+ ];
+
+ if config.secure_cookies {
+ xs.push("Secure".to_string())
+ }
+
+ xs.join(";")
+}
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 481c63a..f362d7b 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -1 +1,2 @@
+pub mod cookie;
pub mod text;