diff --git a/Cargo.lock b/Cargo.lock index 92ba7ef..1cad3a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" dependencies = [ "base64ct", "blake2", - "cpufeatures", + "cpufeatures 0.2.17", "password-hash", ] @@ -144,7 +144,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -156,6 +156,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bumpalo" version = "3.20.2" @@ -190,6 +199,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.44" @@ -219,6 +239,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -234,6 +260,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -283,13 +318,22 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "der" version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -309,12 +353,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -512,6 +567,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", + "rand_core 0.10.1", "wasip2", "wasip3", ] @@ -569,7 +625,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -626,6 +682,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.9.0" @@ -832,7 +897,7 @@ dependencies = [ "getrandom 0.2.17", "js-sys", "pem", - "rand", + "rand 0.8.6", "serde", "serde_json", "signature", @@ -931,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -987,7 +1052,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -1070,7 +1135,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.4", "subtle", ] @@ -1204,7 +1269,18 @@ checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha", - "rand_core", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", ] [[package]] @@ -1214,7 +1290,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.4", ] [[package]] @@ -1226,6 +1302,12 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1269,9 +1351,12 @@ dependencies = [ "axum", "chrono", "dotenvy", + "hex", "jsonwebtoken", + "rand 0.10.1", "serde", "serde_json", + "sha2 0.11.0", "sqlx", "thiserror", "tokio", @@ -1288,14 +1373,14 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", "pkcs8", - "rand_core", + "rand_core 0.6.4", "signature", "spki", "subtle", @@ -1399,8 +1484,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -1410,8 +1495,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -1445,8 +1541,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", - "rand_core", + "digest 0.10.7", + "rand_core 0.6.4", ] [[package]] @@ -1544,7 +1640,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "thiserror", "tokio", @@ -1582,7 +1678,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -1605,7 +1701,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -1622,11 +1718,11 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand", + "rand 0.8.6", "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -1662,10 +1758,10 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand", + "rand 0.8.6", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", diff --git a/Cargo.toml b/Cargo.toml index 19201e5..a3131ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,6 @@ argon2 = "0.5.3" jsonwebtoken = { version = "10.3.0", features = ["rand"] } chrono = { version = "0.4.44", features = ["serde"] } uuid = { version = "1.23.1", features = ["serde", "v4"] } +rand = "0.10.1" +sha2 = "0.11.0" +hex = "0.4.3" diff --git a/src/service/auth_service.rs b/src/service/auth_service.rs index 75cbda2..92bca23 100644 --- a/src/service/auth_service.rs +++ b/src/service/auth_service.rs @@ -1,11 +1,17 @@ +use std::time::Instant; + use axum::Json; +use chrono::{Duration, Utc}; use crate::controller::model::auth_model::*; +use crate::db::repository::refresh_token_repository::create_refresh_token; use crate::db::repository::user_repository; use crate::errors::AppError; use crate::state::AppState; +use crate::utils::anti_enumeration::anti_enumeration_delay; use crate::utils::hash; use crate::utils::jwt::generate_access_token; +use crate::utils::refresh_token::generate_refresh_token; pub async fn login(state: &AppState, req: LoginRequest) -> Result, AppError> { todo!() @@ -14,16 +20,29 @@ pub async fn register( state: &AppState, req: RegisterRequest, ) -> Result, AppError> { + let start = Instant::now(); let mut tx = state.db.begin().await?; - let user = user_repository::get_user_by_email(&mut *tx, &req.email).await?; - if user.is_some() { - // user already registered - return Err(AppError::Internal); + { + let user = user_repository::get_user_by_email(&mut *tx, &req.email).await?; + if user.is_some() { + // user already registered + anti_enumeration_delay(start, 150, 300).await; + return Err(AppError::Internal); + } } let h = hash::hash(&req.password)?; let user = user_repository::create_user(&mut *tx, req.email, h).await?; let access_token = generate_access_token(user.id, &state.jwt_secret)?; + let (refresh_plain, refresh_hash) = generate_refresh_token(); + let expires_at = chrono::Utc::now() + Duration::days(7); + create_refresh_token(&mut *tx, user.id, refresh_hash, expires_at).await?; + tx.commit().await?; - todo!("generate jwt and refresh token") + anti_enumeration_delay(start, 150, 300).await; + // TODO: put refresh token in cookie + Ok(Json(AuthResponse { + access_token: access_token, + refresh_token: refresh_plain, + })) } diff --git a/src/utils/anti_enumeration.rs b/src/utils/anti_enumeration.rs new file mode 100644 index 0000000..2edf595 --- /dev/null +++ b/src/utils/anti_enumeration.rs @@ -0,0 +1,21 @@ +use std::time::{Duration, Instant}; + +use rand::RngExt; +use tokio::time::sleep; + +/// Anti-enumeration: ensures consistent response timing regardless of outcome. +/// Call at the end of request handler, before returning. +/// Range: 150-350ms (configurable) +/// +/// # Arguments +/// * `start` - The Instant when request processing began +/// * `min_ms` - Minimum delay in milliseconds (default: 150) +/// * `max_ms` - Maximum delay in milliseconds (default: 350) +pub async fn anti_enumeration_delay(start: Instant, min_ms: u64, max_ms: u64) { + let target = min_ms + rand::rng().random::() % (max_ms - min_ms); + let target_duration = Duration::from_millis(target); + + if let Some(remaining) = target_duration.checked_sub(start.elapsed()) { + sleep(remaining).await; + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ee79ece..4ce163b 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,2 +1,4 @@ +pub mod anti_enumeration; pub mod hash; pub mod jwt; +pub mod refresh_token; diff --git a/src/utils/refresh_token.rs b/src/utils/refresh_token.rs new file mode 100644 index 0000000..c78d649 --- /dev/null +++ b/src/utils/refresh_token.rs @@ -0,0 +1,18 @@ +use rand::Rng; +use sha2::{Digest, Sha256}; +pub fn generate_refresh_token() -> (String, String) { + let mut bytes = [0u8; 32]; + + let mut thread_rng = rand::rng(); + thread_rng.fill_bytes(&mut bytes); + + let plain = hex::encode(&bytes); // 64 hex chars for user + let hash = { + // SHA-256 for DB storage + let mut hasher = Sha256::new(); + hasher.update(&plain); + hex::encode(hasher.finalize()) + }; + + (plain, hash) +}