package storage import ( "context" "crypto/rand" "encoding/hex" "fmt" "time" ) const ( ChallengeLength = 32 // bytes ChallengeTTL = 2 * time.Minute ) func generateChallenge() (string, error) { bytes := make([]byte, ChallengeLength) if _, err := rand.Read(bytes); err != nil { return "", fmt.Errorf("failed to generate challenge: %w", err) } return hex.EncodeToString(bytes), nil } func (s *Storage) CreateAuthChallenge(ctx context.Context) (string, error) { challenge, err := generateChallenge() if err != nil { return "", err } now := time.Now().Unix() expiresAt := time.Now().Add(ChallengeTTL).Unix() _, err = s.db.ExecContext(ctx, "INSERT INTO auth_challenges (challenge, created_at, expires_at, used) VALUES (?, ?, ?, 0)", challenge, now, expiresAt, ) if err != nil { return "", fmt.Errorf("failed to store challenge: %w", err) } return challenge, nil } func (s *Storage) ValidateAndConsumeChallenge(ctx context.Context, challenge string) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin transaction: %w", err) } defer tx.Rollback() var expiresAt int64 var used int err = tx.QueryRowContext(ctx, "SELECT expires_at, used FROM auth_challenges WHERE challenge = ?", challenge, ).Scan(&expiresAt, &used) if err != nil { return fmt.Errorf("challenge not found or invalid") } if used != 0 { return fmt.Errorf("challenge already used") } if time.Now().Unix() > expiresAt { return fmt.Errorf("challenge expired") } _, err = tx.ExecContext(ctx, "UPDATE auth_challenges SET used = 1 WHERE challenge = ?", challenge, ) if err != nil { return fmt.Errorf("failed to mark challenge as used: %w", err) } return tx.Commit() } func (s *Storage) CleanupExpiredChallenges(ctx context.Context) error { now := time.Now().Unix() _, err := s.db.ExecContext(ctx, "DELETE FROM auth_challenges WHERE expires_at < ?", now, ) return err }