use base64::Engine as _;
use base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
use hmac::Mac;
use hmac::digest::KeyInit;
const B64_STD_LENIENT: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
const B64_URL_LENIENT: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::URL_SAFE,
GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::Indifferent),
);
#[derive(Debug, Clone, Copy)]
pub enum Codec {
Base64,
Base64Url,
Hex,
}
impl Codec {
pub fn parse(name: &str) -> Option<Codec> {
match name {
"hex" => Some(Codec::Hex),
"base64" => Some(Codec::Base64),
"base64url" => Some(Codec::Base64Url),
_ => None,
}
}
}
pub fn encode_bytes(codec: Codec, bytes: &[u8]) -> String {
match codec {
Codec::Base64 => base64::engine::general_purpose::STANDARD.encode(bytes),
Codec::Base64Url => base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes),
Codec::Hex => hex::encode(bytes),
}
}
pub fn decode_bytes(codec: Codec, s: &str) -> Result<Vec<u8>, String> {
match codec {
Codec::Base64 => B64_STD_LENIENT.decode(s).map_err(|e| e.to_string()),
Codec::Base64Url => B64_URL_LENIENT.decode(s).map_err(|e| e.to_string()),
Codec::Hex => hex::decode(s).map_err(|e| e.to_string()),
}
}
pub fn mac_compute<M: Mac + KeyInit>(key: &[u8], data: &[u8]) -> Vec<u8> {
let mut mac = M::new_from_slice(key).expect("HMAC accepts any key length");
mac.update(data);
mac.finalize().into_bytes().to_vec()
}
pub fn mac_verify<M: Mac + KeyInit>(key: &[u8], data: &[u8], signature: &[u8]) -> bool {
let Ok(mut mac) = M::new_from_slice(key) else {
return false;
};
mac.update(data);
mac.verify_slice(signature).is_ok()
}
pub fn ensure_provider() {
if rustls::crypto::CryptoProvider::get_default().is_none() {
let _ = rustls::crypto::aws_lc_rs::default_provider().install_default();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_encoding_table_is_one_table() {
let bytes = b"\xde\xad\xbe\xef\xff";
assert_eq!(encode_bytes(Codec::Hex, bytes), "deadbeefff");
assert_eq!(encode_bytes(Codec::Base64, bytes), "3q2+7/8=");
assert_eq!(encode_bytes(Codec::Base64Url, bytes), "3q2-7_8");
for codec in [Codec::Hex, Codec::Base64, Codec::Base64Url] {
let encoded = encode_bytes(codec, bytes);
assert_eq!(decode_bytes(codec, &encoded).expect("round trip"), bytes);
}
}
#[test]
fn base64_decoding_is_indifferent_to_padding() {
assert_eq!(
decode_bytes(Codec::Base64, "3q2+7/8").expect("unpadded standard"),
b"\xde\xad\xbe\xef\xff"
);
assert_eq!(
decode_bytes(Codec::Base64Url, "3q2-7_8=").expect("padded url-safe"),
b"\xde\xad\xbe\xef\xff"
);
}
#[test]
fn an_unknown_codec_name_is_not_guessed_at() {
assert!(Codec::parse("base32").is_none());
assert!(Codec::parse("BASE64").is_none());
}
#[test]
fn mac_verify_rejects_a_wrong_length_signature() {
type H = hmac::Hmac<sha2::Sha256>;
let key = b"a-webhook-secret";
let data = b"payload";
let good = mac_compute::<H>(key, data);
assert!(mac_verify::<H>(key, data, &good));
assert!(!mac_verify::<H>(key, data, &good[..16]));
assert!(!mac_verify::<H>(key, data, &[]));
assert!(!mac_verify::<H>(b"wrong-secret", data, &good));
}
}