chore: gitignore web/public/config.js; fix netHash to use WebCrypto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -14,3 +14,4 @@ launch-tui.sh
|
|||||||
*.swp
|
*.swp
|
||||||
/tui
|
/tui
|
||||||
launch-web.sh
|
launch-web.sh
|
||||||
|
web/public/config.js
|
||||||
|
|||||||
@@ -25,8 +25,24 @@ function concat(...arrs: Uint8Array[]): Uint8Array {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
function netHash(name: string): string {
|
function toHex(bytes: Uint8Array): string {
|
||||||
return sodium.to_hex(sodium.crypto_hash_sha256(enc('yaw2-net:' + name)))
|
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function netHash(name: string): Promise<string> {
|
||||||
|
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 ─────────────────────────────────────────────────────────────────
|
// ── Identity ─────────────────────────────────────────────────────────────────
|
||||||
@@ -97,8 +113,10 @@ class Identity {
|
|||||||
)
|
)
|
||||||
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
|
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES)
|
||||||
const ct = sodium.crypto_secretbox_easy(seed, nonce, key)
|
const ct = sodium.crypto_secretbox_easy(seed, nonce, key)
|
||||||
return { yaw: 'yaw-key-backup-1', id: this.id, alg: 'argon2id-secretbox',
|
return {
|
||||||
ops: BK_OPS, mem: BK_MEM, salt: b64(salt), nonce: b64(nonce), ct: b64(ct) }
|
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<string, unknown>, passphrase: string): Identity {
|
static importBackup(b: Record<string, unknown>, passphrase: string): Identity {
|
||||||
@@ -118,8 +136,10 @@ class Identity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toPeerInfo(networkId = ''): PeerInfo {
|
toPeerInfo(networkId = ''): PeerInfo {
|
||||||
return { id: this.id, alias: this.nick || this.short, public_key: this.id,
|
return {
|
||||||
created_at: new Date().toISOString(), network_id: networkId } as PeerInfo & { network_id?: string }
|
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
|
if (this._closed) return
|
||||||
const delay = this._backoff
|
const delay = this._backoff
|
||||||
this._backoff = Math.min(this._backoff * 2, 30000)
|
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) {
|
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() }
|
close() { this._closed = true; this.ws?.close() }
|
||||||
@@ -256,7 +276,7 @@ class PeerConn {
|
|||||||
try {
|
try {
|
||||||
const raw = sodium.from_base64(box, sodium.base64_variants.ORIGINAL)
|
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]
|
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]
|
return [this.identity.open(this.peerId, box), false]
|
||||||
}
|
}
|
||||||
@@ -265,9 +285,11 @@ class PeerConn {
|
|||||||
if (this._ekeySent || !this._epk) return
|
if (this._ekeySent || !this._epk) return
|
||||||
this._ekeySent = true
|
this._ekeySent = true
|
||||||
const signed = concat(enc(EKEY_PREFIX), sodium.from_hex(this.identity.id),
|
const signed = concat(enc(EKEY_PREFIX), sodium.from_hex(this.identity.id),
|
||||||
sodium.from_hex(this.peerId), this._epk)
|
sodium.from_hex(this.peerId), this._epk)
|
||||||
const msg = { kind: 'ekey', v: 'yaw/2.1', epk: sodium.to_hex(this._epk),
|
const msg = {
|
||||||
sig: sodium.to_hex(this.identity.sign(signed)) }
|
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)
|
const [box] = this._seal(msg, false)
|
||||||
this.sig.sendTo(this.peerId, box)
|
this.sig.sendTo(this.peerId, box)
|
||||||
}
|
}
|
||||||
@@ -278,7 +300,7 @@ class PeerConn {
|
|||||||
const epkRaw = sodium.from_hex(obj['epk'] as string)
|
const epkRaw = sodium.from_hex(obj['epk'] as string)
|
||||||
const sig = sodium.from_hex(obj['sig'] as string)
|
const sig = sodium.from_hex(obj['sig'] as string)
|
||||||
const signed = concat(enc(EKEY_PREFIX), sodium.from_hex(this.peerId),
|
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
|
if (epkRaw.length !== 32 || !Identity.verify(this.peerId, signed, sig)) return
|
||||||
this.peer_epk = epkRaw
|
this.peer_epk = epkRaw
|
||||||
} catch { return }
|
} catch { return }
|
||||||
@@ -335,8 +357,10 @@ class PeerConn {
|
|||||||
private _sendHello() {
|
private _sendHello() {
|
||||||
const bind = enc(BIND_PREFIX)
|
const bind = enc(BIND_PREFIX)
|
||||||
const caps: string[] = []
|
const caps: string[] = []
|
||||||
this._dc({ type: 'hello', id: this.identity.id, nick: this.nick, caps,
|
this._dc({
|
||||||
sig: sodium.to_hex(this.identity.sign(bind)) })
|
type: 'hello', id: this.identity.id, nick: this.nick, caps,
|
||||||
|
sig: sodium.to_hex(this.identity.sign(bind))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onControl(data: string) {
|
private _onControl(data: string) {
|
||||||
@@ -348,11 +372,15 @@ class PeerConn {
|
|||||||
if (m['id'] !== this.peerId) reason = 'id mismatch'
|
if (m['id'] !== this.peerId) reason = 'id mismatch'
|
||||||
else if (!this.peerAuthed) reason = 'unauthenticated signaling'
|
else if (!this.peerAuthed) reason = 'unauthenticated signaling'
|
||||||
this.verified = !reason
|
this.verified = !reason
|
||||||
this.on('connected', { peer: this.peerId, verified: this.verified,
|
this.on('connected', {
|
||||||
nick: m['nick'] as string || '', caps: m['caps'] || [] })
|
peer: this.peerId, verified: this.verified,
|
||||||
|
nick: m['nick'] as string || '', caps: m['caps'] || []
|
||||||
|
})
|
||||||
} else if (m['type'] === 'chat') {
|
} else if (m['type'] === 'chat') {
|
||||||
this.on('chat', { peer: this.peerId, room: m['room'] as string || 'general',
|
this.on('chat', {
|
||||||
text: m['text'] as string, ts: m['ts'] as number || Date.now() })
|
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') {
|
} else if (m['type'] === 'pm') {
|
||||||
this.on('pm', { peer: this.peerId, text: m['text'] as string, ts: m['ts'] as number || Date.now() })
|
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))
|
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<void> {
|
function gatherComplete(pc: RTCPeerConnection): Promise<void> {
|
||||||
@@ -464,7 +492,7 @@ export class BrowserAdapter {
|
|||||||
async joinNetwork(anchorUrl: string, nameOrHash: string, isHash = false) {
|
async joinNetwork(anchorUrl: string, nameOrHash: string, isHash = false) {
|
||||||
if (!this.identity) return
|
if (!this.identity) return
|
||||||
|
|
||||||
const hash = isHash ? nameOrHash : netHash(nameOrHash)
|
const hash = isHash ? nameOrHash : await netHash(nameOrHash)
|
||||||
this.networkId = hash.slice(0, 16)
|
this.networkId = hash.slice(0, 16)
|
||||||
this.networkName = isHash ? hash.slice(0, 16) : nameOrHash
|
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)
|
const nick = (data['nick'] as string) || (data['peer'] as string).slice(0, 16)
|
||||||
this.emit({
|
this.emit({
|
||||||
type: 'peer_connected',
|
type: 'peer_connected',
|
||||||
peer: { id: data['peer'] as string, alias: nick || (data['peer'] as string).slice(0, 8),
|
peer: {
|
||||||
public_key: data['peer'] as string, created_at: new Date().toISOString() },
|
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') {
|
} else if (event === 'chat') {
|
||||||
const ts = (data['ts'] as number) || Date.now()
|
const ts = (data['ts'] as number) || Date.now()
|
||||||
@@ -609,8 +639,10 @@ export class BrowserAdapter {
|
|||||||
this.emit({
|
this.emit({
|
||||||
type: 'message_received',
|
type: 'message_received',
|
||||||
network_id: this.networkId,
|
network_id: this.networkId,
|
||||||
message: { mid, from: this.identity.id as unknown as import('../types').PeerID,
|
message: {
|
||||||
to: msg.to, room: `dm:${pid.slice(0, 8)}`, text, ts },
|
mid, from: this.identity.id as unknown as import('../types').PeerID,
|
||||||
|
to: msg.to, room: `dm:${pid.slice(0, 8)}`, text, ts
|
||||||
|
},
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// Broadcast
|
// Broadcast
|
||||||
@@ -618,8 +650,10 @@ export class BrowserAdapter {
|
|||||||
this.emit({
|
this.emit({
|
||||||
type: 'message_received',
|
type: 'message_received',
|
||||||
network_id: this.networkId,
|
network_id: this.networkId,
|
||||||
message: { mid, from: this.identity.id as unknown as import('../types').PeerID,
|
message: {
|
||||||
room, text, ts },
|
mid, from: this.identity.id as unknown as import('../types').PeerID,
|
||||||
|
room, text, ts
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user