diff --git a/KEYS.md b/KEYS.md
new file mode 100644
index 0000000000..a2867ddca2
--- /dev/null
+++ b/KEYS.md
@@ -0,0 +1,104 @@
+# Keys
+
+Keys are required for decrypting most of the file formats used by the Nintendo Switch.
+
+Keysets are stored as text files. These 3 filenames are automatically read:  
+`prod.keys` - Contains common keys usedy by all Switch devices.  
+`console.keys` - Contains console-unique keys.  
+`title.keys` - Contains game-specific keys.
+
+Ryujinx will first look for keys in `RyuFS/system`, and if it doesn't find any there it will look in `$HOME/.switch`.
+
+A guide to assist with dumping your own keys can be found [here](https://gist.github.com/roblabla/d8358ab058bbe3b00614740dcba4f208).
+
+## Common keys
+
+Here is a template for a key file containing the main keys Ryujinx uses to read content files.  
+Both `prod.keys` and `console.keys` use this format.
+
+```
+master_key_00                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+master_key_01                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+master_key_02                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+master_key_03                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+master_key_04                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+master_key_05                         = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+titlekek_source                       = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+key_area_key_application_source       = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+key_area_key_ocean_source             = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+key_area_key_system_source            = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+aes_kek_generation_source             = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+aes_key_generation_source             = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+header_kek_source                     = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+header_key_source                     = XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+```
+
+## Title keys
+
+Title keys are stored in the format `rights_id,key`.
+
+For example:
+
+```
+01000000000100000000000000000003,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+01000000000108000000000000000003,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+01000000000108000000000000000004,XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+```
+
+## Complete key list
+Below is a complete list of keys that are currently recognized.  
+\## represents a hexadecimal number between 00 and 1F  
+@@ represents a hexadecimal number between 00 and 03
+
+### Common keys
+
+```
+master_key_source
+keyblob_mac_key_source
+package2_key_source
+aes_kek_generation_source
+aes_key_generation_source
+key_area_key_application_source
+key_area_key_ocean_source
+key_area_key_system_source
+titlekek_source
+header_kek_source
+header_key_source
+sd_card_kek_source
+sd_card_nca_key_source
+sd_card_save_key_source
+retail_specific_aes_key_source
+per_console_key_source
+bis_kek_source
+bis_key_source_@@
+
+header_key
+xci_header_key
+eticket_rsa_kek
+
+master_key_##
+package1_key_##
+package2_key_##
+titlekek_##
+key_area_key_application_##
+key_area_key_ocean_##
+key_area_key_system_##
+keyblob_key_source_##
+keyblob_##
+```
+
+### Console-unique keys
+
+```
+secure_boot_key
+tsec_key
+device_key
+bis_key_@@
+
+keyblob_key_##
+keyblob_mac_key_##
+encrypted_keyblob_##
+
+sd_seed
+```
diff --git a/Ryujinx.HLE/HOS/Horizon.cs b/Ryujinx.HLE/HOS/Horizon.cs
index a557967523..92a87661df 100644
--- a/Ryujinx.HLE/HOS/Horizon.cs
+++ b/Ryujinx.HLE/HOS/Horizon.cs
@@ -1,3 +1,4 @@
+using LibHac;
 using Ryujinx.HLE.HOS.Font;
 using Ryujinx.HLE.HOS.Kernel;
 using Ryujinx.HLE.HOS.SystemState;
@@ -7,6 +8,7 @@ using Ryujinx.HLE.Logging;
 using System;
 using System.Collections.Concurrent;
 using System.IO;
+using System.Linq;
 
 namespace Ryujinx.HLE.HOS
 {
@@ -30,6 +32,8 @@ namespace Ryujinx.HLE.HOS
 
         internal KEvent VsyncEvent { get; private set; }
 
+        internal Keyset KeySet { get; private set; }
+
         public Horizon(Switch Device)
         {
             this.Device = Device;
@@ -52,6 +56,8 @@ namespace Ryujinx.HLE.HOS
             Font = new SharedFontManager(Device, FontSharedMem.PA);
 
             VsyncEvent = new KEvent();
+
+            LoadKeySet();
         }
 
         public void LoadCart(string ExeFsDir, string RomFsFile = null)
@@ -119,6 +125,179 @@ namespace Ryujinx.HLE.HOS
             MainProcess.Run();
         }
 
+        public void LoadXci(string XciFile)
+        {
+            FileStream File = new FileStream(XciFile, FileMode.Open, FileAccess.Read);
+
+            Xci Xci = new Xci(KeySet, File);
+
+            Nca Nca = GetXciMainNca(Xci);
+
+            if (Nca == null)
+            {
+                Device.Log.PrintError(LogClass.Loader, "Unable to load XCI");
+
+                return;
+            }
+
+            LoadNca(Nca);
+        }
+
+        private Nca GetXciMainNca(Xci Xci)
+        {
+            if (Xci.SecurePartition == null)
+            {
+                throw new InvalidDataException("Could not find XCI secure partition");
+            }
+
+            Nca MainNca = null;
+            Nca PatchNca = null;
+
+            foreach (PfsFileEntry FileEntry in Xci.SecurePartition.Files.Where(x => x.Name.EndsWith(".nca")))
+            {
+                Stream NcaStream = Xci.SecurePartition.OpenFile(FileEntry);
+
+                Nca Nca = new Nca(KeySet, NcaStream, true);
+
+                if (Nca.Header.ContentType == ContentType.Program)
+                {
+                    if (Nca.Sections.Any(x => x?.Type == SectionType.Romfs))
+                    {
+                        MainNca = Nca;
+                    }
+                    else if (Nca.Sections.Any(x => x?.Type == SectionType.Bktr))
+                    {
+                        PatchNca = Nca;
+                    }
+                }
+            }
+
+            if (MainNca == null)
+            {
+                Device.Log.PrintError(LogClass.Loader, "Could not find an Application NCA in the provided XCI file");
+            }
+
+            MainNca.SetBaseNca(PatchNca);
+
+            return MainNca;
+        }
+
+        public void LoadNca(string NcaFile)
+        {
+            FileStream File = new FileStream(NcaFile, FileMode.Open, FileAccess.Read);
+
+            Nca Nca = new Nca(KeySet, File, true);
+
+            LoadNca(Nca);
+        }
+
+        public void LoadNsp(string NspFile)
+        {
+            FileStream File = new FileStream(NspFile, FileMode.Open, FileAccess.Read);
+
+            Pfs Nsp = new Pfs(File);
+
+            PfsFileEntry TicketFile = Nsp.Files.FirstOrDefault(x => x.Name.EndsWith(".tik"));
+
+            // Load title key from the NSP's ticket in case the user doesn't have a title key file
+            if (TicketFile != null)
+            {
+                // todo Change when Ticket(Stream) overload is added
+                Ticket Ticket = new Ticket(new BinaryReader(Nsp.OpenFile(TicketFile)));
+
+                KeySet.TitleKeys[Ticket.RightsId] = Ticket.GetTitleKey(KeySet);
+            }
+
+            foreach (PfsFileEntry NcaFile in Nsp.Files.Where(x => x.Name.EndsWith(".nca")))
+            {
+                Nca Nca = new Nca(KeySet, Nsp.OpenFile(NcaFile), true);
+
+                if (Nca.Header.ContentType == ContentType.Program)
+                {
+                    LoadNca(Nca);
+
+                    return;
+                }
+            }
+
+            Device.Log.PrintError(LogClass.Loader, "Could not find an Application NCA in the provided NSP file");
+        }
+
+        public void LoadNca(Nca Nca)
+        {
+            NcaSection RomfsSection = Nca.Sections.FirstOrDefault(x => x?.Type == SectionType.Romfs);
+            NcaSection ExefsSection = Nca.Sections.FirstOrDefault(x => x?.IsExefs == true);
+
+            if (ExefsSection == null)
+            {
+                Device.Log.PrintError(LogClass.Loader, "No ExeFS found in NCA");
+
+                return;
+            }
+
+            if (RomfsSection == null)
+            {
+                Device.Log.PrintError(LogClass.Loader, "No RomFS found in NCA");
+
+                return;
+            }
+
+            Stream RomfsStream = Nca.OpenSection(RomfsSection.SectionNum, false);
+            Device.FileSystem.SetRomFs(RomfsStream);
+
+            Stream ExefsStream = Nca.OpenSection(ExefsSection.SectionNum, false);
+            Pfs Exefs = new Pfs(ExefsStream);
+
+            Npdm MetaData = null;
+
+            if (Exefs.FileExists("main.npdm"))
+            {
+                Device.Log.PrintInfo(LogClass.Loader, "Loading main.npdm...");
+
+                MetaData = new Npdm(Exefs.OpenFile("main.npdm"));
+            }
+            else
+            {
+                Device.Log.PrintWarning(LogClass.Loader, $"NPDM file not found, using default values!");
+            }
+
+            Process MainProcess = MakeProcess(MetaData);
+
+            void LoadNso(string Filename)
+            {
+                foreach (PfsFileEntry File in Exefs.Files.Where(x => x.Name.StartsWith(Filename)))
+                {
+                    if (Path.GetExtension(File.Name) != string.Empty)
+                    {
+                        continue;
+                    }
+
+                    Device.Log.PrintInfo(LogClass.Loader, $"Loading {Filename}...");
+
+                    string Name = Path.GetFileNameWithoutExtension(File.Name);
+
+                    Nso Program = new Nso(Exefs.OpenFile(File), Name);
+
+                    MainProcess.LoadProgram(Program);
+                }
+            }
+
+            if (!MainProcess.MetaData.Is64Bits)
+            {
+                throw new NotImplementedException("32-bit titles are unsupported!");
+            }
+
+            LoadNso("rtld");
+
+            MainProcess.SetEmptyArgs();
+
+            LoadNso("main");
+            LoadNso("subsdk");
+            LoadNso("sdk");
+
+            MainProcess.Run();
+        }
+
         public void LoadProgram(string FilePath)
         {
             bool IsNro = Path.GetExtension(FilePath).ToLower() == ".nro";
@@ -156,6 +335,42 @@ namespace Ryujinx.HLE.HOS
             MainProcess.Run(IsNro);
         }
 
+        public void LoadKeySet()
+        {
+            string KeyFile        = null;
+            string TitleKeyFile   = null;
+            string ConsoleKeyFile = null;
+
+            string Home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+            LoadSetAtPath(Path.Combine(Home, ".switch"));
+            LoadSetAtPath(Device.FileSystem.GetSystemPath());
+
+            KeySet = ExternalKeys.ReadKeyFile(KeyFile, TitleKeyFile, ConsoleKeyFile);
+
+            void LoadSetAtPath(string BasePath)
+            {
+                string LocalKeyFile        = Path.Combine(BasePath,    "prod.keys");
+                string LocalTitleKeyFile   = Path.Combine(BasePath,   "title.keys");
+                string LocalConsoleKeyFile = Path.Combine(BasePath, "console.keys");
+
+                if (File.Exists(LocalKeyFile))
+                {
+                    KeyFile = LocalKeyFile;
+                }
+
+                if (File.Exists(LocalTitleKeyFile))
+                {
+                    TitleKeyFile = LocalTitleKeyFile;
+                }
+
+                if (File.Exists(LocalConsoleKeyFile))
+                {
+                    ConsoleKeyFile = LocalConsoleKeyFile;
+                }
+            }
+        }
+
         public void SignalVsync() => VsyncEvent.WaitEvent.Set();
 
         private Process MakeProcess(Npdm MetaData = null)
diff --git a/Ryujinx.HLE/Ryujinx.HLE.csproj b/Ryujinx.HLE/Ryujinx.HLE.csproj
index fa4c254e24..cd1bb03458 100644
--- a/Ryujinx.HLE/Ryujinx.HLE.csproj
+++ b/Ryujinx.HLE/Ryujinx.HLE.csproj
@@ -25,6 +25,7 @@
     <ProjectReference Include="..\ChocolArm64\ChocolArm64.csproj" />
     <ProjectReference Include="..\Ryujinx.Audio\Ryujinx.Audio.csproj" />
     <ProjectReference Include="..\Ryujinx.Graphics\Ryujinx.Graphics.csproj" />
+    <PackageReference Include="LibHac" Version="0.1.1" />
   </ItemGroup>
 
 </Project>
diff --git a/Ryujinx.HLE/Switch.cs b/Ryujinx.HLE/Switch.cs
index 32d5bef681..090aae111e 100644
--- a/Ryujinx.HLE/Switch.cs
+++ b/Ryujinx.HLE/Switch.cs
@@ -61,6 +61,21 @@ namespace Ryujinx.HLE
             System.LoadCart(ExeFsDir, RomFsFile);
         }
 
