From 2a9faab3de7648344dcc1973a71ada8125c0afa3 Mon Sep 17 00:00:00 2001 From: Rose Hogenson Date: Sat, 27 Dec 2025 13:34:56 -0800 Subject: Add Google Authenticator --- assets/point.xcf | Bin 0 -> 14646 bytes internal/config/config.go | 7 +++++++ roseh.moe.go | 42 ++++++++++++++++++++++++++++++---------- static/point.webp | Bin 0 -> 6078 bytes static/styles.css | 19 +++++++++++++++++- templates/login.html.template | 15 ++++++++++++-- tools/auth/auth.go | 44 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 114 insertions(+), 13 deletions(-) create mode 100644 assets/point.xcf create mode 100644 internal/config/config.go create mode 100644 static/point.webp create mode 100644 tools/auth/auth.go diff --git a/assets/point.xcf b/assets/point.xcf new file mode 100644 index 0000000..80e0ed5 Binary files /dev/null and b/assets/point.xcf differ diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..d404e12 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,7 @@ +package config + +type SecretsConfig struct { + NotepadPassword []byte + SecretKey []byte + AuthKey []byte +} diff --git a/roseh.moe.go b/roseh.moe.go index 6fd4f11..993282a 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -4,6 +4,7 @@ import ( "context" "crypto/hmac" "crypto/rand" + "crypto/sha1" "crypto/sha256" "crypto/subtle" "embed" @@ -19,12 +20,14 @@ import ( "net/http/httputil" "net/url" "os" + "strconv" "strings" "sync" "time" "github.com/skip2/go-qrcode" "roseh.moe/pkg/ccl" + "roseh.moe/pkg/roseh.moe/internal/config" "roseh.moe/pkg/roseh.moe/internal/pwhash" "roseh.moe/pkg/wordlist" ) @@ -40,28 +43,29 @@ var ( serverStartTime = time.Now() ) -var notepadPassword, secretKey []byte +var notepadPassword, secretKey, authenticatorKey []byte func loadSecrets() error { fileBytes, err := os.ReadFile(*secretsFile) if err != nil { return err } - var secretsConfig struct { - NotepadPassword []byte - SecretKey []byte - } + var secretsConfig config.SecretsConfig if err := ccl.Unmarshal(fileBytes, &secretsConfig); err != nil { return fmt.Errorf("%s: %s", *secretsFile, err) } if len(secretsConfig.NotepadPassword) == 0 { - return fmt.Errorf("%s: missing notepad-password", *secretsFile) + return fmt.Errorf("%s: missing NotepadPassword", *secretsFile) } if len(secretsConfig.SecretKey) == 0 { - return fmt.Errorf("%s: missing secret-key", *secretsFile) + return fmt.Errorf("%s: missing SecretKey", *secretsFile) + } + if len(secretsConfig.AuthKey) == 0 { + return fmt.Errorf("%s: missing AuthKey", *secretsFile) } notepadPassword = secretsConfig.NotepadPassword secretKey = secretsConfig.SecretKey + authenticatorKey = secretsConfig.AuthKey return nil } @@ -420,9 +424,27 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) bool { return true } -func checkPassword(password string) bool { +func totp(key []byte) []byte { + ts := make([]byte, 8) + binary.BigEndian.PutUint64(ts, uint64(time.Now().Unix()/30)) + hash := hmac.New(sha1.New, key) + hash.Write(ts) + mac := hash.Sum(nil) + offset := mac[len(mac)-1] & 0xf + n := int32(mac[offset])&0x7f<<24 | + int32(mac[offset+1])<<16 | + int32(mac[offset+2])<<8 | + int32(mac[offset+3]) + return strconv.AppendInt(nil, int64(n%1_000_000), 10) +} + +func checkPassword(password, auth string) bool { + otp := totp(authenticatorKey) hash, err := pwhash.Hash(password, notepadPassword[:pwhash.SaltSize]) - return err == nil && subtle.ConstantTimeCompare(hash, notepadPassword[pwhash.SaltSize:]) != 0 + if err != nil { + return false + } + return subtle.ConstantTimeCompare(hash, notepadPassword[pwhash.SaltSize:])&subtle.ConstantTimeCompare([]byte(auth), otp) != 0 } var ( @@ -464,7 +486,7 @@ func verifyRedirect(redirect string) (string, bool) { } func login(w http.ResponseWriter, r *http.Request) { - if !checkPassword(r.FormValue("password")) { + if !checkPassword(r.FormValue("password"), r.FormValue("auth")) { if err := loginTemplate.Execute(w, loginTemplateArgs{ Redirect: r.FormValue("redirect"), Error: true, diff --git a/static/point.webp b/static/point.webp new file mode 100644 index 0000000..501e647 Binary files /dev/null and b/static/point.webp differ diff --git a/static/styles.css b/static/styles.css index f147632..d214480 100644 --- a/static/styles.css +++ b/static/styles.css @@ -76,11 +76,16 @@ p.login-error { color: red; margin-top: 0px; } - +/* form.password-form { display: flex; align-items: center; } +*/ + +.password-form-row { + text-align: end; +} label.password-label { padding-right: 20px; @@ -92,4 +97,16 @@ input.password { border: 5px solid #ff69b4; border-radius: 15px; box-shadow: 0 0 10px rgba(255, 105, 180, 0.5); + font-size: 18px; +} + +input.submit { + margin-top: 20px; + padding: 0; + background: #f5f5f5; + outline: none; + border: 5px solid #ff69b4; + border-radius: 15px; + box-shadow: 0 0 10px rgba(255, 105, 180, 0.5); + font-size: 25px; } diff --git a/templates/login.html.template b/templates/login.html.template index 8d1ec9b..8bdc613 100644 --- a/templates/login.html.template +++ b/templates/login.html.template @@ -6,7 +6,18 @@ {{end}}
- - + + + + + + + + + + + + +
diff --git a/tools/auth/auth.go b/tools/auth/auth.go new file mode 100644 index 0000000..3c4f063 --- /dev/null +++ b/tools/auth/auth.go @@ -0,0 +1,44 @@ +package main + +import ( + "encoding/base32" + "flag" + "fmt" + "os" + + "github.com/skip2/go-qrcode" + "roseh.moe/pkg/ccl" + "roseh.moe/pkg/roseh.moe/internal/config" +) + +func run() error { + args := flag.Args() + if len(args) != 1 { + return fmt.Errorf("usage: auth ") + } + fileBytes, err := os.ReadFile(args[0]) + if err != nil { + return err + } + var config config.SecretsConfig + if err := ccl.Unmarshal(fileBytes, &config); err != nil { + return err + } + if len(config.AuthKey) == 0 { + return fmt.Errorf("%s: missing AuthKey", args[0]) + } + qrCode, err := qrcode.New(fmt.Sprintf("otpauth://totp/roseh.moe:rose?secret=%s&issuer=roseh.moe", base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(config.AuthKey)), qrcode.Medium) + if err != nil { + return err + } + fmt.Print(qrCode.ToSmallString(false)) + return nil +} + +func main() { + flag.Parse() + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} -- cgit v1.3.1