# Migrating Nova Android to Shade This document describes the concrete steps to replace Nova's static AES push notification encryption with Shade's Signal Protocol ratcheting. ## Current state **Nova server** (`nova/src/server/services/notifications.ts`): - Uses a per-device static AES-256-GCM key stored in `pushDevices.encryptionKey` - Calls `encryptPayload(notificationJson, key)` directly - Sends via FCM `data: { enc, v: '1' }` **Nova Android** (`Android/nova-app/.../data/PushKeyStore.kt`): - Generates the device's AES key once and stores it via EncryptedSharedPreferences - Decrypts FCM data payload in `NovaFirebaseMessagingService` - Uses `javax.crypto.Cipher` directly **Problem:** A single compromised key exposes all past and future notifications. No forward secrecy, no post-compromise recovery. ## Target state **Nova server:** - Uses `@shade/sdk` with `createShade({ prekeyServer, address: 'nova-server' })` - Per-device Shade sessions stored in PostgreSQL via `@shade/storage-postgres` - To notify a device: `await shade.send('device:${id}', notificationJson)` - The envelope is base64-encoded and sent via FCM `data: { enc, v: '2' }` **Nova Android:** - Uses `shade-android` (Kotlin) with `ShadeSessionManager` - Session state stored via `KeystoreStorage` (EncryptedSharedPreferences) - On FCM receive: decode envelope → `manager.decrypt('nova-server', envelope)` - First time registration: generate identity, upload prekey bundle to the Shade prekey server, and tell the Nova backend the device address ## Migration steps ### Phase 1: Dual-write (both work simultaneously) Add a `v` field to the FCM data payload. Android decrypts v=1 with legacy `PushKeyStore` and v=2 with Shade. Server can send either. Old devices keep working while new devices get Shade. ### Phase 2: Switch reads When 95% of devices have a Shade session established, flip the server to send v=2 by default. Fall back to v=1 only if the device has no Shade session. ### Phase 3: Deprecate Remove v=1 code paths, drop the `pushDevices.encryptionKey` column. ## Smoke test (prove it works end-to-end) 1. TS side creates a Shade instance for `nova-server` (using `@shade/sdk`) 2. TS side calls `shade.send('device:test', '{"title":"Hello"}')` 3. Encode the envelope as base64 → FCM `data.enc` 4. Kotlin side decodes base64 → `WireFormat.decodeEnvelope(bytes)` 5. Kotlin side calls `manager.decrypt('nova-server', envelope)` 6. Assert plaintext matches This is verified by the cross-platform vector tests + a manual smoke run described in `examples/07-nova-integration/` (to be added). ## Files to modify Nova server: - `nova/src/server/services/notifications.ts` — replace `encryptPayload` with `shade.send` - `nova/src/server/services/push-devices.ts` — track Shade address per device - Add `@shade/sdk` to `nova/package.json` Nova Android: - `Android/nova-app/app/src/main/java/no/zyon/nova/data/PushKeyStore.kt` — delegate to `ShadeSessionManager` - `Android/nova-app/app/src/main/java/no/zyon/nova/NovaFirebaseMessagingService.kt` — call `WireFormat.decodeEnvelope` and `manager.decrypt` - Add `shade-android` as a Gradle dependency ## Not done in M-Cross 3 Running a full Android Gradle build + instrumented tests is out of scope for this milestone. The cross-platform vector tests prove byte-for-byte compatibility; the actual Nova integration happens when the user explicitly wires up the Android module in their Nova project.