diff options
| -rw-r--r-- | go.mod | 4 | ||||
| -rw-r--r-- | go.sum | 4 | ||||
| -rw-r--r-- | internal/pwhash/pwhash.go | 44 | ||||
| -rw-r--r-- | roseh.moe.go | 89 | ||||
| -rw-r--r-- | templates/login.html.template | 1 | ||||
| -rw-r--r-- | tools/finditers/finditers.go | 42 | ||||
| -rw-r--r-- | tools/hashpw/hashpw.go | 32 | ||||
| -rw-r--r-- | tools/secret-key/secret-key.go | 14 |
8 files changed, 38 insertions, 192 deletions
@@ -1,7 +1,3 @@ module gitlab.com/rhogenson/roseh.moe go 1.24.0 - -require golang.org/x/term v0.35.0 - -require golang.org/x/sys v0.36.0 // indirect @@ -1,4 +0,0 @@ -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= diff --git a/internal/pwhash/pwhash.go b/internal/pwhash/pwhash.go deleted file mode 100644 index c3c11ff..0000000 --- a/internal/pwhash/pwhash.go +++ /dev/null @@ -1,44 +0,0 @@ -package pwhash - -import ( - "crypto/pbkdf2" - "crypto/rand" - "crypto/sha512" - "crypto/subtle" - "errors" -) - -const defaultIter = 12504615 // from tools/finditers - -func HashIter(pw string, salt []byte, iter int) ([]byte, error) { - return pbkdf2.Key(sha512.New, pw, salt, iter, 32) -} - -func Hash(pw string) ([]byte, error) { - buf := make([]byte, 40) - salt := buf[32:] - rand.Read(salt) - hash, err := HashIter(pw, salt, defaultIter) - if err != nil { - return nil, err - } - copy(buf, hash) - return buf, nil -} - -var errBadPassword = errors.New("bad password") - -func Check(pwHash []byte, pw string) error { - if len(pwHash) < 32 { - return errBadPassword - } - wantHash, salt := pwHash[:32], pwHash[32:] - gotHash, err := HashIter(pw, salt, defaultIter) - if err != nil { - return err - } - if subtle.ConstantTimeCompare(gotHash, wantHash) == 0 { - return errBadPassword - } - return nil -} diff --git a/roseh.moe.go b/roseh.moe.go index 77c78c1..9b5c1a6 100644 --- a/roseh.moe.go +++ b/roseh.moe.go @@ -9,6 +9,7 @@ import ( "embed" "encoding/base64" "encoding/binary" + "encoding/hex" "flag" "fmt" "html/template" @@ -19,8 +20,6 @@ import ( "strings" "sync" "time" - - "gitlab.com/rhogenson/roseh.moe/internal/pwhash" ) var ( @@ -40,12 +39,12 @@ func loadSecrets() error { } for _, line := range bytes.Split(bytes.TrimSuffix(secrets, []byte("\n")), []byte("\n")) { if pw, ok := bytes.CutPrefix(line, []byte("notepad-password=")); ok { - notepadPassword, err = base64.RawURLEncoding.AppendDecode(nil, pw) + notepadPassword, err = hex.AppendDecode(nil, pw) if err != nil { return err } } else if key, ok := bytes.CutPrefix(line, []byte("secret-key=")); ok { - secretKey, err = base64.RawURLEncoding.AppendDecode(nil, key) + secretKey, err = hex.AppendDecode(nil, key) if err != nil { return err } @@ -135,9 +134,10 @@ type wormholeTemplateArgs struct { } func wormhole(w http.ResponseWriter, r *http.Request) { - buf := make([]byte, 20) + const nWords = 10 + buf := make([]byte, 2*nWords) rand.Read(buf) - words := make([]string, 10) + words := make([]string, nWords) for i := range words { words[i] = wordList[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff] } @@ -247,24 +247,22 @@ func wormholeReady(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "event: ready\ndata:\n\n") } -func sign(msg []byte, info string) []byte { +func mac(msg []byte) []byte { mac := hmac.New(sha256.New, secretKey) - mac.Write(binary.AppendVarint(nil, int64(len(info)))) - io.WriteString(mac, info) mac.Write(msg) - return mac.Sum(msg) + return mac.Sum(nil) +} + +func sign(msg []byte) []byte { + return append(msg, mac(msg)...) } -func verify(msg []byte, info string) ([]byte, bool) { +func verify(msg []byte) ([]byte, bool) { if len(msg) < sha256.Size { return nil, false } msg, messageMAC := msg[:len(msg)-sha256.Size], msg[len(msg)-sha256.Size:] - mac := hmac.New(sha256.New, secretKey) - mac.Write(binary.AppendVarint(nil, int64(len(info)))) - io.WriteString(mac, info) - mac.Write(msg) - expectedMAC := mac.Sum(nil) + expectedMAC := mac(msg) if !hmac.Equal(messageMAC, expectedMAC) { return nil, false } @@ -276,27 +274,11 @@ func makeToken() (string, error) { if err != nil { return "", err } - return base64.RawURLEncoding.EncodeToString(sign(b, "auth")), nil + return base64.RawURLEncoding.EncodeToString(sign(b)), nil } const cookieExpiration = 180 * 24 * time.Hour -func checkToken(token string) bool { - authCookie, err := base64.RawURLEncoding.DecodeString(token) - if err != nil { - return false - } - msg, ok := verify(authCookie, "auth") - if !ok { - return false - } - var t time.Time - if err := t.UnmarshalBinary(msg); err != nil { - return false - } - return time.Since(t) < cookieExpiration -} - func attachCookie(w http.ResponseWriter) error { token, err := makeToken() if err != nil { @@ -339,10 +321,25 @@ func cookieAuth(w http.ResponseWriter, r *http.Request) (string, bool) { if err != nil { return csrfToken, false } - if !checkToken(cookie.Value) { + authCookie, err := base64.RawURLEncoding.DecodeString(cookie.Value) + if err != nil { return csrfToken, false } - attachCookie(w) + msg, ok := verify(authCookie) + if !ok { + return csrfToken, false + } + var t time.Time + if err := t.UnmarshalBinary(msg); err != nil { + return csrfToken, false + } + cookieAge := time.Since(t) + if cookieAge > cookieExpiration { + return csrfToken, false + } + if cookieAge > 24*time.Hour { + attachCookie(w) + } return csrfToken, true } @@ -367,15 +364,11 @@ var ( type loginTemplateArgs struct { CSRFToken string - Redirect string Error bool } -func executeLoginTemplate(w io.Writer, csrfToken, redirect string) { - if err := loginTemplate.Execute(w, loginTemplateArgs{ - Redirect: base64.RawURLEncoding.EncodeToString(sign([]byte(redirect), "redirect")), - CSRFToken: csrfToken, - }); err != nil { +func executeLoginTemplate(w io.Writer, csrfToken string) { + if err := loginTemplate.Execute(w, loginTemplateArgs{CSRFToken: csrfToken}); err != nil { log.Printf("Warning: login: %s", err) } } @@ -385,10 +378,10 @@ func login(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad CSRF token", http.StatusBadRequest) return } - if err := pwhash.Check(notepadPassword, r.FormValue("password")); err != nil { + hash := sha256.Sum256([]byte(r.FormValue("password"))) + if subtle.ConstantTimeCompare(notepadPassword, hash[:]) == 0 { if err := loginTemplate.Execute(w, loginTemplateArgs{ Error: true, - Redirect: r.FormValue("redirect"), CSRFToken: r.FormValue("csrf-token"), }); err != nil { log.Printf("Warning: login: %s", err) @@ -396,13 +389,7 @@ func login(w http.ResponseWriter, r *http.Request) { return } attachCookie(w) - redirect := "/" - if b, err := base64.RawURLEncoding.DecodeString(r.FormValue("redirect")); err == nil { - if r, ok := verify(b, "redirect"); ok { - redirect = string(r) - } - } - http.Redirect(w, r, redirect, http.StatusSeeOther) + http.Redirect(w, r, "/notepad", http.StatusSeeOther) } var ( @@ -422,7 +409,7 @@ type notepadTemplateArgs struct { func notepad(w http.ResponseWriter, r *http.Request) { csrfToken, ok := cookieAuth(w, r) if !ok { - executeLoginTemplate(w, csrfToken, "/notepad") + executeLoginTemplate(w, csrfToken) return } notepadContentsMu.Lock() diff --git a/templates/login.html.template b/templates/login.html.template index a197e8e..f08caf7 100644 --- a/templates/login.html.template +++ b/templates/login.html.template @@ -6,7 +6,6 @@ {{end}} <form class="password-form" action="/login" method="post"> <input type="hidden" name="csrf-token" value="{{.CSRFToken}}"> - <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> </form> diff --git a/tools/finditers/finditers.go b/tools/finditers/finditers.go deleted file mode 100644 index 31a7e33..0000000 --- a/tools/finditers/finditers.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "encoding/hex" - "fmt" - "sort" - "testing" - "time" - - "gitlab.com/rhogenson/roseh.moe/internal/pwhash" -) - -func mustHex(t testing.TB, s string) []byte { - t.Helper() - b, err := hex.DecodeString(s) - if err != nil { - t.Fatalf("Invalid hex %q: %s", s, err) - } - return b -} - -var iterations int - -func BenchmarkHashIter(b *testing.B) { - const pw = "atypical evasion foyer roulette throng awning stability exchange humorless vowed" - salt := mustHex(b, "4250af599e07cde7") - for b.Loop() { - pwhash.HashIter(pw, salt, iterations) - } -} - -func main() { - const targetDuration = 5 * time.Second - for iterations = 4096; time.Duration(testing.Benchmark(BenchmarkHashIter).NsPerOp()) < targetDuration; iterations *= 2 { - } - lo := iterations / 2 - hi := iterations - fmt.Println(lo + sort.Search(hi-lo, func(i int) bool { - iterations = lo + i - return time.Duration(testing.Benchmark(BenchmarkHashIter).NsPerOp()) > targetDuration - })) -} diff --git a/tools/hashpw/hashpw.go b/tools/hashpw/hashpw.go deleted file mode 100644 index c668622..0000000 --- a/tools/hashpw/hashpw.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "encoding/base64" - "fmt" - "os" - - "gitlab.com/rhogenson/roseh.moe/internal/pwhash" - "golang.org/x/term" -) - -func hashpw() error { - fmt.Fprint(os.Stderr, "Enter password: ") - password, err := term.ReadPassword(int(os.Stdin.Fd())) - fmt.Fprintln(os.Stderr) - if err != nil { - return err - } - hash, err := pwhash.Hash(string(password)) - if err != nil { - return err - } - fmt.Printf("notepad-password=%s\n", base64.RawURLEncoding.EncodeToString(hash)) - return nil -} - -func main() { - if err := hashpw(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } -} diff --git a/tools/secret-key/secret-key.go b/tools/secret-key/secret-key.go deleted file mode 100644 index 969828a..0000000 --- a/tools/secret-key/secret-key.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import ( - "crypto/rand" - "crypto/sha256" - "encoding/base64" - "fmt" -) - -func main() { - buf := make([]byte, sha256.BlockSize) - rand.Read(buf) - fmt.Printf("secret-key=%s\n", base64.RawURLEncoding.EncodeToString(buf)) -} |
