// 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") }