commit f821baa082b66d46c2db023e0ddece1324086fa0
parent 22bcfff6d24cec3007d2563694154328416b0eca
Author: Alexander Cobleigh <cblgh@cblgh.org>
Date: Mon, 7 Feb 2022 17:45:30 +0100
Merge pull request #33 from cblgh/password-proof
add password reset functionality
Diffstat:
11 files changed, 411 insertions(+), 7 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -2,3 +2,5 @@
*.db
data/
data/.gitkeep
+pwtool
+*.json
diff --git a/cmd/pwtool/main.go b/cmd/pwtool/main.go
@@ -0,0 +1,28 @@
+package main
+
+import (
+ "cerca/crypto"
+ "flag"
+ "fmt"
+ "os"
+)
+
+func main() {
+ var kpPath string
+ var payload string
+ flag.StringVar(&kpPath, "keypair", "", "path to your account-securing public-keypair (the thing you got during registering and was told to save:)")
+ flag.StringVar(&payload, "payload", "", "the payload presented on the restore password page")
+ flag.Parse()
+
+ if kpPath == "" {
+ fmt.Println(`usage:
+ tool --keypair <path-to-keypair.json> --payload <payload from website>
+ tool --help for more information`)
+ os.Exit(0)
+ }
+ kp, _ := crypto.ReadKeypair(kpPath)
+ proof := crypto.CreateProof(kp, []byte(payload))
+ fmt.Println("your proof:")
+ fmt.Println(fmt.Sprintf("%x", proof))
+ fmt.Println("\nplease paste the proof in the proof box of the restore password page, thank you!")
+}
diff --git a/cmd/pwtool/main_test.go b/cmd/pwtool/main_test.go
@@ -0,0 +1,19 @@
+package main_test
+
+import (
+ "cerca/crypto"
+ "github.com/stretchr/testify/assert"
+ "testing"
+)
+
+func TestMain(t *testing.T) {
+ a := assert.New(t)
+
+ kp, err := crypto.GenerateKeypair()
+ a.NoError(err)
+ msg := []byte("hi")
+ proof := crypto.CreateProof(kp, msg)
+ a.NotZero(len(proof), "proof length greater than zero")
+ proofVerificationCorrect := crypto.VerifyProof(kp.Public, msg, proof)
+ a.True(proofVerificationCorrect)
+}
diff --git a/crypto/crypto.go b/crypto/crypto.go
@@ -4,16 +4,20 @@ import (
"cerca/util"
"crypto/ed25519"
crand "crypto/rand"
+ "encoding/base64"
"encoding/binary"
"encoding/json"
"fmt"
"github.com/synacor/argon2id"
rand "math/rand"
+ "os"
+ "strings"
+ "time"
)
type Keypair struct {
- Public string `json:"public"`
- Private string `json:"private"`
+ Public ed25519.PublicKey `json:"public"`
+ Private ed25519.PrivateKey `json:"private"`
}
func GenerateKeypair() (Keypair, error) {
@@ -22,7 +26,46 @@ func GenerateKeypair() (Keypair, error) {
if err != nil {
return Keypair{}, ed.Eout(err, "generating key")
}
- return Keypair{Public: fmt.Sprintf("%x", pub), Private: fmt.Sprintf("%x", priv)}, nil
+ return Keypair{Public: pub, Private: priv}, nil
+}
+
+func ReadKeypair(kpPath string) (Keypair, error) {
+ var kp Keypair
+ ed := util.Describe("read keypair from disk")
+ b, err := os.ReadFile(kpPath)
+ if err != nil {
+ return kp, ed.Eout(err, "read file")
+ }
+ err = kp.Unmarshal(b)
+ if err != nil {
+ return kp, ed.Eout(err, "unmarshal kp")
+ }
+ return kp, nil
+}
+
+func CreateProof(kp Keypair, payload []byte) []byte {
+ return ed25519.Sign(kp.Private, payload)
+}
+
+func VerifyProof(public ed25519.PublicKey, payload, proof []byte) bool {
+ return ed25519.Verify(public, payload, proof)
+}
+
+/* kinda cludgy oh well */
+func (kp *Keypair) PublicString() (string, error) {
+ b, err := json.Marshal(kp.Public)
+ if err != nil {
+ return "", util.Eout(err, "marshal public key")
+ }
+ return string(b), nil
+}
+
+func (kp *Keypair) PrivateString() (string, error) {
+ b, err := json.Marshal(kp.Private)
+ if err != nil {
+ return "", util.Eout(err, "marshal private key")
+ }
+ return string(b), nil
}
func (kp *Keypair) Marshal() ([]byte, error) {
@@ -34,13 +77,61 @@ func (kp *Keypair) Marshal() ([]byte, error) {
}
func (kp *Keypair) Unmarshal(input []byte) error {
- err := json.Unmarshal(input, &kp)
+ ed := util.Describe("unmarshal keypair")
+ type stringKp struct {
+ Public string `json:"public"`
+ Private string `json:"private"`
+ }
+ var m stringKp
+ err := json.Unmarshal(input, &m)
+ if err != nil {
+ return ed.Eout(err, "unmarshal into string struct")
+ }
+
+ // handle the unfortunate case that the first generated keypairs were all in hex :')
+ // meaning: convert them from hex to base64 (the format expected by crypto/ed25519)
+ if len(m.Private) == 128 {
+ convertedVal, err := util.Hex2Base64(m.Private)
+ if err != nil {
+ return ed.Eout(err, "failed to convert privkey hex to base64")
+ }
+ m.Private = convertedVal
+ convertedVal, err = util.Hex2Base64(m.Public)
+ if err != nil {
+ return ed.Eout(err, "failed to convert pubkey hex to base64")
+ }
+ m.Public = convertedVal
+
+ // marshal the corrected version to a slice of bytes, so that we can pretend this debacle never happened
+ input, err = json.Marshal(m)
+ if err != nil {
+ return ed.Eout(err, "failed to marshal converted hex")
+ }
+ }
+
+ err = json.Unmarshal(input, kp)
if err != nil {
- return util.Eout(err, "unmarshal keypair")
+ return ed.Eout(err, "unmarshal keypair")
}
return nil
}
+func PublicKeyFromString(s string) ed25519.PublicKey {
+ ed := util.Describe("public key from string")
+ var err error
+ // handle legacy case of some pubkeys being stored in wrong column due to faulty query <.<
+ s = strings.ReplaceAll(s, `"`, "")
+ // handle legacy case of some pubkeys being stored as hex
+ if len(s) == 64 {
+ s, err = util.Hex2Base64(s)
+ ed.Check(err, "convert hex to base64")
+ }
+ b, err := base64.StdEncoding.DecodeString(s)
+ ed.Check(err, "decode base64 string")
+ pub := (ed25519.PublicKey)(b)
+ return pub
+}
+
func HashPassword(s string) (string, error) {
ed := util.Describe("hash password")
hash, err := argon2id.DefaultHashPassword(s)
@@ -64,6 +155,14 @@ func GenerateVerificationCode() int {
return rnd.Intn(999999)
}
+func GenerateNonce() string {
+ const MaxUint = ^uint(0)
+ const MaxInt = int(MaxUint >> 1)
+ var src cryptoSource
+ rnd := rand.New(src)
+ return fmt.Sprintf("%d%d", time.Now().Unix(), rnd.Intn(MaxInt))
+}
+
type cryptoSource struct{}
func (s cryptoSource) Seed(seed int64) {}
diff --git a/database/database.go b/database/database.go
@@ -55,6 +55,12 @@ func createTables(db *sql.DB) {
);
`,
`
+ CREATE TABLE IF NOT EXISTS nonces (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ nonce TEXT NOT NULL UNIQUE
+ );
+ `,
+ `
CREATE TABLE IF NOT EXISTS pubkeys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
pubkey TEXT NOT NULL UNIQUE,
@@ -324,6 +330,16 @@ func (d DB) CreateUser(name, hash string) (int, error) {
return userid, nil
}
+func (d DB) GetUserID(name string) (int, error) {
+ stmt := `SELECT id FROM users where name = ?`
+ var userid int
+ err := d.db.QueryRow(stmt, name).Scan(&userid)
+ if err != nil {
+ return -1, util.Eout(err, "get user id")
+ }
+ return userid, nil
+}
+
func (d DB) GetPasswordHash(username string) (string, int, error) {
stmt := `SELECT passwordhash, id FROM users where name = ?`
var hash string
@@ -350,6 +366,11 @@ func (d DB) CheckUserExists(userid int) (bool, error) {
return d.existsQuery(stmt, userid)
}
+func (d DB) CheckNonceExists(nonce string) (bool, error) {
+ stmt := `SELECT 1 FROM nonces WHERE nonce = ?`
+ return d.existsQuery(stmt, nonce)
+}
+
func (d DB) CheckUsernameExists(username string) (bool, error) {
stmt := `SELECT 1 FROM users WHERE name = ?`
return d.existsQuery(stmt, username)
@@ -375,6 +396,7 @@ func (d DB) DeleteUser(userid int) {
func (d DB) AddPubkey(userid int, pubkey string) error {
ed := util.Describe("add pubkey")
+ // TODO (2022-02-03): the insertion order is wrong >.<
stmt := `INSERT INTO pubkeys (pubkey, userid) VALUES (?, ?)`
_, err := d.Exec(stmt, userid, pubkey)
if err = ed.Eout(err, "inserting record"); err != nil {
@@ -383,6 +405,29 @@ func (d DB) AddPubkey(userid int, pubkey string) error {
return nil
}
+func (d DB) GetPubkey(userid int) (pubkey string, err error) {
+ ed := util.Describe("get pubkey")
+ // due to a mishap in the query in AddPubkey the column `pubkey` contains the userid
+ // and the column `userid` contains the pubkey
+ // :'))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
+ // TODO (2022-02-03): when we have migration logic, fix this mishap
+ stmt := `SELECT userid from pubkeys where pubkey = ?`
+ err = d.db.QueryRow(stmt, userid).Scan(&pubkey)
+ err = ed.Eout(err, "query & scan")
+ return
+}
+
+// TODO (2022-02-04): extend mrv verification code length and reuse nonce scheme to fix registration bug?
+func (d DB) AddNonce(nonce string) error {
+ ed := util.Describe("add nonce")
+ stmt := `INSERT INTO nonces (nonce) VALUES (?)`
+ _, err := d.Exec(stmt, nonce)
+ if err != nil {
+ return ed.Eout(err, "insert nonce")
+ }
+ return nil
+}
+
func (d DB) AddRegistration(userid int, verificationLink string) error {
ed := util.Describe("add registration")
stmt := `INSERT INTO registrations (userid, host, link, time) VALUES (?, ?, ?, ?)`
diff --git a/go.mod b/go.mod
@@ -8,6 +8,7 @@ require (
github.com/gorilla/sessions v1.2.1 // indirect
github.com/mattn/go-sqlite3 v1.14.9 // indirect
github.com/microcosm-cc/bluemonday v1.0.17 // indirect
+ github.com/stretchr/testify v1.7.0 // indirect
github.com/synacor/argon2id v0.0.0-20190318165710-18569dfc600b // indirect
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect
diff --git a/go.sum b/go.sum
@@ -2,6 +2,8 @@ github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuP
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/carlmjohnson/requests v0.22.1 h1:YoifpEbpJW4LPRX/+0dJe3vTLducEE9Ib10k6lElIUM=
github.com/carlmjohnson/requests v0.22.1/go.mod h1:Hw4fFOk3xDlHQbNRTGo4oc52TUTpVEq93sNy/H+mrQM=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gomarkdown/markdown v0.0.0-20211212230626-5af6ad2f47df h1:M7mdNDTRraBcrHZg2aOYiFP9yTDajb6fquRZRpXnbVA=
@@ -19,6 +21,11 @@ github.com/microcosm-cc/bluemonday v1.0.17 h1:Z1a//hgsQ4yjC+8zEkV8IWySkXnsxmdSY6
github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/synacor/argon2id v0.0.0-20190318165710-18569dfc600b h1:Yu/2y+2iAAcTRfdlMZ3dEdb1aYWXesDDaQjb7xLgy7Y=
github.com/synacor/argon2id v0.0.0-20190318165710-18569dfc600b/go.mod h1:RQlLg9p2W+/d3q6xRWilTA2R4ltKiwEmzoI1urnKm9U=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -47,3 +54,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/html/head.html b/html/head.html
@@ -13,6 +13,7 @@
/* end reset */
/* below is the minimum viable css for simply getting the forum out the door; contributions, both in css and
design mockups, welcome from the community for v2,v3,...,vn :) */
+ br { margin-bottom: unset; }
html {
max-width: 50rem;
font-family: sans-serif;
diff --git a/html/password-reset.html b/html/password-reset.html
@@ -0,0 +1,45 @@
+{{ template "head" . }}
+<p>On this page we'll go through a few steps to securely reset your password—without resorting to any emails!</p>
+<p>First up: what was your username?</p>
+{{ if eq .Data.Action "/reset/generate" }}
+<form method="post" action="{{.Data.Action}}">
+ <label type="text" for="username">Username:</label>
+ <input required id="username" name="username">
+ <div>
+ <input type="submit" value="Generate payload">
+ </div>
+</form>
+{{ end }}
+
+
+{{ if eq .Data.Action "/reset/submit" }}
+<input disabled value="{{ .Data.Username }}">
+<p>Now, first copy the snippet (aka <i>proof payload</i>) below:</p>
+<pre style="user-select: all;">
+<code>{{ .Data.Payload }}</code>
+</pre>
+<p>Follow the <b>tool instructions</b> to finalize the password reset.</p>
+<details>
+ <summary>Tool instructions</summary>
+ <ul>
+ <li><a href="/">Download the tool</a></li>
+ <li>Run as:<br><code>pwtool --payload <proof payload from above> --keypair <path to file with yr keypair from registration></code>
+ </li>
+ <li>Copy the generated proof and paste below</li>
+ <li>(Remember to save your password :)</li>
+ </ul>
+</details>
+
+<form method="post" action="{{.Data.Action}}">
+ <input type="hidden" required id="username" name="username" value="{{ .Data.Username }}">
+ <input type="hidden" required id="payload" name="payload" value="{{ .Data.Payload }}">
+ <label for="proof">Proof</label>
+ <input type="text" required id="proof" name="proof">
+ <label for="password">New password</label>
+ <input type="password" minlength="9" required id="password" name="password" aria-describedby="password-help">
+ <div>
+ <input type="submit" value="Change password">
+ </div>
+</form>
+{{ end }}
+{{ template "footer" . }}
diff --git a/server/server.go b/server/server.go
@@ -2,6 +2,7 @@ package server
import (
"context"
+ "encoding/hex"
"errors"
"fmt"
"html/template"
@@ -29,6 +30,12 @@ type TemplateData struct {
Title string
}
+type PasswordResetData struct {
+ Action string
+ Username string
+ Payload string
+}
+
type IndexData struct {
Threads []database.Thread
}
@@ -134,6 +141,7 @@ func generateTemplates() (*template.Template, error) {
"register",
"register-success",
"thread",
+ "password-reset",
}
rootTemplate := template.New("root")
@@ -292,6 +300,136 @@ func hasVerificationCode(link, verification string) bool {
return strings.Contains(strings.TrimSpace(linkBody), strings.TrimSpace(verification))
}
+func (h RequestHandler) ResetPasswordRoute(res http.ResponseWriter, req *http.Request) {
+ ed := util.Describe("password proof route")
+ loggedIn, _ := h.IsLoggedIn(req)
+ if loggedIn {
+ data := GenericMessageData{
+ Title: "Reset password",
+ Message: "You are logged in, log out to reset password using proof",
+ Link: "/logout",
+ LinkText: "Logout",
+ }
+ h.renderView(res, "generic-message", TemplateData{Data: data, LoggedIn: loggedIn, Title: "Reset password"})
+ return
+ }
+
+ renderErr := func(errFmt string, args ...interface{}) {
+ errMessage := fmt.Sprintf(errFmt, args...)
+ fmt.Println(errMessage)
+ data := GenericMessageData{
+ Title: "Reset password",
+ Message: errMessage,
+ Link: "/reset",
+ LinkText: "Go back",
+ }
+ h.renderView(res, "generic-message", TemplateData{Data: data, Title: "password reset"})
+ }
+
+ switch req.Method {
+ case "GET":
+ switch req.URL.Path {
+ case "/reset/submit":
+ params := req.URL.Query()
+ getParam := func(key string) string {
+ if q, exists := params[key]; exists {
+ return q[0]
+ }
+ fmt.Println("can't find param", key)
+ return ""
+ }
+ username := getParam("username")
+ payload := getParam("payload")
+ h.renderView(res, "password-reset", TemplateData{Data: PasswordResetData{Action: "/reset/submit", Username: username, Payload: payload}})
+ default:
+ h.renderView(res, "password-reset", TemplateData{Data: PasswordResetData{Action: "/reset/generate"}})
+ }
+ case "POST":
+ username := req.PostFormValue("username")
+ switch req.URL.Path {
+ case "/reset/generate":
+ constructProofPayload := func() string {
+ return fmt.Sprintf("%s::%s", username, crypto.GenerateNonce())
+ }
+ payload := constructProofPayload()
+ params := fmt.Sprintf("?payload=%s&username=%s", payload, username)
+ http.Redirect(res, req, "/reset/submit"+params, http.StatusSeeOther)
+ case "/reset/submit":
+ password := req.PostFormValue("password")
+ proofString := req.PostFormValue("proof")
+ payload := req.PostFormValue("payload")
+
+ // make sure the user exists
+ userid, err := h.db.GetUserID(username)
+ if err != nil {
+ renderErr("Wrong username, or a non-existent user")
+ return
+ }
+
+ // make sure the nonce / payload is not being reused
+ nonceExisted, err := h.db.CheckNonceExists(payload)
+ if err != nil {
+ dump(ed.Eout(err, "check nonce existed"))
+ return
+ }
+ if nonceExisted {
+ renderErr("This payload has already been used, please generate a new one")
+ return
+ }
+
+ // get the pubkey, as it is saved in the database for the corresponding user
+ pubkeyString, err := h.db.GetPubkey(userid)
+ if err != nil {
+ renderErr("No matching pubkey found")
+ return
+ }
+ // convert to ed25519.PublicKey
+ pubkey := crypto.PublicKeyFromString(pubkeyString)
+
+ proof, err := hex.DecodeString(proofString)
+ if err != nil {
+ renderErr("The proof format was incorrect")
+ return
+ }
+
+ correct := crypto.VerifyProof(pubkey, []byte(payload), proof)
+ if !correct {
+ renderErr("The proof was incorrect")
+ return
+ }
+ // proof was correct!
+ // save the nonce, so it's not reused
+ err = h.db.AddNonce(payload)
+ if err != nil {
+ dump(ed.Eout(err, "insert nonce into database"))
+ return
+ }
+ // let's set the new password in the database. first, hash it
+ pwhash, err := crypto.HashPassword(password)
+ if err != nil {
+ dump(ed.Eout(err, "hash password during reset"))
+ return
+ }
+ h.db.UpdateUserPasswordHash(userid, pwhash)
+ // render a success message & show a link to the login page :')
+ data := GenericMessageData{
+ Title: "Reset password—success!",
+ Message: "You reset your password!",
+ Link: "/login",
+ LinkMessage: "Give it a try and",
+ LinkText: "login",
+ }
+ h.renderView(res, "generic-message", TemplateData{Data: data, Title: "password reset"})
+ default:
+ fmt.Printf("unsupported POST route (%s), redirecting to /\n", req.URL.Path)
+ IndexRedirect(res, req)
+ }
+ default:
+ fmt.Println("non get/post method, redirecting to index")
+ IndexRedirect(res, req)
+ }
+}
+
func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request) {
ed := util.Describe("register route")
loggedIn, _ := h.IsLoggedIn(req)
@@ -389,12 +527,17 @@ func (h RequestHandler) RegisterRoute(res http.ResponseWriter, req *http.Request
}
// generate and pass public keypair
keypair, err := crypto.GenerateKeypair()
+ ed.Check(err, "generate keypair")
// record generated pubkey in database for eventual later use
- err = h.db.AddPubkey(userID, keypair.Public)
+ pub, err := keypair.PublicString()
+ if err = ed.Eout(err, "convert pubkey to string"); err != nil {
+ dump(err)
+ }
+ ed.Check(err, "stringify pubkey")
+ err = h.db.AddPubkey(userID, pub)
if err = ed.Eout(err, "insert pubkey in db"); err != nil {
dump(err)
}
- ed.Check(err, "generate keypair")
kpJson, err := keypair.Marshal()
ed.Check(err, "marshal keypair")
h.renderView(res, "register-success", TemplateData{Data: RegisterSuccessData{string(kpJson)}, LoggedIn: loggedIn, Title: "registered successfully"})
@@ -532,6 +675,7 @@ func Serve(allowlist []string, sessionKey string, isdev bool) {
handler := RequestHandler{&db, session.New(sessionKey, developing), allowlist}
/* note: be careful with trailing slashes; go's default handler is a bit sensitive */
// TODO (2022-01-10): introduce middleware to make sure there is never an issue with trailing slashes
+ http.HandleFunc("/reset/", handler.ResetPasswordRoute)
http.HandleFunc("/about", handler.AboutRoute)
http.HandleFunc("/logout", handler.LogoutRoute)
http.HandleFunc("/login", handler.LoginRoute)
diff --git a/util/util.go b/util/util.go
@@ -1,6 +1,8 @@
package util
import (
+ "encoding/base64"
+ "encoding/hex"
"fmt"
"html/template"
"log"
@@ -97,6 +99,15 @@ func GetThreadSlug(threadid int, title string, threadLen int) string {
return fmt.Sprintf("/thread/%d/%s-%d/", threadid, SanitizeURL(title), threadLen)
}
+func Hex2Base64(s string) (string, error) {
+ b, err := hex.DecodeString(s)
+ if err != nil {
+ return "", err
+ }
+ b64 := base64.StdEncoding.EncodeToString(b)
+ return b64, nil
+}
+
// make a string be suitable for use as part of a url
func SanitizeURL(input string) string {
input = strings.ReplaceAll(input, " ", "-")