# 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
Available?"} OSCredential["Store in OS Credential Manager
- Linux: libsecret/GNOME Keyring
- macOS: Keychain
- Windows: Credential Manager
- Android: EncryptedSharedPrefs"] EncryptedFallback["Encrypted File Fallback
(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, 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 `