diff options
author | Sven-Hendrik Haase <svenstaro@gmail.com> | 2019-04-26 14:27:03 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-04-26 14:27:03 +0000 |
commit | eddeefc3ae3a655cf57e3e259464cee464b6f96e (patch) | |
tree | ebb46cbcdde44f87e0d02791bb8e3b20baf8ec37 | |
parent | Use rstest test fixtures to cut down on code duplication in integration tests (diff) | |
parent | Use 'if let' (diff) | |
download | miniserve-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 '')
-rw-r--r-- | Cargo.lock | 83 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | src/args.rs | 128 | ||||
-rw-r--r-- | src/auth.rs | 122 | ||||
-rw-r--r-- | src/errors.rs | 12 | ||||
-rw-r--r-- | src/main.rs | 2 | ||||
-rw-r--r-- | stale_outputs_checked | 0 |
7 files changed, 318 insertions, 31 deletions
@@ -276,6 +276,25 @@ dependencies = [ ] [[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "block-padding" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "brotli-sys" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -307,6 +326,11 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "byteorder" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -514,6 +538,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "digest" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "dirs" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -641,6 +673,11 @@ dependencies = [ ] [[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "filetime" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -730,6 +767,14 @@ dependencies = [ ] [[package]] +name = "generic-array" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "globset" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -776,6 +821,11 @@ dependencies = [ ] [[package]] +name = "hex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "hostname" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1094,6 +1144,7 @@ dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)", + "hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", "libflate 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1106,6 +1157,7 @@ dependencies = [ "rstest 0.2.2 (git+https://github.com/la10736/rstest.git)", "select 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.90 (registry+https://github.com/rust-lang/crates.io-index)", + "sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", "structopt 0.2.15 (registry+https://github.com/rust-lang/crates.io-index)", "strum 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1267,6 +1319,11 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "opaque-debug" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "openssl" version = "0.10.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1850,6 +1907,17 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] +name = "sha2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", + "fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] name = "signal-hook" version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2381,6 +2449,11 @@ dependencies = [ ] [[package]] +name = "typenum" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] name = "ucd-util" version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2653,10 +2726,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum bit-vec 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "02b4ff8b16e6076c3e14220b39fbc1fabb6737522281a388998046859400895f" "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" "checksum blake2-rfc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "5d6d530bdd2d52966a6d03b7a964add7ae1a288d25214066fd4b600f0f796400" +"checksum block-buffer 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +"checksum block-padding 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d75255892aeb580d3c566f213a2b6fdc1c66667839f45719ee1d30ebf2aea591" "checksum brotli-sys 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "4445dea95f4c2b41cde57cc9fee236ae4dbae88d8fcbdb4750fc1bb5d86aaecd" "checksum brotli2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "0cb036c3eade309815c15ddbacec5b22c4d1f3983a774ab2eac2e3e9ea85568e" "checksum bstr 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6c8203ca06c502958719dae5f653a79e0cc6ba808ed02beffbf27d09610f2143" "checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39" +"checksum byte-tools 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" "checksum byteorder 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a019b10a2a7cdeb292db131fc8113e57ea2a908f6e7894b0c3c671893b65dbeb" "checksum bytes 0.4.12 (registry+https://github.com/rust-lang/crates.io-index)" = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" "checksum bytesize 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "716960a18f978640f25101b5cbf1c6f6b0d3192fab36a2d98ca96f0ecbe41010" @@ -2681,6 +2757,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum ctor 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3b4c17619643c1252b5f690084b82639dd7fac141c57c8e77a00e0148132092c" "checksum debug_unreachable 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9a032eac705ca39214d169f83e3d3da290af06d8d1d344d1baad2fd002dca4b3" "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +"checksum digest 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "05f47366984d3ad862010e22c7ce81a7dbcaebbdfb37241a620f8b6596ee135c" "checksum dirs 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "3fd78930633bd1c6e35c4b42b1df7b0cbc6bc191146e512bb3bedf243fcc3901" "checksum dtoa 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "6d301140eb411af13d3115f9a562c85cc6b541ade9dfa314132244aaee7489dd" "checksum encoding 0.2.33 (registry+https://github.com/rust-lang/crates.io-index)" = "6b0d943856b990d12d3b55b359144ff341533e516d94098b1d3fc1ac666d36ec" @@ -2696,6 +2773,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum escargot 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ceb9adbf9874d5d028b5e4c5739d22b71988252b25c9c98fe7cf9738bee84597" "checksum failure 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "795bd83d3abeb9220f257e597aa0080a508b27533824adf336529648f6abf7e2" "checksum failure_derive 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ea1063915fd7ef4309e222a5a07cf9c319fb9c7836b1f89b85458672dbb127e1" +"checksum fake-simd 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" "checksum filetime 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2f8c63033fcba1f51ef744505b3cad42510432b904c062afa67ad7ece008429d" "checksum flate2 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f87e68aa82b2de08a6e037f1385455759df6e445a8df5e005b4297191dbf18aa" "checksum float-cmp 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "134a8fa843d80a51a5b77d36d42bc2def9edcb0262c914861d08129fd1926600" @@ -2708,10 +2786,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum futf 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" "checksum futures 0.1.26 (registry+https://github.com/rust-lang/crates.io-index)" = "62941eff9507c8177d448bd83a44d9b9760856e184081d8cd79ba9f03dd24981" "checksum futures-cpupool 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "ab90cde24b3319636588d0c35fe03b1333857621051837ed769faefb4c2162e4" +"checksum generic-array 0.12.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3c0f28c2f5bfb5960175af447a2da7c18900693738343dc896ffbcabd9839592" "checksum globset 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ef4feaabe24a0a658fd9cf4a9acf6ed284f045c77df0f49020ba3245cfb7b454" "checksum globwalk 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89fa2e29859da05acd066bd45996f05c271b271d7ec4a781f909682328f65d25" "checksum h2 0.1.18 (registry+https://github.com/rust-lang/crates.io-index)" = "85ab6286db06040ddefb71641b50017c06874614001a134b423783e2db2920bd" "checksum heck 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +"checksum hex 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" "checksum hostname 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "21ceb46a83a85e824ef93669c8b390009623863b5c195d1ba747292c0c72f94e" "checksum html5ever 0.18.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a49d5001dd1bddf042ea41ed4e0a671d50b1bf187e66b349d7ec613bdce4ad90" "checksum htmlescape 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" @@ -2765,6 +2845,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum num-traits 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3a5d7cc97d6d30d8b9bc8fa19bf45349ffe46241e8816f50f62f6d6aaabee1" "checksum num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1a23f0ed30a54abaa0c7e83b1d2d87ada7c3c23078d1d87815af3e3b6385fbba" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +"checksum opaque-debug 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "93f5bb2e8e8dec81642920ccff6b61f1eb94fa3020c5a325c9851ff604152409" "checksum openssl 0.10.20 (registry+https://github.com/rust-lang/crates.io-index)" = "5a0d6b781aac4ac1bd6cafe2a2f0ad8c16ae8e1dd5184822a16c50139f8838d9" "checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de" "checksum openssl-sys 0.9.43 (registry+https://github.com/rust-lang/crates.io-index)" = "33c86834957dd5b915623e94f2f4ab2c70dd8f6b70679824155d5ae21dbd495d" @@ -2831,6 +2912,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" "checksum serde_urlencoded 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "642dd69105886af2efd227f75a520ec9b44a820d65bc133a9131f7d229fd165a" "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +"checksum sha2 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b4d8bfd0e469f417657573d8451fb33d16cfe0989359b93baf3a1ffc639543d" "checksum signal-hook 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "97a47ae722318beceb0294e6f3d601205a1e6abaa4437d9d33e3a212233e3021" "checksum simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e95345f185d5adeb8ec93459d2dc99654e294cc6ccf5b75414d8ea262de9a13" "checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" @@ -2881,6 +2963,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum trust-dns-resolver 0.10.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8a9f877f7a1ad821ab350505e1f1b146a4960402991787191d6d8cab2ce2de2c" "checksum try-lock 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e604eb7b43c06650e854be16a2a03155743d3752dd1c943f6829e26b7a36e382" "checksum try_from 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "283d3b89e1368717881a9d51dad843cc435380d8109c9e47d38780a324698d8b" +"checksum typenum 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "612d636f949607bdf9b123b4a6f6d966dedf3ff669f7f045890d3a4a73948169" "checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86" "checksum unicase 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7f4765f83163b74f957c797ad9253caf97f103fb064d3999aea9568d09fc8a33" "checksum unicase 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "41d17211f887da8e4a70a45b9536f26fc5de166b81e2d5d80de4a17fd22553bd" @@ -45,6 +45,8 @@ failure = "0.1.5" log = "0.4.6" strum = "0.15.0" strum_macros = "0.15.0" +sha2 = "0.8.0" +hex = "0.3.2" [dev-dependencies] assert_cmd = "0.11" 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, diff --git a/stale_outputs_checked b/stale_outputs_checked new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/stale_outputs_checked |