From d3de3db0d9d6403ae94e4372654d2ac0e594be68 Mon Sep 17 00:00:00 2001 From: sigoden Date: Sat, 4 Nov 2023 18:12:58 +0800 Subject: [PATCH] feat: support hashed password (#283) --- Cargo.lock | 35 ++++++++++++++++++++++++++++++----- Cargo.toml | 3 ++- README.md | 25 ++++++++++++++++++++++++- src/auth.rs | 40 +++++++++++++++++++++++++++++----------- src/server.rs | 2 +- tests/auth.rs | 26 ++++++++++++++++++++++++++ 6 files changed, 112 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d59ed5..e28b076 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -224,9 +224,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bitflags" @@ -453,7 +459,7 @@ dependencies = [ "assert_fs", "async-stream", "async_zip", - "base64 0.21.2", + "base64 0.21.5", "chardetng", "chrono", "clap", @@ -482,6 +488,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha-crypt", "socket2 0.5.3", "tokio", "tokio-rustls", @@ -1305,7 +1312,7 @@ version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", "bytes", "encoding_rs", "futures-core", @@ -1443,7 +1450,7 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.5", ] [[package]] @@ -1543,6 +1550,18 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "sha-crypt" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88e79009728d8311d42d754f2f319a975f9e38f156fd5e422d2451486c78b286" +dependencies = [ + "base64ct", + "rand", + "sha2", + "subtle", +] + [[package]] name = "sha1" version = "0.10.5" @@ -1615,6 +1634,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "2.0.29" diff --git a/Cargo.toml b/Cargo.toml index 4ee1c3e..a5e70e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,6 @@ percent-encoding = "2.3" serde = { version = "1", features = ["derive"] } serde_json = "1" futures = "0.3" -base64 = "0.21" async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] } headers = "0.3" mime_guess = "2.0" @@ -46,6 +45,8 @@ chardetng = "0.1" glob = "0.3.1" indexmap = "2.0" serde_yaml = "0.9.27" +sha-crypt = "0.5.0" +base64 = "0.21.5" [features] default = ["tls"] diff --git a/README.md b/README.md index 4d0e141..69eaf61 100644 --- a/README.md +++ b/README.md @@ -243,10 +243,33 @@ dufs -A -a user:pass@/dir1:rw,/dir2:rw,dir3 `user` has all permissions for `/dir1/*` and `/dir2/*`, has readonly permissions for `/dir3/`. ``` -dufs -a admin:admin@/ +dufs -A -a admin:admin@/ ``` Since dufs only allows viewing/downloading, `admin` can only view/download files. +### Hashed Password + +DUFS supports the use of sha-512 hashed password. + +Create hashed password + +``` +$ mkpasswd -m sha-512 -s +Password: 123456 +$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/ +``` + +Use hashed password +``` +dufs -A -a 'admin:$6$qCAVUG7yn7t/hH4d$BWm8r5MoDywNmDP/J3V2S2a6flmKHC1IpblfoqZfuK.LtLBZ0KFXP9QIfJP8RqL8MCw4isdheoAMTuwOz.pAO/@/:rw' +``` + +Two important things for hashed passwords: + +1. Dufs only supports SHA-512 hashed passwords, so ensure that the password string always starts with `$6$`. +2. Digest auth does not work with hashed passwords. + + ### Hide Paths Dufs supports hiding paths from directory listings via option `--hidden ,...`. diff --git a/src/auth.rs b/src/auth.rs index 9c64ea9..b7f57d2 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -11,7 +11,7 @@ use std::{ }; use uuid::Uuid; -use crate::utils::unix_now; +use crate::{args::Args, utils::unix_now}; const REALM: &str = "DUFS"; const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days @@ -27,6 +27,7 @@ lazy_static! { #[derive(Debug)] pub struct AccessControl { + use_hashed_password: bool, users: IndexMap, anony: Option, } @@ -34,6 +35,7 @@ pub struct AccessControl { impl Default for AccessControl { fn default() -> Self { AccessControl { + use_hashed_password: false, anony: Some(AccessPaths::new(AccessPerm::ReadWrite)), users: IndexMap::new(), } @@ -45,6 +47,7 @@ impl AccessControl { if raw_rules.is_empty() { return Ok(Default::default()); } + let mut use_hashed_password = false; let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); let mut anony = None; let mut anony_paths = vec![]; @@ -72,6 +75,9 @@ impl AccessControl { if user.is_empty() || pass.is_empty() { return Err(create_err(rule)); } + if pass.starts_with("$6$") { + use_hashed_password = true; + } users.insert(user.to_string(), (pass.to_string(), paths)); } else { return Err(create_err(rule)); @@ -82,7 +88,11 @@ impl AccessControl { paths.add(path, perm) } } - Ok(Self { users, anony }) + Ok(Self { + use_hashed_password, + users, + anony, + }) } pub fn exist(&self) -> bool { @@ -244,12 +254,16 @@ impl AccessPerm { } } -pub fn www_authenticate() -> Result { - let nonce = create_nonce()?; - let value = format!( - "Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"", - REALM, nonce, REALM - ); +pub fn www_authenticate(args: &Args) -> Result { + let value = if args.auth.use_hashed_password { + format!("Basic realm=\"{}\"", REALM) + } else { + let nonce = create_nonce()?; + format!( + "Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"", + REALM, nonce, REALM + ) + }; Ok(HeaderValue::from_str(&value)?) } @@ -274,14 +288,18 @@ pub fn check_auth( auth_pass: &str, ) -> Option<()> { if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") { - let basic_value: Vec = general_purpose::STANDARD.decode(value).ok()?; - let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect(); + let value: Vec = general_purpose::STANDARD.decode(value).ok()?; + let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect(); if parts[0] != auth_user { return None; } - if parts[1] == auth_pass { + if auth_pass.starts_with("$6$") { + if let Ok(()) = sha_crypt::sha512_check(parts[1], auth_pass) { + return Some(()); + } + } else if parts[1] == auth_pass { return Some(()); } diff --git a/src/server.rs b/src/server.rs index 95de3dd..ba7bef6 100644 --- a/src/server.rs +++ b/src/server.rs @@ -1037,7 +1037,7 @@ impl Server { fn auth_reject(&self, res: &mut Response) -> Result<()> { set_webdav_headers(res); res.headers_mut() - .append(WWW_AUTHENTICATE, www_authenticate()?); + .append(WWW_AUTHENTICATE, www_authenticate(&self.args)?); // set 401 to make the browser pop up the login box *res.status_mut() = StatusCode::UNAUTHORIZED; Ok(()) diff --git a/tests/auth.rs b/tests/auth.rs index 5ad47f6..fa206da 100644 --- a/tests/auth.rs +++ b/tests/auth.rs @@ -29,6 +29,32 @@ fn auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Resu Ok(()) } +const HASHED_PASSWORD_AUTH: &str = "user:$6$gQxZwKyWn/ZmWEA2$4uV7KKMnSUnET2BtWTj/9T5.Jq3h/MdkOlnIl5hdlTxDZ4MZKmJ.kl6C.NL9xnNPqC4lVHC1vuI0E5cLpTJX81@/:rw"; // user:pass + +#[rstest] +fn auth_hashed_password( + #[with(&["--auth", HASHED_PASSWORD_AUTH, "-A"])] server: TestServer, +) -> Result<(), Error> { + let url = format!("{}file1", server.url()); + let resp = fetch!(b"PUT", &url).body(b"abc".to_vec()).send()?; + assert_eq!(resp.status(), 401); + if let Err(err) = fetch!(b"PUT", &url) + .body(b"abc".to_vec()) + .send_with_digest_auth("user", "pass") + { + assert_eq!( + format!("{err:?}"), + r#"DigestAuth(MissingRequired("realm", "Basic realm=\"DUFS\""))"# + ); + } + let resp = fetch!(b"PUT", &url) + .body(b"abc".to_vec()) + .basic_auth("user", Some("pass")) + .send()?; + assert_eq!(resp.status(), 201); + Ok(()) +} + #[rstest] fn auth_and_public( #[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer,