1#[cfg(feature = "serialization")]
54mod serde_;
55#[cfg(test)]
56mod tests;
57#[cfg(feature = "vapid")]
58pub mod vapid;
59
60use std::time::Duration;
61
62use aes_gcm::aead::{
63 generic_array::{typenum::U16, GenericArray},
64 rand_core::RngCore,
65 OsRng,
66};
67use hkdf::Hkdf;
68use http::{self, header, Request, Uri};
69#[cfg(feature = "vapid")]
70pub use jwt_simple;
71pub use p256;
72use p256::elliptic_curve::sec1::ToEncodedPoint;
73use sha2::Sha256;
74
75#[derive(Debug)]
77pub enum Error {
78 ECE(ece_native::Error),
80 Extension(Box<dyn std::error::Error + Send + Sync + 'static>),
82}
83
84impl std::error::Error for Error {}
85
86impl std::fmt::Display for Error {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 Error::ECE(ece) => write!(f, "ECE: {}", ece),
90 Error::Extension(ext) => write!(f, "Extension: {}", ext),
91 }
92 }
93}
94
95pub type Auth = GenericArray<u8, U16>;
97
98#[derive(Clone, Debug)]
100pub struct WebPushBuilder<A = ()> {
101 endpoint: Uri,
102 valid_duration: Duration,
103 ua_public: p256::PublicKey,
104 ua_auth: Auth,
105 #[cfg_attr(not(feature = "vapid"), allow(dead_code))]
106 http_auth: A,
107}
108
109impl WebPushBuilder {
110 pub fn new(endpoint: Uri, ua_public: p256::PublicKey, ua_auth: Auth) -> Self {
119 Self {
120 endpoint,
121 ua_public,
122 ua_auth,
123 valid_duration: Duration::from_secs(12 * 60 * 60),
124 http_auth: (),
125 }
126 }
127
128 pub fn with_valid_duration(self, valid_duration: Duration) -> Self {
130 Self {
131 valid_duration,
132 ..self
133 }
134 }
135
136 #[cfg(feature = "vapid")]
138 pub fn with_vapid<'a>(
139 self,
140 vapid_kp: &'a jwt_simple::algorithms::ES256KeyPair,
141 contact: &'a str,
142 ) -> WebPushBuilder<vapid::VapidAuthorization<'a>> {
143 WebPushBuilder {
144 endpoint: self.endpoint,
145 valid_duration: self.valid_duration,
146 ua_public: self.ua_public,
147 ua_auth: self.ua_auth,
148 http_auth: vapid::VapidAuthorization::new(vapid_kp, contact),
149 }
150 }
151}
152
153trait AddHeaders: Sized {
154 type Error: Into<Box<dyn std::error::Error + Sync + Send + 'static>>;
155
156 fn add_headers(
157 this: &WebPushBuilder<Self>,
158 builder: http::request::Builder,
159 ) -> Result<http::request::Builder, Self::Error>;
160}
161
162impl AddHeaders for () {
163 type Error = std::convert::Infallible;
164
165 fn add_headers(
166 _this: &WebPushBuilder<Self>,
167 builder: http::request::Builder,
168 ) -> Result<http::request::Builder, Self::Error> {
169 Ok(builder)
170 }
171}
172
173#[expect(private_bounds)]
174impl<A: AddHeaders> WebPushBuilder<A> {
175 pub fn build<T: Into<Vec<u8>>>(&self, body: T) -> Result<Request<Vec<u8>>, Error> {
178 let body = body.into();
179
180 let payload = encrypt(body, &self.ua_public, &self.ua_auth)?;
181 let builder = Request::builder()
182 .uri(self.endpoint.clone())
183 .method(http::method::Method::POST)
184 .header("TTL", self.valid_duration.as_secs())
185 .header(header::CONTENT_ENCODING, "aes128gcm")
186 .header(header::CONTENT_TYPE, "application/octet-stream")
187 .header(header::CONTENT_LENGTH, payload.len());
188
189 let builder =
190 AddHeaders::add_headers(self, builder).map_err(|it| Error::Extension(it.into()))?;
191
192 Ok(builder
193 .body(payload)
194 .expect("builder arguments are always well-defined"))
195 }
196}
197
198pub fn encrypt(
200 message: Vec<u8>,
201 ua_public: &p256::PublicKey,
202 ua_auth: &Auth,
203) -> Result<Vec<u8>, Error> {
204 let mut salt = [0u8; 16];
205 OsRng.fill_bytes(&mut salt);
206 let as_secret = p256::SecretKey::random(&mut OsRng);
207 encrypt_predictably(salt, message, &as_secret, ua_public, ua_auth).map_err(Error::ECE)
208}
209
210fn encrypt_predictably(
211 salt: [u8; 16],
212 message: Vec<u8>,
213 as_secret: &p256::SecretKey,
214 ua_public: &p256::PublicKey,
215 ua_auth: &Auth,
216) -> Result<Vec<u8>, ece_native::Error> {
217 let as_public = as_secret.public_key();
218 let shared = p256::ecdh::diffie_hellman(as_secret.to_nonzero_scalar(), ua_public.as_affine());
219
220 let ikm = compute_ikm(ua_auth, &shared, ua_public, &as_public);
221 let keyid = as_public.as_affine().to_encoded_point(false);
222 let encrypted_record_length = (message.len() + 17)
223 .try_into()
224 .map_err(|_| ece_native::Error::RecordLengthInvalid)?;
225
226 ece_native::encrypt(
227 ikm,
228 salt,
229 keyid,
230 Some(message).into_iter(),
231 encrypted_record_length,
232 )
233}
234
235pub fn decrypt(
237 encrypted_message: Vec<u8>,
238 as_secret: &p256::SecretKey,
239 ua_auth: &Auth,
240) -> Result<Vec<u8>, Error> {
241 let keyid = view_keyid(&encrypted_message).map_err(Error::ECE)?;
242 let ua_public = p256::PublicKey::from_sec1_bytes(keyid)
243 .map_err(|_| ece_native::Error::Aes128Gcm)
244 .map_err(Error::ECE)?;
245 let shared = p256::ecdh::diffie_hellman(as_secret.to_nonzero_scalar(), ua_public.as_affine());
246
247 let ikm = compute_ikm(ua_auth, &shared, &as_secret.public_key(), &ua_public);
248
249 ece_native::decrypt(ikm, encrypted_message).map_err(Error::ECE)
250}
251
252fn compute_ikm(
253 auth: &Auth,
254 shared: &p256::ecdh::SharedSecret,
255 ua_public: &p256::PublicKey,
256 as_public: &p256::PublicKey,
257) -> [u8; 32] {
258 let mut info = Vec::new();
259 info.extend_from_slice(&b"WebPush: info"[..]);
260 info.push(0u8);
261 info.extend_from_slice(ua_public.as_affine().to_encoded_point(false).as_bytes());
262 info.extend_from_slice(as_public.as_affine().to_encoded_point(false).as_bytes());
263
264 let mut okm = [0u8; 32];
265 let hk = Hkdf::<Sha256>::new(Some(auth), shared.raw_secret_bytes().as_ref());
266 hk.expand(&info, &mut okm)
267 .expect("okm length is always 32 bytes, cannot be too large");
268
269 okm
270}
271
272fn view_keyid(encrypted_message: &[u8]) -> Result<&[u8], ece_native::Error> {
273 if encrypted_message.len() < 21 {
274 return Err(ece_native::Error::HeaderLengthInvalid);
275 }
276
277 let idlen: usize = encrypted_message[20].into();
278 if encrypted_message[21..].len() < idlen {
279 return Err(ece_native::Error::KeyIdLengthInvalid);
280 }
281
282 Ok(&encrypted_message[21..21 + idlen])
283}