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_" // WorkspaceAdminPrefix steht vor jedem Token eines Workspace-Admins // (AUSBAUSTUFEN.md 4.8). // // Das eigene Präfix ist mehr als Kosmetik: APIAuth lässt nur zu, was // LooksLikeKey erkennt. Ein Workspace-Admin-Token scheitert dort schon an der // Form und kommt so nie an die MCP-Schnittstelle — der Zugang ist ausdrücklich // nur für die Weboberfläche gedacht. const WorkspaceAdminPrefix = "wa_" // 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) { return generateWithPrefix(KeyPrefix) } // GenerateWorkspaceAdminToken erzeugt das Token eines Workspace-Admins im // Klartext. Aufbau und Stärke wie bei GenerateKey, nur mit eigenem Präfix — // siehe WorkspaceAdminPrefix, warum das wichtig ist. func GenerateWorkspaceAdminToken() (string, error) { return generateWithPrefix(WorkspaceAdminPrefix) } // generateWithPrefix erzeugt keyBytes Zufallsbytes und stellt das Präfix davor. func generateWithPrefix(prefix string) (string, error) { buf := make([]byte, keyBytes) if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("Zufallszahlen konnten nicht erzeugt werden: %w", err) } return prefix + 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 { return looksLike(key, KeyPrefix) } // LooksLikeWorkspaceAdminToken prüft die äußere Form eines // Workspace-Admin-Tokens — Gegenstück zu LooksLikeKey. func LooksLikeWorkspaceAdminToken(token string) bool { return looksLike(token, WorkspaceAdminPrefix) } // looksLike prüft Präfix, Länge und Hex-Form. func looksLike(key, prefix string) bool { if !strings.HasPrefix(key, prefix) { return false } rest := key[len(prefix):] if len(rest) != keyBytes*2 { return false } _, err := hex.DecodeString(rest) return err == nil }