diff --git a/src/auth_helpers.rs b/src/auth_helpers.rs index fa0217dd..d2e0a9fa 100644 --- a/src/auth_helpers.rs +++ b/src/auth_helpers.rs @@ -9,24 +9,79 @@ use openssl::sha; use std::path::PathBuf; +fn compute_csrf_secret_digest( + timestamp: i64, + secret: &[u8], + username: &str, +) -> String { + + let mut hasher = sha::Sha256::new(); + let data = format!("{:08X}:{}:", timestamp, username); + hasher.update(data.as_bytes()); + hasher.update(secret); + + base64::encode_config(&hasher.finish(), base64::STANDARD_NO_PAD) +} + pub fn assemble_csrf_prevention_token( secret: &[u8], username: &str, ) -> String { let epoch = std::time::SystemTime::now().duration_since( - std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs(); + std::time::SystemTime::UNIX_EPOCH).unwrap().as_secs() as i64; - let mut hasher = sha::Sha256::new(); - let data = format!("{:08X}:{}:", epoch, username); - hasher.update(data.as_bytes()); - hasher.update(secret); - - let digest = base64::encode_config(&hasher.finish(), base64::STANDARD_NO_PAD); + let digest = compute_csrf_secret_digest(epoch, secret, username); format!("{:08X}:{}", epoch, digest) } +pub fn verify_csrf_prevention_token( + secret: &[u8], + username: &str, + token: &str, + min_age: i64, + max_age: i64, +) -> Result { + + use std::collections::VecDeque; + + let mut parts: VecDeque<&str> = token.split(':').collect(); + + try_block!({ + + if parts.len() != 2 { + bail!("format error - wrong number of parts."); + } + + let timestamp = parts.pop_front().unwrap(); + let sig = parts.pop_front().unwrap(); + + let ttime = i64::from_str_radix(timestamp, 16). + map_err(|err| format_err!("timestamp format error - {}", err))?; + + let digest = compute_csrf_secret_digest(ttime, secret, username); + + if digest != sig { + bail!("invalid signature."); + } + + let now = std::time::SystemTime::now().duration_since( + std::time::SystemTime::UNIX_EPOCH)?.as_secs() as i64; + + let age = now - ttime; + if age < min_age { + bail!("timestamp newer than expected."); + } + + if age > max_age { + bail!("timestamp too old."); + } + + Ok(age) + }).map_err(|err| format_err!("invalid csrf token - {}", err)) +} + pub fn generate_csrf_key() -> Result<(), Error> { let path = PathBuf::from(configdir!("/csrf.key")); diff --git a/src/server/rest.rs b/src/server/rest.rs index 0910f6f4..5d0f8249 100644 --- a/src/server/rest.rs +++ b/src/server/rest.rs @@ -422,6 +422,48 @@ fn handle_static_file_download(filename: PathBuf) -> BoxFut { return Box::new(response); } +fn extract_auth_data(headers: &http::HeaderMap) -> (Option, Option) { + + let mut ticket = None; + if let Some(raw_cookie) = headers.get("COOKIE") { + if let Ok(cookie) = raw_cookie.to_str() { + ticket = tools::extract_auth_cookie(cookie, "PBSAuthCookie"); + } + } + + let token = match headers.get("CSRFPreventionToken").map(|v| v.to_str()) { + Some(Ok(v)) => Some(v.to_owned()), + _ => None, + }; + + (ticket, token) +} + +fn check_auth(method: &hyper::Method, ticket: Option, token: Option) -> Result { + + let ticket_lifetime = 3600*2; // 2 hours + + let username = match ticket { + Some(ticket) => match tools::ticket::verify_rsa_ticket(public_auth_key(), "PBS", &ticket, None, -300, ticket_lifetime) { + Ok((_age, Some(username))) => username.to_owned(), + Ok((_, None)) => bail!("ticket without username."), + Err(err) => return Err(err), + } + None => bail!("missing ticket"), + }; + + if method != hyper::Method::GET { + if let Some(token) = token { + println!("CSRF prev token: {:?}", token); + verify_csrf_prevention_token(csrf_secret(), &username, &token, -300, ticket_lifetime)?; + } else { + bail!(""); + } + } + + Ok(username) +} + pub fn handle_request(api: Arc, req: Request) -> BoxFut { let (parts, body) = req.into_parts(); @@ -457,20 +499,9 @@ pub fn handle_request(api: Arc, req: Request) -> BoxFut { let delay_unauth_time = std::time::Instant::now() + std::time::Duration::from_millis(3000); - if let Some(raw_cookie) = parts.headers.get("COOKIE") { - if let Ok(cookie) = raw_cookie.to_str() { - if let Some(ticket) = tools::extract_auth_cookie(cookie, "PBSAuthCookie") { - if let Ok((_, Some(username))) = tools::ticket::verify_rsa_ticket( - public_auth_key(), "PBS", &ticket, None, -300, 3600*2) { - rpcenv.set_user(Some(username)); - } - } - } - } - - if comp_len >= 1 && components[0] == "api2" { println!("GOT API REQUEST"); + if comp_len >= 2 { let format = components[1]; let formatter = match format { @@ -486,16 +517,24 @@ pub fn handle_request(api: Arc, req: Request) -> BoxFut { if comp_len == 4 && components[2] == "access" && components[3] == "ticket" { // explicitly allow those calls without auth } else { - if let Some(_username) = rpcenv.get_user() { - // fixme: check permissions - } else { - // always delay unauthorized calls by 3 seconds (from start of request) - let resp = (formatter.format_error)(http_err!(UNAUTHORIZED, "permission check failed.".into())); - let delayed_response = tokio::timer::Delay::new(delay_unauth_time) - .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err))) - .and_then(|_| Ok(resp)); + let (ticket, token) = extract_auth_data(&parts.headers); + match check_auth(&method, ticket, token) { + Ok(username) => { - return Box::new(delayed_response); + // fixme: check permissions + + rpcenv.set_user(Some(username)); + } + Err(err) => { + // always delay unauthorized calls by 3 seconds (from start of request) + let err = http_err!(UNAUTHORIZED, format!("permission check failed - {}", err)); + let resp = (formatter.format_error)(err); + let delayed_response = tokio::timer::Delay::new(delay_unauth_time) + .map_err(|err| http_err!(INTERNAL_SERVER_ERROR, format!("tokio timer delay error: {}", err))) + .and_then(|_| Ok(resp)); + + return Box::new(delayed_response); + } } }