use std::{
collections::BTreeMap,
marker::PhantomData,
time::{SystemTime, SystemTimeError},
};
use base64::{Engine, engine::general_purpose::STANDARD as BASE64_ENGINE};
use crypto_common::InvalidLength;
use hmac::{Hmac, Mac};
use reqwest::{Method, Url};
use sha2::Sha512;
use uuid::Uuid;
pub trait SignatureMethod {
type Error;
const SIGNATURE_METHOD: &'static str;
fn digest(key: &str, signature: &str) -> Result<String, Self::Error>;
}
pub type HmacSha512 = Hmac<Sha512>;
impl SignatureMethod for HmacSha512 {
type Error = InvalidLength;
const SIGNATURE_METHOD: &'static str = "HMAC-SHA512";
#[inline]
fn digest(key: &str, signature: &str) -> Result<String, Self::Error> {
let mut hasher = Hmac::<Sha512>::new_from_slice(key.as_bytes())?;
hasher.update(signature.as_bytes());
Ok(BASE64_ENGINE.encode(hasher.finalize().into_bytes()))
}
}
#[derive(Debug, thiserror::Error)]
pub enum SignError<T: SignatureMethod = HmacSha512> {
#[error("failed to compute time since Unix Epoch, {0}")]
Clock(#[from] SystemTimeError),
#[error("failed to generate signature for {method}, {0}", method = T::SIGNATURE_METHOD)]
Digest(T::Error),
}
pub const OAUTH1_CONSUMER_KEY: &str = "oauth_consumer_key";
pub const OAUTH1_NONCE: &str = "oauth_nonce";
pub const OAUTH1_SIGNATURE: &str = "oauth_signature";
pub const OAUTH1_SIGNATURE_METHOD: &str = "oauth_signature_method";
pub const OAUTH1_TIMESTAMP: &str = "oauth_timestamp";
pub const OAUTH1_VERSION: &str = "oauth_version";
pub const OAUTH1_VERSION_1: &str = "1.0";
pub const OAUTH1_TOKEN: &str = "oauth_token";
#[derive(Debug)]
pub struct Signer<'a, T> {
nonce: String,
timestamp: String,
token: &'a str,
secret: &'a str,
consumer_key: &'a str,
consumer_secret: &'a str,
_marker: PhantomData<T>,
}
impl<'a, T: SignatureMethod> Signer<'a, T> {
pub fn new(
token: &'a str,
secret: &'a str,
consumer_key: &'a str,
consumer_secret: &'a str,
) -> Result<Self, SignError<T>> {
let nonce = Uuid::new_v4().to_string();
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)?
.as_secs()
.to_string();
Ok(Self {
nonce,
timestamp,
token,
secret,
consumer_key,
consumer_secret,
_marker: PhantomData,
})
}
fn params(&self) -> BTreeMap<&str, &str> {
let mut params = BTreeMap::new();
let _ = params.insert(OAUTH1_CONSUMER_KEY, self.consumer_key);
let _ = params.insert(OAUTH1_NONCE, &self.nonce);
let _ = params.insert(OAUTH1_SIGNATURE_METHOD, T::SIGNATURE_METHOD);
let _ = params.insert(OAUTH1_TIMESTAMP, &self.timestamp);
let _ = params.insert(OAUTH1_TOKEN, self.token);
let _ = params.insert(OAUTH1_VERSION, OAUTH1_VERSION_1);
params
}
fn signature(&self, method: &Method, endpoint: &Url) -> String {
let mut params = self.params();
let pairs = endpoint.query_pairs().collect::<Vec<_>>();
params.extend(pairs.iter().map(|(k, v)| (k.as_ref(), v.as_ref())));
let mut params = params
.into_iter()
.map(|(k, v)| format!("{k}={}", urlencoding::encode(v)))
.collect::<Vec<_>>();
params.sort();
let base_url = format!(
"{}{}",
endpoint.origin().unicode_serialization(),
endpoint.path()
);
format!(
"{}&{}&{}",
urlencoding::encode(method.as_str()),
urlencoding::encode(&base_url),
urlencoding::encode(¶ms.join("&"))
)
}
fn signing_key(&self) -> String {
format!(
"{}&{}",
urlencoding::encode(self.consumer_secret),
urlencoding::encode(self.secret)
)
}
pub fn sign(self, method: &Method, endpoint: &Url) -> Result<String, SignError<T>> {
let signing_key = self.signing_key();
let signature = self.signature(method, endpoint);
let signature = T::digest(&signing_key, &signature).map_err(SignError::Digest)?;
let signature = urlencoding::encode(&signature);
let mut params = self.params();
let _ = params.insert(OAUTH1_SIGNATURE, &signature);
let mut params = params
.into_iter()
.map(|(k, v)| format!("{k}=\"{v}\""))
.collect::<Vec<_>>();
params.sort();
Ok(format!("OAuth {}", params.join(", ")))
}
}