From 01a3bef9039531c5dc717db9441de067740f26fe Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Wed, 9 Jan 2019 12:21:12 -0600 Subject: [PATCH] Initial AesXtsFileSystem --- src/LibHac/IO/AesXtsDirectory.cs | 57 ++++++++++++++ src/LibHac/IO/AesXtsFile.cs | 121 ++++++++++++++++++++++++++++++ src/LibHac/IO/AesXtsFileSystem.cs | 93 +++++++++++++++++++++++ src/LibHac/IO/StorageFile.cs | 2 + 4 files changed, 273 insertions(+) create mode 100644 src/LibHac/IO/AesXtsDirectory.cs create mode 100644 src/LibHac/IO/AesXtsFile.cs create mode 100644 src/LibHac/IO/AesXtsFileSystem.cs diff --git a/src/LibHac/IO/AesXtsDirectory.cs b/src/LibHac/IO/AesXtsDirectory.cs new file mode 100644 index 00000000..ca45a1dd --- /dev/null +++ b/src/LibHac/IO/AesXtsDirectory.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; + +namespace LibHac.IO +{ + public class AesXtsDirectory : IDirectory + { + public IFileSystem ParentFileSystem { get; } + public string FullPath { get; } + public OpenDirectoryMode Mode { get; } + + private IFileSystem BaseFileSystem { get; } + private IDirectory BaseDirectory { get; } + + public AesXtsDirectory(IFileSystem parentFs, IDirectory baseDir, OpenDirectoryMode mode) + { + ParentFileSystem = parentFs; + BaseDirectory = baseDir; + Mode = mode; + BaseFileSystem = BaseDirectory.ParentFileSystem; + FullPath = BaseDirectory.FullPath; + } + + public IEnumerable Read() + { + foreach (DirectoryEntry entry in BaseDirectory.Read()) + { + if (entry.Type == DirectoryEntryType.Directory) + { + yield return entry; + } + else + { + long size = GetAesXtsFileSize(entry.FullPath); + yield return new DirectoryEntry(entry.Name, entry.FullPath, entry.Type, size); + } + } + } + + public int GetEntryCount() + { + return BaseDirectory.GetEntryCount(); + } + + private long GetAesXtsFileSize(string path) + { + IFile file = BaseFileSystem.OpenFile(path, OpenMode.Read); + var buffer = new byte[8]; + + file.Read(buffer, 0); + if (BitConverter.ToUInt32(buffer, 0) != 0x3058414E) return 0; + + file.Read(buffer, 0x48); + return BitConverter.ToInt32(buffer, 0); + } + } +} diff --git a/src/LibHac/IO/AesXtsFile.cs b/src/LibHac/IO/AesXtsFile.cs new file mode 100644 index 00000000..8c7d44fb --- /dev/null +++ b/src/LibHac/IO/AesXtsFile.cs @@ -0,0 +1,121 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace LibHac.IO +{ + public class AesXtsFile : FileBase + { + private IFile BaseFile { get; } + private string Path { get; } + private byte[] KekSeed { get; } + private byte[] VerificationKey { get; } + private int BlockSize { get; } + + public byte[] Hmac { get; private set; } + public byte[][] EncKeys { get; } = Util.CreateJaggedArray(2, 0x10); + public byte[][] Keys { get; } = Util.CreateJaggedArray(2, 0x10); + public byte[] Key { get; } = new byte[0x20]; + public long Length { get; private set; } + private IStorage BaseStorage { get; } + + private const int HeaderLength = 0x4000; + + public AesXtsFile(OpenMode mode, IFile baseFile, string path, ReadOnlySpan kekSeed, ReadOnlySpan verificationKey, int blockSize) + { + Mode = mode; + BaseFile = baseFile; + Path = path; + KekSeed = kekSeed.ToArray(); + VerificationKey = verificationKey.ToArray(); + BlockSize = blockSize; + + ReadHeader(BaseFile); + + DeriveKeys(); + Storage encStorage = new FileStorage(BaseFile).Slice(HeaderLength, Length); + BaseStorage = new CachedStorage(new Aes128XtsStorage(encStorage, Key, BlockSize, true), 4, true); + } + + private void ReadHeader(IFile file) + { + var reader = new BinaryReader(file.AsStream()); + + Hmac = reader.ReadBytes(0x20); + string magic = reader.ReadAscii(4); + reader.BaseStream.Position += 4; + if (magic != "NAX0") throw new InvalidDataException("Not an NAX0 file"); + EncKeys[0] = reader.ReadBytes(0x10); + EncKeys[1] = reader.ReadBytes(0x10); + Length = reader.ReadInt64(); + } + + private void DeriveKeys() + { + var validationHashKey = new byte[0x60]; + BaseFile.Read(validationHashKey, 0x20); + + var naxSpecificKeys = Util.CreateJaggedArray(2, 0x10); + + // Use the sd path to generate the kek for this NAX0 + var hash = new HMACSHA256(KekSeed); + byte[] sdPathBytes = Encoding.ASCII.GetBytes(Path); + byte[] checksum = hash.ComputeHash(sdPathBytes, 0, sdPathBytes.Length); + Array.Copy(checksum, 0, naxSpecificKeys[0], 0, 0x10); + Array.Copy(checksum, 0x10, naxSpecificKeys[1], 0, 0x10); + + // Decrypt this NAX0's keys + Crypto.DecryptEcb(naxSpecificKeys[0], EncKeys[0], Keys[0], 0x10); + Crypto.DecryptEcb(naxSpecificKeys[1], EncKeys[1], Keys[1], 0x10); + Array.Copy(Keys[0], 0, Key, 0, 0x10); + Array.Copy(Keys[1], 0, Key, 0x10, 0x10); + + // Copy the decrypted keys into the NAX0 header and use that for the HMAC key + // for validating that the keys are correct + Array.Copy(Keys[0], 0, validationHashKey, 8, 0x10); + Array.Copy(Keys[1], 0, validationHashKey, 0x18, 0x10); + + var validationHash = new HMACSHA256(validationHashKey); + byte[] validationMac = validationHash.ComputeHash(VerificationKey); + + if (Util.ArraysEqual(Hmac, validationMac)) + { + return; + } + + throw new ArgumentException("NAX0 key derivation failed."); + } + + public override int Read(Span destination, long offset) + { + int toRead = ValidateReadParamsAndGetSize(destination, offset); + + BaseStorage.Read(destination.Slice(0, toRead), offset); + + return toRead; + } + + public override void Write(ReadOnlySpan source, long offset) + { + ValidateWriteParams(source, offset); + + BaseStorage.Write(source, offset); + } + + public override void Flush() + { + BaseStorage.Flush(); + } + + public override long GetSize() + { + return Length; + } + + public override void SetSize(long size) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LibHac/IO/AesXtsFileSystem.cs b/src/LibHac/IO/AesXtsFileSystem.cs new file mode 100644 index 00000000..ca4fccf5 --- /dev/null +++ b/src/LibHac/IO/AesXtsFileSystem.cs @@ -0,0 +1,93 @@ +using System; + +namespace LibHac.IO +{ + public class AesXtsFileSystem : IFileSystem + { + public int BlockSize { get; } + + private IFileSystem BaseFileSystem { get; } + private byte[] KekSource { get; } + private byte[] ValidationKey { get; } + + public AesXtsFileSystem(IFileSystem fs, byte[] kekSource, byte[] validationKey, int blockSize) + { + BaseFileSystem = fs; + KekSource = kekSource; + ValidationKey = validationKey; + BlockSize = blockSize; + } + + public AesXtsFileSystem(IFileSystem fs, byte[] keys, int blockSize) + { + BaseFileSystem = fs; + KekSource = keys.AsSpan(0, 0x10).ToArray(); + ValidationKey = keys.AsSpan(0x10, 0x10).ToArray(); + BlockSize = blockSize; + } + + public void CreateDirectory(string path) + { + throw new NotImplementedException(); + } + + public void CreateFile(string path, long size) + { + throw new NotImplementedException(); + } + + public void DeleteDirectory(string path) + { + throw new NotImplementedException(); + } + + public void DeleteFile(string path) + { + throw new NotImplementedException(); + } + + public IDirectory OpenDirectory(string path, OpenDirectoryMode mode) + { + path = PathTools.Normalize(path); + + IDirectory baseDir = BaseFileSystem.OpenDirectory(path, mode); + + var dir = new AesXtsDirectory(this, baseDir, mode); + return dir; + } + + public IFile OpenFile(string path, OpenMode mode) + { + path = PathTools.Normalize(path); + + IFile baseFile = BaseFileSystem.OpenFile(path, mode); + var file = new AesXtsFile(mode, baseFile, path, KekSource, ValidationKey, BlockSize); + return file; + } + + public void RenameDirectory(string srcPath, string dstPath) + { + throw new NotImplementedException(); + } + + public void RenameFile(string srcPath, string dstPath) + { + throw new NotImplementedException(); + } + + public bool DirectoryExists(string path) + { + throw new NotImplementedException(); + } + + public bool FileExists(string path) + { + throw new NotImplementedException(); + } + + public void Commit() + { + throw new NotImplementedException(); + } + } +} diff --git a/src/LibHac/IO/StorageFile.cs b/src/LibHac/IO/StorageFile.cs index 24fab335..f356659f 100644 --- a/src/LibHac/IO/StorageFile.cs +++ b/src/LibHac/IO/StorageFile.cs @@ -23,6 +23,8 @@ namespace LibHac.IO public override void Write(ReadOnlySpan source, long offset) { + ValidateWriteParams(source, offset); + BaseStorage.Write(source, offset); }