// Package sign — HMAC-подписи ссылок, которые клиент открывает без заголовка Authorization // (обложки в , стрим в ; docs/decisions.md, ADR-006). package sign import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "encoding/base64" "fmt" "strings" ) // Signer подписывает наборы строк ключом сервера. type Signer struct{ key []byte } // New создаёт Signer. Пустой ключ — случайный: ссылки перестанут действовать после перезапуска. func New(keyBase64 string) (*Signer, error) { if strings.TrimSpace(keyBase64) == "" { key := make([]byte, 32) if _, err := rand.Read(key); err != nil { return nil, err } return &Signer{key: key}, nil } key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(keyBase64)) if err != nil || len(key) < 16 { return nil, fmt.Errorf("sign: key must be base64 of at least 16 bytes") } return &Signer{key: key}, nil } // Sign возвращает подпись частей (base64url, 22 символа — первые 16 байт HMAC-SHA256). func (s *Signer) Sign(parts ...string) string { m := hmac.New(sha256.New, s.key) for _, p := range parts { _, _ = m.Write([]byte(p)) _, _ = m.Write([]byte{0}) // разделитель: ("ab","c") и ("a","bc") подписываются по-разному } return base64.RawURLEncoding.EncodeToString(m.Sum(nil)[:16]) } // Verify проверяет подпись за постоянное время. func (s *Signer) Verify(sig string, parts ...string) bool { return hmac.Equal([]byte(sig), []byte(s.Sign(parts...))) }