Skip to content
WolfWave
Credentials stay safe

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:

  1. The app requests a device code from Twitch
  2. You authorize on Twitch's own website
  3. The app polls for completion and receives tokens
  4. 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/8 or ::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 Host header is a literal local name or address (localhost, 127/8, [::1]), which blocks DNS-rebinding bootstrap. Token-bearing responses are marked Cache-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 enforces no-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:

EndpointProtocolPurpose
api.twitch.tv/helixHTTPSTwitch Helix API
id.twitch.tv/oauth2HTTPSTwitch OAuth
eventsub.wss.twitch.tv/wsWSSTwitch EventSub WebSocket
itunes.apple.com/searchHTTPSAlbum artwork for Discord Rich Presence
github.com/MrDemonWolf/wolfwave/releases/...HTTPSSparkle appcast + signed updates
0.0.0.0:8765WS (plaintext LAN)Read-only now-playing over LAN; control only from loopback, both token-gated
0.0.0.0:8766HTTP (plaintext LAN)Unauthenticated widget shell
Local Unix socketIPCDiscord 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/catch with structured logging; malformed responses are logged, never silently ignored, and never crash the service
  • Every EventSub message must carry message_id and message_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 in Info.plist and 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. SUSendProfileInfo is not enabled, so background checks send only the appcast request itself.
  • DEBUG builds. Sparkle initializes with startingUpdater: false and manual checks read a bundled dev-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.

KeyPurposeSensitivity
TWITCH_CLIENT_IDIdentifies the app to TwitchPublic (not a secret)
DISCORD_CLIENT_IDIdentifies the app to DiscordPublic (not a secret)
GITHUB_REPO_OWNERGitHub owner used for Sparkle updates + issue linksPublic (branding)
GITHUB_REPO_NAMEGitHub repo name used for Sparkle updates + issue linksPublic (branding)
DOCS_URLDocumentation site root; privacy/terms/changelog derive from itPublic (branding)
COMMUNITY_DISCORD_URLCommunity Discord invite in tray menuPublic (branding)
COPYRIGHT_HOLDERLegal entity in About + Monthly Wrap footersPublic (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:

EntitlementPurpose
app-sandboxRequired for Mac App Store and adopted here for defense-in-depth; notarization requires Hardened Runtime, not App Sandbox.
keychain-access-groupsSecure credential storage
files.user-selected.read-writeSettings export/import and log-export file pickers (NSSavePanel / NSOpenPanel).
automation.apple-eventsScriptingBridge to Apple Music (Music is driven by Apple Events, not MusicKit)
temporary-exception.apple-eventsBelt-and-suspenders grant scoped to com.apple.Music for pre-existing TCC entries
temporary-exception.sbplDiscord IPC socket access, narrowed to the discord-ipc-N socket path pattern
temporary-exception.files.absolute-path.read-writeRead-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.

On this page