Correct the POST_NOTIFICATIONS mechanism: the lockscreen notification is exempt because of the MediaSession token, not because it belongs to a foreground service — FGS notifications are explicitly NOT exempt. So no permission prompt and no checkSelfPermission gate; instead both notification builders bind the token once and log loudly if it is ever null, turning a silent failure into a logcat line. Stop the webview undoing the network security config: mixedContentMode COMPATIBILITY, allowFileAccess/allowContentAccess false. Conflict resolution: this branch's DR-198 collided with the Tauri branch's, so it was renumbered DR-200 (3 TRACES in JellyTauPlaybackService.kt and the UR-006 matrix row updated). DR-199 was uncontested. Pinned counts summed to DR 191 / total 334; UR-071 takes both DR-198 and DR-199.
133 lines
7.8 KiB
Markdown
133 lines
7.8 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 |
|
|
|
|
## Webview Content Security Policy
|
|
|
|
`app.security.csp` in `tauri.conf.json` (TRACES: UR-012, UR-071 | DR-198). It was
|
|
`null` — CSP disabled — which meant any script that reached the web layer
|
|
inherited the full IPC surface. Tauri computes the header from this value when it
|
|
serves the embedded HTML, injecting a nonce for SvelteKit's inline bootstrap
|
|
script, so `script-src` needs no `'unsafe-inline'`.
|
|
|
|
```
|
|
default-src 'self';
|
|
script-src 'self';
|
|
style-src 'self' 'unsafe-inline';
|
|
font-src 'self' data:;
|
|
img-src 'self' data: blob: asset: http://asset.localhost http: https:;
|
|
media-src 'self' blob: asset: http://asset.localhost http://127.0.0.1:* http: https:;
|
|
connect-src 'self' ipc: http://ipc.localhost http: https:;
|
|
worker-src 'self' blob:;
|
|
object-src 'none'; frame-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
|
|
```
|
|
|
|
| Directive | Why |
|
|
|-----------|-----|
|
|
| `default-src 'self'` | Everything not named below is same-origin only. |
|
|
| `script-src 'self'` | The genuinely restrictive half. Bundled JS only; Tauri's build-time nonce covers the one inline `<script>` in `index.html`. Adding `'unsafe-inline'` here would silently do nothing anyway — a nonce in a directive voids it. |
|
|
| `style-src 'self' 'unsafe-inline'` | Svelte compiles `style="…"` attributes into markup, including `app.html`'s `display: contents` wrapper, and CSP treats a style *attribute* as inline. Safe only while no `<style>` **element** survives into `index.html`: Tauri would nonce it, and the nonce would then void `'unsafe-inline'`. The production build extracts all CSS to files, so it currently has none. |
|
|
| `img-src` | Thumbnails come from two places: the asset protocol (`asset://localhost/…` on Linux/macOS, `http://asset.localhost/…` on Windows/Android — the same protocol, named differently by `convertFileSrc`) and, on a cache miss, straight from the Jellyfin server. `data:`/`blob:` cover inline and generated images. |
|
|
| `media-src` | `<video>`/`<audio>` sources: HLS transcodes and progressive streams from the server, the token-guarded loopback media server on `http://127.0.0.1:<random port>` (DR-137), and `blob:` for the MSE object URL hls.js attaches. |
|
|
| `connect-src` | `ipc:` / `http://ipc.localhost` is Tauri's `invoke` transport (custom scheme on Linux/macOS, `http` host on Windows/Android) — without it every command is blocked. `http:`/`https:` is hls.js fetching manifests and segments; ordinary API traffic goes through Rust and is not subject to CSP. |
|
|
| `worker-src 'self' blob:` | hls.js runs its demuxer in a worker built from a blob (`enableWorker: true`). Without `blob:` it falls back to main-thread demuxing — playback survives but costs more CPU. |
|
|
| `object-src`, `frame-src` = `'none'` | No plugins, no iframes; both are classic injection sinks. |
|
|
| `base-uri 'self'`, `form-action 'self'`, `frame-ancestors 'none'` | Block `<base>` hijacking, form exfiltration and framing. `frame-ancestors` is only honoured when the policy is delivered as a header, which is platform-dependent; it is harmless where it is not. |
|
|
|
|
**`img-src`/`media-src`/`connect-src` are deliberately permissive.** The Jellyfin
|
|
origin is typed in by the user at run time and is routinely plain `http` on a
|
|
LAN, so it cannot be enumerated at build time. `http: https:` is a wide grant for
|
|
*data* — but it still bars `file:`, `filesystem:` and scripting schemes, and it
|
|
does not touch `script-src`, which is where an injected origin would actually
|
|
hurt. A run-time policy naming the server exactly was considered and rejected:
|
|
Tauri derives the header from immutable config at the moment it serves the HTML,
|
|
so it would mean rebuilding the config and reloading the webview whenever the
|
|
user adds or switches a server, to constrain a destination the user chooses
|
|
anyway.
|
|
|
|
`devCsp` mirrors the policy with `'unsafe-inline' 'unsafe-eval'` on `script-src`
|
|
and `ws:`/`wss:` on `connect-src`, because the Vite dev server injects styles and
|
|
code and drives HMR over a websocket. It applies only to `tauri dev`.
|
|
|
|
### Asset protocol scope
|
|
|
|
`app.security.assetProtocol.scope` is `$APPDATA/thumbnails/**` — not the storage
|
|
root. `imageCache.ts` is the only `convertFileSrc` caller left in the frontend:
|
|
downloaded media moved to the loopback media server in DR-137, and downloaded
|
|
audio is opened by MPV/ExoPlayer directly from its path. The old `$APPDATA/**`
|
|
grant let the webview read the SQLite database and the encrypted-token fallback
|
|
file alongside the thumbnails it actually needs.
|
|
|
|
If a new feature hands the webview a local file, widen this scope to that
|
|
subdirectory specifically; a path outside it resolves to nothing and the webview
|
|
reports `NETWORK_NO_SOURCE` (which is exactly how DR-134's failure presented).
|
|
|
|
## 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
|
|
6. **Webview Containment**: A restrictive `script-src` keeps injected script off the IPC surface; the asset protocol is scoped to the thumbnail cache only (see above)
|