MainActivity set mixedContentMode = MIXED_CONTENT_ALWAYS_ALLOW together with allowFileAccess/allowContentAccess = true, which is a blanket cleartext opt-in reached by hand — the exact thing network_security_config.xml exists to prevent and its own comment warns against. Nothing needed any of the three: - file:// is never loaded. Cached thumbnails go through convertFileSrc, which on Android resolves to http://asset.localhost/... and is answered by wry's request interceptor rather than the filesystem; downloaded media goes over the loopback HTTP server (DR-137), which exists precisely because the asset/file route cannot stream a large file. - content:// is never loaded. The manifest's FileProvider is for outbound share intents, not webview navigation. - Mixed content never arises. Tauri serves the UI from http://tauri.localhost (use_https_scheme defaults false and is not set), and both 127.0.0.1 and asset.localhost are loopback/.localhost origins Chromium treats as potentially trustworthy. A plain-HTTP remote server would be mixed content, but the network security config already rejects it first — so ALWAYS_ALLOW bought nothing. COMPATIBILITY_MODE rather than NEVER_ALLOW is a deliberate hedge: the platform default at targetSdk 21+ is NEVER_ALLOW, so this is still one step looser, and it keeps passive content working if the analysis missed a path. The two files now cross-reference each other so the pair cannot drift apart again. Also records why POST_NOTIFICATIONS is declared but never requested. An audit read the missing runtime request as a threat to the lockscreen controls; it is not. A foreground-service notification is explicitly NOT exempt, but a media-session one is, and the platform predicate (Notification.isMediaNotification) requires MediaStyle AND a non-null session token. Confirmed on device: appops POST_NOTIFICATION: ignore with the transport notification live. So no permission prompt is added and startForeground stays ungated — a guard there would trade a cosmetic problem for the "did not then call Service.startForeground()" kill. What is added is the guard matching the real precondition: both builders bind the token once and log an error if it is ever null, since SystemUI's media carousel is gated on the same predicate and a token-less notification loses the lockscreen controls entirely, silently. TRACES: UR-006, UR-071 | DR-198, DR-199
72 lines
3.0 KiB
Markdown
72 lines
3.0 KiB
Markdown
# Security
|
|
|
|
## Authentication Token Storage
|
|
|
|
Access tokens are **not** stored in the SQLite database. Instead, they are stored using platform-native secure storage:
|
|
|
|
```mermaid
|
|
flowchart TB
|
|
LoginSuccess["Login Success"]
|
|
KeyringCheck{"System Keyring<br/>Available?"}
|
|
OSCredential["Store in OS Credential Manager<br/>- Linux: libsecret/GNOME Keyring<br/>- macOS: Keychain<br/>- Windows: Credential Manager<br/>- Android: EncryptedSharedPrefs"]
|
|
EncryptedFallback["Encrypted File Fallback<br/>(AES-256-GCM)"]
|
|
|
|
LoginSuccess --> KeyringCheck
|
|
KeyringCheck -->|"Yes"| OSCredential
|
|
KeyringCheck -->|"No"| EncryptedFallback
|
|
```
|
|
|
|
**Key Format:**
|
|
```
|
|
jellytau::{server_id}::{user_id}::access_token
|
|
```
|
|
|
|
**Rationale:**
|
|
- Tokens in SQLite would be readable if the database file is accessed
|
|
- System keyrings provide OS-level encryption and access control
|
|
- Fallback ensures functionality on minimal systems without a keyring daemon
|
|
|
|
## Secure Storage Module
|
|
|
|
**Location**: `src-tauri/src/secure_storage/` (planned)
|
|
|
|
```rust
|
|
pub trait SecureStorage: Send + Sync {
|
|
fn store(&self, key: &str, value: &str) -> Result<(), SecureStorageError>;
|
|
fn retrieve(&self, key: &str) -> Result<Option<String>, SecureStorageError>;
|
|
fn delete(&self, key: &str) -> Result<(), SecureStorageError>;
|
|
}
|
|
|
|
// Platform implementations
|
|
pub struct KeyringStorage; // Uses keyring crate
|
|
pub struct EncryptedFileStorage; // AES-256-GCM fallback
|
|
```
|
|
|
|
## Network Security
|
|
|
|
| Aspect | Implementation |
|
|
|--------|----------------|
|
|
| Transport | HTTPS required for all Jellyfin API calls |
|
|
| Certificate Validation | System CA store (configurable for self-signed) |
|
|
| Token Transmission | Bearer token in `Authorization` header only |
|
|
| Token Refresh | Handled by Jellyfin server (long-lived tokens) |
|
|
| Android cleartext | `res/xml/network_security_config.xml` blocks cleartext everywhere except `127.0.0.1` (the loopback media server, DR-137/DR-138). The manifest's `usesCleartextTraffic` is ignored once the config is present, so the config is the single authority |
|
|
| Android WebView | `mixedContentMode = COMPATIBILITY` with `allowFileAccess`/`allowContentAccess` both `false` (DR-199). These are the second half of the cleartext policy: `ALWAYS_ALLOW` re-opened by hand what the network security config closes. Change the two together |
|
|
|
|
## Local Data Protection
|
|
|
|
| Data Type | Protection |
|
|
|-----------|------------|
|
|
| Access Tokens | System keyring or encrypted file |
|
|
| Database (SQLite) | Plaintext (metadata only, no secrets) |
|
|
| Downloaded Media | Filesystem permissions only |
|
|
| Cached Thumbnails | Filesystem permissions only |
|
|
|
|
## Security Considerations
|
|
|
|
1. **No Secrets in SQLite**: The database contains only non-sensitive metadata
|
|
2. **Token Isolation**: Each user/server combination has a separate token entry
|
|
3. **Logout Cleanup**: Token deletion from secure storage on logout
|
|
4. **No Token Logging**: Tokens are never written to logs or debug output
|
|
5. **IPC Security**: Tauri's IPC uses structured commands, not arbitrary code execution
|