Files
Shade/android/shade-android/MIGRATION-NOVA.md
Sterister 4bf9307548
Some checks failed
Test / test (push) Has been cancelled
feat(android): M-Cross 1-3 — Kotlin module + cross-platform test vectors
Phase C complete: Shade now has a Kotlin implementation with byte-for-byte
compatibility to the TypeScript core, verified by shared test vectors.

M-Cross 1: shade-android Kotlin module
- build.gradle.kts with Tink, EncryptedSharedPreferences, kotlinx.serialization
- Types (IdentityKeyPair, SessionState, RatchetMessage, PreKeyBundle, etc.)
- CryptoProvider interface
- TinkProvider implementation (X25519, Ed25519, AES-GCM, HKDF, HMAC)
- KDF chain functions (kdfRootKey, kdfChainKey, deriveInitialRootKey)
  with the same info strings and salts as @shade/core
- Fingerprint (safety number) computation matching TS exactly
- X3DH protocol: identity gen, signed prekey gen, OTPK gen, bundle processing
- Double Ratchet: initSenderSession, initReceiverSession, ratchetEncrypt,
  ratchetDecrypt, DH ratchet step, skipped key cache
- Wire format matching @shade/proto byte-for-byte
- StorageProvider interface + MemoryStorage impl
- High-level ShadeSessionManager mirroring @shade/core's API

M-Cross 2: Cross-platform test vectors
- scripts/generate-vectors.ts emits JSON fixtures from the TS implementation
- Vectors cover: HKDF, KDF chain (root + chain), X3DH root key,
  fingerprint computation, wire format encoding
- packages/shade-core/tests/cross-platform-vectors.test.ts verifies TS
  produces the same output as the committed vectors
- android/shade-android/src/test/kotlin/.../CrossPlatformVectorTest.kt
  loads the SAME JSON and verifies Kotlin produces identical bytes

M-Cross 3: Nova Android migration plan
- android/shade-android/MIGRATION-NOVA.md — concrete steps to replace
  Nova's static PushKeyStore AES with Shade sessions
- Phase 1 (dual-write) / Phase 2 (switch reads) / Phase 3 (deprecate)
- Smoke test recipe for end-to-end TS → Kotlin push flow

251 tests passing on the TS side. Kotlin tests run via Gradle when
the Android SDK is available; the vectors guarantee they'll pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 00:45:38 +02:00

86 lines
3.4 KiB
Markdown

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