Merge pull request #849 from DarkLordZach/xci

XCI and Encrypted NCA Support
This commit is contained in:
bunnei 2018-08-04 14:33:11 -04:00 committed by GitHub
commit 2b06301dbf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1405 additions and 80 deletions

View file

@ -0,0 +1,149 @@
// Copyright 2018 yuzu emulator team
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <array>
#include <string>
#include <core/loader/loader.h>
#include "core/file_sys/card_image.h"
#include "core/file_sys/partition_filesystem.h"
#include "core/file_sys/vfs_offset.h"
namespace FileSys {
XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
if (file->ReadObject(&header) != sizeof(GamecardHeader)) {
status = Loader::ResultStatus::ErrorInvalidFormat;
return;
}
if (header.magic != Common::MakeMagic('H', 'E', 'A', 'D')) {
status = Loader::ResultStatus::ErrorInvalidFormat;
return;
}
PartitionFilesystem main_hfs(
std::make_shared<OffsetVfsFile>(file, header.hfs_size, header.hfs_offset));
if (main_hfs.GetStatus() != Loader::ResultStatus::Success) {
status = main_hfs.GetStatus();
return;
}
static constexpr std::array<const char*, 0x4> partition_names = {"update", "normal", "secure",
"logo"};
for (XCIPartition partition :
{XCIPartition::Update, XCIPartition::Normal, XCIPartition::Secure, XCIPartition::Logo}) {
auto raw = main_hfs.GetFile(partition_names[static_cast<size_t>(partition)]);
if (raw != nullptr)
partitions[static_cast<size_t>(partition)] = std::make_shared<PartitionFilesystem>(raw);
}
auto result = AddNCAFromPartition(XCIPartition::Secure);
if (result != Loader::ResultStatus::Success) {
status = result;
return;
}
result = AddNCAFromPartition(XCIPartition::Update);
if (result != Loader::ResultStatus::Success) {
status = result;
return;
}
result = AddNCAFromPartition(XCIPartition::Normal);
if (result != Loader::ResultStatus::Success) {
status = result;
return;
}
if (GetFormatVersion() >= 0x2) {
result = AddNCAFromPartition(XCIPartition::Logo);
if (result != Loader::ResultStatus::Success) {
status = result;
return;
}
}
status = Loader::ResultStatus::Success;
}
Loader::ResultStatus XCI::GetStatus() const {
return status;
}
VirtualDir XCI::GetPartition(XCIPartition partition) const {
return partitions[static_cast<size_t>(partition)];
}
VirtualDir XCI::GetSecurePartition() const {
return GetPartition(XCIPartition::Secure);
}
VirtualDir XCI::GetNormalPartition() const {
return GetPartition(XCIPartition::Normal);
}
VirtualDir XCI::GetUpdatePartition() const {
return GetPartition(XCIPartition::Update);
}
VirtualDir XCI::GetLogoPartition() const {
return GetPartition(XCIPartition::Logo);
}
std::shared_ptr<NCA> XCI::GetNCAByType(NCAContentType type) const {
const auto iter =
std::find_if(ncas.begin(), ncas.end(),
[type](const std::shared_ptr<NCA>& nca) { return nca->GetType() == type; });
return iter == ncas.end() ? nullptr : *iter;
}
VirtualFile XCI::GetNCAFileByType(NCAContentType type) const {
auto nca = GetNCAByType(type);
if (nca != nullptr)
return nca->GetBaseFile();
return nullptr;
}
std::vector<std::shared_ptr<VfsFile>> XCI::GetFiles() const {
return {};
}
std::vector<std::shared_ptr<VfsDirectory>> XCI::GetSubdirectories() const {
return std::vector<std::shared_ptr<VfsDirectory>>();
}
std::string XCI::GetName() const {
return file->GetName();
}
std::shared_ptr<VfsDirectory> XCI::GetParentDirectory() const {
return file->GetContainingDirectory();
}
bool XCI::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
return false;
}
Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
if (partitions[static_cast<size_t>(part)] == nullptr) {
return Loader::ResultStatus::ErrorInvalidFormat;
}
for (const VirtualFile& file : partitions[static_cast<size_t>(part)]->GetFiles()) {
if (file->GetExtension() != "nca")
continue;
auto nca = std::make_shared<NCA>(file);
if (nca->GetStatus() == Loader::ResultStatus::Success)
ncas.push_back(std::move(nca));
}
return Loader::ResultStatus::Success;
}
u8 XCI::GetFormatVersion() const {
return GetLogoPartition() == nullptr ? 0x1 : 0x2;
}
} // namespace FileSys

