summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-10-16 18:08:14 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-10-16 20:08:24 -0700
commit8bdf9024e0274e627d0fa62d4caedc1ce29481c4 (patch)
treef670ac3c950e283b848a6497945c1546d0ef1c62
parentServe files from a certain directory on root (diff)
downloadroseh.moe-8bdf9024e0274e627d0fa62d4caedc1ce29481c4.tar.zst
Require authenticator code every 24 hours
-rw-r--r--roseh.moe.go133
-rw-r--r--templates/login.html.template2
2 files changed, 66 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 {
diff --git a/templates/login.html.template b/templates/login.html.template
index a64f3c6..43f91fd 100644
--- a/templates/login.html.template
+++ b/templates/login.html.template
@@ -25,9 +25,11 @@
{{- end}}
<form class="password-form" method="post">
<table>
+ {{- if not .Quick}}
<tr class="password-form-row">
<td><label class="password-label" for="password">Enter password</label>
<td><input id="password" class="password" type="password" name="password" autofocus>
+ {{- end}}
<tr class="password-form-row">
<td><label class="password-label" for="auth">Google authenticator code</label>
<td><input id="auth" class="password" name="auth">