From eacec4c45d661fdc35bcdc461dc3ac26861ffc7c Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 19 Jul 2026 15:17:40 +0200 Subject: [PATCH] Fix JMAP: Web Push payloads with `Content-Encoding: aes128gcm` should not be base64-encoded but sent as raw bytes --- CHANGELOG.md | 4 +++- crates/services/src/state_manager/http.rs | 9 +++------ crates/services/src/state_manager/push.rs | 3 ++- tests/src/jmap/core/push_subscription.rs | 8 +------- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b0abbc4e..b2078519 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,7 +12,9 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If ## Fixed - IMAP: Mailbox object-quota only enforced in JMAP. -- JMAP: Read-only sharee cannot set `isSubscribed` on a shared mailbox. +- JMAP: + - Read-only sharee cannot set `isSubscribed` on a shared mailbox. + - Web Push payloads with `Content-Encoding: aes128gcm` should not be base64-encoded but sent as raw bytes. - Calendar: - Uppercase `MAILTO` calendar addresses become invalid SMTP recipients. - Scheduling invitations on a shared, non-owned calendar fail with `MAIL FROM unauthorized`. diff --git a/crates/services/src/state_manager/http.rs b/crates/services/src/state_manager/http.rs index 153dc755..3d583dd8 100644 --- a/crates/services/src/state_manager/http.rs +++ b/crates/services/src/state_manager/http.rs @@ -6,7 +6,6 @@ use super::{Event, ece::ece_encrypt}; use crate::state_manager::PushRegistration; -use base64::Engine; use calcard::jscalendar::JSCalendarDateTime; use common::ipc::PushNotification; use email::push::PushSubscription; @@ -80,7 +79,7 @@ impl PushRegistration { .send( if http_request( &server, - serde_json::to_string(&response).unwrap(), + serde_json::to_string(&response).unwrap().into_bytes(), push_timeout, ) .await @@ -98,7 +97,7 @@ impl PushRegistration { pub(crate) async fn http_request( details: &PushSubscription, - mut body: String, + mut body: Vec, push_timeout: Duration, ) -> bool { let client_builder = reqwest::Client::builder().timeout(push_timeout); @@ -114,9 +113,7 @@ pub(crate) async fn http_request( .header("TTL", "86400"); if let Some(keys) = &details.keys { - match ece_encrypt(&keys.p256dh, &keys.auth, body.as_bytes()) - .map(|b| base64::engine::general_purpose::URL_SAFE.encode(b)) - { + match ece_encrypt(&keys.p256dh, &keys.auth, &body) { Ok(body_) => { body = body_; client = client.header(CONTENT_ENCODING, "aes128gcm"); diff --git a/crates/services/src/state_manager/push.rs b/crates/services/src/state_manager/push.rs index 7bcd1942..47587476 100644 --- a/crates/services/src/state_manager/push.rs +++ b/crates/services/src/state_manager/push.rs @@ -223,7 +223,8 @@ pub fn spawn_push_manager(inner: Arc) -> mpsc::Sender { ), Id::from(subscription.id), subscription.verification_code - ), + ) + .into_bytes(), push_timeout, ) .await; diff --git a/tests/src/jmap/core/push_subscription.rs b/tests/src/jmap/core/push_subscription.rs index 883c4d02..cb050f49 100644 --- a/tests/src/jmap/core/push_subscription.rs +++ b/tests/src/jmap/core/push_subscription.rs @@ -5,7 +5,6 @@ */ use crate::{AssertConfig, utils::server::TestServer}; -use base64::{Engine, engine::general_purpose}; use common::{config::server::Listeners, network::SessionData}; use ece::EcKeyComponents; use http_proto::{HtmlResponse, ToHttpResponse, request::fetch_body}; @@ -290,12 +289,7 @@ impl common::network::SessionManager for SessionManager { .is_some_and(|encoding| encoding.to_str().unwrap() == "aes128gcm"); let body = fetch_body(&mut req, 1024 * 1024, 0).await.unwrap(); let message = serde_json::from_slice::(&if is_encrypted { - ece::decrypt( - &push.keypair, - &push.auth_secret, - &general_purpose::URL_SAFE.decode(body).unwrap(), - ) - .unwrap() + ece::decrypt(&push.keypair, &push.auth_secret, &body).unwrap() } else { body })