munin/internal/auth/keys.go
2026-09-15 20:36:26 +02:00

110 lines
3.6 KiB
Go

package auth
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
)
// KeyPrefix steht vor jedem API-Key.
//
// Zwei Gründe dafür:
// - Man erkennt auf einen Blick, worum es sich handelt, wenn ein Key irgendwo
// in einer Konfigurationsdatei oder einem Chat auftaucht.
// - Werkzeuge zum Aufspüren versehentlich veröffentlichter Secrets (GitHub
// Secret Scanning und ähnliche) suchen nach genau solchen Präfixen.
const KeyPrefix = "wk_"
// keyBytes ist die Länge des Zufallsanteils eines Keys.
// 32 Byte = 256 Bit — weit jenseits von allem, was sich durchprobieren lässt.
const keyBytes = 32
// Role beschreibt, was ein Key darf.
//
// Bewusst nur drei Stufen: mehr Feingranularität kostet Verständlichkeit und
// wird bei einem internen Werkzeug erfahrungsgemäß nie genutzt.
type Role string
const (
// RoleRead darf lesen und suchen — für Agenten, die nur Wissen abrufen sollen.
RoleRead Role = "read"
// RoleWrite darf zusätzlich Einträge anlegen, ändern und löschen.
RoleWrite Role = "write"
// RoleAdmin darf zusätzlich die Keys des eigenen Projekts verwalten.
RoleAdmin Role = "admin"
)
// Valid prüft, ob eine Rolle einen der bekannten Werte hat.
// Wichtig beim Lesen aus der Datenbank: dort steht Text, und Text kann alles sein.
func (r Role) Valid() bool {
switch r {
case RoleRead, RoleWrite, RoleAdmin:
return true
default:
return false
}
}
// CanWrite sagt, ob diese Rolle Einträge verändern darf.
func (r Role) CanWrite() bool {
return r == RoleWrite || r == RoleAdmin
}
// CanManageKeys sagt, ob diese Rolle die Keys des Projekts verwalten darf.
func (r Role) CanManageKeys() bool {
return r == RoleAdmin
}
// GenerateKey erzeugt einen neuen API-Key im Klartext.
//
// Der Klartext existiert nur genau einmal — direkt nach dem Erzeugen wird er dem
// Nutzer angezeigt und danach nirgends gespeichert (siehe HashKey).
func GenerateKey() (string, error) {
buf := make([]byte, keyBytes)
if _, err := rand.Read(buf); err != nil {
return "", fmt.Errorf("Zufallszahlen konnten nicht erzeugt werden: %w", err)
}
return KeyPrefix + hex.EncodeToString(buf), nil
}
// HashKey bildet den Speicher-Hash eines API-Keys.
//
// # Warum SHA-256 und nicht bcrypt/Argon2?
//
// Für Passwörter wären langsame Hashverfahren richtig: Menschen wählen Passwörter
// aus einem kleinen, erratbaren Raum, und die künstliche Langsamkeit macht das
// Durchprobieren teuer.
//
// Ein API-Key hier ist aber kein Passwort, sondern 256 Bit aus crypto/rand. Diesen
// Raum kann niemand durchsuchen — auch nicht mit beliebig schneller Hardware. Die
// Langsamkeit brächte also keinen Sicherheitsgewinn, würde aber jeden einzelnen
// Request ausbremsen.
//
// Entscheidend ist der zweite Punkt: Weil SHA-256 schnell und deterministisch ist,
// kann der Hash direkt als indizierte Spalte durchsucht werden — ein Lookup statt
// einer Schleife über alle Keys. Die alte Lösung verglich den Key nacheinander mit
// jedem Team; das skaliert nicht und wurde mit jedem neuen Projekt langsamer.
func HashKey(key string) string {
sum := sha256.Sum256([]byte(key))
return hex.EncodeToString(sum[:])
}
// LooksLikeKey prüft die äußere Form eines Keys, bevor die Datenbank befragt wird.
//
// Das ist keine Sicherheitsprüfung, sondern spart einen Datenbankzugriff bei
// offensichtlichem Unsinn (leerer Header, abgeschnittener Key, Tippfehler).
func LooksLikeKey(key string) bool {
if !strings.HasPrefix(key, KeyPrefix) {
return false
}
rest := key[len(KeyPrefix):]
if len(rest) != keyBytes*2 {
return false
}
_, err := hex.DecodeString(rest)
return err == nil
}