+        public void LoadXci(string XciFile)
+        {
+            System.LoadXci(XciFile);
+        }
+
+        public void LoadNca(string NcaFile)
+        {
+            System.LoadNca(NcaFile);
+        }
+
+        public void LoadNsp(string NspFile)
+        {
+            System.LoadNsp(NspFile);
+        }
+
         public void LoadProgram(string FileName)
         {
             System.LoadProgram(FileName);
diff --git a/Ryujinx.HLE/VirtualFileSystem.cs b/Ryujinx.HLE/VirtualFileSystem.cs
index 31b8e184ce..133538f93a 100644
--- a/Ryujinx.HLE/VirtualFileSystem.cs
+++ b/Ryujinx.HLE/VirtualFileSystem.cs
@@ -17,6 +17,12 @@ namespace Ryujinx.HLE
             RomFs = new FileStream(FileName, FileMode.Open, FileAccess.Read);
         }
 
+        public void SetRomFs(Stream RomfsStream)
+        {
+            RomFs?.Close();
+            RomFs = RomfsStream;
+        }
+
         public string GetFullPath(string BasePath, string FileName)
         {
             if (FileName.StartsWith("//"))
diff --git a/Ryujinx/Ui/Program.cs b/Ryujinx/Ui/Program.cs
index fdbc59a5a8..053cf1be4b 100644
--- a/Ryujinx/Ui/Program.cs
+++ b/Ryujinx/Ui/Program.cs
@@ -50,9 +50,25 @@ namespace Ryujinx
                 }
                 else if (File.Exists(args[0]))
                 {
-                    Console.WriteLine("Loading as homebrew.");
-
-                    Device.LoadProgram(args[0]);
+                    switch (Path.GetExtension(args[0]).ToLowerInvariant())
+                    {
+                        case ".xci":
+                            Console.WriteLine("Loading as XCI.");
+                            Device.LoadXci(args[0]);
+                            break;
+                        case ".nca":
+                            Console.WriteLine("Loading as NCA.");
+                            Device.LoadNca(args[0]);
+                            break;
+                        case ".nsp":
+                            Console.WriteLine("Loading as NSP.");
+                            Device.LoadNsp(args[0]);
+                            break;
+                        default:
+                            Console.WriteLine("Loading as homebrew.");
+                            Device.LoadProgram(args[0]);
+                            break;
+                    }
                 }
             }
             else