Skip to content

Commit

Permalink
feat: support hashed password (#283)
Browse files Browse the repository at this point in the history
  • Loading branch information
sigoden committed Nov 4, 2023
1 parent 80ac9af commit d3de3db
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 19 deletions.
35 changes: 30 additions & 5 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"]
Expand Down
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>,...`.
Expand Down
40 changes: 29 additions & 11 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,13 +27,15 @@ lazy_static! {

#[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(),
}
Expand All @@ -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![];
Expand Down Expand Up @@ -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));
Expand All @@ -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 {
Expand Down Expand Up @@ -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)?)
}

Expand All @@ -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(());
}

Expand Down
2 changes: 1 addition & 1 deletion src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
26 changes: 26 additions & 0 deletions tests/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit d3de3db

Please sign in to comment.