use std::collections::BTreeMap;
use hmac::{Hmac, Mac};
use sha2::Sha256;
use url::form_urlencoded;
use crate::{UniError, DEFAULT_SIGNING_ALGORITHM};
pub(crate) fn sign(
query: &mut BTreeMap<String, String>,
secret: &str,
timestamp: u64,
nonce: &str,
) -> crate::Result<()> {
query.insert("algorithm".to_owned(), DEFAULT_SIGNING_ALGORITHM.to_owned());
query.insert("timestamp".to_owned(), timestamp.to_string());
query.insert("nonce".to_owned(), nonce.to_owned());
let canonical = canonical_query(query);
let mut mac = Hmac::<Sha256>::new_from_slice(secret.as_bytes())
.map_err(|_| UniError::Configuration("invalid signing key".to_owned()))?;
mac.update(canonical.as_bytes());
query.insert(
"signature".to_owned(),
hex::encode(mac.finalize().into_bytes()),
);
Ok(())
}
pub(crate) fn canonical_query(query: &BTreeMap<String, String>) -> String {
let mut serializer = form_urlencoded::Serializer::new(String::new());
for (key, value) in query {
serializer.append_pair(key, value);
}
serializer.finish()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signature_is_stable_and_url_encoded() {
let mut query = BTreeMap::from([
("accessKeyId".to_owned(), "access key/+".to_owned()),
("action".to_owned(), "sms.message.send".to_owned()),
]);
sign(&mut query, "secret", 1_650_000_000, "0123456789abcdef").unwrap();
assert_eq!(
canonical_query(&BTreeMap::from([
("accessKeyId".to_owned(), "access key/+".to_owned()),
("action".to_owned(), "sms.message.send".to_owned()),
("algorithm".to_owned(), "hmac-sha256".to_owned()),
("nonce".to_owned(), "0123456789abcdef".to_owned()),
("timestamp".to_owned(), "1650000000".to_owned()),
])),
"accessKeyId=access+key%2F%2B&action=sms.message.send&algorithm=hmac-sha256&nonce=0123456789abcdef×tamp=1650000000"
);
assert_eq!(
query["signature"],
"25213ad680913a8dc2d21c54fe08a4cb6fc1fe9f256d956766975d6d6cdb1943"
);
}
}