[[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"
"assert_fs",
"async-stream",
"async_zip",
- "base64 0.21.2",
+ "base64 0.21.5",
"chardetng",
"chrono",
"clap",
"serde",
"serde_json",
"serde_yaml",
+ "sha-crypt",
"socket2 0.5.3",
"tokio",
"tokio-rustls",
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",
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [
- "base64 0.21.2",
+ "base64 0.21.5",
]
[[package]]
"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"
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"
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"
glob = "0.3.1"
indexmap = "2.0"
serde_yaml = "0.9.27"
+sha-crypt = "0.5.0"
+base64 = "0.21.5"
[features]
default = ["tls"]
`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>,...`.
};
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
#[derive(Debug)]
pub struct AccessControl {
+ use_hashed_password: bool,
users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>,
}
impl Default for AccessControl {
fn default() -> Self {
AccessControl {
+ use_hashed_password: false,
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(),
}
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![];
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));
paths.add(path, perm)
}
}
- Ok(Self { users, anony })
+ Ok(Self {
+ use_hashed_password,
+ users,
+ anony,
+ })
}
pub fn exist(&self) -> bool {
}
}
-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)?)
}
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(());
}
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(())
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,