add support for 1.19 "Secure Chat Signing"

This commit is contained in:
2022-09-18 02:53:08 +08:00
parent fa86f0cfdb
commit 3dd53caea4
7 changed files with 172 additions and 34 deletions

10
main.go
View File

@@ -104,6 +104,7 @@ func main() {
serverMeta.Meta.ImplementationName = meta.ImplementationName
serverMeta.Meta.ImplementationVersion = meta.ImplementationVersion
serverMeta.Meta.FeatureNoMojangNamespace = true
serverMeta.Meta.FeatureEnableProfileKey = true
serverMeta.Meta.Links.Homepage = meta.SkinRootUrl + "/profile/user.html"
serverMeta.Meta.Links.Register = meta.SkinRootUrl + "/profile/index.html"
serverMeta.SkinDomains = meta.SkinDomains
@@ -160,15 +161,18 @@ func checkRsaKeyFile(privateKeyPath string, publicKeyPath string) {
log.Fatalln("无法序列化 RSA 密钥", err)
}
err = pem.Encode(privatePem, &pem.Block{
Type: "PRIVATE",
Type: "PRIVATE KEY",
Bytes: privateKeyBytes,
})
if err != nil {
log.Fatalln("无法写入私钥文件", err)
}
publicKeyBytes := x509.MarshalPKCS1PublicKey(&privateKey.PublicKey)
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
log.Fatalln("无法序列化 RSA 公钥", err)
}
err = pem.Encode(publicPem, &pem.Block{
Type: "PUBLIC",
Type: "PUBLIC KEY",
Bytes: publicKeyBytes,
})
if err != nil {

View File

@@ -33,6 +33,7 @@ type MetaInfo struct {
FeatureNonEmailLogin bool `json:"feature.non_email_login,omitempty"`
FeatureLegacySkinApi bool `json:"feature.legacy_skin_api,omitempty"`
FeatureNoMojangNamespace bool `json:"feature.no_mojang_namespace,omitempty"`
FeatureEnableProfileKey bool `json:"feature.enable_profile_key,omitempty"`
}
type ServerMeta struct {

View File

@@ -76,4 +76,5 @@ func InitRouters(router *gin.Engine, db *gorm.DB, meta *ServerMeta, skinRootUrl
api.DELETE("/user/profile/:uuid/:textureType", textureRouter.DeleteTexture)
api.GET("/users/profiles/minecraft/:username", userRouter.UsernameToUUID)
}
router.POST("/minecraftservices/player/certificates", userRouter.ProfileKey)
}

View File

@@ -37,6 +37,7 @@ type UserRouter interface {
UsernameToUUID(c *gin.Context)
QueryUUIDs(c *gin.Context)
QueryProfile(c *gin.Context)
ProfileKey(c *gin.Context)
}
type userRouterImpl struct {
@@ -263,3 +264,18 @@ func (u *userRouterImpl) QueryProfile(c *gin.Context) {
}
c.JSON(http.StatusOK, response)
}
func (u *userRouterImpl) ProfileKey(c *gin.Context) {
bearerToken := c.GetHeader("Authorization")
if len(bearerToken) < 8 {
c.AbortWithStatusJSON(http.StatusUnauthorized, util.NewForbiddenOperationError(util.MessageInvalidToken))
return
}
accessToken := bearerToken[7:]
response, err := u.userService.ProfileKey(accessToken)
if err != nil {
util.HandleError(c, err)
return
}
c.JSON(http.StatusOK, response)
}

View File

@@ -47,7 +47,11 @@ func NewSessionService(service TokenService) SessionService {
func (s *sessionStore) JoinServer(accessToken string, serverId string, selectedProfile string, ip string) error {
token, ok := s.tokenService.GetToken(accessToken)
if ok && util.UnsignedString(token.SelectedProfile.Id) == selectedProfile {
if ok {
if token.GetAvailableLevel() != model.Valid ||
util.UnsignedString(token.SelectedProfile.Id) != selectedProfile {
return util.NewForbiddenOperationError(util.MessageInvalidToken)
}
session := model.NewAuthenticationSession(serverId, token, ip)
s.sessionCache.Add(serverId, &session)
} else {

View File

@@ -18,6 +18,11 @@
package service
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"github.com/google/uuid"
lru "github.com/hashicorp/golang-lru"
@@ -28,6 +33,7 @@ import (
"net/url"
"regexp"
"strings"
"time"
"yggdrasil-go/model"
"yggdrasil-go/util"
)
@@ -43,6 +49,7 @@ type UserService interface {
UsernameToUUID(username string) (*model.ProfileResponse, error)
QueryUUIDs(usernames []string) ([]model.ProfileResponse, error)
QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error)
ProfileKey(accessToken string) (*ProfileKeyResponse, error)
}
type LoginResponse struct {
@@ -53,23 +60,43 @@ type LoginResponse struct {
SelectedProfile *model.ProfileResponse `json:"selectedProfile"`
}
type userSrviceImpl struct {
tokenService TokenService
db *gorm.DB
limitLruCache *lru.Cache
type ProfileKeyResponse struct {
ExpiresAt time.Time `json:"expiresAt,omitempty"`
KeyPair *ProfileKeyPair `json:"keyPair,omitempty"`
PublicKeySignature string `json:"publicKeySignature,omitempty"`
PublicKeySignatureV2 string `json:"publicKeySignatureV2,omitempty"`
RefreshedAfter time.Time `json:"refreshedAfter,omitempty"`
}
type ProfileKeyPair struct {
PrivateKey string `json:"privateKey,omitempty"`
PublicKey string `json:"publicKey,omitempty"`
}
type userServiceImpl struct {
tokenService TokenService
db *gorm.DB
limitLruCache *lru.Cache
profileKeyCache *lru.Cache
keyPairCh chan ProfileKeyPair
}
func NewUserService(tokenService TokenService, db *gorm.DB) UserService {
cache, _ := lru.New(10000)
userSrvice := userSrviceImpl{
tokenService: tokenService,
db: db,
limitLruCache: cache,
cache0, _ := lru.New(10000)
cache1, _ := lru.New(10000)
ch := make(chan ProfileKeyPair, 100)
userService := userServiceImpl{
tokenService: tokenService,
db: db,
limitLruCache: cache0,
profileKeyCache: cache1,
keyPairCh: ch,
}
return &userSrvice
go userService.genKeyPair()
return &userService
}
func (u *userSrviceImpl) Register(username string, password string, profileName string) (*model.UserResponse, error) {
func (u *userServiceImpl) Register(username string, password string, profileName string) (*model.UserResponse, error) {
var count int64
if err := u.db.Table("users").Where("email = ?", username).Count(&count).Error; err != nil {
return nil, err
@@ -117,7 +144,7 @@ func isInvalidProfileName(name string) bool {
//return name == "" || !name.matches("^[0-1a-zA-Z_]{2,16}$");
}
func (u *userSrviceImpl) Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error) {
func (u *userServiceImpl) Login(username string, password string, clientToken *string, requestUser bool) (*LoginResponse, error) {
if !u.allowUser(username) {
return nil, util.YggdrasilError{
Status: http.StatusTooManyRequests,
@@ -175,7 +202,7 @@ func (u *userSrviceImpl) Login(username string, password string, clientToken *st
}
}
func (u *userSrviceImpl) ChangeProfile(accessToken string, clientToken *string, changeTo string) error {
func (u *userServiceImpl) ChangeProfile(accessToken string, clientToken *string, changeTo string) error {
if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
return util.NewForbiddenOperationError(util.MessageInvalidToken)
}
@@ -210,7 +237,7 @@ func (u *userSrviceImpl) ChangeProfile(accessToken string, clientToken *string,
return nil
}
func (u *userSrviceImpl) Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error) {
func (u *userServiceImpl) Refresh(accessToken string, clientToken *string, requestUser bool, selectedProfile *model.ProfileResponse) (*LoginResponse, error) {
if len(accessToken) <= 36 {
user := model.User{}
if selectedProfile != nil {
@@ -259,7 +286,7 @@ func (u *userSrviceImpl) Refresh(accessToken string, clientToken *string, reques
}
}
func (u *userSrviceImpl) Validate(accessToken string, clientToken *string) error {
func (u *userServiceImpl) Validate(accessToken string, clientToken *string) error {
if len(accessToken) <= 36 {
if u.tokenService.VerifyToken(accessToken, clientToken) != model.Valid {
return util.NewForbiddenOperationError(util.MessageInvalidToken)
@@ -280,7 +307,7 @@ func (u *userSrviceImpl) Validate(accessToken string, clientToken *string) error
}
}
func (u *userSrviceImpl) Invalidate(accessToken string) error {
func (u *userServiceImpl) Invalidate(accessToken string) error {
if len(accessToken) <= 36 {
u.tokenService.RemoveAccessToken(accessToken)
} else {
@@ -295,7 +322,7 @@ func (u *userSrviceImpl) Invalidate(accessToken string) error {
return nil
}
func (u *userSrviceImpl) Signout(username string, password string) error {
func (u *userServiceImpl) Signout(username string, password string) error {
if !u.allowUser(username) {
return util.YggdrasilError{
Status: http.StatusTooManyRequests,
@@ -325,7 +352,7 @@ func (u *userSrviceImpl) Signout(username string, password string) error {
}
}
func (u *userSrviceImpl) UsernameToUUID(username string) (*model.ProfileResponse, error) {
func (u *userServiceImpl) UsernameToUUID(username string) (*model.ProfileResponse, error) {
user := model.User{}
if result := u.db.Where("profile_name = ?", username).First(&user); result.Error == nil {
return &model.ProfileResponse{
@@ -342,7 +369,7 @@ func (u *userSrviceImpl) UsernameToUUID(username string) (*model.ProfileResponse
}
}
func (u *userSrviceImpl) QueryUUIDs(usernames []string) ([]model.ProfileResponse, error) {
func (u *userServiceImpl) QueryUUIDs(usernames []string) ([]model.ProfileResponse, error) {
var users []model.User
var names []string
if len(usernames) > 10 {
@@ -362,7 +389,7 @@ func (u *userSrviceImpl) QueryUUIDs(usernames []string) ([]model.ProfileResponse
return responses, nil
}
func (u *userSrviceImpl) QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error) {
func (u *userServiceImpl) QueryProfile(profileId uuid.UUID, unsigned bool, textureBaseUrl string) (map[string]interface{}, error) {
user := model.User{}
if err := u.db.First(&user, profileId).Error; err == nil {
profile, err := user.Profile()
@@ -386,7 +413,35 @@ func (u *userSrviceImpl) QueryProfile(profileId uuid.UUID, unsigned bool, textur
}
}
func (u *userSrviceImpl) allowUser(username string) bool {
func (u *userServiceImpl) ProfileKey(accessToken string) (resp *ProfileKeyResponse, err error) {
token, ok := u.tokenService.GetToken(accessToken)
if ok && token.GetAvailableLevel() == model.Valid {
resp = new(ProfileKeyResponse)
now := time.Now().UTC()
resp.RefreshedAfter = now
resp.ExpiresAt = now.Add(time.Hour * 24 * 90)
keyPair, err := u.getProfileKey(token.SelectedProfile.Id)
if err != nil {
return nil, err
}
resp.KeyPair = keyPair
signStr := fmt.Sprintf("%d%s", resp.ExpiresAt.UnixMilli(), keyPair.PublicKey)
sign, err := util.Sign(signStr)
if err != nil {
return nil, err
}
resp.PublicKeySignature = sign
resp.PublicKeySignatureV2 = sign
} else {
err = util.PostForString("https://api.minecraftservices.com/player/certificates", accessToken, []byte(""), resp)
if err != nil {
return nil, err
}
}
return resp, nil
}
func (u *userServiceImpl) allowUser(username string) bool {
if value, ok := u.limitLruCache.Get(username); ok {
if limiter, ok := value.(*rate.Limiter); ok {
return limiter.Allow()
@@ -400,6 +455,51 @@ func (u *userSrviceImpl) allowUser(username string) bool {
return true
}
func (u *userServiceImpl) getProfileKey(profileId uuid.UUID) (*ProfileKeyPair, error) {
if value, ok := u.profileKeyCache.Get(profileId); ok {
if keyPair, ok := value.(*ProfileKeyPair); ok {
return keyPair, nil
}
}
if keyPair, ok := <-u.keyPairCh; ok {
u.profileKeyCache.Add(profileId, &keyPair)
return &keyPair, nil
} else {
return nil, errors.New("unable to generate rsa key pair")
}
}
func (u *userServiceImpl) genKeyPair() {
for {
keyPair := ProfileKeyPair{}
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
util.PrivateKey = privateKey
if err != nil {
close(u.keyPairCh)
panic(err)
}
privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey)
if err != nil {
close(u.keyPairCh)
panic(err)
}
keyPair.PrivateKey = string(pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: privateKeyBytes,
}))
publicKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey)
if err != nil {
close(u.keyPairCh)
panic(err)
}
keyPair.PublicKey = string(pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: publicKeyBytes,
}))
u.keyPairCh <- keyPair
}
}
func mojangUsernameToUUID(username string) (model.ProfileResponse, error) {
response := model.ProfileResponse{}
reqUrl := fmt.Sprintf("https://api.mojang.com/users/profiles/minecraft/%s", url.PathEscape(username))

View File

@@ -19,6 +19,7 @@ package util
import (
"bytes"
"context"
"encoding/json"
"io/ioutil"
"net/http"
@@ -138,28 +139,39 @@ func PostObjectForError(url string, data interface{}) error {
}
}
func PostForString(url string, data []byte) (string, error) {
func PostForString(url string, accessToken string, data []byte, value interface{}) error {
reader := bytes.NewReader(data)
resp, err := http.Post(url, "application/json", reader)
request, err := http.NewRequestWithContext(context.Background(), "POST", url, reader)
if err != nil {
return "", err
return err
}
if accessToken != "" {
request.Header.Set("Authorization", "Bearer "+accessToken)
}
resp, err := http.DefaultClient.Do(request)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNoContent {
return "", nil
return nil
} else if resp.StatusCode/100 == 4 {
decoder := json.NewDecoder(resp.Body)
errResp := YggdrasilError{}
if resp.ContentLength <= 0 {
errResp.Status = resp.StatusCode
return errResp
}
decoder := json.NewDecoder(resp.Body)
err = decoder.Decode(&errResp)
if err != nil {
return "", err
return err
}
return "", errResp
return errResp
} else {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
return err
}
return string(body), nil
return json.Unmarshal(body, value)
}
}