this repo has no description
at main 139 lines 4.4 kB view raw
1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; 2use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; 3use serde::Serialize; 4 5use crate::errors::AppError; 6 7#[derive(Debug, Serialize)] 8struct VapidClaims { 9 aud: String, 10 exp: usize, 11 sub: String, 12} 13 14/// Send a push notification to a subscriber. 15pub async fn send_push( 16 private_key_pem: &str, 17 public_key_b64url: &str, 18 endpoint: &str, 19 p256dh_b64url: &str, 20 auth_b64url: &str, 21 payload: &[u8], 22) -> Result<(), AppError> { 23 // Decode subscriber keys 24 let p256dh = URL_SAFE_NO_PAD 25 .decode(p256dh_b64url) 26 .map_err(|e| AppError::Internal(format!("Invalid p256dh: {e}")))?; 27 let auth = URL_SAFE_NO_PAD 28 .decode(auth_b64url) 29 .map_err(|e| AppError::Internal(format!("Invalid auth: {e}")))?; 30 31 // Encrypt payload using RFC 8291 (aes128gcm) 32 let encrypted = ece::encrypt(&p256dh, &auth, payload) 33 .map_err(|e| AppError::Internal(format!("Encryption failed: {e}")))?; 34 35 // Build VAPID JWT 36 let origin = get_origin(endpoint); 37 let exp = (chrono::Utc::now().timestamp() + 86400) as usize; 38 let claims = VapidClaims { 39 aud: origin, 40 exp, 41 sub: "mailto:admin@ayos.app".to_string(), 42 }; 43 let header = Header::new(Algorithm::ES256); 44 let key = EncodingKey::from_ec_pem(private_key_pem.as_bytes()) 45 .map_err(|e| AppError::Internal(format!("Invalid VAPID key: {e}")))?; 46 let jwt = encode(&header, &claims, &key) 47 .map_err(|e| AppError::Internal(format!("JWT signing failed: {e}")))?; 48 49 // Send HTTP request to push service 50 let client = reqwest::Client::new(); 51 let response = client 52 .post(endpoint) 53 .header("TTL", "86400") 54 .header("Content-Encoding", "aes128gcm") 55 .header( 56 "Authorization", 57 format!("vapid t={jwt}, k={public_key_b64url}"), 58 ) 59 .header("Content-Type", "application/octet-stream") 60 .body(encrypted) 61 .send() 62 .await 63 .map_err(|e| AppError::Internal(format!("Push send failed: {e}")))?; 64 65 if response.status().as_u16() == 410 || response.status().as_u16() == 404 { 66 return Err(AppError::NotFound("Subscription expired".to_string())); 67 } 68 69 if !response.status().is_success() { 70 let status = response.status(); 71 let body = response.text().await.unwrap_or_default(); 72 return Err(AppError::Internal(format!( 73 "Push service returned {status}: {body}" 74 ))); 75 } 76 77 Ok(()) 78} 79 80fn get_origin(url: &str) -> String { 81 if let Some(idx) = url.find("://") { 82 let rest = &url[idx + 3..]; 83 if let Some(path_idx) = rest.find('/') { 84 url[..idx + 3 + path_idx].to_string() 85 } else { 86 url.to_string() 87 } 88 } else { 89 url.to_string() 90 } 91} 92 93/// Generate a new VAPID key pair. 94/// Returns (private_key_pem, public_key_base64url). 95pub fn generate_vapid_keys() -> Result<(String, String), AppError> { 96 use p256::elliptic_curve::sec1::ToEncodedPoint; 97 use p256::pkcs8::EncodePrivateKey; 98 use p256::SecretKey; 99 100 let secret_key = SecretKey::random(&mut rand::rngs::OsRng); 101 102 let pem = secret_key 103 .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) 104 .map_err(|e| AppError::Internal(format!("Key generation failed: {e}")))?; 105 106 let public_key = secret_key.public_key(); 107 let point = public_key.to_encoded_point(false); 108 let public_key_b64url = URL_SAFE_NO_PAD.encode(point.as_bytes()); 109 110 Ok((pem.to_string(), public_key_b64url)) 111} 112 113/// Load or generate VAPID keys from the database. 114pub async fn init_vapid_keys(pool: &sqlx::SqlitePool) -> (String, String) { 115 let existing = sqlx::query_as::<_, (String, String)>( 116 "SELECT private_key_pem, public_key_base64url FROM vapid_keys WHERE id = 1", 117 ) 118 .fetch_optional(pool) 119 .await 120 .expect("Failed to query vapid_keys"); 121 122 if let Some((pem, pub_key)) = existing { 123 return (pem, pub_key); 124 } 125 126 let (pem, pub_key) = generate_vapid_keys().expect("VAPID key generation failed"); 127 128 sqlx::query( 129 "INSERT INTO vapid_keys (id, private_key_pem, public_key_base64url) VALUES (1, ?, ?)", 130 ) 131 .bind(&pem) 132 .bind(&pub_key) 133 .execute(pool) 134 .await 135 .expect("Failed to store VAPID keys"); 136 137 println!("Generated new VAPID keys"); 138 (pem, pub_key) 139}