Push-értesítések iOS-re, Androidra és webre (Sygnal), titkosított üzenetek visszafejtése a telefonon #14

Merged
thatumi merged 18 commits from thatumi/native-push into main 2026-09-29 22:09:11 +00:00 AGit
Owner

Összefoglaló

Push-értesítések zárt alkalmazásnál is, egyetlen átjárón keresztül:
iPhone/iPad (APNs), Android (FCM) és böngésző (Web Push). A mobilappokban a
titkosított üzenetek szövegét is maga a telefon fejti vissza, a feladó vagy a
tér profilképével. Az asztali (Tauri) app nem kap háttér-pusht.

Átjáró – services/sygnal/

  • Sygnal v0.15.1 a jorboxon, a push.szabotar.dev mögött (NPM → 127.0.0.1:5000).
  • A titkok (APNs-kulcs, Firebase service account, VAPID) csak a szerveren
    vannak. A repóban csak a sygnal.example.yaml van.
  • A korábbi, csak Web Pusht tudó services/push-gateway törölve, a CI-lépésével együtt.

Kliens – apps/web/src/lib/push/

  • Platformonkénti pusher: dev.szabotar.jorkonvo.ios / .android / .web.
    Bejelentkezés után egyszeri engedélykérés mobilon. Token-cserénél a régi
    pusher törlődik.
  • Koppintásra megnyílik a szoba (hidegindításnál is). Ha a szobát megnyitod,
    az értesítései eltűnnek.
  • A mobilappok event_id_only pushert használnak. Az átjáró és az
    Apple/Google csak azonosítókat lát: szoba, esemény, olvasatlan üzenetek
    száma, és hogy melyik fióknak szól. Feladó, szobanév, szöveg és titkosított
    tartalom nem megy át rajtuk.
  • A web marad a teljes formátumnál, mert a service workernek nincsenek kulcsai.
  • Eszközönkénti beállítás (Beállítások → Értesítések → Értesítés tartalma):
    Teljes előnézet / Csak a feladó / Rejtett.
  • Futás közben az app egy titkosított natív tárolóba menti, amire az
    értesítésekhez szüksége lesz (nativeStore.ts): szerver-URL, token,
    Megolm-kulcsok (csak teljes módban), szoba- és tagnevek, kis profilképek.

Android

  • Saját FirebaseMessagingService. A push alapján lekéri az eseményt,
    visszafejti, és MessagingStyle-értesítést készít szobánként, a feladó vagy a
    tér képével.
  • PushStore: AES-256-GCM titkosítás, nem exportálható Android Keystore-kulccsal.
  • Megolm.java: csak visszafejtésre képes Megolm, a vodozemac-ból generált
    tesztvektorokon tesztelve (MegolmTest).
  • A google-services.json nincs a repóban.

iOS

  • Új Notification Service Extension (dev.szabotar.jorkonvo.NotificationService):
    lekéri és visszafejti az üzenetet, és Communication Notificationt
    készít belőle (INSendMessageIntent, a feladó képével).
  • JorPushCore helyi Swift csomag, amit az app és az extension közösen használ:
    • Megolm-visszafejtés (CryptoKit + CommonCrypto, Ed25519-aláírás-ellenőrzéssel);
    • tároló az App Groupban: AES-GCM, a kulcs a Keychainben (csak ezen az
      eszközön, az első feloldás után érhető el), a biztonsági mentésekből kimarad;
    • a feloldó logika.
  • A swift test a Codemagic build-unsigned workflow-jában fut, ugyanazokkal a vektorokkal, mint az Android.
  • JorPushStore Capacitor-plugin (ugyanaz az API, mint Androidon).
  • Új jogosultságok: aps-environment production, Communication Notifications,
    NSUserActivityTypes. A Sygnal loc-keyjeihez Localizable.strings en/hu.
  • codemagic.yaml: az új profil (jorkonvo-notification-service).
  • A 140-es build nagybetűs APNs-tokennel regisztrált pushere most
    automatikusan törlődik. Enélkül minden üzenet kétszer jönne, és a
    kikapcsolás sem törölné a pushert.

