diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-20 21:02:18 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-20 21:09:43 -0700 |
| commit | b678e28d4acaecb234831f39f8000912ed2d7200 (patch) | |
| tree | 53f3c402f0bde0f5b037efca94f31c2176127efc /roseh.moe.go | |
| parent | Add HSTS header (diff) | |
| download | roseh.moe-b678e28d4acaecb234831f39f8000912ed2d7200.tar.zst | |
Some small simplifications
Diffstat (limited to 'roseh.moe.go')
| -rw-r--r-- | roseh.moe.go | 173 |
1 files changed, 106 insertions, 67 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index bd07aed..3b9a54d 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -26,7 +26,6 @@ import ( "context" "crypto" "crypto/ecdsa" - "crypto/ed25519" "crypto/hkdf" "crypto/hmac" "crypto/rand" @@ -123,10 +122,6 @@ func sigVerifier(keyBytes []byte) (func(msg, sig []byte) bool, error) { hash := sha256.Sum256(msg) return rsa.VerifyPKCS1v15(key, crypto.SHA256, hash[:], sig) == nil }, nil - case ed25519.PublicKey: - return func(msg, sig []byte) bool { - return ed25519.Verify(key, msg, sig) - }, nil default: return nil, fmt.Errorf("unsupported key type") } @@ -228,7 +223,12 @@ func loadConfig() (*serviceConfiguration, error) { return nil, fmt.Errorf("%s: duplicate name %q", *configFile, passkey.ID) } names[passkey.ID] = true - keyBytes, err := base64.RawURLEncoding.DecodeString(passkey.Key) + keyBytes, err := base64.RawURLEncoding.DecodeString(strings.Map(func(r rune) rune { + if unicode.IsSpace(r) { + return -1 + } + return r + }, passkey.Key)) if err != nil { return nil, fmt.Errorf("%s: invalid key for passkey %q: %s", *configFile, passkey.ID, err) } @@ -842,10 +842,27 @@ func cookieAuth(r *http.Request) bool { return true } +func verifyRedirect(redirect string) bool { + u, err := url.Parse(redirect) + if err != nil { + return false + } + switch u.Host { + case + "cinema." + selfURL.Host, + "downloads." + selfURL.Host, + selfURL.Host: + + return true + } + return false +} + +const challengeExpiry = 10 * time.Minute + var ( - challengeMu sync.Mutex - challenge []byte - challengePosed time.Time + challengeMu sync.Mutex + challenges = make(map[string]time.Time) ) var ( @@ -860,8 +877,8 @@ type loginTemplateArgs struct { func serveLogin(w http.ResponseWriter, r *http.Request) { if cookieAuth(r) { - redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect") - if !ok { + redirect := r.FormValue("redirect") + if !verifyRedirect(redirect) { redirect = "/" } http.Redirect(w, r, redirect, http.StatusSeeOther) @@ -869,9 +886,14 @@ func serveLogin(w http.ResponseWriter, r *http.Request) { } buf := make([]byte, 32) rand.Read(buf) + now := time.Now() challengeMu.Lock() - challenge = buf - challengePosed = time.Now() + if len(challenges) >= 100 { + challengeMu.Unlock() + http.Error(w, "Too many active requests", http.StatusTooManyRequests) + return + } + challenges[string(buf)] = now challengeMu.Unlock() if err := loginTemplate.Execute(w, loginTemplateArgs{ Challenge: base64.StdEncoding.EncodeToString(buf), @@ -881,19 +903,6 @@ func serveLogin(w http.ResponseWriter, r *http.Request) { } func login(w http.ResponseWriter, r *http.Request) { - challengeMu.Lock() - myChallenge := challenge - challenge = nil - posed := challengePosed - challengeMu.Unlock() - if len(myChallenge) == 0 { - http.Error(w, "Request already consumed", http.StatusConflict) - return - } - if time.Since(posed) > 10*time.Minute { - http.Error(w, "Challenge is too old. Please try again", http.StatusConflict) - return - } user, err := base64.StdEncoding.DecodeString(r.FormValue("user")) if err != nil { http.Error(w, "Bad user", http.StatusBadRequest) @@ -909,27 +918,6 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Bad client data: invalid base64", http.StatusBadRequest) return } - var cd struct { - Type string `json:"type"` - Challenge string `json:"challenge"` - Origin string `json:"origin"` - CrossOrigin bool `json:"crossOrigin"` - } - if err := json.Unmarshal(clientData, &cd); err != nil { - http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest) - return - } - challengeBase64 := base64.RawURLEncoding.EncodeToString(myChallenge) - if cd.Type != "webauthn.get" || cd.Challenge != challengeBase64 || cd.Origin != selfURL.String() || cd.CrossOrigin { - http.Error(w, "Bad client data", http.StatusBadRequest) - return - } - expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname())) - const flagUserPresence, flagUserVerification = 0x01, 0x04 - if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) { - http.Error(w, "Bad authenticator data", http.StatusBadRequest) - return - } sig, err := base64.StdEncoding.DecodeString(r.FormValue("sig")) if err != nil { http.Error(w, "Bad sig", http.StatusBadRequest) @@ -951,12 +939,49 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "Challenge failed", http.StatusBadRequest) return } + var cd struct { + Type string `json:"type"` + Challenge string `json:"challenge"` + Origin string `json:"origin"` + CrossOrigin bool `json:"crossOrigin"` + } + if err := json.Unmarshal(clientData, &cd); err != nil { + http.Error(w, "Bad client data: invalid JSON", http.StatusBadRequest) + return + } + challenge, err := base64.RawURLEncoding.DecodeString(cd.Challenge) + if err != nil { + http.Error(w, "Bad client data: bad challenge", http.StatusBadRequest) + return + } + challengeMu.Lock() + initiated, ok := challenges[string(challenge)] + delete(challenges, string(challenge)) + challengeMu.Unlock() + if !ok { + http.Error(w, "Request already consumed", http.StatusConflict) + return + } + if time.Since(initiated) > challengeExpiry { + http.Error(w, "Challenge is too old. Please try again", http.StatusConflict) + return + } + if cd.Type != "webauthn.get" || cd.Origin != selfURL.String() || cd.CrossOrigin { + http.Error(w, "Bad client data", http.StatusBadRequest) + return + } + expectedRPHash := sha256.Sum256([]byte(selfURL.Hostname())) + const flagUserPresence, flagUserVerification = 0x01, 0x04 + if len(authData) < 37 || !bytes.Equal(authData[:32], expectedRPHash[:]) || authData[32]&(flagUserPresence|flagUserVerification) != (flagUserPresence|flagUserVerification) { + http.Error(w, "Bad authenticator data", http.StatusBadRequest) + return + } attachCookie(w, &authToken{ expiry: time.Now().Add(30 * 24 * time.Hour), userID: user, }) - redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect") - if !ok { + redirect := r.FormValue("redirect") + if !verifyRedirect(redirect) { redirect = "/" } http.Redirect(w, r, redirect, http.StatusSeeOther) @@ -985,6 +1010,19 @@ func serveRegister(w http.ResponseWriter, r *http.Request) { } } +func wrap(s string) string { + out := new(strings.Builder) + for s != "" { + if out.Len() > 0 { + out.WriteString("\n\t ") + } + n := min(50, len(s)) + out.WriteString(s[:n]) + s = s[n:] + } + return out.String() +} + var ( //go:embed templates/register/register-success.html.template registerSuccessTemplateString string @@ -1014,7 +1052,7 @@ func register(w http.ResponseWriter, r *http.Request) { } if err := registerSuccessTemplate.Execute(w, registerSuccessTemplateArgs{ ID: r.FormValue("name"), - Key: base64.RawURLEncoding.EncodeToString(slices.Concat(userID, key)), + Key: wrap(base64.RawURLEncoding.EncodeToString(slices.Concat(userID, key))), }); err != nil { log.Printf("Warning: register: %s", err) } @@ -1344,21 +1382,9 @@ func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "not logged in", http.StatusUnauthorized) } else { u := *r.URL - switch r.Host { - case - "cinema." + selfURL.Host, - "downloads." + selfURL.Host: - - u.Host = r.Host - default: - u.Host = selfURL.Host - } - u.Scheme = "http" - if *https != "" { - u.Scheme = "https" - } - redirect := signMsg(u.String(), "redirect") - http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(redirect), http.StatusSeeOther) + u.Host = r.Host + u.Scheme = selfURL.Scheme + http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(u.String()), http.StatusSeeOther) } return } @@ -1414,7 +1440,7 @@ func (h *hstsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - /*var cookies []*http.Cookie + var cookies []*http.Cookie for _, c := range r.Cookies() { if c.Name != authCookieName { cookies = append(cookies, c) @@ -1423,7 +1449,7 @@ func (h *removeAuthCookieHandler) ServeHTTP(w http.ResponseWriter, r *http.Reque r.Header.Del("Cookie") for _, c := range cookies { r.AddCookie(c) - }*/ + } h.h.ServeHTTP(w, r) } @@ -1446,6 +1472,19 @@ func main() { } }() + go func() { + for now := range time.NewTicker(time.Hour).C { + log.Printf("Cleaning expired challenges") + challengeMu.Lock() + for challenge, initiated := range challenges { + if now.Sub(initiated) > challengeExpiry { + delete(challenges, challenge) + } + } + challengeMu.Unlock() + } + }() + mux := http.NewServeMux() indexTemplate, err := outlinedTemplate(indexString) if err != nil { |
