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.
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
// 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")
|
|
}
|