This commit is contained in:
BruceChen 2022-08-28 13:18:07 +08:00
parent 842c968220
commit 4757c4be53
8 changed files with 97 additions and 189 deletions

View file

@ -8,31 +8,37 @@ using System.IO;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
namespace MinecraftClient.Crypto.Streams namespace MinecraftClient.Crypto
{ {
internal class AesCfb8Stream : Stream, IAesStream public class AesCfb8Stream : Stream
{ {
public static readonly int blockSize = 16; public static readonly int blockSize = 16;
private readonly Aes? Aes = null; private readonly Aes? Aes = null;
private readonly FastAes? FastAes = null;
private readonly AesContext? FastAes = null; public Stream BaseStream { get; set; }
public System.IO.Stream BaseStream { get; set; }
private bool inStreamEnded = false; private bool inStreamEnded = false;
private byte[] ReadStreamIV = new byte[16]; private byte[] ReadStreamIV = new byte[16];
private byte[] WriteStreamIV = new byte[16]; private byte[] WriteStreamIV = new byte[16];
public AesCfb8Stream(System.IO.Stream stream, byte[] key) public AesCfb8Stream(Stream stream, byte[] key)
{ {
BaseStream = stream; BaseStream = stream;
if (System.Runtime.Intrinsics.X86.Sse2.IsSupported && System.Runtime.Intrinsics.X86.Aes.IsSupported) if (FastAes.IsSupport())
FastAes = new AesContext(key); FastAes = new FastAes(key);
else else
Aes = GenerateAES(key); {
Aes = Aes.Create();
Aes.BlockSize = 128;
Aes.KeySize = 128;
Aes.Key = key;
Aes.Mode = CipherMode.ECB;
Aes.Padding = PaddingMode.None;
}
Array.Copy(key, ReadStreamIV, 16); Array.Copy(key, ReadStreamIV, 16);
Array.Copy(key, WriteStreamIV, 16); Array.Copy(key, WriteStreamIV, 16);
@ -77,17 +83,37 @@ namespace MinecraftClient.Crypto.Streams
public override int ReadByte() public override int ReadByte()
{ {
byte[] temp = new byte[1]; if (inStreamEnded)
Read(temp, 0, 1); return -1;
return temp[0];
int inputBuf = BaseStream.ReadByte();
if (inputBuf == -1)
{
inStreamEnded = true;
return -1;
}
Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes != null)
FastAes.EncryptEcb(ReadStreamIV, blockOutput);
else
Aes!.EncryptEcb(ReadStreamIV, blockOutput, PaddingMode.None);
// Shift left
Array.Copy(ReadStreamIV, 1, ReadStreamIV, 0, blockSize - 1);
ReadStreamIV[blockSize - 1] = (byte)inputBuf;
return (byte)(blockOutput[0] ^ inputBuf);
} }
[MethodImpl(MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override int Read(byte[] buffer, int outOffset, int required) public override int Read(byte[] buffer, int outOffset, int required)
{ {
if (this.inStreamEnded) if (inStreamEnded)
return 0; return 0;
Span<byte> blockOutput = FastAes != null ? stackalloc byte[blockSize] : null;
byte[] inputBuf = new byte[blockSize + required]; byte[] inputBuf = new byte[blockSize + required];
Array.Copy(ReadStreamIV, inputBuf, blockSize); Array.Copy(ReadStreamIV, inputBuf, blockSize);
@ -96,25 +122,35 @@ namespace MinecraftClient.Crypto.Streams
curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed); curRead = BaseStream.Read(inputBuf, blockSize + readed, required - readed);
if (curRead == 0) if (curRead == 0)
{ {
this.inStreamEnded = true; inStreamEnded = true;
return readed; return readed;
} }
OrderablePartitioner<Tuple<int, int>> rangePartitioner = (curRead <= 256) ? int processEnd = readed + curRead;
Partitioner.Create(readed, readed + curRead, 32) : Partitioner.Create(readed, readed + curRead); if (FastAes != null)
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{ {
Span<byte> blockOutput = stackalloc byte[blockSize]; for (int idx = readed; idx < processEnd; idx++)
for (int idx = range.Item1; idx < range.Item2; idx++)
{ {
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize); ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
if (FastAes != null) FastAes.EncryptEcb(blockInput, blockOutput);
FastAes.EncryptEcb(blockInput, blockOutput);
else
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]); buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
} }
}); }
else
{
OrderablePartitioner<Tuple<int, int>> rangePartitioner = curRead <= 256 ?
Partitioner.Create(readed, processEnd, 32) : Partitioner.Create(readed, processEnd);
Parallel.ForEach(rangePartitioner, (range, loopState) =>
{
Span<byte> blockOutput = stackalloc byte[blockSize];
for (int idx = range.Item1; idx < range.Item2; idx++)
{
ReadOnlySpan<byte> blockInput = new(inputBuf, idx, blockSize);
Aes!.EncryptEcb(blockInput, blockOutput, PaddingMode.None);
buffer[outOffset + idx] = (byte)(blockOutput[0] ^ inputBuf[idx + blockSize]);
}
});
}
} }
Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize); Array.Copy(inputBuf, required, ReadStreamIV, 0, blockSize);
@ -122,7 +158,7 @@ namespace MinecraftClient.Crypto.Streams
return required; return required;
} }
public override long Seek(long offset, System.IO.SeekOrigin origin) public override long Seek(long offset, SeekOrigin origin)
{ {
throw new NotSupportedException(); throw new NotSupportedException();
} }
@ -134,9 +170,23 @@ namespace MinecraftClient.Crypto.Streams
public override void WriteByte(byte b) public override void WriteByte(byte b)
{ {
Write(new byte[] { b }, 0, 1); Span<byte> blockOutput = stackalloc byte[blockSize];
if (FastAes != null)
FastAes.EncryptEcb(WriteStreamIV, blockOutput);
else
Aes!.EncryptEcb(WriteStreamIV, blockOutput, PaddingMode.None);
byte outputBuf = (byte)(blockOutput[0] ^ b);
BaseStream.WriteByte(outputBuf);
// Shift left
Array.Copy(WriteStreamIV, 1, WriteStreamIV, 0, blockSize - 1);
WriteStreamIV[blockSize - 1] = outputBuf;
} }
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public override void Write(byte[] input, int offset, int required) public override void Write(byte[] input, int offset, int required)
{ {
byte[] outputBuf = new byte[blockSize + required]; byte[] outputBuf = new byte[blockSize + required];
@ -156,16 +206,5 @@ namespace MinecraftClient.Crypto.Streams
Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize); Array.Copy(outputBuf, required, WriteStreamIV, 0, blockSize);
} }
private static Aes GenerateAES(byte[] key)
{
Aes aes = Aes.Create();
aes.BlockSize = 128;
aes.KeySize = 128;
aes.Key = key;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.None;
return aes;
}
} }
} }

