From 3b8c44fd0e986d81cd0d4f2c0a2107ca9e904f3b Mon Sep 17 00:00:00 2001 From: JosJuice Date: Tue, 14 Apr 2020 11:40:32 +0200 Subject: [PATCH] WIA: Decrypt Wii data when writing --- Source/Core/DiscIO/VolumeWii.cpp | 25 +-- Source/Core/DiscIO/VolumeWii.h | 2 + Source/Core/DiscIO/WIABlob.cpp | 339 ++++++++++++++++++++++++++++--- Source/Core/DiscIO/WIABlob.h | 47 +++-- 4 files changed, 350 insertions(+), 63 deletions(-) diff --git a/Source/Core/DiscIO/VolumeWii.cpp b/Source/Core/DiscIO/VolumeWii.cpp index 6df1b56817..227cedb5cf 100644 --- a/Source/Core/DiscIO/VolumeWii.cpp +++ b/Source/Core/DiscIO/VolumeWii.cpp @@ -201,18 +201,9 @@ bool VolumeWii::Read(u64 offset, u64 length, u8* buffer, const Partition& partit if (!m_reader->Read(block_offset_on_disc, BLOCK_TOTAL_SIZE, read_buffer.data())) return false; - // Decrypt the block's data. - // 0x3D0 - 0x3DF in read_buffer will be overwritten, - // but that won't affect anything, because we won't - // use the content of read_buffer anymore after this - mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, BLOCK_DATA_SIZE, &read_buffer[0x3D0], - &read_buffer[BLOCK_HEADER_SIZE], m_last_decrypted_block_data); + // Decrypt the block's data + DecryptBlockData(read_buffer.data(), m_last_decrypted_block_data, aes_context); m_last_decrypted_block = block_offset_on_disc; - - // The only thing we currently use from the 0x000 - 0x3FF part - // of the block is the IV (at 0x3D0), but it also contains SHA-1 - // hashes that IOS uses to check that discs aren't tampered with. - // http://wiibrew.org/wiki/Wii_Disc#Encrypted } // Copy the decrypted data @@ -487,9 +478,7 @@ bool VolumeWii::CheckBlockIntegrity(u64 block_index, const std::vector& encr encrypted_data.data(), reinterpret_cast(&hashes)); u8 cluster_data[BLOCK_DATA_SIZE]; - std::memcpy(iv, encrypted_data.data() + 0x3D0, 16); - mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, sizeof(cluster_data), iv, - encrypted_data.data() + sizeof(HashBlock), cluster_data); + DecryptBlockData(encrypted_data.data(), cluster_data, aes_context); for (u32 hash_index = 0; hash_index < 31; ++hash_index) { @@ -671,4 +660,12 @@ bool VolumeWii::EncryptGroup( return true; } +void VolumeWii::DecryptBlockData(const u8* in, u8* out, mbedtls_aes_context* aes_context) +{ + std::array iv; + std::copy(&in[0x3d0], &in[0x3e0], iv.data()); + mbedtls_aes_crypt_cbc(aes_context, MBEDTLS_AES_DECRYPT, BLOCK_DATA_SIZE, iv.data(), + &in[BLOCK_HEADER_SIZE], out); +} + } // namespace DiscIO diff --git a/Source/Core/DiscIO/VolumeWii.h b/Source/Core/DiscIO/VolumeWii.h index 2722cc995a..b0fdf713cb 100644 --- a/Source/Core/DiscIO/VolumeWii.h +++ b/Source/Core/DiscIO/VolumeWii.h @@ -104,6 +104,8 @@ public: const std::function& hash_exception_callback = {}); + static void DecryptBlockData(const u8* in, u8* out, mbedtls_aes_context* aes_context); + protected: u32 GetOffsetShift() const override { return 2; } diff --git a/Source/Core/DiscIO/WIABlob.cpp b/Source/Core/DiscIO/WIABlob.cpp index 4a523a5c25..af11451c61 100644 --- a/Source/Core/DiscIO/WIABlob.cpp +++ b/Source/Core/DiscIO/WIABlob.cpp @@ -6,10 +6,12 @@ #include #include +#include #include #include #include #include +#include #include #include @@ -27,6 +29,8 @@ #include "Common/Swap.h" #include "DiscIO/Blob.h" +#include "DiscIO/DiscExtractor.h" +#include "DiscIO/Volume.h" #include "DiscIO/VolumeWii.h" #include "DiscIO/WiiEncryptionCache.h" @@ -902,7 +906,161 @@ bool WIAFileReader::PadTo4(File::IOFile* file, u64* bytes_written) return file->WriteBytes(&ZEROES, bytes_to_write); } +void WIAFileReader::AddRawDataEntry(u64 offset, u64 size, int chunk_size, u32* total_groups, + std::vector* raw_data_entries, + std::vector* data_entries) +{ + constexpr size_t SKIP_SIZE = sizeof(WIAHeader2::disc_header); + const u64 skip = offset < SKIP_SIZE ? std::min(SKIP_SIZE - offset, size) : 0; + + offset += skip; + size -= skip; + + if (size == 0) + return; + + const u32 group_index = *total_groups; + const u32 groups = static_cast(Common::AlignUp(size, chunk_size) / chunk_size); + *total_groups += groups; + + data_entries->emplace_back(raw_data_entries->size()); + raw_data_entries->emplace_back(RawDataEntry{Common::swap64(offset), Common::swap64(size), + Common::swap32(group_index), Common::swap32(groups)}); +} + +WIAFileReader::PartitionDataEntry WIAFileReader::CreatePartitionDataEntry( + u64 offset, u64 size, u32 index, int chunk_size, u32* total_groups, + const std::vector& partition_entries, std::vector* data_entries) +{ + const u32 group_index = *total_groups; + const u64 rounded_size = Common::AlignDown(size, VolumeWii::BLOCK_TOTAL_SIZE); + const u32 groups = static_cast(Common::AlignUp(rounded_size, chunk_size) / chunk_size); + *total_groups += groups; + + data_entries->emplace_back(partition_entries.size(), index); + return PartitionDataEntry{Common::swap32(offset / VolumeWii::BLOCK_TOTAL_SIZE), + Common::swap32(size / VolumeWii::BLOCK_TOTAL_SIZE), + Common::swap32(group_index), Common::swap32(groups)}; +} + +WIAFileReader::ConversionResult WIAFileReader::SetUpDataEntriesForWriting( + const VolumeDisc* volume, int chunk_size, u64 iso_size, u32* total_groups, + std::vector* partition_entries, std::vector* raw_data_entries, + std::vector* data_entries) +{ + std::vector partitions; + if (volume && volume->IsEncryptedAndHashed()) + partitions = volume->GetPartitions(); + + std::sort(partitions.begin(), partitions.end(), + [](const Partition& a, const Partition& b) { return a.offset < b.offset; }); + + *total_groups = 0; + + u64 last_partition_end_offset = 0; + + const auto add_raw_data_entry = [&](u64 offset, u64 size) { + return AddRawDataEntry(offset, size, chunk_size, total_groups, raw_data_entries, data_entries); + }; + + const auto create_partition_data_entry = [&](u64 offset, u64 size, u32 index) { + return CreatePartitionDataEntry(offset, size, index, chunk_size, total_groups, + *partition_entries, data_entries); + }; + + for (const Partition& partition : partitions) + { + // If a partition is odd in some way that prevents us from encoding it as a partition, + // we encode it as raw data instead by skipping the current loop iteration. + // Partitions can always be encoded as raw data, but it is less space efficient. + + if (partition.offset < last_partition_end_offset) + { + WARN_LOG(DISCIO, "Overlapping partitions at %" PRIx64, partition.offset); + continue; + } + + if (volume->ReadSwapped(partition.offset, PARTITION_NONE) != u32(0x10001)) + { + // This looks more like garbage data than an actual partition. + // The values of data_offset and data_size will very likely also be garbage. + // Some WBFS writing programs scrub the SSBB Masterpiece partitions without + // removing them from the partition table, causing this problem. + WARN_LOG(DISCIO, "Invalid partition at %" PRIx64, partition.offset); + continue; + } + + std::optional data_offset = + volume->ReadSwappedAndShifted(partition.offset + 0x2b8, PARTITION_NONE); + std::optional data_size = + volume->ReadSwappedAndShifted(partition.offset + 0x2bc, PARTITION_NONE); + + if (!data_offset || !data_size) + return ConversionResult::ReadFailed; + + const u64 data_start = partition.offset + *data_offset; + const u64 data_end = data_start + *data_size; + + if (data_start % VolumeWii::BLOCK_TOTAL_SIZE != 0) + { + WARN_LOG(DISCIO, "Misaligned partition at %" PRIx64, partition.offset); + continue; + } + + if (*data_size < VolumeWii::BLOCK_TOTAL_SIZE) + { + WARN_LOG(DISCIO, "Very small partition at %" PRIx64, partition.offset); + continue; + } + + if (data_end > iso_size) + { + WARN_LOG(DISCIO, "Too large partition at %" PRIx64, partition.offset); + *data_size = iso_size - *data_offset - partition.offset; + } + + const std::optional fst_offset = GetFSTOffset(*volume, partition); + const std::optional fst_size = GetFSTSize(*volume, partition); + + if (!fst_offset || !fst_size) + return ConversionResult::ReadFailed; + + const IOS::ES::TicketReader& ticket = volume->GetTicket(partition); + if (!ticket.IsValid()) + return ConversionResult::ReadFailed; + + add_raw_data_entry(last_partition_end_offset, partition.offset - last_partition_end_offset); + + add_raw_data_entry(partition.offset, *data_offset); + + const u64 fst_end = volume->PartitionOffsetToRawOffset(*fst_offset + *fst_size, partition); + const u64 split_point = std::min( + data_end, Common::AlignUp(fst_end - data_start, VolumeWii::GROUP_TOTAL_SIZE) + data_start); + + PartitionEntry partition_entry; + partition_entry.partition_key = ticket.GetTitleKey(); + partition_entry.data_entries[0] = + create_partition_data_entry(data_start, split_point - data_start, 0); + partition_entry.data_entries[1] = + create_partition_data_entry(split_point, data_end - split_point, 1); + + // Note: We can't simply set last_partition_end_offset to data_end, + // because construct_partition_data_entry may have rounded it + last_partition_end_offset = + (Common::swap32(partition_entry.data_entries[1].first_sector) + + Common::swap32(partition_entry.data_entries[1].number_of_sectors)) * + VolumeWii::BLOCK_TOTAL_SIZE; + + partition_entries->emplace_back(std::move(partition_entry)); + } + + add_raw_data_entry(last_partition_end_offset, iso_size - last_partition_end_offset); + + return ConversionResult::Success; +} + WIAFileReader::ConversionResult WIAFileReader::ConvertToWIA(BlobReader* infile, + const VolumeDisc* infile_volume, File::IOFile* outfile, int chunk_size, CompressCB callback, void* arg) { @@ -913,6 +1071,7 @@ WIAFileReader::ConversionResult WIAFileReader::ConvertToWIA(BlobReader* infile, u64 bytes_read = 0; u64 bytes_written = 0; + size_t groups_written = 0; // These two headers will be filled in with proper values at the very end WIAHeader1 header_1; @@ -923,18 +1082,11 @@ WIAFileReader::ConversionResult WIAFileReader::ConvertToWIA(BlobReader* infile, if (!PadTo4(outfile, &bytes_written)) return ConversionResult::WriteFailed; - std::vector group_entries; - group_entries.resize(Common::AlignUp(iso_size, chunk_size) / chunk_size); - - std::vector raw_data_entries; - raw_data_entries.emplace_back( - RawDataEntry{Common::swap64(header_2.disc_header.size()), - Common::swap64(iso_size - header_2.disc_header.size()), 0, - Common::swap32(static_cast(group_entries.size()))}); - std::vector partition_entries; + std::vector raw_data_entries; + std::vector group_entries; - const auto run_callback = [&](size_t groups_written) { + const auto run_callback = [&] { int ratio = 0; if (bytes_read != 0) ratio = static_cast(100 * bytes_written / bytes_read); @@ -942,42 +1094,152 @@ WIAFileReader::ConversionResult WIAFileReader::ConvertToWIA(BlobReader* infile, const std::string temp = StringFromFormat(Common::GetStringT("%i of %i blocks. Compression ratio %i%%").c_str(), groups_written, group_entries.size(), ratio); - return callback(temp, static_cast(groups_written) / group_entries.size(), arg); + + float completion = 0.0f; + if (group_entries.size() != 0) + completion = static_cast(groups_written) / group_entries.size(); + + return callback(temp, completion, arg); }; + if (!run_callback()) + return ConversionResult::Canceled; + + u32 total_groups; + std::vector data_entries; + + const ConversionResult set_up_data_entries_result = + SetUpDataEntriesForWriting(infile_volume, chunk_size, iso_size, &total_groups, + &partition_entries, &raw_data_entries, &data_entries); + if (set_up_data_entries_result != ConversionResult::Success) + return set_up_data_entries_result; + + group_entries.resize(total_groups); + if (!infile->Read(0, header_2.disc_header.size(), header_2.disc_header.data())) return ConversionResult::ReadFailed; // We intentially do not increment bytes_read here, since these bytes will be read again - if (!run_callback(0)) - return ConversionResult::Canceled; - std::vector buffer(chunk_size); - for (size_t i = 0; i < group_entries.size(); ++i) + std::vector decryption_buffer(VolumeWii::BLOCK_DATA_SIZE); + for (const DataEntry& data_entry : data_entries) { - const u64 bytes_to_read = std::min(chunk_size, iso_size - bytes_read); + if (data_entry.is_partition) + { + const PartitionEntry& partition_entry = partition_entries[data_entry.index]; + const PartitionDataEntry& partition_data_entry = + partition_entry.data_entries[data_entry.partition_data_index]; - if (bytes_written >> 2 > std::numeric_limits::max()) - return ConversionResult::InternalError; + const u32 first_group = Common::swap32(partition_data_entry.group_index); + const u32 last_group = first_group + Common::swap32(partition_data_entry.number_of_groups); - ASSERT((bytes_written & 3) == 0); - group_entries[i] = GroupEntry{Common::swap32(static_cast(bytes_written >> 2)), - Common::swap32(static_cast(bytes_to_read))}; + const u64 data_offset = + Common::swap32(partition_data_entry.first_sector) * VolumeWii::BLOCK_TOTAL_SIZE; + const u64 data_size = + Common::swap32(partition_data_entry.number_of_sectors) * VolumeWii::BLOCK_TOTAL_SIZE; - if (!infile->Read(bytes_read, bytes_to_read, buffer.data())) - return ConversionResult::ReadFailed; - if (!outfile->WriteArray(buffer.data(), bytes_to_read)) - return ConversionResult::WriteFailed; + ASSERT(groups_written == first_group); + ASSERT(bytes_read == data_offset); - bytes_read += bytes_to_read; - bytes_written += bytes_to_read; - if (!PadTo4(outfile, &bytes_written)) - return ConversionResult::WriteFailed; + mbedtls_aes_context aes_context; + mbedtls_aes_setkey_dec(&aes_context, partition_entry.partition_key.data(), 128); - if (!run_callback(i)) - return ConversionResult::Canceled; + for (u32 i = first_group; i < last_group; ++i) + { + const u64 bytes_to_read = std::min(chunk_size, data_offset + data_size - bytes_read); + + ASSERT(bytes_to_read % VolumeWii::BLOCK_TOTAL_SIZE == 0); + const u64 bytes_to_write = + bytes_to_read / VolumeWii::BLOCK_TOTAL_SIZE * VolumeWii::BLOCK_DATA_SIZE; + + if (!infile->Read(bytes_read, bytes_to_read, buffer.data())) + return ConversionResult::ReadFailed; + + const u64 exception_lists = Common::AlignUp(bytes_to_read, VolumeWii::GROUP_TOTAL_SIZE) / + VolumeWii::GROUP_TOTAL_SIZE; + + const u64 exceptions_size = Common::AlignUp(exception_lists * sizeof(u16), 4); + const u64 total_size = exceptions_size + bytes_to_write; + ASSERT((bytes_written & 3) == 0); + group_entries[i].data_offset = Common::swap32(static_cast(bytes_written >> 2)); + group_entries[i].data_size = Common::swap32(static_cast(total_size)); + + for (u64 j = 0; j < exception_lists; ++j) + { + const u16 exceptions = 0; + if (!outfile->WriteArray(&exceptions, 1)) + return ConversionResult::WriteFailed; + bytes_written += sizeof(u16); + } + + if (!PadTo4(outfile, &bytes_written)) + return ConversionResult::WriteFailed; + + for (u64 j = 0; j < bytes_to_read; j += VolumeWii::BLOCK_TOTAL_SIZE) + { + VolumeWii::DecryptBlockData(buffer.data() + j, decryption_buffer.data(), &aes_context); + if (!outfile->WriteArray(decryption_buffer.data(), VolumeWii::BLOCK_DATA_SIZE)) + return ConversionResult::WriteFailed; + } + + bytes_read += bytes_to_read; + bytes_written += bytes_to_write; + ++groups_written; + if (!PadTo4(outfile, &bytes_written)) + return ConversionResult::WriteFailed; + + if (!run_callback()) + return ConversionResult::Canceled; + } + } + else + { + const RawDataEntry& raw_data_entry = raw_data_entries[data_entry.index]; + + const u32 first_group = Common::swap32(raw_data_entry.group_index); + const u32 last_group = first_group + Common::swap32(raw_data_entry.number_of_groups); + + u64 data_offset = Common::swap64(raw_data_entry.data_offset); + u64 data_size = Common::swap64(raw_data_entry.data_size); + + const u64 skipped_data = data_offset % VolumeWii::BLOCK_TOTAL_SIZE; + data_offset -= skipped_data; + data_size += skipped_data; + + ASSERT(groups_written == first_group); + ASSERT(bytes_read == data_offset); + + for (u32 i = first_group; i < last_group; ++i) + { + const u64 bytes_to_read = std::min(chunk_size, data_offset + data_size - bytes_read); + + if (bytes_written >> 2 > std::numeric_limits::max()) + return ConversionResult::InternalError; + + ASSERT((bytes_written & 3) == 0); + group_entries[i].data_offset = Common::swap32(static_cast(bytes_written >> 2)); + group_entries[i].data_size = Common::swap32(static_cast(bytes_to_read)); + + if (!infile->Read(bytes_read, bytes_to_read, buffer.data())) + return ConversionResult::ReadFailed; + if (!outfile->WriteArray(buffer.data(), bytes_to_read)) + return ConversionResult::WriteFailed; + + bytes_read += bytes_to_read; + bytes_written += bytes_to_read; + ++groups_written; + if (!PadTo4(outfile, &bytes_written)) + return ConversionResult::WriteFailed; + + if (!run_callback()) + return ConversionResult::Canceled; + } + } } + ASSERT(groups_written == total_groups); + ASSERT(bytes_read == iso_size); + const u64 partition_entries_offset = bytes_written; const u64 partition_entries_size = partition_entries.size() * sizeof(PartitionEntry); if (!outfile->WriteArray(partition_entries.data(), partition_entries.size())) @@ -1002,7 +1264,16 @@ WIAFileReader::ConversionResult WIAFileReader::ConvertToWIA(BlobReader* infile, if (!PadTo4(outfile, &bytes_written)) return ConversionResult::WriteFailed; - header_2.disc_type = 0; // TODO + u32 disc_type = 0; + if (infile_volume) + { + if (infile_volume->GetVolumeType() == Platform::GameCubeDisc) + disc_type = 1; + else if (infile_volume->GetVolumeType() == Platform::WiiDisc) + disc_type = 2; + } + + header_2.disc_type = Common::swap32(disc_type); header_2.compression_type = Common::swap32(static_cast(CompressionType::None)); header_2.compression_level = 0; header_2.chunk_size = Common::swap32(static_cast(chunk_size)); @@ -1059,8 +1330,10 @@ bool ConvertToWIA(BlobReader* infile, const std::string& infile_path, return false; } + std::unique_ptr infile_volume = CreateDisc(infile_path); + WIAFileReader::ConversionResult result = - WIAFileReader::ConvertToWIA(infile, &outfile, chunk_size, callback, arg); + WIAFileReader::ConvertToWIA(infile, infile_volume.get(), &outfile, chunk_size, callback, arg); if (result == WIAFileReader::ConversionResult::ReadFailed) PanicAlertT("Failed to read from the input file \"%s\".", infile_path.c_str()); diff --git a/Source/Core/DiscIO/WIABlob.h b/Source/Core/DiscIO/WIABlob.h index d42aa92fef..c1f46cc5ea 100644 --- a/Source/Core/DiscIO/WIABlob.h +++ b/Source/Core/DiscIO/WIABlob.h @@ -22,6 +22,8 @@ namespace DiscIO { +class VolumeDisc; + constexpr u32 WIA_MAGIC = 0x01414957; // "WIA\x1" (byteswapped to little endian) class WIAFileReader : public BlobReader @@ -53,8 +55,9 @@ public: InternalError, }; - static ConversionResult ConvertToWIA(BlobReader* infile, File::IOFile* outfile, int chunk_size, - CompressCB callback, void* arg); + static ConversionResult ConvertToWIA(BlobReader* infile, const VolumeDisc* infile_volume, + File::IOFile* outfile, int chunk_size, CompressCB callback, + void* arg); private: using SHA1 = std::array; @@ -159,6 +162,20 @@ private: static_assert(sizeof(PurgeSegment) == 0x08, "Wrong size for WIA purge segment"); #pragma pack(pop) + struct DataEntry + { + u32 index; + bool is_partition; + u8 partition_data_index; + + DataEntry(size_t index_) : index(static_cast(index_)), is_partition(false) {} + DataEntry(size_t index_, size_t partition_data_index_) + : index(static_cast(index_)), is_partition(true), + partition_data_index(static_cast(partition_data_index_)) + { + } + }; + struct DecompressionBuffer { std::vector data; @@ -288,6 +305,18 @@ private: static std::string VersionToString(u32 version); static bool PadTo4(File::IOFile* file, u64* bytes_written); + static void AddRawDataEntry(u64 offset, u64 size, int chunk_size, u32* total_groups, + std::vector* raw_data_entries, + std::vector* data_entries); + static PartitionDataEntry + CreatePartitionDataEntry(u64 offset, u64 size, u32 index, int chunk_size, u32* total_groups, + const std::vector& partition_entries, + std::vector* data_entries); + static ConversionResult SetUpDataEntriesForWriting(const VolumeDisc* volume, int chunk_size, + u64 iso_size, u32* total_groups, + std::vector* partition_entries, + std::vector* raw_data_entries, + std::vector* data_entries); bool m_valid; CompressionType m_compression_type; @@ -303,20 +332,6 @@ private: std::vector m_raw_data_entries; std::vector m_group_entries; - struct DataEntry - { - u32 index; - bool is_partition; - u8 partition_data_index; - - DataEntry(size_t index_) : index(static_cast(index_)), is_partition(false) {} - DataEntry(size_t index_, size_t partition_data_index_) - : index(static_cast(index_)), is_partition(true), - partition_data_index(static_cast(partition_data_index_)) - { - } - }; - std::map m_data_entries; static constexpr u32 WIA_VERSION = 0x01000000;