diff --git a/src/LibHac/Crypto.cs b/src/LibHac/Crypto.cs index c0f5201c..6d7689fd 100644 --- a/src/LibHac/Crypto.cs +++ b/src/LibHac/Crypto.cs @@ -44,6 +44,21 @@ namespace LibHac public static void DecryptEcb(byte[] key, byte[] src, byte[] dest, int length) => DecryptEcb(key, src, 0, dest, 0, length); + public static void EncryptEcb(byte[] key, byte[] src, int srcIndex, byte[] dest, int destIndex, int length) + { + using (Aes aes = Aes.Create()) + { + if (aes == null) throw new CryptographicException("Unable to create AES object"); + aes.Key = key; + aes.Mode = CipherMode.ECB; + aes.Padding = PaddingMode.None; + Array.Copy(aes.CreateEncryptor().TransformFinalBlock(src, srcIndex, length), 0, dest, destIndex, length); + } + } + + public static void EncryptEcb(byte[] key, byte[] src, byte[] dest, int length) => + EncryptEcb(key, src, 0, dest, 0, length); + public static void DecryptCbc(byte[] key, byte[] iv, byte[] src, int srcIndex, byte[] dest, int destIndex, int length) { using (Aes aes = Aes.Create()) diff --git a/src/LibHac/IO/AesXtsDirectory.cs b/src/LibHac/IO/AesXtsDirectory.cs index 1751c6a4..afb1acba 100644 --- a/src/LibHac/IO/AesXtsDirectory.cs +++ b/src/LibHac/IO/AesXtsDirectory.cs @@ -46,9 +46,9 @@ namespace LibHac.IO { using (IFile file = BaseFileSystem.OpenFile(path, OpenMode.Read)) { - var buffer = new byte[8]; + var buffer = new byte[8]; - file.Read(buffer, 0); + file.Read(buffer, 0x20); if (BitConverter.ToUInt32(buffer, 0) != 0x3058414E) return 0; file.Read(buffer, 0x48); diff --git a/src/LibHac/IO/AesXtsFile.cs b/src/LibHac/IO/AesXtsFile.cs index 8c7d44fb..6fb98bc4 100644 --- a/src/LibHac/IO/AesXtsFile.cs +++ b/src/LibHac/IO/AesXtsFile.cs @@ -1,7 +1,4 @@ using System; -using System.IO; -using System.Security.Cryptography; -using System.Text; namespace LibHac.IO { @@ -13,14 +10,10 @@ namespace LibHac.IO 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 AesXtsFileHeader Header { get; } private IStorage BaseStorage { get; } - private const int HeaderLength = 0x4000; + internal const int HeaderLength = 0x4000; public AesXtsFile(OpenMode mode, IFile baseFile, string path, ReadOnlySpan kekSeed, ReadOnlySpan verificationKey, int blockSize) { @@ -31,60 +24,15 @@ namespace LibHac.IO VerificationKey = verificationKey.ToArray(); BlockSize = blockSize; - ReadHeader(BaseFile); + Header = new AesXtsFileHeader(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)) + if (!Header.TryDecryptHeader(Path, KekSeed, VerificationKey)) { - return; + throw new ArgumentException("NAX0 key derivation failed."); } - throw new ArgumentException("NAX0 key derivation failed."); + Storage encStorage = new FileStorage(BaseFile).Slice(HeaderLength, Header.Size); + BaseStorage = new CachedStorage(new Aes128XtsStorage(encStorage, Header.DecryptedKey1, Header.DecryptedKey2, BlockSize, true), 4, true); } public override int Read(Span destination, long offset) @@ -110,7 +58,7 @@ namespace LibHac.IO public override long GetSize() { - return Length; + return Header.Size; } public override void SetSize(long size) diff --git a/src/LibHac/IO/AesXtsFileHeader.cs b/src/LibHac/IO/AesXtsFileHeader.cs new file mode 100644 index 00000000..b77767d4 --- /dev/null +++ b/src/LibHac/IO/AesXtsFileHeader.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; + +namespace LibHac.IO +{ + public class AesXtsFileHeader + { + private const uint AesXtsFileMagic = 0x3058414E; + public byte[] Signature { get; set; } = new byte[0x20]; + public uint Magic { get; } + public byte[] EncryptedKey1 { get; } = new byte[0x10]; + public byte[] EncryptedKey2 { get; } = new byte[0x10]; + public long Size { get; } + + public byte[] DecryptedKey1 { get; } = new byte[0x10]; + public byte[] DecryptedKey2 { get; } = new byte[0x10]; + public byte[] Kek1 { get; } = new byte[0x10]; + public byte[] Kek2 { get; } = new byte[0x10]; + + public AesXtsFileHeader(IFile aesXtsFile) + { + var reader = new FileReader(aesXtsFile); + + reader.ReadBytes(Signature); + Magic = reader.ReadUInt32(); + reader.Position += 4; + reader.ReadBytes(EncryptedKey1); + reader.ReadBytes(EncryptedKey2); + Size = reader.ReadInt64(); + + if (Magic != AesXtsFileMagic) + { + throw new InvalidDataException("Invalid NAX0 magic value"); + } + } + + public AesXtsFileHeader(byte[] key1, byte[] key2, long fileSize, string path, byte[] kekSeed, byte[] verificationKey) + { + Array.Copy(key1, DecryptedKey1, 0x10); + Array.Copy(key2, DecryptedKey2, 0x10); + Magic = AesXtsFileMagic; + Size = fileSize; + + EncryptHeader(path, kekSeed, verificationKey); + } + + private void EncryptHeader(string path, byte[] kekSeed, byte[] verificationKey) + { + GenerateKek(kekSeed, path); + EncryptKeys(); + Signature = CalculateHmac(verificationKey); + } + + public bool TryDecryptHeader(string path, byte[] kekSeed, byte[] verificationKey) + { + GenerateKek(kekSeed, path); + DecryptKeys(); + + byte[] hmac = CalculateHmac(verificationKey); + return Util.ArraysEqual(hmac, Signature); + } + + private void DecryptKeys() + { + Crypto.DecryptEcb(Kek1, EncryptedKey1, DecryptedKey1, 0x10); + Crypto.DecryptEcb(Kek2, EncryptedKey2, DecryptedKey2, 0x10); + } + + private void EncryptKeys() + { + Crypto.EncryptEcb(Kek1, DecryptedKey1, EncryptedKey1, 0x10); + Crypto.EncryptEcb(Kek2, DecryptedKey2, EncryptedKey2, 0x10); + } + + private void GenerateKek(byte[] kekSeed, string path) + { + var hash = new HMACSHA256(kekSeed); + byte[] pathBytes = Encoding.ASCII.GetBytes(path); + + byte[] checksum = hash.ComputeHash(pathBytes, 0, pathBytes.Length); + Array.Copy(checksum, 0, Kek1, 0, 0x10); + Array.Copy(checksum, 0x10, Kek2, 0, 0x10); + } + + private byte[] CalculateHmac(byte[] key) + { + byte[] message = ToBytes(true).AsSpan(0x20).ToArray(); + var hash = new HMACSHA256(message); + + return hash.ComputeHash(key); + } + + public byte[] ToBytes(bool writeDecryptedKey) + { + uint magic = Magic; + long size = Size; + byte[] key1 = writeDecryptedKey ? DecryptedKey1 : EncryptedKey1; + byte[] key2 = writeDecryptedKey ? DecryptedKey2 : EncryptedKey2; + + var data = new byte[0x80]; + + Array.Copy(Signature, data, 0x20); + MemoryMarshal.Write(data.AsSpan(0x20), ref magic); + + Array.Copy(key1, 0, data, 0x28, 0x10); + Array.Copy(key2, 0, data, 0x38, 0x10); + + MemoryMarshal.Write(data.AsSpan(0x48), ref size); + + return data; + } + } +} diff --git a/src/LibHac/IO/AesXtsFileSystem.cs b/src/LibHac/IO/AesXtsFileSystem.cs index 27662686..e26953f7 100644 --- a/src/LibHac/IO/AesXtsFileSystem.cs +++ b/src/LibHac/IO/AesXtsFileSystem.cs @@ -33,7 +33,15 @@ namespace LibHac.IO public void CreateFile(string path, long size, CreateFileOptions options) { - throw new NotImplementedException(); + long containerSize = AesXtsFile.HeaderLength + Util.AlignUp(size, 0x16); + BaseFileSystem.CreateFile(path, containerSize, options); + + var header = new AesXtsFileHeader(new byte[0x10], new byte[0x10], size, path, KekSource, ValidationKey); + + using (IFile baseFile = BaseFileSystem.OpenFile(path, OpenMode.Write)) + { + baseFile.Write(header.ToBytes(false), 0); + } } public void DeleteDirectory(string path) @@ -60,8 +68,10 @@ namespace LibHac.IO { path = PathTools.Normalize(path); - IFile baseFile = BaseFileSystem.OpenFile(path, mode); + IFile baseFile = BaseFileSystem.OpenFile(path, mode | OpenMode.Read); var file = new AesXtsFile(mode, baseFile, path, KekSource, ValidationKey, BlockSize); + + file.ToDispose.Add(baseFile); return file; } diff --git a/src/LibHac/IO/FileBase.cs b/src/LibHac/IO/FileBase.cs index d975df5d..ab280fd5 100644 --- a/src/LibHac/IO/FileBase.cs +++ b/src/LibHac/IO/FileBase.cs @@ -6,7 +6,7 @@ namespace LibHac.IO public abstract class FileBase : IFile { private bool _isDisposed; - protected List ToDispose { get; } = new List(); + internal List ToDispose { get; } = new List(); public abstract int Read(Span destination, long offset); public abstract void Write(ReadOnlySpan source, long offset); diff --git a/src/LibHac/IO/FileReader.cs b/src/LibHac/IO/FileReader.cs new file mode 100644 index 00000000..4639c61d --- /dev/null +++ b/src/LibHac/IO/FileReader.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics; +using System.Runtime.InteropServices; + +namespace LibHac.IO +{ + public class FileReader + { + private const int BufferSize = 0x10; + private IFile _file; + private byte[] _buffer; + private long _start; + + public long Position { get; set; } + + public FileReader(IFile file) + { + _file = file; + _buffer = new byte[BufferSize]; + } + + public FileReader(IFile file, long start) + { + _file = file; + _start = start; + _buffer = new byte[BufferSize]; + } + + private void FillBuffer(long offset, int count, bool updatePosition) + { + Debug.Assert(count <= BufferSize); + + _file.Read(_buffer.AsSpan(0, count), _start + offset); + if (updatePosition) Position = offset + count; + } + + public byte ReadUInt8(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(byte), updatePosition); + + return _buffer[0]; + } + + public sbyte ReadInt8(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(sbyte), updatePosition); + + return (sbyte)_buffer[0]; + } + + public ushort ReadUInt16(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(ushort), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public short ReadInt16(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(short), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public uint ReadUInt32(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(uint), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public int ReadInt32(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(int), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public ulong ReadUInt64(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(ulong), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public long ReadInt64(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(long), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public float ReadSingle(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(float), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public double ReadDouble(long offset, bool updatePosition) + { + FillBuffer(offset, sizeof(double), updatePosition); + + return MemoryMarshal.Read(_buffer); + } + + public byte[] ReadBytes(long offset, int length, bool updatePosition) + { + var result = new byte[length]; + _file.Read(result, offset); + + if (updatePosition) Position = offset + length; + return result; + } + + public void ReadBytes(Span destination, long offset, bool updatePosition) + { + _file.Read(destination, offset); + + if (updatePosition) Position = offset + destination.Length; + } + + public byte ReadUInt8(long offset) => ReadUInt8(offset, true); + public sbyte ReadInt8(long offset) => ReadInt8(offset, true); + public ushort ReadUInt16(long offset) => ReadUInt16(offset, true); + public short ReadInt16(long offset) => ReadInt16(offset, true); + public uint ReadUInt32(long offset) => ReadUInt32(offset, true); + public int ReadInt32(long offset) => ReadInt32(offset, true); + public ulong ReadUInt64(long offset) => ReadUInt64(offset, true); + public long ReadInt64(long offset) => ReadInt64(offset, true); + public float ReadSingle(long offset) => ReadSingle(offset, true); + public double ReadDouble(long offset) => ReadDouble(offset, true); + public byte[] ReadBytes(long offset, int length) => ReadBytes(offset, length, true); + public void ReadBytes(Span destination, long offset) => ReadBytes(destination, offset, true); + + public byte ReadUInt8() => ReadUInt8(Position, true); + public sbyte ReadInt8() => ReadInt8(Position, true); + public ushort ReadUInt16() => ReadUInt16(Position, true); + public short ReadInt16() => ReadInt16(Position, true); + public uint ReadUInt32() => ReadUInt32(Position, true); + public int ReadInt32() => ReadInt32(Position, true); + public ulong ReadUInt64() => ReadUInt64(Position, true); + public long ReadInt64() => ReadInt64(Position, true); + public float ReadSingle() => ReadSingle(Position, true); + public double ReadDouble() => ReadDouble(Position, true); + public byte[] ReadBytes(int length) => ReadBytes(Position, length, true); + public void ReadBytes(Span destination) => ReadBytes(destination, Position, true); + } +} diff --git a/src/LibHac/IO/HierarchicalRomFileTable.cs b/src/LibHac/IO/HierarchicalRomFileTable.cs deleted file mode 100644 index ca6b77f2..00000000 --- a/src/LibHac/IO/HierarchicalRomFileTable.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace LibHac.IO -{ - public class HierarchicalRomFileTable - { - - } -}