summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2025-12-27 13:34:56 -0800
committerRose Hogenson <rosehogenson@posteo.net>2025-12-27 13:37:47 -0800
commit2a9faab3de7648344dcc1973a71ada8125c0afa3 (patch)
treea6fd29a8d4ea62aaf17fdf214535dbdbb1c00d50
parent26235f94f87b01058cc06e69902f7fb30bffb22f (diff)
downloadroseh.moe-2a9faab3de7648344dcc1973a71ada8125c0afa3.tar.zst
Add Google Authenticator
-rw-r--r--assets/point.xcfbin0 -> 14646 bytes
-rw-r--r--internal/config/config.go7
-rw-r--r--roseh.moe.go42
-rw-r--r--static/point.webpbin0 -> 6078 bytes
-rw-r--r--static/styles.css19
-rw-r--r--templates/login.html.template15
-rw-r--r--tools/auth/auth.go44
7 files changed, 114 insertions, 13 deletions
diff --git a/assets/point.xcf b/assets/point.xcf
new file mode 100644
index 0000000..80e0ed5
--- /dev/null
+++ b/assets/point.xcf
Binary files 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
--- /dev/null
+++ b/static/point.webp
Binary files 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}}
<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)
+ }
+}