Add support for NCA0 files.

Any operation that is supported with other NCAs is supported NCA0s. This includes reading both encrypted and decrypted NCA0s, and encrypting and decrypting NCA0s.
This commit is contained in:
Alex Barney 2020-08-13 22:09:36 -07:00
parent 6bab1d9273
commit f20337d774
10 changed files with 406 additions and 61 deletions

View File

@ -90,10 +90,10 @@ namespace LibHac
} }
} }
internal static BigInteger GetBigInteger(byte[] bytes) internal static BigInteger GetBigInteger(ReadOnlySpan<byte> bytes)
{ {
var signPadded = new byte[bytes.Length + 1]; var signPadded = new byte[bytes.Length + 1];
Buffer.BlockCopy(bytes, 0, signPadded, 1, bytes.Length); bytes.CopyTo(signPadded.AsSpan(1));
Array.Reverse(signPadded); Array.Reverse(signPadded);
return new BigInteger(signPadded); return new BigInteger(signPadded);
} }
@ -142,11 +142,18 @@ namespace LibHac
{ {
using (var rsa = RSA.Create()) using (var rsa = RSA.Create())
{ {
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus }); try
{
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus });
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1) return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1)
? Validity.Valid ? Validity.Valid
: Validity.Invalid; : Validity.Invalid;
}
catch (CryptographicException)
{
return Validity.Invalid;
}
} }
} }
@ -154,20 +161,54 @@ namespace LibHac
{ {
using (var rsa = RSA.Create()) using (var rsa = RSA.Create())
{ {
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus }); try
{
rsa.ImportParameters(new RSAParameters { Exponent = new byte[] { 1, 0, 1 }, Modulus = modulus });
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss) return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss)
? Validity.Valid ? Validity.Valid
: Validity.Invalid; : Validity.Invalid;
}
catch (CryptographicException)
{
return Validity.Invalid;
}
} }
} }
public static byte[] DecryptTitleKey(byte[] titleKeyblock, RSAParameters rsaParams) public static byte[] DecryptRsaOaep(byte[] data, RSAParameters rsaParams)
{ {
var rsa = RSA.Create(); var rsa = RSA.Create();
rsa.ImportParameters(rsaParams); rsa.ImportParameters(rsaParams);
return rsa.Decrypt(titleKeyblock, RSAEncryptionPadding.OaepSHA256); return rsa.Decrypt(data, RSAEncryptionPadding.OaepSHA256);
}
public static bool DecryptRsaOaep(ReadOnlySpan<byte> data, Span<byte> destination, RSAParameters rsaParams, out int bytesWritten)
{
using (var rsa = RSA.Create())
{
try
{
rsa.ImportParameters(rsaParams);
return rsa.TryDecrypt(data, destination, RSAEncryptionPadding.OaepSHA256, out bytesWritten);
}
catch (CryptographicException)
{
bytesWritten = 0;
return false;
}
}
}
public static RSAParameters RecoverRsaParameters(ReadOnlySpan<byte> modulus, ReadOnlySpan<byte> exponent)
{
BigInteger dInt = GetBigInteger(exponent);
BigInteger nInt = GetBigInteger(modulus);
BigInteger eInt = GetBigInteger(new byte[] { 1, 0, 1 });
return RecoverRsaParameters(nInt, eInt, dInt);
} }
private static RSAParameters RecoverRsaParameters(BigInteger n, BigInteger e, BigInteger d) private static RSAParameters RecoverRsaParameters(BigInteger n, BigInteger e, BigInteger d)

View File

