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 }