From b27561c91efc39e24c2d7bbcf6b1a192e8062320 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Tue, 25 Aug 2026 23:50:28 +0200 Subject: [PATCH] Update VAPID key handling and improve push notification encoding --- .dockerignore | 1 + README.md | 14 ++++++++++---- config.example.toml | 2 +- crates/adapters/web-push/src/lib.rs | 25 +++++++++---------------- spa/src/hooks/use-push.ts | 11 +++++++++-- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/.dockerignore b/.dockerignore index 712417c..10c2da0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,3 +8,4 @@ data/ config.toml spa/node_modules/ spa/dist/ +spa/.env diff --git a/README.md b/README.md index 4f58668..01d1827 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,17 @@ Copy `config.example.toml` to `config.toml` and adjust as needed. K-Mood supports Web Push notifications (works on iOS 16.4+ when added to Home Screen, Android, and desktop browsers). No Firebase or third-party service required. -**1. Generate VAPID keys:** +**1. Generate a VAPID private key** (base64url-encoded, 32 bytes): ```bash -openssl ecparam -genkey -name prime256v1 -noout -out vapid_private.pem -openssl ec -in vapid_private.pem -outform PEM 2>/dev/null | base64 +python3 -c " +import subprocess, base64 +key = subprocess.check_output( + 'openssl ecparam -genkey -name prime256v1 -noout 2>/dev/null | openssl ec -outform DER 2>/dev/null', + shell=True +) +print(base64.urlsafe_b64encode(key[7:39]).rstrip(b'=').decode()) +" ``` **2. Add to `config.toml`:** @@ -86,7 +92,7 @@ openssl ec -in vapid_private.pem -outform PEM 2>/dev/null | base64 ```toml [push] enabled = true -vapid_private_key = "" +vapid_private_key = "" vapid_subject = "mailto:you@example.com" ``` diff --git a/config.example.toml b/config.example.toml index a32c744..3417494 100644 --- a/config.example.toml +++ b/config.example.toml @@ -38,5 +38,5 @@ allow_registration = true # [push] # enabled = true -# vapid_private_key = "" +# vapid_private_key = "" # vapid_subject = "mailto:you@example.com" diff --git a/crates/adapters/web-push/src/lib.rs b/crates/adapters/web-push/src/lib.rs index 5f2f7cb..2672c52 100644 --- a/crates/adapters/web-push/src/lib.rs +++ b/crates/adapters/web-push/src/lib.rs @@ -12,7 +12,7 @@ use domain::user::UserId; pub struct WebPushSender { client: IsahcWebPushClient, - vapid_private_key: Vec, + vapid_private_key: String, vapid_subject: String, subscription_query: Arc, } @@ -32,14 +32,15 @@ impl WebPushSender { .as_deref() .ok_or_else(|| DomainError::InvalidInput("vapid_subject is required".into()))?; - let decoded = base64_decode(private_key)?; + VapidSignatureBuilder::from_base64_no_sub(private_key) + .map_err(|e| DomainError::InvalidInput(format!("invalid VAPID key: {e}")))?; let client = IsahcWebPushClient::new() .map_err(|e| DomainError::InvalidInput(format!("failed to create push client: {e}")))?; Ok(Self { client, - vapid_private_key: decoded, + vapid_private_key: private_key.to_string(), vapid_subject: subject.to_string(), subscription_query, }) @@ -51,9 +52,7 @@ impl WebPushSender { .as_deref() .ok_or_else(|| DomainError::InvalidInput("vapid_private_key is required".into()))?; - let decoded = base64_decode(private_key)?; - - let sig_builder = VapidSignatureBuilder::from_pem_no_sub(std::io::Cursor::new(&decoded)) + let sig_builder = VapidSignatureBuilder::from_base64_no_sub(private_key) .map_err(|e| DomainError::InvalidInput(format!("invalid VAPID key: {e}")))?; let public_key = sig_builder.get_public_key(); @@ -61,13 +60,6 @@ impl WebPushSender { } } -fn base64_decode(input: &str) -> Result, DomainError> { - use base64::Engine; - base64::engine::general_purpose::STANDARD - .decode(input) - .map_err(|e| DomainError::InvalidInput(format!("invalid base64: {e}"))) -} - fn base64_url_encode(input: &[u8]) -> String { use base64::Engine; base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(input) @@ -91,10 +83,11 @@ impl domain::ports::ReminderSenderPort for WebPushSender { let payload_str = payload.to_string(); for sub in &subscriptions { - let subscription_info = SubscriptionInfo::new(sub.endpoint(), sub.p256dh(), sub.auth()); + let subscription_info = + SubscriptionInfo::new(sub.endpoint(), sub.p256dh(), sub.auth()); - let mut sig_builder = VapidSignatureBuilder::from_pem( - std::io::Cursor::new(&self.vapid_private_key), + let mut sig_builder = VapidSignatureBuilder::from_base64( + &self.vapid_private_key, &subscription_info, ) .map_err(|e| { diff --git a/spa/src/hooks/use-push.ts b/spa/src/hooks/use-push.ts index cc16b75..29002bf 100644 --- a/spa/src/hooks/use-push.ts +++ b/spa/src/hooks/use-push.ts @@ -13,6 +13,13 @@ function urlBase64ToUint8Array(base64String: string): Uint8Array { return array } +function arrayBufferToBase64Url(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer) + let binary = "" + for (const b of bytes) binary += String.fromCharCode(b) + return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "") +} + export function usePushNotifications() { const [isSubscribed, setIsSubscribed] = useState(false) const [isSupported, setIsSupported] = useState(false) @@ -59,8 +66,8 @@ export function usePushNotifications() { await push.subscribe({ endpoint: sub.endpoint, - p256dh: btoa(String.fromCharCode(...new Uint8Array(key))), - auth: btoa(String.fromCharCode(...new Uint8Array(auth))), + p256dh: arrayBufferToBase64Url(key), + auth: arrayBufferToBase64Url(auth), }) }, onSuccess: () => setIsSubscribed(true),