diff options
| -rw-r--r-- | roseh.moe.go | 173 | ||||
| -rw-r--r-- | templates/login.html.template | 24 | ||||
| -rw-r--r-- | templates/register/register-success.html.template | 2 | ||||
| -rw-r--r-- | templates/register/register.html.template | 51 |
4 files changed, 144 insertions, 106 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 { diff --git a/templates/login.html.template b/templates/login.html.template index ae0874a..2a498a5 100644 --- a/templates/login.html.template +++ b/templates/login.html.template @@ -27,19 +27,19 @@ </form> <script> -async function main() { - const publicKeyCredential = await navigator.credentials.get({publicKey: { - challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer, - }}); - const rsp = publicKeyCredential.response; - document.getElementById("user").value = new Uint8Array(rsp.userHandle).toBase64(); - document.getElementById("auth-data").value = new Uint8Array(rsp.authenticatorData).toBase64(); - document.getElementById("client-data").value = new Uint8Array(rsp.clientDataJSON).toBase64(); - document.getElementById("sig").value = new Uint8Array(rsp.signature).toBase64(); - document.getElementById("form").submit(); -} + async function main() { + const publicKeyCredential = await navigator.credentials.get({publicKey: { + challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer, + }}); + const rsp = publicKeyCredential.response; + document.getElementById("user").value = new Uint8Array(rsp.userHandle).toBase64(); + document.getElementById("auth-data").value = new Uint8Array(rsp.authenticatorData).toBase64(); + document.getElementById("client-data").value = new Uint8Array(rsp.clientDataJSON).toBase64(); + document.getElementById("sig").value = new Uint8Array(rsp.signature).toBase64(); + document.getElementById("form").submit(); + } -main(); + main(); </script> This page requires JavaScriptâ„¢ to be enabled diff --git a/templates/register/register-success.html.template b/templates/register/register-success.html.template index 847f97a..d9c64e1 100644 --- a/templates/register/register-success.html.template +++ b/templates/register/register-success.html.template @@ -24,5 +24,5 @@ <p>A device key has been generated, but it must be authorized on the server:</p> <pre> <Passkey ID="{{.ID | xmlEscape}}" - Key="{{.Key | xmlEscape}}"/></pre> + Key="{{.Key}}"/></pre> </section> diff --git a/templates/register/register.html.template b/templates/register/register.html.template index 833a1a7..d97b585 100644 --- a/templates/register/register.html.template +++ b/templates/register/register.html.template @@ -37,30 +37,29 @@ </section> <script> -async function generateKey() { - const name = document.getElementById("name").value; - const publicKeyCredential = await navigator.credentials.create({publicKey: { - authenticatorSelection: { - residentKey: "required", - userVerification: "required", - }, - challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer, - pubKeyCredParams: [ - {alg: -9, type: "public-key"}, - {alg: -257, type: "public-key"}, - {alg: -7, type: "public-key"}, - {alg: -8, type: "public-key"}, - ], - rp: { - name: "roseh.moe", - }, - user: { - displayName: name, - id: Uint8Array.fromBase64(document.getElementById("id").value).buffer, - name: name, - }, - }}); - document.getElementById("key").value = new Uint8Array(publicKeyCredential.response.getPublicKey()).toBase64(); - document.getElementById("form").submit(); -} + async function generateKey() { + const name = document.getElementById("name").value; + const publicKeyCredential = await navigator.credentials.create({publicKey: { + authenticatorSelection: { + residentKey: "required", + userVerification: "required", + }, + challenge: Uint8Array.fromBase64("{{.Challenge}}").buffer, + pubKeyCredParams: [ + {alg: -9, type: "public-key"}, + {alg: -257, type: "public-key"}, + {alg: -7, type: "public-key"}, + ], + rp: { + name: "roseh.moe", + }, + user: { + displayName: name, + id: Uint8Array.fromBase64(document.getElementById("id").value).buffer, + name: name, + }, + }}); + document.getElementById("key").value = new Uint8Array(publicKeyCredential.response.getPublicKey()).toBase64(); + document.getElementById("form").submit(); + } </script> |
