package core import ( "context" "crypto" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "errors" "fmt" "os" "strings" "time" "gitea.mrixs.me/minecraft-platform/backend/internal/database" "gitea.mrixs.me/minecraft-platform/backend/internal/models" "github.com/google/uuid" ) type ProfileService struct { UserRepo *database.UserRepository privateKey *rsa.PrivateKey domain string } // NewProfileService создает новый сервис и загружает приватный ключ. func NewProfileService(repo *database.UserRepository, keyPath, domain string) (*ProfileService, error) { keyData, err := os.ReadFile(keyPath) if err != nil { return nil, fmt.Errorf("failed to read private key file: %w", err) } block, _ := pem.Decode(keyData) if block == nil { return nil, errors.New("failed to parse PEM block containing the key") } privateKey, err := x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { // Попробуем PKCS#8, если PKCS#1 не удался key, errPkcs8 := x509.ParsePKCS8PrivateKey(block.Bytes) if errPkcs8 != nil { return nil, fmt.Errorf("failed to parse private key: %v / %v", err, errPkcs8) } var ok bool privateKey, ok = key.(*rsa.PrivateKey) if !ok { return nil, errors.New("key is not an RSA private key") } } return &ProfileService{ UserRepo: repo, privateKey: privateKey, domain: domain, }, nil } // GetSignedProfile формирует и подписывает профиль игрока. func (s *ProfileService) GetSignedProfile(ctx context.Context, playerUUID uuid.UUID) (*models.SessionProfileResponse, error) { user, profile, err := s.UserRepo.GetProfileByUUID(ctx, playerUUID) if err != nil { return nil, err } // 1. Формируем структуру со свойствами текстур textures := models.Textures{} if profile.SkinHash != "" { textures.SKIN = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.SkinHash)} } if profile.CapeHash != "" { textures.CAPE = &models.TextureInfo{URL: fmt.Sprintf("http://%s/files/textures/%s", s.domain, profile.CapeHash)} } profileID := strings.ReplaceAll(user.UUID.String(), "-", "") propValue := models.ProfilePropertyValue{ Timestamp: time.Now().UnixMilli(), ProfileID: profileID, ProfileName: user.Username, Textures: textures, } // 2. Маршализация в JSON и кодирование в Base64 valueJSON, err := json.Marshal(propValue) if err != nil { return nil, err } valueBase64 := base64.StdEncoding.EncodeToString(valueJSON) // 3. Подпись hasher := sha1.New() hasher.Write([]byte(valueBase64)) hashed := hasher.Sum(nil) signature, err := rsa.SignPKCS1v15(rand.Reader, s.privateKey, crypto.SHA1, hashed) if err != nil { return nil, err } signatureBase64 := base64.StdEncoding.EncodeToString(signature) // 4. Формирование итогового ответа response := &models.SessionProfileResponse{ ID: profileID, Name: user.Username, Properties: []models.ProfileProperty{ { Name: "textures", Value: valueBase64, Signature: signatureBase64, }, }, } return response, nil }