Просмотр исходного кода

Merge pull request #1094 from DarkLordZach/nax0

file_sys: Add support for NAX archives
Mat M 8 лет назад
Родитель
Сommit
6426b0f551

+ 6 - 0
src/core/CMakeLists.txt

@@ -20,6 +20,8 @@ add_library(core STATIC
     crypto/key_manager.h
     crypto/ctr_encryption_layer.cpp
     crypto/ctr_encryption_layer.h
+    crypto/xts_encryption_layer.cpp
+    crypto/xts_encryption_layer.h
     file_sys/bis_factory.cpp
     file_sys/bis_factory.h
     file_sys/card_image.cpp
@@ -57,6 +59,8 @@ add_library(core STATIC
     file_sys/vfs_real.h
     file_sys/vfs_vector.cpp
     file_sys/vfs_vector.h
+    file_sys/xts_archive.cpp
+    file_sys/xts_archive.h
     frontend/emu_window.cpp
     frontend/emu_window.h
     frontend/framebuffer_layout.cpp
@@ -347,6 +351,8 @@ add_library(core STATIC
     loader/linker.h
     loader/loader.cpp
     loader/loader.h
+    loader/nax.cpp
+    loader/nax.h
     loader/nca.cpp
     loader/nca.h
     loader/nro.cpp

+ 2 - 5
src/core/crypto/aes_util.cpp

@@ -99,10 +99,7 @@ void AESCipher<Key, KeySize>::Transcode(const u8* src, size_t size, u8* dest, Op
 template <typename Key, size_t KeySize>
 void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, size_t size, u8* dest, size_t sector_id,
                                            size_t sector_size, Op op) {
-    if (size % sector_size > 0) {
-        LOG_CRITICAL(Crypto, "Data size must be a multiple of sector size.");
-        return;
-    }
+    ASSERT_MSG(size % sector_size == 0, "XTS decryption size must be a multiple of sector size.");
 
     for (size_t i = 0; i < size; i += sector_size) {
         SetIV(CalculateNintendoTweak(sector_id++));
@@ -112,4 +109,4 @@ void AESCipher<Key, KeySize>::XTSTranscode(const u8* src, size_t size, u8* dest,
 
 template class AESCipher<Key128>;
 template class AESCipher<Key256>;
-} // namespace Core::Crypto
+} // namespace Core::Crypto

+ 3 - 5
src/core/crypto/ctr_encryption_layer.cpp

@@ -20,10 +20,8 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
     if (sector_offset == 0) {
         UpdateIV(base_offset + offset);
         std::vector<u8> raw = base->ReadBytes(length, offset);
-        if (raw.size() != length)
-            return Read(data, raw.size(), offset);
-        cipher.Transcode(raw.data(), length, data, Op::Decrypt);
-        return length;
+        cipher.Transcode(raw.data(), raw.size(), data, Op::Decrypt);
+        return raw.size();
     }
 
     // offset does not fall on block boundary (0x10)
@@ -34,7 +32,7 @@ size_t CTREncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
 
     if (length + sector_offset < 0x10) {
         std::memcpy(data, block.data() + sector_offset, std::min<u64>(length, read));
-        return read;
+        return std::min<u64>(length, read);
     }
     std::memcpy(data, block.data() + sector_offset, read);
     return read + Read(data + read, length - read, offset + read);

+ 165 - 7
src/core/crypto/key_manager.cpp

@@ -12,11 +12,112 @@
 #include "common/file_util.h"
 #include "common/hex_util.h"
 #include "common/logging/log.h"
+#include "core/crypto/aes_util.h"
 #include "core/crypto/key_manager.h"
 #include "core/settings.h"
 
 namespace Core::Crypto {
 
+Key128 GenerateKeyEncryptionKey(Key128 source, Key128 master, Key128 kek_seed, Key128 key_seed) {
+    Key128 out{};
+
+    AESCipher<Key128> cipher1(master, Mode::ECB);
+    cipher1.Transcode(kek_seed.data(), kek_seed.size(), out.data(), Op::Decrypt);
+    AESCipher<Key128> cipher2(out, Mode::ECB);
+    cipher2.Transcode(source.data(), source.size(), out.data(), Op::Decrypt);
+
+    if (key_seed != Key128{}) {
+        AESCipher<Key128> cipher3(out, Mode::ECB);
+        cipher3.Transcode(key_seed.data(), key_seed.size(), out.data(), Op::Decrypt);
+    }
+
+    return out;
+}
+
+boost::optional<Key128> DeriveSDSeed() {
+    const FileUtil::IOFile save_43(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir) +
+                                       "/system/save/8000000000000043",
+                                   "rb+");
+    if (!save_43.IsOpen())
+        return boost::none;
+    const FileUtil::IOFile sd_private(
+        FileUtil::GetUserPath(FileUtil::UserPath::SDMCDir) + "/Nintendo/Contents/private", "rb+");
+    if (!sd_private.IsOpen())
+        return boost::none;
+
+    sd_private.Seek(0, SEEK_SET);
+    std::array<u8, 0x10> private_seed{};
+    if (sd_private.ReadBytes(private_seed.data(), private_seed.size()) != 0x10)
+        return boost::none;
+
+    std::array<u8, 0x10> buffer{};
+    size_t offset = 0;
+    for (; offset + 0x10 < save_43.GetSize(); ++offset) {
+        save_43.Seek(offset, SEEK_SET);
+        save_43.ReadBytes(buffer.data(), buffer.size());
+        if (buffer == private_seed)
+            break;
+    }
+
+    if (offset + 0x10 >= save_43.GetSize())
+        return boost::none;
+
+    Key128 seed{};
+    save_43.Seek(offset + 0x10, SEEK_SET);
+    save_43.ReadBytes(seed.data(), seed.size());
+    return seed;
+}
+
+Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, const KeyManager& keys) {
+    if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK)))
+        return Loader::ResultStatus::ErrorMissingSDKEKSource;
+    if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration)))
+        return Loader::ResultStatus::ErrorMissingAESKEKGenerationSource;
+    if (!keys.HasKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration)))
+        return Loader::ResultStatus::ErrorMissingAESKeyGenerationSource;
+
+    const auto sd_kek_source =
+        keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK));
+    const auto aes_kek_gen =
+        keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration));
+    const auto aes_key_gen =
+        keys.GetKey(S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration));
+    const auto master_00 = keys.GetKey(S128KeyType::Master);
+    const auto sd_kek =
+        GenerateKeyEncryptionKey(sd_kek_source, master_00, aes_kek_gen, aes_key_gen);
+
+    if (!keys.HasKey(S128KeyType::SDSeed))
+        return Loader::ResultStatus::ErrorMissingSDSeed;
+    const auto sd_seed = keys.GetKey(S128KeyType::SDSeed);
+
+    if (!keys.HasKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save)))
+        return Loader::ResultStatus::ErrorMissingSDSaveKeySource;
+    if (!keys.HasKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA)))
+        return Loader::ResultStatus::ErrorMissingSDNCAKeySource;
+
+    std::array<Key256, 2> sd_key_sources{
+        keys.GetKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save)),
+        keys.GetKey(S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA)),
+    };
+
+    // Combine sources and seed
+    for (auto& source : sd_key_sources) {
+        for (size_t i = 0; i < source.size(); ++i)
+            source[i] ^= sd_seed[i & 0xF];
+    }
+
+    AESCipher<Key128> cipher(sd_kek, Mode::ECB);
+    // The transform manipulates sd_keys as part of the Transcode, so the return/output is
+    // unnecessary. This does not alter sd_keys_sources.
+    std::transform(sd_key_sources.begin(), sd_key_sources.end(), sd_keys.begin(),
+                   sd_key_sources.begin(), [&cipher](const Key256& source, Key256& out) {
+                       cipher.Transcode(source.data(), source.size(), out.data(), Op::Decrypt);
+                       return source; ///< Return unaltered source to satisfy output requirement.
+                   });
+
+    return Loader::ResultStatus::Success;
+}
+
 KeyManager::KeyManager() {
     // Initialize keys
     const std::string hactool_keys_dir = FileUtil::GetHactoolConfigurationPath();
@@ -24,12 +125,15 @@ KeyManager::KeyManager() {
     if (Settings::values.use_dev_keys) {
         dev_mode = true;
         AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "dev.keys", false);
+        AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "dev.keys_autogenerated", false);
     } else {
         dev_mode = false;
         AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "prod.keys", false);
