mirror of
https://github.com/kwsch/NHSE.git
synced 2026-08-27 21:34:15 -05:00
Rewrite crypto to use span
This commit is contained in:
@@ -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();
|
||||
}
|
||||
72
NHSE.Core/Encryption/AesCtr.cs
Normal file
72
NHSE.Core/Encryption/AesCtr.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user