summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-10-20 21:02:18 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-10-20 21:09:43 -0700
commitb678e28d4acaecb234831f39f8000912ed2d7200 (patch)
tree53f3c402f0bde0f5b037efca94f31c2176127efc
parentAdd HSTS header (diff)
downloadroseh.moe-b678e28d4acaecb234831f39f8000912ed2d7200.tar.zst
Some small simplifications
-rw-r--r--roseh.moe.go173
-rw-r--r--templates/login.html.template24
-rw-r--r--templates/register/register-success.html.template2
-rw-r--r--templates/register/register.html.template51
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> &lt;Passkey ID="{{.ID | xmlEscape}}"
- Key="{{.Key | xmlEscape}}"/&gt;</pre>
+ Key="{{.Key}}"/&gt;</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>