Initial commit: DeathWatch, an Elden Ring death counter for OBS

Reads the game's process memory read-only (AOB signature scanning,
re-resolved every tick — never a cached pointer) and serves a live
death counter as a browser-source overlay plus a status panel.

Supports co-op: each player runs the program locally, one acts as
hub. The peer link is authenticated (HMAC challenge/response, no
token on the wire, constant-time compare) but not yet encrypted —
see CLAUDE.md's TODO section for the planned TLS+pinning split.

Zero external dependencies — including a hand-rolled WebSocket
implementation — so the whole thing ships as one .exe.
This commit is contained in:
emmatherock committed 2026-09-17 21:10:22 -03:00
commit e1f7e6f529
18 files changed
+3690

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
// auth.go: autenticacion entre el hub y los peers.
//
// El transporte NO se asume confiable. Puede ser Tailscale, ZeroTier,
// WireGuard, o un puerto abierto directo a internet: eso lo decide quien
// lo usa, no nosotros. Asi que la conexion se defiende sola.
//
// El token es OBLIGATORIO y lo genera el programa (nadie elige "1234"), y
// nunca viaja por la red: el hub manda un desafio al azar y el peer
// responde con un HMAC del desafio usando el token como clave. Quien
// escuche el trafico no se lleva el token, y no puede repetir una
// respuesta vieja porque el desafio cambia en cada conexion.
//
// Lo que esto NO hace: cifrar. Los mensajes (nombre y numero de muertes)
// viajan en claro. Para un contador de muertes en un stream es
// aceptable, pero si te importa, poné la conexion adentro de una VPN o
// un tunel con TLS. Lo que el token garantiza es que nadie pueda
// INYECTAR datos falsos en tu overlay.
package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
"log"
"os"
"path/filepath"
"strings"
)
const (
tokenBytes = 16 // 128 bits
nonceBytes = 16
// Plazo para autenticarse. Una conexion que se queda callada tras el
// desafio se cierra en vez de ocupar un lugar para siempre.
authTimeoutSeconds = 10
// Techo de conexiones simultaneas: evita que alguien deje el puerto
// sin recursos abriendo sockets.
maxPeerConns = 8
)
func tokenPath() string {
if dir, ok := exeDir(); ok {
return filepath.Join(dir, "token.txt")
}
return "token.txt"
}
func randomHex(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// resolveToken decide el token efectivo. Si el config trae uno, manda ese
// (para quien quiera elegirlo a mano o compartir uno ya acordado). Si no,
// se usa el de token.txt, y si tampoco existe se genera uno y se guarda.
//
// Devuelve tambien si lo acaba de generar, para avisarlo fuerte en la
// consola: es lo que hay que pasarle al compañero.
func resolveToken(cfg config) (token string, generated bool, err error) {
if t := strings.TrimSpace(cfg.Token); t != "" {
return t, false, nil
}
path := tokenPath()
if data, rerr := os.ReadFile(path); rerr == nil {
if t := strings.TrimSpace(string(data)); t != "" {
return t, false, nil
}
}
t, gerr := randomHex(tokenBytes)
if gerr != nil {
return "", false, fmt.Errorf("no pude generar un token: %w", gerr)
}
if werr := os.WriteFile(path, []byte(t), 0600); werr != nil {
return "", false, fmt.Errorf("no pude guardar token.txt: %w", werr)
}
return t, true, nil
}
// proofFor calcula la respuesta al desafio: HMAC-SHA256(token, nonce).
func proofFor(token, nonce string) string {
m := hmac.New(sha256.New, []byte(token))
m.Write([]byte(nonce))
return hex.EncodeToString(m.Sum(nil))
}
// proofValid compara en tiempo constante. Con == la comparacion corta en
// el primer byte distinto, y eso filtra informacion por tiempos.
func proofValid(token, nonce, got string) bool {
want := proofFor(token, nonce)
return subtle.ConstantTimeCompare([]byte(want), []byte(got)) == 1
}
func logTokenBanner(token string, generated bool) {
if generated {
log.Printf("generé un token nuevo y lo guardé en token.txt")
}
log.Printf("token de conexion: %s", token)
log.Printf("tu compañero tiene que poner ESE token en su config.toml; sin el, su conexion se rechaza")
}