Adatvédelem

Frissítve:

  • store/privacy-policy.md és apps/web/public/privacy.html (EN + HU);
  • store/play-data-safety.md;
  • új store/app-store-privacy.md;
  • Play-áruházlap;
  • Sygnal README.

Ismert korlát, dokumentálva: ha egy üzenet olyan kulcsot használ, ami az
app utolsó megnyitása óta érkezett, az értesítésben „Titkosított üzenet” áll.

Tesztelés

  • npm run build + npm test zöld: 525 teszt átment, 1 kihagyva.
  • Swift-tesztek: 12/12 zöld (Codemagic). Android MegolmTest: 7/7 zöld.
  • Android (P30 Pro), bezárt appal: titkosított üzenet visszafejtve, a
    feladó képével. Mindhárom előnézeti mód működik.
  • iPhone, TestFlight 142:
    • DM és csatornaüzenet a feladó képével;
    • titkosított üzenet olvasható szöveggel;
    • koppintásra megnyílik a szoba;
    • „Csak a feladó” módban csak „Új üzenet” a feladóval;
    • „Rejtett” módban csak „Új üzenet”.

Összevonás után

  • A web kitelepítése.
  • Android AAB versionCode 3 feltöltése, a Play „Data safety” űrlap kitöltése
    a store/play-data-safety.md alapján.
  • Az App Store „App Privacy” űrlap kitöltése a store/app-store-privacy.md
    alapján, utána beküldés review-ra.

🤖 Generated with Claude Code

