package auth import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "net/http" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" ) const unlockLifetime = 24 * time.Hour type Artifact struct { secret []byte secure bool } func NewArtifact(secretHex string, secure bool) *Artifact { return &Artifact{secret: []byte(secretHex), secure: secure} } func HashPassword(p string) (string, error) { b, err := bcrypt.GenerateFromPassword([]byte(p), bcrypt.DefaultCost) return string(b), err } func VerifyPassword(hash, p string) bool { return bcrypt.CompareHashAndPassword([]byte(hash), []byte(p)) == nil } func unlockCookieName(slug string) string { return "artifacto_unlock_" + slug } func (a *Artifact) Issue(slug string) string { issued := strconv.FormatInt(time.Now().Unix(), 10) return issued + "." + a.sign(slug+"|"+issued) } func (a *Artifact) SetCookie(w http.ResponseWriter, slug string) { http.SetCookie(w, &http.Cookie{ Name: unlockCookieName(slug), Value: a.Issue(slug), Path: "/p/" + slug, HttpOnly: true, Secure: a.secure, SameSite: http.SameSiteLaxMode, Expires: time.Now().Add(unlockLifetime), }) } func (a *Artifact) Verify(r *http.Request, slug string) bool { c, err := r.Cookie(unlockCookieName(slug)) if err != nil { return false } parts := strings.SplitN(c.Value, ".", 2) if len(parts) != 2 { return false } issued, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return false } if time.Since(time.Unix(issued, 0)) > unlockLifetime { return false } expected := a.sign(slug + "|" + parts[0]) return hmac.Equal([]byte(expected), []byte(parts[1])) } func (a *Artifact) sign(s string) string { m := hmac.New(sha256.New, a.secret) m.Write([]byte(s)) return base64.RawURLEncoding.EncodeToString(m.Sum(nil)) }