#![cfg_attr(feature = "clippy", feature(plugin))]
#![cfg_attr(feature = "clippy", plugin(clippy))]
#![cfg_attr(feature = "clippy", deny(clippy, clippy_pedantic))]
#![deny(missing_docs)]
extern crate chrono;
#[macro_use]
extern crate log;
extern crate rustc_serialize;
extern crate sodium_sys;
extern crate urlparse;
mod error;
mod types;
mod utils;
pub use error::{AWSAuthError, ParseRegionError, ParseServiceError};
pub use types::{Mode, Region, Service, SigningVersion};
pub use utils::{hashed_data, signed_data};
use chrono::{DateTime, UTC};
use rustc_serialize::base64::{STANDARD, ToBase64};
use rustc_serialize::hex::ToHex;
use sodium_sys::crypto::utils::init;
use std::collections::HashMap;
use std::fmt;
use std::sync::{ONCE_INIT, Once};
use urlparse::{quote, urlparse};
const AWS4_REQUEST: &'static str = "aws4_request";
const DATE_FMT: &'static str = "%Y%m%d";
const DATE_TIME_FMT: &'static str = "%Y%m%dT%H%M%SZ";
static START: Once = ONCE_INIT;
fn init() {
START.call_once(|| {
debug!("sodium_sys initialized");
init::init();
});
}
pub struct AWSAuth {
access_key_id: String,
chunk_size: usize,
date: DateTime<UTC>,
headers: HashMap<String, String>,
host: String,
mode: Mode,
path: String,
payload_hash: String,
query: String,
region: Region,
req_type: HttpRequestMethod,
sam: SAM,
secret_access_key: String,
seed: bool,
service: Service,
version: SigningVersion,
}
impl Default for AWSAuth {
fn default() -> AWSAuth {
AWSAuth {
access_key_id: String::new(),
chunk_size: 0,
date: UTC::now(),
headers: HashMap::new(),
host: String::new(),
mode: Mode::Normal,
path: String::new(),
payload_hash: String::new(),
query: String::new(),
region: Region::UsEast1,
req_type: HttpRequestMethod::GET,
sam: SAM::AWS4HMACSHA256,
secret_access_key: String::new(),
seed: false,
service: Service::S3,
version: SigningVersion::Four,
}
}
}
pub type AWSAuthResult = Result<String, AWSAuthError>;
impl AWSAuth {
pub fn new(url: &str) -> Result<AWSAuth, AWSAuthError> {
let parsed = urlparse(url);
let mut auth = AWSAuth { path: parsed.path, ..Default::default() };
if let Some(h) = parsed.hostname {
auth.host = h;
}
if let Some(q) = parsed.query {
auth.query = q;
}
Ok(auth)
}
pub fn add_header(&mut self, key: &str, val: &str) -> &mut AWSAuth {
self.headers.insert(key.to_owned(), val.to_owned());
self
}
pub fn set_access_key_id(&mut self, access_key_id: &str) -> &mut AWSAuth {
self.access_key_id = access_key_id.to_owned();
self
}
pub fn set_chunk_size(&mut self, chunk_size: usize) -> &mut AWSAuth {
self.chunk_size = chunk_size;
self
}
pub fn set_date(&mut self, date: DateTime<UTC>) -> &mut AWSAuth {
self.date = date;
self
}
pub fn set_mode(&mut self, mode: Mode) -> &mut AWSAuth {
self.mode = mode;
self
}
pub fn set_payload_hash(&mut self, payload_hash: &str) -> &mut AWSAuth {
self.payload_hash = payload_hash.to_owned();
self
}
pub fn set_region(&mut self, region: Region) -> &mut AWSAuth {
self.region = region;
self
}
pub fn set_request_type(&mut self, verb: HttpRequestMethod) -> &mut AWSAuth {
self.req_type = verb;
self
}
pub fn set_secret_access_key(&mut self, secret_access_key: &str) -> &mut AWSAuth {
self.secret_access_key = secret_access_key.to_owned();
self
}
pub fn set_sam(&mut self, sam: SAM) -> &mut AWSAuth {
self.sam = sam;
self
}
pub fn set_seed(&mut self, seed: bool) -> &mut AWSAuth {
self.seed = seed;
self
}
pub fn set_service(&mut self, service: Service) -> &mut AWSAuth {
self.service = service;
self
}
pub fn set_version(&mut self, version: SigningVersion) -> &mut AWSAuth {
self.version = version;
self
}
fn scope(&self) -> String {
let date_fmt = self.date.format(DATE_FMT).to_string();
format!("{}/{}/{}/{}",
date_fmt,
self.region,
self.service,
AWS4_REQUEST)
}
fn credential(&self) -> String {
format!("{}/{}", self.access_key_id, self.scope())
}
fn signed_headers(&self) -> String {
let mut keys: Vec<String> = self.headers.keys().map(|x| x.to_lowercase()).collect();
keys.sort();
let mut buf = String::new();
for key in &keys {
buf.push_str(key);
buf.push(';');
}
let trimmed = buf.trim_right_matches(';');
trimmed.to_owned()
}
fn canonical_uri(&self) -> AWSAuthResult {
if self.path.is_empty() {
Ok("/".to_owned())
} else {
Ok(try!(quote(&self.path, b"/")))
}
}
fn canonical_query_string(&self) -> AWSAuthResult {
let cqs = if self.query.is_empty() {
"".to_owned()
} else {
let qstrs: Vec<&str> = self.query.split('&').collect();
let mut params = HashMap::new();
for qstr in &qstrs {
let kvs: Vec<&str> = qstr.split('=').collect();
let key = try!(quote(kvs[0], b""));
if kvs.len() == 1 {
params.insert(key, "".to_owned());
} else {
let value = try!(quote(kvs[1], b""));
params.insert(key, value);
}
}
let mut keys: Vec<&str> = params.keys().map(|x| &x[..]).collect();
keys.sort();
let mut cqs = String::new();
let mut first = true;
for key in keys {
if let Some(val) = params.get(key) {
if !first {
cqs.push('&');
}
let kvp = format!("{}={}", key, val);
cqs.push_str(&kvp);
}
first = false;
}
cqs
};
Ok(cqs)
}
fn canonical_headers(&self) -> String {
let mut keys: Vec<&String> = self.headers.keys().collect();
keys.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
let mut buf = String::new();
for key in &keys {
if let Some(val) = self.headers.get(*key) {
buf.push_str(&format!("{}:{}\n", key.to_lowercase(), val.trim())[..]);
}
}
buf
}
fn sign_string(&self, string_to_sign: &str) -> AWSAuthResult {
let mut key = String::from("AWS4");
key.push_str(&self.secret_access_key);
let date = self.date.format(DATE_FMT).to_string();
let region = self.region.to_string();
let service = self.service.to_string();
let aws4 = AWS4_REQUEST.as_bytes();
let date_key = try!(utils::signed_data(date.as_bytes(), key.as_bytes()));
let date_region_key = try!(utils::signed_data(region.as_bytes(), &date_key));
let date_region_service_key = try!(utils::signed_data(service.as_bytes(),
&date_region_key));
let signing_key = try!(utils::signed_data(aws4, &date_region_service_key));
let signature = try!(utils::signed_data(string_to_sign.as_bytes(), &signing_key));
debug!("Signature\n{}", signature.to_hex());
Ok(signature.to_hex())
}
fn v2(&self) -> AWSAuthResult {
let string_to_sign = format!("{}\n{}\n{}\n{}",
self.req_type,
self.host,
try!(self.canonical_uri()),
try!(self.canonical_query_string()));
debug!("V2: StringToSign\n{}", string_to_sign);
let key = &self.secret_access_key;
let signature = try!(utils::signed_data(string_to_sign.as_bytes(), key.as_bytes()));
let encoded_sig = try!(quote(signature.to_base64(STANDARD), b""));
debug!("V2: Signature\n{}", encoded_sig);
Ok(encoded_sig)
}
fn v4(&self) -> AWSAuthResult {
let hash = if self.seed {
"STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
} else {
&self.payload_hash[..]
};
let canonical_request = format!("{}\n{}\n{}\n{}\n{}\n{}",
self.req_type,
try!(self.canonical_uri()),
try!(self.canonical_query_string()),
self.canonical_headers(),
self.signed_headers(),
hash);
debug!("V4: CanonicalRequest\n{}", canonical_request);
let string_to_sign = format!("{}\n{}\n{}\n{}",
self.sam,
self.date.format(DATE_TIME_FMT),
self.scope(),
try!(utils::hashed_data(Some(canonical_request.as_bytes()))));
debug!("V4: StringToSign\n{}", string_to_sign);
self.sign_string(&string_to_sign)
}
pub fn signature(&self) -> AWSAuthResult {
match self.version {
SigningVersion::Two => self.v2(),
SigningVersion::Four => self.v4(),
}
}
pub fn auth_header(&self) -> AWSAuthResult {
init();
let signature = match self.mode {
Mode::Normal => try!(self.signature()),
Mode::Chunked => try!(self.seed_signature()),
};
Ok(format!("{} Credential={},SignedHeaders={},Signature={}",
self.sam,
self.credential(),
self.signed_headers(),
signature))
}
pub fn query_string(&self) -> AWSAuthResult {
init();
match (&self.version, &self.mode) {
(&SigningVersion::Four, &Mode::Normal) => {
let fmtdate = self.date.format(DATE_TIME_FMT).to_string();
Ok(format!("X-Amz-Algorithm={}\
&X-Amz-Credential={}\
&X-Amz-Date={}\
&X-Amz-SignedHeaders={}\
&X-Amz-Signature={}",
self.sam,
try!(quote(self.credential(), b"")),
fmtdate,
try!(quote(self.signed_headers(), b"")),
try!(self.signature())))
}
_ => Err(AWSAuthError::ModeError),
}
}
pub fn seed_signature(&self) -> AWSAuthResult {
match (self.seed, &self.mode) {
(true, &Mode::Chunked) => Ok(try!(self.signature())),
_ => Err(AWSAuthError::ModeError),
}
}
pub fn chunk_signature(&self, previous_signature: &str, chunk: &[u8]) -> AWSAuthResult {
match self.mode {
Mode::Chunked => {
let hashed_chunk = if chunk.len() == 0 {
try!(utils::hashed_data(None))
} else {
try!(utils::hashed_data(Some(chunk)))
};
let string_to_sign = format!("{}\n{}\n{}\n{}\n{}\n{}",
self.sam,
self.date.format(DATE_TIME_FMT),
self.scope(),
previous_signature,
try!(utils::hashed_data(None)),
hashed_chunk);
debug!("StringToSign\n{}", string_to_sign);
Ok(try!(self.sign_string(&string_to_sign)))
}
_ => Err(AWSAuthError::ModeError),
}
}
pub fn chunk_body(&self, chunk_signature: &str, chunk: &[u8]) -> Result<Vec<u8>, AWSAuthError> {
match self.mode {
Mode::Chunked => {
let hex = format!("{:x}", chunk.len());
let capacity = hex.len() + 85 + chunk.len();
let mut buf = Vec::with_capacity(capacity);
buf.extend(hex.as_bytes());
buf.extend_from_slice(b";chunk-signature=");
buf.extend(chunk_signature.as_bytes());
buf.extend_from_slice(b"\r\n");
buf.extend_from_slice(chunk);
buf.extend_from_slice(b"\r\n");
Ok(buf)
}
_ => Err(AWSAuthError::ModeError),
}
}
pub fn content_length(&self, payload_size: usize) -> Result<usize, AWSAuthError> {
let mut remaining = payload_size;
let mut length = 0;
loop {
if remaining < self.chunk_size {
length += remaining as usize;
length += format!("{:x}", remaining).len();
} else {
length += self.chunk_size as usize;
length += format!("{:x}", self.chunk_size).len();
}
length += 85;
if remaining == 0 {
break;
}
remaining = match remaining.checked_sub(self.chunk_size) {
Some(r) => r,
None => 0,
};
}
Ok(length)
}
}
pub enum SAM {
AWS4HMACSHA256,
AWS4HMACSHA256PAYLOAD,
}
impl Default for SAM {
fn default() -> SAM {
SAM::AWS4HMACSHA256
}
}
impl<'a> Into<String> for &'a SAM {
fn into(self) -> String {
match *self {
SAM::AWS4HMACSHA256 => "AWS4-HMAC-SHA256".to_owned(),
SAM::AWS4HMACSHA256PAYLOAD => "AWS4-HMAC-SHA256-PAYLOAD".to_owned(),
}
}
}
impl fmt::Display for SAM {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let display: String = self.into();
write!(f, "{}", display)
}
}
#[derive(Clone,Copy,Debug)]
pub enum HttpRequestMethod {
GET,
HEAD,
POST,
PUT,
DELETE,
CONNECT,
OPTIONS,
TRACE,
}
#[cfg_attr(feature = "clippy", allow(use_debug))]
impl fmt::Display for HttpRequestMethod {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}