aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorSven-Hendrik Haase <svenstaro@gmail.com>2019-04-26 14:27:03 +0000
committerGitHub <noreply@github.com>2019-04-26 14:27:03 +0000
commiteddeefc3ae3a655cf57e3e259464cee464b6f96e (patch)
treeebb46cbcdde44f87e0d02791bb8e3b20baf8ec37 /src
parentUse rstest test fixtures to cut down on code duplication in integration tests (diff)
parentUse 'if let' (diff)
downloadminiserve-eddeefc3ae3a655cf57e3e259464cee464b6f96e.tar.gz
miniserve-eddeefc3ae3a655cf57e3e259464cee464b6f96e.zip
Merge pull request #76 from KSXGitHub/pullrequest.hashed-password
Add support for hashed password (sha256 and sha512)
Diffstat (limited to 'src')
-rw-r--r--src/args.rs128
-rw-r--r--src/auth.rs122
-rw-r--r--src/errors.rs12
-rw-r--r--src/main.rs2
4 files changed, 233 insertions, 31 deletions
diff --git a/src/args.rs b/src/args.rs
index 8d2e105..4077f35 100644
--- a/src/args.rs
+++ b/src/args.rs
@@ -38,9 +38,10 @@ struct CLIArgs {
)]
interfaces: Vec<IpAddr>,
- /// Set authentication (username:password)
+ /// Set authentication. Currently supported formats:
+ /// username:password, username:sha256:hash, username:sha512:hash
#[structopt(short = "a", long = "auth", parse(try_from_str = "parse_auth"))]
- auth: Option<(String, String)>,
+ auth: Option<auth::RequiredAuth>,
/// Generate a random 6-hexdigit route
#[structopt(long = "random-route")]
@@ -77,34 +78,55 @@ fn parse_interface(src: &str) -> Result<IpAddr, std::net::AddrParseError> {
}
/// Checks wether the auth string is valid, i.e. it follows the syntax username:password
-fn parse_auth(src: &str) -> Result<(String, String), ContextualError> {
- let mut split = src.splitn(2, ':');
+fn parse_auth(src: &str) -> Result<auth::RequiredAuth, ContextualError> {
+ let mut split = src.splitn(3, ':');
+ let invalid_auth_format = Err(
+ ContextualError::new(ContextualErrorKind::InvalidAuthFormat)
+ );
let username = match split.next() {
Some(username) => username,
- None => {
- return Err(ContextualError::new(ContextualErrorKind::InvalidAuthFormat));
- }
+ None => return invalid_auth_format,
};
- let password = match split.next() {
+ // second_part is either password in username:password or method in username:method:hash
+ let second_part = match split.next() {
// This allows empty passwords, as the spec does not forbid it
Some(password) => password,
- None => {
- return Err(ContextualError::new(ContextualErrorKind::InvalidAuthFormat));
- }
+ None => return invalid_auth_format,
};
- // To make it Windows-compatible, the password needs to be shorter than 255 characters.
- // After 255 characters, Windows will truncate the value.
- // As for the username, the spec does not mention a limit in length
- if password.len() > 255 {
- return Err(ContextualError::new(
- ContextualErrorKind::PasswordTooLongError,
- ));
- }
+ let password = if let Some(hash_hex) = split.next() {
+ let hash_bin = if let Ok(hash_bin) = hex::decode(hash_hex) {
+ hash_bin
+ } else {
+ return Err(ContextualError::new(ContextualErrorKind::InvalidPasswordHash))
+ };
+
+ match second_part {
+ "sha256" => auth::RequiredAuthPassword::Sha256(hash_bin.to_owned()),
+ "sha512" => auth::RequiredAuthPassword::Sha512(hash_bin.to_owned()),
+ _ => {
+ return Err(ContextualError::new(
+ ContextualErrorKind::InvalidHashMethod(second_part.to_owned())
+ ))
+ },
+ }
+ } else {
+ // To make it Windows-compatible, the password needs to be shorter than 255 characters.
+ // After 255 characters, Windows will truncate the value.
+ // As for the username, the spec does not mention a limit in length
+ if second_part.len() > 255 {
+ return Err(ContextualError::new(ContextualErrorKind::PasswordTooLongError));
+ }
+
+ auth::RequiredAuthPassword::Plain(second_part.to_owned())
+ };
- Ok((username.to_owned(), password.to_owned()))
+ Ok(auth::RequiredAuth {
+ username: username.to_owned(),
+ password,
+ })
}
/// Parses the command line arguments
@@ -120,11 +142,6 @@ pub fn parse_args() -> crate::MiniserveConfig {
]
};
- let auth = match args.auth {
- Some((username, password)) => Some(auth::BasicAuthParams { username, password }),
- None => None,
- };
-
let random_route = if args.random_route {
Some(nanoid::custom(6, &ROUTE_ALPHABET))
} else {
@@ -140,7 +157,7 @@ pub fn parse_args() -> crate::MiniserveConfig {
path: args.path.unwrap_or_else(|| PathBuf::from(".")),
port: args.port,
interfaces,
- auth,
+ auth: args.auth,
path_explicitly_chosen,
no_symlinks: args.no_symlinks,
random_route,
@@ -149,3 +166,62 @@ pub fn parse_args() -> crate::MiniserveConfig {
file_upload: args.file_upload,
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rstest::rstest_parametrize;
+
+ /// Helper function that creates a `RequiredAuth` structure
+ fn create_required_auth(username: &str, password: &str, encrypt: &str) -> auth::RequiredAuth {
+ use auth::*;
+ use RequiredAuthPassword::*;
+
+ RequiredAuth {
+ username: username.to_owned(),
+ password: match encrypt {
+ "plain" => Plain(password.to_owned()),
+ "sha256" => Sha256(hex::decode(password.to_owned()).unwrap()),
+ "sha512" => Sha512(hex::decode(password.to_owned()).unwrap()),
+ _ => panic!("Unknown encryption type"),
+ },
+ }
+ }
+
+ #[rstest_parametrize(
+ auth_string, username, password, encrypt,
+ case("username:password", "username", "password", "plain"),
+ case("username:sha256:abcd", "username", "abcd", "sha256"),
+ case("username:sha512:abcd", "username", "abcd", "sha512")
+ )]
+ fn parse_auth_valid(auth_string: &str, username: &str, password: &str, encrypt: &str) {
+ assert_eq!(
+ parse_auth(auth_string).unwrap(),
+ create_required_auth(username, password, encrypt),
+ );
+ }
+
+ #[rstest_parametrize(
+ auth_string, err_msg,
+ case(
+ "foo",
+ "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash"
+ ),
+ case(
+ "username:blahblah:abcd",
+ "blahblah is not a valid hashing method. Expected sha256 or sha512"
+ ),
+ case(
+ "username:sha256:invalid",
+ "Invalid format for password hash. Expected hex code"
+ ),
+ case(
+ "username:sha512:invalid",
+ "Invalid format for password hash. Expected hex code"
+ ),
+ )]
+ fn parse_auth_invalid(auth_string: &str, err_msg: &str) {
+ let err = parse_auth(auth_string).unwrap_err();
+ assert_eq!(format!("{}", err), err_msg.to_owned());
+ }
+}
diff --git a/src/auth.rs b/src/auth.rs
index 1bdf0be..e75f498 100644
--- a/src/auth.rs
+++ b/src/auth.rs
@@ -1,6 +1,7 @@
use actix_web::http::header;
use actix_web::middleware::{Middleware, Response};
use actix_web::{HttpRequest, HttpResponse, Result};
+use sha2::{Digest, Sha256, Sha512};
use crate::errors::{ContextualError, ContextualErrorKind};
@@ -13,6 +14,21 @@ pub struct BasicAuthParams {
pub password: String,
}
+#[derive(Clone, Debug, PartialEq)]
+/// `password` field of `RequiredAuth`
+pub enum RequiredAuthPassword {
+ Plain(String),
+ Sha256(Vec<u8>),
+ Sha512(Vec<u8>),
+}
+
+#[derive(Clone, Debug, PartialEq)]
+/// Authentication structure to match `BasicAuthParams` against
+pub struct RequiredAuth {
+ pub username: String,
+ pub password: RequiredAuthPassword,
+}
+
/// Decode a HTTP basic auth string into a tuple of username and password.
pub fn parse_basic_auth(
authorization_header: &header::HeaderValue,
@@ -39,6 +55,37 @@ pub fn parse_basic_auth(
})
}
+/// Verify authentication
+pub fn match_auth(basic_auth: BasicAuthParams, required_auth: &RequiredAuth) -> bool {
+ if basic_auth.username != required_auth.username {
+ return false;
+ }
+
+ match &required_auth.password {
+ RequiredAuthPassword::Plain(ref required_password) => {
+ basic_auth.password == *required_password
+ }
+ RequiredAuthPassword::Sha256(password_hash) => {
+ compare_hash::<Sha256>(basic_auth.password, password_hash)
+ }
+ RequiredAuthPassword::Sha512(password_hash) => {
+ compare_hash::<Sha512>(basic_auth.password, password_hash)
+ }
+ }
+}
+
+/// Return `true` if hashing of `password` by `T` algorithm equals to `hash`
+pub fn compare_hash<T: Digest>(password: String, hash: &Vec<u8>) -> bool {
+ get_hash::<T>(password) == *hash
+}
+
+/// Get hash of a `text`
+pub fn get_hash<T: Digest>(text: String) -> Vec<u8> {
+ let mut hasher = T::new();
+ hasher.input(text);
+ hasher.result().to_vec()
+}
+
impl Middleware<crate::MiniserveConfig> for Auth {
fn response(
&self,
@@ -58,9 +105,7 @@ impl Middleware<crate::MiniserveConfig> for Auth {
));
}
};
- if auth_req.username != required_auth.username
- || auth_req.password != required_auth.password
- {
+ if !match_auth(auth_req, required_auth) {
let new_resp = HttpResponse::Unauthorized().finish();
return Ok(Response::Done(new_resp));
}
@@ -77,3 +122,74 @@ impl Middleware<crate::MiniserveConfig> for Auth {
Ok(Response::Done(resp))
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use rstest::rstest_parametrize;
+
+ /// Return a hashing function corresponds to given name
+ fn get_hash_func(name: &str) -> impl FnOnce(String) -> Vec<u8> {
+ match name {
+ "sha256" => get_hash::<Sha256>,
+ "sha512" => get_hash::<Sha512>,
+ _ => panic!("Invalid hash method"),
+ }
+ }
+
+ #[rstest_parametrize(
+ password, hash_method, hash,
+ case("abc", "sha256", "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
+ case("abc", "sha512", "ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"),
+ )]
+ fn test_get_hash(password: &str, hash_method: &str, hash: &str) {
+ let hash_func = get_hash_func(hash_method);
+ let expected = hex::decode(hash).expect("Provided hash is not a valid hex code");
+ let received = hash_func(password.to_owned());
+ assert_eq!(received, expected);
+ }
+
+ /// Helper function that creates a `RequiredAuth` structure and encrypt `password` if necessary
+ fn create_required_auth(username: &str, password: &str, encrypt: &str) -> RequiredAuth {
+ use RequiredAuthPassword::*;
+
+ RequiredAuth {
+ username: username.to_owned(),
+ password: match encrypt {
+ "plain" => Plain(password.to_owned()),
+ "sha256" => Sha256(get_hash::<sha2::Sha256>(password.to_owned())),
+ "sha512" => Sha512(get_hash::<sha2::Sha512>(password.to_owned())),
+ _ => panic!("Unknown encryption type"),
+ },
+ }
+ }
+
+ #[rstest_parametrize(
+ should_pass, param_username, param_password, required_username, required_password, encrypt,
+ case(true, "obi", "hello there", "obi", "hello there", "plain"),
+ case(false, "obi", "hello there", "obi", "hi!", "plain"),
+ case(true, "obi", "hello there", "obi", "hello there", "sha256"),
+ case(false, "obi", "hello there", "obi", "hi!", "sha256"),
+ case(true, "obi", "hello there", "obi", "hello there", "sha512"),
+ case(false, "obi", "hello there", "obi", "hi!", "sha512"),
+ )]
+ fn test_auth(
+ should_pass: bool,
+ param_username: &str,
+ param_password: &str,
+ required_username: &str,
+ required_password: &str,
+ encrypt: &str,
+ ) {
+ assert_eq!(
+ match_auth(
+ BasicAuthParams {
+ username: param_username.to_owned(),
+ password: param_password.to_owned(),
+ },
+ &create_required_auth(required_username, required_password, encrypt),
+ ),
+ should_pass,
+ )
+ }
+}
diff --git a/src/errors.rs b/src/errors.rs
index bd6dc66..833e9c4 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -27,9 +27,19 @@ pub enum ContextualErrorKind {
InvalidPathError(String),
/// This error might occur if the HTTP credential string does not respect the expected format
- #[fail(display = "Invalid format for credentials string. Expected is username:format")]
+ #[fail(
+ display = "Invalid format for credentials string. Expected username:password, username:sha256:hash or username:sha512:hash"
+ )]
InvalidAuthFormat,
+ /// This error might occure if the hash method is neither sha256 nor sha512
+ #[fail(display = "{} is not a valid hashing method. Expected sha256 or sha512", _0)]
+ InvalidHashMethod(String),
+
+ /// This error might occur if the HTTP auth hash password is not a valid hex code
+ #[fail(display = "Invalid format for password hash. Expected hex code")]
+ InvalidPasswordHash,
+
/// This error might occur if the HTTP auth password exceeds 255 characters
#[fail(display = "HTTP password length exceeds 255 characters")]
PasswordTooLongError,
diff --git a/src/main.rs b/src/main.rs
index cf7ca93..bc8f3f0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -37,7 +37,7 @@ pub struct MiniserveConfig {
pub interfaces: Vec<IpAddr>,
/// Enable HTTP basic authentication
- pub auth: Option<auth::BasicAuthParams>,
+ pub auth: Option<auth::RequiredAuth>,
/// If false, miniserve will serve the current working directory
pub path_explicitly_chosen: bool,