use ece::encrypt;
use crate::error::WebPushError;
use crate::message::WebPushPayload;
use crate::vapid::VapidSignature;
#[derive(Debug, PartialEq, Copy, Clone, Default)]
pub enum ContentEncoding {
#[default]
Aes128Gcm,
AesGcm,
}
impl ContentEncoding {
pub fn to_str(&self) -> &'static str {
match &self {
ContentEncoding::Aes128Gcm => "aes128gcm",
ContentEncoding::AesGcm => "aesgcm",
}
}
}
pub struct HttpEce<'a> {
peer_public_key: &'a [u8],
peer_secret: &'a [u8],
encoding: ContentEncoding,
vapid_signature: Option<VapidSignature>,
}
impl<'a> HttpEce<'a> {
pub fn new(
encoding: ContentEncoding,
peer_public_key: &'a [u8],
peer_secret: &'a [u8],
vapid_signature: Option<VapidSignature>,
) -> HttpEce<'a> {
HttpEce {
peer_public_key,
peer_secret,
encoding,
vapid_signature,
}
}
pub fn encrypt(&self, content: &'a [u8]) -> Result<WebPushPayload, WebPushError> {
if content.len() > 3052 {
return Err(WebPushError::PayloadTooLarge);
}
match self.encoding {
ContentEncoding::Aes128Gcm => {
let result = encrypt(self.peer_public_key, self.peer_secret, content);
let mut headers = Vec::new();
self.add_vapid_headers(&mut headers);
match result {
Ok(data) => Ok(WebPushPayload {
content: data,
crypto_headers: headers,
content_encoding: self.encoding,
}),
_ => Err(WebPushError::InvalidCryptoKeys),
}
}
ContentEncoding::AesGcm => {
let result = self.aesgcm_encrypt(content);
let data = result.map_err(|_| WebPushError::InvalidCryptoKeys)?;
let mut headers = data.headers(self.vapid_signature.as_ref().map(|v| v.auth_k.as_slice()));
self.add_vapid_headers(&mut headers);
let data = base64::decode_config(data.body(), base64::URL_SAFE_NO_PAD)
.expect("ECE library should always base64 encode");
Ok(WebPushPayload {
content: data,
crypto_headers: headers,
content_encoding: self.encoding,
})
}
}
}
fn add_vapid_headers(&self, headers: &mut Vec<(&str, String)>) {
if let Some(signature) = &self.vapid_signature {
headers.push((
"Authorization",
format!(
"vapid t={}, k={}",
signature.auth_t,
base64::encode_config(&signature.auth_k, base64::URL_SAFE_NO_PAD)
),
));
}
}
fn aesgcm_encrypt(&self, content: &[u8]) -> ece::Result<ece::legacy::AesGcmEncryptedBlock> {
ece::legacy::encrypt_aesgcm(self.peer_public_key, self.peer_secret, content)
}
}
#[cfg(test)]
mod tests {
use base64::{self, URL_SAFE};
use regex::Regex;
use crate::error::WebPushError;
use crate::http_ece::{ContentEncoding, HttpEce};
use crate::VapidSignature;
use crate::WebPushPayload;
#[test]
fn test_payload_too_big() {
let p256dh = base64::decode_config(
"BLMaF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
URL_SAFE,
)
.unwrap();
let auth = base64::decode_config("xS03Fj5ErfTNH_l9WHE9Ig", URL_SAFE).unwrap();
let http_ece = HttpEce::new(ContentEncoding::Aes128Gcm, &p256dh, &auth, None);
let content = [0u8; 3801];
assert_eq!(Err(WebPushError::PayloadTooLarge), http_ece.encrypt(&content));
}
#[test]
fn test_payload_encrypts_128() {
let (key, auth) = ece::generate_keypair_and_auth_secret().unwrap();
let p_key = key.raw_components().unwrap();
let p_key = p_key.public_key();
let http_ece = HttpEce::new(ContentEncoding::Aes128Gcm, p_key, &auth, None);
let plaintext = "Hello world!";
let ciphertext = http_ece.encrypt(plaintext.as_bytes()).unwrap();
assert_ne!(plaintext.as_bytes(), ciphertext.content);
assert_eq!(
String::from_utf8(ece::decrypt(&key.raw_components().unwrap(), &auth, &ciphertext.content).unwrap())
.unwrap(),
plaintext
)
}
#[test]
fn test_payload_encrypts() {
let (key, auth) = ece::generate_keypair_and_auth_secret().unwrap();
let p_key = key.raw_components().unwrap();
let p_key = p_key.public_key();
let http_ece = HttpEce::new(ContentEncoding::AesGcm, p_key, &auth, None);
let plaintext = "Hello world!";
let ciphertext = http_ece.aesgcm_encrypt(plaintext.as_bytes()).unwrap();
assert_ne!(plaintext, ciphertext.body());
assert_eq!(
String::from_utf8(ece::legacy::decrypt_aesgcm(&key.raw_components().unwrap(), &auth, &ciphertext).unwrap())
.unwrap(),
plaintext
)
}
fn setup_payload(vapid_signature: Option<VapidSignature>, encoding: ContentEncoding) -> WebPushPayload {
let p256dh = base64::decode_config(
"BLMbF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
URL_SAFE,
)
.unwrap();
let auth = base64::decode_config("xS03Fi5ErfTNH_l9WHE9Ig", URL_SAFE).unwrap();
let http_ece = HttpEce::new(encoding, &p256dh, &auth, vapid_signature);
let content = "Hello, world!".as_bytes();
http_ece.encrypt(content).unwrap()
}
#[test]
fn test_aes128gcm_headers_no_vapid() {
let wp_payload = setup_payload(None, ContentEncoding::Aes128Gcm);
assert_eq!(wp_payload.crypto_headers.len(), 0);
}
#[test]
fn test_aesgcm_headers_no_vapid() {
let wp_payload = setup_payload(None, ContentEncoding::AesGcm);
assert_eq!(wp_payload.crypto_headers.len(), 2);
}
#[test]
fn test_aes128gcm_headers_vapid() {
let auth_re = Regex::new(r"vapid t=(?P<sig_t>[^,]*), k=(?P<sig_k>[^,]*)").unwrap();
let vapid_signature = VapidSignature {
auth_t: String::from("foo"),
auth_k: String::from("bar").into_bytes(),
};
let wp_payload = setup_payload(Some(vapid_signature), ContentEncoding::Aes128Gcm);
assert_eq!(wp_payload.crypto_headers.len(), 1);
let auth = wp_payload.crypto_headers[0].clone();
assert_eq!(auth.0, "Authorization");
assert!(auth_re.captures(&auth.1).is_some());
}
#[test]
fn test_aesgcm_headers_vapid() {
let auth_re = Regex::new(r"vapid t=(?P<sig_t>[^,]*), k=(?P<sig_k>[^,]*)").unwrap();
let vapid_signature = VapidSignature {
auth_t: String::from("foo"),
auth_k: String::from("bar").into_bytes(),
};
let wp_payload = setup_payload(Some(vapid_signature), ContentEncoding::AesGcm);
assert_eq!(wp_payload.crypto_headers.len(), 3);
let auth = wp_payload.crypto_headers[2].clone();
assert_eq!(auth.0, "Authorization");
assert!(auth_re.captures(&auth.1).is_some());
}
}