137 lines
4.3 KiB
Markdown
137 lines
4.3 KiB
Markdown
|
|
# Phone Recorder - Architecture Overview
|
||
|
|
|
||
|
|
## Purpose
|
||
|
|
|
||
|
|
Android app that records audio continuously, encrypts recordings immediately (never stored unencrypted), and syncs them to a server over LAN or internet.
|
||
|
|
|
||
|
|
## Security Model
|
||
|
|
|
||
|
|
```
|
||
|
|
Phone (has: server public key) Server (has: private key)
|
||
|
|
───────────────────────────── ────────────────────────
|
||
|
|
Record audio → temp.ogg
|
||
|
|
Encrypt with public key →
|
||
|
|
recording_*.ogg.enc
|
||
|
|
│
|
||
|
|
└──── HTTP POST ────────→ Receive encrypted file
|
||
|
|
Decrypt with private key
|
||
|
|
Store as .ogg in recordings/
|
||
|
|
```
|
||
|
|
|
||
|
|
- **Sealed box encryption**: X25519 key exchange + XSalsa20-Poly1305
|
||
|
|
- **Forward secrecy**: Each encryption uses ephemeral keys
|
||
|
|
- **Phone never stores unencrypted**: Temp file deleted immediately after encryption
|
||
|
|
- **If no public key configured**: Recordings are deleted (security-first design)
|
||
|
|
|
||
|
|
## File Format (SREC)
|
||
|
|
|
||
|
|
Encrypted files use a chunked format for streaming large files:
|
||
|
|
|
||
|
|
```
|
||
|
|
[4 bytes: "SREC" magic]
|
||
|
|
[1 byte: version (1)]
|
||
|
|
[4 bytes: chunk count, big-endian]
|
||
|
|
For each chunk (64KB max):
|
||
|
|
[4 bytes: encrypted length, big-endian]
|
||
|
|
[N bytes: sealed box ciphertext]
|
||
|
|
```
|
||
|
|
|
||
|
|
## Android App Structure
|
||
|
|
|
||
|
|
### Main Files
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `MainActivity.kt` | UI, settings, sync controls |
|
||
|
|
| `RecordingService.kt` | Foreground service for recording, encryption on stop |
|
||
|
|
| `StorageHelper.kt` | File paths, temp vs recordings directories |
|
||
|
|
|
||
|
|
### Crypto Package (`crypto/`)
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `CryptoHelper.kt` | SREC format encryption using lazysodium |
|
||
|
|
| `KeyManager.kt` | Store/retrieve server public key (SharedPreferences) |
|
||
|
|
|
||
|
|
### Sync Package (`sync/`)
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `SyncSettings.kt` | Sync mode enum, server URLs, LAN subnet prefix |
|
||
|
|
| `NetworkHelper.kt` | Detect WiFi, LAN (by IP prefix), network availability |
|
||
|
|
| `SyncTracker.kt` | Track which files synced successfully (SharedPreferences) |
|
||
|
|
| `SyncWorker.kt` | WorkManager background job for uploads |
|
||
|
|
| `SyncManager.kt` | Schedule periodic (15min) and immediate syncs |
|
||
|
|
|
||
|
|
### API Package (`api/`)
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `RecordingsApi.kt` | OkHttp client, upload with SHA-256 verification |
|
||
|
|
|
||
|
|
## Sync Modes
|
||
|
|
|
||
|
|
| Mode | Behavior |
|
||
|
|
|------|----------|
|
||
|
|
| Never | No sync, files stay on phone |
|
||
|
|
| LAN Only | Sync only when IP matches configured subnet (e.g., `10.0.0.`) |
|
||
|
|
| WiFi Only | Sync on any WiFi network |
|
||
|
|
| Any Network | Sync on WiFi or mobile data |
|
||
|
|
|
||
|
|
## Server Structure (Python/FastAPI)
|
||
|
|
|
||
|
|
| File | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| `main.py` | FastAPI endpoints |
|
||
|
|
| `crypto.py` | PyNaCl decryption, keypair management |
|
||
|
|
| `config.py` | Paths, ports |
|
||
|
|
|
||
|
|
### Endpoints
|
||
|
|
|
||
|
|
| Endpoint | Method | Description |
|
||
|
|
|----------|--------|-------------|
|
||
|
|
| `/health` | GET | Returns `{"status": "ok"}` |
|
||
|
|
| `/api/v1/public-key` | GET | Returns Base64 public key |
|
||
|
|
| `/api/v1/recordings/upload` | POST | Upload encrypted file, returns hash for verification |
|
||
|
|
| `/api/v1/recordings` | GET | List decrypted recordings |
|
||
|
|
|
||
|
|
## Data Flow
|
||
|
|
|
||
|
|
1. **Recording starts**: `RecordingService` writes to `temp/recording_TIMESTAMP.ogg`
|
||
|
|
2. **Recording stops**:
|
||
|
|
- `CryptoHelper.encryptFile()` reads temp, writes `recordings/recording_TIMESTAMP.ogg.enc`
|
||
|
|
- Temp file deleted
|
||
|
|
- `SyncManager.scheduleImmediateSync()` triggered
|
||
|
|
3. **Sync runs**:
|
||
|
|
- `SyncWorker` checks network conditions match `SyncMode`
|
||
|
|
- Computes SHA-256 of encrypted file
|
||
|
|
- Uploads to server
|
||
|
|
- Server decrypts, returns hash
|
||
|
|
- If hashes match, marks file as synced
|
||
|
|
4. **Server storage**: `recordings/YYYY/MM/DD/recording_TIMESTAMP.ogg`
|
||
|
|
|
||
|
|
## Key Dependencies
|
||
|
|
|
||
|
|
### Android
|
||
|
|
- `lazysodium-android` - libsodium bindings
|
||
|
|
- `androidx.work` - Background job scheduling
|
||
|
|
- `okhttp3` - HTTP client
|
||
|
|
|
||
|
|
### Server
|
||
|
|
- `fastapi` + `uvicorn` - Web framework
|
||
|
|
- `pynacl` - libsodium bindings
|
||
|
|
- `aiofiles` - Async file I/O
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
### Android (in-app settings)
|
||
|
|
- Sync mode (Never/LAN/WiFi/Any)
|
||
|
|
- LAN subnet prefix (default: `10.0.0.`)
|
||
|
|
- Server URLs (LAN and remote)
|
||
|
|
- Server public key (Base64)
|
||
|
|
|
||
|
|
### Server (config.py or env vars)
|
||
|
|
- `HOST` - Bind address (default: 0.0.0.0)
|
||
|
|
- `PORT` - Listen port (default: 8000)
|
||
|
|
- `BASE_DIR` - Base directory for keys and recordings
|