]> OzVa Git service - ozva-cloud/commitdiff
feat: support hashed password (#283)
authorsigoden <sigoden@gmail.com>
Sat, 4 Nov 2023 10:12:58 +0000 (18:12 +0800)
committerGitHub <noreply@github.com>
Sat, 4 Nov 2023 10:12:58 +0000 (18:12 +0800)
Cargo.lock
Cargo.toml
README.md
src/auth.rs
src/server.rs
tests/auth.rs

index 0d59ed5c823b28945244cfbad04146b430b32824..e28b0769598cef0019fd82d5346fb43407f59d51 100644 (file)
@@ -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"
index 4ee1c3e50a7f56385caa2127d34d4519d78aec50..a5e70e7caffc1abbc2170f7098e7ef94c1870ccb 100644 (file)
@@ -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"]
index 4d0e14172823fbb19ff053e139b5d1613adfb700..69eaf616f434b6f32ab8780a234247d97a506118 100644 (file)
--- 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 <glob>,...`.
index 9c64ea975251110ab32682702a031f55bb3f4c91..b7f57d250dc09542029e007d80d865b92b8b7959 100644 (file)
@@ -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<String, (String, AccessPaths)>,
     anony: Option<AccessPaths>,
 }
@@ -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<HeaderValue> {
-    let nonce = create_nonce()?;
-    let value = format!(
-        "Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"",
-        REALM, nonce, REALM
-    );
+pub fn www_authenticate(args: &Args) -> Result<HeaderValue> {
+    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<u8> = general_purpose::STANDARD.decode(value).ok()?;
-        let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect();
+        let value: Vec<u8> = 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(());
         }
 
index 95de3ddd84745836bdac196660010ed507c8e4ac..ba7bef6e247de2d7f3c5bbefaa0624bf5ea99f13 100644 (file)
@@ -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(())
index 5ad47f6eb7958ef1e5a768c113ddcde5b7a7dbf3..fa206da58586a0420fc56db0a4634b5c10df1a26 100644 (file)
@@ -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,