feat: support hashed password (#283)
This commit is contained in:
parent
80ac9afe68
commit
d3de3db0d9
6 changed files with 112 additions and 19 deletions
35
Cargo.lock
generated
35
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"]
|
||||
|
|
25
README.md
25
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>,...`.
|
||||
|
|
34
src/auth.rs
34
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<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> {
|
||||
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()?;
|
||||
let value = format!(
|
||||
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(());
|
||||
}
|
||||
|
||||
|
|
|
@ -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(())
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue