diff --git a/.gitignore b/.gitignore index d2bd79a..0152b06 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ launch-tui.sh *.swp /tui launch-web.sh +web/public/config.js diff --git a/web/src/adapter/browser.ts b/web/src/adapter/browser.ts index fc75e34..1e5e574 100644 --- a/web/src/adapter/browser.ts +++ b/web/src/adapter/browser.ts @@ -25,8 +25,24 @@ function concat(...arrs: Uint8Array[]): Uint8Array { return out } -function netHash(name: string): string { - return sodium.to_hex(sodium.crypto_hash_sha256(enc('yaw2-net:' + name))) +function toHex(bytes: Uint8Array): string { + return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') +} + +async function netHash(name: string): Promise { + const input = enc('yaw2-net:' + name) + + // Use native SHA-256 for protocol-compatible network IDs. + if (globalThis.crypto?.subtle) { + const digest = await globalThis.crypto.subtle.digest('SHA-256', input) + return toHex(new Uint8Array(digest)) + } + + // Fallback for environments where subtle crypto is unavailable. + const sha256 = (sodium as unknown as { crypto_hash_sha256?: (m: Uint8Array) => Uint8Array }).crypto_hash_sha256 + if (sha256) return sodium.to_hex(sha256(input)) + + throw new Error('SHA-256 not available in this browser runtime') } // ── Identity ───────────────────────────────────────────────────────────────── @@ -97,8 +113,10 @@ class Identity { ) const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES) const ct = sodium.crypto_secretbox_easy(seed, nonce, key) - return { yaw: 'yaw-key-backup-1', id: this.id, alg: 'argon2id-secretbox', - ops: BK_OPS, mem: BK_MEM, salt: b64(salt), nonce: b64(nonce), ct: b64(ct) } + return { + yaw: 'yaw-key-backup-1', id: this.id, alg: 'argon2id-secretbox', + ops: BK_OPS, mem: BK_MEM, salt: b64(salt), nonce: b64(nonce), ct: b64(ct) + } } static importBackup(b: Record, passphrase: string): Identity { @@ -118,8 +136,10 @@ class Identity { } toPeerInfo(networkId = ''): PeerInfo { - return { id: this.id, alias: this.nick || this.short, public_key: this.id, - created_at: new Date().toISOString(), network_id: networkId } as PeerInfo & { network_id?: string } + return { + id: this.id, alias: this.nick || this.short, public_key: this.id, + created_at: new Date().toISOString(), network_id: networkId + } as PeerInfo & { network_id?: string } } } @@ -192,11 +212,11 @@ class Signaling { if (this._closed) return const delay = this._backoff this._backoff = Math.min(this._backoff * 2, 30000) - setTimeout(() => { if (!this._closed) this._open(false).catch(() => {}) }, delay) + setTimeout(() => { if (!this._closed) this._open(false).catch(() => { }) }, delay) } sendTo(toId: string, box: string) { - try { this.ws?.send(JSON.stringify({ type: 'to', to: toId, box })) } catch {} + try { this.ws?.send(JSON.stringify({ type: 'to', to: toId, box })) } catch { } } close() { this._closed = true; this.ws?.close() } @@ -256,7 +276,7 @@ class PeerConn { try { const raw = sodium.from_base64(box, sodium.base64_variants.ORIGINAL) return [sodium.crypto_box_open_easy(raw.slice(24), raw.slice(0, 24), this.peer_epk, this._esk), true] - } catch {} + } catch { } } return [this.identity.open(this.peerId, box), false] } @@ -265,9 +285,11 @@ class PeerConn { if (this._ekeySent || !this._epk) return this._ekeySent = true const signed = concat(enc(EKEY_PREFIX), sodium.from_hex(this.identity.id), - sodium.from_hex(this.peerId), this._epk) - const msg = { kind: 'ekey', v: 'yaw/2.1', epk: sodium.to_hex(this._epk), - sig: sodium.to_hex(this.identity.sign(signed)) } + sodium.from_hex(this.peerId), this._epk) + const msg = { + kind: 'ekey', v: 'yaw/2.1', epk: sodium.to_hex(this._epk), + sig: sodium.to_hex(this.identity.sign(signed)) + } const [box] = this._seal(msg, false) this.sig.sendTo(this.peerId, box) } @@ -278,7 +300,7 @@ class PeerConn { const epkRaw = sodium.from_hex(obj['epk'] as string) const sig = sodium.from_hex(obj['sig'] as string) const signed = concat(enc(EKEY_PREFIX), sodium.from_hex(this.peerId), - sodium.from_hex(this.identity.id), epkRaw) + sodium.from_hex(this.identity.id), epkRaw) if (epkRaw.length !== 32 || !Identity.verify(this.peerId, signed, sig)) return this.peer_epk = epkRaw } catch { return } @@ -335,8 +357,10 @@ class PeerConn { private _sendHello() { const bind = enc(BIND_PREFIX) const caps: string[] = [] - this._dc({ type: 'hello', id: this.identity.id, nick: this.nick, caps, - sig: sodium.to_hex(this.identity.sign(bind)) }) + this._dc({ + type: 'hello', id: this.identity.id, nick: this.nick, caps, + sig: sodium.to_hex(this.identity.sign(bind)) + }) } private _onControl(data: string) { @@ -348,11 +372,15 @@ class PeerConn { if (m['id'] !== this.peerId) reason = 'id mismatch' else if (!this.peerAuthed) reason = 'unauthenticated signaling' this.verified = !reason - this.on('connected', { peer: this.peerId, verified: this.verified, - nick: m['nick'] as string || '', caps: m['caps'] || [] }) + this.on('connected', { + peer: this.peerId, verified: this.verified, + nick: m['nick'] as string || '', caps: m['caps'] || [] + }) } else if (m['type'] === 'chat') { - this.on('chat', { peer: this.peerId, room: m['room'] as string || 'general', - text: m['text'] as string, ts: m['ts'] as number || Date.now() }) + this.on('chat', { + peer: this.peerId, room: m['room'] as string || 'general', + text: m['text'] as string, ts: m['ts'] as number || Date.now() + }) } else if (m['type'] === 'pm') { this.on('pm', { peer: this.peerId, text: m['text'] as string, ts: m['ts'] as number || Date.now() }) } @@ -370,7 +398,7 @@ class PeerConn { if (this.dc?.readyState === 'open') this.dc.send(JSON.stringify(obj)) } - close() { try { this.pc.close() } catch {} } + close() { try { this.pc.close() } catch { } } } function gatherComplete(pc: RTCPeerConnection): Promise { @@ -464,7 +492,7 @@ export class BrowserAdapter { async joinNetwork(anchorUrl: string, nameOrHash: string, isHash = false) { if (!this.identity) return - const hash = isHash ? nameOrHash : netHash(nameOrHash) + const hash = isHash ? nameOrHash : await netHash(nameOrHash) this.networkId = hash.slice(0, 16) this.networkName = isHash ? hash.slice(0, 16) : nameOrHash @@ -547,8 +575,10 @@ export class BrowserAdapter { const nick = (data['nick'] as string) || (data['peer'] as string).slice(0, 16) this.emit({ type: 'peer_connected', - peer: { id: data['peer'] as string, alias: nick || (data['peer'] as string).slice(0, 8), - public_key: data['peer'] as string, created_at: new Date().toISOString() }, + peer: { + id: data['peer'] as string, alias: nick || (data['peer'] as string).slice(0, 8), + public_key: data['peer'] as string, created_at: new Date().toISOString() + }, }) } else if (event === 'chat') { const ts = (data['ts'] as number) || Date.now() @@ -609,8 +639,10 @@ export class BrowserAdapter { this.emit({ type: 'message_received', network_id: this.networkId, - message: { mid, from: this.identity.id as unknown as import('../types').PeerID, - to: msg.to, room: `dm:${pid.slice(0, 8)}`, text, ts }, + message: { + mid, from: this.identity.id as unknown as import('../types').PeerID, + to: msg.to, room: `dm:${pid.slice(0, 8)}`, text, ts + }, }) } else { // Broadcast @@ -618,8 +650,10 @@ export class BrowserAdapter { this.emit({ type: 'message_received', network_id: this.networkId, - message: { mid, from: this.identity.id as unknown as import('../types').PeerID, - room, text, ts }, + message: { + mid, from: this.identity.id as unknown as import('../types').PeerID, + room, text, ts + }, }) } return