142 lines
4.8 KiB
Go
142 lines
4.8 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_"
|
|
|
|
// 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
|
|
}
|