Merge pull request #8827 from german77/amiibo_release
core: nfp: Implement amiibo encryption
This commit is contained in:
		| @@ -519,6 +519,9 @@ add_library(core STATIC | ||||
|     hle/service/ncm/ncm.h | ||||
|     hle/service/nfc/nfc.cpp | ||||
|     hle/service/nfc/nfc.h | ||||
|     hle/service/nfp/amiibo_crypto.cpp | ||||
|     hle/service/nfp/amiibo_crypto.h | ||||
|     hle/service/nfp/amiibo_types.h | ||||
|     hle/service/nfp/nfp.cpp | ||||
|     hle/service/nfp/nfp.h | ||||
|     hle/service/nfp/nfp_user.cpp | ||||
|   | ||||
| @@ -32,7 +32,7 @@ enum class MiiEditResult : u32 { | ||||
| }; | ||||
|  | ||||
| struct MiiEditCharInfo { | ||||
|     Service::Mii::MiiInfo mii_info{}; | ||||
|     Service::Mii::CharInfo mii_info{}; | ||||
| }; | ||||
| static_assert(sizeof(MiiEditCharInfo) == 0x58, "MiiEditCharInfo has incorrect size."); | ||||
|  | ||||
|   | ||||
| @@ -43,7 +43,7 @@ public: | ||||
|             {20, nullptr, "IsBrokenDatabaseWithClearFlag"}, | ||||
|             {21, &IDatabaseService::GetIndex, "GetIndex"}, | ||||
|             {22, &IDatabaseService::SetInterfaceVersion, "SetInterfaceVersion"}, | ||||
|             {23, nullptr, "Convert"}, | ||||
|             {23, &IDatabaseService::Convert, "Convert"}, | ||||
|             {24, nullptr, "ConvertCoreDataToCharInfo"}, | ||||
|             {25, nullptr, "ConvertCharInfoToCoreData"}, | ||||
|             {26, nullptr, "Append"}, | ||||
| @@ -130,7 +130,7 @@ private: | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         std::vector<MiiInfo> values; | ||||
|         std::vector<CharInfo> values; | ||||
|         for (const auto& element : *result) { | ||||
|             values.emplace_back(element.info); | ||||
|         } | ||||
| @@ -144,7 +144,7 @@ private: | ||||
|  | ||||
|     void UpdateLatest(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto info{rp.PopRaw<MiiInfo>()}; | ||||
|         const auto info{rp.PopRaw<CharInfo>()}; | ||||
|         const auto source_flag{rp.PopRaw<SourceFlag>()}; | ||||
|  | ||||
|         LOG_DEBUG(Service_Mii, "called with source_flag={}", source_flag); | ||||
| @@ -156,9 +156,9 @@ private: | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; | ||||
|         rb.Push(ResultSuccess); | ||||
|         rb.PushRaw<MiiInfo>(*result); | ||||
|         rb.PushRaw<CharInfo>(*result); | ||||
|     } | ||||
|  | ||||
|     void BuildRandom(Kernel::HLERequestContext& ctx) { | ||||
| @@ -191,9 +191,9 @@ private: | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; | ||||
|         rb.Push(ResultSuccess); | ||||
|         rb.PushRaw<MiiInfo>(manager.BuildRandom(age, gender, race)); | ||||
|         rb.PushRaw<CharInfo>(manager.BuildRandom(age, gender, race)); | ||||
|     } | ||||
|  | ||||
|     void BuildDefault(Kernel::HLERequestContext& ctx) { | ||||
| @@ -210,14 +210,14 @@ private: | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(MiiInfo) / sizeof(u32)}; | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; | ||||
|         rb.Push(ResultSuccess); | ||||
|         rb.PushRaw<MiiInfo>(manager.BuildDefault(index)); | ||||
|         rb.PushRaw<CharInfo>(manager.BuildDefault(index)); | ||||
|     } | ||||
|  | ||||
|     void GetIndex(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|         const auto info{rp.PopRaw<MiiInfo>()}; | ||||
|         const auto info{rp.PopRaw<CharInfo>()}; | ||||
|  | ||||
|         LOG_DEBUG(Service_Mii, "called"); | ||||
|  | ||||
| @@ -239,6 +239,18 @@ private: | ||||
|         rb.Push(ResultSuccess); | ||||
|     } | ||||
|  | ||||
|     void Convert(Kernel::HLERequestContext& ctx) { | ||||
|         IPC::RequestParser rp{ctx}; | ||||
|  | ||||
|         const auto mii_v3{rp.PopRaw<Ver3StoreData>()}; | ||||
|  | ||||
|         LOG_INFO(Service_Mii, "called"); | ||||
|  | ||||
|         IPC::ResponseBuilder rb{ctx, 2 + sizeof(CharInfo) / sizeof(u32)}; | ||||
|         rb.Push(ResultSuccess); | ||||
|         rb.PushRaw<CharInfo>(manager.ConvertV3ToCharInfo(mii_v3)); | ||||
|     } | ||||
|  | ||||
|     constexpr bool IsInterfaceVersionSupported(u32 interface_version) const { | ||||
|         return current_interface_version >= interface_version; | ||||
|     } | ||||
|   | ||||
| @@ -42,7 +42,7 @@ std::array<T, DestArraySize> ResizeArray(const std::array<T, SourceArraySize>& i | ||||
|     return out; | ||||
| } | ||||
|  | ||||
| MiiInfo ConvertStoreDataToInfo(const MiiStoreData& data) { | ||||
| CharInfo ConvertStoreDataToInfo(const MiiStoreData& data) { | ||||
|     MiiStoreBitFields bf; | ||||
|     std::memcpy(&bf, data.data.data.data(), sizeof(MiiStoreBitFields)); | ||||
|  | ||||
| @@ -409,8 +409,8 @@ u32 MiiManager::GetCount(SourceFlag source_flag) const { | ||||
|     return static_cast<u32>(count); | ||||
| } | ||||
|  | ||||
| ResultVal<MiiInfo> MiiManager::UpdateLatest([[maybe_unused]] const MiiInfo& info, | ||||
|                                             SourceFlag source_flag) { | ||||
| ResultVal<CharInfo> MiiManager::UpdateLatest([[maybe_unused]] const CharInfo& info, | ||||
|                                              SourceFlag source_flag) { | ||||
|     if ((source_flag & SourceFlag::Database) == SourceFlag::None) { | ||||
|         return ERROR_CANNOT_FIND_ENTRY; | ||||
|     } | ||||
| @@ -419,14 +419,91 @@ ResultVal<MiiInfo> MiiManager::UpdateLatest([[maybe_unused]] const MiiInfo& info | ||||
|     return ERROR_CANNOT_FIND_ENTRY; | ||||
| } | ||||
|  | ||||
| MiiInfo MiiManager::BuildRandom(Age age, Gender gender, Race race) { | ||||
| CharInfo MiiManager::BuildRandom(Age age, Gender gender, Race race) { | ||||
|     return ConvertStoreDataToInfo(BuildRandomStoreData(age, gender, race, user_id)); | ||||
| } | ||||
|  | ||||
| MiiInfo MiiManager::BuildDefault(std::size_t index) { | ||||
| CharInfo MiiManager::BuildDefault(std::size_t index) { | ||||
|     return ConvertStoreDataToInfo(BuildDefaultStoreData(RawData::DefaultMii.at(index), user_id)); | ||||
| } | ||||
|  | ||||
| CharInfo MiiManager::ConvertV3ToCharInfo(Ver3StoreData mii_v3) const { | ||||
|     Service::Mii::MiiManager manager; | ||||
|     auto mii = manager.BuildDefault(0); | ||||
|  | ||||
|     // Check if mii data exist | ||||
|     if (mii_v3.mii_name[0] == 0) { | ||||
|         return mii; | ||||
|     } | ||||
|  | ||||
|     // TODO: We are ignoring a bunch of data from the mii_v3 | ||||
|  | ||||
|     mii.gender = static_cast<u8>(mii_v3.mii_information.gender); | ||||
|     mii.favorite_color = static_cast<u8>(mii_v3.mii_information.favorite_color); | ||||
|     mii.height = mii_v3.height; | ||||
|     mii.build = mii_v3.build; | ||||
|  | ||||
|     memset(mii.name.data(), 0, sizeof(mii.name)); | ||||
|     memcpy(mii.name.data(), mii_v3.mii_name.data(), sizeof(mii_v3.mii_name)); | ||||
|     mii.font_region = mii_v3.region_information.character_set; | ||||
|  | ||||
|     mii.faceline_type = mii_v3.appearance_bits1.face_shape; | ||||
|     mii.faceline_color = mii_v3.appearance_bits1.skin_color; | ||||
|     mii.faceline_wrinkle = mii_v3.appearance_bits2.wrinkles; | ||||
|     mii.faceline_make = mii_v3.appearance_bits2.makeup; | ||||
|  | ||||
|     mii.hair_type = mii_v3.hair_style; | ||||
|     mii.hair_color = mii_v3.appearance_bits3.hair_color; | ||||
|     mii.hair_flip = mii_v3.appearance_bits3.flip_hair; | ||||
|  | ||||
|     mii.eye_type = static_cast<u8>(mii_v3.appearance_bits4.eye_type); | ||||
|     mii.eye_color = static_cast<u8>(mii_v3.appearance_bits4.eye_color); | ||||
|     mii.eye_scale = static_cast<u8>(mii_v3.appearance_bits4.eye_scale); | ||||
|     mii.eye_aspect = static_cast<u8>(mii_v3.appearance_bits4.eye_vertical_stretch); | ||||
|     mii.eye_rotate = static_cast<u8>(mii_v3.appearance_bits4.eye_rotation); | ||||
|     mii.eye_x = static_cast<u8>(mii_v3.appearance_bits4.eye_spacing); | ||||
|     mii.eye_y = static_cast<u8>(mii_v3.appearance_bits4.eye_y_position); | ||||
|  | ||||
|     mii.eyebrow_type = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_style); | ||||
|     mii.eyebrow_color = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_color); | ||||
|     mii.eyebrow_scale = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_scale); | ||||
|     mii.eyebrow_aspect = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_yscale); | ||||
|     mii.eyebrow_rotate = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_rotation); | ||||
|     mii.eyebrow_x = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_spacing); | ||||
|     mii.eyebrow_y = static_cast<u8>(mii_v3.appearance_bits5.eyebrow_y_position); | ||||
|  | ||||
|     mii.nose_type = static_cast<u8>(mii_v3.appearance_bits6.nose_type); | ||||
|     mii.nose_scale = static_cast<u8>(mii_v3.appearance_bits6.nose_scale); | ||||
|     mii.nose_y = static_cast<u8>(mii_v3.appearance_bits6.nose_y_position); | ||||
|  | ||||
|     mii.mouth_type = static_cast<u8>(mii_v3.appearance_bits7.mouth_type); | ||||
|     mii.mouth_color = static_cast<u8>(mii_v3.appearance_bits7.mouth_color); | ||||
|     mii.mouth_scale = static_cast<u8>(mii_v3.appearance_bits7.mouth_scale); | ||||
|     mii.mouth_aspect = static_cast<u8>(mii_v3.appearance_bits7.mouth_horizontal_stretch); | ||||
|     mii.mouth_y = static_cast<u8>(mii_v3.appearance_bits8.mouth_y_position); | ||||
|  | ||||
|     mii.mustache_type = static_cast<u8>(mii_v3.appearance_bits8.mustache_type); | ||||
|     mii.mustache_scale = static_cast<u8>(mii_v3.appearance_bits9.mustache_scale); | ||||
|     mii.mustache_y = static_cast<u8>(mii_v3.appearance_bits9.mustache_y_position); | ||||
|  | ||||
|     mii.beard_type = static_cast<u8>(mii_v3.appearance_bits9.bear_type); | ||||
|     mii.beard_color = static_cast<u8>(mii_v3.appearance_bits9.facial_hair_color); | ||||
|  | ||||
|     mii.glasses_type = static_cast<u8>(mii_v3.appearance_bits10.glasses_type); | ||||
|     mii.glasses_color = static_cast<u8>(mii_v3.appearance_bits10.glasses_color); | ||||
|     mii.glasses_scale = static_cast<u8>(mii_v3.appearance_bits10.glasses_scale); | ||||
|     mii.glasses_y = static_cast<u8>(mii_v3.appearance_bits10.glasses_y_position); | ||||
|  | ||||
|     mii.mole_type = static_cast<u8>(mii_v3.appearance_bits11.mole_enabled); | ||||
|     mii.mole_scale = static_cast<u8>(mii_v3.appearance_bits11.mole_scale); | ||||
|     mii.mole_x = static_cast<u8>(mii_v3.appearance_bits11.mole_x_position); | ||||
|     mii.mole_y = static_cast<u8>(mii_v3.appearance_bits11.mole_y_position); | ||||
|  | ||||
|     // TODO: Validate mii data | ||||
|  | ||||
|     return mii; | ||||
| } | ||||
|  | ||||
| ResultVal<std::vector<MiiInfoElement>> MiiManager::GetDefault(SourceFlag source_flag) { | ||||
|     std::vector<MiiInfoElement> result; | ||||
|  | ||||
| @@ -441,7 +518,7 @@ ResultVal<std::vector<MiiInfoElement>> MiiManager::GetDefault(SourceFlag source_ | ||||
|     return result; | ||||
| } | ||||
|  | ||||
| Result MiiManager::GetIndex([[maybe_unused]] const MiiInfo& info, u32& index) { | ||||
| Result MiiManager::GetIndex([[maybe_unused]] const CharInfo& info, u32& index) { | ||||
|     constexpr u32 INVALID_INDEX{0xFFFFFFFF}; | ||||
|  | ||||
|     index = INVALID_INDEX; | ||||
|   | ||||
| @@ -19,11 +19,12 @@ public: | ||||
|     bool CheckAndResetUpdateCounter(SourceFlag source_flag, u64& current_update_counter); | ||||
|     bool IsFullDatabase() const; | ||||
|     u32 GetCount(SourceFlag source_flag) const; | ||||
|     ResultVal<MiiInfo> UpdateLatest(const MiiInfo& info, SourceFlag source_flag); | ||||
|     MiiInfo BuildRandom(Age age, Gender gender, Race race); | ||||
|     MiiInfo BuildDefault(std::size_t index); | ||||
|     ResultVal<CharInfo> UpdateLatest(const CharInfo& info, SourceFlag source_flag); | ||||
|     CharInfo BuildRandom(Age age, Gender gender, Race race); | ||||
|     CharInfo BuildDefault(std::size_t index); | ||||
|     CharInfo ConvertV3ToCharInfo(Ver3StoreData mii_v3) const; | ||||
|     ResultVal<std::vector<MiiInfoElement>> GetDefault(SourceFlag source_flag); | ||||
|     Result GetIndex(const MiiInfo& info, u32& index); | ||||
|     Result GetIndex(const CharInfo& info, u32& index); | ||||
|  | ||||
| private: | ||||
|     const Common::UUID user_id{}; | ||||
|   | ||||
| @@ -86,7 +86,8 @@ enum class SourceFlag : u32 { | ||||
| }; | ||||
| DECLARE_ENUM_FLAG_OPERATORS(SourceFlag); | ||||
|  | ||||
| struct MiiInfo { | ||||
| // nn::mii::CharInfo | ||||
| struct CharInfo { | ||||
|     Common::UUID uuid; | ||||
|     std::array<char16_t, 11> name; | ||||
|     u8 font_region; | ||||
| @@ -140,16 +141,16 @@ struct MiiInfo { | ||||
|     u8 mole_y; | ||||
|     u8 padding; | ||||
| }; | ||||
| static_assert(sizeof(MiiInfo) == 0x58, "MiiInfo has incorrect size."); | ||||
| static_assert(std::has_unique_object_representations_v<MiiInfo>, | ||||
|               "All bits of MiiInfo must contribute to its value."); | ||||
| static_assert(sizeof(CharInfo) == 0x58, "CharInfo has incorrect size."); | ||||
| static_assert(std::has_unique_object_representations_v<CharInfo>, | ||||
|               "All bits of CharInfo must contribute to its value."); | ||||
|  | ||||
| #pragma pack(push, 4) | ||||
|  | ||||
| struct MiiInfoElement { | ||||
|     MiiInfoElement(const MiiInfo& info_, Source source_) : info{info_}, source{source_} {} | ||||
|     MiiInfoElement(const CharInfo& info_, Source source_) : info{info_}, source{source_} {} | ||||
|  | ||||
|     MiiInfo info{}; | ||||
|     CharInfo info{}; | ||||
|     Source source{}; | ||||
| }; | ||||
| static_assert(sizeof(MiiInfoElement) == 0x5c, "MiiInfoElement has incorrect size."); | ||||
| @@ -243,6 +244,131 @@ static_assert(sizeof(MiiStoreBitFields) == 0x1c, "MiiStoreBitFields has incorrec | ||||
| static_assert(std::is_trivially_copyable_v<MiiStoreBitFields>, | ||||
|               "MiiStoreBitFields is not trivially copyable."); | ||||
|  | ||||
| // This is nn::mii::Ver3StoreData | ||||
| // Based on citra HLE::Applets::MiiData and PretendoNetwork. | ||||
| // https://github.com/citra-emu/citra/blob/master/src/core/hle/applets/mii_selector.h#L48 | ||||
| // https://github.com/PretendoNetwork/mii-js/blob/master/mii.js#L299 | ||||
| struct Ver3StoreData { | ||||
|     u8 version; | ||||
|     union { | ||||
|         u8 raw; | ||||
|  | ||||
|         BitField<0, 1, u8> allow_copying; | ||||
|         BitField<1, 1, u8> profanity_flag; | ||||
|         BitField<2, 2, u8> region_lock; | ||||
|         BitField<4, 2, u8> character_set; | ||||
|     } region_information; | ||||
|     u16_be mii_id; | ||||
|     u64_be system_id; | ||||
|     u32_be specialness_and_creation_date; | ||||
|     std::array<u8, 0x6> creator_mac; | ||||
|     u16_be padding; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 1, u16> gender; | ||||
|         BitField<1, 4, u16> birth_month; | ||||
|         BitField<5, 5, u16> birth_day; | ||||
|         BitField<10, 4, u16> favorite_color; | ||||
|         BitField<14, 1, u16> favorite; | ||||
|     } mii_information; | ||||
|     std::array<char16_t, 0xA> mii_name; | ||||
|     u8 height; | ||||
|     u8 build; | ||||
|     union { | ||||
|         u8 raw; | ||||
|  | ||||
|         BitField<0, 1, u8> disable_sharing; | ||||
|         BitField<1, 4, u8> face_shape; | ||||
|         BitField<5, 3, u8> skin_color; | ||||
|     } appearance_bits1; | ||||
|     union { | ||||
|         u8 raw; | ||||
|  | ||||
|         BitField<0, 4, u8> wrinkles; | ||||
|         BitField<4, 4, u8> makeup; | ||||
|     } appearance_bits2; | ||||
|     u8 hair_style; | ||||
|     union { | ||||
|         u8 raw; | ||||
|  | ||||
|         BitField<0, 3, u8> hair_color; | ||||
|         BitField<3, 1, u8> flip_hair; | ||||
|     } appearance_bits3; | ||||
|     union { | ||||
|         u32 raw; | ||||
|  | ||||
|         BitField<0, 6, u32> eye_type; | ||||
|         BitField<6, 3, u32> eye_color; | ||||
|         BitField<9, 4, u32> eye_scale; | ||||
|         BitField<13, 3, u32> eye_vertical_stretch; | ||||
|         BitField<16, 5, u32> eye_rotation; | ||||
|         BitField<21, 4, u32> eye_spacing; | ||||
|         BitField<25, 5, u32> eye_y_position; | ||||
|     } appearance_bits4; | ||||
|     union { | ||||
|         u32 raw; | ||||
|  | ||||
|         BitField<0, 5, u32> eyebrow_style; | ||||
|         BitField<5, 3, u32> eyebrow_color; | ||||
|         BitField<8, 4, u32> eyebrow_scale; | ||||
|         BitField<12, 3, u32> eyebrow_yscale; | ||||
|         BitField<16, 4, u32> eyebrow_rotation; | ||||
|         BitField<21, 4, u32> eyebrow_spacing; | ||||
|         BitField<25, 5, u32> eyebrow_y_position; | ||||
|     } appearance_bits5; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 5, u16> nose_type; | ||||
|         BitField<5, 4, u16> nose_scale; | ||||
|         BitField<9, 5, u16> nose_y_position; | ||||
|     } appearance_bits6; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 6, u16> mouth_type; | ||||
|         BitField<6, 3, u16> mouth_color; | ||||
|         BitField<9, 4, u16> mouth_scale; | ||||
|         BitField<13, 3, u16> mouth_horizontal_stretch; | ||||
|     } appearance_bits7; | ||||
|     union { | ||||
|         u8 raw; | ||||
|  | ||||
|         BitField<0, 5, u8> mouth_y_position; | ||||
|         BitField<5, 3, u8> mustache_type; | ||||
|     } appearance_bits8; | ||||
|     u8 allow_copying; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 3, u16> bear_type; | ||||
|         BitField<3, 3, u16> facial_hair_color; | ||||
|         BitField<6, 4, u16> mustache_scale; | ||||
|         BitField<10, 5, u16> mustache_y_position; | ||||
|     } appearance_bits9; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 4, u16> glasses_type; | ||||
|         BitField<4, 3, u16> glasses_color; | ||||
|         BitField<7, 4, u16> glasses_scale; | ||||
|         BitField<11, 5, u16> glasses_y_position; | ||||
|     } appearance_bits10; | ||||
|     union { | ||||
|         u16 raw; | ||||
|  | ||||
|         BitField<0, 1, u16> mole_enabled; | ||||
|         BitField<1, 4, u16> mole_scale; | ||||
|         BitField<5, 5, u16> mole_x_position; | ||||
|         BitField<10, 5, u16> mole_y_position; | ||||
|     } appearance_bits11; | ||||
|  | ||||
|     std::array<u16_le, 0xA> author_name; | ||||
|     INSERT_PADDING_BYTES(0x4); | ||||
| }; | ||||
| static_assert(sizeof(Ver3StoreData) == 0x60, "Ver3StoreData is an invalid size"); | ||||
|  | ||||
| struct MiiStoreData { | ||||
|     using Name = std::array<char16_t, 10>; | ||||
|  | ||||
|   | ||||
							
								
								
									
										383
									
								
								src/core/hle/service/nfp/amiibo_crypto.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										383
									
								
								src/core/hle/service/nfp/amiibo_crypto.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,383 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| // SPDX-FileCopyrightText: Copyright 2017 socram8888/amiitool | ||||
| // SPDX-License-Identifier: MIT | ||||
|  | ||||
| #include <array> | ||||
| #include <mbedtls/aes.h> | ||||
| #include <mbedtls/hmac_drbg.h> | ||||
|  | ||||
| #include "common/fs/file.h" | ||||
| #include "common/fs/path_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "core/hle/service/mii/mii_manager.h" | ||||
| #include "core/hle/service/nfp/amiibo_crypto.h" | ||||
|  | ||||
| namespace Service::NFP::AmiiboCrypto { | ||||
|  | ||||
| bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file) { | ||||
|     const auto& amiibo_data = ntag_file.user_memory; | ||||
|     LOG_DEBUG(Service_NFP, "uuid_lock=0x{0:x}", ntag_file.static_lock); | ||||
|     LOG_DEBUG(Service_NFP, "compability_container=0x{0:x}", ntag_file.compability_container); | ||||
|     LOG_INFO(Service_NFP, "write_count={}", amiibo_data.write_counter); | ||||
|  | ||||
|     LOG_INFO(Service_NFP, "character_id=0x{0:x}", amiibo_data.model_info.character_id); | ||||
|     LOG_INFO(Service_NFP, "character_variant={}", amiibo_data.model_info.character_variant); | ||||
|     LOG_INFO(Service_NFP, "amiibo_type={}", amiibo_data.model_info.amiibo_type); | ||||
|     LOG_INFO(Service_NFP, "model_number=0x{0:x}", amiibo_data.model_info.model_number); | ||||
|     LOG_INFO(Service_NFP, "series={}", amiibo_data.model_info.series); | ||||
|     LOG_DEBUG(Service_NFP, "fixed_value=0x{0:x}", amiibo_data.model_info.constant_value); | ||||
|  | ||||
|     LOG_DEBUG(Service_NFP, "tag_dynamic_lock=0x{0:x}", ntag_file.dynamic_lock); | ||||
|     LOG_DEBUG(Service_NFP, "tag_CFG0=0x{0:x}", ntag_file.CFG0); | ||||
|     LOG_DEBUG(Service_NFP, "tag_CFG1=0x{0:x}", ntag_file.CFG1); | ||||
|  | ||||
|     // Validate UUID | ||||
|     constexpr u8 CT = 0x88; // As defined in `ISO / IEC 14443 - 3` | ||||
|     if ((CT ^ ntag_file.uuid[0] ^ ntag_file.uuid[1] ^ ntag_file.uuid[2]) != ntag_file.uuid[3]) { | ||||
|         return false; | ||||
|     } | ||||
|     if ((ntag_file.uuid[4] ^ ntag_file.uuid[5] ^ ntag_file.uuid[6] ^ ntag_file.uuid[7]) != | ||||
|         ntag_file.uuid[8]) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Check against all know constants on an amiibo binary | ||||
|     if (ntag_file.static_lock != 0xE00F) { | ||||
|         return false; | ||||
|     } | ||||
|     if (ntag_file.compability_container != 0xEEFF10F1U) { | ||||
|         return false; | ||||
|     } | ||||
|     if (amiibo_data.constant_value != 0xA5) { | ||||
|         return false; | ||||
|     } | ||||
|     if (amiibo_data.model_info.constant_value != 0x02) { | ||||
|         return false; | ||||
|     } | ||||
|     // dynamic_lock value apparently is not constant | ||||
|     // ntag_file.dynamic_lock == 0x0F0001 | ||||
|     if (ntag_file.CFG0 != 0x04000000U) { | ||||
|         return false; | ||||
|     } | ||||
|     if (ntag_file.CFG1 != 0x5F) { | ||||
|         return false; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data) { | ||||
|     NTAG215File encoded_data{}; | ||||
|  | ||||
|     memcpy(encoded_data.uuid2.data(), nfc_data.uuid.data() + 0x8, sizeof(encoded_data.uuid2)); | ||||
|     encoded_data.static_lock = nfc_data.static_lock; | ||||
|     encoded_data.compability_container = nfc_data.compability_container; | ||||
|     encoded_data.hmac_data = nfc_data.user_memory.hmac_data; | ||||
|     encoded_data.constant_value = nfc_data.user_memory.constant_value; | ||||
|     encoded_data.write_counter = nfc_data.user_memory.write_counter; | ||||
|     encoded_data.settings = nfc_data.user_memory.settings; | ||||
|     encoded_data.owner_mii = nfc_data.user_memory.owner_mii; | ||||
|     encoded_data.title_id = nfc_data.user_memory.title_id; | ||||
|     encoded_data.applicaton_write_counter = nfc_data.user_memory.applicaton_write_counter; | ||||
|     encoded_data.application_area_id = nfc_data.user_memory.application_area_id; | ||||
|     encoded_data.unknown = nfc_data.user_memory.unknown; | ||||
|     encoded_data.hash = nfc_data.user_memory.hash; | ||||
|     encoded_data.application_area = nfc_data.user_memory.application_area; | ||||
|     encoded_data.hmac_tag = nfc_data.user_memory.hmac_tag; | ||||
|     memcpy(encoded_data.uuid.data(), nfc_data.uuid.data(), sizeof(encoded_data.uuid)); | ||||
|     encoded_data.model_info = nfc_data.user_memory.model_info; | ||||
|     encoded_data.keygen_salt = nfc_data.user_memory.keygen_salt; | ||||
|     encoded_data.dynamic_lock = nfc_data.dynamic_lock; | ||||
|     encoded_data.CFG0 = nfc_data.CFG0; | ||||
|     encoded_data.CFG1 = nfc_data.CFG1; | ||||
|     encoded_data.password = nfc_data.password; | ||||
|  | ||||
|     return encoded_data; | ||||
| } | ||||
|  | ||||
| EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data) { | ||||
|     EncryptedNTAG215File nfc_data{}; | ||||
|  | ||||
|     memcpy(nfc_data.uuid.data() + 0x8, encoded_data.uuid2.data(), sizeof(encoded_data.uuid2)); | ||||
|     memcpy(nfc_data.uuid.data(), encoded_data.uuid.data(), sizeof(encoded_data.uuid)); | ||||
|     nfc_data.static_lock = encoded_data.static_lock; | ||||
|     nfc_data.compability_container = encoded_data.compability_container; | ||||
|     nfc_data.user_memory.hmac_data = encoded_data.hmac_data; | ||||
|     nfc_data.user_memory.constant_value = encoded_data.constant_value; | ||||
|     nfc_data.user_memory.write_counter = encoded_data.write_counter; | ||||
|     nfc_data.user_memory.settings = encoded_data.settings; | ||||
|     nfc_data.user_memory.owner_mii = encoded_data.owner_mii; | ||||
|     nfc_data.user_memory.title_id = encoded_data.title_id; | ||||
|     nfc_data.user_memory.applicaton_write_counter = encoded_data.applicaton_write_counter; | ||||
|     nfc_data.user_memory.application_area_id = encoded_data.application_area_id; | ||||
|     nfc_data.user_memory.unknown = encoded_data.unknown; | ||||
|     nfc_data.user_memory.hash = encoded_data.hash; | ||||
|     nfc_data.user_memory.application_area = encoded_data.application_area; | ||||
|     nfc_data.user_memory.hmac_tag = encoded_data.hmac_tag; | ||||
|     nfc_data.user_memory.model_info = encoded_data.model_info; | ||||
|     nfc_data.user_memory.keygen_salt = encoded_data.keygen_salt; | ||||
|     nfc_data.dynamic_lock = encoded_data.dynamic_lock; | ||||
|     nfc_data.CFG0 = encoded_data.CFG0; | ||||
|     nfc_data.CFG1 = encoded_data.CFG1; | ||||
|     nfc_data.password = encoded_data.password; | ||||
|  | ||||
|     return nfc_data; | ||||
| } | ||||
|  | ||||
| u32 GetTagPassword(const TagUuid& uuid) { | ||||
|     // Verifiy that the generated password is correct | ||||
|     u32 password = 0xAA ^ (uuid[1] ^ uuid[3]); | ||||
|     password &= (0x55 ^ (uuid[2] ^ uuid[4])) << 8; | ||||
|     password &= (0xAA ^ (uuid[3] ^ uuid[5])) << 16; | ||||
|     password &= (0x55 ^ (uuid[4] ^ uuid[6])) << 24; | ||||
|     return password; | ||||
| } | ||||
|  | ||||
| HashSeed GetSeed(const NTAG215File& data) { | ||||
|     HashSeed seed{ | ||||
|         .magic = data.write_counter, | ||||
|         .padding = {}, | ||||
|         .uuid1 = {}, | ||||
|         .uuid2 = {}, | ||||
|         .keygen_salt = data.keygen_salt, | ||||
|     }; | ||||
|  | ||||
|     // Copy the first 8 bytes of uuid | ||||
|     memcpy(seed.uuid1.data(), data.uuid.data(), sizeof(seed.uuid1)); | ||||
|     memcpy(seed.uuid2.data(), data.uuid.data(), sizeof(seed.uuid2)); | ||||
|  | ||||
|     return seed; | ||||
| } | ||||
|  | ||||
| std::vector<u8> GenerateInternalKey(const InternalKey& key, const HashSeed& seed) { | ||||
|     const std::size_t seedPart1Len = sizeof(key.magic_bytes) - key.magic_length; | ||||
|     const std::size_t string_size = key.type_string.size(); | ||||
|     std::vector<u8> output(string_size + seedPart1Len); | ||||
|  | ||||
|     // Copy whole type string | ||||
|     memccpy(output.data(), key.type_string.data(), '\0', string_size); | ||||
|  | ||||
|     // Append (16 - magic_length) from the input seed | ||||
|     memcpy(output.data() + string_size, &seed, seedPart1Len); | ||||
|  | ||||
|     // Append all bytes from magicBytes | ||||
|     output.insert(output.end(), key.magic_bytes.begin(), | ||||
|                   key.magic_bytes.begin() + key.magic_length); | ||||
|  | ||||
|     output.insert(output.end(), seed.uuid1.begin(), seed.uuid1.end()); | ||||
|     output.insert(output.end(), seed.uuid2.begin(), seed.uuid2.end()); | ||||
|  | ||||
|     for (std::size_t i = 0; i < sizeof(seed.keygen_salt); i++) { | ||||
|         output.emplace_back(static_cast<u8>(seed.keygen_salt[i] ^ key.xor_pad[i])); | ||||
|     } | ||||
|  | ||||
|     return output; | ||||
| } | ||||
|  | ||||
| void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, | ||||
|                 const std::vector<u8>& seed) { | ||||
|  | ||||
|     // Initialize context | ||||
|     ctx.used = false; | ||||
|     ctx.counter = 0; | ||||
|     ctx.buffer_size = sizeof(ctx.counter) + seed.size(); | ||||
|     memcpy(ctx.buffer.data() + sizeof(u16), seed.data(), seed.size()); | ||||
|  | ||||
|     // Initialize HMAC context | ||||
|     mbedtls_md_init(&hmac_ctx); | ||||
|     mbedtls_md_setup(&hmac_ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); | ||||
|     mbedtls_md_hmac_starts(&hmac_ctx, hmac_key.data(), hmac_key.size()); | ||||
| } | ||||
|  | ||||
| void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output) { | ||||
|     // If used at least once, reinitialize the HMAC | ||||
|     if (ctx.used) { | ||||
|         mbedtls_md_hmac_reset(&hmac_ctx); | ||||
|     } | ||||
|  | ||||
|     ctx.used = true; | ||||
|  | ||||
|     // Store counter in big endian, and increment it | ||||
|     ctx.buffer[0] = static_cast<u8>(ctx.counter >> 8); | ||||
|     ctx.buffer[1] = static_cast<u8>(ctx.counter >> 0); | ||||
|     ctx.counter++; | ||||
|  | ||||
|     // Do HMAC magic | ||||
|     mbedtls_md_hmac_update(&hmac_ctx, reinterpret_cast<const unsigned char*>(ctx.buffer.data()), | ||||
|                            ctx.buffer_size); | ||||
|     mbedtls_md_hmac_finish(&hmac_ctx, output.data()); | ||||
| } | ||||
|  | ||||
| DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data) { | ||||
|     const auto seed = GetSeed(data); | ||||
|  | ||||
|     // Generate internal seed | ||||
|     const std::vector<u8> internal_key = GenerateInternalKey(key, seed); | ||||
|  | ||||
|     // Initialize context | ||||
|     CryptoCtx ctx{}; | ||||
|     mbedtls_md_context_t hmac_ctx; | ||||
|     CryptoInit(ctx, hmac_ctx, key.hmac_key, internal_key); | ||||
|  | ||||
|     // Generate derived keys | ||||
|     DerivedKeys derived_keys{}; | ||||
|     std::array<DrgbOutput, 2> temp{}; | ||||
|     CryptoStep(ctx, hmac_ctx, temp[0]); | ||||
|     CryptoStep(ctx, hmac_ctx, temp[1]); | ||||
|     memcpy(&derived_keys, temp.data(), sizeof(DerivedKeys)); | ||||
|  | ||||
|     // Cleanup context | ||||
|     mbedtls_md_free(&hmac_ctx); | ||||
|  | ||||
|     return derived_keys; | ||||
| } | ||||
|  | ||||
| void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& out_data) { | ||||
|     mbedtls_aes_context aes; | ||||
|     std::size_t nc_off = 0; | ||||
|     std::array<u8, sizeof(keys.aes_iv)> nonce_counter{}; | ||||
|     std::array<u8, sizeof(keys.aes_iv)> stream_block{}; | ||||
|  | ||||
|     const auto aes_key_size = static_cast<u32>(keys.aes_key.size() * 8); | ||||
|     mbedtls_aes_setkey_enc(&aes, keys.aes_key.data(), aes_key_size); | ||||
|     memcpy(nonce_counter.data(), keys.aes_iv.data(), sizeof(keys.aes_iv)); | ||||
|  | ||||
|     constexpr std::size_t encrypted_data_size = HMAC_TAG_START - SETTINGS_START; | ||||
|     mbedtls_aes_crypt_ctr(&aes, encrypted_data_size, &nc_off, nonce_counter.data(), | ||||
|                           stream_block.data(), | ||||
|                           reinterpret_cast<const unsigned char*>(&in_data.settings), | ||||
|                           reinterpret_cast<unsigned char*>(&out_data.settings)); | ||||
|  | ||||
|     // Copy the rest of the data directly | ||||
|     out_data.uuid2 = in_data.uuid2; | ||||
|     out_data.static_lock = in_data.static_lock; | ||||
|     out_data.compability_container = in_data.compability_container; | ||||
|  | ||||
|     out_data.constant_value = in_data.constant_value; | ||||
|     out_data.write_counter = in_data.write_counter; | ||||
|  | ||||
|     out_data.uuid = in_data.uuid; | ||||
|     out_data.model_info = in_data.model_info; | ||||
|     out_data.keygen_salt = in_data.keygen_salt; | ||||
|     out_data.dynamic_lock = in_data.dynamic_lock; | ||||
|     out_data.CFG0 = in_data.CFG0; | ||||
|     out_data.CFG1 = in_data.CFG1; | ||||
|     out_data.password = in_data.password; | ||||
| } | ||||
|  | ||||
| bool LoadKeys(InternalKey& locked_secret, InternalKey& unfixed_info) { | ||||
|     const auto yuzu_keys_dir = Common::FS::GetYuzuPath(Common::FS::YuzuPath::KeysDir); | ||||
|  | ||||
|     const Common::FS::IOFile keys_file{yuzu_keys_dir / "key_retail.bin", | ||||
|                                        Common::FS::FileAccessMode::Read, | ||||
|                                        Common::FS::FileType::BinaryFile}; | ||||
|  | ||||
|     if (!keys_file.IsOpen()) { | ||||
|         LOG_ERROR(Service_NFP, "No keys detected"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     if (keys_file.Read(unfixed_info) != 1) { | ||||
|         LOG_ERROR(Service_NFP, "Failed to read unfixed_info"); | ||||
|         return false; | ||||
|     } | ||||
|     if (keys_file.Read(locked_secret) != 1) { | ||||
|         LOG_ERROR(Service_NFP, "Failed to read locked-secret"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool DecodeAmiibo(const EncryptedNTAG215File& encrypted_tag_data, NTAG215File& tag_data) { | ||||
|     InternalKey locked_secret{}; | ||||
|     InternalKey unfixed_info{}; | ||||
|  | ||||
|     if (!LoadKeys(locked_secret, unfixed_info)) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Generate keys | ||||
|     NTAG215File encoded_data = NfcDataToEncodedData(encrypted_tag_data); | ||||
|     const auto data_keys = GenerateKey(unfixed_info, encoded_data); | ||||
|     const auto tag_keys = GenerateKey(locked_secret, encoded_data); | ||||
|  | ||||
|     // Decrypt | ||||
|     Cipher(data_keys, encoded_data, tag_data); | ||||
|  | ||||
|     // Regenerate tag HMAC. Note: order matters, data HMAC depends on tag HMAC! | ||||
|     constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; | ||||
|     mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), | ||||
|                     sizeof(HmacKey), reinterpret_cast<const unsigned char*>(&tag_data.uuid), | ||||
|                     input_length, reinterpret_cast<unsigned char*>(&tag_data.hmac_tag)); | ||||
|  | ||||
|     // Regenerate data HMAC | ||||
|     constexpr std::size_t input_length2 = DYNAMIC_LOCK_START - WRITE_COUNTER_START; | ||||
|     mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), data_keys.hmac_key.data(), | ||||
|                     sizeof(HmacKey), | ||||
|                     reinterpret_cast<const unsigned char*>(&tag_data.write_counter), input_length2, | ||||
|                     reinterpret_cast<unsigned char*>(&tag_data.hmac_data)); | ||||
|  | ||||
|     if (tag_data.hmac_data != encrypted_tag_data.user_memory.hmac_data) { | ||||
|         LOG_ERROR(Service_NFP, "hmac_data doesn't match"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     if (tag_data.hmac_tag != encrypted_tag_data.user_memory.hmac_tag) { | ||||
|         LOG_ERROR(Service_NFP, "hmac_tag doesn't match"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool EncodeAmiibo(const NTAG215File& tag_data, EncryptedNTAG215File& encrypted_tag_data) { | ||||
|     InternalKey locked_secret{}; | ||||
|     InternalKey unfixed_info{}; | ||||
|  | ||||
|     if (!LoadKeys(locked_secret, unfixed_info)) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Generate keys | ||||
|     const auto data_keys = GenerateKey(unfixed_info, tag_data); | ||||
|     const auto tag_keys = GenerateKey(locked_secret, tag_data); | ||||
|  | ||||
|     NTAG215File encoded_tag_data{}; | ||||
|  | ||||
|     // Generate tag HMAC | ||||
|     constexpr std::size_t input_length = DYNAMIC_LOCK_START - UUID_START; | ||||
|     constexpr std::size_t input_length2 = HMAC_TAG_START - WRITE_COUNTER_START; | ||||
|     mbedtls_md_hmac(mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), tag_keys.hmac_key.data(), | ||||
|                     sizeof(HmacKey), reinterpret_cast<const unsigned char*>(&tag_data.uuid), | ||||
|                     input_length, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_tag)); | ||||
|  | ||||
|     // Init mbedtls HMAC context | ||||
|     mbedtls_md_context_t ctx; | ||||
|     mbedtls_md_init(&ctx); | ||||
|     mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1); | ||||
|  | ||||
|     // Generate data HMAC | ||||
|     mbedtls_md_hmac_starts(&ctx, data_keys.hmac_key.data(), sizeof(HmacKey)); | ||||
|     mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char*>(&tag_data.write_counter), | ||||
|                            input_length2); // Data | ||||
|     mbedtls_md_hmac_update(&ctx, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_tag), | ||||
|                            sizeof(HashData)); // Tag HMAC | ||||
|     mbedtls_md_hmac_update(&ctx, reinterpret_cast<const unsigned char*>(&tag_data.uuid), | ||||
|                            input_length); | ||||
|     mbedtls_md_hmac_finish(&ctx, reinterpret_cast<unsigned char*>(&encoded_tag_data.hmac_data)); | ||||
|  | ||||
|     // HMAC cleanup | ||||
|     mbedtls_md_free(&ctx); | ||||
|  | ||||
|     // Encrypt | ||||
|     Cipher(data_keys, tag_data, encoded_tag_data); | ||||
|  | ||||
|     // Convert back to hardware | ||||
|     encrypted_tag_data = EncodedDataToNfcData(encoded_tag_data); | ||||
|  | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| } // namespace Service::NFP::AmiiboCrypto | ||||
							
								
								
									
										98
									
								
								src/core/hle/service/nfp/amiibo_crypto.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/core/hle/service/nfp/amiibo_crypto.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,98 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
|  | ||||
| #include "core/hle/service/nfp/amiibo_types.h" | ||||
|  | ||||
| struct mbedtls_md_context_t; | ||||
|  | ||||
| namespace Service::NFP::AmiiboCrypto { | ||||
| // Byte locations in Service::NFP::NTAG215File | ||||
| constexpr std::size_t HMAC_DATA_START = 0x8; | ||||
| constexpr std::size_t SETTINGS_START = 0x2c; | ||||
| constexpr std::size_t WRITE_COUNTER_START = 0x29; | ||||
| constexpr std::size_t HMAC_TAG_START = 0x1B4; | ||||
| constexpr std::size_t UUID_START = 0x1D4; | ||||
| constexpr std::size_t DYNAMIC_LOCK_START = 0x208; | ||||
|  | ||||
| using HmacKey = std::array<u8, 0x10>; | ||||
| using DrgbOutput = std::array<u8, 0x20>; | ||||
|  | ||||
| struct HashSeed { | ||||
|     u16 magic; | ||||
|     std::array<u8, 0xE> padding; | ||||
|     std::array<u8, 0x8> uuid1; | ||||
|     std::array<u8, 0x8> uuid2; | ||||
|     std::array<u8, 0x20> keygen_salt; | ||||
| }; | ||||
| static_assert(sizeof(HashSeed) == 0x40, "HashSeed is an invalid size"); | ||||
|  | ||||
| struct InternalKey { | ||||
|     HmacKey hmac_key; | ||||
|     std::array<char, 0xE> type_string; | ||||
|     u8 reserved; | ||||
|     u8 magic_length; | ||||
|     std::array<u8, 0x10> magic_bytes; | ||||
|     std::array<u8, 0x20> xor_pad; | ||||
| }; | ||||
| static_assert(sizeof(InternalKey) == 0x50, "InternalKey is an invalid size"); | ||||
| static_assert(std::is_trivially_copyable_v<InternalKey>, "InternalKey must be trivially copyable."); | ||||
|  | ||||
| struct CryptoCtx { | ||||
|     std::array<char, 480> buffer; | ||||
|     bool used; | ||||
|     std::size_t buffer_size; | ||||
|     s16 counter; | ||||
| }; | ||||
|  | ||||
| struct DerivedKeys { | ||||
|     std::array<u8, 0x10> aes_key; | ||||
|     std::array<u8, 0x10> aes_iv; | ||||
|     std::array<u8, 0x10> hmac_key; | ||||
| }; | ||||
| static_assert(sizeof(DerivedKeys) == 0x30, "DerivedKeys is an invalid size"); | ||||
|  | ||||
| /// Validates that the amiibo file is not corrupted | ||||
| bool IsAmiiboValid(const EncryptedNTAG215File& ntag_file); | ||||
|  | ||||
| /// Converts from encrypted file format to encoded file format | ||||
| NTAG215File NfcDataToEncodedData(const EncryptedNTAG215File& nfc_data); | ||||
|  | ||||
| /// Converts from encoded file format to encrypted file format | ||||
| EncryptedNTAG215File EncodedDataToNfcData(const NTAG215File& encoded_data); | ||||
|  | ||||
| /// Returns password needed to allow write access to protected memory | ||||
| u32 GetTagPassword(const TagUuid& uuid); | ||||
|  | ||||
| // Generates Seed needed for key derivation | ||||
| HashSeed GetSeed(const NTAG215File& data); | ||||
|  | ||||
| // Middle step on the generation of derived keys | ||||
| std::vector<u8> GenerateInternalKey(const InternalKey& key, const HashSeed& seed); | ||||
|  | ||||
| // Initializes mbedtls context | ||||
| void CryptoInit(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, const HmacKey& hmac_key, | ||||
|                 const std::vector<u8>& seed); | ||||
|  | ||||
| // Feeds data to mbedtls context to generate the derived key | ||||
| void CryptoStep(CryptoCtx& ctx, mbedtls_md_context_t& hmac_ctx, DrgbOutput& output); | ||||
|  | ||||
| // Generates the derived key from amiibo data | ||||
| DerivedKeys GenerateKey(const InternalKey& key, const NTAG215File& data); | ||||
|  | ||||
| // Encodes or decodes amiibo data | ||||
| void Cipher(const DerivedKeys& keys, const NTAG215File& in_data, NTAG215File& out_data); | ||||
|  | ||||
| /// Loads both amiibo keys from key_retail.bin | ||||
| bool LoadKeys(InternalKey& locked_secret, InternalKey& unfixed_info); | ||||
|  | ||||
| /// Decodes encripted amiibo data returns true if output is valid | ||||
| bool DecodeAmiibo(const EncryptedNTAG215File& encrypted_tag_data, NTAG215File& tag_data); | ||||
|  | ||||
| /// Encodes plain amiibo data returns true if output is valid | ||||
| bool EncodeAmiibo(const NTAG215File& tag_data, EncryptedNTAG215File& encrypted_tag_data); | ||||
|  | ||||
| } // namespace Service::NFP::AmiiboCrypto | ||||
							
								
								
									
										197
									
								
								src/core/hle/service/nfp/amiibo_types.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										197
									
								
								src/core/hle/service/nfp/amiibo_types.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,197 @@ | ||||
| // SPDX-FileCopyrightText: Copyright 2022 yuzu Emulator Project | ||||
| // SPDX-License-Identifier: GPL-3.0-or-later | ||||
|  | ||||
| #pragma once | ||||
|  | ||||
| #include <array> | ||||
|  | ||||
| #include "core/hle/service/mii/types.h" | ||||
|  | ||||
| namespace Service::NFP { | ||||
| static constexpr std::size_t amiibo_name_length = 0xA; | ||||
|  | ||||
| enum class ServiceType : u32 { | ||||
|     User, | ||||
|     Debug, | ||||
|     System, | ||||
| }; | ||||
|  | ||||
| enum class State : u32 { | ||||
|     NonInitialized, | ||||
|     Initialized, | ||||
| }; | ||||
|  | ||||
| enum class DeviceState : u32 { | ||||
|     Initialized, | ||||
|     SearchingForTag, | ||||
|     TagFound, | ||||
|     TagRemoved, | ||||
|     TagMounted, | ||||
|     Unaviable, | ||||
|     Finalized, | ||||
| }; | ||||
|  | ||||
| enum class ModelType : u32 { | ||||
|     Amiibo, | ||||
| }; | ||||
|  | ||||
| enum class MountTarget : u32 { | ||||
|     Rom, | ||||
|     Ram, | ||||
|     All, | ||||
| }; | ||||
|  | ||||
| enum class AmiiboType : u8 { | ||||
|     Figure, | ||||
|     Card, | ||||
|     Yarn, | ||||
| }; | ||||
|  | ||||
| enum class AmiiboSeries : u8 { | ||||
|     SuperSmashBros, | ||||
|     SuperMario, | ||||
|     ChibiRobo, | ||||
|     YoshiWoollyWorld, | ||||
|     Splatoon, | ||||
|     AnimalCrossing, | ||||
|     EightBitMario, | ||||
|     Skylanders, | ||||
|     Unknown8, | ||||
|     TheLegendOfZelda, | ||||
|     ShovelKnight, | ||||
|     Unknown11, | ||||
|     Kiby, | ||||
|     Pokemon, | ||||
|     MarioSportsSuperstars, | ||||
|     MonsterHunter, | ||||
|     BoxBoy, | ||||
|     Pikmin, | ||||
|     FireEmblem, | ||||
|     Metroid, | ||||
|     Others, | ||||
|     MegaMan, | ||||
|     Diablo, | ||||
| }; | ||||
|  | ||||
| using TagUuid = std::array<u8, 10>; | ||||
| using HashData = std::array<u8, 0x20>; | ||||
| using ApplicationArea = std::array<u8, 0xD8>; | ||||
|  | ||||
| struct AmiiboDate { | ||||
|     u16 raw_date{}; | ||||
|  | ||||
|     u16 GetYear() const { | ||||
|         return static_cast<u16>(((raw_date & 0xFE00) >> 9) + 2000); | ||||
|     } | ||||
|     u8 GetMonth() const { | ||||
|         return static_cast<u8>(((raw_date & 0x01E0) >> 5) - 1); | ||||
|     } | ||||
|     u8 GetDay() const { | ||||
|         return static_cast<u8>(raw_date & 0x001F); | ||||
|     } | ||||
| }; | ||||
| static_assert(sizeof(AmiiboDate) == 2, "AmiiboDate is an invalid size"); | ||||
|  | ||||
| struct Settings { | ||||
|     union { | ||||
|         u8 raw{}; | ||||
|  | ||||
|         BitField<4, 1, u8> amiibo_initialized; | ||||
|         BitField<5, 1, u8> appdata_initialized; | ||||
|     }; | ||||
| }; | ||||
| static_assert(sizeof(Settings) == 1, "AmiiboDate is an invalid size"); | ||||
|  | ||||
| struct AmiiboSettings { | ||||
|     Settings settings; | ||||
|     u8 country_code_id; | ||||
|     u16_be crc_counter; // Incremented each time crc is changed | ||||
|     AmiiboDate init_date; | ||||
|     AmiiboDate write_date; | ||||
|     u32_be crc; | ||||
|     std::array<u16_be, amiibo_name_length> amiibo_name; // UTF-16 text | ||||
| }; | ||||
| static_assert(sizeof(AmiiboSettings) == 0x20, "AmiiboSettings is an invalid size"); | ||||
|  | ||||
| struct AmiiboModelInfo { | ||||
|     u16 character_id; | ||||
|     u8 character_variant; | ||||
|     AmiiboType amiibo_type; | ||||
|     u16 model_number; | ||||
|     AmiiboSeries series; | ||||
|     u8 constant_value;         // Must be 02 | ||||
|     INSERT_PADDING_BYTES(0x4); // Unknown | ||||
| }; | ||||
| static_assert(sizeof(AmiiboModelInfo) == 0xC, "AmiiboModelInfo is an invalid size"); | ||||
|  | ||||
| struct NTAG215Password { | ||||
|     u32 PWD;  // Password to allow write access | ||||
|     u16 PACK; // Password acknowledge reply | ||||
|     u16 RFUI; // Reserved for future use | ||||
| }; | ||||
| static_assert(sizeof(NTAG215Password) == 0x8, "NTAG215Password is an invalid size"); | ||||
|  | ||||
| #pragma pack(1) | ||||
| struct EncryptedAmiiboFile { | ||||
|     u8 constant_value;                     // Must be A5 | ||||
|     u16 write_counter;                     // Number of times the amiibo has been written? | ||||
|     INSERT_PADDING_BYTES(0x1);             // Unknown 1 | ||||
|     AmiiboSettings settings;               // Encrypted amiibo settings | ||||
|     HashData hmac_tag;                     // Hash | ||||
|     AmiiboModelInfo model_info;            // Encrypted amiibo model info | ||||
|     HashData keygen_salt;                  // Salt | ||||
|     HashData hmac_data;                    // Hash | ||||
|     Service::Mii::Ver3StoreData owner_mii; // Encrypted Mii data | ||||
|     u64_be title_id;                       // Encrypted Game id | ||||
|     u16_be applicaton_write_counter;       // Encrypted Counter | ||||
|     u32_be application_area_id;            // Encrypted Game id | ||||
|     std::array<u8, 0x2> unknown; | ||||
|     HashData hash;                    // Probably a SHA256-HMAC hash? | ||||
|     ApplicationArea application_area; // Encrypted Game data | ||||
| }; | ||||
| static_assert(sizeof(EncryptedAmiiboFile) == 0x1F8, "AmiiboFile is an invalid size"); | ||||
|  | ||||
| struct NTAG215File { | ||||
|     std::array<u8, 0x2> uuid2; | ||||
|     u16 static_lock;           // Set defined pages as read only | ||||
|     u32 compability_container; // Defines available memory | ||||
|     HashData hmac_data;        // Hash | ||||
|     u8 constant_value;         // Must be A5 | ||||
|     u16 write_counter;         // Number of times the amiibo has been written? | ||||
|     INSERT_PADDING_BYTES(0x1); // Unknown 1 | ||||
|     AmiiboSettings settings; | ||||
|     Service::Mii::Ver3StoreData owner_mii; // Encrypted Mii data | ||||
|     u64_be title_id; | ||||
|     u16_be applicaton_write_counter; // Encrypted Counter | ||||
|     u32_be application_area_id; | ||||
|     std::array<u8, 0x2> unknown; | ||||
|     HashData hash;                    // Probably a SHA256-HMAC hash? | ||||
|     ApplicationArea application_area; // Encrypted Game data | ||||
|     HashData hmac_tag;                // Hash | ||||
|     std::array<u8, 0x8> uuid; | ||||
|     AmiiboModelInfo model_info; | ||||
|     HashData keygen_salt;     // Salt | ||||
|     u32 dynamic_lock;         // Dynamic lock | ||||
|     u32 CFG0;                 // Defines memory protected by password | ||||
|     u32 CFG1;                 // Defines number of verification attempts | ||||
|     NTAG215Password password; // Password data | ||||
| }; | ||||
| static_assert(sizeof(NTAG215File) == 0x21C, "NTAG215File is an invalid size"); | ||||
| static_assert(std::is_trivially_copyable_v<NTAG215File>, "NTAG215File must be trivially copyable."); | ||||
| #pragma pack() | ||||
|  | ||||
| struct EncryptedNTAG215File { | ||||
|     TagUuid uuid;                    // Unique serial number | ||||
|     u16 static_lock;                 // Set defined pages as read only | ||||
|     u32 compability_container;       // Defines available memory | ||||
|     EncryptedAmiiboFile user_memory; // Writable data | ||||
|     u32 dynamic_lock;                // Dynamic lock | ||||
|     u32 CFG0;                        // Defines memory protected by password | ||||
|     u32 CFG1;                        // Defines number of verification attempts | ||||
|     NTAG215Password password;        // Password data | ||||
| }; | ||||
| static_assert(sizeof(EncryptedNTAG215File) == 0x21C, "EncryptedNTAG215File is an invalid size"); | ||||
| static_assert(std::is_trivially_copyable_v<EncryptedNTAG215File>, | ||||
|               "EncryptedNTAG215File must be trivially copyable."); | ||||
|  | ||||
| } // namespace Service::NFP | ||||
| @@ -4,7 +4,10 @@ | ||||
| #include <array> | ||||
| #include <atomic> | ||||
|  | ||||
| #include "common/fs/file.h" | ||||
| #include "common/fs/path_util.h" | ||||
| #include "common/logging/log.h" | ||||
| #include "common/string_util.h" | ||||
| #include "core/core.h" | ||||
| #include "core/hid/emulated_controller.h" | ||||
| #include "core/hid/hid_core.h" | ||||
| @@ -12,6 +15,7 @@ | ||||
| #include "core/hle/ipc_helpers.h" | ||||
| #include "core/hle/kernel/k_event.h" | ||||
| #include "core/hle/service/mii/mii_manager.h" | ||||
| #include "core/hle/service/nfp/amiibo_crypto.h" | ||||
| #include "core/hle/service/nfp/nfp.h" | ||||
| #include "core/hle/service/nfp/nfp_user.h" | ||||
|  | ||||
| @@ -19,12 +23,14 @@ namespace Service::NFP { | ||||
| namespace ErrCodes { | ||||
| constexpr Result DeviceNotFound(ErrorModule::NFP, 64); | ||||
| constexpr Result WrongDeviceState(ErrorModule::NFP, 73); | ||||
| constexpr Result NfcDisabled(ErrorModule::NFP, 80); | ||||
| constexpr Result WriteAmiiboFailed(ErrorModule::NFP, 88); | ||||
| constexpr Result TagRemoved(ErrorModule::NFP, 97); | ||||
| constexpr Result ApplicationAreaIsNotInitialized(ErrorModule::NFP, 128); | ||||
| constexpr Result WrongApplicationAreaId(ErrorModule::NFP, 152); | ||||
| constexpr Result ApplicationAreaExist(ErrorModule::NFP, 168); | ||||
| } // namespace ErrCodes | ||||
|  | ||||
| constexpr u32 ApplicationAreaSize = 0xD8; | ||||
|  | ||||
| IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) | ||||
|     : ServiceFramework{system_, "NFP::IUser"}, service_context{system_, service_name}, | ||||
|       nfp_interface{nfp_interface_} { | ||||
| @@ -39,7 +45,7 @@ IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) | ||||
|         {7, &IUser::OpenApplicationArea, "OpenApplicationArea"}, | ||||
|         {8, &IUser::GetApplicationArea, "GetApplicationArea"}, | ||||
|         {9, &IUser::SetApplicationArea, "SetApplicationArea"}, | ||||
|         {10, nullptr, "Flush"}, | ||||
|         {10, &IUser::Flush, "Flush"}, | ||||
|         {11, nullptr, "Restore"}, | ||||
|         {12, &IUser::CreateApplicationArea, "CreateApplicationArea"}, | ||||
|         {13, &IUser::GetTagInfo, "GetTagInfo"}, | ||||
| @@ -53,7 +59,7 @@ IUser::IUser(Module::Interface& nfp_interface_, Core::System& system_) | ||||
|         {21, &IUser::GetNpadId, "GetNpadId"}, | ||||
|         {22, &IUser::GetApplicationAreaSize, "GetApplicationAreaSize"}, | ||||
|         {23, &IUser::AttachAvailabilityChangeEvent, "AttachAvailabilityChangeEvent"}, | ||||
|         {24, nullptr, "RecreateApplicationArea"}, | ||||
|         {24, &IUser::RecreateApplicationArea, "RecreateApplicationArea"}, | ||||
|     }; | ||||
|     RegisterHandlers(functions); | ||||
|  | ||||
| @@ -87,11 +93,23 @@ void IUser::Finalize(Kernel::HLERequestContext& ctx) { | ||||
| void IUser::ListDevices(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_INFO(Service_NFP, "called"); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     std::vector<u64> devices; | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     devices.push_back(nfp_interface.GetHandle()); | ||||
|  | ||||
|     if (devices.size() == 0) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::DeviceNotFound); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     ctx.WriteBuffer(devices); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 3}; | ||||
| @@ -105,6 +123,12 @@ void IUser::StartDetection(Kernel::HLERequestContext& ctx) { | ||||
|     const auto nfp_protocol{rp.Pop<s32>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}, nfp_protocol={}", device_handle, nfp_protocol); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.StartDetection(nfp_protocol); | ||||
| @@ -124,6 +148,12 @@ void IUser::StopDetection(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.StopDetection(); | ||||
| @@ -146,6 +176,12 @@ void IUser::Mount(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}, model_type={}, mount_target={}", device_handle, | ||||
|              model_type, mount_target); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.Mount(); | ||||
| @@ -165,6 +201,12 @@ void IUser::Unmount(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.Unmount(); | ||||
| @@ -186,6 +228,12 @@ void IUser::OpenApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, access_id={}", device_handle, | ||||
|                 access_id); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.OpenApplicationArea(access_id); | ||||
| @@ -205,9 +253,15 @@ void IUser::GetApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         std::vector<u8> data{}; | ||||
|         ApplicationArea data{}; | ||||
|         const auto result = nfp_interface.GetApplicationArea(data); | ||||
|         ctx.WriteBuffer(data); | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
| @@ -229,6 +283,12 @@ void IUser::SetApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}", device_handle, | ||||
|                 data.size()); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.SetApplicationArea(data); | ||||
| @@ -243,6 +303,31 @@ void IUser::SetApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     rb.Push(ErrCodes::DeviceNotFound); | ||||
| } | ||||
|  | ||||
| void IUser::Flush(Kernel::HLERequestContext& ctx) { | ||||
|     IPC::RequestParser rp{ctx}; | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.Flush(); | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(result); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_NFP, "Handle not found, device_handle={}", device_handle); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2}; | ||||
|     rb.Push(ErrCodes::DeviceNotFound); | ||||
| } | ||||
|  | ||||
| void IUser::CreateApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     IPC::RequestParser rp{ctx}; | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
| @@ -251,6 +336,12 @@ void IUser::CreateApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}, access_id={}", | ||||
|                 device_handle, access_id, data.size()); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.CreateApplicationArea(access_id, data); | ||||
| @@ -270,6 +361,12 @@ void IUser::GetTagInfo(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         TagInfo tag_info{}; | ||||
| @@ -291,6 +388,12 @@ void IUser::GetRegisterInfo(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         RegisterInfo register_info{}; | ||||
| @@ -312,6 +415,12 @@ void IUser::GetCommonInfo(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         CommonInfo common_info{}; | ||||
| @@ -333,6 +442,12 @@ void IUser::GetModelInfo(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_INFO(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         ModelInfo model_info{}; | ||||
| @@ -354,6 +469,12 @@ void IUser::AttachActivateEvent(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 1}; | ||||
| @@ -373,6 +494,12 @@ void IUser::AttachDeactivateEvent(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2, 1}; | ||||
| @@ -419,6 +546,12 @@ void IUser::GetNpadId(Kernel::HLERequestContext& ctx) { | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     LOG_DEBUG(Service_NFP, "called, device_handle={}", device_handle); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
| @@ -442,7 +575,7 @@ void IUser::GetApplicationAreaSize(Kernel::HLERequestContext& ctx) { | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         IPC::ResponseBuilder rb{ctx, 3}; | ||||
|         rb.Push(ResultSuccess); | ||||
|         rb.Push(ApplicationAreaSize); | ||||
|         rb.Push(sizeof(ApplicationArea)); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
| @@ -455,11 +588,45 @@ void IUser::GetApplicationAreaSize(Kernel::HLERequestContext& ctx) { | ||||
| void IUser::AttachAvailabilityChangeEvent(Kernel::HLERequestContext& ctx) { | ||||
|     LOG_DEBUG(Service_NFP, "(STUBBED) called"); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2, 1}; | ||||
|     rb.Push(ResultSuccess); | ||||
|     rb.PushCopyObjects(availability_change_event->GetReadableEvent()); | ||||
| } | ||||
|  | ||||
| void IUser::RecreateApplicationArea(Kernel::HLERequestContext& ctx) { | ||||
|     IPC::RequestParser rp{ctx}; | ||||
|     const auto device_handle{rp.Pop<u64>()}; | ||||
|     const auto access_id{rp.Pop<u32>()}; | ||||
|     const auto data{ctx.ReadBuffer()}; | ||||
|     LOG_WARNING(Service_NFP, "(STUBBED) called, device_handle={}, data_size={}, access_id={}", | ||||
|                 device_handle, access_id, data.size()); | ||||
|  | ||||
|     if (state == State::NonInitialized) { | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(ErrCodes::NfcDisabled); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     // TODO(german77): Loop through all interfaces | ||||
|     if (device_handle == nfp_interface.GetHandle()) { | ||||
|         const auto result = nfp_interface.RecreateApplicationArea(access_id, data); | ||||
|         IPC::ResponseBuilder rb{ctx, 2}; | ||||
|         rb.Push(result); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_NFP, "Handle not found, device_handle={}", device_handle); | ||||
|  | ||||
|     IPC::ResponseBuilder rb{ctx, 2}; | ||||
|     rb.Push(ErrCodes::DeviceNotFound); | ||||
| } | ||||
|  | ||||
| Module::Interface::Interface(std::shared_ptr<Module> module_, Core::System& system_, | ||||
|                              const char* name) | ||||
|     : ServiceFramework{system_, name}, module{std::move(module_)}, | ||||
| @@ -478,37 +645,43 @@ void Module::Interface::CreateUserInterface(Kernel::HLERequestContext& ctx) { | ||||
|     rb.PushIpcInterface<IUser>(*this, system); | ||||
| } | ||||
|  | ||||
| bool Module::Interface::LoadAmiibo(const std::vector<u8>& buffer) { | ||||
| bool Module::Interface::LoadAmiiboFile(const std::string& filename) { | ||||
|     constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password); | ||||
|     const Common::FS::IOFile amiibo_file{filename, Common::FS::FileAccessMode::Read, | ||||
|                                          Common::FS::FileType::BinaryFile}; | ||||
|  | ||||
|     if (!amiibo_file.IsOpen()) { | ||||
|         LOG_ERROR(Service_NFP, "Amiibo is already on use"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Workaround for files with missing password data | ||||
|     std::array<u8, sizeof(EncryptedNTAG215File)> buffer{}; | ||||
|     if (amiibo_file.Read(buffer) < tag_size_without_password) { | ||||
|         LOG_ERROR(Service_NFP, "Failed to read amiibo file"); | ||||
|         return false; | ||||
|     } | ||||
|     memcpy(&encrypted_tag_data, buffer.data(), sizeof(EncryptedNTAG215File)); | ||||
|  | ||||
|     if (!AmiiboCrypto::IsAmiiboValid(encrypted_tag_data)) { | ||||
|         LOG_INFO(Service_NFP, "Invalid amiibo"); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     file_path = filename; | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| bool Module::Interface::LoadAmiibo(const std::string& filename) { | ||||
|     if (device_state != DeviceState::SearchingForTag) { | ||||
|         LOG_ERROR(Service_NFP, "Game is not looking for amiibos, current state {}", device_state); | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     constexpr auto tag_size = sizeof(NTAG215File); | ||||
|     constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password); | ||||
|  | ||||
|     std::vector<u8> amiibo_buffer = buffer; | ||||
|  | ||||
|     if (amiibo_buffer.size() < tag_size_without_password) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong file size {}", buffer.size()); | ||||
|     if (!LoadAmiiboFile(filename)) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // Ensure it has the correct size | ||||
|     if (amiibo_buffer.size() != tag_size) { | ||||
|         amiibo_buffer.resize(tag_size, 0); | ||||
|     } | ||||
|  | ||||
|     LOG_INFO(Service_NFP, "Amiibo detected"); | ||||
|     std::memcpy(&tag_data, buffer.data(), tag_size); | ||||
|  | ||||
|     if (!IsAmiiboValid()) { | ||||
|         return false; | ||||
|     } | ||||
|  | ||||
|     // This value can't be dumped from a tag. Generate it | ||||
|     tag_data.password.PWD = GetTagPassword(tag_data.uuid); | ||||
|  | ||||
|     device_state = DeviceState::TagFound; | ||||
|     activate_event->GetWritableEvent().Signal(); | ||||
|     return true; | ||||
| @@ -517,55 +690,13 @@ bool Module::Interface::LoadAmiibo(const std::vector<u8>& buffer) { | ||||
| void Module::Interface::CloseAmiibo() { | ||||
|     LOG_INFO(Service_NFP, "Remove amiibo"); | ||||
|     device_state = DeviceState::TagRemoved; | ||||
|     is_data_decoded = false; | ||||
|     is_application_area_initialized = false; | ||||
|     application_area_id = 0; | ||||
|     application_area_data.clear(); | ||||
|     encrypted_tag_data = {}; | ||||
|     tag_data = {}; | ||||
|     deactivate_event->GetWritableEvent().Signal(); | ||||
| } | ||||
|  | ||||
| bool Module::Interface::IsAmiiboValid() const { | ||||
|     const auto& amiibo_data = tag_data.user_memory; | ||||
|     LOG_DEBUG(Service_NFP, "uuid_lock=0x{0:x}", tag_data.lock_bytes); | ||||
|     LOG_DEBUG(Service_NFP, "compability_container=0x{0:x}", tag_data.compability_container); | ||||
|     LOG_DEBUG(Service_NFP, "crypto_init=0x{0:x}", amiibo_data.crypto_init); | ||||
|     LOG_DEBUG(Service_NFP, "write_count={}", amiibo_data.write_count); | ||||
|  | ||||
|     LOG_DEBUG(Service_NFP, "character_id=0x{0:x}", amiibo_data.model_info.character_id); | ||||
|     LOG_DEBUG(Service_NFP, "character_variant={}", amiibo_data.model_info.character_variant); | ||||
|     LOG_DEBUG(Service_NFP, "amiibo_type={}", amiibo_data.model_info.amiibo_type); | ||||
|     LOG_DEBUG(Service_NFP, "model_number=0x{0:x}", amiibo_data.model_info.model_number); | ||||
|     LOG_DEBUG(Service_NFP, "series={}", amiibo_data.model_info.series); | ||||
|     LOG_DEBUG(Service_NFP, "fixed_value=0x{0:x}", amiibo_data.model_info.fixed); | ||||
|  | ||||
|     LOG_DEBUG(Service_NFP, "tag_dynamic_lock=0x{0:x}", tag_data.dynamic_lock); | ||||
|     LOG_DEBUG(Service_NFP, "tag_CFG0=0x{0:x}", tag_data.CFG0); | ||||
|     LOG_DEBUG(Service_NFP, "tag_CFG1=0x{0:x}", tag_data.CFG1); | ||||
|  | ||||
|     // Check against all know constants on an amiibo binary | ||||
|     if (tag_data.lock_bytes != 0xE00F) { | ||||
|         return false; | ||||
|     } | ||||
|     if (tag_data.compability_container != 0xEEFF10F1U) { | ||||
|         return false; | ||||
|     } | ||||
|     if ((amiibo_data.crypto_init & 0xFF) != 0xA5) { | ||||
|         return false; | ||||
|     } | ||||
|     if (amiibo_data.model_info.fixed != 0x02) { | ||||
|         return false; | ||||
|     } | ||||
|     if ((tag_data.dynamic_lock & 0xFFFFFF) != 0x0F0001) { | ||||
|         return false; | ||||
|     } | ||||
|     if (tag_data.CFG0 != 0x04000000U) { | ||||
|         return false; | ||||
|     } | ||||
|     if (tag_data.CFG1 != 0x5F) { | ||||
|         return false; | ||||
|     } | ||||
|     return true; | ||||
| } | ||||
|  | ||||
| Kernel::KReadableEvent& Module::Interface::GetActivateEvent() const { | ||||
|     return activate_event->GetReadableEvent(); | ||||
| } | ||||
| @@ -576,13 +707,20 @@ Kernel::KReadableEvent& Module::Interface::GetDeactivateEvent() const { | ||||
|  | ||||
| void Module::Interface::Initialize() { | ||||
|     device_state = DeviceState::Initialized; | ||||
|     is_data_decoded = false; | ||||
|     is_application_area_initialized = false; | ||||
|     encrypted_tag_data = {}; | ||||
|     tag_data = {}; | ||||
| } | ||||
|  | ||||
| void Module::Interface::Finalize() { | ||||
|     if (device_state == DeviceState::TagMounted) { | ||||
|         Unmount(); | ||||
|     } | ||||
|     if (device_state == DeviceState::SearchingForTag || device_state == DeviceState::TagRemoved) { | ||||
|         StopDetection(); | ||||
|     } | ||||
|     device_state = DeviceState::Unaviable; | ||||
|     is_application_area_initialized = false; | ||||
|     application_area_id = 0; | ||||
|     application_area_data.clear(); | ||||
| } | ||||
|  | ||||
| Result Module::Interface::StartDetection(s32 protocol_) { | ||||
| @@ -618,42 +756,102 @@ Result Module::Interface::StopDetection() { | ||||
|     return ErrCodes::WrongDeviceState; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::Mount() { | ||||
|     if (device_state == DeviceState::TagFound) { | ||||
|         device_state = DeviceState::TagMounted; | ||||
| Result Module::Interface::Flush() { | ||||
|     // Ignore write command if we can't encrypt the data | ||||
|     if (!is_data_decoded) { | ||||
|         return ResultSuccess; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|     return ErrCodes::WrongDeviceState; | ||||
|     constexpr auto tag_size_without_password = sizeof(NTAG215File) - sizeof(NTAG215Password); | ||||
|     EncryptedNTAG215File tmp_encrypted_tag_data{}; | ||||
|     const Common::FS::IOFile amiibo_file{file_path, Common::FS::FileAccessMode::ReadWrite, | ||||
|                                          Common::FS::FileType::BinaryFile}; | ||||
|  | ||||
|     if (!amiibo_file.IsOpen()) { | ||||
|         LOG_ERROR(Core, "Amiibo is already on use"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     // Workaround for files with missing password data | ||||
|     std::array<u8, sizeof(EncryptedNTAG215File)> buffer{}; | ||||
|     if (amiibo_file.Read(buffer) < tag_size_without_password) { | ||||
|         LOG_ERROR(Core, "Failed to read amiibo file"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|     memcpy(&tmp_encrypted_tag_data, buffer.data(), sizeof(EncryptedNTAG215File)); | ||||
|  | ||||
|     if (!AmiiboCrypto::IsAmiiboValid(tmp_encrypted_tag_data)) { | ||||
|         LOG_INFO(Service_NFP, "Invalid amiibo"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     bool is_uuid_equal = memcmp(tmp_encrypted_tag_data.uuid.data(), tag_data.uuid.data(), 8) == 0; | ||||
|     bool is_character_equal = tmp_encrypted_tag_data.user_memory.model_info.character_id == | ||||
|                               tag_data.model_info.character_id; | ||||
|     if (!is_uuid_equal || !is_character_equal) { | ||||
|         LOG_ERROR(Service_NFP, "Not the same amiibo"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     if (!AmiiboCrypto::EncodeAmiibo(tag_data, encrypted_tag_data)) { | ||||
|         LOG_ERROR(Service_NFP, "Failed to encode data"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     // Return to the start of the file | ||||
|     if (!amiibo_file.Seek(0)) { | ||||
|         LOG_ERROR(Service_NFP, "Error writting to file"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     if (!amiibo_file.Write(encrypted_tag_data)) { | ||||
|         LOG_ERROR(Service_NFP, "Error writting to file"); | ||||
|         return ErrCodes::WriteAmiiboFailed; | ||||
|     } | ||||
|  | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::Mount() { | ||||
|     if (device_state != DeviceState::TagFound) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     is_data_decoded = AmiiboCrypto::DecodeAmiibo(encrypted_tag_data, tag_data); | ||||
|     LOG_INFO(Service_NFP, "Is amiibo decoded {}", is_data_decoded); | ||||
|  | ||||
|     is_application_area_initialized = false; | ||||
|     device_state = DeviceState::TagMounted; | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::Unmount() { | ||||
|     if (device_state == DeviceState::TagMounted) { | ||||
|         is_application_area_initialized = false; | ||||
|         application_area_id = 0; | ||||
|         application_area_data.clear(); | ||||
|         device_state = DeviceState::TagFound; | ||||
|         return ResultSuccess; | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|     return ErrCodes::WrongDeviceState; | ||||
|     is_data_decoded = false; | ||||
|     is_application_area_initialized = false; | ||||
|     device_state = DeviceState::TagFound; | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::GetTagInfo(TagInfo& tag_info) const { | ||||
|     if (device_state == DeviceState::TagFound || device_state == DeviceState::TagMounted) { | ||||
|         tag_info = { | ||||
|             .uuid = tag_data.uuid, | ||||
|             .uuid_length = static_cast<u8>(tag_data.uuid.size()), | ||||
|             .protocol = protocol, | ||||
|             .tag_type = static_cast<u32>(tag_data.user_memory.model_info.amiibo_type), | ||||
|         }; | ||||
|         return ResultSuccess; | ||||
|     if (device_state != DeviceState::TagFound && device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|     return ErrCodes::WrongDeviceState; | ||||
|     tag_info = { | ||||
|         .uuid = encrypted_tag_data.uuid, | ||||
|         .uuid_length = static_cast<u8>(encrypted_tag_data.uuid.size()), | ||||
|         .protocol = protocol, | ||||
|         .tag_type = static_cast<u32>(encrypted_tag_data.user_memory.model_info.amiibo_type), | ||||
|     }; | ||||
|  | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::GetCommonInfo(CommonInfo& common_info) const { | ||||
| @@ -662,14 +860,28 @@ Result Module::Interface::GetCommonInfo(CommonInfo& common_info) const { | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     // Read this data from the amiibo save file | ||||
|     if (is_data_decoded && tag_data.settings.settings.amiibo_initialized != 0) { | ||||
|         const auto& settings = tag_data.settings; | ||||
|         // TODO: Validate this data | ||||
|         common_info = { | ||||
|             .last_write_year = settings.write_date.GetYear(), | ||||
|             .last_write_month = settings.write_date.GetMonth(), | ||||
|             .last_write_day = settings.write_date.GetDay(), | ||||
|             .write_counter = settings.crc_counter, | ||||
|             .version = 1, | ||||
|             .application_area_size = sizeof(ApplicationArea), | ||||
|         }; | ||||
|         return ResultSuccess; | ||||
|     } | ||||
|  | ||||
|     // Generate a generic answer | ||||
|     common_info = { | ||||
|         .last_write_year = 2022, | ||||
|         .last_write_month = 2, | ||||
|         .last_write_day = 7, | ||||
|         .write_counter = tag_data.user_memory.write_count, | ||||
|         .write_counter = 0, | ||||
|         .version = 1, | ||||
|         .application_area_size = ApplicationAreaSize, | ||||
|         .application_area_size = sizeof(ApplicationArea), | ||||
|     }; | ||||
|     return ResultSuccess; | ||||
| } | ||||
| @@ -680,26 +892,53 @@ Result Module::Interface::GetModelInfo(ModelInfo& model_info) const { | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     model_info = tag_data.user_memory.model_info; | ||||
|     const auto& model_info_data = encrypted_tag_data.user_memory.model_info; | ||||
|     model_info = { | ||||
|         .character_id = model_info_data.character_id, | ||||
|         .character_variant = model_info_data.character_variant, | ||||
|         .amiibo_type = model_info_data.amiibo_type, | ||||
|         .model_number = model_info_data.model_number, | ||||
|         .series = model_info_data.series, | ||||
|         .constant_value = model_info_data.constant_value, | ||||
|     }; | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::GetRegisterInfo(RegisterInfo& register_info) const { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     Service::Mii::MiiManager manager; | ||||
|  | ||||
|     // Read this data from the amiibo save file | ||||
|     if (is_data_decoded && tag_data.settings.settings.amiibo_initialized != 0) { | ||||
|         const auto& settings = tag_data.settings; | ||||
|  | ||||
|         // TODO: Validate this data | ||||
|         register_info = { | ||||
|             .mii_char_info = manager.ConvertV3ToCharInfo(tag_data.owner_mii), | ||||
|             .first_write_year = settings.init_date.GetYear(), | ||||
|             .first_write_month = settings.init_date.GetMonth(), | ||||
|             .first_write_day = settings.init_date.GetDay(), | ||||
|             .amiibo_name = GetAmiiboName(settings), | ||||
|             .font_region = {}, | ||||
|         }; | ||||
|  | ||||
|         return ResultSuccess; | ||||
|     } | ||||
|  | ||||
|     // Generate a generic answer | ||||
|     register_info = { | ||||
|         .mii_char_info = manager.BuildDefault(0), | ||||
|         .first_write_year = 2022, | ||||
|         .first_write_month = 2, | ||||
|         .first_write_day = 7, | ||||
|         .amiibo_name = {'Y', 'u', 'z', 'u', 'A', 'm', 'i', 'i', 'b', 'o', 0}, | ||||
|         .unknown = {}, | ||||
|         .font_region = {}, | ||||
|     }; | ||||
|     return ResultSuccess; | ||||
| } | ||||
| @@ -707,31 +946,47 @@ Result Module::Interface::GetRegisterInfo(RegisterInfo& register_info) const { | ||||
| Result Module::Interface::OpenApplicationArea(u32 access_id) { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|     if (AmiiboApplicationDataExist(access_id)) { | ||||
|         application_area_data = LoadAmiiboApplicationData(access_id); | ||||
|         application_area_id = access_id; | ||||
|         is_application_area_initialized = true; | ||||
|     } | ||||
|     if (!is_application_area_initialized) { | ||||
|  | ||||
|     // Fallback for lack of amiibo keys | ||||
|     if (!is_data_decoded) { | ||||
|         LOG_WARNING(Service_NFP, "Application area is not initialized"); | ||||
|         return ErrCodes::ApplicationAreaIsNotInitialized; | ||||
|     } | ||||
|  | ||||
|     if (tag_data.settings.settings.appdata_initialized == 0) { | ||||
|         LOG_WARNING(Service_NFP, "Application area is not initialized"); | ||||
|         return ErrCodes::ApplicationAreaIsNotInitialized; | ||||
|     } | ||||
|  | ||||
|     if (tag_data.application_area_id != access_id) { | ||||
|         LOG_WARNING(Service_NFP, "Wrong application area id"); | ||||
|         return ErrCodes::WrongApplicationAreaId; | ||||
|     } | ||||
|  | ||||
|     is_application_area_initialized = true; | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::GetApplicationArea(std::vector<u8>& data) const { | ||||
| Result Module::Interface::GetApplicationArea(ApplicationArea& data) const { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     if (!is_application_area_initialized) { | ||||
|         LOG_ERROR(Service_NFP, "Application area is not initialized"); | ||||
|         return ErrCodes::ApplicationAreaIsNotInitialized; | ||||
|     } | ||||
|  | ||||
|     data = application_area_data; | ||||
|     data = tag_data.application_area; | ||||
|  | ||||
|     return ResultSuccess; | ||||
| } | ||||
| @@ -739,46 +994,69 @@ Result Module::Interface::GetApplicationArea(std::vector<u8>& data) const { | ||||
| Result Module::Interface::SetApplicationArea(const std::vector<u8>& data) { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
|     if (!is_application_area_initialized) { | ||||
|         LOG_ERROR(Service_NFP, "Application area is not initialized"); | ||||
|         return ErrCodes::ApplicationAreaIsNotInitialized; | ||||
|     } | ||||
|     application_area_data = data; | ||||
|     SaveAmiiboApplicationData(application_area_id, application_area_data); | ||||
|  | ||||
|     if (data.size() != sizeof(ApplicationArea)) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); | ||||
|         return ResultUnknown; | ||||
|     } | ||||
|  | ||||
|     std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| Result Module::Interface::CreateApplicationArea(u32 access_id, const std::vector<u8>& data) { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|     if (AmiiboApplicationDataExist(access_id)) { | ||||
|  | ||||
|     if (tag_data.settings.settings.appdata_initialized != 0) { | ||||
|         LOG_ERROR(Service_NFP, "Application area already exist"); | ||||
|         return ErrCodes::ApplicationAreaExist; | ||||
|     } | ||||
|     application_area_data = data; | ||||
|     application_area_id = access_id; | ||||
|     SaveAmiiboApplicationData(application_area_id, application_area_data); | ||||
|  | ||||
|     if (data.size() != sizeof(ApplicationArea)) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); | ||||
|         return ResultUnknown; | ||||
|     } | ||||
|  | ||||
|     std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); | ||||
|     tag_data.application_area_id = access_id; | ||||
|  | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| bool Module::Interface::AmiiboApplicationDataExist(u32 access_id) const { | ||||
|     // TODO(german77): Check if file exist | ||||
|     return false; | ||||
| } | ||||
| Result Module::Interface::RecreateApplicationArea(u32 access_id, const std::vector<u8>& data) { | ||||
|     if (device_state != DeviceState::TagMounted) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong device state {}", device_state); | ||||
|         if (device_state == DeviceState::TagRemoved) { | ||||
|             return ErrCodes::TagRemoved; | ||||
|         } | ||||
|         return ErrCodes::WrongDeviceState; | ||||
|     } | ||||
|  | ||||
| std::vector<u8> Module::Interface::LoadAmiiboApplicationData(u32 access_id) const { | ||||
|     // TODO(german77): Read file | ||||
|     std::vector<u8> data(ApplicationAreaSize); | ||||
|     return data; | ||||
| } | ||||
|     if (data.size() != sizeof(ApplicationArea)) { | ||||
|         LOG_ERROR(Service_NFP, "Wrong data size {}", data.size()); | ||||
|         return ResultUnknown; | ||||
|     } | ||||
|  | ||||
| void Module::Interface::SaveAmiiboApplicationData(u32 access_id, | ||||
|                                                   const std::vector<u8>& data) const { | ||||
|     // TODO(german77): Save file | ||||
|     std::memcpy(&tag_data.application_area, data.data(), sizeof(ApplicationArea)); | ||||
|     tag_data.application_area_id = access_id; | ||||
|  | ||||
|     return ResultSuccess; | ||||
| } | ||||
|  | ||||
| u64 Module::Interface::GetHandle() const { | ||||
| @@ -791,16 +1069,25 @@ DeviceState Module::Interface::GetCurrentState() const { | ||||
| } | ||||
|  | ||||
| Core::HID::NpadIdType Module::Interface::GetNpadId() const { | ||||
|     return npad_id; | ||||
|     // Return first connected npad id as a workaround for lack of a single nfc interface per | ||||
|     // controller | ||||
|     return system.HIDCore().GetFirstNpadId(); | ||||
| } | ||||
|  | ||||
| u32 Module::Interface::GetTagPassword(const TagUuid& uuid) const { | ||||
|     // Verifiy that the generated password is correct | ||||
|     u32 password = 0xAA ^ (uuid[1] ^ uuid[3]); | ||||
|     password &= (0x55 ^ (uuid[2] ^ uuid[4])) << 8; | ||||
|     password &= (0xAA ^ (uuid[3] ^ uuid[5])) << 16; | ||||
|     password &= (0x55 ^ (uuid[4] ^ uuid[6])) << 24; | ||||
|     return password; | ||||
| AmiiboName Module::Interface::GetAmiiboName(const AmiiboSettings& settings) const { | ||||
|     std::array<char16_t, amiibo_name_length> settings_amiibo_name{}; | ||||
|     AmiiboName amiibo_name{}; | ||||
|  | ||||
|     // Convert from big endian to little endian | ||||
|     for (std::size_t i = 0; i < amiibo_name_length; i++) { | ||||
|         settings_amiibo_name[i] = static_cast<u16>(settings.amiibo_name[i]); | ||||
|     } | ||||
|  | ||||
|     // Convert from utf16 to utf8 | ||||
|     const auto amiibo_name_utf8 = Common::UTF16ToUTF8(settings_amiibo_name.data()); | ||||
|     memcpy(amiibo_name.data(), amiibo_name_utf8.data(), amiibo_name_utf8.size()); | ||||
|  | ||||
|     return amiibo_name; | ||||
| } | ||||
|  | ||||
| void InstallInterfaces(SM::ServiceManager& service_manager, Core::System& system) { | ||||
|   | ||||
| @@ -9,6 +9,7 @@ | ||||
| #include "common/common_funcs.h" | ||||
| #include "core/hle/service/kernel_helpers.h" | ||||
| #include "core/hle/service/mii/types.h" | ||||
| #include "core/hle/service/nfp/amiibo_types.h" | ||||
| #include "core/hle/service/service.h" | ||||
|  | ||||
| namespace Kernel { | ||||
| @@ -21,71 +22,7 @@ enum class NpadIdType : u32; | ||||
| } // namespace Core::HID | ||||
|  | ||||
| namespace Service::NFP { | ||||
|  | ||||
| enum class ServiceType : u32 { | ||||
|     User, | ||||
|     Debug, | ||||
|     System, | ||||
| }; | ||||
|  | ||||
| enum class State : u32 { | ||||
|     NonInitialized, | ||||
|     Initialized, | ||||
| }; | ||||
|  | ||||
| enum class DeviceState : u32 { | ||||
|     Initialized, | ||||
|     SearchingForTag, | ||||
|     TagFound, | ||||
|     TagRemoved, | ||||
|     TagMounted, | ||||
|     Unaviable, | ||||
|     Finalized, | ||||
| }; | ||||
|  | ||||
| enum class ModelType : u32 { | ||||
|     Amiibo, | ||||
| }; | ||||
|  | ||||
| enum class MountTarget : u32 { | ||||
|     Rom, | ||||
|     Ram, | ||||
|     All, | ||||
| }; | ||||
|  | ||||
| enum class AmiiboType : u8 { | ||||
|     Figure, | ||||
|     Card, | ||||
|     Yarn, | ||||
| }; | ||||
|  | ||||
| enum class AmiiboSeries : u8 { | ||||
|     SuperSmashBros, | ||||
|     SuperMario, | ||||
|     ChibiRobo, | ||||
|     YoshiWoollyWorld, | ||||
|     Splatoon, | ||||
|     AnimalCrossing, | ||||
|     EightBitMario, | ||||
|     Skylanders, | ||||
|     Unknown8, | ||||
|     TheLegendOfZelda, | ||||
|     ShovelKnight, | ||||
|     Unknown11, | ||||
|     Kiby, | ||||
|     Pokemon, | ||||
|     MarioSportsSuperstars, | ||||
|     MonsterHunter, | ||||
|     BoxBoy, | ||||
|     Pikmin, | ||||
|     FireEmblem, | ||||
|     Metroid, | ||||
|     Others, | ||||
|     MegaMan, | ||||
|     Diablo | ||||
| }; | ||||
|  | ||||
| using TagUuid = std::array<u8, 10>; | ||||
| using AmiiboName = std::array<char, (amiibo_name_length * 4) + 1>; | ||||
|  | ||||
| struct TagInfo { | ||||
|     TagUuid uuid; | ||||
| @@ -114,21 +51,19 @@ struct ModelInfo { | ||||
|     AmiiboType amiibo_type; | ||||
|     u16 model_number; | ||||
|     AmiiboSeries series; | ||||
|     u8 fixed;                   // Must be 02 | ||||
|     INSERT_PADDING_BYTES(0x4);  // Unknown | ||||
|     INSERT_PADDING_BYTES(0x20); // Probably a SHA256-(HMAC?) hash | ||||
|     INSERT_PADDING_BYTES(0x14); // SHA256-HMAC | ||||
|     u8 constant_value;          // Must be 02 | ||||
|     INSERT_PADDING_BYTES(0x38); // Unknown | ||||
| }; | ||||
| static_assert(sizeof(ModelInfo) == 0x40, "ModelInfo is an invalid size"); | ||||
|  | ||||
| struct RegisterInfo { | ||||
|     Service::Mii::MiiInfo mii_char_info; | ||||
|     Service::Mii::CharInfo mii_char_info; | ||||
|     u16 first_write_year; | ||||
|     u8 first_write_month; | ||||
|     u8 first_write_day; | ||||
|     std::array<u8, 11> amiibo_name; | ||||
|     u8 unknown; | ||||
|     INSERT_PADDING_BYTES(0x98); | ||||
|     AmiiboName amiibo_name; | ||||
|     u8 font_region; | ||||
|     INSERT_PADDING_BYTES(0x7A); | ||||
| }; | ||||
| static_assert(sizeof(RegisterInfo) == 0x100, "RegisterInfo is an invalid size"); | ||||
|  | ||||
| @@ -140,39 +75,9 @@ public: | ||||
|                            const char* name); | ||||
|         ~Interface() override; | ||||
|  | ||||
|         struct EncryptedAmiiboFile { | ||||
|             u16 crypto_init;             // Must be A5 XX | ||||
|             u16 write_count;             // Number of times the amiibo has been written? | ||||
|             INSERT_PADDING_BYTES(0x20);  // System crypts | ||||
|             INSERT_PADDING_BYTES(0x20);  // SHA256-(HMAC?) hash | ||||
|             ModelInfo model_info;        // This struct is bigger than documentation | ||||
|             INSERT_PADDING_BYTES(0xC);   // SHA256-HMAC | ||||
|             INSERT_PADDING_BYTES(0x114); // section 1 encrypted buffer | ||||
|             INSERT_PADDING_BYTES(0x54);  // section 2 encrypted buffer | ||||
|         }; | ||||
|         static_assert(sizeof(EncryptedAmiiboFile) == 0x1F8, "AmiiboFile is an invalid size"); | ||||
|  | ||||
|         struct NTAG215Password { | ||||
|             u32 PWD;  // Password to allow write access | ||||
|             u16 PACK; // Password acknowledge reply | ||||
|             u16 RFUI; // Reserved for future use | ||||
|         }; | ||||
|         static_assert(sizeof(NTAG215Password) == 0x8, "NTAG215Password is an invalid size"); | ||||
|  | ||||
|         struct NTAG215File { | ||||
|             TagUuid uuid;                    // Unique serial number | ||||
|             u16 lock_bytes;                  // Set defined pages as read only | ||||
|             u32 compability_container;       // Defines available memory | ||||
|             EncryptedAmiiboFile user_memory; // Writable data | ||||
|             u32 dynamic_lock;                // Dynamic lock | ||||
|             u32 CFG0;                        // Defines memory protected by password | ||||
|             u32 CFG1;                        // Defines number of verification attempts | ||||
|             NTAG215Password password;        // Password data | ||||
|         }; | ||||
|         static_assert(sizeof(NTAG215File) == 0x21C, "NTAG215File is an invalid size"); | ||||
|  | ||||
|         void CreateUserInterface(Kernel::HLERequestContext& ctx); | ||||
|         bool LoadAmiibo(const std::vector<u8>& buffer); | ||||
|         bool LoadAmiibo(const std::string& filename); | ||||
|         bool LoadAmiiboFile(const std::string& filename); | ||||
|         void CloseAmiibo(); | ||||
|  | ||||
|         void Initialize(); | ||||
| @@ -182,6 +87,7 @@ public: | ||||
|         Result StopDetection(); | ||||
|         Result Mount(); | ||||
|         Result Unmount(); | ||||
|         Result Flush(); | ||||
|  | ||||
|         Result GetTagInfo(TagInfo& tag_info) const; | ||||
|         Result GetCommonInfo(CommonInfo& common_info) const; | ||||
| @@ -189,9 +95,10 @@ public: | ||||
|         Result GetRegisterInfo(RegisterInfo& register_info) const; | ||||
|  | ||||
|         Result OpenApplicationArea(u32 access_id); | ||||
|         Result GetApplicationArea(std::vector<u8>& data) const; | ||||
|         Result GetApplicationArea(ApplicationArea& data) const; | ||||
|         Result SetApplicationArea(const std::vector<u8>& data); | ||||
|         Result CreateApplicationArea(u32 access_id, const std::vector<u8>& data); | ||||
|         Result RecreateApplicationArea(u32 access_id, const std::vector<u8>& data); | ||||
|  | ||||
|         u64 GetHandle() const; | ||||
|         DeviceState GetCurrentState() const; | ||||
| @@ -204,27 +111,21 @@ public: | ||||
|         std::shared_ptr<Module> module; | ||||
|  | ||||
|     private: | ||||
|         /// Validates that the amiibo file is not corrupted | ||||
|         bool IsAmiiboValid() const; | ||||
|  | ||||
|         bool AmiiboApplicationDataExist(u32 access_id) const; | ||||
|         std::vector<u8> LoadAmiiboApplicationData(u32 access_id) const; | ||||
|         void SaveAmiiboApplicationData(u32 access_id, const std::vector<u8>& data) const; | ||||
|  | ||||
|         /// return password needed to allow write access to protected memory | ||||
|         u32 GetTagPassword(const TagUuid& uuid) const; | ||||
|         AmiiboName GetAmiiboName(const AmiiboSettings& settings) const; | ||||
|  | ||||
|         const Core::HID::NpadIdType npad_id; | ||||
|  | ||||
|         DeviceState device_state{DeviceState::Unaviable}; | ||||
|         KernelHelpers::ServiceContext service_context; | ||||
|         bool is_data_decoded{}; | ||||
|         bool is_application_area_initialized{}; | ||||
|         s32 protocol; | ||||
|         std::string file_path{}; | ||||
|         Kernel::KEvent* activate_event; | ||||
|         Kernel::KEvent* deactivate_event; | ||||
|         DeviceState device_state{DeviceState::Unaviable}; | ||||
|         KernelHelpers::ServiceContext service_context; | ||||
|  | ||||
|         NTAG215File tag_data{}; | ||||
|         s32 protocol; | ||||
|         bool is_application_area_initialized{}; | ||||
|         u32 application_area_id; | ||||
|         std::vector<u8> application_area_data; | ||||
|         EncryptedNTAG215File encrypted_tag_data{}; | ||||
|     }; | ||||
| }; | ||||
|  | ||||
| @@ -243,6 +144,7 @@ private: | ||||
|     void OpenApplicationArea(Kernel::HLERequestContext& ctx); | ||||
|     void GetApplicationArea(Kernel::HLERequestContext& ctx); | ||||
|     void SetApplicationArea(Kernel::HLERequestContext& ctx); | ||||
|     void Flush(Kernel::HLERequestContext& ctx); | ||||
|     void CreateApplicationArea(Kernel::HLERequestContext& ctx); | ||||
|     void GetTagInfo(Kernel::HLERequestContext& ctx); | ||||
|     void GetRegisterInfo(Kernel::HLERequestContext& ctx); | ||||
| @@ -255,6 +157,7 @@ private: | ||||
|     void GetNpadId(Kernel::HLERequestContext& ctx); | ||||
|     void GetApplicationAreaSize(Kernel::HLERequestContext& ctx); | ||||
|     void AttachAvailabilityChangeEvent(Kernel::HLERequestContext& ctx); | ||||
|     void RecreateApplicationArea(Kernel::HLERequestContext& ctx); | ||||
|  | ||||
|     KernelHelpers::ServiceContext service_context; | ||||
|  | ||||
|   | ||||
| @@ -3259,26 +3259,7 @@ void GMainWindow::LoadAmiibo(const QString& filename) { | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     QFile nfc_file{filename}; | ||||
|     if (!nfc_file.open(QIODevice::ReadOnly)) { | ||||
|         QMessageBox::warning(this, tr("Error opening Amiibo data file"), | ||||
|                              tr("Unable to open Amiibo file \"%1\" for reading.").arg(filename)); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     const u64 nfc_file_size = nfc_file.size(); | ||||
|     std::vector<u8> buffer(nfc_file_size); | ||||
|     const u64 read_size = nfc_file.read(reinterpret_cast<char*>(buffer.data()), nfc_file_size); | ||||
|     if (nfc_file_size != read_size) { | ||||
|         QMessageBox::warning(this, tr("Error reading Amiibo data file"), | ||||
|                              tr("Unable to fully read Amiibo data. Expected to read %1 bytes, but " | ||||
|                                 "was only able to read %2 bytes.") | ||||
|                                  .arg(nfc_file_size) | ||||
|                                  .arg(read_size)); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (!nfc->LoadAmiibo(buffer)) { | ||||
|     if (!nfc->LoadAmiibo(filename.toStdString())) { | ||||
|         QMessageBox::warning(this, tr("Error loading Amiibo data"), | ||||
|                              tr("Unable to load Amiibo data.")); | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user