diff options
| -rw-r--r-- | assets/point.xcf | bin | 0 -> 14646 bytes | |||
| -rw-r--r-- | internal/config/config.go | 7 | ||||
| -rw-r--r-- | roseh.moe.go | 42 | ||||
| -rw-r--r-- | static/point.webp | bin | 0 -> 6078 bytes | |||
| -rw-r--r-- | static/styles.css | 19 | ||||
| -rw-r--r-- | templates/login.html.template | 15 | ||||
| -rw-r--r-- | tools/auth/auth.go | 44 |
7 files changed, 114 insertions, 13 deletions
diff --git a/assets/point.xcf b/assets/point.xcf Binary files differnew file mode 100644 index 0000000..80e0ed5 --- /dev/null +++ b/assets/point.xcf 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 Binary files differnew file mode 100644 index 0000000..501e647 --- /dev/null +++ b/static/point.webp 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}} <form class="password-form" action="/login" method="post"> <input type="hidden" name="redirect" value="{{.Redirect}}"> - <label class="password-label" for="password">Enter password</label> - <input id="password" class="password" type="password" name="password" autofocus> + <table> + <tr class="password-form-row"> + <td><label class="password-label" for="password">Enter password</label></td> + <td><input id="password" class="password" type="password" name="password" autofocus></td> + </tr> + <tr class="password-form-row"> + <td><label class="password-label" for="auth">Google authenticator code</label></td> + <td><input id="auth" class="password" name="auth"></td> + </tr> + <tr> + <td></td> + <td><input type="image" class="submit" src="/static/point.webp" alt="Submit"></td> + </table> </form> </section> 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 <config file>") + } + 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) + } +} |
