diff --git a/README.md b/README.md index 8a77f5a..40c3b9c 100644 --- a/README.md +++ b/README.md @@ -98,4 +98,5 @@ All configuration options are handled via environment variables | `PN_FRIENDS_CONFIG_ACCOUNT_GRPC_PORT` | Port for your account server gRPC service | Yes | | `PN_FRIENDS_CONFIG_ACCOUNT_GRPC_API_KEY` | API key for your account server gRPC service | No (Assumed to be an open gRPC API) | | `PN_FRIENDS_CONFIG_HEALTH_CHECK_PORT` | Port for the basic UDP health check server | No | -| `PN_FRIENDS_CONFIG_ENABLE_BELLA` | Enables a debug user named "Bella" which is always assigned as your friend | No | No +| `PN_FRIENDS_CONFIG_ENABLE_BELLA` | Enables a debug user named "Bella" which is always assigned as your friend | No | +| `PN_FRIENDS_CONFIG_MII_DECRYPT_KEY` | AES key used to decrypt 3DS Mii data (as a hex string) | Yes | diff --git a/globals/config.go b/globals/config.go index 8203bc8..02dcf7b 100644 --- a/globals/config.go +++ b/globals/config.go @@ -14,6 +14,7 @@ type config struct { AccountGRPCAPIKey string `envconf:"optional"` HealthCheckPort uint16 `envconf:"optional"` EnableBella bool `envconf:"optional"` + MiiDecryptKey string } var Config *config = &config{} diff --git a/grpc/get_user_data.go b/grpc/get_user_data.go index ccbd35d..4ab9f03 100644 --- a/grpc/get_user_data.go +++ b/grpc/get_user_data.go @@ -8,6 +8,7 @@ import ( database_3ds "github.com/PretendoNetwork/friends/database/3ds" database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" "github.com/PretendoNetwork/friends/globals" + "github.com/PretendoNetwork/friends/utility" pb "github.com/PretendoNetwork/grpc/go/friends/v2" "github.com/PretendoNetwork/nex-go/v2/types" "google.golang.org/grpc/codes" @@ -106,7 +107,11 @@ func (s *gRPCFriendsV2Server) GetUserData3DS(ctx context.Context, in *pb.GetUser ProfanityFlag: bool(miiData.Mii.ProfanityFlag), CharacterSet: uint32(miiData.Mii.CharacterSet), MiiDataEncrypted: miiData.Mii.MiiData, - // TODO: Implement MiiData + } + + mii_data, err := utility.DecryptMiiData(miiData.Mii.MiiData) + if err == nil { + mii.MiiData = mii_data } friendMii := &pb.FriendMii{ Pid: uint32(miiData.PID), diff --git a/grpc/get_user_friends_data.go b/grpc/get_user_friends_data.go index 05443aa..0db2201 100644 --- a/grpc/get_user_friends_data.go +++ b/grpc/get_user_friends_data.go @@ -12,6 +12,7 @@ import ( database_3ds "github.com/PretendoNetwork/friends/database/3ds" database_wiiu "github.com/PretendoNetwork/friends/database/wiiu" "github.com/PretendoNetwork/friends/globals" + "github.com/PretendoNetwork/friends/utility" pb "github.com/PretendoNetwork/grpc/go/friends/v2" "github.com/PretendoNetwork/nex-go/v2/types" friends_3ds_types "github.com/PretendoNetwork/nex-protocols-go/v2/friends-3ds/types" @@ -241,10 +242,15 @@ func (s *gRPCFriendsV2Server) GetUserFriendsData3DS(ctx context.Context, in *pb. miiData := miiList[miiIndex] mii := &pb.Mii{ - Name: string(miiData.Mii.Name), - ProfanityFlag: bool(miiData.Mii.ProfanityFlag), - CharacterSet: uint32(miiData.Mii.CharacterSet), - MiiData: miiData.Mii.MiiData, + Name: string(miiData.Mii.Name), + ProfanityFlag: bool(miiData.Mii.ProfanityFlag), + CharacterSet: uint32(miiData.Mii.CharacterSet), + MiiDataEncrypted: miiData.Mii.MiiData, + } + + mii_data, err := utility.DecryptMiiData(miiData.Mii.MiiData) + if err == nil { + mii.MiiData = mii_data } friendMii := &pb.FriendMii{ diff --git a/utility/decrypt_mii.go b/utility/decrypt_mii.go new file mode 100644 index 0000000..bdbbd5c --- /dev/null +++ b/utility/decrypt_mii.go @@ -0,0 +1,57 @@ +package utility + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/hex" + "fmt" + + "github.com/PretendoNetwork/friends/globals" + "github.com/PretendoNetwork/nex-go/v2/types" +) + +func ccmDecrypt(key, nonce, ciphertext []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + L := 15 - len(nonce) + + counter := make([]byte, aes.BlockSize) + counter[0] = byte(L - 1) + copy(counter[1:1+len(nonce)], nonce) + stream := cipher.NewCTR(block, counter) + + padded := make([]byte, aes.BlockSize+len(ciphertext)) + copy(padded[aes.BlockSize:], ciphertext) + + out := make([]byte, len(padded)) + stream.XORKeyStream(out, padded) + + return out[aes.BlockSize:], nil +} + +func DecryptMiiData(mii_data types.Buffer) ([]byte, error) { + if len(mii_data) < 96 { + return nil, fmt.Errorf("Mii data length is incorrect: %d", len(mii_data)) + } + nonce := mii_data[:8] + ciphertext := mii_data[8 : 8+0x58] + + key, err := hex.DecodeString(globals.Config.MiiDecryptKey) + if err != nil { + return nil, err + } + content, err := ccmDecrypt(key, append(append([]byte{}, nonce...), 0, 0, 0, 0), ciphertext) + if err != nil { + return nil, err + } + + result := make([]byte, 0, len(content)+len(nonce)) + result = append(result, content[:12]...) + result = append(result, nonce...) + result = append(result, content[12:]...) + + return result, nil +}