1use ct_codecs::{Base64UrlSafeNoPadding, Decoder, Encoder};
4use ece::encrypt;
5
6use crate::{error::WebPushError, message::WebPushPayload, vapid::VapidSignature};
7
8#[derive(Debug, PartialEq, Copy, Clone, Default)]
10pub enum ContentEncoding {
11 #[default]
13 Aes128Gcm,
14 AesGcm,
16}
17
18impl ContentEncoding {
19 pub fn to_str(&self) -> &'static str {
21 match &self {
22 ContentEncoding::Aes128Gcm => "aes128gcm",
23 ContentEncoding::AesGcm => "aesgcm",
24 }
25 }
26}
27
28pub struct HttpEce<'a> {
30 peer_public_key: &'a [u8],
31 peer_secret: &'a [u8],
32 encoding: ContentEncoding,
33 vapid_signature: Option<VapidSignature>,
34}
35
36impl<'a> HttpEce<'a> {
37 pub fn new(
42 encoding: ContentEncoding,
43 peer_public_key: &'a [u8],
44 peer_secret: &'a [u8],
45 vapid_signature: Option<VapidSignature>,
46 ) -> HttpEce<'a> {
47 HttpEce {
48 peer_public_key,
49 peer_secret,
50 encoding,
51 vapid_signature,
52 }
53 }
54
55 pub fn encrypt(&self, content: &'a [u8]) -> Result<WebPushPayload, WebPushError> {
59 if content.len() > 3052 {
60 return Err(WebPushError::PayloadTooLarge);
61 }
62
63 match self.encoding {
65 ContentEncoding::Aes128Gcm => {
66 let result = encrypt(self.peer_public_key, self.peer_secret, content);
67
68 let mut headers = Vec::new();
69
70 self.add_vapid_headers(&mut headers);
71
72 match result {
73 Ok(data) => Ok(WebPushPayload {
74 content: data,
75 crypto_headers: headers,
76 content_encoding: self.encoding,
77 }),
78 _ => Err(WebPushError::InvalidCryptoKeys),
79 }
80 }
81 ContentEncoding::AesGcm => {
82 let result = self.aesgcm_encrypt(content);
83
84 let data = result.map_err(|_| WebPushError::InvalidCryptoKeys)?;
85
86 let mut headers = data.headers(self.vapid_signature.as_ref().map(|v| v.auth_k.as_slice()));
88
89 self.add_vapid_headers(&mut headers);
90
91 let data = Base64UrlSafeNoPadding::decode_to_vec(data.body(), None)
93 .expect("ECE library should always base64 encode");
94
95 Ok(WebPushPayload {
96 content: data,
97 crypto_headers: headers,
98 content_encoding: self.encoding,
99 })
100 }
101 }
102 }
103
104 fn add_vapid_headers(&self, headers: &mut Vec<(&str, String)>) {
106 if let Some(signature) = &self.vapid_signature {
108 headers.push((
109 "Authorization",
110 format!(
111 "vapid t={}, k={}",
112 signature.auth_t,
113 Base64UrlSafeNoPadding::encode_to_string(&signature.auth_k)
114 .expect("encoding a valid auth_k cannot overflow")
115 ),
116 ));
117 }
118 }
119
120 fn aesgcm_encrypt(&self, content: &[u8]) -> ece::Result<ece::legacy::AesGcmEncryptedBlock> {
124 ece::legacy::encrypt_aesgcm(self.peer_public_key, self.peer_secret, content)
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use ct_codecs::{Base64UrlSafeNoPadding, Decoder};
131 use regex::Regex;
132
133 use crate::{
134 error::WebPushError,
135 http_ece::{ContentEncoding, HttpEce},
136 VapidSignature, WebPushPayload,
137 };
138
139 #[test]
140 fn test_payload_too_big() {
141 let p256dh = Base64UrlSafeNoPadding::decode_to_vec(
142 "BLMaF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
143 None,
144 )
145 .unwrap();
146 let auth = Base64UrlSafeNoPadding::decode_to_vec("xS03Fj5ErfTNH_l9WHE9Ig", None).unwrap();
147 let http_ece = HttpEce::new(ContentEncoding::Aes128Gcm, &p256dh, &auth, None);
148 let content = [0u8; 3801];
150
151 assert!(matches!(http_ece.encrypt(&content), Err(WebPushError::PayloadTooLarge)));
152 }
153
154 #[test]
156 fn test_payload_encrypts_128() {
157 let (key, auth) = ece::generate_keypair_and_auth_secret().unwrap();
158 let p_key = key.raw_components().unwrap();
159 let p_key = p_key.public_key();
160
161 let http_ece = HttpEce::new(ContentEncoding::Aes128Gcm, p_key, &auth, None);
162 let plaintext = "Hello world!";
163 let ciphertext = http_ece.encrypt(plaintext.as_bytes()).unwrap();
164
165 assert_ne!(plaintext.as_bytes(), ciphertext.content);
166
167 assert_eq!(
168 String::from_utf8(ece::decrypt(&key.raw_components().unwrap(), &auth, &ciphertext.content).unwrap())
169 .unwrap(),
170 plaintext
171 )
172 }
173
174 #[test]
176 fn test_payload_encrypts() {
177 let (key, auth) = ece::generate_keypair_and_auth_secret().unwrap();
178 let p_key = key.raw_components().unwrap();
179 let p_key = p_key.public_key();
180
181 let http_ece = HttpEce::new(ContentEncoding::AesGcm, p_key, &auth, None);
182 let plaintext = "Hello world!";
183 let ciphertext = http_ece.aesgcm_encrypt(plaintext.as_bytes()).unwrap();
184
185 assert_ne!(plaintext, ciphertext.body());
186
187 assert_eq!(
188 String::from_utf8(ece::legacy::decrypt_aesgcm(&key.raw_components().unwrap(), &auth, &ciphertext).unwrap())
189 .unwrap(),
190 plaintext
191 )
192 }
193
194 fn setup_payload(vapid_signature: Option<VapidSignature>, encoding: ContentEncoding) -> WebPushPayload {
195 let p256dh = Base64UrlSafeNoPadding::decode_to_vec(
196 "BLMbF9ffKBiWQLCKvTHb6LO8Nb6dcUh6TItC455vu2kElga6PQvUmaFyCdykxY2nOSSL3yKgfbmFLRTUaGv4yV8",
197 None,
198 )
199 .unwrap();
200 let auth = Base64UrlSafeNoPadding::decode_to_vec("xS03Fi5ErfTNH_l9WHE9Ig", None).unwrap();
201
202 let http_ece = HttpEce::new(encoding, &p256dh, &auth, vapid_signature);
203 let content = "Hello, world!".as_bytes();
204
205 http_ece.encrypt(content).unwrap()
206 }
207
208 #[test]
209 fn test_aes128gcm_headers_no_vapid() {
210 let wp_payload = setup_payload(None, ContentEncoding::Aes128Gcm);
211 assert_eq!(wp_payload.crypto_headers.len(), 0);
212 }
213
214 #[test]
215 fn test_aesgcm_headers_no_vapid() {
216 let wp_payload = setup_payload(None, ContentEncoding::AesGcm);
217 assert_eq!(wp_payload.crypto_headers.len(), 2);
218 }
219
220 #[test]
221 fn test_aes128gcm_headers_vapid() {
222 let auth_re = Regex::new(r"vapid t=(?P<sig_t>[^,]*), k=(?P<sig_k>[^,]*)").unwrap();
223 let vapid_signature = VapidSignature {
224 auth_t: String::from("foo"),
225 auth_k: String::from("bar").into_bytes(),
226 };
227 let wp_payload = setup_payload(Some(vapid_signature), ContentEncoding::Aes128Gcm);
228 assert_eq!(wp_payload.crypto_headers.len(), 1);
229 let auth = wp_payload.crypto_headers[0].clone();
230 assert_eq!(auth.0, "Authorization");
231 assert!(auth_re.captures(&auth.1).is_some());
232 }
233
234 #[test]
235 fn test_aesgcm_headers_vapid() {
236 let auth_re = Regex::new(r"vapid t=(?P<sig_t>[^,]*), k=(?P<sig_k>[^,]*)").unwrap();
237 let vapid_signature = VapidSignature {
238 auth_t: String::from("foo"),
239 auth_k: String::from("bar").into_bytes(),
240 };
241 let wp_payload = setup_payload(Some(vapid_signature), ContentEncoding::AesGcm);
242 assert_eq!(wp_payload.crypto_headers.len(), 3);
244 let auth = wp_payload.crypto_headers[2].clone();
245 assert_eq!(auth.0, "Authorization");
246 assert!(auth_re.captures(&auth.1).is_some());
247 }
248}