skins work!!!!!!!!!!!!

This commit is contained in:
TaiAurori 2022-06-25 18:09:27 -04:00
parent c266a5d9fd
commit 77ce652e28
12 changed files with 239 additions and 68 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ secrets.go
tripwire
skins/
capes/
keys/

22
auth.go
View File

@ -1,13 +1,18 @@
package main
import (
"crypto/x509"
"net/http"
"github.com/gorilla/mux"
)
func rootEndpoint(w http.ResponseWriter, r *http.Request) {
sendJSON(w, YggdrasilInfo{
skinDomains := []string{
config.BaseUrl,
}
response := YggdrasilInfo{
Status: "OK",
RuntimeMode: "productionMode",
AppAuthor: "Tripwire Team",
@ -16,7 +21,20 @@ func rootEndpoint(w http.ResponseWriter, r *http.Request) {
AppName: "tripwire.yggdrasil",
ImplVersion: "5.2.0",
AppOwner: "Who knows?",
})
//Used by authlib-injector
SkinDomains: skinDomains,
}
if publicKey != nil {
pubkey, err := x509.MarshalPKIXPublicKey(publicKey)
if err == nil {
pubKeyString := string(encodePem(pubkey, "PUBLIC KEY"))
response.PublicKey = &pubKeyString
}
}
sendJSON(w, response)
}
func authenticateEndpoint(w http.ResponseWriter, r *http.Request) {

View File

@ -8,6 +8,7 @@ import (
type Configuration struct {
BaseUrl string `yaml:"baseUrl"`
Protocol string `yaml:"protocol"`
DebugMode bool `yaml:"debugMode"`
MaxTextureSize int `yaml:"maxTextureSize"`
}

View File

@ -1,5 +1,9 @@
# The URL the Tripwire instance is hosted on.
baseUrl: "http://localhost:10000"
# The primary domain the Tripwire instance is hosted on.
# It shouldn't be a local IP in production.
baseUrl: "localhost:10000"
# The protocol to be used for Tripwire
protocol: "http://"
# Show debug output (if unsure, set to false)
debugMode: true

5
go.mod
View File

@ -8,3 +8,8 @@ require (
github.com/mattn/go-sqlite3 v1.14.13
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect
)

4
go.sum
View File

@ -4,6 +4,10 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/mattn/go-sqlite3 v1.14.13 h1:1tj15ngiFfcZzii7yd82foL+ks+ouQcj8j/TPq3fk1I=
github.com/mattn/go-sqlite3 v1.14.13/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU=
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k=
golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

17
main.go
View File

@ -38,11 +38,6 @@ func handleRequests() {
r.Use(logger)
r.NotFoundHandler = http.HandlerFunc(notfoundlogger)
err := loadConfig()
if err != nil {
log.Fatalln("Failed to load config.yaml:", err)
}
// todo: make this cleaner if possible
registerAuthEndpoints(r)
registerSessionEndpoints(r)
@ -53,10 +48,22 @@ func handleRequests() {
}
func main() {
if len(os.Args) > 1 && os.Args[1] == "gen-keys" {
genKeys()
return
}
log.Println("Tripwire initializing...")
os.Mkdir("skins", 0755)
os.Mkdir("capes", 0755)
err := loadConfig()
if err != nil {
log.Fatalln("Failed to load config.yaml:", err)
}
initDB()
initKeys()
handleRequests()

View File

@ -12,6 +12,20 @@ import (
func profileEndpoint(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
query := r.URL.Query()
if len(uuid) == 32 {
uuid = growUUID(uuid)
}
if len(uuid) != 36 {
sendError(w, YggError{
Code: 400,
Error: "Bad Request",
ErrorMessage: "Invalid UUID.",
})
return
}
exists, err := playerExistsByUUID(uuid)
if err != nil {
@ -33,7 +47,9 @@ func profileEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
response, err := generateProfileResponse(uuid, player.Username)
signed := query.Has("unsigned") && query.Get("unsigned") == "false"
response, err := generateProfileResponse(uuid, player.Username, signed)
if err != nil {
handleError(w, err)
return
@ -55,7 +71,7 @@ func hasJoinedEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
response, err := generateProfileResponse(player.UUID, params.Get("username"))
response, err := generateProfileResponse(player.UUID, params.Get("username"), true)
if err != nil {
handleError(w, err)
return
@ -100,10 +116,10 @@ func registerSessionEndpoints(r *mux.Router) {
r.HandleFunc(prefix+"/session/minecraft/hasJoined", hasJoinedEndpoint).Methods("GET")
}
func generateProfileResponse(uuid string, username string) (ProfileResponse, error) {
func generateProfileResponse(uuid string, username string, signed bool) (ProfileResponse, error) {
// todo: make this more visually appealing if possible
skin := SkinTexture{}
skin.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=skin"
skin.Url = config.Protocol + config.BaseUrl + "/getTexture/" + uuid + "?type=skin"
skin.Metadata = SkinMetadata{}
skin.Metadata.Model = "default"
@ -116,7 +132,7 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
_, err := os.Stat("capes/" + uuid + ".png")
if err == nil {
cape := &Texture{}
cape.Url = config.BaseUrl + "/getTexture/" + uuid + "?type=cape"
cape.Url = config.Protocol + config.BaseUrl + "/getTexture/" + uuid + "?type=cape"
textures.Textures.Cape = cape
}
@ -124,6 +140,7 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
if err != nil {
return ProfileResponse{}, err
}
encodedTextures := base64.StdEncoding.EncodeToString(marshalledTextures)
response := ProfileResponse{}
@ -135,6 +152,19 @@ func generateProfileResponse(uuid string, username string) (ProfileResponse, err
Value: encodedTextures,
},
}
if signed && privateKey != nil {
signedTextures, err := signWithPrivateKey(string(encodedTextures))
if err != nil {
return ProfileResponse{}, err
}
b64 := make([]byte, base64.StdEncoding.EncodedLen(len(signedTextures)))
base64.StdEncoding.Encode(b64, signedTextures)
stringified := string(b64)
response.Properties[0].Signature = &stringified
}
return response, nil
}
@ -144,7 +174,7 @@ func shrinkUUID(uuid string) string {
func growUUID(uuid string) string {
if len(uuid) == 32 {
return uuid[0:7] + "-" + uuid[8:11] + "-" + uuid[12:15] + "-" + uuid[16:19] + "-" + uuid[20:31]
return uuid[0:8] + "-" + uuid[8:12] + "-" + uuid[12:16] + "-" + uuid[16:20] + "-" + uuid[20:32]
}
return ""
}

39
sha.go
View File

@ -1,39 +0,0 @@
// Ripped from https://gist.github.com/toqueteos/5372776
package main
import (
"crypto/sha1"
"encoding/hex"
"io"
"strings"
)
func authDigest(s string) string {
h := sha1.New()
io.WriteString(h, s)
hash := h.Sum(nil)
negative := (hash[0] & 0x80) == 0x80
if negative {
hash = twosComplement(hash)
}
res := strings.TrimLeft(hex.EncodeToString(hash), "0")
if negative {
res = "-" + res
}
return res
}
func twosComplement(p []byte) []byte {
carry := true
for i := len(p) - 1; i >= 0; i-- {
p[i] = byte(^p[i])
if carry {
carry = p[i] == 0xff
p[i]++
}
}
return p
}

124
signature.go Normal file
View File

@ -0,0 +1,124 @@
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha1"
"crypto/x509"
"encoding/pem"
"errors"
"io/fs"
"log"
"os"
)
var publicKey *rsa.PublicKey
var privateKey *rsa.PrivateKey
func initKeys() {
pubKeyBytes, err1 := os.ReadFile("keys/public.key")
privKeyBytes, err2 := os.ReadFile("keys/private.key")
if err1 != nil || err2 != nil {
log.Println("WARNING: At least one key half could not be opened, players will not have any textures!")
log.Println("Try generating a keypair by running \"tripwire gen-keys\".")
if config.DebugMode {
log.Println(err1)
log.Println(err2)
}
return
}
pubDer, _ := pem.Decode(pubKeyBytes)
privDer, _ := pem.Decode(privKeyBytes)
pubKey, err1 := x509.ParsePKIXPublicKey(pubDer.Bytes)
privKey, err2 := x509.ParsePKCS8PrivateKey(privDer.Bytes)
if err1 != nil || err2 != nil {
log.Println("WARNING: At least one key half could not be loaded, players will not have any textures!")
log.Println("Try generating a keypair by running \"tripwire gen-keys\".")
if config.DebugMode {
log.Println(err1)
log.Println(err2)
}
return
}
publicKey = pubKey.(*rsa.PublicKey)
privateKey = privKey.(*rsa.PrivateKey)
}
func genKeys() {
os.Mkdir("keys", 0700)
_, err1 := os.Stat("keys/public.key")
_, err2 := os.Stat("keys/public.key")
if err1 == nil || err2 == nil {
log.Println(
"Error: At least one key half is already present. " +
"If you are having errors reading the key " +
"files, you have likely incorrectly configured " +
"folder permissions.",
)
log.Println(
"If you would like to generate a new keypair anyway, " +
"delete the keys folder and run this command again.",
)
os.Exit(1)
}
if !errors.Is(err1, fs.ErrNotExist) || !errors.Is(err2, fs.ErrNotExist) {
log.Fatalln(
"Error: Could not access keys folder. " +
"Try recreating the folder, or running this command " +
"as a user that has permissions to view it.",
)
}
log.Println("Generating RSA keypair at 4096 bits...")
privkey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
log.Fatalln(err)
}
pubkeygen, err := x509.MarshalPKIXPublicKey(&privkey.PublicKey)
if err != nil {
log.Fatalln(err)
}
err = os.WriteFile(
"keys/public.key",
encodePem(pubkeygen, "PUBLIC KEY"),
0600,
)
if err != nil {
log.Fatalln(err)
}
privkeygen, err := x509.MarshalPKCS8PrivateKey(privkey)
if err != nil {
log.Fatalln(err)
}
err = os.WriteFile(
"keys/private.key",
encodePem(privkeygen, "PRIVATE KEY"),
0600,
)
if err != nil {
log.Fatalln(err)
}
log.Println("Done!")
}
func signWithPrivateKey(value string) ([]byte, error) {
hasher := sha1.New()
hasher.Write([]byte(value))
return rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA1, hasher.Sum(nil))
}
func encodePem(in []byte, name string) []byte {
return pem.EncodeToMemory(
&pem.Block{
Type: name,
Bytes: in,
},
)
}

View File

@ -67,19 +67,22 @@ type PlayerData struct {
}
type YggdrasilInfo struct {
Status string `json:"Status"`
RuntimeMode string `json:"Runtime-Mode"`
AppAuthor string `json:"Application-Author"`
AppDescription string `json:"Application-Description"`
SpecVersion string `json:"Specification-Version"`
AppName string `json:"Application-Name"`
ImplVersion string `json:"Implementation-Version"`
AppOwner string `json:"Application-Owner"`
Status string `json:"Status"`
RuntimeMode string `json:"Runtime-Mode"`
AppAuthor string `json:"Application-Author"`
AppDescription string `json:"Application-Description"`
SpecVersion string `json:"Specification-Version"`
AppName string `json:"Application-Name"`
ImplVersion string `json:"Implementation-Version"`
AppOwner string `json:"Application-Owner"`
SkinDomains []string `json:"skinDomains"`
PublicKey *string `json:"signaturePublickey"`
}
type Property struct {
Name string `json:"name"`
Value string `json:"value"`
Name string `json:"name"`
Value string `json:"value"`
Signature *string `json:"signature"`
}
type ProfileResponse struct {

21
web.go
View File

@ -69,7 +69,7 @@ func logInEndpoint(w http.ResponseWriter, r *http.Request) {
}
}
func getResourceEndpoint(w http.ResponseWriter, r *http.Request) {
func getTextureEndpoint(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
query := r.URL.Query()
if !query.Has("type") {
@ -107,7 +107,7 @@ func getResourceEndpoint(w http.ResponseWriter, r *http.Request) {
w.Write(skin)
}
func setResourceEndpoint(w http.ResponseWriter, r *http.Request) {
func setTextureEndpoint(w http.ResponseWriter, r *http.Request) {
uuid := mux.Vars(r)["uuid"]
query := r.URL.Query()
if !query.Has("type") {
@ -124,6 +124,19 @@ func setResourceEndpoint(w http.ResponseWriter, r *http.Request) {
return
}
if len(uuid) == 32 {
uuid = growUUID(uuid)
}
if len(uuid) != 36 {
sendError(w, YggError{
Code: 400,
Error: "Bad Request",
ErrorMessage: "Invalid UUID.",
})
return
}
r.ParseMultipartForm(int64(config.MaxTextureSize))
token := r.FormValue("token")
@ -196,6 +209,6 @@ func registerWebEndpoints(r *mux.Router) {
r.PathPrefix(webDir).
Handler(http.StripPrefix(webDir, http.FileServer(http.Dir("."+webDir))))
r.HandleFunc("/getTexture/{uuid}", getResourceEndpoint).Methods("GET")
r.HandleFunc("/setTexture/{uuid}", setResourceEndpoint).Methods("POST")
r.HandleFunc("/getTexture/{uuid}", getTextureEndpoint).Methods("GET")
r.HandleFunc("/setTexture/{uuid}", setTextureEndpoint).Methods("POST")
}