Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,9 @@ public bool TryObfuscate(ReadOnlySpan<byte> data, Span<byte> destination, out in
{
bytesWritten = 0;

byte[] current = data.ToArray();
foreach (IObfuscationProvider stage in _stages)
if (!TryRunForward(data.ToArray(), out byte[] current))
{
current = stage.Obfuscate(current);
return false;
}

if (destination.Length < current.Length)
Expand All @@ -84,10 +83,9 @@ public bool TryObfuscate(Stream data, Stream destination)

using MemoryStream buffer = new();
data.CopyTo(buffer);
byte[] current = buffer.ToArray();
foreach (IObfuscationProvider stage in _stages)
if (!TryRunForward(buffer.ToArray(), out byte[] current))
{
current = stage.Obfuscate(current);
return false;
}

destination.Write(current, 0, current.Length);
Expand All @@ -99,10 +97,9 @@ public bool TryDeobfuscate(ReadOnlySpan<byte> obfuscatedData, Span<byte> destina
{
bytesWritten = 0;

byte[] current = obfuscatedData.ToArray();
for (int i = _stages.Count - 1; i >= 0; i--)
if (!TryRunReverse(obfuscatedData.ToArray(), out byte[] current))
{
current = _stages[i].Deobfuscate(current);
return false;
}

if (destination.Length < current.Length)
Expand All @@ -125,13 +122,58 @@ public bool TryDeobfuscate(Stream obfuscatedData, Stream destination)

using MemoryStream buffer = new();
obfuscatedData.CopyTo(buffer);
byte[] current = buffer.ToArray();
for (int i = _stages.Count - 1; i >= 0; i--)
if (!TryRunReverse(buffer.ToArray(), out byte[] current))
{
current = _stages[i].Deobfuscate(current);
return false;
}

destination.Write(current, 0, current.Length);
return true;
}

/// <summary>
/// Obfuscates <paramref name="data"/> through every stage in order, stopping at the first stage
/// that fails. Each stage runs through its own <c>Try</c> method, so a failure is reported rather
/// than thrown and the composite keeps the <c>Try</c> contract of its stages.
/// </summary>
private bool TryRunForward(byte[] data, out byte[] result)
{
result = data;
foreach (IObfuscationProvider stage in _stages)
{
byte[] buffer = new byte[stage.GetMaxObfuscatedLength(result.Length)];
if (!stage.TryObfuscate(result, buffer, out int written))
{
result = [];
return false;
}

result = buffer.AsSpan(0, written).ToArray();
}

return true;
}

/// <summary>
/// Deobfuscates <paramref name="data"/> through every stage in reverse order, stopping at the
/// first stage that fails, for the same reason as <see cref="TryRunForward"/>.
/// </summary>
private bool TryRunReverse(byte[] data, out byte[] result)
{
result = data;
for (int i = _stages.Count - 1; i >= 0; i--)
{
IObfuscationProvider stage = _stages[i];
byte[] buffer = new byte[stage.GetMaxDeobfuscatedLength(result.Length)];
if (!stage.TryDeobfuscate(result, buffer, out int written))
{
result = [];
return false;
}

result = buffer.AsSpan(0, written).ToArray();
}

return true;
}
}
36 changes: 36 additions & 0 deletions Essentials.Tests/ObfuscationProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using System.Text;
using ktsu.Essentials;
using ktsu.Essentials.ObfuscationProviders.Base64;
using ktsu.Essentials.ObfuscationProviders.Composite;
using ktsu.Essentials.ObfuscationProviders.Hex;
using ktsu.Essentials.ObfuscationProviders.Xor;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;

Expand Down Expand Up @@ -95,4 +97,38 @@
Assert.AreNotEqual(original, obfuscated, "Obfuscated text should not match the input");
Assert.AreEqual(original, restored);
}

private static IObfuscationProvider CompositeWithFallibleStage()
=> new CompositeObfuscationProvider([new XorObfuscationProvider(), new Base64ObfuscationProvider()]);

[TestMethod]
public void Obfuscation_Composite_TryDeobfuscate_Span_ReturnsFalseWhenAStageFails()
{
IObfuscationProvider provider = CompositeWithFallibleStage();
byte[] invalid = Encoding.UTF8.GetBytes("!!!");
byte[] destination = new byte[provider.GetMaxDeobfuscatedLength(invalid.Length)];

Assert.IsFalse(provider.TryDeobfuscate(invalid, destination, out int bytesWritten));
Assert.AreEqual(0, bytesWritten);
}

[TestMethod]
public void Obfuscation_Composite_TryDeobfuscate_Stream_ReturnsFalseWhenAStageFails()
{
IObfuscationProvider provider = CompositeWithFallibleStage();
using MemoryStream invalid = new(Encoding.UTF8.GetBytes("!!!"));
using MemoryStream destination = new();

Assert.IsFalse(provider.TryDeobfuscate(invalid, destination));
Assert.AreEqual(0, destination.Length, "Nothing should be written when a stage fails");
}

[TestMethod]
public void Obfuscation_Composite_WithFallibleStage_Roundtrips()
{
IObfuscationProvider provider = CompositeWithFallibleStage();
byte[] original = Encoding.UTF8.GetBytes("composite with a base64 stage");

CollectionAssert.AreEqual(original, provider.Deobfuscate(provider.Obfuscate(original)));

Check warning on line 132 in Essentials.Tests/ObfuscationProviderTests.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use 'Assert.AreSequenceEqual' instead of 'CollectionAssert.AreEqual'

See more on https://sonarcloud.io/project/issues?id=ktsu-dev_Essentials&issues=AaDdIUinosULGQsgk029&open=AaDdIUinosULGQsgk029&pullRequest=42
}
}
Loading