Skip to content

Commit

Permalink
Merge pull request #37 from SceneGate/feature/hash-verify-methods
Browse files Browse the repository at this point in the history
✨ Add API to verify all the hashes and signature
  • Loading branch information
pleonex authored May 12, 2024
2 parents b43ecce + 10cd8ca commit 5864da7
Show file tree
Hide file tree
Showing 5 changed files with 287 additions and 10 deletions.
8 changes: 4 additions & 4 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
<PackageVersion Include="Texim" Version="0.1.0-preview.210" />
<PackageVersion Include="System.Data.HashFunction.CRC" Version="2.0.0" />
<PackageVersion Include="Portable.BouncyCastle" Version="1.9.0" />
<PackageVersion Include="YamlDotNet" Version="15.1.1" />
<PackageVersion Include="YamlDotNet" Version="15.1.4" />
<PackageVersion Include="FluentAssertions" Version="6.12.0" />
<PackageVersion Include="nunit" Version="4.0.1" />
<PackageVersion Include="nunit" Version="4.1.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="BenchmarkDotNet" Version="0.13.12" />
<PackageVersion Include="SonarAnalyzer.CSharp" Version="9.19.0.84025" />
<PackageVersion Include="SonarAnalyzer.CSharp" Version="9.25.0.90414" />
</ItemGroup>
</Project>
6 changes: 6 additions & 0 deletions src/Ekona.Tests/Containers/Rom/Binary2NitroRomTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ public void DeserializeRomWithKeysHasValidSignatures(string infoPath, string rom

programInfo.DsiInfo.DigestHashesStatus.Should().Be(HashStatus.Valid);
}

programInfo.HasValidHashes().Should().BeTrue();
programInfo.HasValidSignature().Should().BeTrue();
}

[TestCaseSource(nameof(GetFiles))]
Expand Down Expand Up @@ -379,6 +382,9 @@ public void ReadWriteThreeWaysGenerateValidHashes(string infoPath, string romPat
programInfo.DsiInfo.DigestHashesStatus.Should().Be(HashStatus.Valid);
}
}

programInfo.HasValidHashes().Should().BeTrue();
programInfo.HasValidSignature().Should().Be(!isDsi);
}
}
}
198 changes: 198 additions & 0 deletions src/Ekona.Tests/Containers/Rom/ProgramInfoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
namespace SceneGate.Ekona.Tests.Containers.Rom;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NUnit.Framework;
using SceneGate.Ekona.Containers.Rom;
using SceneGate.Ekona.Security;

