diff options
| author | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-16 18:08:14 -0700 |
|---|---|---|
| committer | Rose Hogenson <rosehogenson@posteo.net> | 2026-10-16 20:08:24 -0700 |
| commit | 8bdf9024e0274e627d0fa62d4caedc1ce29481c4 (patch) | |
| tree | f670ac3c950e283b848a6497945c1546d0ef1c62 /roseh.moe.go | |
| parent | Serve files from a certain directory on root (diff) | |
| download | roseh.moe-8bdf9024e0274e627d0fa62d4caedc1ce29481c4.tar.zst | |
Require authenticator code every 24 hours
Diffstat (limited to 'roseh.moe.go')
| -rw-r--r-- | roseh.moe.go | 133 |
1 files changed, 64 insertions, 69 deletions
diff --git a/roseh.moe.go b/roseh.moe.go index 063a08a..180ce5f 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -347,13 +347,8 @@ func sharedFileHandler(w http.ResponseWriter, r *http.Request) { } func qrHandler(w http.ResponseWriter, r *http.Request) { - sig, err := base64.RawURLEncoding.DecodeString(r.FormValue("s")) - if err != nil { - http.Error(w, fmt.Sprintf("Bad signature: %s", err), http.StatusBadRequest) - return - } - text := r.FormValue("t") - if !verify([]byte(text), sig, "qr") { + text, ok := verify(r.FormValue("t"), "qr") + if !ok { http.Error(w, "Bad signature", http.StatusBadRequest) return } @@ -610,7 +605,7 @@ func wormholeUpload(w http.ResponseWriter, r *http.Request) { downloadURL := selfURL.String() + "/wormhole/" + hole if err := wormholeSuccessTemplate.Execute(w, wormholeSuccessTemplateArgs{ URL: downloadURL, - QR: "/qr.png?t=" + url.QueryEscape(downloadURL) + "&s=" + base64.RawURLEncoding.EncodeToString(mac([]byte(downloadURL), "qr")), + QR: "/qr.png?t=" + url.QueryEscape(downloadURL) + mac(downloadURL, "qr"), }); err != nil { log.Printf("Warning: wormholeSend: %s", err) } @@ -690,19 +685,25 @@ func cleanHole() error { } } -const macSize = sha256.Size - -func mac(msg []byte, purpose string) []byte { +func mac(msg string, purpose string) string { mac := hmac.New(sha256.New, secretKey) io.WriteString(mac, purpose) io.WriteString(mac, ":") - mac.Write(msg) - return mac.Sum(nil) + io.WriteString(mac, msg) + return base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } -func verify(msg, messageMAC []byte, purpose string) bool { +func verify(signedMsg, purpose string) (string, bool) { + macSize := base64.RawURLEncoding.EncodedLen(sha256.Size) + if len(signedMsg) < macSize { + return "", false + } + msg, msgMAC := signedMsg[:len(signedMsg)-macSize], signedMsg[len(signedMsg)-macSize:] expectedMAC := mac(msg, purpose) - return hmac.Equal(messageMAC, expectedMAC) + if !hmac.Equal([]byte(msgMAC), []byte(expectedMAC)) { + return "", false + } + return msg, true } func marshalInt(x int64) []byte { @@ -722,11 +723,10 @@ func unmarshalInt(b []byte) (int64, bool) { return int64(binary.BigEndian.Uint64(buf)), true } -const yearOffset = 2025 +var myUnixEpoch = time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) func marshalTime(t time.Time) []byte { - year, month, day := t.UTC().Date() - return marshalInt((int64(year)-yearOffset)<<9 | int64(month)<<5 | int64(day)) + return marshalInt(int64(t.Sub(myUnixEpoch) / time.Second)) } func unmarshalTime(b []byte) (time.Time, bool) { @@ -734,15 +734,14 @@ func unmarshalTime(b []byte) (time.Time, bool) { if !ok { return time.Time{}, false } - return time.Date(int(n>>9+yearOffset), time.Month(n>>5&0xf), int(n&0x1f), 0, 0, 0, 0, time.UTC), true + return myUnixEpoch.Add(time.Duration(n) * time.Second), true } func makeToken() string { - t := marshalTime(time.Now()) - return base64.RawURLEncoding.EncodeToString(append(t, mac(t, "auth")...)) + t := base64.RawURLEncoding.EncodeToString(marshalTime(time.Now())) + return t + mac(t, "auth") } -const cookieExpiration = 300 * 24 * time.Hour const authCookieName = "a" func attachCookie(w http.ResponseWriter) { @@ -751,7 +750,7 @@ func attachCookie(w http.ResponseWriter) { Value: makeToken(), Path: "/", Domain: selfURL.Hostname(), - Expires: time.Now().Add(cookieExpiration), + Expires: time.Now().Add(longLoginTimeout), Secure: true, HttpOnly: true, SameSite: http.SameSiteStrictMode, @@ -759,34 +758,28 @@ func attachCookie(w http.ResponseWriter) { }) } -func cookieAuth(w http.ResponseWriter, r *http.Request) bool { +func cookieAuth(r *http.Request) (time.Duration, bool) { cookie, err := r.Cookie(authCookieName) if err != nil { - return false - } - authCookie, err := base64.RawURLEncoding.DecodeString(cookie.Value) - if err != nil { - return false + return 0, false } - if len(authCookie) < macSize { - return false + msg, ok := verify(cookie.Value, "auth") + if !ok { + return 0, false } - msg, mac := authCookie[:len(authCookie)-macSize], authCookie[len(authCookie)-macSize:] - if !verify(msg, mac, "auth") { - return false + msgBytes, err := base64.RawURLEncoding.DecodeString(msg) + if err != nil { + return 0, false } - t, ok := unmarshalTime(msg) + t, ok := unmarshalTime(msgBytes) if !ok { - return false - } - cookieAge := time.Since(t) - if cookieAge > cookieExpiration { - return false + return 0, false } - if cookieAge > 24*time.Hour { - attachCookie(w) + age := time.Since(t) + if age > longLoginTimeout { + return 0, false } - return true + return age, true } func totp(t int64) []byte { @@ -808,13 +801,12 @@ func checkOTP(auth string) int { return subtle.ConstantTimeCompare([]byte(auth), totp(now)) | subtle.ConstantTimeCompare([]byte(auth), totp(now-1)) } -func checkPassword(password, auth string) bool { - otpMatches := checkOTP(auth) +func checkPassword(password string) int { hash, err := pwhash.Hash(password, notepadPassword[:pwhash.SaltSize]) if err != nil { - return false + return 0 } - return subtle.ConstantTimeCompare(hash, notepadPassword[pwhash.SaltSize:])&otpMatches != 0 + return subtle.ConstantTimeCompare(hash, notepadPassword[pwhash.SaltSize:]) } var ( @@ -824,45 +816,47 @@ var ( ) type loginTemplateArgs struct { + Quick bool Error bool } func redirectLogin(w http.ResponseWriter, r *http.Request, redirect string) { - redirect += base64.RawURLEncoding.EncodeToString(mac([]byte(redirect), "redirect")) + redirect += mac(redirect, "redirect") http.Redirect(w, r, "//"+selfURL.Host+"/login?redirect="+url.QueryEscape(redirect), http.StatusSeeOther) } +const ( + quickLoginTimeout = 24 * time.Hour + longLoginTimeout = 300 * 24 * time.Hour +) + func serveLogin(w http.ResponseWriter, r *http.Request) { - if err := loginTemplate.Execute(w, loginTemplateArgs{}); err != nil { + _, cookieOK := cookieAuth(r) + if err := loginTemplate.Execute(w, loginTemplateArgs{ + Quick: cookieOK, + }); err != nil { log.Printf("Warning: login: %s", err) } } -func verifyRedirect(redirect string) (string, bool) { - base64MacSize := base64.RawURLEncoding.EncodedLen(macSize) - if len(redirect) < base64MacSize { - return "", false - } - redirect, macBase64 := redirect[:len(redirect)-base64MacSize], redirect[len(redirect)-base64MacSize:] - mac, err := base64.RawURLEncoding.DecodeString(macBase64) - if err != nil { - return "", false - } - if !verify([]byte(redirect), mac, "redirect") { - return "", false - } - return redirect, true -} - func login(w http.ResponseWriter, r *http.Request) { - if !checkPassword(r.FormValue("password"), r.FormValue("auth")) { - if err := loginTemplate.Execute(w, loginTemplateArgs{Error: true}); err != nil { + otpMatches := checkOTP(r.FormValue("auth")) + passwordMatches := 1 + _, cookieOK := cookieAuth(r) + if !cookieOK { + passwordMatches = checkPassword(r.FormValue("password")) + } + if otpMatches&passwordMatches == 0 { + if err := loginTemplate.Execute(w, loginTemplateArgs{ + Quick: cookieOK, + Error: true, + }); err != nil { log.Printf("Warning: login: %s", err) } return } attachCookie(w) - redirect, ok := verifyRedirect(r.FormValue("redirect")) + redirect, ok := verify(r.FormValue("redirect"), "redirect") if !ok { redirect = "/" } @@ -1119,7 +1113,8 @@ type authHandler struct { } func (h *authHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - if !cookieAuth(w, r) { + if age, ok := cookieAuth(r); !ok || age > quickLoginTimeout { + log.Printf("Warning: ok age = %v %v", ok, age) if h.noRedirect { http.Error(w, "not logged in", http.StatusUnauthorized) } else { |