## Összefoglaló Push-értesítések zárt alkalmazásnál is, egyetlen átjárón keresztül: iPhone/iPad (APNs), Android (FCM) és böngésző (Web Push). A mobilappokban a titkosított üzenetek szövegét is maga a telefon fejti vissza, a feladó vagy a tér profilképével. Az asztali (Tauri) app nem kap háttér-pusht. ## Átjáró – `services/sygnal/` - Sygnal v0.15.1 a jorboxon, a `push.szabotar.dev` mögött (NPM → 127.0.0.1:5000). - A titkok (APNs-kulcs, Firebase service account, VAPID) csak a szerveren vannak. A repóban csak a `sygnal.example.yaml` van. - A korábbi, csak Web Pusht tudó `services/push-gateway` törölve, a CI-lépésével együtt. ## Kliens – `apps/web/src/lib/push/` - Platformonkénti pusher: `dev.szabotar.jorkonvo.ios` / `.android` / `.web`. Bejelentkezés után egyszeri engedélykérés mobilon. Token-cserénél a régi pusher törlődik. - Koppintásra megnyílik a szoba (hidegindításnál is). Ha a szobát megnyitod, az értesítései eltűnnek. - **A mobilappok `event_id_only` pushert használnak.** Az átjáró és az Apple/Google csak azonosítókat lát: szoba, esemény, olvasatlan üzenetek száma, és hogy melyik fióknak szól. Feladó, szobanév, szöveg és titkosított tartalom nem megy át rajtuk. - A web marad a teljes formátumnál, mert a service workernek nincsenek kulcsai. - Eszközönkénti beállítás (Beállítások → Értesítések → Értesítés tartalma): **Teljes előnézet** / **Csak a feladó** / **Rejtett**. - Futás közben az app egy titkosított natív tárolóba menti, amire az értesítésekhez szüksége lesz (`nativeStore.ts`): szerver-URL, token, Megolm-kulcsok (csak teljes módban), szoba- és tagnevek, kis profilképek. ## Android - Saját `FirebaseMessagingService`. A push alapján lekéri az eseményt, visszafejti, és MessagingStyle-értesítést készít szobánként, a feladó vagy a tér képével. - `PushStore`: AES-256-GCM titkosítás, nem exportálható Android Keystore-kulccsal. - `Megolm.java`: csak visszafejtésre képes Megolm, a vodozemac-ból generált tesztvektorokon tesztelve (`MegolmTest`). - A `google-services.json` nincs a repóban. ## iOS - Új **Notification Service Extension** (`dev.szabotar.jorkonvo.NotificationService`): lekéri és visszafejti az üzenetet, és **Communication Notificationt** készít belőle (`INSendMessageIntent`, a feladó képével). - **`JorPushCore`** helyi Swift csomag, amit az app és az extension közösen használ: - Megolm-visszafejtés (CryptoKit + CommonCrypto, Ed25519-aláírás-ellenőrzéssel); - tároló az App Groupban: AES-GCM, a kulcs a Keychainben (csak ezen az eszközön, az első feloldás után érhető el), a biztonsági mentésekből kimarad; - a feloldó logika. - A `swift test` a Codemagic `build-unsigned` workflow-jában fut, ugyanazokkal a vektorokkal, mint az Android. - `JorPushStore` Capacitor-plugin (ugyanaz az API, mint Androidon). - Új jogosultságok: `aps-environment` production, Communication Notifications, `NSUserActivityTypes`. A Sygnal loc-keyjeihez `Localizable.strings` en/hu. - `codemagic.yaml`: az új profil (`jorkonvo-notification-service`). - A 140-es build nagybetűs APNs-tokennel regisztrált pushere most automatikusan törlődik. Enélkül minden üzenet kétszer jönne, és a kikapcsolás sem törölné a pushert. ## Adatvédelem Frissítve: - `store/privacy-policy.md` és `apps/web/public/privacy.html` (EN + HU); - `store/play-data-safety.md`; - új `store/app-store-privacy.md`; - Play-áruházlap; - Sygnal README. **Ismert korlát, dokumentálva:** ha egy üzenet olyan kulcsot használ, ami az app utolsó megnyitása óta érkezett, az értesítésben „Titkosított üzenet” áll. ## Tesztelés - `npm run build` + `npm test` zöld: 525 teszt átment, 1 kihagyva. - Swift-tesztek: 12/12 zöld (Codemagic). Android `MegolmTest`: 7/7 zöld. - **Android (P30 Pro), bezárt appal:** titkosított üzenet visszafejtve, a feladó képével. Mindhárom előnézeti mód működik. - **iPhone, TestFlight 142:** - DM és csatornaüzenet a feladó képével; - titkosított üzenet olvasható szöveggel; - koppintásra megnyílik a szoba; - „Csak a feladó” módban csak „Új üzenet” a feladóval; - „Rejtett” módban csak „Új üzenet”. ## Összevonás után - A web kitelepítése. - Android AAB versionCode 3 feltöltése, a Play „Data safety” űrlap kitöltése a `store/play-data-safety.md` alapján. - Az App Store „App Privacy” űrlap kitöltése a `store/app-store-privacy.md` alapján, utána beküldés review-ra. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
One matrixdotorg/sygnal container on jorbox behind push.szabotar.dev serves
all three app_ids. The repo carries only the compose file and an example
config; the APNs .p8, the Firebase service account and the VAPID key live in
~/sygnal/secrets on the server. README documents what each pushkin actually
forwards (checked against the v0.15.1 source) - notably APNs never carries
the ciphertext of an encrypted event, only room_id + event_id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lib/push/ replaces lib/webPush.ts with one entry point for all platforms:

- pusher.ts builds the exact pusher Sygnal expects per app_id (webpush:
  pushkey = p256dh + endpoint/auth; iOS: hex APNs token + default sound;
  Android: FCM token) with the FULL format, so notifications can name the
  sender and show the text. Gateway defaults to push.szabotar.dev (any
  homeserver), VITE_PUSH_GATEWAY_URL="" turns push off. Unit tested.
- nativePush.ts (@capacitor/push-notifications): permission prompt the
  first time an account is ready, token -> pusher, token rotation drops the
  stale pusher, Settings can turn it off per device; a room's delivered
  notifications are cleared when it's opened.