+        AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "prod.keys_autogenerated", false);
     }
 
     AttemptLoadKeyFile(yuzu_keys_dir, hactool_keys_dir, "title.keys", true);
+    AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, "title.keys_autogenerated", true);
 }
 
 void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
@@ -56,17 +160,17 @@ void KeyManager::LoadFromFile(const std::string& filename, bool is_title_keys) {
             u128 rights_id{};
             std::memcpy(rights_id.data(), rights_id_raw.data(), rights_id_raw.size());
             Key128 key = Common::HexStringToArray<16>(out[1]);
-            SetKey(S128KeyType::Titlekey, key, rights_id[1], rights_id[0]);
+            s128_keys[{S128KeyType::Titlekey, rights_id[1], rights_id[0]}] = key;
         } else {
             std::transform(out[0].begin(), out[0].end(), out[0].begin(), ::tolower);
             if (s128_file_id.find(out[0]) != s128_file_id.end()) {
                 const auto index = s128_file_id.at(out[0]);
                 Key128 key = Common::HexStringToArray<16>(out[1]);
-                SetKey(index.type, key, index.field1, index.field2);
+                s128_keys[{index.type, index.field1, index.field2}] = key;
             } else if (s256_file_id.find(out[0]) != s256_file_id.end()) {
                 const auto index = s256_file_id.at(out[0]);
                 Key256 key = Common::HexStringToArray<32>(out[1]);
-                SetKey(index.type, key, index.field1, index.field2);
+                s256_keys[{index.type, index.field1, index.field2}] = key;
             }
         }
     }
@@ -100,11 +204,50 @@ Key256 KeyManager::GetKey(S256KeyType id, u64 field1, u64 field2) const {
     return s256_keys.at({id, field1, field2});
 }
 
+template <size_t Size>
+void KeyManager::WriteKeyToFile(bool title_key, std::string_view keyname,
+                                const std::array<u8, Size>& key) {
+    const std::string yuzu_keys_dir = FileUtil::GetUserPath(FileUtil::UserPath::KeysDir);
+    std::string filename = "title.keys_autogenerated";
+    if (!title_key)
+        filename = dev_mode ? "dev.keys_autogenerated" : "prod.keys_autogenerated";
+    const auto add_info_text = !FileUtil::Exists(yuzu_keys_dir + DIR_SEP + filename);
+    FileUtil::CreateFullPath(yuzu_keys_dir + DIR_SEP + filename);
+    std::ofstream file(yuzu_keys_dir + DIR_SEP + filename, std::ios::app);
+    if (!file.is_open())
+        return;
+    if (add_info_text) {
+        file
+            << "# This file is autogenerated by Yuzu\n"
+            << "# It serves to store keys that were automatically generated from the normal keys\n"
+            << "# If you are experiencing issues involving keys, it may help to delete this file\n";
+    }
+
+    file << fmt::format("\n{} = {}", keyname, Common::HexArrayToString(key));
+    AttemptLoadKeyFile(yuzu_keys_dir, yuzu_keys_dir, filename, title_key);
+}
+
 void KeyManager::SetKey(S128KeyType id, Key128 key, u64 field1, u64 field2) {
+    const auto iter = std::find_if(
+        s128_file_id.begin(), s128_file_id.end(),
+        [&id, &field1, &field2](const std::pair<std::string, KeyIndex<S128KeyType>> elem) {
+            return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
+                   std::tie(id, field1, field2);
+        });
+    if (iter != s128_file_id.end())
+        WriteKeyToFile(id == S128KeyType::Titlekey, iter->first, key);
     s128_keys[{id, field1, field2}] = key;
 }
 
 void KeyManager::SetKey(S256KeyType id, Key256 key, u64 field1, u64 field2) {
+    const auto iter = std::find_if(
+        s256_file_id.begin(), s256_file_id.end(),
+        [&id, &field1, &field2](const std::pair<std::string, KeyIndex<S256KeyType>> elem) {
+            return std::tie(elem.second.type, elem.second.field1, elem.second.field2) ==
+                   std::tie(id, field1, field2);
+        });
+    if (iter != s256_file_id.end())
+        WriteKeyToFile(false, iter->first, key);
     s256_keys[{id, field1, field2}] = key;
 }
 
@@ -125,7 +268,16 @@ bool KeyManager::KeyFileExists(bool title) {
            FileUtil::Exists(yuzu_keys_dir + DIR_SEP + "prod.keys");
 }
 
-const std::unordered_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = {
+void KeyManager::DeriveSDSeedLazy() {
+    if (HasKey(S128KeyType::SDSeed))
+        return;
+
+    const auto res = DeriveSDSeed();
+    if (res != boost::none)
+        SetKey(S128KeyType::SDSeed, res.get());
+}
+
+const boost::container::flat_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_file_id = {
     {"master_key_00", {S128KeyType::Master, 0, 0}},
     {"master_key_01", {S128KeyType::Master, 1, 0}},
     {"master_key_02", {S128KeyType::Master, 2, 0}},
@@ -167,11 +319,17 @@ const std::unordered_map<std::string, KeyIndex<S128KeyType>> KeyManager::s128_fi
     {"key_area_key_system_02", {S128KeyType::KeyArea, 2, static_cast<u64>(KeyAreaKeyType::System)}},
     {"key_area_key_system_03", {S128KeyType::KeyArea, 3, static_cast<u64>(KeyAreaKeyType::System)}},
     {"key_area_key_system_04", {S128KeyType::KeyArea, 4, static_cast<u64>(KeyAreaKeyType::System)}},
+    {"sd_card_kek_source", {S128KeyType::Source, static_cast<u64>(SourceKeyType::SDKEK), 0}},
+    {"aes_kek_generation_source",
+     {S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKEKGeneration), 0}},
+    {"aes_key_generation_source",
+     {S128KeyType::Source, static_cast<u64>(SourceKeyType::AESKeyGeneration), 0}},
+    {"sd_seed", {S128KeyType::SDSeed, 0, 0}},
 };
 
-const std::unordered_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = {
+const boost::container::flat_map<std::string, KeyIndex<S256KeyType>> KeyManager::s256_file_id = {
     {"header_key", {S256KeyType::Header, 0, 0}},
-    {"sd_card_save_key", {S256KeyType::SDSave, 0, 0}},
-    {"sd_card_nca_key", {S256KeyType::SDNCA, 0, 0}},
+    {"sd_card_save_key_source", {S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::Save), 0}},
+    {"sd_card_nca_key_source", {S256KeyType::SDKeySource, static_cast<u64>(SDKeyType::NCA), 0}},
 };
 } // namespace Core::Crypto

