diff --git a/.gitignore b/.gitignore index 81ad378..57cdf3c 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ secrets.go tripwire skins/ capes/ +keys/ diff --git a/auth.go b/auth.go index 263a8da..d96d5e1 100644 --- a/auth.go +++ b/auth.go @@ -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) { diff --git a/config.go b/config.go index 9c4674b..b3f815d 100644 --- a/config.go +++ b/config.go @@ -8,6 +8,7 @@ import ( type Configuration struct { BaseUrl string `yaml:"baseUrl"` + Protocol string `yaml:"protocol"` DebugMode bool `yaml:"debugMode"` MaxTextureSize int `yaml:"maxTextureSize"` } diff --git a/config.yaml b/config.yaml index f792acb..6aa16bf 100644 --- a/config.yaml +++ b/config.yaml @@ -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 diff --git a/go.mod b/go.mod index aa5f3c1..91b217a 100644 --- a/go.mod +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum index 60a3404..0e07ce1 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/main.go b/main.go index 826d584..ee59b47 100644 --- a/main.go +++ b/main.go @@ -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() diff --git a/session.go b/session.go index 84598f3..98e826e 100644 --- a/session.go +++ b/session.go @@ -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 "" } diff --git a/sha.go b/sha.go deleted file mode 100644 index 03c2b91..0000000 --- a/sha.go +++ /dev/null @@ -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 -} diff --git a/signature.go b/signature.go new file mode 100644 index 0000000..4ebaf49 --- /dev/null +++ b/signature.go @@ -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, + }, + ) +} diff --git a/types.go b/types.go index 1fd99ad..1c688d3 100644 --- a/types.go +++ b/types.go @@ -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 { diff --git a/web.go b/web.go index df0d53e..0a003b3 100644 --- a/web.go +++ b/web.go @@ -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") }