package crypto import ( "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "fmt" ) // tokenByteLength ist die Länge eines erzeugten Tokens in Byte. // // 32 Byte entsprechen 256 Bit Entropie. Ein Erraten ist damit auch bei // unbegrenzten Versuchen praktisch ausgeschlossen. const tokenByteLength = 32 // GenerateToken erzeugt ein kryptografisch sicheres Token. // // Die Ausgabe ist URL-sicher kodiert und kann damit unverändert in Headern // übertragen werden. func GenerateToken() (string, error) { tokenBytes := make([]byte, tokenByteLength) if _, randomError := rand.Read(tokenBytes); randomError != nil { return "", fmt.Errorf("es konnte kein sicheres token erzeugt werden: %w", randomError) } return base64.RawURLEncoding.EncodeToString(tokenBytes), nil } // HashToken bildet den Speicherwert eines Tokens. // // Gespeichert wird ausschließlich dieser Hash: ein Datenbankleck erlaubt damit // keine Übernahme laufender Sitzungen. Anders als bei Passwörtern genügt hier // SHA-256, weil ein Token bereits 256 Bit Zufall enthält und deshalb nicht // erraten werden kann — die Rechenkosten von Argon2id hätten keinen Nutzen. func HashToken(plainToken string) string { tokenDigest := sha256.Sum256([]byte(plainToken)) return hex.EncodeToString(tokenDigest[:]) } // TokensEqual vergleicht zwei Token-Hashes in konstanter Zeit. // // Ein zeichenweiser Vergleich würde über die Laufzeit verraten, wie viele // Zeichen bereits übereinstimmen. func TokensEqual(firstHash string, secondHash string) bool { return subtle.ConstantTimeCompare([]byte(firstHash), []byte(secondHash)) == 1 } // recoveryCodeByteLength ist die Länge eines Wiederherstellungscodes in Byte. // // 10 Byte ergeben 16 Zeichen in Base32 — kurz genug zum Abschreiben und mit // 80 Bit Entropie nicht zu erraten. const recoveryCodeByteLength = 10 // GenerateRecoveryCode erzeugt einen gut abschreibbaren Wiederherstellungscode. // // Verwendet wird ein Alphabet ohne leicht verwechselbare Zeichen (kein 0/O, // kein 1/I), da diese Codes typischerweise ausgedruckt und abgetippt werden. func GenerateRecoveryCode() (string, error) { // Das Alphabet enthält 32 Zeichen, sodass 5 Bit je Zeichen kodiert werden. const codeAlphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" randomBytes := make([]byte, recoveryCodeByteLength) if _, randomError := rand.Read(randomBytes); randomError != nil { return "", fmt.Errorf("es konnte kein sicherer wiederherstellungscode erzeugt werden: %w", randomError) } // Die Umwandlung nutzt jedes Byte modulo der Alphabetgröße. Da 256 kein // Vielfaches von 32 ist, entstünde eine minimale Ungleichverteilung - // bei 32 als Teiler von 256 ist sie jedoch exakt null. codeCharacters := make([]byte, len(randomBytes)) for byteIndex, randomByte := range randomBytes { codeCharacters[byteIndex] = codeAlphabet[int(randomByte)%len(codeAlphabet)] } // Die Gruppierung erleichtert das fehlerfreie Abtippen. return fmt.Sprintf("%s-%s", codeCharacters[:5], codeCharacters[5:]), nil }