Identifiers, comments, and log/console messages in auth.go, i18n.go, counter.go, totals.go, and overlay.html's script/CSS comments — the files the TLS commit didn't already touch. No functional changes. locales/*.json and overlay.html's own FALLBACK/data-i18n strings stay bilingual on purpose: that's end-user text for the overlay/panel, a different audience than the source code, served by the existing i18n system rather than this convention.
106 lines
3.4 KiB
Go
106 lines
3.4 KiB
Go
// auth.go: authentication between the hub and its peers.
|
|
//
|
|
// The transport is NOT assumed trustworthy. It could be Tailscale,
|
|
// ZeroTier, WireGuard, or a port opened straight to the internet: that's
|
|
// up to whoever runs it, not us. So the connection defends itself.
|
|
//
|
|
// The token is REQUIRED and generated by the program (nobody gets to pick
|
|
// "1234"), and it never travels over the network: the hub sends a random
|
|
// challenge and the peer replies with an HMAC of it, keyed by the token.
|
|
// Anyone listening to the traffic doesn't get the token, and can't replay
|
|
// an old response because the challenge changes on every connection.
|
|
//
|
|
// This proves the PEER's identity to the hub. It doesn't encrypt
|
|
// anything by itself — that's TLS's job now, one layer up (see
|
|
// tlscert.go/pin.go), which in turn proves the HUB's identity to the
|
|
// peer via certificate pinning. Neither layer replaces the other.
|
|
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
|
|
// Deadline to authenticate. A connection that goes quiet after the
|
|
// challenge gets closed instead of holding a slot forever.
|
|
authTimeoutSeconds = 10
|
|
// Cap on simultaneous connections: keeps someone from starving the
|
|
// port of resources by opening 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 decides the effective token. If the config has one, that
|
|
// wins (for anyone who wants to pick it by hand or share a pre-agreed
|
|
// one). Otherwise it uses the one in token.txt, generating and saving one
|
|
// if that doesn't exist either.
|
|
//
|
|
// Also returns whether it was just generated, so it can be announced
|
|
// loudly on the console: that's what needs to be handed to your partner.
|
|
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("couldn't generate a token: %w", gerr)
|
|
}
|
|
if werr := os.WriteFile(path, []byte(t), 0600); werr != nil {
|
|
return "", false, fmt.Errorf("couldn't save token.txt: %w", werr)
|
|
}
|
|
return t, true, nil
|
|
}
|
|
|
|
// proofFor computes the answer to the challenge: 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 compares in constant time. With == the comparison short-
|
|
// circuits at the first differing byte, which leaks information via timing.
|
|
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("generated a new token and saved it to token.txt")
|
|
}
|
|
log.Printf("connection token: %s", token)
|
|
log.Printf("your partner needs to put THAT exact token in their config.toml; without it, their connection gets rejected")
|
|
}
|