MUMBLE "failed to get access using private key": чекер держит share-токен, выданный ещё старым keygen, а после перехода на HMAC ValidateKey его отвергал. - keygen/legacy.go: восстановленный из keygen.a алгоритм seed=(seed*17+42)%62 принимается ТОЛЬКО для video.id <= cutoff, то есть для видео, существовавших на момент перехода. Выше cutoff -- лишь HMAC. - cutoff берётся из max(video.id) при первом старте и пишется в public/.legacy_cutoff на volume: рестарт не должен расширять окно. Не смогли прочитать -- fail closed, legacy выключен. - Когда старые флаги протухнут: echo 0 > public/.legacy_cutoff + рестарт, и legacy отключается полностью. Ещё MUMBLE "cannot create video" -- он же DoS, у NOP-команды то же самое: - checkGeometry сделан FAIL-OPEN. Неразобранный ffprobe больше не отклоняет загрузку, режем только успешно прочитанную и абсурдную геометрию. - ffmpeg -max_alloc 128M: 1080p кадру нужно ~8 МБ, бомбе ~1 ГБ. Работает даже когда probe промолчал. check_tiktak.sh: smoke-тест для vulnbox, гоняет сценарий чекера целиком и отдельно проверяет, что патчи реально в задеплоенном билде. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
149 lines
4.2 KiB
Go
149 lines
4.2 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"fmt"
|
|
|
|
"github.com/gocraft/dbr/v2"
|
|
)
|
|
|
|
// pepper is mixed into every stored password. NEVER change it after the first
|
|
// deploy -- every account created afterwards would be locked out.
|
|
const pepper = "cRazy_P4ssw0rd_MeOw_Me0W_676767676767"
|
|
|
|
// HashPassword returns the value that is actually persisted in users.password.
|
|
func HashPassword(password string) string {
|
|
h := sha256.New()
|
|
h.Write([]byte(pepper))
|
|
h.Write([]byte(password))
|
|
return fmt.Sprintf("sha256$%x", h.Sum(nil))
|
|
}
|
|
|
|
// passwordMatches accepts the new hashed form and, for accounts created before
|
|
// this patch, the legacy plaintext form -- otherwise existing users (including
|
|
// the checker's) would be locked out and the SLA would drop.
|
|
func passwordMatches(stored, supplied string) bool {
|
|
if subtle.ConstantTimeCompare([]byte(stored), []byte(HashPassword(supplied))) == 1 {
|
|
return true
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(stored), []byte(supplied)) == 1
|
|
}
|
|
|
|
type Storage struct {
|
|
conn *dbr.Connection
|
|
}
|
|
|
|
func New(conn *dbr.Connection) *Storage {
|
|
s := Storage{conn}
|
|
s.init()
|
|
return &s
|
|
}
|
|
|
|
func (s *Storage) init() {
|
|
for _, q := range []string{
|
|
"CREATE TABLE IF NOT EXISTS users(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, login VARCHAR(200) UNIQUE , password VARCHAR(200))",
|
|
"CREATE TABLE IF NOT EXISTS video(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, user_id INTEGER, description VARCHAR(500), private BOOLEAN, link VARCHAR(400))",
|
|
"CREATE TABLE IF NOT EXISTS access(id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, user_id INTEGER, video_id INTEGER)",
|
|
} {
|
|
_, err := s.conn.Exec(q)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *Storage) InsertUser(ctx context.Context, user *User) error {
|
|
sess := s.conn.NewSession(nil)
|
|
user.Password = HashPassword(user.Password)
|
|
_, err := sess.InsertInto("users").Columns("login", "password").Record(user).ExecContext(ctx)
|
|
return err
|
|
}
|
|
|
|
func (s *Storage) FindUser(ctx context.Context, login, password string) (*User, error) {
|
|
sess := s.conn.NewSession(nil)
|
|
user := new(User)
|
|
// Look the account up by login only, then verify the secret in Go: the
|
|
// password no longer travels through the WHERE clause as a comparable value.
|
|
err := sess.Select("*").
|
|
From("users").
|
|
Where("login = ?", login).
|
|
LoadOneContext(ctx, user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !passwordMatches(user.Password, password) {
|
|
return nil, dbr.ErrNotFound
|
|
}
|
|
return user, nil
|
|
}
|
|
|
|
func (s *Storage) AddVideo(ctx context.Context, v *Video) error {
|
|
sess := s.conn.NewSession(nil)
|
|
_, err := sess.InsertInto("video").
|
|
Columns("user_id", "description", "private", "link").
|
|
Record(v).ExecContext(ctx)
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *Storage) ListVideo(ctx context.Context, limit int) (v []Video, err error) {
|
|
sess := s.conn.NewSession(nil)
|
|
_, err = sess.Select("*").
|
|
From("video").
|
|
OrderDesc("id").
|
|
Limit(uint64(limit)).
|
|
LoadContext(ctx, &v)
|
|
return
|
|
}
|
|
|
|
func (s *Storage) ListUserVideo(ctx context.Context, userId int64) (v []Video, err error) {
|
|
sess := s.conn.NewSession(nil)
|
|
_, err = sess.Select("*").
|
|
From("video").
|
|
Where("user_id = ?", userId).
|
|
OrderDesc("id").LoadContext(ctx, &v)
|
|
return
|
|
}
|
|
|
|
func (s *Storage) GetVideo(ctx context.Context, id interface{}) (*Video, error) {
|
|
sess := s.conn.NewSession(nil)
|
|
v := new(Video)
|
|
err := sess.Select("*").From("video").Where("id = ?", id).LoadOneContext(ctx, v)
|
|
return v, err
|
|
}
|
|
|
|
func (s *Storage) HaveAccess(ctx context.Context, id interface{}, userId int64) bool {
|
|
sess := s.conn.NewSession(nil)
|
|
|
|
var c int
|
|
err := sess.Select("count(*)").
|
|
From("access").
|
|
Where("user_id = ?", userId).
|
|
Where("video_id = ?", id).
|
|
LoadOneContext(ctx, &c)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return c > 0
|
|
}
|
|
|
|
func (s *Storage) AddAccess(ctx context.Context, id int, userId int64) error {
|
|
sess := s.conn.NewSession(nil)
|
|
|
|
_, err := sess.InsertInto("access").
|
|
Pair("video_id", id).
|
|
Pair("user_id", userId).
|
|
ExecContext(ctx)
|
|
return err
|
|
}
|
|
|
|
// MaxVideoID reports the highest video id currently stored, 0 when empty.
|
|
func (s *Storage) MaxVideoID(ctx context.Context) (int64, error) {
|
|
sess := s.conn.NewSession(nil)
|
|
var id int64
|
|
err := sess.Select("COALESCE(MAX(id), 0)").From("video").LoadOneContext(ctx, &id)
|
|
return id, err
|
|
}
|