@ -40,7 +40,7 @@ namespace LibHac.FsSrv.Creators
} }
storage = storageTemp; storage = storageTemp;
fsHeader = nca.Header.GetFsHeader(fsIndex); fsHeader = nca.GetFsHeader(fsIndex);
return Result.Success; return Result.Success;
} }

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using LibHac.Common; using LibHac.Common;
using LibHac.Diag;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
using LibHac.FsSystem.RomFs; using LibHac.FsSystem.RomFs;
@ -14,6 +15,10 @@ namespace LibHac.FsSystem.NcaUtils
{ {
private Keyset Keyset { get; } private Keyset Keyset { get; }
private bool IsEncrypted { get; set; } private bool IsEncrypted { get; set; }
private byte[] Nca0KeyArea { get; set; }
private IStorage Nca0TransformedBody { get; set; }
public IStorage BaseStorage { get; } public IStorage BaseStorage { get; }
public NcaHeader Header { get; } public NcaHeader Header { get; }
@ -33,6 +38,12 @@ namespace LibHac.FsSystem.NcaUtils
{ {
if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index)); if (index < 0 || index > 3) throw new ArgumentOutOfRangeException(nameof(index));
// Handle old NCA0s that use different key area encryption
if (Header.FormatVersion == NcaVersion.Nca0FixedKey || Header.FormatVersion == NcaVersion.Nca0RsaOaep)
{
return GetDecryptedKeyAreaNca0().AsSpan(0x10 * index, 0x10).ToArray();
}
int keyRevision = Utilities.GetMasterKeyRevision(Header.KeyGeneration); int keyRevision = Utilities.GetMasterKeyRevision(Header.KeyGeneration);
byte[] keyAreaKey = Keyset.KeyAreaKeys[keyRevision][Header.KeyAreaKeyIndex]; byte[] keyAreaKey = Keyset.KeyAreaKeys[keyRevision][Header.KeyAreaKeyIndex];
@ -94,7 +105,7 @@ namespace LibHac.FsSystem.NcaUtils
public bool CanOpenSection(int index) public bool CanOpenSection(int index)
{ {
if (!SectionExists(index)) return false; if (!SectionExists(index)) return false;
if (Header.GetFsHeader(index).EncryptionType == NcaEncryptionType.None) return true; if (GetFsHeader(index).EncryptionType == NcaEncryptionType.None) return true;
int keyRevision = Utilities.GetMasterKeyRevision(Header.KeyGeneration); int keyRevision = Utilities.GetMasterKeyRevision(Header.KeyGeneration);
@ -122,6 +133,14 @@ namespace LibHac.FsSystem.NcaUtils
return Header.IsSectionEnabled(index); return Header.IsSectionEnabled(index);
} }
public NcaFsHeader GetFsHeader(int index)
{
if (Header.IsNca0())
return GetNca0FsHeader(index);
return Header.GetFsHeader(index);
}
private IStorage OpenSectionStorage(int index) private IStorage OpenSectionStorage(int index)
{ {
if (!SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing); if (!SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing);
@ -142,7 +161,7 @@ namespace LibHac.FsSystem.NcaUtils
private IStorage OpenDecryptedStorage(IStorage baseStorage, int index, bool decrypting) private IStorage OpenDecryptedStorage(IStorage baseStorage, int index, bool decrypting)
{ {
NcaFsHeader header = Header.GetFsHeader(index); NcaFsHeader header = GetFsHeader(index);
switch (header.EncryptionType) switch (header.EncryptionType)
{ {
@ -174,7 +193,7 @@ namespace LibHac.FsSystem.NcaUtils
private IStorage OpenAesCtrStorage(IStorage baseStorage, int index) private IStorage OpenAesCtrStorage(IStorage baseStorage, int index)
{ {
NcaFsHeader fsHeader = Header.GetFsHeader(index); NcaFsHeader fsHeader = GetFsHeader(index);
byte[] key = GetContentKey(NcaKeyType.AesCtr); byte[] key = GetContentKey(NcaKeyType.AesCtr);
byte[] counter = Aes128CtrStorage.CreateCounter(fsHeader.Counter, Header.GetSectionStartOffset(index)); byte[] counter = Aes128CtrStorage.CreateCounter(fsHeader.Counter, Header.GetSectionStartOffset(index));
@ -184,7 +203,7 @@ namespace LibHac.FsSystem.NcaUtils
private IStorage OpenAesCtrExStorage(IStorage baseStorage, int index, bool decrypting) private IStorage OpenAesCtrExStorage(IStorage baseStorage, int index, bool decrypting)
{ {
NcaFsHeader fsHeader = Header.GetFsHeader(index); NcaFsHeader fsHeader = GetFsHeader(index);
NcaFsPatchInfo info = fsHeader.GetPatchInfo(); NcaFsPatchInfo info = fsHeader.GetPatchInfo();
long sectionOffset = Header.GetSectionStartOffset(index); long sectionOffset = Header.GetSectionStartOffset(index);
@ -233,6 +252,9 @@ namespace LibHac.FsSystem.NcaUtils
public IStorage OpenRawStorage(int index, bool openEncrypted) public IStorage OpenRawStorage(int index, bool openEncrypted)
{ {
if (Header.IsNca0())
return OpenNca0RawStorage(index, openEncrypted);
IStorage storage = OpenSectionStorage(index); IStorage storage = OpenSectionStorage(index);
if (IsEncrypted == openEncrypted) if (IsEncrypted == openEncrypted)
@ -255,7 +277,7 @@ namespace LibHac.FsSystem.NcaUtils
patchStorage.GetSize(out long patchSize).ThrowIfFailure(); patchStorage.GetSize(out long patchSize).ThrowIfFailure();
baseStorage.GetSize(out long baseSize).ThrowIfFailure(); baseStorage.GetSize(out long baseSize).ThrowIfFailure();
NcaFsHeader header = patchNca.Header.GetFsHeader(index); NcaFsHeader header = patchNca.GetFsHeader(index);
NcaFsPatchInfo patchInfo = header.GetPatchInfo(); NcaFsPatchInfo patchInfo = header.GetPatchInfo();
if (patchInfo.RelocationTreeSize == 0) if (patchInfo.RelocationTreeSize == 0)
@ -286,7 +308,7 @@ namespace LibHac.FsSystem.NcaUtils
public IStorage OpenStorage(int index, IntegrityCheckLevel integrityCheckLevel) public IStorage OpenStorage(int index, IntegrityCheckLevel integrityCheckLevel)
{ {
IStorage rawStorage = OpenRawStorage(index); IStorage rawStorage = OpenRawStorage(index);
NcaFsHeader header = Header.GetFsHeader(index); NcaFsHeader header = GetFsHeader(index);
if (header.EncryptionType == NcaEncryptionType.AesCtrEx) if (header.EncryptionType == NcaEncryptionType.AesCtrEx)
{ {
@ -307,7 +329,7 @@ namespace LibHac.FsSystem.NcaUtils
public IStorage OpenStorageWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel) public IStorage OpenStorageWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel)
{ {
IStorage rawStorage = OpenRawStorageWithPatch(patchNca, index); IStorage rawStorage = OpenRawStorageWithPatch(patchNca, index);
NcaFsHeader header = patchNca.Header.GetFsHeader(index); NcaFsHeader header = patchNca.GetFsHeader(index);
switch (header.HashType) switch (header.HashType)
{ {
@ -323,7 +345,7 @@ namespace LibHac.FsSystem.NcaUtils
public IFileSystem OpenFileSystem(int index, IntegrityCheckLevel integrityCheckLevel) public IFileSystem OpenFileSystem(int index, IntegrityCheckLevel integrityCheckLevel)
{ {
IStorage storage = OpenStorage(index, integrityCheckLevel); IStorage storage = OpenStorage(index, integrityCheckLevel);
NcaFsHeader header = Header.GetFsHeader(index); NcaFsHeader header = GetFsHeader(index);
return OpenFileSystem(storage, header); return OpenFileSystem(storage, header);
} }
@ -331,7 +353,7 @@ namespace LibHac.FsSystem.NcaUtils
public IFileSystem OpenFileSystemWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel) public IFileSystem OpenFileSystemWithPatch(Nca patchNca, int index, IntegrityCheckLevel integrityCheckLevel)
{ {
IStorage storage = OpenStorageWithPatch(patchNca, index, integrityCheckLevel); IStorage storage = OpenStorageWithPatch(patchNca, index, integrityCheckLevel);
NcaFsHeader header = patchNca.Header.GetFsHeader(index); NcaFsHeader header = patchNca.GetFsHeader(index);
return OpenFileSystem(storage, header); return OpenFileSystem(storage, header);
} }
@ -392,6 +414,12 @@ namespace LibHac.FsSystem.NcaUtils
var builder = new ConcatenationStorageBuilder(); var builder = new ConcatenationStorageBuilder();
builder.Add(OpenHeaderStorage(openEncrypted), 0); builder.Add(OpenHeaderStorage(openEncrypted), 0);
if (Header.IsNca0())
{
builder.Add(OpenNca0BodyStorage(openEncrypted), 0x400);
return builder.Build();
}
for (int i = 0; i < NcaHeader.SectionCount; i++) for (int i = 0; i < NcaHeader.SectionCount; i++)
{ {
if (Header.IsSectionEnabled(i)) if (Header.IsSectionEnabled(i))
@ -566,6 +594,9 @@ namespace LibHac.FsSystem.NcaUtils
case 2: case 2:
header = OpenNca2Header(headerSize, !openEncrypted); header = OpenNca2Header(headerSize, !openEncrypted);
break; break;
case 0:
header = new CachedStorage(new Aes128XtsStorage(BaseStorage.Slice(0, 0x400), Keyset.HeaderKey, NcaHeader.HeaderSectorSize, true, !openEncrypted), 1, true);
break;
default: default:
throw new NotSupportedException("Unsupported NCA version"); throw new NotSupportedException("Unsupported NCA version");
} }
@ -588,6 +619,96 @@ namespace LibHac.FsSystem.NcaUtils
return new ConcatenationStorage(sources, true); return new ConcatenationStorage(sources, true);
} }
private byte[] GetDecryptedKeyAreaNca0()
{
if (Nca0KeyArea != null)
return Nca0KeyArea;
if (Header.FormatVersion == NcaVersion.Nca0FixedKey)
{
Nca0KeyArea = Header.GetKeyArea().ToArray();
}
else if (Header.FormatVersion == NcaVersion.Nca0RsaOaep)
{
Span<byte> keyArea = Header.GetKeyArea();
var decKeyArea = new byte[0x100];
if (CryptoOld.DecryptRsaOaep(keyArea, decKeyArea, Keyset.Nca0RsaKeyAreaKey, out _))
{
Nca0KeyArea = decKeyArea;
}
else
{
throw new InvalidDataException("Unable to decrypt NCA0 key area.");
}
}
else
{
throw new NotSupportedException();
}
return Nca0KeyArea;
}
private IStorage OpenNca0BodyStorage(bool openEncrypted)
{
// NCA0 encrypts the entire NCA body using AES-XTS instead of
// using different encryption types and IVs for each section.
Assert.Equal(0, Header.Version);
if (openEncrypted == IsEncrypted)
{
return GetRawStorage();
}
if (Nca0TransformedBody != null)
return Nca0TransformedBody;
byte[] key0 = GetContentKey(NcaKeyType.AesXts0);
byte[] key1 = GetContentKey(NcaKeyType.AesXts1);
Nca0TransformedBody = new CachedStorage(new Aes128XtsStorage(GetRawStorage(), key0, key1, NcaHeader.HeaderSectorSize, true, !openEncrypted), 1, true);
return Nca0TransformedBody;
IStorage GetRawStorage()
{
BaseStorage.GetSize(out long ncaSize).ThrowIfFailure();
return BaseStorage.Slice(0x400, ncaSize - 0x400);
}
}
private IStorage OpenNca0RawStorage(int index, bool openEncrypted)
{
if (!SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing);
long offset = Header.GetSectionStartOffset(index) - 0x400;
long size = Header.GetSectionSize(index);
IStorage bodyStorage = OpenNca0BodyStorage(openEncrypted);
bodyStorage.GetSize(out long baseSize).ThrowIfFailure();
if (!Utilities.IsSubRange(offset, size, baseSize))
{
throw new InvalidDataException(
$"Section offset (0x{offset + 0x400:x}) and length (0x{size:x}) fall outside the total NCA length (0x{baseSize + 0x400:x}).");
}
return new SubStorage(bodyStorage, offset, size);
}
public NcaFsHeader GetNca0FsHeader(int index)
{
// NCA0 stores the FS header in the first block of the section instead of the header
IStorage bodyStorage = OpenNca0BodyStorage(false);
long offset = Header.GetSectionStartOffset(index) - 0x400;
var fsHeaderData = new byte[0x200];
bodyStorage.Read(offset, fsHeaderData).ThrowIfFailure();
return new NcaFsHeader(fsHeaderData);
}
public Validity VerifyHeaderSignature() public Validity VerifyHeaderSignature()
{ {
return Header.VerifySignature1(Keyset.NcaHdrFixedKeyModulus); return Header.VerifySignature1(Keyset.NcaHdrFixedKeyModulus);
@ -598,7 +719,7 @@ namespace LibHac.FsSystem.NcaUtils
int counterType; int counterType;
int counterVersion; int counterVersion;
NcaFsHeader header = Header.GetFsHeader(sectionIndex); NcaFsHeader header = GetFsHeader(sectionIndex);
if (header.EncryptionType != NcaEncryptionType.AesCtr && if (header.EncryptionType != NcaEncryptionType.AesCtr &&
header.EncryptionType != NcaEncryptionType.AesCtrEx) return; header.EncryptionType != NcaEncryptionType.AesCtrEx) return;

View File

@ -56,7 +56,7 @@ namespace LibHac.FsSystem.NcaUtils
if (!nca.SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing); if (!nca.SectionExists(index)) throw new ArgumentException(nameof(index), Messages.NcaSectionMissing);
if (!nca.CanOpenSection(index)) return Validity.MissingKey; if (!nca.CanOpenSection(index)) return Validity.MissingKey;
NcaFsHeader header = nca.Header.GetFsHeader(index); NcaFsHeader header = nca.GetFsHeader(index);
// The base data is needed to validate the hash, so use a trick involving the AES-CTR extended // The base data is needed to validate the hash, so use a trick involving the AES-CTR extended
// encryption table to check if the decryption is invalid. // encryption table to check if the decryption is invalid.
@ -116,7 +116,7 @@ namespace LibHac.FsSystem.NcaUtils
Debug.Assert(nca.CanOpenSection(index)); Debug.Assert(nca.CanOpenSection(index));
NcaFsPatchInfo header = nca.Header.GetFsHeader(index).GetPatchInfo(); NcaFsPatchInfo header = nca.GetFsHeader(index).GetPatchInfo();
IStorage decryptedStorage = nca.OpenRawStorage(index); IStorage decryptedStorage = nca.OpenRawStorage(index);
Span<byte> buffer = stackalloc byte[sizeof(long)]; Span<byte> buffer = stackalloc byte[sizeof(long)];
@ -145,7 +145,7 @@ namespace LibHac.FsSystem.NcaUtils
public static Validity VerifySection(this Nca nca, int index, IProgressReport logger = null, bool quiet = false) public static Validity VerifySection(this Nca nca, int index, IProgressReport logger = null, bool quiet = false)
{ {
NcaFsHeader sect = nca.Header.GetFsHeader(index); NcaFsHeader sect = nca.GetFsHeader(index);
NcaHashType hashType = sect.HashType; NcaHashType hashType = sect.HashType;
if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked; if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked;
@ -176,7 +176,7 @@ namespace LibHac.FsSystem.NcaUtils
public static Validity VerifySection(this Nca nca, Nca patchNca, int index, IProgressReport logger = null, bool quiet = false) public static Validity VerifySection(this Nca nca, Nca patchNca, int index, IProgressReport logger = null, bool quiet = false)
{ {
NcaFsHeader sect = nca.Header.GetFsHeader(index); NcaFsHeader sect = nca.GetFsHeader(index);
NcaHashType hashType = sect.HashType; NcaHashType hashType = sect.HashType;
if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked; if (hashType != NcaHashType.Sha256 && hashType != NcaHashType.Ivfc) return Validity.Unchecked;

View File

@ -2,6 +2,7 @@
using System.IO; using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Crypto; using LibHac.Crypto;
using LibHac.Fs; using LibHac.Fs;
@ -16,15 +17,22 @@ namespace LibHac.FsSystem.NcaUtils
private readonly Memory<byte> _header; private readonly Memory<byte> _header;
public NcaVersion FormatVersion { get; private set; }
public NcaHeader(IStorage headerStorage) public NcaHeader(IStorage headerStorage)
{ {
_header = new byte[HeaderSize]; _header = new byte[HeaderSize];
headerStorage.Read(0, _header.Span); Span<byte> headerSpan = _header.Span;
headerStorage.Read(0, headerSpan).ThrowIfFailure();
FormatVersion = DetectNcaVersion(headerSpan);
} }
public NcaHeader(Keyset keyset, IStorage headerStorage) public NcaHeader(Keyset keyset, IStorage headerStorage)
{ {
_header = DecryptHeader(keyset, headerStorage); _header = DecryptHeader(keyset, headerStorage);
FormatVersion = DetectNcaVersion(_header.Span);
} }
private ref NcaHeaderStruct Header => ref Unsafe.As<byte, NcaHeaderStruct>(ref _header.Span[0]); private ref NcaHeaderStruct Header => ref Unsafe.As<byte, NcaHeaderStruct>(ref _header.Span[0]);
@ -104,6 +112,11 @@ namespace LibHac.FsSystem.NcaUtils
public bool HasRightsId => !Utilities.IsEmpty(RightsId); public bool HasRightsId => !Utilities.IsEmpty(RightsId);
public Span<byte> GetKeyArea()
{
return _header.Span.Slice(NcaHeaderStruct.KeyAreaOffset, NcaHeaderStruct.KeyAreaSize);
}
private ref NcaSectionEntryStruct GetSectionEntry(int index) private ref NcaSectionEntryStruct GetSectionEntry(int index)
{ {
ValidateSectionIndex(index); ValidateSectionIndex(index);
@ -161,7 +174,6 @@ namespace LibHac.FsSystem.NcaUtils
Span<byte> expectedHash = GetFsHeaderHash(index); Span<byte> expectedHash = GetFsHeaderHash(index);
int offset = NcaHeaderStruct.FsHeadersOffset + NcaHeaderStruct.FsHeaderSize * index; int offset = NcaHeaderStruct.FsHeadersOffset + NcaHeaderStruct.FsHeaderSize * index;
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
Memory<byte> headerData = _header.Slice(offset, NcaHeaderStruct.FsHeaderSize); Memory<byte> headerData = _header.Slice(offset, NcaHeaderStruct.FsHeaderSize);
Span<byte> actualHash = stackalloc byte[Sha256.DigestSize]; Span<byte> actualHash = stackalloc byte[Sha256.DigestSize];
@ -223,7 +235,7 @@ namespace LibHac.FsSystem.NcaUtils
transform.TransformBlock(buf, i, HeaderSectorSize, 0); transform.TransformBlock(buf, i, HeaderSectorSize, 0);
} }
} }
else else if (version != 0)
{ {
throw new NotSupportedException($"NCA version {version} is not supported."); throw new NotSupportedException($"NCA version {version} is not supported.");
} }
@ -231,6 +243,39 @@ namespace LibHac.FsSystem.NcaUtils
return buf; return buf;
} }
private static NcaVersion DetectNcaVersion(ReadOnlySpan<byte> header)
{
int version = header[0x203] - '0';
if (version == 3) return NcaVersion.Nca3;
if (version == 2) return NcaVersion.Nca2;
if (version != 0) return NcaVersion.Unknown;
// There are multiple versions of NCA0 that each encrypt the key area differently.
// Examine the key area to detect which version this NCA is.
ReadOnlySpan<byte> keyArea = header.Slice(NcaHeaderStruct.KeyAreaOffset, NcaHeaderStruct.KeyAreaSize);
// The end of the key area will only be non-zero if it's RSA-OAEP encrypted
var zeros = new Buffer16();
if (!keyArea.Slice(0x80, 0x10).SequenceEqual(zeros))
{
return NcaVersion.Nca0RsaOaep;
}
// Key areas using fixed, unencrypted keys always use the same keys.
// Check for these keys by comparing the key area with the known hash of the fixed body keys.
Buffer32 hash = default;
Sha256.GenerateSha256Hash(keyArea.Slice(0, 0x20), hash);
if (Nca0FixedBodyKeySha256Hash.SequenceEqual(hash))
{
return NcaVersion.Nca0FixedKey;
}
// Otherwise the key area is encrypted the same as modern NCAs.
return NcaVersion.Nca0;
}
public Validity VerifySignature1(byte[] modulus) public Validity VerifySignature1(byte[] modulus)
{ {
return CryptoOld.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature1.ToArray(), modulus); return CryptoOld.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature1.ToArray(), modulus);
@ -241,6 +286,14 @@ namespace LibHac.FsSystem.NcaUtils
return CryptoOld.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature2.ToArray(), modulus); return CryptoOld.Rsa2048PssVerify(_header.Span.Slice(0x200, 0x200).ToArray(), Signature2.ToArray(), modulus);
} }
public bool IsNca0() => FormatVersion >= NcaVersion.Nca0;
private static ReadOnlySpan<byte> Nca0FixedBodyKeySha256Hash => new byte[]
{
0x9A, 0xBB, 0xD2, 0x11, 0x86, 0x00, 0x21, 0x9D, 0x7A, 0xDC, 0x5B, 0x43, 0x95, 0xF8, 0x4E, 0xFD,
0xFF, 0x6B, 0x25, 0xEF, 0x9F, 0x96, 0x85, 0x28, 0x18, 0x9E, 0x76, 0xB0, 0x92, 0xF0, 0x6A, 0xCB
};
[StructLayout(LayoutKind.Explicit, Size = 0xC00)] [StructLayout(LayoutKind.Explicit, Size = 0xC00)]
private struct NcaHeaderStruct private struct NcaHeaderStruct
{ {
@ -250,6 +303,7 @@ namespace LibHac.FsSystem.NcaUtils
public const int FsHeaderHashOffset = 0x280; public const int FsHeaderHashOffset = 0x280;
public const int FsHeaderHashSize = 0x20; public const int FsHeaderHashSize = 0x20;
public const int KeyAreaOffset = 0x300; public const int KeyAreaOffset = 0x300;
public const int KeyAreaSize = 0x100;
public const int FsHeadersOffset = 0x400; public const int FsHeadersOffset = 0x400;
public const int FsHeaderSize = 0x200; public const int FsHeaderSize = 0x200;
@ -277,4 +331,14 @@ namespace LibHac.FsSystem.NcaUtils
public bool IsEnabled; public bool IsEnabled;
} }
} }
public enum NcaVersion
{
Unknown,
Nca3,
Nca2,
Nca0,
Nca0FixedKey,
Nca0RsaOaep
}
} }

View File

@ -1,4 +1,5 @@
using LibHac.Common; using System;
using LibHac.Common;
using LibHac.Fs; using LibHac.Fs;
using LibHac.Fs.Fsa; using LibHac.Fs.Fsa;
@ -125,16 +126,30 @@ namespace LibHac.FsSystem.RomFs
{ {
var reader = new FileReader(file); var reader = new FileReader(file);
HeaderSize = reader.ReadInt64(); HeaderSize = reader.ReadInt32();
DirHashTableOffset = reader.ReadInt64();
DirHashTableSize = reader.ReadInt64(); Func<long> func;
DirMetaTableOffset = reader.ReadInt64();
DirMetaTableSize = reader.ReadInt64(); // Old pre-release romfs is exactly the same except the fields in the header are 32-bit instead of 64-bit
FileHashTableOffset = reader.ReadInt64(); if (HeaderSize == 0x28)
FileHashTableSize = reader.ReadInt64(); {
FileMetaTableOffset = reader.ReadInt64(); func = () => reader.ReadInt32();
FileMetaTableSize = reader.ReadInt64(); }
DataOffset = reader.ReadInt64(); else
{
func = reader.ReadInt64;
reader.Position += 4;
}
DirHashTableOffset = func();
DirHashTableSize = func();
DirMetaTableOffset = func();
DirMetaTableSize = func();
FileHashTableOffset = func();
FileHashTableSize = func();
FileMetaTableOffset = func();
FileMetaTableSize = func();
DataOffset = func();
} }
} }
} }

View File

@ -72,6 +72,20 @@ namespace LibHac
public RSAParameters EticketExtKeyRsa { get; set; } public RSAParameters EticketExtKeyRsa { get; set; }
private RSAParameters? _nca0RsaKeyAreaKey;
public RSAParameters Nca0RsaKeyAreaKey
{
// Lazily init this key
get
{
if (_nca0RsaKeyAreaKey.HasValue)
return _nca0RsaKeyAreaKey.Value;
_nca0RsaKeyAreaKey = CryptoOld.RecoverRsaParameters(BetaNca0Modulus, BetaNca0Exponent);
return _nca0RsaKeyAreaKey.Value;
}
}
public bool KeysetForDev; public bool KeysetForDev;
public byte[] NcaHdrFixedKeyModulus public byte[] NcaHdrFixedKeyModulus
{ {
@ -118,6 +132,46 @@ namespace LibHac
} }
} }
public readonly byte[] BetaNca0Modulus =
{
0xAD, 0x58, 0xEE, 0x97, 0xF9, 0x47, 0x90, 0x7D, 0xF9, 0x29, 0x5F, 0x1F, 0x39, 0x68, 0xEE, 0x49,
0x4C, 0x1E, 0x8D, 0x84, 0x91, 0x31, 0x5D, 0xE5, 0x96, 0x27, 0xB2, 0xB3, 0x59, 0x7B, 0xDE, 0xFD,
0xB7, 0xEB, 0x40, 0xA1, 0xE7, 0xEB, 0xDC, 0x60, 0xD0, 0x3D, 0xC5, 0x50, 0x92, 0xAD, 0x3D, 0xC4,
0x8C, 0x17, 0xD2, 0x37, 0x66, 0xE3, 0xF7, 0x14, 0x34, 0x38, 0x6B, 0xA7, 0x2B, 0x21, 0x10, 0x9B,
0x73, 0x49, 0x15, 0xD9, 0x2A, 0x90, 0x86, 0x76, 0x81, 0x6A, 0x10, 0xBD, 0x74, 0xC4, 0x20, 0x55,
0x25, 0xA8, 0x02, 0xC5, 0xA0, 0x34, 0x36, 0x7B, 0x66, 0x47, 0x2C, 0x7E, 0x47, 0x82, 0xA5, 0xD4,
0xA3, 0x42, 0x45, 0xE8, 0xFD, 0x65, 0x72, 0x48, 0xA1, 0xB0, 0x44, 0x10, 0xEF, 0xAC, 0x1D, 0x0F,
0xB5, 0x12, 0x19, 0xA8, 0x41, 0x0B, 0x76, 0x3B, 0xBC, 0xF1, 0x4A, 0x10, 0x46, 0x22, 0xB8, 0xF1,
0xBC, 0x21, 0x81, 0x69, 0x9B, 0x63, 0x6F, 0xD7, 0xB9, 0x60, 0x2A, 0x9A, 0xE5, 0x2C, 0x47, 0x72,
0x59, 0x65, 0xA2, 0x21, 0x60, 0xC4, 0xFC, 0xB0, 0xD7, 0x6F, 0x42, 0xC9, 0x0C, 0xF5, 0x76, 0x7D,
0xF2, 0x5C, 0xE0, 0x80, 0x0F, 0xEE, 0x45, 0x7E, 0x4E, 0x3A, 0x8D, 0x9C, 0x5B, 0x5B, 0xD9, 0xD1,
0x43, 0x94, 0x2C, 0xC7, 0x2E, 0xB9, 0x4A, 0xE5, 0x3E, 0x15, 0xDD, 0x43, 0x00, 0xF7, 0x78, 0xE7,
0x7C, 0x39, 0xB0, 0x4D, 0xC5, 0xD1, 0x1C, 0xF2, 0xB4, 0x7A, 0x2A, 0xEA, 0x0A, 0x8E, 0xB9, 0x13,
0xB4, 0x4F, 0xD7, 0x5B, 0x4D, 0x7B, 0x43, 0xB0, 0x3A, 0x9A, 0x60, 0x22, 0x47, 0x91, 0x78, 0xC7,
0x10, 0x64, 0xE0, 0x2C, 0x69, 0xD1, 0x66, 0x3C, 0x42, 0x2E, 0xEF, 0x19, 0x21, 0x89, 0x8E, 0xE1,
0xB0, 0xB4, 0xD0, 0x17, 0xA1, 0x0F, 0x73, 0x98, 0x5A, 0xF6, 0xEE, 0xC0, 0x2F, 0x9E, 0xCE, 0xC5
};
public readonly byte[] BetaNca0Exponent =
{
0x3C, 0x66, 0x37, 0x44, 0x26, 0xAC, 0x63, 0xD1, 0x30, 0xE6, 0xD4, 0x68, 0xF9, 0xC4, 0xF0, 0xFA,
0x03, 0x16, 0xC6, 0x32, 0x81, 0xB0, 0x94, 0xC9, 0xF1, 0x26, 0xC5, 0xE2, 0x2D, 0xF4, 0xB6, 0x3E,
0xEB, 0x3D, 0x82, 0x18, 0xA7, 0xC9, 0x8B, 0xD1, 0x03, 0xDD, 0xF2, 0x09, 0x60, 0x02, 0x12, 0xFA,
0x8F, 0xE1, 0xA0, 0xF2, 0x82, 0xDC, 0x3D, 0x74, 0x01, 0xBA, 0x02, 0xF0, 0x8D, 0x5B, 0x89, 0x00,
0xD1, 0x0B, 0x8F, 0x1C, 0x4A, 0xF3, 0x6E, 0x96, 0x8E, 0x03, 0x19, 0xF0, 0x19, 0x66, 0x58, 0xE9,
0xB2, 0x24, 0x37, 0x4B, 0x0A, 0xC6, 0x06, 0x91, 0xBA, 0x92, 0x64, 0x13, 0x5F, 0xF1, 0x4A, 0xBC,
0xAB, 0x61, 0xE5, 0x20, 0x08, 0x62, 0xB7, 0x8E, 0x4D, 0x20, 0x30, 0xA7, 0x42, 0x0B, 0x53, 0x58,
0xEC, 0xBB, 0x70, 0xCB, 0x2A, 0x56, 0xC7, 0x0C, 0x8B, 0x89, 0xFB, 0x47, 0x6E, 0x58, 0x9C, 0xDD,
0xB2, 0xE5, 0x4F, 0x49, 0x52, 0x0B, 0xD9, 0x96, 0x30, 0x8D, 0xDE, 0xC9, 0x0F, 0x6A, 0x82, 0xC7,
0xE8, 0x20, 0xB6, 0xB3, 0x95, 0xDD, 0xEB, 0xDF, 0xF7, 0x25, 0x23, 0x6B, 0xF8, 0x5B, 0xD4, 0x81,
0x7A, 0xBC, 0x94, 0x13, 0x30, 0x59, 0x28, 0xC8, 0xC9, 0x3A, 0x5D, 0xCC, 0x8D, 0xFD, 0x1A, 0xE1,
0xCB, 0xA4, 0x1D, 0xD4, 0x45, 0xF1, 0xBF, 0x87, 0x6C, 0x0E, 0xB1, 0x44, 0xC7, 0x88, 0x62, 0x2B,
0x43, 0xAD, 0x75, 0xE6, 0x69, 0xFF, 0xD3, 0x39, 0xF5, 0x7F, 0x2A, 0xA2, 0x5F, 0x7A, 0x5E, 0xE6,
0xEF, 0xCB, 0x2F, 0x2C, 0x90, 0xE6, 0x4B, 0x2D, 0x94, 0x62, 0xE8, 0xEC, 0x54, 0x7B, 0x94, 0xB7,
0xFB, 0x72, 0x05, 0xFB, 0xB3, 0x23, 0xCA, 0xF8, 0xD4, 0x5C, 0xF6, 0xAC, 0x7D, 0xEC, 0x47, 0xC6,
0xD3, 0x22, 0x5D, 0x7C, 0x15, 0xDD, 0x48, 0xE9, 0xBF, 0xA8, 0x99, 0x33, 0x02, 0x79, 0xD3, 0x65
};
private static readonly byte[] NcaHdrFixedKeyModulusProd = private static readonly byte[] NcaHdrFixedKeyModulusProd =
{ {
0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F, 0xBF, 0xBE, 0x40, 0x6C, 0xF4, 0xA7, 0x80, 0xE9, 0xF0, 0x7D, 0x0C, 0x99, 0x61, 0x1D, 0x77, 0x2F,

View File

@ -40,14 +40,25 @@ namespace LibHac.Npdm
int kernelAccessControlOffset = reader.ReadInt32(); int kernelAccessControlOffset = reader.ReadInt32();
int kernelAccessControlSize = reader.ReadInt32(); int kernelAccessControlSize = reader.ReadInt32();
var accessHeader = new FsAccessHeader(stream, offset + fsAccessHeaderOffset); if (fsAccessHeaderSize > 0)
{
var accessHeader = new FsAccessHeader(stream, offset + fsAccessHeaderOffset);
FsVersion = accessHeader.Version; FsVersion = accessHeader.Version;
FsPermissionsBitmask = accessHeader.PermissionsBitmask; FsPermissionsBitmask = accessHeader.PermissionsBitmask;
}
ServiceAccess = new ServiceAccessControl(stream, offset + serviceAccessControlOffset, serviceAccessControlSize); if (serviceAccessControlSize > 0)
{
ServiceAccess = new ServiceAccessControl(stream, offset + serviceAccessControlOffset,
serviceAccessControlSize);
}
KernelAccess = new KernelAccessControl(stream, offset + kernelAccessControlOffset, kernelAccessControlSize); if (kernelAccessControlSize > 0)
{
KernelAccess =
new KernelAccessControl(stream, offset + kernelAccessControlOffset, kernelAccessControlSize);
}
} }
} }
} }

View File

@ -153,7 +153,7 @@ namespace LibHac
return commonKey; return commonKey;
} }
return CryptoOld.DecryptTitleKey(TitleKeyBlock, keyset.EticketExtKeyRsa); return CryptoOld.DecryptRsaOaep(TitleKeyBlock, keyset.EticketExtKeyRsa);
} }
} }

