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