jwt for webapp
This commit is contained in:
parent
7e77fd7d27
commit
9d332bbda4
5 changed files with 112 additions and 152 deletions
|
|
@ -31,11 +31,6 @@ func (db *DB) GetUser(username string) (u core.User, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) UserIsAdmin(username string) bool {
|
|
||||||
u, _ := db.GetUser(username)
|
|
||||||
return u.Admin
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DB) UserExists(username string) error {
|
func (db *DB) UserExists(username string) error {
|
||||||
return db.users.Get(username, nil)
|
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) {
|
func (db *DB) Login(username, password string) (u core.User, err error) {
|
||||||
var u core.User
|
|
||||||
u, err = db.GetUser(username)
|
u, err = db.GetUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if u.Locked == 3 {
|
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 {
|
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||||
u.Locked++
|
u.Locked++
|
||||||
|
|
@ -115,7 +110,8 @@ func (db *DB) Login(username, password string) (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return errors.New("db: wrong login")
|
err = errors.New("db: wrong login")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if u.Locked > 0 {
|
if u.Locked > 0 {
|
||||||
u.Locked = 0
|
u.Locked = 0
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,42 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/subtle"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
"errors"
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||||
|
"git.giftfish.de/ston1th/jwt"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"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 := w.Header()
|
||||||
h.Set("X-Frame-Options", "DENY")
|
h.Set("X-Frame-Options", "DENY")
|
||||||
h.Set("X-Content-Type-Options", "nosniff")
|
h.Set("X-Content-Type-Options", "nosniff")
|
||||||
h.Set("X-XSS-Protection", "1; mode=block")
|
h.Set("X-XSS-Protection", "1; mode=block")
|
||||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
||||||
return &Context{
|
ctx = &Context{
|
||||||
Request: r,
|
Request: r,
|
||||||
Response: w,
|
Response: w,
|
||||||
Srv: s,
|
Srv: s,
|
||||||
Time: time.Now(),
|
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 {
|
type Context struct {
|
||||||
|
|
@ -36,6 +50,8 @@ type Context struct {
|
||||||
Err error
|
Err error
|
||||||
Time time.Time
|
Time time.Time
|
||||||
|
|
||||||
|
Token jwt.Token
|
||||||
|
|
||||||
Data webData
|
Data webData
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,7 +80,8 @@ func (c *Context) Exec() {
|
||||||
c.Err = errors.New("template is nil")
|
c.Err = errors.New("template is nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.newToken()
|
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
|
||||||
|
|
||||||
c.Data.Version = c.Srv.Version
|
c.Data.Version = c.Srv.Version
|
||||||
if c.Data.BodyTitle == "" {
|
if c.Data.BodyTitle == "" {
|
||||||
c.Data.BodyTitle = c.Data.Title
|
c.Data.BodyTitle = c.Data.Title
|
||||||
|
|
@ -139,111 +156,54 @@ func (c *Context) Var(name string) (ret string) {
|
||||||
return
|
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
|
// CheckToken validates the xsrf token
|
||||||
func (c *Context) CheckToken(f func()) bool {
|
func (c *Context) CheckXsrf(f func()) (ok bool) {
|
||||||
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
|
ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
|
||||||
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
|
|
||||||
if !ok {
|
if !ok {
|
||||||
if f != nil {
|
if f != nil {
|
||||||
f()
|
f()
|
||||||
return ok
|
return
|
||||||
}
|
}
|
||||||
c.Data.Msg = "wrong csrf token"
|
c.Data.Msg = "wrong csrf token"
|
||||||
c.Exec()
|
c.Exec()
|
||||||
}
|
}
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) LoggedOn() (ok bool) {
|
|
||||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
|
||||||
_, ok = s.Values["user"]
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) SetUdata(user string, admin bool) {
|
func (c *Context) LoggedOn() (ok bool) {
|
||||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
_, ok = c.Token.Claims["user"]
|
||||||
s.Values["user"] = user
|
return
|
||||||
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) UnsetUdata() {
|
func (c *Context) SetCookie(claims map[string]interface{}) {
|
||||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
t := jwt.NewToken(claims, nil)
|
||||||
s.Values = make(map[interface{}]interface{})
|
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||||
s.Options = c.Srv.Store.Options
|
|
||||||
if err := s.Save(c.Request, c.Response); err != nil {
|
|
||||||
log.Println(err)
|
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 {
|
func (c *Context) GetUser() string {
|
||||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
if ret, ok := c.Token.Claims["user"].(string); ok {
|
||||||
if ret, ok := s.Values["user"].(string); ok {
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) GetAdmin() bool {
|
func (c *Context) GetAdmin() bool {
|
||||||
s, _ := c.Srv.Store.Get(c.Request, cookieName)
|
if ret, ok := c.Token.Claims["admin"].(bool); ok {
|
||||||
if ret, ok := s.Values["admin"].(bool); ok {
|
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
return false
|
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)
|
type ctxHandler func(*Context)
|
||||||
|
|
|
||||||
|
|
@ -68,9 +68,6 @@ func staticHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexHandler(ctx *Context) {
|
func indexHandler(ctx *Context) {
|
||||||
if !ctx.LoggedOn() {
|
|
||||||
ctx.UnsetUdata()
|
|
||||||
}
|
|
||||||
ctx.Redirect("/wiki/Index", 302)
|
ctx.Redirect("/wiki/Index", 302)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,7 +88,7 @@ func loginHandler(ctx *Context) {
|
||||||
user := ctx.Form("user")
|
user := ctx.Form("user")
|
||||||
password := ctx.Form("password")
|
password := ctx.Form("password")
|
||||||
submit := ctx.Form("submit")
|
submit := ctx.Form("submit")
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user == "" || password == "" || submit != "Login" {
|
if user == "" || password == "" || submit != "Login" {
|
||||||
|
|
@ -99,13 +96,13 @@ func loginHandler(ctx *Context) {
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err := ctx.Srv.DB.Login(user, password)
|
u, err := ctx.Srv.DB.Login(user, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Data.Msg = err.Error()
|
ctx.Data.Msg = err.Error()
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
return
|
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)
|
ctx.Redirect("/wiki/Index", 302)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +119,7 @@ func searchHandler(ctx *Context) {
|
||||||
case "POST":
|
case "POST":
|
||||||
search := ctx.Form("search")
|
search := ctx.Form("search")
|
||||||
submit := ctx.Form("submit")
|
submit := ctx.Form("submit")
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if submit != "Search" {
|
if submit != "Search" {
|
||||||
|
|
@ -178,7 +175,7 @@ func wikiNewHandler(ctx *Context) {
|
||||||
MD: text,
|
MD: text,
|
||||||
}
|
}
|
||||||
ctx.Data.Data = p
|
ctx.Data.Data = p
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if submit != "Submit" {
|
if submit != "Submit" {
|
||||||
|
|
@ -256,7 +253,7 @@ func wikiEditHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
ctx.Data.Title = ftitle
|
ctx.Data.Title = ftitle
|
||||||
ctx.Data.BodyTitle = ftitle
|
ctx.Data.BodyTitle = ftitle
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if submit != "Submit" {
|
if submit != "Submit" {
|
||||||
|
|
@ -281,7 +278,7 @@ func wikiDelHandler(ctx *Context) {
|
||||||
f := func() {
|
f := func() {
|
||||||
ctx.Redirect("/wiki/"+title, 302)
|
ctx.Redirect("/wiki/"+title, 302)
|
||||||
}
|
}
|
||||||
if !ctx.CheckToken(f) {
|
if !ctx.CheckXsrf(f) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if submit != "Delete" {
|
if submit != "Delete" {
|
||||||
|
|
@ -333,7 +330,7 @@ func userNewHandler(ctx *Context) {
|
||||||
adm = true
|
adm = true
|
||||||
}
|
}
|
||||||
submit := ctx.Form("submit")
|
submit := ctx.Form("submit")
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
||||||
|
|
@ -376,7 +373,7 @@ func userEditHandler(ctx *Context) {
|
||||||
adm = true
|
adm = true
|
||||||
}
|
}
|
||||||
submit := ctx.Form("submit")
|
submit := ctx.Form("submit")
|
||||||
if !ctx.CheckToken(nil) {
|
if !ctx.CheckXsrf(nil) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if user == "" || password != repeat || submit != "Submit" {
|
if user == "" || password != repeat || submit != "Submit" {
|
||||||
|
|
@ -438,6 +435,6 @@ func userDelHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutHandler(ctx *Context) {
|
func logoutHandler(ctx *Context) {
|
||||||
ctx.UnsetUdata()
|
ctx.SetCookie(nil)
|
||||||
ctx.Redirect("/wiki/Index", 302)
|
ctx.Redirect("/wiki/Index", 302)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,33 +2,30 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||||
|
"git.giftfish.de/ston1th/jwt"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
keyLen = 32
|
|
||||||
authLen = keyLen * 2
|
|
||||||
cookieName = "gosession"
|
cookieName = "gosession"
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPServer struct {
|
type HTTPServer struct {
|
||||||
Version string
|
Version string
|
||||||
SecCookie bool
|
listener net.Listener
|
||||||
Store *sessions.CookieStore
|
srv *http.Server
|
||||||
DB *db.DB
|
|
||||||
listener net.Listener
|
DB *db.DB
|
||||||
srv *http.Server
|
JWT *jwt.JWT
|
||||||
templ map[string]*template.Template
|
|
||||||
res map[string][]byte
|
templ map[string]*template.Template
|
||||||
|
res map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) {
|
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,
|
Version: version,
|
||||||
SecCookie: secCookie,
|
SecCookie: secCookie,
|
||||||
listener: l,
|
listener: l,
|
||||||
|
JWT: jwt.New(time.Hour*12, nil, nil),
|
||||||
}
|
}
|
||||||
srv.cookieStore()
|
srv.cookieStore()
|
||||||
srv.loadTemplates()
|
srv.loadTemplates()
|
||||||
|
|
@ -46,7 +44,7 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HTTPServer) Start() (err error) {
|
func (s *HTTPServer) Start() (err error) {
|
||||||
s.BS, err = NewBoltStore()
|
s.DB, err = db.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -81,37 +79,3 @@ func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||||
h(newContext(w, r, s))
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
43
pkg/server/xsrf.go
Normal file
43
pkg/server/xsrf.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue