Security
How WolfWave protects Twitch and local WebSocket credentials with macOS Keychain, App Sandbox, OAuth Device Code flow, and signed updates.
Credentials are stored in macOS Keychain. Twitch OAuth tokens go directly to Twitch over encrypted connections, while overlay and control tokens are presented only to your WolfWave server. WolfWave never sends these credentials to MrDemonWolf, Inc.
Credential Storage
All credentials live in the macOS Keychain, accessed through the app's KeychainService wrapper:
- Twitch OAuth access token. Used for Helix API + EventSub
- Twitch refresh token. For silent token refresh
- Twitch broadcaster + bot account info. IDs and usernames for the connected account
- WebSocket overlay token. Read-only credential for OBS and LAN state consumers
- WebSocket control token. Separate same-Mac credential for Stream Deck commands
try KeychainService.saveTwitchCredentialGrant(
.init(accessToken: "access-token", refreshToken: "refresh-token")
)
let grant = try KeychainService.loadTwitchCredentialGrantChecked()
try KeychainService.deleteTwitchCredentialGrant()Tokens are never written to UserDefaults or stored on disk in plain text.
Twitch Authentication
WolfWave uses the OAuth Device Code Flow:
- The app requests a device code from Twitch
- You authorize on Twitch's own website
- The app polls for completion and receives tokens
- Tokens go straight into the Keychain
You never type credentials into the app, and authorization happens on Twitch's secure pages.
On every launch, stored tokens are validated. An expired access token refreshes silently when Twitch accepts the refresh token. Only an explicit token rejection or confirmed missing permissions asks you to reconnect. Network errors, rate limits, and malformed responses keep the saved credentials and retry within a bounded window instead of signing you out.
WebSocket Server Security
The built-in WebSocket server (port 8765) binds to all interfaces so a second-PC OBS or a phone browser on the same LAN can connect. Authentication assigns one of two roles:
- Both tokens are independent 64-character hex strings (32 random bytes), minted at first launch and stored separately in the macOS Keychain.
- Overlay role: a client presents
Sec-WebSocket-Protocol: wolfwave.overlay.<hex>. Loopback and LAN peers may connect and receive state, but inbound commands are rejected. - Control role: a client presents
Sec-WebSocket-Protocol: wolfwave.control.<hex>. The server accepts it only from a literal loopback IP (127/8or::1), and only this role may execute commands. Reusing the overlay secret with a control prefix does not authenticate. - Selected subprotocols are revalidated after the handshake before a connection enters the broadcast set.
- The static HTTP shell (port 8766) is unauthenticated. It injects only the overlay token into served HTML when the TCP peer is loopback and the
Hostheader is a literal local name or address (localhost,127/8,[::1]), which blocks DNS-rebinding bootstrap. Token-bearing responses are markedCache-Control: no-store. - LAN URLs carry
?token=…solely as an overlay bootstrap. The widget script forwards it into the overlay subprotocol; the query never authenticates the HTTP request itself. Widget HTML enforcesno-referrer, so cross-origin artwork requests cannot leak the URL token. - Either token is rotatable from Settings → Stream Widgets. Rotation drops every connected client; update the matching browser-source URL or Stream Deck setting afterward.
- Logs redact both tokens to their first 4 characters, never the full values.
The Stream Deck control channel uses this listener but a separate credential and loopback-only authorization policy. Discord Rich Presence uses a separate local Unix socket ($TMPDIR/discord-ipc-{0..9}) that is never LAN-reachable.
Network Security
External traffic uses encrypted transport. Local browser-source traffic is plaintext HTTP/WS on the same Mac or LAN:
| Endpoint | Protocol | Purpose |
|---|---|---|
api.twitch.tv/helix | HTTPS | Twitch Helix API |
id.twitch.tv/oauth2 | HTTPS | Twitch OAuth |
eventsub.wss.twitch.tv/ws | WSS | Twitch EventSub WebSocket |
itunes.apple.com/search | HTTPS | Album artwork for Discord Rich Presence |
github.com/MrDemonWolf/wolfwave/releases/... | HTTPS | Sparkle appcast + signed updates |
0.0.0.0:8765 | WS (plaintext LAN) | Read-only now-playing over LAN; control only from loopback, both token-gated |
0.0.0.0:8766 | HTTP (plaintext LAN) | Unauthenticated widget shell |
| Local Unix socket | IPC | Discord Rich Presence ($TMPDIR/discord-ipc-{0..9}) |
The two local ports are governed by the auth model in WebSocket Server Security.
The local transport is not encrypted and the HTTP shell is public. Expose these ports only on a trusted LAN.
EventSub & API Validation
Twitch responses are treated as untrusted input:
- All JSON parsing uses explicit
do/catchwith structured logging; malformed responses are logged, never silently ignored, and never crash the service - Every EventSub message must carry
message_idandmessage_timestamp - Messages older than 10 minutes are rejected to prevent replay attacks
- Subscription type is verified before processing; unexpected types are ignored
Sparkle Updates
Sparkle runs with privacy-conscious defaults:
- EdDSA-signed appcast. Every release is signed with an Ed25519 key; the public key (
SUPublicEDKey) ships inInfo.plistand verifies the signature before any update is applied. - Explicit consent for downloads.
automaticallyDownloadsUpdates = false; Sparkle never writes bytes to disk until you click Install. - No system profile transmitted.
SUSendProfileInfois not enabled, so background checks send only the appcast request itself. - DEBUG builds. Sparkle initializes with
startingUpdater: falseand manual checks read a bundleddev-appcast.xml, so development never touches the production feed. - Homebrew installs. Sparkle is disabled entirely; Homebrew manages updates.
Stable vs. Nightly update channels are covered in Nightly Builds.
Listening History
Listening History, Stats, and Monthly Wrap are off by default. When enabled, the play log is an append-only file inside the app's sandbox; nothing is uploaded and no server is contacted. Recording rules and reset options live in Listening History, and the privacy commitments are in the Privacy Policy.
Code Signing & Notarization
WolfWave ships as a signed and notarized DMG:
- Developer ID Application certificate for distribution signing
- Hardened Runtime enabled with all exceptions explicitly disabled
- App Sandbox enabled with scoped entitlements
- Apple notarization ensures the app is free of known malware
- Stapled ticket allows offline Gatekeeper verification
No Gatekeeper warnings on first launch.
For Contributors
These details only matter when building WolfWave from source. See Development for the full setup.
Build configuration
API identifiers live in Config.xcconfig, which is gitignored so each developer uses their own keys. Values are baked into Info.plist at build time.
| Key | Purpose | Sensitivity |
|---|---|---|
TWITCH_CLIENT_ID | Identifies the app to Twitch | Public (not a secret) |
DISCORD_CLIENT_ID | Identifies the app to Discord | Public (not a secret) |
GITHUB_REPO_OWNER | GitHub owner used for Sparkle updates + issue links | Public (branding) |
GITHUB_REPO_NAME | GitHub repo name used for Sparkle updates + issue links | Public (branding) |
DOCS_URL | Documentation site root; privacy/terms/changelog derive from it | Public (branding) |
COMMUNITY_DISCORD_URL | Community Discord invite in tray menu | Public (branding) |
COPYRIGHT_HOLDER | Legal entity in About + Monthly Wrap footers | Public (branding) |
All values are public: OAuth client identifiers and branding strings, not secrets. The branding keys are optional fork overrides that fall back to the upstream MrDemonWolf defaults when blank.
Entitlements
WolfWave runs inside the macOS App Sandbox with the minimum required entitlements:
| Entitlement | Purpose |
|---|---|
app-sandbox | Required for Mac App Store and adopted here for defense-in-depth; notarization requires Hardened Runtime, not App Sandbox. |
keychain-access-groups | Secure credential storage |
files.user-selected.read-write | Settings export/import and log-export file pickers (NSSavePanel / NSOpenPanel). |
automation.apple-events | ScriptingBridge to Apple Music (Music is driven by Apple Events, not MusicKit) |
temporary-exception.apple-events | Belt-and-suspenders grant scoped to com.apple.Music for pre-existing TCC entries |
temporary-exception.sbpl | Discord IPC socket access, narrowed to the discord-ipc-N socket path pattern |
temporary-exception.files.absolute-path.read-write | Read-write access to /private/var/folders/ where Discord exposes its IPC sockets |
These entitlements are load-bearing. Removing any of the Apple Events entries silently breaks Music.app control under the sandbox.
Stream Deck Control API
Control WolfWave from a same-Mac Stream Deck client. Protocol v3 command envelope, 12 actions, acks, and live queue and health broadcasts.
Design System
WolfWave's visual language. Design tokens, reusable SwiftUI components, and brand assets. One source of truth powering the native app, docs, widget, and marketing.