+ 35 - 33
src/core/crypto/key_manager.h

@@ -6,11 +6,13 @@
 
 #include <array>
 #include <string>
+#include <string_view>
 #include <type_traits>
-#include <unordered_map>
 #include <vector>
+#include <boost/container/flat_map.hpp>
 #include <fmt/format.h>
 #include "common/common_types.h"
+#include "core/loader/loader.h"
 
 namespace Core::Crypto {
 
@@ -22,9 +24,8 @@ static_assert(sizeof(Key128) == 16, "Key128 must be 128 bytes big.");
 static_assert(sizeof(Key256) == 32, "Key128 must be 128 bytes big.");
 
 enum class S256KeyType : u64 {
-    Header, //
-    SDSave, //
-    SDNCA,  //
+    Header,      //
+    SDKeySource, // f1=SDKeyType
 };
 
 enum class S128KeyType : u64 {
@@ -36,6 +37,7 @@ enum class S128KeyType : u64 {
     KeyArea,       // f1=crypto revision f2=type {app, ocean, system}
     SDSeed,        //
     Titlekey,      // f1=rights id LSB f2=rights id MSB
+    Source,        // f1=source type, f2= sub id
 };
 
 enum class KeyAreaKeyType : u8 {
@@ -44,6 +46,17 @@ enum class KeyAreaKeyType : u8 {
     System,
 };
 
+enum class SourceKeyType : u8 {
+    SDKEK,
+    AESKEKGeneration,
+    AESKeyGeneration,
+};
+
+enum class SDKeyType : u8 {
+    Save,
+    NCA,
+};
+
 template <typename KeyType>
 struct KeyIndex {
     KeyType type;
@@ -59,34 +72,12 @@ struct KeyIndex {
     }
 };
 
-// The following two (== and hash) are so KeyIndex can be a key in unordered_map
-
+// boost flat_map requires operator< for O(log(n)) lookups.
 template <typename KeyType>
-bool operator==(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
-    return std::tie(lhs.type, lhs.field1, lhs.field2) == std::tie(rhs.type, rhs.field1, rhs.field2);
+bool operator<(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
+    return std::tie(lhs.type, lhs.field1, lhs.field2) < std::tie(rhs.type, rhs.field1, rhs.field2);
 }
 
-template <typename KeyType>
-bool operator!=(const KeyIndex<KeyType>& lhs, const KeyIndex<KeyType>& rhs) {
-    return !operator==(lhs, rhs);
-}
-
-} // namespace Core::Crypto
-
-namespace std {
-template <typename KeyType>
-struct hash<Core::Crypto::KeyIndex<KeyType>> {
-    size_t operator()(const Core::Crypto::KeyIndex<KeyType>& k) const {
-        using std::hash;
-
-        return ((hash<u64>()(static_cast<u64>(k.type)) ^ (hash<u64>()(k.field1) << 1)) >> 1) ^
-               (hash<u64>()(k.field2) << 1);
-    }
-};
-} // namespace std
-
-namespace Core::Crypto {
-
 class KeyManager {
 public:
     KeyManager();
@@ -102,16 +93,27 @@ public:
 
     static bool KeyFileExists(bool title);
 
+    // Call before using the sd seed to attempt to derive it if it dosen't exist. Needs system save
+    // 8*43 and the private file to exist.
+    void DeriveSDSeedLazy();
+
 private:
-    std::unordered_map<KeyIndex<S128KeyType>, Key128> s128_keys;
-    std::unordered_map<KeyIndex<S256KeyType>, Key256> s256_keys;
+    boost::container::flat_map<KeyIndex<S128KeyType>, Key128> s128_keys;
+    boost::container::flat_map<KeyIndex<S256KeyType>, Key256> s256_keys;
 
     bool dev_mode;
     void LoadFromFile(const std::string& filename, bool is_title_keys);
     void AttemptLoadKeyFile(const std::string& dir1, const std::string& dir2,
                             const std::string& filename, bool title);
+    template <size_t Size>
+    void WriteKeyToFile(bool title_key, std::string_view keyname, const std::array<u8, Size>& key);
 
-    static const std::unordered_map<std::string, KeyIndex<S128KeyType>> s128_file_id;
-    static const std::unordered_map<std::string, KeyIndex<S256KeyType>> s256_file_id;
+    static const boost::container::flat_map<std::string, KeyIndex<S128KeyType>> s128_file_id;
+    static const boost::container::flat_map<std::string, KeyIndex<S256KeyType>> s256_file_id;
 };
+
+Key128 GenerateKeyEncryptionKey(Key128 source, Key128 master, Key128 kek_seed, Key128 key_seed);
+boost::optional<Key128> DeriveSDSeed();
+Loader::ResultStatus DeriveSDKeys(std::array<Key256, 2>& sd_keys, const KeyManager& keys);
+
 } // namespace Core::Crypto

+ 58 - 0
src/core/crypto/xts_encryption_layer.cpp

@@ -0,0 +1,58 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <cstring>
+#include "common/assert.h"
+#include "core/crypto/xts_encryption_layer.h"
+
+namespace Core::Crypto {
+
+constexpr u64 XTS_SECTOR_SIZE = 0x4000;
+
+XTSEncryptionLayer::XTSEncryptionLayer(FileSys::VirtualFile base_, Key256 key_)
+    : EncryptionLayer(std::move(base_)), cipher(key_, Mode::XTS) {}
+
+size_t XTSEncryptionLayer::Read(u8* data, size_t length, size_t offset) const {
+    if (length == 0)
+        return 0;
+
+    const auto sector_offset = offset & 0x3FFF;
+    if (sector_offset == 0) {
+        if (length % XTS_SECTOR_SIZE == 0) {
+            std::vector<u8> raw = base->ReadBytes(length, offset);
+            cipher.XTSTranscode(raw.data(), raw.size(), data, offset / XTS_SECTOR_SIZE,
+                                XTS_SECTOR_SIZE, Op::Decrypt);
+            return raw.size();
+        }
+        if (length > XTS_SECTOR_SIZE) {
+            const auto rem = length % XTS_SECTOR_SIZE;
+            const auto read = length - rem;
+            return Read(data, read, offset) + Read(data + read, rem, offset + read);
+        }
+        std::vector<u8> buffer = base->ReadBytes(XTS_SECTOR_SIZE, offset);
+        if (buffer.size() < XTS_SECTOR_SIZE)
+            buffer.resize(XTS_SECTOR_SIZE);
+        cipher.XTSTranscode(buffer.data(), buffer.size(), buffer.data(), offset / XTS_SECTOR_SIZE,
+                            XTS_SECTOR_SIZE, Op::Decrypt);
+        std::memcpy(data, buffer.data(), std::min(buffer.size(), length));
+        return std::min(buffer.size(), length);
+    }
+
+    // offset does not fall on block boundary (0x4000)
+    std::vector<u8> block = base->ReadBytes(0x4000, offset - sector_offset);
+    if (block.size() < XTS_SECTOR_SIZE)
+        block.resize(XTS_SECTOR_SIZE);
+    cipher.XTSTranscode(block.data(), block.size(), block.data(),
+                        (offset - sector_offset) / XTS_SECTOR_SIZE, XTS_SECTOR_SIZE, Op::Decrypt);
+    const size_t read = XTS_SECTOR_SIZE - sector_offset;
+
+    if (length + sector_offset < XTS_SECTOR_SIZE) {
+        std::memcpy(data, block.data() + sector_offset, std::min<u64>(length, read));
+        return std::min<u64>(length, read);
+    }
+    std::memcpy(data, block.data() + sector_offset, read);
+    return read + Read(data + read, length - read, offset + read);
+}
+} // namespace Core::Crypto

+ 25 - 0
src/core/crypto/xts_encryption_layer.h

@@ -0,0 +1,25 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include "core/crypto/aes_util.h"
+#include "core/crypto/encryption_layer.h"
+#include "core/crypto/key_manager.h"
+
+namespace Core::Crypto {
+
+// Sits on top of a VirtualFile and provides XTS-mode AES decription.
+class XTSEncryptionLayer : public EncryptionLayer {
+public:
+    XTSEncryptionLayer(FileSys::VirtualFile base, Key256 key);
+
+    size_t Read(u8* data, size_t length, size_t offset) const override;
+
+private:
+    // Must be mutable as operations modify cipher contexts.
+    mutable AESCipher<Key256> cipher;
+};
+
+} // namespace Core::Crypto

+ 2 - 9
src/core/file_sys/bis_factory.cpp

@@ -6,19 +6,12 @@
 
 namespace FileSys {
 
-static VirtualDir GetOrCreateDirectory(const VirtualDir& dir, std::string_view path) {
-    const auto res = dir->GetDirectoryRelative(path);
-    if (res == nullptr)
-        return dir->CreateDirectoryRelative(path);
-    return res;
-}
-
 BISFactory::BISFactory(VirtualDir nand_root_)
     : nand_root(std::move(nand_root_)),
       sysnand_cache(std::make_shared<RegisteredCache>(
-          GetOrCreateDirectory(nand_root, "/system/Contents/registered"))),
+          GetOrCreateDirectoryRelative(nand_root, "/system/Contents/registered"))),
       usrnand_cache(std::make_shared<RegisteredCache>(
-          GetOrCreateDirectory(nand_root, "/user/Contents/registered"))) {}
+          GetOrCreateDirectoryRelative(nand_root, "/user/Contents/registered"))) {}
 
 std::shared_ptr<RegisteredCache> BISFactory::GetSystemNANDContents() const {
     return sysnand_cache;

+ 12 - 0
src/core/file_sys/card_image.cpp

@@ -43,6 +43,8 @@ XCI::XCI(VirtualFile file_) : file(std::move(file_)), partitions(0x4) {
             partitions[static_cast<size_t>(partition)] = std::make_shared<PartitionFilesystem>(raw);
     }
 
+    program_nca_status = Loader::ResultStatus::ErrorXCIMissingProgramNCA;
+
     auto result = AddNCAFromPartition(XCIPartition::Secure);
     if (result != Loader::ResultStatus::Success) {
         status = result;
@@ -76,6 +78,10 @@ Loader::ResultStatus XCI::GetStatus() const {
     return status;
 }
 
+Loader::ResultStatus XCI::GetProgramNCAStatus() const {
+    return program_nca_status;
+}
+
 VirtualDir XCI::GetPartition(XCIPartition partition) const {
     return partitions[static_cast<size_t>(partition)];
 }
@@ -143,6 +149,12 @@ Loader::ResultStatus XCI::AddNCAFromPartition(XCIPartition part) {
         if (file->GetExtension() != "nca")
             continue;
         auto nca = std::make_shared<NCA>(file);
+        // TODO(DarkLordZach): Add proper Rev1+ Support
+        if (nca->IsUpdate())
+            continue;
+        if (nca->GetType() == NCAContentType::Program) {
+            program_nca_status = nca->GetStatus();
+        }
         if (nca->GetStatus() == Loader::ResultStatus::Success) {
             ncas.push_back(std::move(nca));
         } else {

+ 2 - 0
src/core/file_sys/card_image.h

@@ -59,6 +59,7 @@ public:
     explicit XCI(VirtualFile file);
 
     Loader::ResultStatus GetStatus() const;
+    Loader::ResultStatus GetProgramNCAStatus() const;
 
     u8 GetFormatVersion() const;
 
@@ -90,6 +91,7 @@ private:
     GamecardHeader header{};
 
     Loader::ResultStatus status;
+    Loader::ResultStatus program_nca_status;
 
     std::vector<VirtualDir> partitions;
     std::vector<std::shared_ptr<NCA>> ncas;

+ 9 - 1
src/core/file_sys/content_archive.cpp

@@ -178,7 +178,7 @@ VirtualFile NCA::Decrypt(NCASectionHeader s_header, VirtualFile in, u64 starting
             return std::static_pointer_cast<VfsFile>(out);
         }
     case NCASectionCryptoType::XTS:
-        // TODO(DarkLordZach): Implement XTSEncryptionLayer.
+        // TODO(DarkLordZach): Find a test case for XTS-encrypted NCAs
     default:
         LOG_ERROR(Crypto, "called with unhandled crypto type={:02X}",
                   static_cast<u8>(s_header.raw.header.crypto_type));
@@ -258,6 +258,10 @@ NCA::NCA(VirtualFile file_) : file(std::move(file_)) {
         file->ReadBytes(sections.data(), length_sections, SECTION_HEADER_OFFSET);
     }
 
+    is_update = std::find_if(sections.begin(), sections.end(), [](const NCASectionHeader& header) {
+                    return header.raw.header.crypto_type == NCASectionCryptoType::BKTR;
+                }) != sections.end();
+
     for (std::ptrdiff_t i = 0; i < number_sections; ++i) {
         auto section = sections[i];
 
@@ -358,6 +362,10 @@ VirtualFile NCA::GetBaseFile() const {
     return file;
 }
 
+bool NCA::IsUpdate() const {
+    return is_update;
+}
+
 bool NCA::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
     return false;
 }

+ 3 - 0
src/core/file_sys/content_archive.h

@@ -93,6 +93,8 @@ public:
 
     VirtualFile GetBaseFile() const;
 
+    bool IsUpdate() const;
+
 protected:
     bool ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) override;
 
@@ -111,6 +113,7 @@ private:
 
     NCAHeader header{};
     bool has_rights_id{};
+    bool is_update{};
 
     Loader::ResultStatus status{};
 

+ 14 - 0
src/core/file_sys/registered_cache.cpp

@@ -254,6 +254,8 @@ RegisteredCache::RegisteredCache(VirtualDir dir_, RegisteredCacheParsingFunction
     Refresh();
 }
 
+RegisteredCache::~RegisteredCache() = default;
+
 bool RegisteredCache::HasEntry(u64 title_id, ContentRecordType type) const {
     return GetEntryRaw(title_id, type) != nullptr;
 }
@@ -262,6 +264,18 @@ bool RegisteredCache::HasEntry(RegisteredCacheEntry entry) const {
     return GetEntryRaw(entry) != nullptr;
 }
 
+VirtualFile RegisteredCache::GetEntryUnparsed(u64 title_id, ContentRecordType type) const {
+    const auto id = GetNcaIDFromMetadata(title_id, type);
+    if (id == boost::none)
+        return nullptr;
+
+    return GetFileAtID(id.get());
+}
+
+VirtualFile RegisteredCache::GetEntryUnparsed(RegisteredCacheEntry entry) const {
+    return GetEntryUnparsed(entry.title_id, entry.type);
+}
+
 VirtualFile RegisteredCache::GetEntryRaw(u64 title_id, ContentRecordType type) const {
     const auto id = GetNcaIDFromMetadata(title_id, type);
     if (id == boost::none)

+ 4 - 0
src/core/file_sys/registered_cache.h

@@ -63,12 +63,16 @@ public:
     explicit RegisteredCache(VirtualDir dir,
                              RegisteredCacheParsingFunction parsing_function =
                                  [](const VirtualFile& file, const NcaID& id) { return file; });
+    ~RegisteredCache();
 
     void Refresh();
 
     bool HasEntry(u64 title_id, ContentRecordType type) const;
     bool HasEntry(RegisteredCacheEntry entry) const;
 
+    VirtualFile GetEntryUnparsed(u64 title_id, ContentRecordType type) const;
+    VirtualFile GetEntryUnparsed(RegisteredCacheEntry entry) const;
+
     VirtualFile GetEntryRaw(u64 title_id, ContentRecordType type) const;
     VirtualFile GetEntryRaw(RegisteredCacheEntry entry) const;
 

+ 14 - 1
src/core/file_sys/sdmc_factory.cpp

@@ -3,14 +3,27 @@
 // Refer to the license.txt file included.
 
 #include <memory>
+#include "core/file_sys/registered_cache.h"
 #include "core/file_sys/sdmc_factory.h"
+#include "core/file_sys/xts_archive.h"
 
 namespace FileSys {
 
-SDMCFactory::SDMCFactory(VirtualDir dir) : dir(std::move(dir)) {}
+SDMCFactory::SDMCFactory(VirtualDir dir_)
+    : dir(std::move(dir_)), contents(std::make_shared<RegisteredCache>(
+                                GetOrCreateDirectoryRelative(dir, "/Nintendo/Contents/registered"),
+                                [](const VirtualFile& file, const NcaID& id) {
+                                    return std::make_shared<NAX>(file, id)->GetDecrypted();
+                                })) {}
+
+SDMCFactory::~SDMCFactory() = default;
 
 ResultVal<VirtualDir> SDMCFactory::Open() {
     return MakeResult<VirtualDir>(dir);
 }
 
+std::shared_ptr<RegisteredCache> SDMCFactory::GetSDMCContents() const {
+    return contents;
+}
+
 } // namespace FileSys

+ 7 - 0
src/core/file_sys/sdmc_factory.h

@@ -4,20 +4,27 @@
 
 #pragma once
 
+#include <memory>
 #include "core/file_sys/vfs.h"
 #include "core/hle/result.h"
 
 namespace FileSys {
 
+class RegisteredCache;
+
 /// File system interface to the SDCard archive
 class SDMCFactory {
 public:
     explicit SDMCFactory(VirtualDir dir);
+    ~SDMCFactory();
 
     ResultVal<VirtualDir> Open();
+    std::shared_ptr<RegisteredCache> GetSDMCContents() const;
 
 private:
     VirtualDir dir;
+
+    std::shared_ptr<RegisteredCache> contents;
 };
 
 } // namespace FileSys

+ 7 - 0
src/core/file_sys/vfs.cpp

@@ -462,4 +462,11 @@ bool VfsRawCopy(VirtualFile src, VirtualFile dest) {
     std::vector<u8> data = src->ReadAllBytes();
     return dest->WriteBytes(data, 0) == data.size();
 }
+
+VirtualDir GetOrCreateDirectoryRelative(const VirtualDir& rel, std::string_view path) {
+    const auto res = rel->GetDirectoryRelative(path);
+    if (res == nullptr)
+        return rel->CreateDirectoryRelative(path);
+    return res;
+}
 } // namespace FileSys

+ 4 - 0
src/core/file_sys/vfs.h

@@ -318,4 +318,8 @@ bool DeepEquals(const VirtualFile& file1, const VirtualFile& file2, size_t block
 // directory of src/dest.
 bool VfsRawCopy(VirtualFile src, VirtualFile dest);
 
+// Checks if the directory at path relative to rel exists. If it does, returns that. If it does not
+// it attempts to create it and returns the new dir or nullptr on failure.
+VirtualDir GetOrCreateDirectoryRelative(const VirtualDir& rel, std::string_view path);
+
 } // namespace FileSys

+ 169 - 0
src/core/file_sys/xts_archive.cpp

@@ -0,0 +1,169 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <regex>
+#include <string>
+#include <mbedtls/md.h>
+#include <mbedtls/sha256.h>
+#include "common/assert.h"
+#include "common/hex_util.h"
+#include "common/logging/log.h"
+#include "core/crypto/aes_util.h"
+#include "core/crypto/xts_encryption_layer.h"
+#include "core/file_sys/partition_filesystem.h"
+#include "core/file_sys/vfs_offset.h"
+#include "core/file_sys/xts_archive.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+constexpr u64 NAX_HEADER_PADDING_SIZE = 0x4000;
+
+template <typename SourceData, typename SourceKey, typename Destination>
+static bool CalculateHMAC256(Destination* out, const SourceKey* key, size_t key_length,
+                             const SourceData* data, size_t data_length) {
+    mbedtls_md_context_t context;
+    mbedtls_md_init(&context);
+
+    const auto key_f = reinterpret_cast<const u8*>(key);
+    const std::vector<u8> key_v(key_f, key_f + key_length);
+
+    if (mbedtls_md_setup(&context, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 1) ||
+        mbedtls_md_hmac_starts(&context, reinterpret_cast<const u8*>(key), key_length) ||
+        mbedtls_md_hmac_update(&context, reinterpret_cast<const u8*>(data), data_length) ||
+        mbedtls_md_hmac_finish(&context, reinterpret_cast<u8*>(out))) {
+        mbedtls_md_free(&context);
+        return false;
+    }
+
+    mbedtls_md_free(&context);
+    return true;
+}
+
+NAX::NAX(VirtualFile file_) : file(std::move(file_)), header(std::make_unique<NAXHeader>()) {
+    std::string path = FileUtil::SanitizePath(file->GetFullPath());
+    static const std::regex nax_path_regex("/registered/(000000[0-9A-F]{2})/([0-9A-F]{32})\\.nca",
+                                           std::regex_constants::ECMAScript |
+                                               std::regex_constants::icase);
+    std::smatch match;
+    if (!std::regex_search(path, match, nax_path_regex)) {
+        status = Loader::ResultStatus::ErrorBadNAXFilePath;
+        return;
+    }
+
+    std::string two_dir = match[1];
+    std::string nca_id = match[2];
+    std::transform(two_dir.begin(), two_dir.end(), two_dir.begin(), ::toupper);
+    std::transform(nca_id.begin(), nca_id.end(), nca_id.begin(), ::tolower);
+
+    status = Parse(fmt::format("/registered/{}/{}.nca", two_dir, nca_id));
+}
+
+NAX::NAX(VirtualFile file_, std::array<u8, 0x10> nca_id)
+    : file(std::move(file_)), header(std::make_unique<NAXHeader>()) {
+    Core::Crypto::SHA256Hash hash{};
+    mbedtls_sha256(nca_id.data(), nca_id.size(), hash.data(), 0);
+    status = Parse(fmt::format("/registered/000000{:02X}/{}.nca", hash[0],
+                               Common::HexArrayToString(nca_id, false)));
+}
+
+Loader::ResultStatus NAX::Parse(std::string_view path) {
+    if (file->ReadObject(header.get()) != sizeof(NAXHeader))
+        return Loader::ResultStatus::ErrorBadNAXHeader;
+
+    if (header->magic != Common::MakeMagic('N', 'A', 'X', '0'))
+        return Loader::ResultStatus::ErrorBadNAXHeader;
+
+    if (file->GetSize() < NAX_HEADER_PADDING_SIZE + header->file_size)
+        return Loader::ResultStatus::ErrorIncorrectNAXFileSize;
+
+    keys.DeriveSDSeedLazy();
+    std::array<Core::Crypto::Key256, 2> sd_keys{};
+    const auto sd_keys_res = Core::Crypto::DeriveSDKeys(sd_keys, keys);
+    if (sd_keys_res != Loader::ResultStatus::Success) {
+        return sd_keys_res;
+    }
+
+    const auto enc_keys = header->key_area;
+
+    size_t i = 0;
+    for (; i < sd_keys.size(); ++i) {
+        std::array<Core::Crypto::Key128, 2> nax_keys{};
+        if (!CalculateHMAC256(nax_keys.data(), sd_keys[i].data(), 0x10, std::string(path).c_str(),
+                              path.size())) {
+            return Loader::ResultStatus::ErrorNAXKeyHMACFailed;
+        }
+
+        for (size_t j = 0; j < nax_keys.size(); ++j) {
+            Core::Crypto::AESCipher<Core::Crypto::Key128> cipher(nax_keys[j],
+                                                                 Core::Crypto::Mode::ECB);
+            cipher.Transcode(enc_keys[j].data(), 0x10, header->key_area[j].data(),
+                             Core::Crypto::Op::Decrypt);
+        }
+
+        Core::Crypto::SHA256Hash validation{};
+        if (!CalculateHMAC256(validation.data(), &header->magic, 0x60, sd_keys[i].data() + 0x10,
+                              0x10)) {
+            return Loader::ResultStatus::ErrorNAXValidationHMACFailed;
+        }
+        if (header->hmac == validation)
+            break;
+    }
+
+    if (i == 2) {
+        return Loader::ResultStatus::ErrorNAXKeyDerivationFailed;
+    }
+
+    type = static_cast<NAXContentType>(i);
+
+    Core::Crypto::Key256 final_key{};
+    std::memcpy(final_key.data(), &header->key_area, final_key.size());
+    const auto enc_file =
+        std::make_shared<OffsetVfsFile>(file, header->file_size, NAX_HEADER_PADDING_SIZE);
+    dec_file = std::make_shared<Core::Crypto::XTSEncryptionLayer>(enc_file, final_key);
+
+    return Loader::ResultStatus::Success;
+}
+
+Loader::ResultStatus NAX::GetStatus() const {
+    return status;
+}
+
+VirtualFile NAX::GetDecrypted() const {
+    return dec_file;
+}
+
+std::shared_ptr<NCA> NAX::AsNCA() const {
+    if (type == NAXContentType::NCA)
+        return std::make_shared<NCA>(GetDecrypted());
+    return nullptr;
+}
+
+NAXContentType NAX::GetContentType() const {
+    return type;
+}
+
+std::vector<std::shared_ptr<VfsFile>> NAX::GetFiles() const {
+    return {dec_file};
+}
+
+std::vector<std::shared_ptr<VfsDirectory>> NAX::GetSubdirectories() const {
+    return {};
+}
+
+std::string NAX::GetName() const {
+    return file->GetName();
+}
+
+std::shared_ptr<VfsDirectory> NAX::GetParentDirectory() const {
+    return file->GetContainingDirectory();
+}
+
+bool NAX::ReplaceFileWithSubdirectory(VirtualFile file, VirtualDir dir) {
+    return false;
+}
+} // namespace FileSys

+ 69 - 0
src/core/file_sys/xts_archive.h

@@ -0,0 +1,69 @@
+// 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/crypto/key_manager.h"
+#include "core/file_sys/content_archive.h"
+#include "core/file_sys/vfs.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+struct NAXHeader {
+    std::array<u8, 0x20> hmac;
+    u64_le magic;
+    std::array<Core::Crypto::Key128, 2> key_area;
+    u64_le file_size;
+    INSERT_PADDING_BYTES(0x30);
+};
+static_assert(sizeof(NAXHeader) == 0x80, "NAXHeader has incorrect size.");
+
+enum class NAXContentType : u8 {
+    Save = 0,
+    NCA = 1,
+};
+
+class NAX : public ReadOnlyVfsDirectory {
+public:
+    explicit NAX(VirtualFile file);
+    explicit NAX(VirtualFile file, std::array<u8, 0x10> nca_id);
+
+    Loader::ResultStatus GetStatus() const;
+
+    VirtualFile GetDecrypted() const;
+
+    std::shared_ptr<NCA> AsNCA() const;
+
+    NAXContentType GetContentType() 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 Parse(std::string_view path);
+
+    std::unique_ptr<NAXHeader> header;
+
+    VirtualFile file;
+    Loader::ResultStatus status;
+    NAXContentType type;
+
+    VirtualFile dec_file;
+
+    Core::Crypto::KeyManager keys;
+};
+} // namespace FileSys