[TestFixture]
internal class ProgramInfoTests
{
[Test]
public void HasValidHashesTrueIfNull()
{
var info = new ProgramInfo();

Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasValidHashesTrueIfGenerated()
{
var info = new ProgramInfo();
_ = CreateDsHashes(info, HashStatus.Generated);

Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasValidHashesThrowsIfNotValidated()
{
var info = new ProgramInfo();
_ = CreateDsHashes(info, HashStatus.NotValidated);

Assert.That(() => info.HasValidHashes(), Throws.InvalidOperationException);
}

[Test]
public void HasValidHashesFalseIfAnyInvalid()
{
var info = new ProgramInfo();
HashInfo[] hashInfos = CreateDsHashes(info, HashStatus.Invalid);

foreach (HashInfo hashInfo in hashInfos) {
Assert.That(info.HasValidHashes(), Is.False);
hashInfo.Status = HashStatus.Valid;
}

Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasValidHashesTrueIfAllValid()
{
var info = new ProgramInfo();
_ = CreateDsHashes(info, HashStatus.Valid);

Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasValidHashesDsi()
{
var info = new ProgramInfo();
HashInfo[] hashInfos = CreateDsiHashes(info, HashStatus.Invalid);

foreach (HashInfo hashInfo in hashInfos) {
Assert.That(info.HasValidHashes(), Is.False);
hashInfo.Status = HashStatus.Valid;
}

Assert.That(info.HasValidHashes(), Is.False);

info.DsiInfo.DigestHashesStatus = HashStatus.Valid;
Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasValidHashesDsExtended()
{
var info = new ProgramInfo();
HashInfo[] hashInfos = CreateDsExtendedHashes(info, HashStatus.Invalid);

foreach (HashInfo hashInfo in hashInfos) {
Assert.That(info.HasValidHashes(), Is.False);
hashInfo.Status = HashStatus.Valid;
}

Assert.That(info.HasValidHashes(), Is.True);
}

[Test]
public void HasSignatureForDsIsTrue()
{
var info = new ProgramInfo();
info.UnitCode = DeviceUnitKind.DS;

Assert.That(info.HasValidSignature(), Is.True);

info.Signature = CreateHashInfo(HashStatus.Invalid);
Assert.That(info.HasValidSignature(), Is.True);
}

[Test]
public void HasSignatureForDsi()
{
var info = new ProgramInfo();
info.UnitCode = DeviceUnitKind.DSiExclusive;

// null signature
Assert.That(info.HasValidSignature(), Is.True);

info.Signature = CreateHashInfo(HashStatus.Generated);
Assert.That(info.HasValidSignature(), Is.True);

info.Signature.Status = HashStatus.Invalid;
Assert.That(info.HasValidSignature(), Is.False);

info.Signature.Status = HashStatus.Valid;
Assert.That(info.HasValidSignature(), Is.True);

info.Signature.Status = HashStatus.NotValidated;
Assert.That(() => info.HasValidSignature(), Throws.InvalidOperationException);
}

private static HashInfo[] CreateDsHashes(ProgramInfo info, HashStatus status)
{
info.UnitCode = DeviceUnitKind.DS;

#pragma warning disable S1121 // cool syntax for tests
var hashInfos = new List<HashInfo> {
(info.ChecksumHeader = CreateHashInfo(status)),
(info.ChecksumLogo = CreateHashInfo(status)),
(info.ChecksumSecureArea = CreateHashInfo(status)),
};
#pragma warning restore S1121

return hashInfos.ToArray();
}

private static HashInfo[] CreateDsExtendedHashes(ProgramInfo info, HashStatus status)
{
info.UnitCode = DeviceUnitKind.DS;

info.ProgramFeatures = DsiRomFeatures.NitroProgramSigned | DsiRomFeatures.NitroBannerSigned;

#pragma warning disable S1121 // cool syntax for tests
var hashInfos = new List<HashInfo> {
(info.ChecksumHeader = CreateHashInfo(status)),
(info.ChecksumLogo = CreateHashInfo(status)),
(info.ChecksumSecureArea = CreateHashInfo(status)),

(info.BannerMac = CreateHashInfo(status)),

(info.NitroProgramMac = CreateHashInfo(status)),
(info.NitroOverlaysMac = CreateHashInfo(status)),
};
#pragma warning restore S1121

return hashInfos.ToArray();
}

private static HashInfo[] CreateDsiHashes(ProgramInfo info, HashStatus status)
{
info.UnitCode = DeviceUnitKind.DSiExclusive;
info.DsiInfo ??= new DsiProgramInfo();

info.DsiInfo.DigestHashesStatus = status;

#pragma warning disable S1121 // cool syntax for tests
var hashInfos = new List<HashInfo> {
(info.ChecksumHeader = CreateHashInfo(status)),
(info.ChecksumLogo = CreateHashInfo(status)),
(info.ChecksumSecureArea = CreateHashInfo(status)),

(info.BannerMac = CreateHashInfo(status)),

(info.DsiInfo.Arm9SecureMac = CreateHashInfo(status)),
(info.DsiInfo.Arm7Mac = CreateHashInfo(status)),
(info.DsiInfo.DigestMain = CreateHashInfo(status)),
(info.DsiInfo.Arm9iMac = CreateHashInfo(status)),
(info.DsiInfo.Arm7iMac = CreateHashInfo(status)),
(info.DsiInfo.Arm9Mac = CreateHashInfo(status)),
};
#pragma warning restore S1121

return hashInfos.ToArray();
}

private static HashInfo CreateHashInfo(HashStatus status)
{
return new HashInfo("TestAlgo", new byte[] { 0x42 }) {
Status = status,
};
}
}
5 changes: 4 additions & 1 deletion src/Ekona.Tests/Ekona.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
<PackageReference Include="nunit" />
<PackageReference Include="NUnit3TestAdapter" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="coverlet.collector" />
<PackageReference Include="coverlet.collector">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>

<ItemGroup>
Expand Down
80 changes: 75 additions & 5 deletions src/Ekona/Containers/Rom/ProgramInfo.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright(c) 2021 SceneGate
// Copyright(c) 2021 SceneGate
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
Expand All @@ -17,12 +17,13 @@
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
using System.Collections.ObjectModel;
using SceneGate.Ekona.Security;
using Yarhl.FileFormat;

namespace SceneGate.Ekona.Containers.Rom
{
using System;
using System.Collections.ObjectModel;
using SceneGate.Ekona.Security;
using Yarhl.FileFormat;

/// <summary>
/// Information about the content of the program.
/// </summary>
Expand Down Expand Up @@ -240,5 +241,74 @@ public class ProgramInfo : IFormat
/// Gets or sets the collection of information of overlays for ARM-7.
/// </summary>
public Collection<OverlayInfo> Overlays7Info { get; set; } = new Collection<OverlayInfo>();

/// <summary>
/// Gets a value indicating if all the content hashes are valid.
/// </summary>
/// <returns>Value indicating whether the hashes are valid.</returns>
/// <remarks>It takes into account hashes only available in DS or DSi games.</remarks>
public bool HasValidHashes()
{
// Standard DS header checksums (CRC)
bool isValid = IsValidHash(ChecksumHeader)
&& IsValidHash(ChecksumLogo)
&& IsValidHash(ChecksumSecureArea);

bool isDsi = UnitCode != DeviceUnitKind.DS;

// HMACs from DS games after DSi release
if (isDsi || ProgramFeatures.HasFlag(DsiRomFeatures.NitroBannerSigned)) {
isValid = isValid && IsValidHash(BannerMac);
}

if (ProgramFeatures.HasFlag(DsiRomFeatures.NitroProgramSigned)) {
isValid = isValid && IsValidHash(NitroProgramMac);
isValid = isValid && IsValidHash(NitroOverlaysMac);
}

// HMACs from DSi and DSi enhanced games
if (isDsi) {
isValid = isValid && IsValidHash(DsiInfo?.Arm9SecureMac);
isValid = isValid && IsValidHash(DsiInfo?.Arm7Mac);
isValid = isValid && IsValidHash(DsiInfo?.DigestMain);
isValid = isValid && IsValidHash(DsiInfo?.Arm9iMac);
isValid = isValid && IsValidHash(DsiInfo?.Arm7iMac);
isValid = isValid && IsValidHash(DsiInfo?.Arm9Mac);

isValid = isValid && IsValidHash(DsiInfo?.DigestHashesStatus ?? HashStatus.Valid);
}

return isValid;
}

/// <summary>
/// Gets a value indicating if the content signature is valid.
/// </summary>
/// <returns>Value indicating whether the signature is valid.</returns>
/// <remarks>In the case of DS games it always returns true.</remarks>
public bool HasValidSignature()
{
return (UnitCode is DeviceUnitKind.DS) || IsValidHash(Signature);
}

private static bool IsValidHash(HashInfo hashInfo)
{
if (hashInfo is null) {
return true;
}

return IsValidHash(hashInfo.Status);
}

private static bool IsValidHash(HashStatus status)
{
return status switch {
HashStatus.Valid => true,
HashStatus.Generated => true,
HashStatus.Invalid => false,
HashStatus.NotValidated => throw new InvalidOperationException("Hash not validated yet"),
_ => throw new NotSupportedException("Unsupported hash status"),
};
}
}
}

0 comments on commit 5864da7

Please sign in to comment.