View file

@ -8,17 +8,24 @@ namespace MinecraftClient.Crypto
{ {
// Using the AES-NI instruction set // Using the AES-NI instruction set
// https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85 // https://gist.github.com/Thealexbarney/9f75883786a9f3100408ff795fb95d85
public class AesContext public class FastAes
{ {
private Vector128<byte>[] RoundKeys { get; } private Vector128<byte>[] RoundKeys { get; }
public byte[] Iv { get; } = new byte[0x10]; public FastAes(Span<byte> key)
public AesContext(Span<byte> key)
{ {
RoundKeys = KeyExpansion(key); RoundKeys = KeyExpansion(key);
} }
/// <summary>
/// Detects if the required instruction set is supported
/// </summary>
/// <returns>Is it supported</returns>
public static bool IsSupport()
{
return Sse2.IsSupported && Aes.IsSupported;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)] [MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination) public void EncryptEcb(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{ {
@ -88,10 +95,5 @@ namespace MinecraftClient.Crypto
keys[i] = Sse2.Xor(s, t); keys[i] = Sse2.Xor(s, t);
} }
public void SetIv(Span<byte> iv)
{
iv.Slice(0, 0x10).CopyTo(Iv);
}
} }
} }

View file

@ -193,18 +193,5 @@ namespace MinecraftClient.Crypto
} }
return p; return p;
} }
/// <summary>
/// Get a new AES-encrypted stream for wrapping a non-encrypted stream.
/// </summary>
/// <param name="underlyingStream">Stream to encrypt</param>
/// <param name="AesKey">Key to use</param>
/// <returns>Return an appropriate stream depending on the framework being used</returns>
public static IAesStream getAesStream(Stream underlyingStream, byte[] AesKey)
{
// return new Streams.RegularAesStream(underlyingStream, AesKey);
return new Streams.AesCfb8Stream(underlyingStream, AesKey);
}
} }
} }

View file

@ -1,18 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace MinecraftClient.Crypto
{
/// <summary>
/// Interface for AES stream
/// Allows to use a different implementation depending on the framework being used.
/// </summary>
public interface IAesStream
{
int Read(byte[] buffer, int offset, int count);
void Write(byte[] buffer, int offset, int count);
}
}

