From 060933aa27052358176b955d44a5de3156566abd Mon Sep 17 00:00:00 2001 From: shoopea Date: Sun, 17 Nov 2024 23:37:42 +0100 Subject: [PATCH] test tokens --- admin.go | 24 ++-- http.go | 78 ++++++++++++- http_tokens.go | 298 +++++++++++++++++++++++++++++++++++++++++++++++++ user.go | 27 ++++- version.go | 8 +- 5 files changed, 418 insertions(+), 17 deletions(-) create mode 100644 http_tokens.go diff --git a/admin.go b/admin.go index 4e1aa6d..fcdf8af 100644 --- a/admin.go +++ b/admin.go @@ -3,6 +3,7 @@ package main import ( "context" "embed" + "fmt" "html/template" "io/fs" "net/http" @@ -20,6 +21,7 @@ type AdminConfig struct { Users []*User `json:"users"` Secrets *SecretsConfig `json:"secrets"` Addr string `json:"addr"` + URL string `json:"url"` } type SecretsConfig struct { @@ -42,6 +44,7 @@ func NewAdmin() *AdminConfig { Addr: "0.0.0.0:8080", Secrets: NewSecrets(), Users: make([]*User, 0), + URL: "https://backup.example.com/", } return a @@ -100,12 +103,6 @@ func (a *AdminConfig) Run() { r.GET("/", HttpAnyIndex) r.POST("/", HttpAnyIndex) - r.GET("/ping", func(c *gin.Context) { - c.JSON(http.StatusOK, gin.H{ - "message": "pong", - }) - }) - r.GET("/run", func(c *gin.Context) { cfg.Run() c.JSON(http.StatusOK, gin.H{ @@ -148,8 +145,7 @@ func (a *AdminConfig) Run() { protected.GET("home", HttpAnyHome) unprotected := r.Group("u", HttpNoAuth()) - unprotected.GET("signin", HttpAnySignIn) - unprotected.POST("signin", HttpAnySignIn) + unprotected.POST("submit", HttpPostSubmit) unprotected.GET("recover", HttpGetRecover) srv := &http.Server{ @@ -186,3 +182,15 @@ func (a *AdminConfig) Run() { } } + +func FindUserID(user string) (uint64, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + for _, v := range cfg.Admin.Users { + if v.Username == user { + return v.ID, nil + } + } + return 0, fmt.Errorf("no user") +} diff --git a/http.go b/http.go index b506ba3..1e31711 100644 --- a/http.go +++ b/http.go @@ -4,9 +4,13 @@ import ( "net/http" "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" ) func HttpAuth() gin.HandlerFunc { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + return func(c *gin.Context) { // Search signed-in userID userID := 0 @@ -19,11 +23,39 @@ func HttpAuth() gin.HandlerFunc { } func HttpNoAuth() gin.HandlerFunc { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + return func(c *gin.Context) { } } -func HttpAnySignIn(c *gin.Context) { +func HttpPostSubmit(c *gin.Context) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + t, err := GetCSRFToken(c) + + if err != nil { + c.String(http.StatusBadRequest, "bad token") + return + } + + if !t.Valid() { + c.String(http.StatusBadRequest, "expired token") + return + } + + switch t.GetPath() { + case "signin": + log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit signin") + HttpSubmitSignIn(c) + HttpAnyIndex(c) + default: + log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("submit %s", t.GetPath()) + c.String(http.StatusBadRequest, "") + } + if GetWebSessionUserID(c) > 0 { c.Redirect(http.StatusTemporaryRedirect, "/p/home") } else { @@ -37,10 +69,16 @@ func HttpAnySignIn(c *gin.Context) { } func HttpGetRecover(c *gin.Context) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + c.HTML(http.StatusOK, "page-recover.html", gin.H{}) } func HttpAnyIndex(c *gin.Context) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + if GetWebSessionUserID(c) > 0 { c.Redirect(http.StatusTemporaryRedirect, "/p/home") } else { @@ -49,6 +87,9 @@ func HttpAnyIndex(c *gin.Context) { } func HttpAnyHome(c *gin.Context) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + if GetWebSessionUserID(c) == 0 { c.Redirect(http.StatusTemporaryRedirect, "/u/signin") } else { @@ -58,8 +99,41 @@ func HttpAnyHome(c *gin.Context) { } func GetWebSessionUserID(c *gin.Context) uint64 { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + return 0 } -func SetCSRFToken(c *gin.Context) { +func HttpSubmitSignIn(c *gin.Context) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + err := c.Request.ParseForm() + if err != nil { + c.SetCookie("warning", "Unable to parse form", 0, "/", cfg.Admin.URL, false, true) + log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Debugf("") + return + } + + username := c.Request.FormValue("username") + password := c.Request.FormValue("password") + + userID, err := FindUserID(username) + if err != nil { + c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true) + log.WithFields(log.Fields{"call": "FindUserID", "attr": username, "err": err}).Debugf("") + return + } + + if !VerifyUserPassword(userID, password) { + c.SetCookie("warning", "Invalid user or password", 0, "/", cfg.Admin.URL, false, true) + log.WithFields(log.Fields{"call": "VerifyUserPassword", "attr": "***"}).Debugf("auth not ok") + return + } + + t := NewSessionToken(userID) + + c.SetCookie("session", t.Encode(), 9999999999, "/", cfg.Admin.URL, false, true) + c.SetCookie("warning", "", -1, "/", cfg.Admin.URL, false, true) } diff --git a/http_tokens.go b/http_tokens.go new file mode 100644 index 0000000..0fbb453 --- /dev/null +++ b/http_tokens.go @@ -0,0 +1,298 @@ +package main + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "encoding/binary" + "fmt" + "hash/crc32" + "time" + + "github.com/gin-gonic/gin" + log "github.com/sirupsen/logrus" +) + +var sessionMap map[string]uint64 + +type SessionToken struct { + Identifier [32]byte + Verifier [32]byte +} + +type CSRFToken struct { + Seed [4]byte + Path [16]byte + Time int64 + Checksum uint32 +} + +func NewSessionToken(userID uint64) *SessionToken { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + identifier := make([]byte, 32) + _, err := rand.Read(identifier) + if err != nil { + log.WithFields(log.Fields{"call": "rand.Read", "attr": "identifier", "err": err}).Debugf("") + return &SessionToken{} + } + + verifier := make([]byte, 32) + _, err = rand.Read(verifier) + if err != nil { + log.WithFields(log.Fields{"call": "rand.Read", "attr": "verifier", "err": err}).Debugf("") + return &SessionToken{} + } + + hash := hmac.New(sha256.New, verifier) + hash.Write(verifier) + hashVerifier := hash.Sum(nil) + + t := SessionToken{} + + copy(t.Verifier[:], verifier[:32]) + copy(t.Identifier[:], hashVerifier[:32]) + + return &t + +} + +func (t *SessionToken) Bytes() []byte { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, t) + return buf.Bytes() +} + +func (t *SessionToken) Load(b []byte) error { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if len(b) != binary.Size(t) { + return fmt.Errorf("wrong size") + } + + r := bytes.NewReader(b) + + err := binary.Read(r, binary.LittleEndian, t) + if err != nil { + return err + } + + return nil +} + +func (t *SessionToken) GetSessionID() uint64 { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if sessionID, ok := sessionMap[string(t.Identifier[:])]; !ok { + return 0 + } else { + return sessionID + } +} + +func GetSessionTokenParam(c *gin.Context) (*SessionToken, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + err := c.Request.ParseForm() + if err != nil { + log.WithFields(log.Fields{"call": "Context.Request.ParseForm", "err": err}).Errorf("") + return nil, err + } + session := c.Param("key") + + return DecodeSessionToken(session) +} + +func GetSessionTokenCookie(c *gin.Context) (*SessionToken, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if session, err := c.Cookie("session"); err != nil { + log.WithFields(log.Fields{"call": "Context.Cookie", "attr": "session", "err": err}).Errorf("") + return nil, err + } else { + return DecodeSessionToken(session) + } +} + +func (t *SessionToken) Encode() string { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + return base64.StdEncoding.EncodeToString(t.Bytes()) +} + +func DecodeSessionToken(s string) (*SessionToken, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + log.WithFields(log.Fields{"call": "base64.StdEncoding.DecodeString", "attr": "session", "err": err}).Errorf("") + return nil, err + } + + t := SessionToken{} + err = t.Load(b) + if err != nil { + return nil, err + } + + return &t, nil +} + +func (t *CSRFToken) GetPath() string { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + buf := bytes.Buffer{} + for _, b := range t.Path { + if b > 0 { + buf.WriteByte(b) + } else { + break + } + } + return buf.String() +} + +func (t *CSRFToken) Bytes() []byte { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + buf := new(bytes.Buffer) + _ = binary.Write(buf, binary.LittleEndian, t) + return buf.Bytes() +} + +func (t *CSRFToken) Load(b []byte) error { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if len(b) != binary.Size(t) { + return fmt.Errorf("wrong size") + } + r := bytes.NewReader(b) + err := binary.Read(r, binary.LittleEndian, t) + if err != nil { + log.WithFields(log.Fields{"call": "binary.Read", "attr": "b", "err": err}).Errorf("") + return err + } + chk := t.Checksum + t.Checksum = 0 + if crc32.ChecksumIEEE(t.Bytes()) != chk { + return fmt.Errorf("wrong checksum") + } + t.Checksum = chk + return nil +} + +func NewCSRFToken(c *gin.Context) *CSRFToken { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + t := CSRFToken{} + + copy(t.Path[:], c.Request.URL.Path[0:]) + + t.Time = time.Now().UTC().Unix() + + _, err := rand.Read(t.Seed[:]) + log.WithFields(log.Fields{"call": "rand.Read", "attr": "seed", "err": err}).Errorf("") + + t.Checksum = crc32.ChecksumIEEE(t.Bytes()) + + return &t +} + +func GetCSRFToken(c *gin.Context) (*CSRFToken, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if context, err := c.Cookie("context"); err != nil { + return nil, fmt.Errorf("no context param") + } else { + return DecodeCSRFToken(context) + } +} + +func (t *CSRFToken) Encode() string { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + hash := sha256.New() + hash.Write([]byte(cfg.Admin.Secrets.ContextKey)) + key := hash.Sum(nil) + + block, _ := aes.NewCipher(key) + ciphertext := make([]byte, 32) // size of CSRFToken + if binary.Size(t) != 32 { + log.WithFields(log.Fields{"err": fmt.Errorf("size is wrong")}).Fatalf("") + } + + iv := make([]byte, aes.BlockSize) + + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(ciphertext[:], t.Bytes()) + + return base64.StdEncoding.EncodeToString(ciphertext) +} + +func DecodeCSRFToken(s string) (*CSRFToken, error) { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return nil, err + } + + if len(b) != binary.Size(CSRFToken{}) { + return nil, fmt.Errorf("wrong size") + } + + hash := sha256.New() + hash.Write([]byte(cfg.Admin.Secrets.ContextKey)) + key := hash.Sum(nil) + + block, err := aes.NewCipher(key) + + iv := make([]byte, aes.BlockSize) + + mode := cipher.NewCBCDecrypter(block, iv) + mode.CryptBlocks(b, b) + + t := CSRFToken{} + err = t.Load(b) + if err != nil { + return nil, err + } + + return &t, nil +} + +func (t *CSRFToken) Valid() bool { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + if time.Now().UTC().Sub(time.Unix(t.Time, 0)) > time.Duration(cfg.Admin.Secrets.ContextExpiration)*time.Second { + return false + } + return true +} + +func SetCSRFToken(c *gin.Context) { + c.SetCookie("context", NewCSRFToken(c).Encode(), cfg.Admin.Secrets.ContextExpiration, "/", cfg.Admin.URL, false, true) + return +} diff --git a/user.go b/user.go index ef0a3d2..d69a82a 100644 --- a/user.go +++ b/user.go @@ -15,7 +15,7 @@ type User struct { ID uint64 `json:"id"` Username string `json:"username"` Salt string `json:"salt"` - Passwd string `json:"passwd"` + Password string `json:"passwd"` Email string `json:"email"` } @@ -23,7 +23,7 @@ func NewUser(name, passwd string) (*User, error) { log.WithFields(log.Fields{"name": name}).Debugf("starting") defer log.WithFields(log.Fields{"name": name}).Debugf("done") - userID := uint64(0) + userID := uint64(1) for _, v := range cfg.Admin.Users { userID = max(v.ID+1, userID) if v.Username == name { @@ -49,7 +49,7 @@ func NewUser(name, passwd string) (*User, error) { log.WithFields(log.Fields{"name": name, "call": "HashPassword", "error": err}).Errorf("") return nil, err } else { - u.Passwd = hex.EncodeToString(pass) + u.Password = hex.EncodeToString(pass) } return u, nil @@ -79,3 +79,24 @@ func (u *User) HashPassword(passwd string) ([]byte, error) { } } + +func VerifyUserPassword(userID uint64, clearPass string) bool { + log.WithFields(log.Fields{}).Debugf("starting") + defer log.WithFields(log.Fields{}).Debugf("done") + + var u *User + + for _, v := range cfg.Admin.Users { + if v.ID == userID { + u = v + } + } + + hashPass, err := u.HashPassword(clearPass) + if err != nil { + log.WithFields(log.Fields{"call": "user.HashPassword", "attr": "***", "error": err}).Errorf("") + return false + } + + return hex.EncodeToString(hashPass) == u.Password +} diff --git a/version.go b/version.go index 995dd64..acb4f2c 100644 --- a/version.go +++ b/version.go @@ -1,7 +1,7 @@ // Code generated by version.sh (@generated) DO NOT EDIT. package main -var githash = "b83b0e8" +var githash = "3bd57a4" var branch = "master" -var buildstamp = "2024-11-17_17:56:49" -var commits = "104" -var version = "b83b0e8-b104 - 2024-11-17_17:56:49" +var buildstamp = "2024-11-17_22:37:22" +var commits = "105" +var version = "3bd57a4-b105 - 2024-11-17_22:37:22"