cleanup auth code, verify CSRF prevention token
This commit is contained in:
parent
c8fd2dd2b8
commit
5ddf8cb10a
@ -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<i64, Error> {
|
||||
|
||||
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"));
|
||||
|
@ -422,6 +422,48 @@ fn handle_static_file_download(filename: PathBuf) -> BoxFut {
|
||||
return Box::new(response);
|
||||
}
|
||||
|
||||
fn extract_auth_data(headers: &http::HeaderMap) -> (Option<String>, Option<String>) {
|
||||
|
||||
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<String>, token: Option<String>) -> Result<String, Error> {
|
||||
|
||||
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<ApiConfig>, req: Request<Body>) -> BoxFut {
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
@ -457,20 +499,9 @@ pub fn handle_request(api: Arc<ApiConfig>, req: Request<Body>) -> 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<ApiConfig>, req: Request<Body>) -> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user