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 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 servers: RTCIceServer[] = [{ urls: STUN }]
if (cfg?.turnURL && cfg?.turnSecret) {
// TURN with time-limited credentials derived from static-auth-secret
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
}
@@ -267,14 +277,14 @@ class PeerConn {
// push-initiated files waiting for file-accept: xid → File
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.sig = sig
this.peerId = peerId
this.on = on
this.nick = nick
this.share = share
this.pc = new RTCPeerConnection({ iceServers: iceServers() })
this.pc = new RTCPeerConnection({ iceServers: ice })
const kp = sodium.crypto_box_keypair()
this._esk = kp.privateKey
this._epk = kp.publicKey
@@ -715,8 +725,9 @@ export class BrowserAdapter {
if (existing.verified || open || (alive && fresh)) return
}
const ice = await iceServers()
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._emitDiscovered(pid)
await peer.startOffer()
@@ -726,8 +737,9 @@ export class BrowserAdapter {
if (!this.identity || from === this.identity.id) return
let peer = this.peers.get(from)
if (!peer || peer.pc.connectionState === 'failed' || peer.pc.connectionState === 'closed') {
const ice = await iceServers()
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.peers.set(from, peer)
}