ci / mobile / mobile (Metro-бандл Android) (push) Successful in 55s
ci / ts / ts (packages + web) (push) Successful in 1m0s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 34s
ci / api / api (push) Successful in 26s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 34s
- covers are taken from the embedded picture of the first track or a
cover/folder/front image in the album folder, converted by ffmpeg to
WebP 64/300/1200 and named by content hash, so albums sharing a cover
share the files; a full scan looks again for albums without one
- a 3-colour palette (dominant, second, most saturated accent) from a
deterministic k-means feeds placeholders, page tints and the wave
- /covers/{id} is public but HMAC-signed and immutable; nginx serves
the file via X-Accel-Redirect, X_ACCEL=false serves it from api
- the web client picks the size by display size and pixel density
- install.sh and update.sh generate STREAM_SIGNING_KEY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
47 lines
1.7 KiB
Go
47 lines
1.7 KiB
Go
// Package sign — HMAC-подписи ссылок, которые клиент открывает без заголовка Authorization
|
|
// (обложки в <img>, стрим в <audio>; 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...)))
|
|
}
|