View File

@ -60,7 +60,7 @@ namespace hactoolnet
if (ctx.Options.Validate && nca.SectionExists(i)) if (ctx.Options.Validate && nca.SectionExists(i))
{ {
if (nca.Header.GetFsHeader(i).IsPatchSection() && baseNca != null) if (nca.GetFsHeader(i).IsPatchSection() && baseNca != null)
{ {
ncaHolder.Validities[i] = baseNca.VerifySection(nca, i, ctx.Logger); ncaHolder.Validities[i] = baseNca.VerifySection(nca, i, ctx.Logger);
} }
@ -261,24 +261,55 @@ namespace hactoolnet
} }
else else
{ {
PrintItem(sb, colLen, "Key Area Encryption Key:", nca.Header.KeyAreaKeyIndex); PrintKeyArea();
sb.AppendLine("Key Area (Encrypted):");
for (int i = 0; i < 4; i++)
{
PrintItem(sb, colLen, $" Key {i} (Encrypted):", nca.Header.GetEncryptedKey(i).ToArray());
}
sb.AppendLine("Key Area (Decrypted):");
for (int i = 0; i < 4; i++)
{
PrintItem(sb, colLen, $" Key {i} (Decrypted):", nca.GetDecryptedKey(i));
}
} }
PrintSections(); PrintSections();
return sb.ToString(); return sb.ToString();
void PrintKeyArea()
{
NcaVersion version = nca.Header.FormatVersion;
if (version == NcaVersion.Nca0RsaOaep)
{
sb.AppendLine("Key Area (Encrypted):");
PrintItem(sb, colLen, "Key (RSA-OAEP Encrypted):", nca.Header.GetKeyArea().ToArray());
sb.AppendLine("Key Area (Decrypted):");
for (int i = 0; i < 2; i++)
{
PrintItem(sb, colLen, $" Key {i} (Encrypted):", nca.Header.GetEncryptedKey(i).ToArray());
}
}
else if (version == NcaVersion.Nca0FixedKey)
{
sb.AppendLine("Key Area:");
for (int i = 0; i < 2; i++)
{
PrintItem(sb, colLen, $" Key {i}:", nca.Header.GetEncryptedKey(i).ToArray());
}
}
else
{
int keyCount = version == NcaVersion.Nca0 ? 2 : 4;
PrintItem(sb, colLen, "Key Area Encryption Key:", nca.Header.KeyAreaKeyIndex);
sb.AppendLine("Key Area (Encrypted):");
for (int i = 0; i < keyCount; i++)
{
PrintItem(sb, colLen, $" Key {i} (Encrypted):", nca.Header.GetEncryptedKey(i).ToArray());
}
sb.AppendLine("Key Area (Decrypted):");
for (int i = 0; i < keyCount; i++)
{
PrintItem(sb, colLen, $" Key {i} (Decrypted):", nca.GetDecryptedKey(i));
}
}
}
void PrintSections() void PrintSections()
{ {
sb.AppendLine("Sections:"); sb.AppendLine("Sections:");
@ -287,13 +318,13 @@ namespace hactoolnet
{ {
if (!nca.Header.IsSectionEnabled(i)) continue; if (!nca.Header.IsSectionEnabled(i)) continue;
NcaFsHeader sectHeader = nca.Header.GetFsHeader(i); NcaFsHeader sectHeader = nca.GetFsHeader(i);
bool isExefs = nca.Header.ContentType == NcaContentType.Program && i == 0; bool isExefs = nca.Header.ContentType == NcaContentType.Program && i == 0;
sb.AppendLine($" Section {i}:"); sb.AppendLine($" Section {i}:");
PrintItem(sb, colLen, " Offset:", $"0x{nca.Header.GetSectionStartOffset(i):x12}"); PrintItem(sb, colLen, " Offset:", $"0x{nca.Header.GetSectionStartOffset(i):x12}");
PrintItem(sb, colLen, " Size:", $"0x{nca.Header.GetSectionSize(i):x12}"); PrintItem(sb, colLen, " Size:", $"0x{nca.Header.GetSectionSize(i):x12}");
PrintItem(sb, colLen, " Partition Type:", (isExefs ? "ExeFS" : sectHeader.FormatType.ToString()) + (sectHeader.IsPatchSection() ? " patch" : "")); PrintItem(sb, colLen, " Partition Type:", GetPartitionType(sectHeader, isExefs, nca.Header.IsNca0()));
PrintItem(sb, colLen, " Section CTR:", $"{sectHeader.Counter:x16}"); PrintItem(sb, colLen, " Section CTR:", $"{sectHeader.Counter:x16}");
PrintItem(sb, colLen, " Section Validity:", $"{ncaHolder.Validities[i]}"); PrintItem(sb, colLen, " Section Validity:", $"{ncaHolder.Validities[i]}");
@ -314,12 +345,20 @@ namespace hactoolnet
} }
} }
static string GetPartitionType(NcaFsHeader fsHeader, bool isExefs, bool isNca0)
{
if (isExefs) return "ExeFS";
if (isNca0 && fsHeader.FormatType == NcaFormatType.Romfs) return "NCA0 RomFS";
return fsHeader.FormatType + (fsHeader.IsPatchSection() ? " patch" : "");
}
void PrintSha256Hash(NcaFsHeader sect, int index) void PrintSha256Hash(NcaFsHeader sect, int index)
{ {
NcaFsIntegrityInfoSha256 hashInfo = sect.GetIntegrityInfoSha256(); NcaFsIntegrityInfoSha256 hashInfo = sect.GetIntegrityInfoSha256();
PrintItem(sb, colLen, $" Master Hash{nca.ValidateSectionMasterHash(index).GetValidityString()}:", hashInfo.MasterHash.ToArray()); PrintItem(sb, colLen, $" Master Hash{nca.ValidateSectionMasterHash(index).GetValidityString()}:", hashInfo.MasterHash.ToArray());
sb.AppendLine($" Hash Table:"); sb.AppendLine(" Hash Table:");
PrintItem(sb, colLen, " Offset:", $"0x{hashInfo.GetLevelOffset(0):x12}"); PrintItem(sb, colLen, " Offset:", $"0x{hashInfo.GetLevelOffset(0):x12}");
PrintItem(sb, colLen, " Size:", $"0x{hashInfo.GetLevelSize(0):x12}"); PrintItem(sb, colLen, " Size:", $"0x{hashInfo.GetLevelSize(0):x12}");