Skip to content

Security: BurntToasters/Arlet

Security

docs/SECURITY.md

Security Model

Webview privileges

Tauri capabilities are a real boundary, not documentation. Permissions are granted per window label in src-tauri/capabilities/; the default set covers only version, window management, updater, clipboard text, the library cache (fixed Rust commands, no SQL), settings, pins, the saved playback session (fixed load/save/delete commands, size- and item-capped), logging, window effects, the Windows media session, opening the diagnostic window, and full-screen toggling for Now Playing (core:window:allow-set-fullscreen and core:window:allow-is-fullscreen). Browser opening is limited to two fixed commands: open_support_page (no arguments, https://rosie.run/support) and open_about_link, which accepts only project, release_notes, or license and builds its GitHub URL in Rust, so web content cannot choose a URL. Both go through open_in_default_browser. There is no shell, dialog, notification, process, or global-shortcut plugin, no arbitrary command execution, and remote Apple origins get no filesystem/shell/updater access.

The tray icon is Tauri's core tray-icon feature, built in Rust. Its menu emits the same windows-media-control event as the Windows media buttons and grants the webview no additional permissions.

The Phase 0 music-diagnostic window loads https://music.apple.com/ with an empty permission list, local: false, and remote URL scope limited to that origin. It has zero Tauri commands, and its navigation is limited to https://*.apple.com because it shares the app's cookies. Do not scrape Apple's DOM, inject scripts into that page, or add privileges to that capability unless a later gate failure documents a narrowly scoped need.

The main window refuses top-level navigation to anything but the bundled app (tauri.localhost, or the dev server in debug builds). A popup or a stray link can therefore never turn the privileged window into a remote page.

MusicKit authorize() uses window.open to authorize.music.apple.com. Tauri 2 denies webview popups unless on_new_window allows them. The main window is created from Rust (create: false) so that handler can allow only Apple auth hosts and about:blank. Allowed popups use WebView2's default window (no Tauri IPC). Never log the authorize URL; the query string is the developer JWT.

Content Security Policy in src-tauri/tauri.conf.json limits scripts to the exact js-cdn.music.apple.com host and uses an exact host for the authorization frame, plus provisional https://*.apple.com and https://*.mzstatic.com subdomain wildcards for image, connect, and media traffic. Narrowing scripts to the /musickit/ path waits on a signed-in playback check, because MusicKit may load further code from that host during playback. The observed host list is still pending; docs/MUSICKIT_NETWORK_SURFACE.md records that status and the provisional surface. Reconcile those entries after a fresh Phase 0 export. Never broaden CSP or capabilities to make a bug disappear; document the required origin first.

Apple Music credentials

  • There are two token classes: the developer token (signed from your Media Services private key) and the Music User Token (the subscriber's, managed by MusicKit). Keep them conceptually separate everywhere.
  • Never ship the .p8 private key in the app, installer, repo, bundle, binary, or CI log. Only the signed developer JWT is shipped.
  • MUSICKIT_DEVELOPER_TOKEN lives in .env (gitignored; copy .env.example → .env). npm run phase0:mint-token mints it from MUSICKIT_TEAM_ID, MUSICKIT_KEY_ID, and an absolute MUSICKIT_P8_PATH outside the repo; it never prints the JWT or .p8.
  • Debug builds read the token from the process environment at runtime (dotenv -e .env -- tauri dev).
  • Release builds embed the token at compile time. src-tauri/build.rs reads it from the release machine's .env and validates it with src-tauri/src/token_policy.rs (ES256 JWT, integer exp, at least 30 days left), or fails the build. At runtime get_developer_token serves only the embedded token, never the user's environment, and reports an expired token as "install the latest update". release-preflight.js runs the same check before a draft release exists and needs 150 days left, so the token outlives the weekly canary's 150-day release-age warning.
  • The embedded JWT can be extracted from the binary, as with any MusicKit web app. Its lifetime is the exposure window: Apple caps it at 6 months, so a new release must ship before the embedded token expires.
  • Release tokens carry no origin claim. Apple accepts an origin-restricted token for catalog requests but refuses library (/v1/me) requests, even from http://tauri.localhost, so 0.1.0 cannot load anyone's library. build.rs and release:preflight reject one, and the native E2E checks the embedded token has none. The claim only limited browsers anyway: a native client can send any Origin header.
  • Mint on a machine that holds the .p8, and copy only the JWT to the release machine.
  • ARLET_SKIP_MUSICKIT_TOKEN=1 builds a release binary without a token for unpublished CI smoke builds only. tauri-windows-build.js and the release preflight refuse it.

Logging

Auth data is toxic: Apple passwords, Music User Tokens, developer private keys, cookies, full Authorization headers, the updater private key, and token-service bearer values are never logged. Both the Rust logger (src-tauri/src/logging.rs) and the frontend (src/platform/redact.ts) redact JWT-shaped values and values labelled by a token key (Music-User-Token, media-user-token, musicUserToken, developerToken). The frontend also redacts the exact developer and Music User Token values once MusicKit has produced them. Request logs record method/path/status/ duration only. The Rust redactor has unit tests; the native E2E checks that a Music User Token never reaches the log file.

The log file (arlet.log in the local app data folder) is opt-in: it is written only while Settings › Debug Mode is on, which is off by default. The in-memory recent log that Copy diagnostics report uses works either way and never leaves the machine unless the user pastes it. One exception is written regardless of Debug Mode: a native crash, the main window failing to open, or a WebView2 process dying appends a single redacted [native] line, because nothing else would record it.

Third-party script

MusicKit JS loads from Apple's CDN at runtime and runs in the main webview, so it can call every command the main window is granted. It cannot be pinned with Subresource Integrity because Apple updates it in place. The mitigation is to keep the grant small: the webview has no generic SQL, filesystem, shell, dialog, notification, or process access, and the library cache is reachable only through fixed, size-bounded commands (src-tauri/src/library_cache.rs). Clipboard read remains for paste in the context menu. Window permissions are the ones the custom title bar uses, the full-screen pair Now Playing uses (set-fullscreen, is-fullscreen), and set-size for the native E2E. Tauri's isolation pattern is the next step if the grant ever needs to grow.

Now-playing artwork is fetched by Windows itself for the media overlay, so only *.mzstatic.com and *.apple.com artwork URLs are passed on; any other host would learn the user's IP address.

Remote script exposure

Threat: a compromised or misbehaving MusicKit script, or a bug in the page, calls the Rust commands with hostile input. The script runs in the main window, so every command that window is granted is reachable from it.

Why no isolation yet: Tauri's isolation pattern would route every IPC call through a sandboxed iframe, which means rewiring the whole controller and retesting every native path. That is too invasive for this release, so the mitigation is input validation at each command. Revisit it if the grant grows.

Each command treats its arguments as hostile. Caps are checked in Rust before any disk or OS call:

  • save_settings: 512 KiB, must be a JSON object.
  • save_pins: 64 KiB, at most 100 entries, each with a non-empty id.
  • save_playback_session: 256 KiB, at most 500 items, schemaVersion 1.
  • library_cache_*: scope, section, cursor, next cursor, and storefront at most 2048 bytes; item id and type at most 2048 bytes; 10,000 items and 16 MiB of item data per page. A rejected write is rolled back. There is no SQL.
  • append_local_log: 8 KiB per entry, line breaks escaped; the file rotates at 5 MiB.
  • update_windows_media_session: title, artist, and album are cut to 1024 characters; artwork URLs over 2048 bytes are dropped; only *.mzstatic.com and *.apple.com HTTPS artwork is passed to Windows.
  • update_windows_media_timeline: non-finite or non-positive durations are rejected; positions are clamped to the duration.
  • set_snap_overlay_bounds: integers within ±1,000,000, dimensions up to 16,384.
  • set_window_fx: exactly acrylic, mica, or solid.
  • open_about_link: exactly project, release_notes, or license; the URL is built in Rust.
  • No argument from the page: open_support_page, open_music_diagnostic, get_app_info, get_developer_token, get_beta_updater_target, get_log_dir, clear_logs, supports_window_fx, reset_settings, clear_windows_media_session, quit_ready. Each returns fixed data or acts on fixed paths.

Native events are not commands. Suspend and default-output-change pauses (src-tauri/src/system_events.rs) are triggered by Windows, not by the page, and screen lock is deliberately not a trigger.

Sign-out

Signing out stops playback, revokes MusicKit authorization, clears the SQLite library cache, and deletes the local pinned-playlist file and the saved playback queue, so the next Apple ID on the same Windows account starts clean.

Signing separation

Windows Authenticode (Azure Artifact Signing), Tauri updater signatures, and GPG detached signatures are three independent mechanisms; one is never accepted as proof of another. See docs/RELEASE.md.

Reporting

Do not file public issues for suspected vulnerabilities. Open a private security advisory on the GitHub repository so the issue can be fixed before disclosure.

There aren't any published security advisories