+ 31 - 11
src/core/hle/service/filesystem/filesystem.cpp

@@ -305,17 +305,38 @@ ResultVal<FileSys::VirtualDir> OpenSDMC() {
 }
 
 std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents() {
+    LOG_TRACE(Service_FS, "Opening System NAND Contents");
+
+    if (bis_factory == nullptr)
+        return nullptr;
+
     return bis_factory->GetSystemNANDContents();
 }
 
 std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents() {
+    LOG_TRACE(Service_FS, "Opening User NAND Contents");
+
+    if (bis_factory == nullptr)
+        return nullptr;
+
     return bis_factory->GetUserNANDContents();
 }
 
-void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) {
-    romfs_factory = nullptr;
-    save_data_factory = nullptr;
-    sdmc_factory = nullptr;
+std::shared_ptr<FileSys::RegisteredCache> GetSDMCContents() {
+    LOG_TRACE(Service_FS, "Opening SDMC Contents");
+
+    if (sdmc_factory == nullptr)
+        return nullptr;
+
+    return sdmc_factory->GetSDMCContents();
+}
+
+void CreateFactories(const FileSys::VirtualFilesystem& vfs, bool overwrite) {
+    if (overwrite) {
+        bis_factory = nullptr;
+        save_data_factory = nullptr;
+        sdmc_factory = nullptr;
+    }
 
     auto nand_directory = vfs->OpenDirectory(FileUtil::GetUserPath(FileUtil::UserPath::NANDDir),
                                              FileSys::Mode::ReadWrite);
@@ -324,16 +345,15 @@ void RegisterFileSystems(const FileSys::VirtualFilesystem& vfs) {
 
     if (bis_factory == nullptr)
         bis_factory = std::make_unique<FileSys::BISFactory>(nand_directory);
-
-    auto savedata = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory));
-    save_data_factory = std::move(savedata);
-
-    auto sdcard = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory));
-    sdmc_factory = std::move(sdcard);
+    if (save_data_factory == nullptr)
+        save_data_factory = std::make_unique<FileSys::SaveDataFactory>(std::move(nand_directory));
+    if (sdmc_factory == nullptr)
+        sdmc_factory = std::make_unique<FileSys::SDMCFactory>(std::move(sd_directory));
 }
 
 void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs) {
-    RegisterFileSystems(vfs);
+    romfs_factory = nullptr;
+    CreateFactories(vfs, false);
     std::make_shared<FSP_LDR>()->InstallAsService(service_manager);
     std::make_shared<FSP_PR>()->InstallAsService(service_manager);
     std::make_shared<FSP_SRV>()->InstallAsService(service_manager);

+ 5 - 1
src/core/hle/service/filesystem/filesystem.h

@@ -46,8 +46,12 @@ ResultVal<FileSys::VirtualDir> OpenSDMC();
 
 std::shared_ptr<FileSys::RegisteredCache> GetSystemNANDContents();
 std::shared_ptr<FileSys::RegisteredCache> GetUserNANDContents();
+std::shared_ptr<FileSys::RegisteredCache> GetSDMCContents();
+
+// Creates the SaveData, SDMC, and BIS Factories. Should be called once and before any function
+// above is called.
+void CreateFactories(const FileSys::VirtualFilesystem& vfs, bool overwrite = true);
 
-/// Registers all Filesystem services with the specified service manager.
 void InstallInterfaces(SM::ServiceManager& service_manager, const FileSys::VirtualFilesystem& vfs);
 
 // A class that wraps a VfsDirectory with methods that return ResultVal and ResultCode instead of

+ 26 - 3
src/core/loader/loader.cpp

@@ -11,6 +11,7 @@
 #include "core/hle/kernel/process.h"
 #include "core/loader/deconstructed_rom_directory.h"
 #include "core/loader/elf.h"
+#include "core/loader/nax.h"
 #include "core/loader/nca.h"
 #include "core/loader/nro.h"
 #include "core/loader/nso.h"
@@ -32,6 +33,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
     CHECK_TYPE(NRO)
     CHECK_TYPE(NCA)
     CHECK_TYPE(XCI)
+    CHECK_TYPE(NAX)
 
 #undef CHECK_TYPE
 
@@ -73,6 +75,8 @@ std::string GetFileTypeString(FileType type) {
         return "NCA";
     case FileType::XCI:
         return "XCI";
+    case FileType::NAX:
+        return "NAX";
     case FileType::DeconstructedRomDirectory:
         return "Directory";
     case FileType::Error:
@@ -83,7 +87,7 @@ std::string GetFileTypeString(FileType type) {
     return "unknown";
 }
 
-constexpr std::array<const char*, 36> RESULT_MESSAGES{
+constexpr std::array<const char*, 49> RESULT_MESSAGES{
     "The operation completed successfully.",
     "The loader requested to load is already loaded.",
     "The operation is not implemented.",
@@ -120,6 +124,19 @@ constexpr std::array<const char*, 36> RESULT_MESSAGES{
     "There was a general error loading the NRO into emulated memory.",
     "There is no icon available.",
     "There is no control data available.",
+    "The NAX file has a bad header.",
+    "The NAX file has incorrect size as determined by the header.",
+    "The HMAC to generated the NAX decryption keys failed.",
+    "The HMAC to validate the NAX decryption keys failed.",
+    "The NAX key derivation failed.",
+    "The NAX file cannot be interpreted as an NCA file.",
+    "The NAX file has an incorrect path.",
+    "The SD seed could not be found or derived.",
+    "The SD KEK Source could not be found.",
+    "The AES KEK Generation Source could not be found.",
+    "The AES Key Generation Source could not be found.",
+    "The SD Save Key Source could not be found.",
+    "The SD NCA Key Source could not be found.",
 };
 
 std::ostream& operator<<(std::ostream& os, ResultStatus status) {
@@ -150,13 +167,18 @@ static std::unique_ptr<AppLoader> GetFileLoader(FileSys::VirtualFile file, FileT
     case FileType::NRO:
         return std::make_unique<AppLoader_NRO>(std::move(file));
 
-    // NX NCA file format.
+    // NX NCA (Nintendo Content Archive) file format.
     case FileType::NCA:
         return std::make_unique<AppLoader_NCA>(std::move(file));
 
+    // NX XCI (nX Card Image) file format.
     case FileType::XCI:
         return std::make_unique<AppLoader_XCI>(std::move(file));
 
+    // NX NAX (NintendoAesXts) file format.
+    case FileType::NAX:
+        return std::make_unique<AppLoader_NAX>(std::move(file));
+
     // NX deconstructed ROM directory.
     case FileType::DeconstructedRomDirectory:
         return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file));
@@ -170,7 +192,8 @@ std::unique_ptr<AppLoader> GetLoader(FileSys::VirtualFile file) {
     FileType type = IdentifyFile(file);
     FileType filename_type = GuessFromFilename(file->GetName());
 
-    if (type != filename_type) {
+    // Special case: 00 is either a NCA or NAX.
+    if (type != filename_type && !(file->GetName() == "00" && type == FileType::NAX)) {
         LOG_WARNING(Loader, "File {} has a different type than its extension.", file->GetName());
         if (FileType::Unknown == type)
             type = filename_type;

+ 14 - 0
src/core/loader/loader.h

@@ -32,6 +32,7 @@ enum class FileType {
     NRO,
     NCA,
     XCI,
+    NAX,
     DeconstructedRomDirectory,
 };
 
@@ -93,6 +94,19 @@ enum class ResultStatus : u16 {
     ErrorLoadingNRO,
     ErrorNoIcon,
     ErrorNoControl,
+    ErrorBadNAXHeader,
+    ErrorIncorrectNAXFileSize,
+    ErrorNAXKeyHMACFailed,
+    ErrorNAXValidationHMACFailed,
+    ErrorNAXKeyDerivationFailed,
+    ErrorNAXInconvertibleToNCA,
+    ErrorBadNAXFilePath,
+    ErrorMissingSDSeed,
+    ErrorMissingSDKEKSource,
+    ErrorMissingAESKEKGenerationSource,
+    ErrorMissingAESKeyGenerationSource,
+    ErrorMissingSDSaveKeySource,
+    ErrorMissingSDNCAKeySource,
 };
 
 std::ostream& operator<<(std::ostream& os, ResultStatus status);

+ 66 - 0
src/core/loader/nax.cpp

@@ -0,0 +1,66 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#include "common/logging/log.h"
+#include "core/file_sys/content_archive.h"
+#include "core/file_sys/romfs.h"
+#include "core/file_sys/xts_archive.h"
+#include "core/hle/kernel/process.h"
+#include "core/loader/nax.h"
+#include "core/loader/nca.h"
+
+namespace Loader {
+
+AppLoader_NAX::AppLoader_NAX(FileSys::VirtualFile file)
+    : AppLoader(file), nax(std::make_unique<FileSys::NAX>(file)),
+      nca_loader(std::make_unique<AppLoader_NCA>(nax->GetDecrypted())) {}
+
+AppLoader_NAX::~AppLoader_NAX() = default;
+
+FileType AppLoader_NAX::IdentifyType(const FileSys::VirtualFile& file) {
+    FileSys::NAX nax(file);
+
+    if (nax.GetStatus() == ResultStatus::Success && nax.AsNCA() != nullptr &&
+        nax.AsNCA()->GetStatus() == ResultStatus::Success) {
+        return FileType::NAX;
+    }
+
+    return FileType::Error;
+}
+
+ResultStatus AppLoader_NAX::Load(Kernel::SharedPtr<Kernel::Process>& process) {
+    if (is_loaded) {
+        return ResultStatus::ErrorAlreadyLoaded;
+    }
+
+    if (nax->GetStatus() != ResultStatus::Success)
+        return nax->GetStatus();
+
+    const auto nca = nax->AsNCA();
+    if (nca == nullptr) {
+        if (!Core::Crypto::KeyManager::KeyFileExists(false))
+            return ResultStatus::ErrorMissingProductionKeyFile;
+        return ResultStatus::ErrorNAXInconvertibleToNCA;
+    }
+
+    if (nca->GetStatus() != ResultStatus::Success)
+        return nca->GetStatus();
+
+    const auto result = nca_loader->Load(process);
+    if (result != ResultStatus::Success)
+        return result;
+
+    is_loaded = true;
+
+    return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NAX::ReadRomFS(FileSys::VirtualFile& dir) {
+    return nca_loader->ReadRomFS(dir);
+}
+
+ResultStatus AppLoader_NAX::ReadProgramId(u64& out_program_id) {
+    return nca_loader->ReadProgramId(out_program_id);
+}
+} // namespace Loader

+ 48 - 0
src/core/loader/nax.h

@@ -0,0 +1,48 @@
+// Copyright 2018 yuzu emulator team
+// Licensed under GPLv2 or any later version
+// Refer to the license.txt file included.
+
+#pragma once
+
+#include <memory>
+#include "common/common_types.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+class NAX;
+
+} // namespace FileSys
+
+namespace Loader {
+
+class AppLoader_NCA;
+
+/// Loads a NAX file
+class AppLoader_NAX final : public AppLoader {
+public:
+    explicit AppLoader_NAX(FileSys::VirtualFile file);
+    ~AppLoader_NAX() override;
+
+    /**
+     * Returns the type of the file
+     * @param file std::shared_ptr<VfsFile> open file
+     * @return FileType found, or FileType::Error if this loader doesn't know it
+     */
+    static FileType IdentifyType(const FileSys::VirtualFile& file);
+
+    FileType GetFileType() override {
+        return IdentifyType(file);
+    }
+
+    ResultStatus Load(Kernel::SharedPtr<Kernel::Process>& process) override;
+
+    ResultStatus ReadRomFS(FileSys::VirtualFile& dir) override;
+    ResultStatus ReadProgramId(u64& out_program_id) override;
+
+private:
+    std::unique_ptr<FileSys::NAX> nax;
+    std::unique_ptr<AppLoader_NCA> nca_loader;
+};
+
+} // namespace Loader

+ 6 - 5
src/core/loader/xci.cpp

@@ -61,11 +61,12 @@ ResultStatus AppLoader_XCI::Load(Kernel::SharedPtr<Kernel::Process>& process) {
     if (xci->GetStatus() != ResultStatus::Success)
         return xci->GetStatus();
 
-    if (xci->GetNCAFileByType(FileSys::NCAContentType::Program) == nullptr) {
-        if (!Core::Crypto::KeyManager::KeyFileExists(false))
-            return ResultStatus::ErrorMissingProductionKeyFile;
-        return ResultStatus::ErrorXCIMissingProgramNCA;
-    }
+    if (xci->GetProgramNCAStatus() != ResultStatus::Success)
+        return xci->GetProgramNCAStatus();
+
+    const auto nca = xci->GetNCAFileByType(FileSys::NCAContentType::Program);
+    if (nca == nullptr && !Core::Crypto::KeyManager::KeyFileExists(false))
+        return ResultStatus::ErrorMissingProductionKeyFile;
 
     auto result = nca_loader->Load(process);
     if (result != ResultStatus::Success)

+ 2 - 2
src/yuzu/configuration/config.cpp

@@ -126,8 +126,8 @@ void Config::ReadValues() {
     qt_config->beginGroup("UIGameList");
     UISettings::values.show_unknown = qt_config->value("show_unknown", true).toBool();
     UISettings::values.icon_size = qt_config->value("icon_size", 64).toUInt();
-    UISettings::values.row_1_text_id = qt_config->value("row_1_text_id", 0).toUInt();
-    UISettings::values.row_2_text_id = qt_config->value("row_2_text_id", 3).toUInt();
+    UISettings::values.row_1_text_id = qt_config->value("row_1_text_id", 3).toUInt();
+    UISettings::values.row_2_text_id = qt_config->value("row_2_text_id", 2).toUInt();
     qt_config->endGroup();
 
     qt_config->beginGroup("UILayout");

+ 11 - 11
src/yuzu/game_list.cpp

@@ -426,13 +426,12 @@ static void GetMetadataFromControlNCA(const std::shared_ptr<FileSys::NCA>& nca,
     }
 }
 
-void GameListWorker::AddInstalledTitlesToGameList() {
-    const auto usernand = Service::FileSystem::GetUserNANDContents();
-    const auto installed_games = usernand->ListEntriesFilter(FileSys::TitleType::Application,
-                                                             FileSys::ContentRecordType::Program);
+void GameListWorker::AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache) {
+    const auto installed_games = cache->ListEntriesFilter(FileSys::TitleType::Application,
+                                                          FileSys::ContentRecordType::Program);
 
     for (const auto& game : installed_games) {
-        const auto& file = usernand->GetEntryRaw(game);
+        const auto& file = cache->GetEntryUnparsed(game);
         std::unique_ptr<Loader::AppLoader> loader = Loader::GetLoader(file);
         if (!loader)
             continue;
@@ -442,8 +441,7 @@ void GameListWorker::AddInstalledTitlesToGameList() {
         u64 program_id = 0;
         loader->ReadProgramId(program_id);
 
-        const auto& control =
-            usernand->GetEntry(game.title_id, FileSys::ContentRecordType::Control);
+        const auto& control = cache->GetEntry(game.title_id, FileSys::ContentRecordType::Control);
         if (control != nullptr)
             GetMetadataFromControlNCA(control, icon, name);
         emit EntryReady({
@@ -457,11 +455,11 @@ void GameListWorker::AddInstalledTitlesToGameList() {
         });
     }
 
-    const auto control_data = usernand->ListEntriesFilter(FileSys::TitleType::Application,
-                                                          FileSys::ContentRecordType::Control);
+    const auto control_data = cache->ListEntriesFilter(FileSys::TitleType::Application,
+                                                       FileSys::ContentRecordType::Control);
 
     for (const auto& entry : control_data) {
-        const auto nca = usernand->GetEntry(entry);
+        const auto nca = cache->GetEntry(entry);
         if (nca != nullptr)
             nca_control_map.insert_or_assign(entry.title_id, nca);
     }
@@ -549,7 +547,9 @@ void GameListWorker::run() {
     stop_processing = false;
     watch_list.append(dir_path);
     FillControlMap(dir_path.toStdString());
-    AddInstalledTitlesToGameList();
+    AddInstalledTitlesToGameList(Service::FileSystem::GetUserNANDContents());
+    AddInstalledTitlesToGameList(Service::FileSystem::GetSystemNANDContents());
+    AddInstalledTitlesToGameList(Service::FileSystem::GetSDMCContents());
     AddFstEntriesToGameList(dir_path.toStdString(), deep_scan ? 256 : 0);
     nca_control_map.clear();
     emit Finished(watch_list);

+ 1 - 1
src/yuzu/game_list_p.h

@@ -172,7 +172,7 @@ private:
     bool deep_scan;
     std::atomic_bool stop_processing;
 
-    void AddInstalledTitlesToGameList();
+    void AddInstalledTitlesToGameList(std::shared_ptr<FileSys::RegisteredCache> cache);
     void FillControlMap(const std::string& dir_path);
     void AddFstEntriesToGameList(const std::string& dir_path, unsigned int recursion = 0);
 };

+ 1 - 2
src/yuzu/main.cpp

@@ -133,8 +133,7 @@ GMainWindow::GMainWindow()
     show();
 
     // Necessary to load titles from nand in gamelist.
-    Service::FileSystem::RegisterBIS(std::make_unique<FileSys::BISFactory>(vfs->OpenDirectory(
-        FileUtil::GetUserPath(FileUtil::UserPath::NANDDir), FileSys::Mode::ReadWrite)));
+    Service::FileSystem::CreateFactories(vfs);
     game_list->PopulateAsync(UISettings::values.gamedir, UISettings::values.gamedir_deepscan);
 
     // Show one-time "callout" messages to the user