View file

@ -0,0 +1,96 @@
// Copyright 2018 yuzu emulator team
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#pragma once
#include <array>
#include <vector>
#include "common/common_types.h"
#include "common/swap.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/vfs.h"
#include "core/loader/loader.h"
namespace FileSys {
enum class GamecardSize : u8 {
S_1GB = 0xFA,
S_2GB = 0xF8,
S_4GB = 0xF0,
S_8GB = 0xE0,
S_16GB = 0xE1,
S_32GB = 0xE2,
};
struct GamecardInfo {
std::array<u8, 0x70> data;
};
static_assert(sizeof(GamecardInfo) == 0x70, "GamecardInfo has incorrect size.");
struct GamecardHeader {
std::array<u8, 0x100> signature;
u32_le magic;
u32_le secure_area_start;
u32_le backup_area_start;
u8 kek_index;
GamecardSize size;
u8 header_version;
u8 flags;
u64_le package_id;
u64_le valid_data_end;
u128 info_iv;
u64_le hfs_offset;
u64_le hfs_size;
std::array<u8, 0x20> hfs_header_hash;
std::array<u8, 0x20> initial_data_hash;
u32_le secure_mode_flag;
u32_le title_key_flag;
u32_le key_flag;
u32_le normal_area_end;
GamecardInfo info;
};
static_assert(sizeof(GamecardHeader) == 0x200, "GamecardHeader has incorrect size.");
enum class XCIPartition : u8 { Update, Normal, Secure, Logo };
class XCI : public ReadOnlyVfsDirectory {
public:
explicit XCI(VirtualFile file);
Loader::ResultStatus GetStatus() const;
u8 GetFormatVersion() const;
VirtualDir GetPartition(XCIPartition partition) const;
VirtualDir GetSecurePartition() const;
VirtualDir GetNormalPartition() const;
VirtualDir GetUpdatePartition() const;
VirtualDir GetLogoPartition() const;
std::shared_ptr<NCA> GetNCAByType(NCAContentType type) const;
VirtualFile GetNCAFileByType(NCAContentType type) const;
std::vector<std::shared_ptr<VfsFile>> GetFiles() const override;
std::vector<std::shared_ptr<VfsDirectory>> GetSubdirectories() const override;
std::string GetName() const override;
std::shared_ptr<VfsDirectory> GetParentDirectory() const override;
protected:
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
private:
Loader::ResultStatus AddNCAFromPartition(XCIPartition part);
VirtualFile file;
GamecardHeader header{};
Loader::ResultStatus status;
std::vector<VirtualDir> partitions;
std::vector<std::shared_ptr<NCA>> ncas;
};
} // namespace FileSys

View file

