package auth import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "fmt" "golang.org/x/crypto/bcrypt" "strconv" "strings" "time" ) const passwordRounds = 150000 func HashPassword(password string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) return string(b), err } func RandomSecret() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic(err) } return base64.RawURLEncoding.EncodeToString(b) } func CheckPassword(encoded, password string) bool { if strings.HasPrefix(encoded, "$2") { return bcrypt.CompareHashAndPassword([]byte(encoded), []byte(password)) == nil } parts := strings.Split(encoded, "$") if len(parts) != 4 || parts[0] != "sha256" { return false } rounds, err := strconv.Atoi(parts[1]) if err != nil || rounds < 10000 || rounds > 1000000 { return false } salt, err := base64.RawStdEncoding.DecodeString(parts[2]) if err != nil { return false } want, err := base64.RawStdEncoding.DecodeString(parts[3]) if err != nil { return false } got := stretch([]byte(password), salt, rounds) return len(got) == len(want) && subtle.ConstantTimeCompare(got, want) == 1 } func stretch(password, salt []byte, rounds int) []byte { buf := append(append([]byte{}, salt...), password...) s := sha256.Sum256(buf) out := s[:] for i := 1; i < rounds; i++ { h := sha256.New() h.Write(out) h.Write(salt) h.Write(password) out = h.Sum(nil) } return out } func SignSession(secret, username string, expires time.Time) string { payload := fmt.Sprintf("%s|%d", username, expires.Unix()) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(payload)) sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) return base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + sig } func VerifySession(secret, token string, now time.Time) (string, bool) { parts := strings.Split(token, ".") if len(parts) != 2 { return "", false } payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return "", false } mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payloadBytes) want := base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) if subtle.ConstantTimeCompare([]byte(want), []byte(parts[1])) != 1 { return "", false } fields := strings.Split(string(payloadBytes), "|") if len(fields) != 2 { return "", false } exp, err := strconv.ParseInt(fields[1], 10, 64) if err != nil || now.Unix() > exp { return "", false } return fields[0], true }