package calls import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "encoding/hex" "net/http" "strconv" "strings" "time" ) const joinCookieName = "calls_join" // joinCookie tells POST /signin which live link this browser followed. // It carries the token's hash, not the token, so a cookie leak is not a // link leak, and it is signed under the instance key so it cannot be // forged. Its only job is that one hand-off (spec ยง3.2). type joinCookie struct { TokenHash string Expires time.Time } func (a *app) joinMAC(payload string) string { mac := hmac.New(sha256.New, []byte("calls/join\x00"+a.cfg.instanceKey)) mac.Write([]byte(payload)) return hex.EncodeToString(mac.Sum(nil)) } func (a *app) setJoinCookie(w http.ResponseWriter, tokenHash string) { exp := a.store.now().Add(pendingLife) payload := tokenHash + "|" + strconv.FormatInt(exp.Unix(), 10) value := base64.RawURLEncoding.EncodeToString([]byte(payload)) + "." + a.joinMAC(payload) http.SetCookie(w, &http.Cookie{ Name: joinCookieName, Value: value, Path: "/", HttpOnly: true, Secure: strings.HasPrefix(a.cfg.Origin, "https://"), SameSite: http.SameSiteLaxMode, MaxAge: int(pendingLife.Seconds()), }) } func (a *app) clearJoinCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{Name: joinCookieName, Value: "", Path: "/", MaxAge: -1, HttpOnly: true}) } func (a *app) readJoinCookie(r *http.Request) (joinCookie, bool) { ck, err := r.Cookie(joinCookieName) if err != nil { return joinCookie{}, false } parts := strings.SplitN(ck.Value, ".", 2) if len(parts) != 2 { return joinCookie{}, false } raw, err := base64.RawURLEncoding.DecodeString(parts[0]) if err != nil { return joinCookie{}, false } payload := string(raw) if !hmac.Equal([]byte(a.joinMAC(payload)), []byte(parts[1])) { return joinCookie{}, false } hash, expStr, ok := strings.Cut(payload, "|") if !ok || len(hash) != 64 { return joinCookie{}, false } exp, err := strconv.ParseInt(expStr, 10, 64) if err != nil { return joinCookie{}, false } c := joinCookie{TokenHash: hash, Expires: time.Unix(exp, 0)} if !c.Expires.After(a.store.now()) { return joinCookie{}, false } return c, true }