@ -4,12 +4,14 @@
#include <algorithm>
#include <utility>
#include <boost/optional.hpp>
#include "common/logging/log.h"
#include "core/crypto/aes_util.h"
#include "core/crypto/ctr_encryption_layer.h"
#include "core/file_sys/content_archive.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/vfs_offset.h"
#include "core/loader/loader.h"
#include "romfs.h"
namespace FileSys {
@ -29,11 +31,19 @@ enum class NCASectionFilesystemType : u8 {
struct NCASectionHeaderBlock {
INSERT_PADDING_BYTES(3);
NCASectionFilesystemType filesystem_type;
u8 crypto_type;
NCASectionCryptoType crypto_type;
INSERT_PADDING_BYTES(3);
};
static_assert(sizeof(NCASectionHeaderBlock) == 0x8, "NCASectionHeaderBlock has incorrect size.");
struct NCASectionRaw {
NCASectionHeaderBlock header;
std::array<u8, 0x138> block_data;
std::array<u8, 0x8> section_ctr;
INSERT_PADDING_BYTES(0xB8);
};
static_assert(sizeof(NCASectionRaw) == 0x200, "NCASectionRaw has incorrect size.");
struct PFS0Superblock {
NCASectionHeaderBlock header_block;
std::array<u8, 0x20> hash;
@ -43,67 +53,170 @@ struct PFS0Superblock {
u64_le hash_table_size;
u64_le pfs0_header_offset;
u64_le pfs0_size;
INSERT_PADDING_BYTES(432);
INSERT_PADDING_BYTES(0x1B0);
};
static_assert(sizeof(PFS0Superblock) == 0x200, "PFS0Superblock has incorrect size.");
struct RomFSSuperblock {
NCASectionHeaderBlock header_block;
IVFCHeader ivfc;
INSERT_PADDING_BYTES(0x118);
};
static_assert(sizeof(RomFSSuperblock) == 0xE8, "RomFSSuperblock has incorrect size.");
static_assert(sizeof(RomFSSuperblock) == 0x200, "RomFSSuperblock has incorrect size.");
union NCASectionHeader {
NCASectionRaw raw;
PFS0Superblock pfs0;
RomFSSuperblock romfs;
};
static_assert(sizeof(NCASectionHeader) == 0x200, "NCASectionHeader has incorrect size.");
bool IsValidNCA(const NCAHeader& header) {
// TODO(DarkLordZach): Add NCA2/NCA0 support.
return header.magic == Common::MakeMagic('N', 'C', 'A', '3');
}
boost::optional<Core::Crypto::Key128> NCA::GetKeyAreaKey(NCASectionCryptoType type) const {
u8 master_key_id = header.crypto_type;
if (header.crypto_type_2 > master_key_id)
master_key_id = header.crypto_type_2;
if (master_key_id > 0)
--master_key_id;
if (!keys.HasKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index))
return boost::none;
std::vector<u8> key_area(header.key_area.begin(), header.key_area.end());
Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(
keys.GetKey(Core::Crypto::S128KeyType::KeyArea, master_key_id, header.key_index),
Core::Crypto::Mode::ECB);
cipher.Transcode(key_area.data(), key_area.size(), key_area.data(), Core::Crypto::Op::Decrypt);
Core::Crypto::Key128 out;
if (type == NCASectionCryptoType::XTS)
std::copy(key_area.begin(), key_area.begin() + 0x10, out.begin());
else if (type == NCASectionCryptoType::CTR)
std::copy(key_area.begin() + 0x20, key_area.begin() + 0x30, out.begin());
else
LOG_CRITICAL(Crypto, "Called GetKeyAreaKey on invalid NCASectionCryptoType type={:02X}",
static_cast<u8>(type));
u128 out_128{};
memcpy(out_128.data(), out.data(), 16);
LOG_DEBUG(Crypto, "called with crypto_rev={:02X}, kak_index={:02X}, key={:016X}{:016X}",
master_key_id, header.key_index, out_128[1], out_128[0]);
return out;
}
VirtualFile NCA::Decrypt(NCASectionHeader header, VirtualFile in, u64 starting_offset) const {
if (!encrypted)
return in;
switch (header.raw.header.crypto_type) {
case NCASectionCryptoType::NONE:
LOG_DEBUG(Crypto, "called with mode=NONE");
return in;
case NCASectionCryptoType::CTR:
LOG_DEBUG(Crypto, "called with mode=CTR, starting_offset={:016X}", starting_offset);
{
const auto key = GetKeyAreaKey(NCASectionCryptoType::CTR);
if (key == boost::none)
return nullptr;
auto out = std::make_shared<Core::Crypto::CTREncryptionLayer>(
std::move(in), key.value(), starting_offset);
std::vector<u8> iv(16);
for (u8 i = 0; i < 8; ++i)
iv[i] = header.raw.section_ctr[0x8 - i - 1];
out->SetIV(iv);
return std::static_pointer_cast<VfsFile>(out);
}
case NCASectionCryptoType::XTS:
// TODO(DarkLordZach): Implement XTSEncryptionLayer and title key encryption.
default:
LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
static_cast<u8>(header.raw.header.crypto_type));
return nullptr;
}
}
NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
if (sizeof(NCAHeader) != file->ReadObject(&header))
LOG_CRITICAL(Loader, "File reader errored out during header read.");
LOG_ERROR(Loader, "File reader errored out during header read.");
encrypted = false;
if (!IsValidNCA(header)) {
status = Loader::ResultStatus::ErrorInvalidFormat;
return;
NCAHeader dec_header{};
Core::Crypto::AESCipher<Core::Crypto::Key256> cipher(
keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS);
cipher.XTSTranscode(&header, sizeof(NCAHeader), &dec_header, 0, 0x200,
Core::Crypto::Op::Decrypt);
if (IsValidNCA(dec_header)) {
header = dec_header;
encrypted = true;
} else {
if (!keys.HasKey(Core::Crypto::S256KeyType::Header))
status = Loader::ResultStatus::ErrorMissingKeys;
else
status = Loader::ResultStatus::ErrorDecrypting;
return;
}
}
std::ptrdiff_t number_sections =
const std::ptrdiff_t number_sections =
std::count_if(std::begin(header.section_tables), std::end(header.section_tables),
[](NCASectionTableEntry entry) { return entry.media_offset > 0; });
std::vector<NCASectionHeader> sections(number_sections);
const auto length_sections = SECTION_HEADER_SIZE * number_sections;
if (encrypted) {
auto raw = file->ReadBytes(length_sections, SECTION_HEADER_OFFSET);
Core::Crypto::AESCipher<Core::Crypto::Key256> cipher(
keys.GetKey(Core::Crypto::S256KeyType::Header), Core::Crypto::Mode::XTS);
cipher.XTSTranscode(raw.data(), length_sections, sections.data(), 2, SECTION_HEADER_SIZE,
Core::Crypto::Op::Decrypt);
} else {
file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
}
for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
// Seek to beginning of this section.
NCASectionHeaderBlock block{};
if (sizeof(NCASectionHeaderBlock) !=
file->ReadObject(&block, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
LOG_CRITICAL(Loader, "File reader errored out during header read.");
if (block.filesystem_type == NCASectionFilesystemType::ROMFS) {
RomFSSuperblock sb{};
if (sizeof(RomFSSuperblock) !=
file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
LOG_CRITICAL(Loader, "File reader errored out during header read.");
auto section = sections[i];
if (section.raw.header.filesystem_type == NCASectionFilesystemType::ROMFS) {
const size_t romfs_offset =
header.section_tables[i].media_offset * MEDIA_OFFSET_MULTIPLIER +
sb.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
const size_t romfs_size = sb.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
files.emplace_back(std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset));
romfs = files.back();
} else if (block.filesystem_type == NCASectionFilesystemType::PFS0) {
PFS0Superblock sb{};
// Seek back to beginning of this section.
if (sizeof(PFS0Superblock) !=
file->ReadObject(&sb, SECTION_HEADER_OFFSET + i * SECTION_HEADER_SIZE))
LOG_CRITICAL(Loader, "File reader errored out during header read.");
section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].offset;
const size_t romfs_size = section.romfs.ivfc.levels[IVFC_MAX_LEVEL - 1].size;
auto dec =
Decrypt(section, std::make_shared<OffsetVfsFile>(file, romfs_size, romfs_offset),
romfs_offset);
if (dec != nullptr) {
files.push_back(std::move(dec));
romfs = files.back();
} else {
status = Loader::ResultStatus::ErrorMissingKeys;
return;
}
} else if (section.raw.header.filesystem_type == NCASectionFilesystemType::PFS0) {
u64 offset = (static_cast<u64>(header.section_tables[i].media_offset) *
MEDIA_OFFSET_MULTIPLIER) +
sb.pfs0_header_offset;
section.pfs0.pfs0_header_offset;
u64 size = MEDIA_OFFSET_MULTIPLIER * (header.section_tables[i].media_end_offset -
header.section_tables[i].media_offset);
auto npfs = std::make_shared<PartitionFilesystem>(
std::make_shared<OffsetVfsFile>(file, size, offset));
auto dec =
Decrypt(section, std::make_shared<OffsetVfsFile>(file, size, offset), offset);
if (dec != nullptr) {
auto npfs = std::make_shared<PartitionFilesystem>(std::move(dec));
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
dirs.emplace_back(npfs);
if (IsDirectoryExeFS(dirs.back()))
exefs = dirs.back();
if (npfs->GetStatus() == Loader::ResultStatus::Success) {
dirs.push_back(std::move(npfs));
if (IsDirectoryExeFS(dirs.back()))
exefs = dirs.back();
}
} else {
status = Loader::ResultStatus::ErrorMissingKeys;
return;
}
}
}
@ -153,6 +266,10 @@ VirtualDir NCA::GetExeFS() const {
return exefs;
}
VirtualFile NCA::GetBaseFile() const {
return file;
}
bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
return false;
}

View file

@ -8,14 +8,18 @@
#include <memory>
#include <string>
#include <vector>
#include <boost/optional.hpp>
#include "common/common_funcs.h"
#include "common/common_types.h"
#include "common/swap.h"
#include "core/crypto/key_manager.h"
#include "core/file_sys/partition_filesystem.h"
#include "core/loader/loader.h"
namespace FileSys {
union NCASectionHeader;
enum class NCAContentType : u8 {
Program = 0,
Meta = 1,
@ -24,6 +28,13 @@ enum class NCAContentType : u8 {
Data = 4,
};
enum class NCASectionCryptoType : u8 {
NONE = 1,
XTS = 2,
CTR = 3,
BKTR = 4,
};
struct NCASectionTableEntry {
u32_le media_offset;
u32_le media_end_offset;
@ -48,7 +59,7 @@ struct NCAHeader {
std::array<u8, 0x10> rights_id;
std::array<NCASectionTableEntry, 0x4> section_tables;
std::array<std::array<u8, 0x20>, 0x4> hash_tables;
std::array<std::array<u8, 0x10>, 0x4> key_area;
std::array<u8, 0x40> key_area;
INSERT_PADDING_BYTES(0xC0);
};
static_assert(sizeof(NCAHeader) == 0x400, "NCAHeader has incorrect size.");
@ -58,10 +69,7 @@ inline bool IsDirectoryExeFS(const std::shared_ptr<VfsDirectory>& pfs) {
return pfs->GetFile("main") != nullptr && pfs->GetFile("main.npdm") != nullptr;
}
inline bool IsValidNCA(const NCAHeader& header) {
return header.magic == Common::MakeMagic('N', 'C', 'A', '2') ||
header.magic == Common::MakeMagic('N', 'C', 'A', '3');
}
bool IsValidNCA(const NCAHeader& header);
// An implementation of VfsDirectory that represents a Nintendo Content Archive (NCA) conatiner.
// After construction, use GetStatus to determine if the file is valid and ready to be used.
@ -81,10 +89,15 @@ public:
VirtualFile GetRomFS() const;
VirtualDir GetExeFS() const;
VirtualFile GetBaseFile() const;
protected:
bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
private:
boost::optional<Core::Crypto::Key128> GetKeyAreaKey(NCASectionCryptoType type) const;
VirtualFile Decrypt(NCASectionHeader header, VirtualFile in, u64 starting_offset) const;
std::vector<VirtualDir> dirs;
std::vector<VirtualFile> files;
@ -95,6 +108,10 @@ private:
NCAHeader header{};
Loader::ResultStatus status{};
bool encrypted;
Core::Crypto::KeyManager keys;
};
} // namespace FileSys

View file

@ -285,6 +285,26 @@ bool ReadOnlyVfsDirectory::Rename(std::string_view name) {
return false;
}
bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size) {
if (file1->GetSize() != file2->GetSize())
return false;
std::vector<u8> f1_v(block_size);
std::vector<u8> f2_v(block_size);
for (size_t i = 0; i < file1->GetSize(); i += block_size) {
auto f1_vs = file1->Read(f1_v.data(), block_size, i);
auto f2_vs = file2->Read(f2_v.data(), block_size, i);
if (f1_vs != f2_vs)
return false;
auto iters = std::mismatch(f1_v.begin(), f1_v.end(), f2_v.begin(), f2_v.end());
if (iters.first != f1_v.end() && iters.second != f2_v.end())
return false;
}
return true;
}
bool VfsRawCopy(VirtualFile src, VirtualFile dest) {
if (src == nullptr || dest == nullptr)
return false;

View file

@ -245,6 +245,9 @@ struct ReadOnlyVfsDirectory : public VfsDirectory {
bool Rename(std::string_view name) override;
};
// Compare the two files, byte-for-byte, in increments specificed by block_size
bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block_size = 0x200);
// A method that copies the raw data between two different implementations of VirtualFile. If you
// are using the same implementation, it is probably better to use the Copy method in the parent
// directory of src/dest.