diff --git a/common/mario_kart_wii.go b/common/mario_kart_wii.go index d9a4e72..238f3f7 100644 --- a/common/mario_kart_wii.go +++ b/common/mario_kart_wii.go @@ -341,8 +341,12 @@ func (rkgd RKGhostData) GetLocationCode() uint16 { return uint16(rkgd.GetBits(0x36, 0, 16)) } -func (rkgd RKGhostData) GetMiiData() Mii { - return Mii(rkgd[0x3C : 0x3C+0x4C]) +func (rkgd RKGhostData) GetMiiData() RawMii { + return RawMiiFromBytes(rkgd[0x3C : 0x3C+0x4C]) +} + +func (rkgd RKGhostData) SetMiiData(miiData RawMii) { + copy(rkgd[0x3C:0x3C+0x4C], miiData.Data[:]) } func (rkgd RKGhostData) GetCompressedSize() uint32 { @@ -353,6 +357,11 @@ func (rkgd RKGhostData) GetCompressedData() []byte { return []byte(rkgd[0x8C : len(rkgd)-4]) } +func (rkgd RKGhostData) RecalculateCRC() { + crc := crc32.ChecksumIEEE(rkgd[:len(rkgd)-4]) + binary.BigEndian.PutUint32(rkgd[len(rkgd)-4:], crc) +} + func (rkgd RKGhostData) IsRKGDFileValid(moduleName string, expectedCourse MarioKartWiiCourseId, expectedScore int) bool { rkgdFileMagic := []byte{'R', 'K', 'G', 'D'} rkgdFileLength := len(rkgd) @@ -443,7 +452,7 @@ func (rkgd RKGhostData) IsRKGDFileValid(moduleName string, expectedCourse MarioK return false } - if rkgd.GetMiiData().RFLCalculateCRC() != 0x0000 { + if rkgd.GetMiiData().CalculateMiiCRC() != 0x0000 { logging.Error(moduleName, "Invalid RKGD Mii data CRC") return false } diff --git a/common/mii.go b/common/mii.go index 2147cef..31f22fa 100644 --- a/common/mii.go +++ b/common/mii.go @@ -1,15 +1,25 @@ package common +import "encoding/binary" + // References: // https://wiibrew.org/wiki/Mii_Data // https://github.com/kiwi515/ogws/tree/master/src/RVLFaceLib -type Mii [0x4C]byte +type RawMii struct { + Data [0x4C]byte +} -func (data Mii) RFLCalculateCRC() uint16 { +func RawMiiFromBytes(data []byte) RawMii { + var miiData [0x4C]byte + copy(miiData[:], data[:0x4C]) + return RawMii{Data: miiData} +} + +func (data RawMii) CalculateMiiCRC() uint16 { crc := uint16(0) - for _, val := range data { + for _, val := range data.Data { for j := 0; j < 8; j++ { if crc&0x8000 != 0 { crc <<= 1 @@ -38,7 +48,7 @@ var officialMiiList = []uint64{ 0x80000005ECFF82D2, } -func RFLSearchOfficialData(id uint64) (bool, int) { +func SearchOfficialMiiData(id uint64) (bool, int) { for i, v := range officialMiiList { if v == id { return true, i @@ -47,3 +57,39 @@ func RFLSearchOfficialData(id uint64) (bool, int) { return false, -1 } + +// ClearMiiInfo clears any Mii information that generally isn't or shouldn't be shared publicly, +// mainly the "console ID", which can be used to determine the Mii creator's MAC address +func (mii RawMii) ClearMiiInfo() RawMii { + // Clear the create ID, which the MAC address can be derived from + binary.BigEndian.PutUint32(mii.Data[0x18:0x1C], 0x80000000) + binary.BigEndian.PutUint32(mii.Data[0x1C:0x20], 0) + + // Clear all characters in the Mii name succeeding the null terminator + hitNullTerminator := false + for i := 0; i < 20; i += 2 { + if hitNullTerminator { + mii.Data[0x2+i] = 0 + mii.Data[0x2+i+1] = 0 + } else if mii.Data[0x2+i] == 0 && mii.Data[0x2+i+1] == 0 { + hitNullTerminator = true + } + } + + // Clear the creator name + for i := 0; i < 20; i++ { + mii.Data[0x36+i] = 0 + } + + // Clear the birthday + mii.Data[0] &= ^byte(0x3F) + mii.Data[1] &= ^byte(0xE0) + + // Clear checksum and recalculate + mii.Data[0x4A] = 0 + mii.Data[0x4B] = 0 + crc := mii.CalculateMiiCRC() + mii.Data[0x4A] = byte(crc >> 8) + mii.Data[0x4B] = byte(crc & 0xFF) + return mii +} diff --git a/qr2/group.go b/qr2/group.go index 6fb901a..68d29e0 100644 --- a/qr2/group.go +++ b/qr2/group.go @@ -393,15 +393,15 @@ func ProcessUSER(senderPid uint32, senderIP uint64, packet []byte) { } index := 0x08 + i*0x4C - mii := common.Mii(packet[index : index+0x4C]) - if mii.RFLCalculateCRC() != 0x0000 { + mii := common.RawMiiFromBytes(packet[index : index+0x4C]) + if mii.CalculateMiiCRC() != 0x0000 { logging.Error(moduleName, "Received USER packet with invalid Mii data CRC") gpErrorCallback(senderPid, "bad_packet") return } createId := binary.BigEndian.Uint64(packet[index+0x18 : index+0x20]) - official, _ := common.RFLSearchOfficialData(createId) + official, _ := common.SearchOfficialMiiData(createId) if official { miiName = append(miiName, "Player") } else { diff --git a/race/nintendo_racing_service.go b/race/nintendo_racing_service.go index 3878ee6..9be0aca 100644 --- a/race/nintendo_racing_service.go +++ b/race/nintendo_racing_service.go @@ -1,6 +1,7 @@ package race import ( + "encoding/base64" "encoding/xml" "io" "net/http" @@ -153,11 +154,19 @@ func handleGetTopTenRankingsRequest(moduleName string, responseWriter http.Respo numberOfRankings := len(topTenRankings) data := make([]rankingsResponseData, 0, numberOfRankings) for i, topTenRanking := range topTenRankings { + // Filter player info just in case + playerInfo, err := base64.StdEncoding.DecodeString(topTenRanking.PlayerInfo) + if err != nil { + panic(err) + } + miiData := common.RawMiiFromBytes(playerInfo).ClearMiiInfo().Data + playerInfo = append(miiData[:], playerInfo[len(miiData):]...) + rankingData := rankingsResponseRankingData{ OwnerID: topTenRanking.PID, Rank: i + 1, Time: topTenRanking.Score, - UserData: topTenRanking.PlayerInfo, + UserData: base64.StdEncoding.EncodeToString(playerInfo), } responseData := rankingsResponseData{ diff --git a/sake/mario_kart_wii.go b/sake/mario_kart_wii.go index 88f9c07..f3e7731 100644 --- a/sake/mario_kart_wii.go +++ b/sake/mario_kart_wii.go @@ -18,11 +18,11 @@ import ( ) type playerInfo struct { - MiiData common.Mii // 0x00 - ControllerId byte // 0x4C - Unknown byte // 0x4D - StateCode byte // 0x4E - CountryCode byte // 0x4F + MiiData common.RawMii // 0x00 + ControllerId byte // 0x4C + Unknown byte // 0x4D + StateCode byte // 0x4E + CountryCode byte // 0x4F } const ( @@ -266,7 +266,11 @@ func handleMarioKartWiiGhostDownloadRequest(moduleName string, responseWriter ht return } - responseBody := append(downloadedGhostFileHeader(), ghost...) + ghostData := common.RKGhostData(ghost) + ghostData.SetMiiData(ghostData.GetMiiData().ClearMiiInfo()) + ghostData.RecalculateCRC() + + responseBody := append(downloadedGhostFileHeader(), []byte(ghostData)...) responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultSuccess)) responseWriter.Header().Set("Content-Length", strconv.Itoa(len(responseBody))) @@ -330,13 +334,13 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http return } - if !isPlayerInfoValid(playerInfo) { + fixedPlayerInfo, ok := isPlayerInfoValid(playerInfo) + if !ok { logging.Error(moduleName, "Invalid player info:", aurora.Cyan(playerInfo)) responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultMissingParameter)) return } - // Mario Kart Wii expects player information to be in this form - playerInfo, _ = common.Base64Convert(playerInfo, base64.URLEncoding, base64.StdEncoding) + playerInfo = fixedPlayerInfo // The multipart boundary utilized by GameSpy does not conform to RFC 2045. To ensure compliance, // we need to surround it with double quotation marks. @@ -377,17 +381,21 @@ func handleMarioKartWiiGhostUploadRequest(moduleName string, responseWriter http return } - if !common.RKGhostData(ghostFile).IsRKGDFileValid(moduleName, courseId, score) { + ghostData := common.RKGhostData(ghostFile) + if !ghostData.IsRKGDFileValid(moduleName, courseId, score) { logging.Error(moduleName, "Received an invalid ghost file") responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultFileTooLarge)) return } + ghostData.SetMiiData(ghostData.GetMiiData().ClearMiiInfo()) + ghostData.RecalculateCRC() + if isContest { ghostFile = nil } - err = database.InsertMarioKartWiiGhostFile(pool, ctx, regionId, courseId, score, pid, playerInfo, ghostFile) + err = database.InsertMarioKartWiiGhostFile(pool, ctx, regionId, courseId, score, pid, playerInfo, []byte(ghostData)) if err != nil { logging.Error(moduleName, "Failed to insert the ghost file into the database:", err) responseWriter.Header().Set(SakeFileResultHeader, strconv.Itoa(SakeFileResultServerError)) @@ -405,30 +413,44 @@ func downloadedGhostFileHeader() []byte { return downloadedGhostFileHeader[:] } -func isPlayerInfoValid(playerInfoString string) bool { +// isPlayerInfoValid checks if the player info (base64.URLEncoding) string is valid, and if so, returns a "fixed" version of it with base64.StdEncoding +func isPlayerInfoValid(playerInfoString string) (string, bool) { playerInfoByteArray, err := base64.URLEncoding.DecodeString(playerInfoString) if err != nil { - return false + return "", false } if len(playerInfoByteArray) != playerInfoSize { - return false + return "", false } var playerInfo playerInfo reader := bytes.NewReader(playerInfoByteArray) err = binary.Read(reader, binary.BigEndian, &playerInfo) if err != nil { - return false + return "", false } - if playerInfo.MiiData.RFLCalculateCRC() != 0x0000 { - return false + if playerInfo.MiiData.CalculateMiiCRC() != 0x0000 { + return "", false } controllerId := common.MarioKartWiiControllerId(playerInfo.ControllerId) - return controllerId.IsValid() + if !controllerId.IsValid() { + return "", false + } + + playerInfo.MiiData.ClearMiiInfo() + + fixedPlayerInfoByteArray := new(bytes.Buffer) + err = binary.Write(fixedPlayerInfoByteArray, binary.BigEndian, playerInfo) + if err != nil { + return "", false + } + + fixedPlayerInfoString := base64.StdEncoding.EncodeToString(fixedPlayerInfoByteArray.Bytes()) + return fixedPlayerInfoString, true } func getMultipartBoundary(contentType string) string {