diff --git a/pkg/db/user.go b/pkg/db/user.go index 94f1419..b4a66f2 100644 --- a/pkg/db/user.go +++ b/pkg/db/user.go @@ -31,11 +31,6 @@ func (db *DB) GetUser(username string) (u core.User, err error) { return } -func (db *DB) UserIsAdmin(username string) bool { - u, _ := db.GetUser(username) - return u.Admin -} - func (db *DB) UserExists(username string) error { return db.users.Get(username, nil) } @@ -100,14 +95,14 @@ func (db *DB) CreateUser(username, password string, admin bool) error { }) } -func (db *DB) Login(username, password string) (err error) { - var u core.User +func (db *DB) Login(username, password string) (u core.User, err error) { u, err = db.GetUser(username) if err != nil { return } if u.Locked == 3 { - return errors.New("db: user locked") + err = errors.New("db: user locked") + return } if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil { u.Locked++ @@ -115,7 +110,8 @@ func (db *DB) Login(username, password string) (err error) { if err != nil { return } - return errors.New("db: wrong login") + err = errors.New("db: wrong login") + return } if u.Locked > 0 { u.Locked = 0 diff --git a/pkg/server/context.go b/pkg/server/context.go index 4db73ae..edf7ca7 100644 --- a/pkg/server/context.go +++ b/pkg/server/context.go @@ -3,28 +3,42 @@ package server import ( - "crypto/subtle" - "encoding/base64" "errors" "git.giftfish.de/ston1th/gowiki/pkg/log" + "git.giftfish.de/ston1th/jwt" "github.com/gorilla/mux" "html/template" "net/http" "time" ) -func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context { +func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) { h := w.Header() h.Set("X-Frame-Options", "DENY") h.Set("X-Content-Type-Options", "nosniff") h.Set("X-XSS-Protection", "1; mode=block") h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'") - return &Context{ + ctx = &Context{ Request: r, Response: w, Srv: s, Time: time.Now(), } + if c, err := r.Cookie(cookieName); err == nil { + t, err := jwt.DecodeToken(c.Value) + if err != nil { + log.Println("DecodeToken:", err) + } else { + if err = s.JWT.Verify(t); err != nil { + log.Println("VerifyToken:", err) + } else { + ctx.Token = *t + return + } + } + } + ctx.SetCookie(nil) + return } type Context struct { @@ -36,6 +50,8 @@ type Context struct { Err error Time time.Time + Token jwt.Token + Data webData } @@ -64,7 +80,8 @@ func (c *Context) Exec() { c.Err = errors.New("template is nil") return } - c.newToken() + c.Data.Token = newXsrf(c.Token.RawSig()[:keySize]) + c.Data.Version = c.Srv.Version if c.Data.BodyTitle == "" { c.Data.BodyTitle = c.Data.Title @@ -139,111 +156,54 @@ func (c *Context) Var(name string) (ret string) { return } -func (c *Context) initToken() { - token := c.getToken() - if token == nil || len(token) != keyLen { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - s.Values["token"] = genKey(keyLen) - s.Options = c.Srv.Store.Options - err := s.Save(c.Request, c.Response) - if err != nil { - log.Println("InitToken: ", err) - } - } -} - -func (c *Context) newToken() { - c.initToken() - rnd := genKey(keyLen) - c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...)) -} - // CheckToken validates the xsrf token -func (c *Context) CheckToken(f func()) bool { - token, err := base64.StdEncoding.DecodeString(c.Form("token")) - if err != nil { - log.Println("CheckToken: ", err) - return false - } - if len(token) != authLen { - return false - } - ok := subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1 +func (c *Context) CheckXsrf(f func()) (ok bool) { + ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize]) if !ok { if f != nil { f() - return ok + return } c.Data.Msg = "wrong csrf token" c.Exec() } - return ok -} - -func (c *Context) LoggedOn() (ok bool) { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - _, ok = s.Values["user"] return } -func (c *Context) SetUdata(user string, admin bool) { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - s.Values["user"] = user - s.Values["admin"] = admin - s.Options = c.Srv.Store.Options - if err := s.Save(c.Request, c.Response); err != nil { - log.Println(err) - } +func (c *Context) LoggedOn() (ok bool) { + _, ok = c.Token.Claims["user"] + return } -func (c *Context) UnsetUdata() { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - s.Values = make(map[interface{}]interface{}) - s.Options = c.Srv.Store.Options - if err := s.Save(c.Request, c.Response); err != nil { +func (c *Context) SetCookie(claims map[string]interface{}) { + t := jwt.NewToken(claims, nil) + if err := c.Srv.JWT.Sign(t); err != nil { log.Println(err) + return } + c.Token = *t + http.SetCookie(c.Response, &http.Cookie{ + Name: cookieName, + Value: t.Sig(), + MaxAge: 3600 * 12, + Secure: true, + HttpOnly: true, + SameSite: http.SameSiteStrictMode, + }) } func (c *Context) GetUser() string { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - if ret, ok := s.Values["user"].(string); ok { + if ret, ok := c.Token.Claims["user"].(string); ok { return ret } return "" } func (c *Context) GetAdmin() bool { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - if ret, ok := s.Values["admin"].(bool); ok { + if ret, ok := c.Token.Claims["admin"].(bool); ok { return ret } return false } -func (c *Context) getToken() []byte { - s, _ := c.Srv.Store.Get(c.Request, cookieName) - t, ok := s.Values["token"] - if !ok { - return nil - } - b, ok := t.([]byte) - if !ok { - return nil - } - return b -} - -func xor(a, b []byte) (c []byte) { - n := len(a) - if len(b) != n { - return - } - c = make([]byte, n) - for i := 0; i < n; i++ { - c[i] = a[i] ^ b[i] - } - return -} - type ctxHandler func(*Context) diff --git a/pkg/server/handler.go b/pkg/server/handler.go index cbd548a..6d60cca 100644 --- a/pkg/server/handler.go +++ b/pkg/server/handler.go @@ -68,9 +68,6 @@ func staticHandler(ctx *Context) { } func indexHandler(ctx *Context) { - if !ctx.LoggedOn() { - ctx.UnsetUdata() - } ctx.Redirect("/wiki/Index", 302) } @@ -91,7 +88,7 @@ func loginHandler(ctx *Context) { user := ctx.Form("user") password := ctx.Form("password") submit := ctx.Form("submit") - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if user == "" || password == "" || submit != "Login" { @@ -99,13 +96,13 @@ func loginHandler(ctx *Context) { ctx.Exec() return } - err := ctx.Srv.DB.Login(user, password) + u, err := ctx.Srv.DB.Login(user, password) if err != nil { ctx.Data.Msg = err.Error() ctx.Exec() return } - ctx.SetUdata(user, ctx.Srv.DB.UserIsAdmin(user)) + ctx.SetCookie(map[string]interface{}{"username": u.Username, "admin": u.Admin}) ctx.Redirect("/wiki/Index", 302) } } @@ -122,7 +119,7 @@ func searchHandler(ctx *Context) { case "POST": search := ctx.Form("search") submit := ctx.Form("submit") - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if submit != "Search" { @@ -178,7 +175,7 @@ func wikiNewHandler(ctx *Context) { MD: text, } ctx.Data.Data = p - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if submit != "Submit" { @@ -256,7 +253,7 @@ func wikiEditHandler(ctx *Context) { } ctx.Data.Title = ftitle ctx.Data.BodyTitle = ftitle - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if submit != "Submit" { @@ -281,7 +278,7 @@ func wikiDelHandler(ctx *Context) { f := func() { ctx.Redirect("/wiki/"+title, 302) } - if !ctx.CheckToken(f) { + if !ctx.CheckXsrf(f) { return } if submit != "Delete" { @@ -333,7 +330,7 @@ func userNewHandler(ctx *Context) { adm = true } submit := ctx.Form("submit") - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if user == "" || password == "" || password != repeat || submit != "Submit" { @@ -376,7 +373,7 @@ func userEditHandler(ctx *Context) { adm = true } submit := ctx.Form("submit") - if !ctx.CheckToken(nil) { + if !ctx.CheckXsrf(nil) { return } if user == "" || password != repeat || submit != "Submit" { @@ -438,6 +435,6 @@ func userDelHandler(ctx *Context) { } func logoutHandler(ctx *Context) { - ctx.UnsetUdata() + ctx.SetCookie(nil) ctx.Redirect("/wiki/Index", 302) } diff --git a/pkg/server/server.go b/pkg/server/server.go index 7228ad8..74f1450 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -2,33 +2,30 @@ package server import ( "context" - "crypto/rand" "github.com/gorilla/mux" - "github.com/gorilla/sessions" "html/template" - "io" "net" "net/http" "time" "git.giftfish.de/ston1th/gowiki/pkg/db" + "git.giftfish.de/ston1th/jwt" ) const ( - keyLen = 32 - authLen = keyLen * 2 cookieName = "gosession" ) type HTTPServer struct { - Version string - SecCookie bool - Store *sessions.CookieStore - DB *db.DB - listener net.Listener - srv *http.Server - templ map[string]*template.Template - res map[string][]byte + Version string + listener net.Listener + srv *http.Server + + DB *db.DB + JWT *jwt.JWT + + templ map[string]*template.Template + res map[string][]byte } func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) { @@ -36,6 +33,7 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ Version: version, SecCookie: secCookie, listener: l, + JWT: jwt.New(time.Hour*12, nil, nil), } srv.cookieStore() srv.loadTemplates() @@ -46,7 +44,7 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ } func (s *HTTPServer) Start() (err error) { - s.BS, err = NewBoltStore() + s.DB, err = db.New() if err != nil { return } @@ -81,37 +79,3 @@ func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc { h(newContext(w, r, s)) } } -func (s *HTTPServer) cookieStore() { - options := &sessions.Options{ - Path: "/", - MaxAge: 3600 * 12, - Secure: s.SecCookie, - HttpOnly: true, - } - authKey := genKey(authLen) - encKey := genKey(keyLen) - s.Store = sessions.NewCookieStore(authKey, encKey) - s.Store.Options = options - - go func() { - for { - time.Sleep(time.Hour * 12) - newAuthKey := genKey(authLen) - newEncKey := genKey(keyLen) - newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey) - newStore.Options = options - - s.Store = newStore - authKey = newAuthKey - encKey = newEncKey - } - }() -} - -func genKey(length int) (bytes []byte) { - bytes = make([]byte, length) - if _, err := io.ReadFull(rand.Reader, bytes); err != nil { - log.Println("genKey:", err) - } - return -} diff --git a/pkg/server/xsrf.go b/pkg/server/xsrf.go new file mode 100644 index 0000000..8ad8a55 --- /dev/null +++ b/pkg/server/xsrf.go @@ -0,0 +1,43 @@ +package server + +import ( + "crypto/rand" + "crypto/subtle" + "encoding/base64" + "git.giftfish.de/ston1th/gowiki/pkg/log" + "git.giftfish.de/ston1th/jwt" +) + +const keySize = jwt.KeySize / 2 + +func genKey(length int) (bytes []byte) { + bytes = make([]byte, length) + if _, err := io.ReadFull(rand.Reader, bytes); err != nil { + log.Println("genKey:", err) + } + return +} + +func newXsrf(secret []byte) string { + rnd := genKey(keySize) + return base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, secret)...)) +} + +func checkXsrf(xsrf string, secret []byte) bool { + t, err := base64.StdEncoding.DecodeString(xsrf) + if err != nil { + return false + } + return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1 +} + +func xor(a, b []byte) []byte { + n := len(a) + if len(b) != n { + return nil + } + for i := 0; i < n; i++ { + a[i] = a[i] ^ b[i] + } + return a +}