fix: stop leaking TURN secret to browser clients
All checks were successful
Build / Build & release (push) Successful in 13m40s
All checks were successful
Build / Build & release (push) Successful in 13m40s
WASTE_CONFIG.turnSecret was shipped in plaintext config.js and used to compute coturn HMAC credentials client-side in browser.ts. Anyone reading the PWA's JS could read the secret and mint unlimited long-lived TURN credentials, turning the relay into an open proxy. The anchor now mints short-lived (1h) credentials server-side via a new GET /turn-credentials endpoint (-turn-secret flag), mirroring what the daemon already does. The browser fetches credentials instead of holding the secret. Daemon mode was unaffected (already server-side). Docs updated to drop turnSecret from config.js examples, document the new nginx route, and instruct anyone with an old config.js to rotate the coturn secret since it was previously exposed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,23 +16,31 @@ const EKEY_PREFIX = 'yaw/2.1 ekey'
|
||||
const FS_TIMEOUT = 2000
|
||||
const STUN = 'stun:stun.l.google.com:19302'
|
||||
|
||||
async function turnCredential(secret: string, username: string): Promise<string> {
|
||||
const key = await crypto.subtle.importKey(
|
||||
'raw', new TextEncoder().encode(secret),
|
||||
{ name: 'HMAC', hash: 'SHA-1' },
|
||||
false, ['sign']
|
||||
)
|
||||
const sig = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(username))
|
||||
return btoa(String.fromCharCode(...new Uint8Array(sig)))
|
||||
// TURN credentials are short-lived and minted server-side by the anchor's
|
||||
// GET /turn-credentials endpoint (see cmd/anchor). The shared coturn secret
|
||||
// never reaches the browser — only a time-limited username/credential pair.
|
||||
async function fetchTurnCredentials(credentialsURL: string): Promise<{ username: string; credential: string } | null> {
|
||||
try {
|
||||
const res = await fetch(credentialsURL)
|
||||
if (!res.ok) return null
|
||||
const data = await res.json()
|
||||
if (!data.username || !data.credential) return null
|
||||
return { username: data.username, credential: data.credential }
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async function iceServers(): Promise<RTCIceServer[]> {
|
||||
const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnSecret?: string } }).WASTE_CONFIG
|
||||
const cfg = (window as unknown as { WASTE_CONFIG?: { turnURL?: string; turnCredentialsURL?: string; signalURL?: string } }).WASTE_CONFIG
|
||||
const servers: RTCIceServer[] = [{ urls: STUN }]
|
||||
if (cfg?.turnURL && cfg?.turnSecret) {
|
||||
const user = Math.floor(Date.now() / 1000) + 3600 + ':waste'
|
||||
const credential = await turnCredential(cfg.turnSecret, user)
|
||||
servers.push({ urls: cfg.turnURL, username: user, credential })
|
||||
if (cfg?.turnURL) {
|
||||
const credentialsURL = cfg.turnCredentialsURL
|
||||
?? cfg.signalURL?.replace(/^ws/, 'http').replace(/\/ws\/?$/, '/turn-credentials')
|
||||
if (credentialsURL) {
|
||||
const creds = await fetchTurnCredentials(credentialsURL)
|
||||
if (creds) servers.push({ urls: cfg.turnURL, username: creds.username, credential: creds.credential })
|
||||
}
|
||||
}
|
||||
return servers
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user