Rewrite crypto to use span

This commit is contained in:
Kurt
2026-01-14 16:42:57 -06:00
parent ac79cb06f9
commit 0ad0ee0697
8 changed files with 122 additions and 188 deletions

View File

@@ -1,129 +0,0 @@
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
namespace NHSE.Core;
// The MIT License (MIT)
// Copyright (c) 2014 Hans Wolff
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// 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.
public sealed class Aes128CounterMode : SymmetricAlgorithm
{
private readonly byte[] _counter;
private readonly Aes _aes = GetAes();
private static Aes GetAes()
{
var result = Aes.Create();
result.Mode = CipherMode.ECB;
result.Padding = PaddingMode.None;
return result;
}
public Aes128CounterMode(byte[] counter)
{
const int expect = 0x10;
if (counter.Length != expect)
throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {expect})");
_counter = counter;
}
public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? ignoredParameter) => new CounterModeCryptoTransform(_aes, rgbKey, _counter);
public override void GenerateKey() => _aes.GenerateKey();
public override void GenerateIV() { /* IV not needed in Counter Mode */ }
protected override void Dispose(bool disposing) => _aes.Dispose();
}
public sealed class CounterModeCryptoTransform : ICryptoTransform
{
private readonly byte[] _counter;
private readonly ICryptoTransform _counterEncryptor;
private readonly Queue<byte> _xorMask = new();
private readonly SymmetricAlgorithm _symmetricAlgorithm;
public CounterModeCryptoTransform(SymmetricAlgorithm symmetricAlgorithm, byte[] key, byte[] counter)
{
if (counter.Length != symmetricAlgorithm.BlockSize / 8)
throw new ArgumentException($"Counter size must be same as block size (actual: {counter.Length}, expected: {symmetricAlgorithm.BlockSize / 8})");
_symmetricAlgorithm = symmetricAlgorithm;
_encryptOutput = new byte[counter.Length];
_counter = counter;
var zeroIv = new byte[counter.Length];
_counterEncryptor = symmetricAlgorithm.CreateEncryptor(key, zeroIv);
}
public byte[] TransformFinalBlock(byte[] inputBuffer, int inputOffset, int inputCount)
{
var output = new byte[inputCount];
TransformBlock(inputBuffer, inputOffset, inputCount, output, 0);
return output;
}
public int TransformBlock(byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
var xm = _xorMask;
for (var i = 0; i < inputCount; i++)
{
if (xm.Count == 0)
EncryptCounterThenIncrement();
var mask = xm.Dequeue();
outputBuffer[outputOffset + i] = (byte)(inputBuffer[inputOffset + i] ^ mask);
}
return inputCount;
}
private readonly byte[] _encryptOutput;
private void EncryptCounterThenIncrement()
{
var counterModeBlock = _encryptOutput;
_counterEncryptor.TransformBlock(_counter, 0, _counter.Length, counterModeBlock, 0);
IncrementCounter();
var xm = _xorMask;
foreach (var b in counterModeBlock)
xm.Enqueue(b);
}
private void IncrementCounter()
{
var ctr = _counter;
for (var i = ctr.Length - 1; i >= 0; i--)
{
if (++ctr[i] != 0)
break;
}
}
public int InputBlockSize => _symmetricAlgorithm.BlockSize / 8;
public int OutputBlockSize => _symmetricAlgorithm.BlockSize / 8;
public bool CanTransformMultipleBlocks => true;
public bool CanReuseTransform => false;
public void Dispose() => _counterEncryptor.Dispose();
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Security.Cryptography;
namespace NHSE.Core;
/// <summary>
/// AES Counter (CTR) mode encryption/decryption using modern .NET one-shot APIs.
/// </summary>
internal static class AesCtr
{
private const int BlockSize = 16;
/// <summary>
/// Encrypts or decrypts data in-place using AES-CTR mode.
/// </summary>
/// <remarks>AES-CTR is symmetric, so encryption and decryption are the same operation.</remarks>
/// <param name="data">Data to transform in-place.</param>
/// <param name="key">16-byte AES key.</param>
/// <param name="counter">16-byte initial counter value (will be modified).</param>
public static void Crypt(Span<byte> data, ReadOnlySpan<byte> key, Span<byte> counter)
{
using var aes = Aes.Create();
aes.Key = key.ToArray();
aes.Mode = CipherMode.ECB;
Span<byte> encryptedCounter = stackalloc byte[BlockSize];
for (int offset = 0; offset < data.Length; offset += BlockSize)
{
// Encrypt the counter block using ECB mode
aes.EncryptEcb(counter, encryptedCounter, PaddingMode.None);
// XOR the encrypted counter with the data block
int blockLength = Math.Min(BlockSize, data.Length - offset);
var dataBlock = data.Slice(offset, blockLength);
for (int i = 0; i < blockLength; i++)
dataBlock[i] ^= encryptedCounter[i];
// Increment counter (big-endian)
IncrementCounter(counter);
}
}
/// <summary>
/// Encrypts data using AES-CTR mode, returning a new buffer.
/// </summary>
/// <param name="data">Data to encrypt.</param>
/// <param name="key">16-byte AES key.</param>
/// <param name="counter">16-byte initial counter value (will be copied, not modified).</param>
/// <returns>New buffer containing the encrypted data.</returns>
public static byte[] Encrypt(ReadOnlySpan<byte> data, ReadOnlySpan<byte> key, ReadOnlySpan<byte> counter)
{
var result = new byte[data.Length];
data.CopyTo(result);
Span<byte> counterCopy = stackalloc byte[BlockSize];
counter.CopyTo(counterCopy);
Crypt(result, key, counterCopy);
return result;
}
private static void IncrementCounter(Span<byte> counter)
{
for (int i = counter.Length - 1; i >= 0; i--)
{
if (++counter[i] != 0)
break;
}
}
}

View File

@@ -1,3 +1,5 @@
namespace NHSE.Core;
using System;
internal readonly record struct CryptoFile(byte[] Data, byte[] Key, byte[] Ctr);
namespace NHSE.Core;
internal record struct CryptoFile(Memory<byte> Data, Memory<byte> Key, Memory<byte> Ctr);

View File

@@ -57,7 +57,7 @@ public static EncryptedInt32 ReadVerify(ReadOnlySpan<byte> data, int offset)
{
var val = Read(data[offset..]);
if (val.Checksum != CalculateChecksum(val.OriginalEncrypted))
throw new ArgumentException($"Failed to verify the {nameof(EncryptedInt32)} at {nameof(offset)}");
throw new ArgumentException($"Failed to verify the {nameof(EncryptedInt32)} at {nameof(offset)} 0x{offset:X8}.");
return val;
}

View File

@@ -1,20 +1,5 @@
namespace NHSE.Core;
using System;
public readonly ref struct EncryptedSaveFile
{
public readonly byte[] Data;
public readonly byte[] Header;
namespace NHSE.Core;
public EncryptedSaveFile(byte[] data, byte[] header)
{
Data = data;
Header = header;
}
#region Equality Comparison
public override bool Equals(object? obj) => false;
public override int GetHashCode() => Data.GetHashCode();
public static bool operator !=(EncryptedSaveFile left, EncryptedSaveFile right) => !(left == right);
public static bool operator ==(EncryptedSaveFile left, EncryptedSaveFile right) => left.Data == right.Data && left.Header == right.Header;
#endregion
}
public readonly record struct EncryptedSaveFile(ReadOnlyMemory<byte> Data, ReadOnlyMemory<byte> Header);

View File

@@ -1,61 +1,65 @@
using System;
using System.Runtime.InteropServices;
namespace NHSE.Core;
public static class Encryption
{
private static byte[] GetParam(ReadOnlySpan<uint> data, in int index)
private const int BlockSize = 16;
private static void GetParam(ReadOnlySpan<uint> data, int index, Span<byte> result)
{
var rand = new XorShift128(data[(int)data[index] & 0x7F]);
var prms = data[(int)(data[index + 1] & 0x7F)] & 0x7F;
var rndRollCount = (prms & 0xF) + 1;
for (var i = 0; i < rndRollCount; i++)
rand.GetU64();
rand.Next64();
var result = new byte[0x10];
for (var i = 0; i < result.Length; i++)
result[i] = (byte)(rand.GetU32() >> 24);
return result;
result[i] = (byte)(rand.Next() >> 24);
}
/// <summary>
/// Decrypts the <see cref="encData"/> using the <see cref="headerData"/> in place.
/// </summary>
/// <param name="headerData">Header Data</param>
/// <param name="encData">Encrypted SaveData</param>
public static void Decrypt(byte[] headerData, byte[] encData)
/// <param name="headerData">Header Data (at least 0x300 bytes)</param>
/// <param name="encData">Encrypted SaveData (modified in place)</param>
public static void Decrypt(Span<byte> headerData, Span<byte> encData)
{
// First 256 bytes go unused
var importantData = new uint[0x80];
Buffer.BlockCopy(headerData, 0x100, importantData, 0, 0x200);
// First 256 bytes go unused; important data starts at offset 0x100
var sourceSpan = headerData.Slice(0x100, 0x200);
ReadOnlySpan<uint> importantData = MemoryMarshal.Cast<byte, uint>(sourceSpan);
// Set up Key
var key = GetParam(importantData, 0);
// Set up Key and Counter
Span<byte> key = stackalloc byte[BlockSize];
Span<byte> counter = stackalloc byte[BlockSize];
GetParam(importantData, 0, key);
GetParam(importantData, 2, counter);
// Set up counter
var counter = GetParam(importantData, 2);
// Do the AES
using var aesCtr = new Aes128CounterMode(counter);
var transform = aesCtr.CreateDecryptor(key, counter);
transform.TransformBlock(encData, 0, encData.Length, encData, 0);
// Decrypt in place using AES-CTR
AesCtr.Crypt(encData, key, counter);
}
private static CryptoFile GenerateHeaderFile(uint seed, byte[] versionData)
private static CryptoFile GenerateHeaderFile(uint seed, ReadOnlySpan<byte> versionData)
{
// Generate 128 Random uints which will be used for params
var random = new XorShift128(seed);
var encryptData = new uint[128];
Span<uint> encryptData = stackalloc uint[128];
for (var i = 0; i < encryptData.Length; i++)
encryptData[i] = random.GetU32();
encryptData[i] = random.Next();
var headerData = new byte[0x300];
Buffer.BlockCopy(versionData, 0, headerData, 0, 0x100);
Buffer.BlockCopy(encryptData, 0, headerData, 0x100, 0x200);
return new CryptoFile(headerData, GetParam(encryptData, 0), GetParam(encryptData, 2));
var key = new byte[BlockSize];
var ctr = new byte[BlockSize];
versionData[..0x100].CopyTo(headerData);
MemoryMarshal.AsBytes(encryptData).CopyTo(headerData.AsSpan(0x100));
GetParam(encryptData, 0, key);
GetParam(encryptData, 2, ctr);
return new CryptoFile(headerData, key, ctr);
}
/// <summary>
@@ -65,16 +69,13 @@ private static CryptoFile GenerateHeaderFile(uint seed, byte[] versionData)
/// <param name="seed">Seed to encrypt with</param>
/// <param name="versionData">Version data to encrypt with</param>
/// <returns>Encrypted SaveData, and associated headerData</returns>
public static EncryptedSaveFile Encrypt(byte[] data, uint seed, byte[] versionData)
public static EncryptedSaveFile Encrypt(ReadOnlySpan<byte> data, uint seed, ReadOnlySpan<byte> versionData)
{
// Generate header file and get key and counter
var header = GenerateHeaderFile(seed, versionData);
// Encrypt file
using var aesCtr = new Aes128CounterMode(header.Ctr);
var transform = aesCtr.CreateEncryptor(header.Key, header.Ctr);
var encData = new byte[data.Length];
transform.TransformBlock(data, 0, data.Length, encData, 0);
// Encrypt using AES-CTR
var encData = AesCtr.Encrypt(data, header.Key.Span, header.Ctr.Span);
return new EncryptedSaveFile(encData, header.Data);
}

View File

@@ -55,8 +55,8 @@ protected EncryptedFilePair(string folder, string name)
public void Save(uint seed)
{
var encrypt = Encryption.Encrypt(RawData, seed, RawHeader);
File.WriteAllBytes(DataPath, encrypt.Data);
File.WriteAllBytes(HeaderPath, encrypt.Header);
File.WriteAllBytes(DataPath, encrypt.Data.Span);
File.WriteAllBytes(HeaderPath, encrypt.Header.Span);
}
/// <summary>

View File

@@ -22,7 +22,10 @@ public XorShift128(uint seed)
d = (Mersenne * ( c ^ ( c >> 30))) + 4;
}
public uint GetU32()
/// <summary>
/// Returns the next random uint.
/// </summary>
public uint Next()
{
uint t = a;
a = b;
@@ -33,5 +36,5 @@ public uint GetU32()
return d = t ^ d ^ (d >> 19);
}
public ulong GetU64() => ((ulong)GetU32() << 32) | GetU32();
public ulong Next64() => ((ulong)Next() << 32) | Next();
}