From 5cbf9f81a8716ede668367befd1e809dba38ef1b Mon Sep 17 00:00:00 2001 From: Spotlight Date: Thu, 20 Aug 2020 22:57:04 -0500 Subject: [PATCH] Add proper registration response --- database.sql | 21 +++++++----- go.mod | 1 + go.sum | 2 ++ ias.go | 95 ++++++++++++++++++++++++++++++++++++++++++++++------ main.go | 7 ++-- utils.go | 21 ++++++++++-- 6 files changed, 124 insertions(+), 23 deletions(-) diff --git a/database.sql b/database.sql index 35b624f..6ed182d 100644 --- a/database.sql +++ b/database.sql @@ -36,16 +36,19 @@ SET time_zone = "+00:00"; -- CREATE TABLE `userbase` ( - `DeviceId` int(10) UNSIGNED ZEROFILL NOT NULL, - `DeviceToken` varchar(21) NOT NULL, - `AccountId` int(9) UNSIGNED ZEROFILL NOT NULL, - `Region` varchar(2) DEFAULT NULL, - `Country` varchar(2) DEFAULT NULL, - `Language` varchar(2) DEFAULT NULL, - `SerialNo` varchar(11) DEFAULT NULL, - `DeviceCode` int(16) UNSIGNED ZEROFILL NOT NULL + `DeviceId` varchar(10) NOT NULL, + `DeviceToken` varchar(64) NOT NULL COMMENT 'This token should be considered a secret, so after generation only the sha256sum of the md5 the Wii sends is inserted.', + `AccountId` varchar(9) NOT NULL, + `Region` varchar(2) NOT NULL, + `Country` varchar(2) NOT NULL, + `Language` varchar(2) NOT NULL, + `SerialNo` varchar(11) NOT NULL, + `DeviceCode` varchar(16) NOT NULL, + PRIMARY KEY (`AccountId`), + UNIQUE KEY `AccountId` (`AccountId`), + UNIQUE KEY `userbase_DeviceId_uindex` (`DeviceId`), + UNIQUE KEY `userbase_DeviceToken_uindex` (`DeviceToken`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -COMMIT; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; diff --git a/go.mod b/go.mod index fe94377..e502ec3 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/morenatsu-net/WiiSOAP go 1.12 require ( + github.com/RiiConnect24/wiino v0.0.0-20200719211820-910fed2fa406 github.com/antchfx/xmlquery v1.2.4 github.com/go-sql-driver/mysql v1.5.0 ) diff --git a/go.sum b/go.sum index bc8c909..cb2727a 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/RiiConnect24/wiino v0.0.0-20200719211820-910fed2fa406 h1:io4hcjlXc3gsbSLtIR7Xd7nK+Tngl3hHrPVlI7aZfR0= +github.com/RiiConnect24/wiino v0.0.0-20200719211820-910fed2fa406/go.mod h1:BmIQ5QOpoum6rxYqqAjdpwwdfoQeKVi1xnxeEU+56ro= github.com/antchfx/xmlquery v1.2.4 h1:T/SH1bYdzdjTMoz2RgsfVKbM5uWh3gjDYYepFqQmFv4= github.com/antchfx/xmlquery v1.2.4/go.mod h1:KQQuESaxSlqugE2ZBcM/qn+ebIpt+d+4Xx7YcSGAIrM= github.com/antchfx/xpath v1.1.6 h1:6sVh6hB5T6phw1pFpHRQ+C4bd8sNI+O58flqtg7h0R0= diff --git a/ias.go b/ias.go index 5aa9b3d..376eb3f 100644 --- a/ias.go +++ b/ias.go @@ -18,11 +18,32 @@ package main import ( + "crypto/md5" + sha2562 "crypto/sha256" + "errors" "fmt" + "github.com/RiiConnect24/wiino/golang" "github.com/antchfx/xmlquery" + "github.com/go-sql-driver/mysql" + "log" + "math/rand" + "strconv" ) func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) { + // All IAS-related functions should contain these keys. + region, err := getKey(doc, "Region") + if err != nil { + return e.ReturnError(5, "not good enough for me. ;3", err) + } + country, err := getKey(doc, "Country") + if err != nil { + return e.ReturnError(5, "not good enough for me. ;3", err) + } + language, err := getKey(doc, "Language") + if err != nil { + return e.ReturnError(5, "not good enough for me. ;3", err) + } // All actions below are for IAS-related functions. switch e.Action() { @@ -50,14 +71,15 @@ func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) { break case "GetRegistrationInfo": + reason := "how dirty. ;3" accountId, err := getKey(doc, "AccountId") if err != nil { - return e.ReturnError(7, "how dirty. ;3", err) + return e.ReturnError(7, reason, err) } - country, err := getKey(doc, "Country") + deviceCode, err := getKey(doc, "DeviceCode") if err != nil { - return e.ReturnError(7, "how dirty. ;3", err) + return e.ReturnError(7, reason, err) } fmt.Println("The request is valid! Responding...") @@ -66,29 +88,82 @@ func iasHandler(e Envelope, doc *xmlquery.Node) (bool, string) { e.AddKVNode("DeviceTokenExpired", "false") e.AddKVNode("Country", country) e.AddKVNode("ExtAccountId", "") - e.AddKVNode("DeviceCode", "0000000000000000") + e.AddKVNode("DeviceCode", deviceCode) e.AddKVNode("DeviceStatus", "R") // This _must_ be POINTS. e.AddKVNode("Currency", "POINTS") break case "Register": - accountId, err := getKey(doc, "AccountId") + reason := "disgustingly invalid. ;3" + deviceCode, err := getKey(doc, "DeviceCode") if err != nil { - return e.ReturnError(8, "disgustingly invalid. ;3", err) + return e.ReturnError(7, reason, err) } - country, err := getKey(doc, "Country") + registerRegion, err := getKey(doc, "RegisterRegion") if err != nil { - return e.ReturnError(8, "disgustingly invalid. ;3", err) + return e.ReturnError(7, reason, err) + } + if registerRegion != region { + return e.ReturnError(7, reason, errors.New("region does not match registration region")) + } + + serialNo, err := getKey(doc, "SerialNumber") + if err != nil { + return e.ReturnError(7, reason, err) + } + + // Validate given friend code. + userId, err := strconv.ParseUint(deviceCode, 10, 64) + if err != nil { + return e.ReturnError(7, reason, err) + } + if wiino.NWC24CheckUserID(userId) != 0 { + return e.ReturnError(7, reason, err) + } + + // Generate a random 9-digit number, padding zeros as necessary. + accountId := fmt.Sprintf("%9d", rand.Intn(999999999)) + + // This is where it gets hairy. + // Generate a device token, 21 characters... + deviceToken := RandString(21) + // ...and then its md5, because the Wii sends this... + md5DeviceToken := fmt.Sprintf("%x", md5.Sum([]byte(deviceToken))) + // ...and then the sha256 of that md5. + // We'll store this in our database, as storing the md5 itself is effectively the token. + // It would not be good for security to directly store the token either. + // This is the hash of the md5 represented as a string, not individual byte values. + doublyHashedDeviceToken := fmt.Sprintf("%x", sha2562.Sum256([]byte(md5DeviceToken))) + + // Insert all of our obtained values to the database.. + stmt, err := db.Prepare(`INSERT INTO wiisoap.userbase (DeviceId, DeviceToken, AccountId, Region, Country, Language, SerialNo, DeviceCode) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`) + if err != nil { + log.Printf("error preparing statement: %v\n", err) + return e.ReturnError(7, reason, errors.New("failed to prepare statement")) + } + _, err = stmt.Exec(e.DeviceId(), doublyHashedDeviceToken, accountId, region, country, language, serialNo, deviceCode) + if err != nil { + // It's okay if this isn't a MySQL error, as perhaps other issues have come in. + if driverErr, ok := err.(*mysql.MySQLError); ok { + if driverErr.Number == 1062 { + return e.ReturnError(7, reason, errors.New("user already exists")) + } + } + log.Printf("error executing statement: %v\n", err) + return e.ReturnError(7, reason, errors.New("failed to execute db operation")) } fmt.Println("The request is valid! Responding...") e.AddKVNode("AccountId", accountId) - e.AddKVNode("DeviceToken", "00000000") + e.AddKVNode("DeviceToken", deviceToken) + e.AddKVNode("DeviceTokenExpired", "false") e.AddKVNode("Country", country) + // Optionally, one can send back DeviceCode and ExtAccountId to update on device. + // We send these back as-is regardless. e.AddKVNode("ExtAccountId", "") - e.AddKVNode("DeviceCode", "0000000000000000") + e.AddKVNode("DeviceCode", deviceCode) break case "Unregister": diff --git a/main.go b/main.go index 97df355..fcb303e 100644 --- a/main.go +++ b/main.go @@ -29,10 +29,13 @@ import ( ) const ( - // Any given challenge must be 10 characters or less (challenge.length > 0xb) + // SharedChallenge represents a static value to this nonsensical challenge response system. + // The given challenge must be 11 characters or less. Contents do not matter. SharedChallenge = "NintyWhyPls" ) +var db *sql.DB + // checkError makes error handling not as ugly and inefficient. func checkError(err error) { if err != nil { @@ -54,7 +57,7 @@ func main() { fmt.Println("[i] Initializing core...") // Start SQL. - db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", CON.SQLUser, CON.SQLPass, CON.SQLAddress, CON.SQLDB)) + db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s)/%s", CON.SQLUser, CON.SQLPass, CON.SQLAddress, CON.SQLDB)) checkError(err) // Close SQL after everything else is done. diff --git a/utils.go b/utils.go index 60f9b9b..83f3f73 100644 --- a/utils.go +++ b/utils.go @@ -23,6 +23,7 @@ import ( "fmt" "github.com/antchfx/xmlquery" "io" + "math/rand" "regexp" "time" ) @@ -75,7 +76,12 @@ func (e *Envelope) Timestamp() string { return e.Body.Response.TimeStamp } -// obtainCommon interprets a given node, and updates the envelope with common key values. +// DeviceId returns the Device ID for this request. +func (e *Envelope) DeviceId() string { + return e.Body.Response.DeviceId +} + +// ObtainCommon interprets a given node, and updates the envelope with common key values. func (e *Envelope) ObtainCommon(doc *xmlquery.Node) error { var err error @@ -130,7 +136,7 @@ func (e *Envelope) ReturnSuccess() (bool, string) { return e.becomeXML(true) } -// formatError returns a standard SOAP response with an error code. +// ReturnError returns a standard SOAP response with an error code. func (e *Envelope) ReturnError(errorCode int, reason string, err error) (bool, string) { e.Body.Response.ErrorCode = errorCode @@ -180,3 +186,14 @@ func getKey(doc *xmlquery.Node, key string) (string, error) { return node.InnerText(), nil } } + +// Derived from https://stackoverflow.com/a/31832326, adding numbers +const letterBytes = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + +func RandString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = letterBytes[rand.Intn(len(letterBytes))] + } + return string(b) +}