From dfbdd34aaae79b8549546e7af2b1ea430c501636 Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Fri, 26 Jun 2026 20:07:53 +0200 Subject: [PATCH] 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 --- web/src/adapter/browser.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/web/src/adapter/browser.ts b/web/src/adapter/browser.ts index b14e404..0e8fec3 100644 --- a/web/src/adapter/browser.ts +++ b/web/src/adapter/browser.ts @@ -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 { + 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 { 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 = new Map() - constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map) { + constructor(identity: Identity, sig: Signaling, peerId: string, on: PeerCallback, nick: string, share: Map, 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) }