fix(turn): generate HMAC-SHA1 credentials for coturn use-auth-secret

coturn's use-auth-secret mode requires the TURN password to be
HMAC-SHA1(secret, username), not the raw secret. Pass ice servers
as a constructor argument so the async credential generation happens
before RTCPeerConnection is created.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-26 20:07:53 +02:00
parent 295851f966
commit dfbdd34aaa

View File

@@ -16,13 +16,23 @@ const EKEY_PREFIX = 'yaw/2.1 ekey'
const FS_TIMEOUT = 2000 const FS_TIMEOUT = 2000
const STUN = 'stun:stun.l.google.com:19302' const STUN = 'stun:stun.l.google.com:19302'
function iceServers(): RTCIceServer[] { 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)))
}
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; turnSecret?: string } }).WASTE_CONFIG
const servers: RTCIceServer[] = [{ urls: STUN }] const servers: RTCIceServer[] = [{ urls: STUN }]
if (cfg?.turnURL && cfg?.turnSecret) { if (cfg?.turnURL && cfg?.turnSecret) {
// TURN with time-limited credentials derived from static-auth-secret
const user = Math.floor(Date.now() / 1000) + 3600 + ':waste' const user = Math.floor(Date.now() / 1000) + 3600 + ':waste'
servers.push({ urls: cfg.turnURL, username: user, credential: cfg.turnSecret }) const credential = await turnCredential(cfg.turnSecret, user)
servers.push({ urls: cfg.turnURL, username: user, credential })
} }
return servers return servers
} }
@@ -267,14 +277,14 @@ class PeerConn {
// push-initiated files waiting for file-accept: xid → File // push-initiated files waiting for file-accept: xid → File
private _pushQueue: Map<string, File> = new Map() private _pushQueue: Map<string, File> = new Map()
constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map<string, File>) { constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map<string, File>, ice: RTCIceServer[]) {
this.identity = identity this.identity = identity
this.sig = sig this.sig = sig
this.peerId = peerId this.peerId = peerId
this.on = on this.on = on
this.nick = nick this.nick = nick
this.share = share this.share = share
this.pc = new RTCPeerConnection({ iceServers: iceServers() }) this.pc = new RTCPeerConnection({ iceServers: ice })
const kp = sodium.crypto_box_keypair() const kp = sodium.crypto_box_keypair()
this._esk = kp.privateKey this._esk = kp.privateKey
this._epk = kp.publicKey this._epk = kp.publicKey
@@ -715,8 +725,9 @@ export class BrowserAdapter {
if (existing.verified || open || (alive && fresh)) return if (existing.verified || open || (alive && fresh)) return
} }
const ice = await iceServers()
const peer = new PeerConn(this.identity, this.sig!, pid, const peer = new PeerConn(this.identity, this.sig!, pid,
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles) (ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles, ice)
this.peers.set(pid, peer) this.peers.set(pid, peer)
this._emitDiscovered(pid) this._emitDiscovered(pid)
await peer.startOffer() await peer.startOffer()
@@ -726,8 +737,9 @@ export class BrowserAdapter {
if (!this.identity || from === this.identity.id) return if (!this.identity || from === this.identity.id) return
let peer = this.peers.get(from) let peer = this.peers.get(from)
if (!peer || peer.pc.connectionState === 'failed' || peer.pc.connectionState === 'closed') { if (!peer || peer.pc.connectionState === 'failed' || peer.pc.connectionState === 'closed') {
const ice = await iceServers()
peer = new PeerConn(this.identity, this.sig!, from, peer = new PeerConn(this.identity, this.sig!, from,
(ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles) (ev, data) => this._onPeerEvent(ev, data), this.identity.nick, this.sharedFiles, ice)
this._emitDiscovered(from) this._emitDiscovered(from)
this.peers.set(from, peer) this.peers.set(from, peer)
} }