package gate import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "strconv" "strings" "time" ) // The cookie is v1.. // with key derived from the stored bcrypt hash and nothing else. That is // what makes the design stateless and gives it its one rotation rule: // change or clear the password and every cookie minted under the old one // stops verifying at once, on every box, with no key to mint or mirror. // Anyone who can read the hash can mint a cookie — root on the box and a // bucket reader, who already hold the tenant's sealed secrets (spec §4.3). const cookieVersion = "v1" func cookieKey(hash string) []byte { m := hmac.New(sha256.New, []byte("carlos-gate-cookie-v1")) m.Write([]byte(hash)) return m.Sum(nil) } func cookieMAC(key []byte, host string, expiry int64) []byte { m := hmac.New(sha256.New, key) m.Write([]byte(host)) m.Write([]byte{'\n'}) m.Write([]byte(strconv.FormatInt(expiry, 10))) return m.Sum(nil) } // Mint produces a cookie value for host, valid until expiry. func Mint(host, hash string, expiry time.Time) string { exp := expiry.Unix() mac := cookieMAC(cookieKey(hash), host, exp) return cookieVersion + "." + strconv.FormatInt(exp, 10) + "." + base64.RawURLEncoding.EncodeToString(mac) } // Verify reports whether value was minted for host under hash and has not // expired at now. One HMAC and a constant-time compare; no bcrypt. func Verify(value, host, hash string, now time.Time) bool { parts := strings.Split(value, ".") if len(parts) != 3 || parts[0] != cookieVersion { return false } exp, err := strconv.ParseInt(parts[1], 10, 64) // One spelling per expiry: ParseInt accepts "+123" and "-0", which // would MAC differently from what Mint wrote for the same instant. if err != nil || parts[1] != strconv.FormatInt(exp, 10) || exp <= now.Unix() { return false } got, err := base64.RawURLEncoding.DecodeString(parts[2]) if err != nil { return false } return hmac.Equal(got, cookieMAC(cookieKey(hash), host, exp)) }