diff --git a/NHSE.Core/Encryption/Aes128Ctr.cs b/NHSE.Core/Encryption/Aes128Ctr.cs deleted file mode 100644 index 1769c80..0000000 --- a/NHSE.Core/Encryption/Aes128Ctr.cs +++ /dev/null @@ -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 _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(); -} \ No newline at end of file diff --git a/NHSE.Core/Encryption/AesCtr.cs b/NHSE.Core/Encryption/AesCtr.cs new file mode 100644 index 0000000..ab7a829 --- /dev/null +++ b/NHSE.Core/Encryption/AesCtr.cs @@ -0,0 +1,72 @@ +using System; +using System.Security.Cryptography; + +namespace NHSE.Core; + +/// +/// AES Counter (CTR) mode encryption/decryption using modern .NET one-shot APIs. +/// +internal static class AesCtr +{ + private const int BlockSize = 16; + + /// + /// Encrypts or decrypts data in-place using AES-CTR mode. + /// + /// AES-CTR is symmetric, so encryption and decryption are the same operation. + /// Data to transform in-place. + /// 16-byte AES key. + /// 16-byte initial counter value (will be modified). + public static void Crypt(Span data, ReadOnlySpan key, Span counter) + { + using var aes = Aes.Create(); + aes.Key = key.ToArray(); + aes.Mode = CipherMode.ECB; + + Span 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); + } + } + + /// + /// Encrypts data using AES-CTR mode, returning a new buffer. + /// + /// Data to encrypt. + /// 16-byte AES key. + /// 16-byte initial counter value (will be copied, not modified). + /// New buffer containing the encrypted data. + public static byte[] Encrypt(ReadOnlySpan data, ReadOnlySpan key, ReadOnlySpan counter) + { + var result = new byte[data.Length]; + data.CopyTo(result); + + Span counterCopy = stackalloc byte[BlockSize]; + counter.CopyTo(counterCopy); + + Crypt(result, key, counterCopy); + return result; + } + + private static void IncrementCounter(Span counter) + { + for (int i = counter.Length - 1; i >= 0; i--) + { + if (++counter[i] != 0) + break; + } + } +} diff --git a/NHSE.Core/Encryption/CryptoFile.cs b/NHSE.Core/Encryption/CryptoFile.cs index fd66d1c..6fe5779 100644 --- a/NHSE.Core/Encryption/CryptoFile.cs +++ b/NHSE.Core/Encryption/CryptoFile.cs @@ -1,3 +1,5 @@ -namespace NHSE.Core; +using System; -internal readonly record struct CryptoFile(byte[] Data, byte[] Key, byte[] Ctr); \ No newline at end of file +namespace NHSE.Core; + +internal record struct CryptoFile(Memory Data, Memory Key, Memory Ctr); \ No newline at end of file diff --git a/NHSE.Core/Encryption/EncryptedInt32.cs b/NHSE.Core/Encryption/EncryptedInt32.cs index a6ef37b..eb5ff75 100644 --- a/NHSE.Core/Encryption/EncryptedInt32.cs +++ b/NHSE.Core/Encryption/EncryptedInt32.cs @@ -57,7 +57,7 @@ public static EncryptedInt32 ReadVerify(ReadOnlySpan 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; } diff --git a/NHSE.Core/Encryption/EncryptedSaveFile.cs b/NHSE.Core/Encryption/EncryptedSaveFile.cs index ffef86c..9c7f583 100644 --- a/NHSE.Core/Encryption/EncryptedSaveFile.cs +++ b/NHSE.Core/Encryption/EncryptedSaveFile.cs @@ -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 -} \ No newline at end of file +public readonly record struct EncryptedSaveFile(ReadOnlyMemory Data, ReadOnlyMemory Header); \ No newline at end of file diff --git a/NHSE.Core/Encryption/Encryption.cs b/NHSE.Core/Encryption/Encryption.cs index 11bb1c4..5226788 100644 --- a/NHSE.Core/Encryption/Encryption.cs +++ b/NHSE.Core/Encryption/Encryption.cs @@ -1,61 +1,65 @@ using System; +using System.Runtime.InteropServices; namespace NHSE.Core; public static class Encryption { - private static byte[] GetParam(ReadOnlySpan data, in int index) + private const int BlockSize = 16; + + private static void GetParam(ReadOnlySpan data, int index, Span 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); } /// /// Decrypts the using the in place. /// - /// Header Data - /// Encrypted SaveData - public static void Decrypt(byte[] headerData, byte[] encData) + /// Header Data (at least 0x300 bytes) + /// Encrypted SaveData (modified in place) + public static void Decrypt(Span headerData, Span 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 importantData = MemoryMarshal.Cast(sourceSpan); - // Set up Key - var key = GetParam(importantData, 0); + // Set up Key and Counter + Span key = stackalloc byte[BlockSize]; + Span 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 versionData) { // Generate 128 Random uints which will be used for params var random = new XorShift128(seed); - var encryptData = new uint[128]; + Span 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); } /// @@ -65,16 +69,13 @@ private static CryptoFile GenerateHeaderFile(uint seed, byte[] versionData) /// Seed to encrypt with /// Version data to encrypt with /// Encrypted SaveData, and associated headerData - public static EncryptedSaveFile Encrypt(byte[] data, uint seed, byte[] versionData) + public static EncryptedSaveFile Encrypt(ReadOnlySpan data, uint seed, ReadOnlySpan 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); } diff --git a/NHSE.Core/Save/Meta/EncryptedFilePair.cs b/NHSE.Core/Save/Meta/EncryptedFilePair.cs index 951e65d..ec07c2d 100644 --- a/NHSE.Core/Save/Meta/EncryptedFilePair.cs +++ b/NHSE.Core/Save/Meta/EncryptedFilePair.cs @@ -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); } /// diff --git a/NHSE.Core/Structures/XorShift128.cs b/NHSE.Core/Structures/XorShift128.cs index 95224f6..5a2afab 100644 --- a/NHSE.Core/Structures/XorShift128.cs +++ b/NHSE.Core/Structures/XorShift128.cs @@ -22,7 +22,10 @@ public XorShift128(uint seed) d = (Mersenne * ( c ^ ( c >> 30))) + 4; } - public uint GetU32() + /// + /// Returns the next random uint. + /// + 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(); } \ No newline at end of file