package notification import ( "crypto/hmac" "crypto/sha256" "encoding/base64" "errors" "strings" ) // UnsubscribeSigner mints + verifies tamper-proof unsubscribe tokens. Each // token encodes the email address and an HMAC-SHA256 of the address under // a server-side secret. The token has no TTL — unsubscribe links should // keep working forever. // // Token shape: base64url(email) + "." + base64url(hmac) type UnsubscribeSigner struct { secret []byte } func NewUnsubscribeSigner(secret string) *UnsubscribeSigner { return &UnsubscribeSigner{secret: []byte(secret)} } // Sign returns a URL-safe token for the email address. Empty input → empty // token (caller should validate input first). func (s *UnsubscribeSigner) Sign(email string) string { if email == "" { return "" } email = normaliseEmail(email) mac := hmac.New(sha256.New, s.secret) mac.Write([]byte(email)) return base64.RawURLEncoding.EncodeToString([]byte(email)) + "." + base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) } // Verify decodes the token and confirms the HMAC matches, returning the // owning email address. func (s *UnsubscribeSigner) Verify(token string) (string, error) { dot := strings.IndexByte(token, '.') if dot < 0 { return "", errors.New("malformed unsubscribe token") } emailB, err := base64.RawURLEncoding.DecodeString(token[:dot]) if err != nil { return "", err } sigB, err := base64.RawURLEncoding.DecodeString(token[dot+1:]) if err != nil { return "", err } mac := hmac.New(sha256.New, s.secret) mac.Write(emailB) want := mac.Sum(nil) if !hmac.Equal(sigB, want) { return "", errors.New("unsubscribe signature mismatch") } return string(emailB), nil }