1use aes_gcm::{Aes128Gcm, KeyInit as _, aead::Aead as _};
22use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
23use hkdf::Hkdf;
24use p256::{
25 PublicKey, SecretKey,
26 ecdsa::{Signature, SigningKey, signature::Signer as _},
27 elliptic_curve::sec1::ToEncodedPoint as _,
28};
29use serde::{Deserialize, Serialize};
30use sha2::Sha256;
31
32pub use crate::runtime::push::{Sent, send};
33use crate::{Error, Result, token::random_bytes};
34
35pub const VAPID_PUBLIC_KEY: &str = "VAPID_PUBLIC_KEY";
37pub const VAPID_PRIVATE_KEY: &str = "VAPID_PRIVATE_KEY";
39pub const VAPID_SUBJECT: &str = "VAPID_SUBJECT";
41
42pub const MAX_PAYLOAD: usize = 4096 - 16 - 1;
44
45#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
55pub struct Subscription {
56 pub endpoint: String,
58 pub keys: SubscriptionKeys,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
64pub struct SubscriptionKeys {
65 pub p256dh: String,
67 pub auth: String,
69}
70
71#[derive(Clone, PartialEq, Eq)]
73pub struct VapidKeys {
74 pub public_key: String,
76 pub private_key: String,
78}
79
80impl std::fmt::Debug for VapidKeys {
81 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82 f.debug_struct("VapidKeys").field("public_key", &self.public_key).field("private_key", &"[redacted]").finish()
83 }
84}
85
86impl VapidKeys {
87 pub fn generate() -> Self {
98 let secret = random_secret();
99 Self {
100 public_key: URL_SAFE_NO_PAD.encode(secret.public_key().to_encoded_point(false).as_bytes()),
101 private_key: URL_SAFE_NO_PAD.encode(secret.to_bytes()),
102 }
103 }
104}
105
106fn random_secret() -> SecretKey {
108 std::iter::repeat_with(|| SecretKey::from_slice(&random_bytes::<32>())).find_map(Result::ok).expect("random bytes")
109}
110
111pub fn message(title: &str, body: &str, path: &str) -> serde_json::Value {
121 serde_json::json!({ "title": title, "options": { "body": body, "data": { "path": path } } })
122}
123
124pub fn encrypt(keys: &SubscriptionKeys, payload: &[u8]) -> Result<Vec<u8>> {
131 encrypt_with(keys, payload, &random_secret(), random_bytes::<16>())
132}
133
134pub(crate) fn encrypt_with(
136 keys: &SubscriptionKeys,
137 payload: &[u8],
138 sender: &SecretKey,
139 salt: [u8; 16],
140) -> Result<Vec<u8>> {
141 if payload.len() > MAX_PAYLOAD {
142 return Err(Error::bad_request(format!("a push message is at most {MAX_PAYLOAD} bytes")));
143 }
144 let invalid = || Error::bad_request("the push subscription's keys are invalid");
145 let browser_bytes = URL_SAFE_NO_PAD.decode(keys.p256dh.trim_end_matches('=')).map_err(|_| invalid())?;
146 let auth = URL_SAFE_NO_PAD.decode(keys.auth.trim_end_matches('=')).map_err(|_| invalid())?;
147 let browser = PublicKey::from_sec1_bytes(&browser_bytes).map_err(|_| invalid())?;
148 let shared = p256::ecdh::diffie_hellman(sender.to_nonzero_scalar(), browser.as_affine());
149 let sender_public = sender.public_key().to_encoded_point(false);
150 let sender_bytes = sender_public.as_bytes();
151 let key_info = [b"WebPush: info\0".as_slice(), &browser_bytes, sender_bytes].concat();
153 let mut ikm = [0; 32];
154 Hkdf::<Sha256>::new(Some(&auth), shared.raw_secret_bytes())
155 .expand(&key_info, &mut ikm)
156 .expect("32 bytes is a valid HKDF-SHA256 length");
157 let content = Hkdf::<Sha256>::new(Some(&salt), &ikm);
158 let (mut key, mut nonce) = ([0; 16], [0; 12]);
159 content.expand(b"Content-Encoding: aes128gcm\0", &mut key).expect("valid length");
160 content.expand(b"Content-Encoding: nonce\0", &mut nonce).expect("valid length");
161 let plaintext = [payload, &[2]].concat();
163 let ciphertext = Aes128Gcm::new(&key.into())
164 .encrypt(&nonce.into(), plaintext.as_slice())
165 .expect("AES-GCM encrypts any message this short");
166 let record_size: u32 = 4096;
167 let mut body = Vec::with_capacity(16 + 4 + 1 + sender_bytes.len() + ciphertext.len());
168 body.extend_from_slice(&salt);
169 body.extend_from_slice(&record_size.to_be_bytes());
170 body.push(u8::try_from(sender_bytes.len()).expect("65 bytes"));
171 body.extend_from_slice(sender_bytes);
172 body.extend_from_slice(&ciphertext);
173 Ok(body)
174}
175
176pub fn vapid_authorization(endpoint: &str, subject: &str, keys: &VapidKeys, now: i64) -> Result<String> {
193 let invalid = || Error::internal("VAPID_PRIVATE_KEY is not a P-256 private key in URL-safe base64");
194 let private = URL_SAFE_NO_PAD.decode(keys.private_key.trim().trim_end_matches('=')).map_err(|_| invalid())?;
195 let signing = SigningKey::from_slice(&private).map_err(|_| invalid())?;
196 let audience = ["https://", "http://"]
198 .iter()
199 .find_map(|scheme| {
200 endpoint.strip_prefix(scheme).map(|rest| format!("{scheme}{}", rest.split('/').next().unwrap_or_default()))
201 })
202 .ok_or_else(|| Error::internal(format!("a push endpoint is an https:// URL, not `{endpoint}`")))?;
203 let header = URL_SAFE_NO_PAD.encode(br#"{"typ":"JWT","alg":"ES256"}"#);
204 let claims = serde_json::json!({ "aud": audience, "exp": now + 12 * 3600, "sub": subject });
205 let unsigned = format!("{header}.{}", URL_SAFE_NO_PAD.encode(claims.to_string()));
206 let signature: Signature = signing.sign(unsigned.as_bytes());
207 Ok(format!("vapid t={unsigned}.{}, k={}", URL_SAFE_NO_PAD.encode(signature.to_bytes()), keys.public_key.trim()))
208}
209
210#[cfg(test)]
211#[path = "../tests/push.rs"]
212mod tests;