namespace Comet.Network.Security
{
using System;
using System.Threading;
/// <summary>
/// TQ Digital Entertainment's in-house asymmetric counter-based XOR-cipher. Counters
/// are separated by encryption direction to create cipher streams. This implementation
/// implements both directions for encrypting and decrypting data on the server side.
/// </summary>
/// <remarks>
/// This cipher algorithm does not provide effective security, and does not make use
/// of any NP-hard calculations for encryption or key generation. Key derivations are
/// susceptible to brute-force or static key attacks. Only implemented for
/// interoperability with the pre-existing game client. Do not use, otherwise.
/// </remarks>
public sealed class TQCipher : ICipher
{
private const int KEY_SIZE = 0x100;
// Local fields and properties
private byte[] mCryptKey1;
private byte[] mCryptKey2;
private byte[] mCryptKey3;
private byte[] mCryptKey4;
private int mDecryptCounter;
private int mEncryptCounter;
private bool mExchanged;
/// <summary>
/// Instantiates a new instance of <see cref="TQCipher"/> with initial IV keys.
/// </summary>
public TQCipher()
{
this.mExchanged = false;
this.mDecryptCounter = 0;
this.mEncryptCounter = 0;
CreateKeys();
}
/// <summary>
/// Generate the initialization vector keys using static seed values.
/// </summary>
private void CreateKeys()
{
this.mCryptKey1 = new byte[KEY_SIZE];
this.mCryptKey2 = new byte[KEY_SIZE];
byte iKey1 = 0x1F;
byte iKey2 = 0x3F;
for (int i = 0; i < KEY_SIZE; i++)
{
this.mCryptKey1[i] = iKey1;
this.mCryptKey2[i] = iKey2;
iKey1 = (byte)(((byte)(iKey1 << 5) + 0xFD) * iKey1 + 7);
iKey2 = (byte)((iKey2 * 0x7A - 0x31) * iKey2 - 0x1B);
}
}
/// <summary>
/// Generates keys for the game server using the player's token and account ID
/// as key derivation variables. Invoked after the first packet is received on
/// the game server.
/// </summary>
/// <param name="seeds">Array of seeds for generating keys (expects Token and AccountID as uint)</param>
public void GenerateKeys(object[] seeds)
{
uint token = Convert.ToUInt32(seeds[0]);
uint accountId = Convert.ToUInt32(seeds[1]);
this.mCryptKey3 = new byte[KEY_SIZE];
this.mCryptKey4 = new byte[KEY_SIZE];
int tmpkey1 = (int)((token + accountId) ^ 0x4321 ^ token);
int tmpkey2 = tmpkey1 * tmpkey1;
byte[] tmp1 = BitConverter.GetBytes(tmpkey1);
byte[] tmp2 = BitConverter.GetBytes(tmpkey2);
for (int i = 0; i < KEY_SIZE; i++)
{
this.mCryptKey3[i] = (byte)(this.mCryptKey1[i] ^ tmp1[i % 4]);
this.mCryptKey4[i] = (byte)(this.mCryptKey2[i] ^ tmp2[i % 4]);
}
this.mExchanged = true;
}
/// <summary>
/// Decrypts the specified span by XORing the source span with the cipher's
/// keystream. The source and destination may be the same slice, but otherwise
/// should not overlap.
/// </summary>
/// <param name="src">Source span that requires decrypting</param>
/// <param name="dst">Destination span to contain the decrypted result</param>
public void Decrypt(Span<byte> src, Span<byte> dst)
{
for (int i = 0; i < src.Length; i++)
{
byte key1Index = (byte)(this.mDecryptCounter & 0xFF);
byte key2Index = (byte)(this.mDecryptCounter >> 8);
if (this.mExchanged)
{
dst[i] = (byte)(src[i] ^ (this.mCryptKey4[key2Index] ^ this.mCryptKey3[key1Index]));
}
else
{
dst[i] = (byte)(src[i] ^ (this.mCryptKey2[key2Index] ^ this.mCryptKey1[key1Index]));
}
Interlocked.Increment(ref this.mDecryptCounter);
}
}
/// <summary>
/// Encrypt the specified span by XORing the source span with the cipher's
/// keystream. The source and destination may be the same slice, but otherwise
/// should not overlap.
/// </summary>
/// <param name="src">Source span that requires encrypting</param>
/// <param name="dst">Destination span to contain the encrypted result</param>
public void Encrypt(Span<byte> src, Span<byte> dst)
{
for (int i = 0; i < src.Length; i++)
{
byte key1Index = (byte)(this.mEncryptCounter & 0xFF);
byte key2Index = (byte)(this.mEncryptCounter >> 8);
dst[i] = (byte)(src[i] ^ (this.mCryptKey1[key1Index] ^ this.mCryptKey2[key2Index]));
Interlocked.Increment(ref this.mEncryptCounter);
}
}
/// <summary>
/// Reset both encryption and decryption counters.
/// </summary>
public void ResetCounters()
{
this.mEncryptCounter = 0;
this.mDecryptCounter = 0;
}
}
}
namespace Comet.Network.Security
{
using System;
using Comet.Core.Mathematics;
/// <summary>
/// Rivest Cipher 5 is implemented for interoperability with the Conquer Online game
/// client's login procedure. Passwords are encrypted in RC5 by the client, and decrypted
/// on the server to be hashed and compared to the database saved password hash. In
/// newer clients, this was replaced with SRP-6A (a hash based exchange protocol).
/// </summary>
/// <remarks>
/// RC5 has a 32, 64 or 128-bit block size and a variable key length from
/// 1 bit up to 2040 bits. It is a Feistel-like cipher using between 1 to
/// 255 rounds. The suggested values are respectively 128, 64 and 12, although
/// 12-round RC5 (with 64-bit blocks) is susceptible to a differential attack
/// using 2^44 chosen plaintexts. To be still an effective cipher, 18-20 rounds
/// are now suggested.
/// </remarks>
public sealed class RC5 : ICipher
{
// Constants and static properties
private const int KEY_SIZE = 16; // The key size in bytes (0-255, 16 suggested)
private const int BLOCK_SIZE = 8; // The block size in bytes (4, 8, or 16, 8 suggested)
private const int ROUNDS = 12; // The number of rounds (1-255, 12 suggested)
// Magic values
private const uint RC5_PW32 = 0xB7E15163;
private const uint RC5_QW32 = 0x61C88647;
// Internal key sizes for uint32 arrays
private const int RC5_SUB = (ROUNDS * 2) + 2;
private const int RC5_KEY = KEY_SIZE / 4;
// Local fields and properties
private readonly uint[] mKey;
private readonly uint[] mSub;
/// <summary>
/// Initializes a new instance of <see cref="RC5"/> to be interoperable with
/// the Conquer Online game client. Automatically generates keys using the
/// default seed. In later versions of the client, a random buffer is used
/// to seed the cipher.
/// </summary>
public RC5()
{
this.mKey = new uint[RC5_KEY];
this.mSub = new uint[RC5_SUB];
// Zero-fill arrays for security purposes
Array.Clear(this.mKey, 0, this.mKey.Length);
Array.Clear(this.mSub, 0, this.mSub.Length);
// Generate keys with default seed
GenerateKeys(new object[] { new byte[] {
0x3C, 0xDC, 0xFE, 0xE8, 0xC4, 0x54, 0xD6, 0x7E,
0x16, 0xA6, 0xF8, 0x1A, 0xE8, 0xD0, 0x38, 0xBE
} });
}
/// <summary>
/// Generates keys and the subkey words for RC5 using a shared seed, whether
/// that seed is shared statically or shared using a method of transport. Though
/// only one seed is expected to generate keys, multiple may be used. Seed must be
/// divisible by the selected cipher word size (16 bytes in this implementation).
/// </summary>
/// <param name="seeds">An array of seeds used to generate keys</param>
public void GenerateKeys(object[] seeds)
{
var aSeed = seeds[0] as byte[];
// Copy seed into key array
for (int i = 0; i < RC5_KEY; i++)
{
this.mKey[i] = BitConverter.ToUInt32(aSeed, i * 4);
}
// Initialize sub-key array
this.mSub[0] = RC5_PW32;
for (int i = 1; i < RC5_SUB; i++)
{
this.mSub[i] = this.mSub[i - 1] - RC5_QW32;
}
// Generate key schedule
uint x = 0, y = 0;
int iIdx = 0, jIdx = 0;
int iterations = 3 * Math.Max(RC5_KEY, RC5_SUB);
for (int k = 0; k < iterations; k++)
{
this.mSub[iIdx] = (this.mSub[iIdx] + x + y).RotateLeft(3);
x = this.mSub[iIdx];
iIdx = (iIdx + 1) % RC5_SUB;
this.mKey[jIdx] = (this.mKey[jIdx] + x + y).RotateLeft((int)(x + y));
y = this.mKey[jIdx];
jIdx = (jIdx + 1) % RC5_KEY;
}
}
/// <summary>
/// Decrypts bytes from the client. The buffer length must be a multiple of
/// the BLOCK_SIZE constant (8 bytes). The source and destination may be the
/// same slice.
/// </summary>
/// <param name="src">Source span that requires decrypting</param>
/// <param name="dst">Destination span to contain the decrypted result</param>
public void Decrypt(Span<byte> src, Span<byte> dst)
{
int blockCount = src.Length / BLOCK_SIZE;
for (int i = 0; i < blockCount; i++)
{
int offset = i * BLOCK_SIZE;
uint ld = BitConverter.ToUInt32(src.Slice(offset, 4));
uint rd = BitConverter.ToUInt32(src.Slice(offset + 4, 4));
// Decrypt rounds
for (int j = ROUNDS; j >= 1; j--)
{
rd = ((rd - this.mSub[2 * j + 1]).RotateRight((int)ld)) ^ ld;
ld = ((ld - this.mSub[2 * j]).RotateRight((int)rd)) ^ rd;
}
uint a = rd - this.mSub[1];
uint b = ld - this.mSub[0];
BitConverter.GetBytes(b).CopyTo(dst.Slice(offset, 4));
BitConverter.GetBytes(a).CopyTo(dst.Slice(offset + 4, 4));
}
}
/// <summary>
/// Encrypts bytes from the server. The buffer length must be a multiple of the
/// BLOCK_SIZE constant (8 bytes). The source and destination may be the same slice.
/// </summary>
/// <param name="src">Source span that requires encrypting</param>
/// <param name="dst">Destination span to contain the encrypted result</param>
public void Encrypt(Span<byte> src, Span<byte> dst)
{
int blockCount = src.Length / BLOCK_SIZE;
for (int i = 0; i < blockCount; i++)
{
int offset = i * BLOCK_SIZE;
uint a = BitConverter.ToUInt32(src.Slice(offset, 4));
uint b = BitConverter.ToUInt32(src.Slice(offset + 4, 4));
uint le = a + this.mSub[0];
uint re = b + this.mSub[1];
// Encrypt rounds
for (int j = 1; j < ROUNDS; j++)
{
le = ((le ^ re).RotateLeft((int)re)) + this.mSub[2 * j];
re = ((re ^ le).RotateLeft((int)le)) + this.mSub[2 * j + 1];
}
BitConverter.GetBytes(le).CopyTo(dst.Slice(offset, 4));
BitConverter.GetBytes(re).CopyTo(dst.Slice(offset + 4, 4));
}
}
}
}
work with comet base