# 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.