- openFromNotification.ts: a tap (native action or sw.js click) opens the
  room inside its space, parked until login restore + sync know the room.
- sw.js reads Sygnal's webpush payload, stays quiet while a tab is visible
  (NotificationManager already notifies), and its click now really opens the
  room (the old /#/room/ link was never handled).
- PushToggle replaces WebPushToggle and shows errors instead of doing
  nothing; the Tauri desktop shell hides it (no push service behind CEF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sygnal's FCM pushkin sends data-only messages, which Android never displays
and the Capacitor plugin only forwards to (not running) JS. The new
JorKonvoMessagingService replaces the plugin's service in the merged
manifest, still hands tokens/messages to the plugin, and builds the
notification itself: "Messages" channel, one MessagingStyle notification per
room (sender + text, reply fallback stripped, "Encrypted message" for E2EE
rooms in this phase), hidden content on the lock screen, nothing while the
app is on screen. A tap carries google.message_id + room_id so the plugin's
handleOnNewIntent raises pushNotificationActionPerformed and JS opens the
room; a badge-only push with unread=0 clears them.

google-services.json (Firebase project jorkonvo, both dev.szabotar.jorkonvo
and the .screencap test id) stays out of git - builds are local, and the
gradle plugin is only applied when the file exists, so CI still builds.
sw.js gets the same call wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- App.entitlements: aps-environment = production (the App Store profile
  already carries it; checked in the build-130 IPA).
- AppDelegate forwards the APNs registration result to the Capacitor push
  plugin (without it register() never resolves).
- en/hu Localizable.strings for every loc-key Sygnal's APNs pushkin sends
  (argument order from apnspushkin._get_payload_full); encrypted events
  arrive without a body and read "<sender>: Encrypted message". Added to
  the App target's Resources via a new variant group, hu in knownRegions.
- A tap that launches the app reaches the scene's connectionOptions before
  the bridge (and its notification delegate) exists; SceneDelegate parks it
  and MainViewController replays it into Capacitor's NotificationRouter
  after the plugins load, so the room still opens.
- Info.plist note: no remote-notification background mode on purpose -
  alert pushes (and the phase-2 service extension) don't need it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Push uses the full pusher format, so the text of messages in UNENCRYPTED
rooms now passes through the developer's push gateway (push.szabotar.dev)
and Apple's / Google's / the browser's push service; encrypted rooms expose
sender, room name, ids (and on Android/web the unreadable ciphertext).

- privacy-policy.md + privacy.html: new Notifications section (how it
  works, what a notification contains, gateway logs, the Firebase
  installation ID on Android), push in "the app talks to" and Third parties,
  the iOS notification prompt in the permissions list.
- play-data-safety.md: push in the Messages and Device-ID rows (Google FCM
  as service provider), the FCM-added manifest permissions.
- app-store-privacy.md (new): the answers for App Store Connect's App
  Privacy form, which waited for push.
- Sygnal's docker logs are rotated (3x10 MB) so "short-lived logs" is true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Capacitor reports it as uppercase hex; lowercase is the canonical form
Sygnal/aioapns log and match against.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
services/push-gateway was never deployed; services/sygnal serves the same
dev.szabotar.jorkonvo.web app_id plus APNs and FCM. Drops its CI step and
updates RELEASE.md's open items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Push phase 2 decrypts encrypted-room notifications natively, outside the
WebView's rust-crypto store. Megolm.java takes an exported session key (what
matrix-js-sdk exportRoomKeys yields) and a Megolm v3 message: ratchet advance
as in libolm's megolm_advance_to, HKDF-SHA256 "MEGOLM_KEYS", truncated HMAC
check, AES-256-CBC. Ed25519 signatures are verified where the platform has
Ed25519 (Android 13+, any JVM); below that the MAC alone is checked.

MegolmTest runs against vectors from vodozemac itself (scripts/push/
gen_megolm_vectors.py): indices across every ratchet-part boundary, exports
at later indices, a message before the known index, a tampered ciphertext
and a forged signature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With an event_id_only pusher the push carries only room/event ids, the
unread count and jk_uid (the account). PushContent now does the rest on the
phone, per the account's preview mode: fetches the event with the stored
access token, decrypts m.room.encrypted with the exported Megolm keys
(checking the plaintext's room_id), names the sender/room from the app's
directory (or the member/name state as a fallback), and picks avatars:
DM -> the other person, channel -> its space's / room's picture, else the
sender's. Any failure degrades to "Encrypted message" / "New message".

PushStore keeps that data per account in the app-private dir, AES-GCM with
a non-exportable Android Keystore key; PushStorePlugin ("JorPushStore") is
how the app writes it. Legacy full-format pushes are still understood.

Verified on the P30 Pro (Android 10): a real E2EE message sent by a second
matrix-js-sdk client while the app was killed showed decrypted, with the
sender's avatar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Android pushers are event_id_only with default_payload.jk_uid, so the
  gateway, Google and the push payload carry no sender, room name or text;
  existing pushers are replaced on the next launch (data comparison).
- lib/push/nativeStore.ts keeps the native store current while the app
  runs: account + token, Megolm session keys (new sessions exported ~15 s
  after they show up and when the app goes to the background), room/user
  names and 96 px circular avatars; signed-out accounts and a disabled
  push are wiped.
- Settings -> Background notifications: "Notification content" = full
  preview (default) / sender only / hidden, with what each stores on the
  phone. iOS and web keep the phase-1 behaviour until iOS gets its
  Notification Service Extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A Notification Service Extension (dev.szabotar.jorkonvo.NotificationService)
turns the event_id_only push into sender + decrypted text with the
sender's/space's picture (Communication Notification, INSendMessageIntent).

- JorPushCore: local Swift package shared by the app and the extension -
  decrypt-only Megolm (CryptoKit + CommonCrypto, Ed25519 always verified),
  the App Group push store (AES-GCM, key in the Keychain under the App Group
  access group, AfterFirstUnlockThisDeviceOnly, excluded from backups) and
  the resolver. swift test runs the same vodozemac vectors as Android's
  MegolmTest, in the build-unsigned workflow.
- JorPushStore Capacitor plugin (PushStorePlugin.swift), same API as Android.
- App: communication entitlement, NSUserActivityTypes INSendMessageIntent,
  JK_NEW_MESSAGE loc-key. codemagic.yaml signs the new target with the
  jorkonvo-notification-service profile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
iOS now resolves notifications on the device like Android: the pusher is
event_id_only with a mutable "New message" alert (+ jk_uid) as the APNs
default_payload, and the preview-mode setting + key/directory sync apply on
iOS too. The privacy note names Google/Apple.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
setPreviewMode now rethrows a failed native-store sync, so Settings shows it
instead of the store silently behaving like "Hidden" (e.g. a Keychain or
App Group failure on iOS). The Full / Sender-only hints also mention the
room/member names the device keeps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mobile pushers are event_id_only: only identifiers pass the gateway and
Apple/Google; the phone fetches and decrypts on the device, from an
encrypted on-device store. Preview modes, the key-arrival limitation,
Communication Notifications on iOS. Web Push keeps the full format.
Play listing no longer says push is missing. Hub status updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Build 140 registered the APNs token as Capacitor hands it over (upper case);
later builds lowercase it, so with append:true both pushers stayed and every
message would arrive twice (found on the owner's iPhone after build 141).
ensurePusher now deletes same-app pushers whose key differs only in case,
and buildPusherDeletion lowercases iOS tokens like buildPusher - disabling
push on iOS used to miss the registered pusher.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hub: push test cleanup done (rooms purged, temp admin deactivated)
Some checks failed
CI / build-and-test (pull_request) Successful in 38s
CI / android (pull_request) Successful in 3m1s
CI / e2e (pull_request) Failing after 4m25s
ca78970599
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
thatumi/JorKonvo!14
No description provided.