feat: support hashed password (#283)

This commit is contained in:
sigoden 2023-11-04 18:12:58 +08:00 committed by GitHub
parent 80ac9afe68
commit d3de3db0d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 112 additions and 19 deletions

35
Cargo.lock generated
View file

@ -224,9 +224,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.2" version = "0.21.5"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "bitflags" name = "bitflags"
@ -453,7 +459,7 @@ dependencies = [
"assert_fs", "assert_fs",
"async-stream", "async-stream",
"async_zip", "async_zip",
"base64 0.21.2", "base64 0.21.5",
"chardetng", "chardetng",
"chrono", "chrono",
"clap", "clap",
@ -482,6 +488,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",
"sha-crypt",
"socket2 0.5.3", "socket2 0.5.3",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
@ -1305,7 +1312,7 @@ version = "0.11.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.5",
"bytes", "bytes",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -1443,7 +1450,7 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2"
dependencies = [ dependencies = [
"base64 0.21.2", "base64 0.21.5",
] ]
[[package]] [[package]]
@ -1543,6 +1550,18 @@ dependencies = [
"unsafe-libyaml", "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]] [[package]]
name = "sha1" name = "sha1"
version = "0.10.5" version = "0.10.5"
@ -1615,6 +1634,12 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.29" version = "2.0.29"

View file

@ -21,7 +21,6 @@ percent-encoding = "2.3"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
futures = "0.3" futures = "0.3"
base64 = "0.21"
async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] } async_zip = { version = "0.0.15", default-features = false, features = ["deflate", "chrono", "tokio"] }
headers = "0.3" headers = "0.3"
mime_guess = "2.0" mime_guess = "2.0"
@ -46,6 +45,8 @@ chardetng = "0.1"
glob = "0.3.1" glob = "0.3.1"
indexmap = "2.0" indexmap = "2.0"
serde_yaml = "0.9.27" serde_yaml = "0.9.27"
sha-crypt = "0.5.0"
base64 = "0.21.5"
[features] [features]
default = ["tls"] default = ["tls"]

View file

@ -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/`. `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. 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 ### Hide Paths
Dufs supports hiding paths from directory listings via option `--hidden <glob>,...`. Dufs supports hiding paths from directory listings via option `--hidden <glob>,...`.

View file

@ -11,7 +11,7 @@ use std::{
}; };
use uuid::Uuid; use uuid::Uuid;
use crate::utils::unix_now; use crate::{args::Args, utils::unix_now};
const REALM: &str = "DUFS"; const REALM: &str = "DUFS";
const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days const DIGEST_AUTH_TIMEOUT: u32 = 604800; // 7 days
@ -27,6 +27,7 @@ lazy_static! {
#[derive(Debug)] #[derive(Debug)]
pub struct AccessControl { pub struct AccessControl {
use_hashed_password: bool,
users: IndexMap<String, (String, AccessPaths)>, users: IndexMap<String, (String, AccessPaths)>,
anony: Option<AccessPaths>, anony: Option<AccessPaths>,
} }
@ -34,6 +35,7 @@ pub struct AccessControl {
impl Default for AccessControl { impl Default for AccessControl {
fn default() -> Self { fn default() -> Self {
AccessControl { AccessControl {
use_hashed_password: false,
anony: Some(AccessPaths::new(AccessPerm::ReadWrite)), anony: Some(AccessPaths::new(AccessPerm::ReadWrite)),
users: IndexMap::new(), users: IndexMap::new(),
} }
@ -45,6 +47,7 @@ impl AccessControl {
if raw_rules.is_empty() { if raw_rules.is_empty() {
return Ok(Default::default()); return Ok(Default::default());
} }
let mut use_hashed_password = false;
let create_err = |v: &str| anyhow!("Invalid auth `{v}`"); let create_err = |v: &str| anyhow!("Invalid auth `{v}`");
let mut anony = None; let mut anony = None;
let mut anony_paths = vec![]; let mut anony_paths = vec![];
@ -72,6 +75,9 @@ impl AccessControl {
if user.is_empty() || pass.is_empty() { if user.is_empty() || pass.is_empty() {
return Err(create_err(rule)); return Err(create_err(rule));
} }
if pass.starts_with("$6$") {
use_hashed_password = true;
}
users.insert(user.to_string(), (pass.to_string(), paths)); users.insert(user.to_string(), (pass.to_string(), paths));
} else { } else {
return Err(create_err(rule)); return Err(create_err(rule));
@ -82,7 +88,11 @@ impl AccessControl {
paths.add(path, perm) paths.add(path, perm)
} }
} }
Ok(Self { users, anony }) Ok(Self {
use_hashed_password,
users,
anony,
})
} }
pub fn exist(&self) -> bool { pub fn exist(&self) -> bool {
@ -244,12 +254,16 @@ impl AccessPerm {
} }
} }
pub fn www_authenticate() -> Result<HeaderValue> { pub fn www_authenticate(args: &Args) -> Result<HeaderValue> {
let nonce = create_nonce()?; let value = if args.auth.use_hashed_password {
let value = format!( format!("Basic realm=\"{}\"", REALM)
"Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"", } else {
REALM, nonce, REALM let nonce = create_nonce()?;
); format!(
"Digest realm=\"{}\", nonce=\"{}\", qop=\"auth\", Basic realm=\"{}\"",
REALM, nonce, REALM
)
};
Ok(HeaderValue::from_str(&value)?) Ok(HeaderValue::from_str(&value)?)
} }
@ -274,14 +288,18 @@ pub fn check_auth(
auth_pass: &str, auth_pass: &str,
) -> Option<()> { ) -> Option<()> {
if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") { if let Some(value) = strip_prefix(authorization.as_bytes(), b"Basic ") {
let basic_value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?; let value: Vec<u8> = general_purpose::STANDARD.decode(value).ok()?;
let parts: Vec<&str> = std::str::from_utf8(&basic_value).ok()?.split(':').collect(); let parts: Vec<&str> = std::str::from_utf8(&value).ok()?.split(':').collect();
if parts[0] != auth_user { if parts[0] != auth_user {
return None; 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(()); return Some(());
} }

View file

@ -1037,7 +1037,7 @@ impl Server {
fn auth_reject(&self, res: &mut Response) -> Result<()> { fn auth_reject(&self, res: &mut Response) -> Result<()> {
set_webdav_headers(res); set_webdav_headers(res);
res.headers_mut() 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 // set 401 to make the browser pop up the login box
*res.status_mut() = StatusCode::UNAUTHORIZED; *res.status_mut() = StatusCode::UNAUTHORIZED;
Ok(()) Ok(())

View file

@ -29,6 +29,32 @@ fn auth(#[with(&["--auth", "user:pass@/:rw", "-A"])] server: TestServer) -> Resu
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] #[rstest]
fn auth_and_public( fn auth_and_public(
#[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer, #[with(&["--auth", "user:pass@/:rw|@/", "-A"])] server: TestServer,