Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/feat-encrypted-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

Push notifications now use `event_id_only` format — Sygnal never sees message content or sender metadata, and encrypted messages are decrypted client-side when the app tab is open
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,7 @@ export async function enablePushNotifications(
lang: navigator.language || 'en',
data: {
url: clientConfig.pushNotificationDetails?.pushNotifyUrl,
// format: 'event_id_only' as const,
events_only: true,
format: 'event_id_only' as const,
endpoint: pushSubAtom.endpoint,
p256dh: keys.p256dh,
auth: keys.auth,
Expand Down Expand Up @@ -111,7 +110,7 @@ export async function enablePushNotifications(
lang: navigator.language || 'en',
data: {
url: clientConfig.pushNotificationDetails?.pushNotifyUrl,
// format: 'event_id_only' as const,
format: 'event_id_only' as const,
endpoint: newSubscription.endpoint,
p256dh: keys.p256dh,
auth: keys.auth,
Expand Down
79 changes: 79 additions & 0 deletions src/app/pages/client/ClientNonUIFeatures.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { ReactNode, useCallback, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import {
MatrixEvent,
MatrixEventEvent,
PushProcessor,
RoomEvent,
Expand Down Expand Up @@ -44,13 +45,16 @@
resolveNotificationPreviewText,
} from '$utils/notificationStyle';
import { mobileOrTablet } from '$utils/user-agent';
import { createDebugLogger } from '$utils/debugLogger';
import { useSlidingSyncActiveRoom } from '$hooks/useSlidingSyncActiveRoom';
import { getSlidingSyncManager } from '$client/initMatrix';
import { NotificationBanner } from '$components/notification-banner';
import { useCallSignaling } from '$hooks/useCallSignaling';
import { getInboxInvitesPath } from '../pathUtils';
import { BackgroundNotifications } from './BackgroundNotifications';

const pushRelayLog = createDebugLogger('push-relay');

function clearMediaSessionQuickly(): void {
if (!('mediaSession' in navigator)) return;
// iOS registers the lock screen media player as a side-effect of
Expand Down Expand Up @@ -619,6 +623,80 @@
return null;
}

/**
* Listens for decryptPushEvent messages from the service worker, decrypts the
* event using the local Olm/Megolm session, then replies with pushDecryptResult
* so the SW can show a notification with the real message content.
* Falls back gracefully (success: false) on any error or if keys are missing.
*/
function HandleDecryptPushEvent() {
const mx = useMatrixClient();

useEffect(() => {
if (!('serviceWorker' in navigator)) return undefined;

const handleMessage = async (ev: MessageEvent) => {
const { data } = ev;
if (!data || data.type !== 'decryptPushEvent') return;

const { rawEvent } = data as { rawEvent: Record<string, unknown> };
const eventId = rawEvent.event_id as string;
const roomId = rawEvent.room_id as string;
const decryptStart = performance.now();

try {
const mxEvent = new MatrixEvent(rawEvent as any);
await mx.decryptEventIfNeeded(mxEvent);

const room = mx.getRoom(roomId);
const sender = mxEvent.getSender();
let senderName = 'Someone';
if (sender) {
senderName = getMxIdLocalPart(sender) ?? sender;
if (room) senderName = getMemberDisplayName(room, sender) ?? senderName;
}

const decryptMs = Math.round(performance.now() - decryptStart);
const visible = document.visibilityState === 'visible';
pushRelayLog.info('notification', 'Push relay decryption succeeded', {
eventType: mxEvent.getType(),
decryptMs,
appVisible: visible,
});

navigator.serviceWorker.controller?.postMessage({
type: 'pushDecryptResult',
eventId,
success: true,
eventType: mxEvent.getType(),
content: mxEvent.getContent(),
sender_display_name: senderName,
room_name: room?.name ?? '',
visibilityState: document.visibilityState,
});
} catch (err) {
console.warn('[app] HandleDecryptPushEvent: failed to decrypt push event', err);

Check warning on line 678 in src/app/pages/client/ClientNonUIFeatures.tsx

View workflow job for this annotation

GitHub Actions / Lint

Unexpected console statement
pushRelayLog.error(
'notification',
'Push relay decryption failed',
err instanceof Error ? err : new Error(String(err))
);
navigator.serviceWorker.controller?.postMessage({
type: 'pushDecryptResult',
eventId,
success: false,
visibilityState: document.visibilityState,
});
}
};

navigator.serviceWorker.addEventListener('message', handleMessage);
return () => navigator.serviceWorker.removeEventListener('message', handleMessage);
}, [mx]);

return null;
}

function PresenceFeature() {
const mx = useMatrixClient();
const [sendPresence] = useSetting(settingsAtom, 'sendPresence');
Expand Down Expand Up @@ -646,6 +724,7 @@
<MessageNotifications />
<BackgroundNotifications />
<SyncNotificationSettingsWithServiceWorker />
<HandleDecryptPushEvent />
<NotificationBanner />
<SlidingSyncActiveRoomSubscriber />
<PresenceFeature />
Expand Down
2 changes: 1 addition & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ if ('serviceWorker' in navigator) {
const activeId = getLocalStorageItem<string | undefined>(ACTIVE_SESSION_KEY, undefined);
const active =
sessions.find((s) => s.userId === activeId) ?? sessions[0] ?? getFallbackSession();
pushSessionToSW(active?.baseUrl, active?.accessToken);
pushSessionToSW(active?.baseUrl, active?.accessToken, active?.userId);
};

navigator.serviceWorker
Expand Down
3 changes: 2 additions & 1 deletion src/sw-session.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
export function pushSessionToSW(baseUrl?: string, accessToken?: string) {
export function pushSessionToSW(baseUrl?: string, accessToken?: string, userId?: string) {
if (!('serviceWorker' in navigator)) return;
if (!navigator.serviceWorker.controller) return;

navigator.serviceWorker.controller.postMessage({
type: 'setSession',
accessToken,
baseUrl,
userId,
});
}
Loading
Loading