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:
commit
e1f7e6f529
18 files changed
+3690
No files matched your search
@@ -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")
|
||||
}
|
||||
Reference in new issue
Block a user