View file

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
using System.IO;
namespace MinecraftClient.Crypto.Streams
{
/// <summary>
/// An encrypted stream using AES, used for encrypting network data on the fly using AES.
/// This is the regular AesStream class used with the regular .NET framework from Microsoft.
/// </summary>
public class RegularAesStream : Stream, IAesStream
{
CryptoStream enc;
CryptoStream dec;
public RegularAesStream(Stream stream, byte[] key)
{
BaseStream = stream;
enc = new CryptoStream(stream, GenerateAES(key).CreateEncryptor(), CryptoStreamMode.Write);
dec = new CryptoStream(stream, GenerateAES(key).CreateDecryptor(), CryptoStreamMode.Read);
}
public System.IO.Stream BaseStream { get; set; }
public override bool CanRead
{
get { return true; }
}
public override bool CanSeek
{
get { return false; }
}
public override bool CanWrite
{
get { return true; }
}
public override void Flush()
{
BaseStream.Flush();
}
public override long Length
{
get { throw new NotSupportedException(); }
}
public override long Position
{
get
{
throw new NotSupportedException();
}
set
{
throw new NotSupportedException();
}
}
public override int ReadByte()
{
return dec.ReadByte();
}
public override int Read(byte[] buffer, int offset, int count)
{
return dec.Read(buffer, offset, count);
}
public override long Seek(long offset, System.IO.SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void WriteByte(byte b)
{
enc.WriteByte(b);
}
public override void Write(byte[] buffer, int offset, int count)
{
enc.Write(buffer, offset, count);
}
private static Aes GenerateAES(byte[] key)
{
Aes aes = Aes.Create();
aes.Mode = CipherMode.CFB;
aes.Padding = PaddingMode.None;
aes.KeySize = 128;
aes.FeedbackSize = 8;
aes.Key = key;
aes.IV = key;
return aes;
}
}
}

View file

@ -26,7 +26,7 @@ namespace MinecraftClient.Protocol.Handlers
private bool encrypted = false; private bool encrypted = false;
private int protocolversion; private int protocolversion;
private Tuple<Thread, CancellationTokenSource>? netRead = null; private Tuple<Thread, CancellationTokenSource>? netRead = null;
Crypto.IAesStream s; Crypto.AesCfb8Stream s;
TcpClient c; TcpClient c;
public Protocol16Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler) public Protocol16Handler(TcpClient Client, int ProtocolVersion, IMinecraftComHandler Handler)
@ -568,7 +568,7 @@ namespace MinecraftClient.Protocol.Handlers
if (pid[0] == 0xFC) if (pid[0] == 0xFC)
{ {
readData(4); readData(4);
s = CryptoHandler.getAesStream(c.GetStream(), secretKey); s = new AesCfb8Stream(c.GetStream(), secretKey);
encrypted = true; encrypted = true;
return true; return true;
} }

View file

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
//using System.Linq; //using System.Linq;
@ -34,6 +35,7 @@ namespace MinecraftClient.Protocol.Handlers
/// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section. /// Reading the "Block states" field: consists of 4096 entries, representing all the blocks in the chunk section.
/// </summary> /// </summary>
/// <param name="cache">Cache for reading data</param> /// <param name="cache">Cache for reading data</param>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
private Chunk? ReadBlockStatesField(Queue<byte> cache) private Chunk? ReadBlockStatesField(Queue<byte> cache)
{ {
// read Block states (Type: Paletted Container) // read Block states (Type: Paletted Container)
@ -142,6 +144,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="cache">Cache for reading chunk data</param> /// <param name="cache">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param> /// <param name="cancellationToken">token to cancel the task</param>
/// <returns>true if successfully loaded</returns> /// <returns>true if successfully loaded</returns>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public bool ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue<byte> cache, CancellationToken cancellationToken) public bool ProcessChunkColumnData(int chunkX, int chunkZ, ulong[]? verticalStripBitmask, Queue<byte> cache, CancellationToken cancellationToken)
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)
@ -243,6 +246,7 @@ namespace MinecraftClient.Protocol.Handlers
/// <param name="cache">Cache for reading chunk data</param> /// <param name="cache">Cache for reading chunk data</param>
/// <param name="cancellationToken">token to cancel the task</param> /// <param name="cancellationToken">token to cancel the task</param>
/// <returns>true if successfully loaded</returns> /// <returns>true if successfully loaded</returns>
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
public bool ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue<byte> cache, CancellationToken cancellationToken) public bool ProcessChunkColumnData(int chunkX, int chunkZ, ushort chunkMask, ushort chunkMask2, bool hasSkyLight, bool chunksContinuous, int currentDimension, Queue<byte> cache, CancellationToken cancellationToken)
{ {
if (cancellationToken.IsCancellationRequested) if (cancellationToken.IsCancellationRequested)

View file

@ -10,7 +10,7 @@ namespace MinecraftClient.Protocol.Handlers
class SocketWrapper class SocketWrapper
{ {
TcpClient c; TcpClient c;
IAesStream s; AesCfb8Stream s;
bool encrypted = false; bool encrypted = false;
/// <summary> /// <summary>
@ -49,7 +49,7 @@ namespace MinecraftClient.Protocol.Handlers
{ {
if (encrypted) if (encrypted)
throw new InvalidOperationException("Stream is already encrypted!?"); throw new InvalidOperationException("Stream is already encrypted!?");
this.s = CryptoHandler.getAesStream(c.GetStream(), secretKey); this.s = new AesCfb8Stream(c.GetStream(), secretKey);
this.encrypted = true; this.encrypted = true;
} }