use crate::auth::sigv4::hmac_sha256;
use crate::error::ProxyError;
use http::HeaderMap;
pub struct S3RequestSigner {
pub access_key_id: String,
pub secret_access_key: String,
pub region: String,
pub service: String,
pub session_token: Option<String>,
}
impl S3RequestSigner {
pub fn new(
access_key_id: String,
secret_access_key: String,
region: String,
session_token: Option<String>,
) -> Self {
Self {
access_key_id,
secret_access_key,
region,
service: "s3".to_string(),
session_token,
}
}
pub fn sign_request(
&self,
method: &http::Method,
url: &url::Url,
headers: &mut HeaderMap,
payload_hash: &str,
) -> Result<(), ProxyError> {
use chrono::Utc;
let now = Utc::now();
let date_stamp = now.format("%Y%m%d").to_string();
let amz_date = now.format("%Y%m%dT%H%M%SZ").to_string();
headers.insert("x-amz-date", amz_date.parse().unwrap());
headers.insert("x-amz-content-sha256", payload_hash.parse().unwrap());
if let Some(token) = &self.session_token {
headers.insert("x-amz-security-token", token.parse().unwrap());
}
let host = url
.host_str()
.ok_or_else(|| ProxyError::Internal("no host in URL".into()))?;
let host_header = if let Some(port) = url.port() {
format!("{}:{}", host, port)
} else {
host.to_string()
};
headers.insert("host", host_header.parse().unwrap());
let canonical_uri = url.path();
let canonical_querystring = url.query().unwrap_or("");
let mut signed_header_names: Vec<&str> = headers.keys().map(|k| k.as_str()).collect();
signed_header_names.sort();
let canonical_headers: String = signed_header_names
.iter()
.map(|k| {
let v = headers.get(*k).unwrap().to_str().unwrap_or("").trim();
format!("{}:{}\n", k, v)
})
.collect();
let signed_headers = signed_header_names.join(";");
let canonical_request = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
method,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
payload_hash
);
let credential_scope = format!(
"{}/{}/{}/aws4_request",
date_stamp, self.region, self.service
);
use sha2::{Digest, Sha256};
let canonical_request_hash = hex::encode(Sha256::digest(canonical_request.as_bytes()));
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
amz_date, credential_scope, canonical_request_hash
);
let k_date = hmac_sha256(
format!("AWS4{}", self.secret_access_key).as_bytes(),
date_stamp.as_bytes(),
)?;
let k_region = hmac_sha256(&k_date, self.region.as_bytes())?;
let k_service = hmac_sha256(&k_region, self.service.as_bytes())?;
let signing_key = hmac_sha256(&k_service, b"aws4_request")?;
let signature = hex::encode(hmac_sha256(&signing_key, string_to_sign.as_bytes())?);
let auth_header = format!(
"AWS4-HMAC-SHA256 Credential={}/{}, SignedHeaders={}, Signature={}",
self.access_key_id, credential_scope, signed_headers, signature
);
headers.insert("authorization", auth_header.parse().unwrap());
Ok(())
}
}
pub fn hash_payload(payload: &[u8]) -> String {
use sha2::{Digest, Sha256};
hex::encode(Sha256::digest(payload))
}
pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD";
pub const STREAMING_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD";