Add project files.

This commit is contained in:
Wizou 2021-08-04 00:40:09 +02:00
parent fd507c2db1
commit e093a67601
11 changed files with 9432 additions and 0 deletions

404
Client.cs Normal file
View file

@ -0,0 +1,404 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TL;
using static WTelegram.Encryption;
namespace WTelegram
{
public sealed class Client : IDisposable
{
public Config TLConfig { get; private set; }
private readonly Func<string, string> _config;
private readonly Func<ITLObject, Task> _updateHandler;
private readonly int _apiId;
private readonly string _apiHash;
private readonly Session _session;
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private int _frame_seqTx = 0, _frame_seqRx = 0;
private ITLObject _lastSentMsg;
private Type _lastRpcResultType = typeof(object);
private readonly List<long> _msgsToAck = new();
private int _unexpectedSaltChange;
public Client(Func<string,string> configProvider = null, Func<ITLObject, Task> updateHandler = null)
{
_config = configProvider ?? DefaultConfigOrAsk;
_updateHandler = updateHandler;
_apiId = int.Parse(_config("api_id"));
_apiHash = _config("api_hash");
_session = Session.LoadOrCreate(_config("session_pathname"));
}
public static string DefaultConfig(string config) => config switch
{
"session_pathname" => Path.Combine(
Path.GetDirectoryName(Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar))),
"WTelegram.session"),
#if DEBUG
"server_address" => "149.154.167.40:443",
#else
"server_address" => "149.154.167.50:443",
#endif
"device_model" => Environment.Is64BitOperatingSystem ? "PC 64bit" : "PC 32bit",
"system_version" => System.Runtime.InteropServices.RuntimeInformation.OSDescription,
"app_version" => Assembly.GetEntryAssembly().GetName().Version.ToString(),
"system_lang_code" => CultureInfo.InstalledUICulture.TwoLetterISOLanguageName,
"lang_pack" => "",
"lang_code" => CultureInfo.CurrentUICulture.TwoLetterISOLanguageName,
_ => null // api_id api_hash phone_number... it's up to you to reply to these correctly
};
public static string DefaultConfigOrAsk(string config)
{
var value = DefaultConfig(config);
if (value != null) return value;
Console.Write($"Enter {config.Replace('_', ' ')}: ");
return Console.ReadLine();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822")]
public void LoadPublicKey(string pem) => Encryption.LoadPublicKey(pem);
public async Task ConnectAsync()
{
var endpoint = _session.DataCenter == null ? IPEndPoint.Parse(_config("server_address"))
: new IPEndPoint(IPAddress.Parse(_session.DataCenter.ip_address), _session.DataCenter.port);
Console.WriteLine($"Connecting to {endpoint}...");
_tcpClient = new TcpClient(endpoint.AddressFamily);
await _tcpClient.ConnectAsync(endpoint.Address, endpoint.Port);
_networkStream = _tcpClient.GetStream();
_frame_seqTx = _frame_seqRx = 0;
if (_session.AuthKey == null)
await CreateAuthorizationKey(this, _session);
TLConfig = await CallAsync(new InvokeWithLayer<Config>
{
layer = Schema.Layer,
query = new InitConnection<Config>
{
api_id = _apiId,
device_model = _config("device_model"),
system_version = _config("system_version"),
app_version = _config("app_version"),
system_lang_code = _config("system_lang_code"),
lang_pack = _config("lang_pack"),
lang_code = _config("lang_code"),
query = new Help_GetConfig()
}
});
}
private async Task MigrateDCAsync(int dcId)
{
Console.WriteLine($"Migrate to DC {dcId}...");
//TODO: Export/Import client authorization?
var prevFamily = _tcpClient.Client.RemoteEndPoint.AddressFamily;
_tcpClient.Close();
var dcOptions = TLConfig.dc_options.Where(dc => dc.id == dcId && (dc.flags & (DcOption.Flags.media_only | DcOption.Flags.cdn)) == 0);
if (prevFamily == AddressFamily.InterNetworkV6) // try to stay in the same connectivity
dcOptions = dcOptions.OrderByDescending(dc => dc.flags & DcOption.Flags.ipv6); // list ipv6 first
else
dcOptions = dcOptions.OrderBy(dc => dc.flags & DcOption.Flags.ipv6); // list ipv4 first
var dcOption = dcOptions.FirstOrDefault();
_session.DataCenter = dcOption ?? throw new ApplicationException($"Could not find adequate dcOption for DC {dcId}");
_session.AuthKeyID = _session.Salt = _session.Seqno = 0;
_session.AuthKey = null;
await ConnectAsync();
}
public void Dispose()
{
CheckMsgsToAck().Wait(1000);
_networkStream?.Dispose();
_tcpClient?.Dispose();
}
public async Task SendAsync(ITLObject msg, bool isContent = true)
{
if (_session.AuthKeyID != 0) await CheckMsgsToAck();
using var memStream = new MemoryStream(1024); //TODO: choose a useful capacity
using var writer = new BinaryWriter(memStream, Encoding.UTF8);
writer.Write(0); // int32 frame_len (to be patched with full frame length)
writer.Write(_frame_seqTx++); // int32 frame_seq
(long msgId, int seqno) = _session.NewMsg(isContent);
if (_session.AuthKeyID == 0) // send unencrypted message
{
Console.WriteLine($"Sending {msg.GetType().Name}...");
writer.Write(0L); // int64 auth_key_id = 0 (Unencrypted)
writer.Write(msgId); // int64 message_id
writer.Write(0); // int32 message_data_length (to be patched)
Schema.Serialize(writer, msg); // bytes message_data
Helpers.LittleEndian(memStream.GetBuffer(), 24, (int)memStream.Length - 28); // patch message_data_length
}
else
{
Console.WriteLine($"Sending {msg.GetType().Name}... (seqno {seqno})");
//TODO: Implement MTProto 2.0
using var clearStream = new MemoryStream(1024); //TODO: choose a useful capacity
using var clearWriter = new BinaryWriter(clearStream, Encoding.UTF8);
clearWriter.Write(_session.Salt); // int64 salt
clearWriter.Write(_session.Id); // int64 session_id
clearWriter.Write(msgId); // int64 message_id
clearWriter.Write(seqno); // int32 msg_seqno
clearWriter.Write(0); // int32 message_data_length (to be patched)
Schema.Serialize(clearWriter, msg); // bytes message_data
int clearLength = (int)clearStream.Length; // length before padding (= 32 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + padding);
byte[] clearBuffer = clearStream.GetBuffer();
Helpers.LittleEndian(clearBuffer, 28, clearLength - 32); // patch message_data_length
RNG.GetBytes(clearBuffer, clearLength, padding);
var clearSha1 = SHA1.HashData(clearBuffer.AsSpan(0, clearLength)); // padding excluded from computation!
byte[] encrypted_data = EncryptDecryptMessage(clearBuffer.AsSpan(0, clearLength + padding), true, _session.AuthKey, clearSha1);
writer.Write(_session.AuthKeyID); // int64 auth_key_id
writer.Write(clearSha1, 4, 16); // int128 msg_key = low 128-bits of SHA1(clear message body)
writer.Write(encrypted_data); // bytes encrypted_data
}
var buffer = memStream.GetBuffer();
int frameLength = (int)memStream.Length;
//TODO: support Quick Ack?
Helpers.LittleEndian(buffer, 0, frameLength + 4); // patch frame_len with correct value
uint crc = Force.Crc32.Crc32Algorithm.Compute(buffer, 0, frameLength);
writer.Write(crc); // int32 frame_crc
var frame = memStream.GetBuffer().AsMemory(0, frameLength + 4);
//TODO: support Transport obfuscation?
await _networkStream.WriteAsync(frame);
_lastSentMsg = msg;
}
internal async Task<object> RecvInternalAsync()
{
var data = await RecvFrameAsync();
if (data.Length == 4 && data[3] == 0xFF)
throw new ApplicationException($"Server replied with error code: {TransportError(-BitConverter.ToInt32(data))}");
if (data.Length < 24) // authKeyId+msgId+length+ctorNb | authKeyId+msgKey
throw new ApplicationException($"Packet payload too small: {data.Length}");
//TODO: ignore msgId <= lastRecvMsgId, ignore MsgId >= 30 sec in the future or < 300 sec in the past
long authKeyId = BitConverter.ToInt64(data);
if (authKeyId == 0) // Unencrypted message
{
using var reader = new BinaryReader(new MemoryStream(data, 8, data.Length - 8));
long msgId = reader.ReadInt64();
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
int length = reader.ReadInt32();
if (length != data.Length - 20) throw new ApplicationException($"Unexpected unencrypted length {length} != {data.Length - 20}");
var ctorNb = reader.ReadUInt32();
if (!Schema.Mappings.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
Console.WriteLine($"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} unencrypted");
return Schema.DeserializeObject(reader, realType);
}
else if (authKeyId != _session.AuthKeyID)
throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}");
else
{
byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24), false, _session.AuthKey, data[4..24]);
if (decrypted_data.Length < 36) // header below+ctorNb
throw new ApplicationException($"Decrypted packet too small: {decrypted_data.Length}");
using var reader = new BinaryReader(new MemoryStream(decrypted_data));
var serverSalt = reader.ReadInt64(); // int64 salt
var sessionId = reader.ReadInt64(); // int64 session_id
var msgId = reader.ReadInt64(); // int64 message_id
var seqno = reader.ReadInt32(); // int32 msg_seqno
var length = reader.ReadInt32(); // int32 message_data_length
if (serverSalt != _session.Salt)
if (++_unexpectedSaltChange >= 10)
throw new ApplicationException($"Server salt changed unexpectedly more than 10 times during this run");
if (sessionId != _session.Id) throw new ApplicationException($"Unexpected session ID {_session.Id} != {_session.Id}");
if ((msgId & 1) == 0) throw new ApplicationException($"Invalid server msgId {msgId}");
if ((seqno & 1) != 0) lock(_msgsToAck) _msgsToAck.Add(msgId);
if (decrypted_data.Length - 32 - length is < 0 or > 15) throw new ApplicationException($"Unexpected decrypted message_data_length {length} / {decrypted_data.Length - 32}");
if (!data.AsSpan(8, 16).SequenceEqual(SHA1.HashData(decrypted_data.AsSpan(0, 32 + length)).AsSpan(4)))
throw new ApplicationException($"Mismatch between MsgKey & decrypted SHA1");
var ctorNb = reader.ReadUInt32();
if (!Schema.Mappings.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
Console.WriteLine($"Receiving {realType.Name,-50} timestamp={_session.MsgIdToStamp(msgId)} isResponse={(msgId & 2) != 0} {(seqno == -1 ? "clearText" : "isContent")}={(seqno & 1) != 0}");
if (realType == typeof(RpcResult))
return DeserializeRpcResult(reader); // hack necessary because some RPC return bare types like bool or int[]
else
return Schema.DeserializeObject(reader, realType);
}
static string TransportError(int error_code) => error_code switch
{
404 => "Auth key not found",
429 => "Transport flood",
_ => ((HttpStatusCode)error_code).ToString(),
};
}
private async Task<byte[]> RecvFrameAsync()
{
byte[] frame = new byte[8];
await FullReadAsync(frame, 8);
int length = BitConverter.ToInt32(frame) - 12;
if (length <= 0 || length >= 0x10000)
throw new ApplicationException("Invalid frame_len");
int seqno = BitConverter.ToInt32(frame, 4);
if (seqno != _frame_seqRx++)
{
Trace.TraceWarning($"Unexpected frame_seq received: {seqno} instead of {_frame_seqRx}");
_frame_seqRx = seqno + 1;
}
var payload = new byte[length];
await FullReadAsync(payload, length);
uint crc32 = Force.Crc32.Crc32Algorithm.Compute(frame, 0, 8);
crc32 = Force.Crc32.Crc32Algorithm.Append(crc32, payload);
await FullReadAsync(frame, 4);
if (crc32 != BitConverter.ToUInt32(frame))
throw new ApplicationException("Invalid envelope CRC32");
return payload;
}
private async Task FullReadAsync(byte[] buffer, int length)
{
for (int offset = 0; offset != length;)
{
var read = await _networkStream.ReadAsync(buffer.AsMemory(offset, length - offset));
if (read == 0) throw new ApplicationException("Connection shut down");
offset += read;
}
}
private object DeserializeRpcResult(BinaryReader reader)
{
long reqMsgId = reader.ReadInt64();
var rpcResult = new RpcResult { req_msg_id = reqMsgId };
if (reqMsgId == _session.LastSentMsgId)
rpcResult.result = Schema.DeserializeValue(reader, _lastRpcResultType);
else
rpcResult.result = Schema.Deserialize<object>(reader);
return rpcResult;
}
public class RpcException : Exception
{
public readonly int Code;
public RpcException(int code, string message) : base(message) => Code = code;
}
public async Task<X> CallAsync<X>(ITLFunction<X> request)
{
await SendAsync(request);
_lastRpcResultType = typeof(X);
for (; ;)
{
var reply = await RecvInternalAsync();
if (reply is RpcResult rpcResult && rpcResult.req_msg_id == _session.LastSentMsgId)
{
if (rpcResult.result is RpcError rpcError)
{
int migrateDC;
if (rpcError.error_code == 303 && ((migrateDC = rpcError.error_message.IndexOf("_MIGRATE_")) > 0))
{
migrateDC = int.Parse(rpcError.error_message[(migrateDC + 9)..]);
await MigrateDCAsync(migrateDC);
await SendAsync(request);
}
else
throw new RpcException(rpcError.error_code, rpcError.error_message);
}
else if (rpcResult.result is X result)
return result;
else
throw new ApplicationException($"{request.GetType().Name} call got a result of type {rpcResult.result.GetType().Name} instead of {typeof(X).Name}");
}
else
await HandleMessageAsync(reply);
}
}
private async Task CheckMsgsToAck()
{
MsgsAck msgsAck = null;
lock (_msgsToAck)
if (_msgsToAck.Count != 0)
{
msgsAck = new MsgsAck { msg_ids = _msgsToAck.ToArray() };
_msgsToAck.Clear();
}
if (msgsAck != null)
await SendAsync(msgsAck, false);
}
private async Task HandleMessageAsync(object obj)
{
switch (obj)
{
case MsgContainer container:
foreach (var msg in container.messages)
{
Console.Write($" → {msg.body?.GetType().Name}");
Console.WriteLine($"{new string(' ', Math.Max(0, 60 - Console.CursorLeft))} timestamp={_session.MsgIdToStamp(msg.msg_id)} isResponse={(msg.msg_id & 2) != 0} {(msg.seqno == -1 ? "clearText" : "isContent")}={(msg.seqno & 1) != 0}");
if ((msg.seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msg.msg_id);
if (msg.body != null) await HandleMessageAsync(msg.body);
}
break;
case BadServerSalt badServerSalt:
_session.Salt = badServerSalt.new_server_salt;
if (badServerSalt.bad_msg_id == _session.LastSentMsgId)
await SendAsync(_lastSentMsg);
break;
case BadMsgNotification badMsgNotification:
Console.WriteLine($"BadMsgNotification {badMsgNotification.error_code} for msg {badMsgNotification.bad_msg_seqno}");
break;
case RpcResult rpcResult:
if (_session.MsgIdToStamp(rpcResult.req_msg_id) >= _session.SessionStart)
throw new ApplicationException($"Got RpcResult({rpcResult.result.GetType().Name}) for unknown msgId {rpcResult.req_msg_id}");
break; // silently ignore results for msg_id from previous sessions
default:
//_updateHandler?.Invoke(obj);
break;
}
}
public async Task<User> UserAuthIfNeeded(CodeSettings settings = null)
{
if (_session.User != null)
return Schema.Deserialize<User>(_session.User);
string phone_number = _config("phone_number");
var sentCode = await CallAsync(new Auth_SendCode
{
phone_number = phone_number,
api_id = _apiId,
api_hash = _apiHash,
settings = settings ?? new()
});
Console.WriteLine($"A verification code has been sent via {sentCode.type.GetType().Name[17..]}");
var verification_code = _config("verification_code");
var authorization = await CallAsync(new Auth_SignIn
{
phone_number = phone_number,
phone_code_hash = sentCode.phone_code_hash,
phone_code = verification_code
});
if (authorization is not Auth_Authorization { user: User user } auth_success)
throw new ApplicationException("Failed to get Authorization: " + authorization.GetType().Name);
//TODO: support Auth_AuthorizationSignUpRequired?
_session.User = Schema.Serialize(user);
return user;
}
}
}

290
Encryption.cs Normal file
View file

@ -0,0 +1,290 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using TL;
namespace WTelegram
{
internal static class Encryption
{
internal static readonly RNGCryptoServiceProvider RNG = new();
private static readonly Dictionary<long, RSAPublicKey> PublicKeys = new();
internal static async Task CreateAuthorizationKey(Client client, Session session)
{
if (PublicKeys.Count == 0) LoadDefaultPublicKey();
//1)
var reqPQ = new ReqPQ() { nonce = new Int128(RNG) };
await client.SendAsync(reqPQ, false);
//2)
var reply = await client.RecvInternalAsync();
if (reply is not ResPQ resPQ) throw new ApplicationException($"Expected ResPQ but got {reply.GetType().Name}");
if (resPQ.nonce != reqPQ.nonce) throw new ApplicationException("Nonce mismatch");
var fingerprint = resPQ.server_public_key_fingerprints.FirstOrDefault(PublicKeys.ContainsKey);
if (fingerprint == 0) throw new ApplicationException("Couldn't match any server_public_key_fingerprints");
Console.WriteLine($"Selected public key with fingerprint {fingerprint:X}");
//3)
long retry_id = 0;
ulong pq = Helpers.FromBigEndian(resPQ.pq);
ulong p = Helpers.PQFactorize(pq);
ulong q = pq / p;
//4)
var new_nonce = new Int256(RNG);
var reqDHparams = MakeReqDHparam(fingerprint, PublicKeys[fingerprint], new PQInnerData
{
pq = resPQ.pq,
p = Helpers.ToBigEndian(p),
q = Helpers.ToBigEndian(q),
nonce = resPQ.nonce,
server_nonce = resPQ.server_nonce,
new_nonce = new_nonce,
});
await client.SendAsync(reqDHparams, false);
//5)
reply = await client.RecvInternalAsync();
if (reply is not ServerDHParams serverDHparams) throw new ApplicationException($"Expected ServerDHParams but got {reply.GetType().Name}");
var localTime = DateTimeOffset.UtcNow;
if (serverDHparams is not ServerDHParamsOk serverDHparamsOk) throw new ApplicationException("not server_DH_params_ok");
if (serverDHparamsOk.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHparamsOk.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var (tmp_aes_key, tmp_aes_iv) = ConstructTmpAESKeyIV(resPQ.server_nonce, new_nonce);
var answer = AES_IGE_EncryptDecrypt(serverDHparamsOk.encrypted_answer, tmp_aes_key, tmp_aes_iv, false);
using var encryptedReader = new BinaryReader(new MemoryStream(answer));
var answerHash = encryptedReader.ReadBytes(20);
var answerObj = Schema.DeserializeValue(encryptedReader, typeof(object));
if (answerObj is not ServerDHInnerData serverDHinnerData) throw new ApplicationException("not server_DH_inner_data");
long padding = encryptedReader.BaseStream.Length - encryptedReader.BaseStream.Position;
if (padding >= 16) throw new ApplicationException("Too much pad");
if (!Enumerable.SequenceEqual(SHA1.HashData(answer.AsSpan(20..^(int)padding)), answerHash))
throw new ApplicationException("Answer SHA1 mismatch");
if (serverDHinnerData.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (serverDHinnerData.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var g_a = new BigInteger(serverDHinnerData.g_a, true, true);
var dh_prime = new BigInteger(serverDHinnerData.dh_prime, true, true);
ValidityChecks(dh_prime, serverDHinnerData.g);
Console.WriteLine($"Server time: {serverDHinnerData.server_time} UTC");
session.ServerTicksOffset = (serverDHinnerData.server_time - localTime).Ticks;
//6)
var bData = new byte[256];
RNG.GetBytes(bData);
var b = new BigInteger(bData, true, true);
var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime);
var setClientDHparams = MakeClientDHparams(tmp_aes_key, tmp_aes_iv, new ClientDHInnerData
{
nonce = resPQ.nonce,
server_nonce = resPQ.server_nonce,
retry_id = retry_id,
g_b = g_b.ToByteArray(true, true)
});
await client.SendAsync(setClientDHparams, false);
//7)
var gab = BigInteger.ModPow(g_a, b, dh_prime);
var authKey = gab.ToByteArray(true, true);
//8)
var authKeyHash = SHA1.HashData(authKey);
retry_id = BitConverter.ToInt64(authKeyHash); // (auth_key_aux_hash)
//9)
reply = await client.RecvInternalAsync();
if (reply is not SetClientDHParamsAnswer setClientDHparamsAnswer) throw new ApplicationException($"Expected SetClientDHParamsAnswer but got {reply.GetType().Name}");
if (setClientDHparamsAnswer is not DHGenOk) throw new ApplicationException("not dh_gen_ok");
if (setClientDHparamsAnswer.nonce != resPQ.nonce) throw new ApplicationException("Nonce mismatch");
if (setClientDHparamsAnswer.server_nonce != resPQ.server_nonce) throw new ApplicationException("Server Nonce mismatch");
var expected_new_nonceN = new byte[32 + 1 + 8];
new_nonce.raw.CopyTo(expected_new_nonceN, 0);
expected_new_nonceN[32] = 1;
Array.Copy(authKeyHash, 0, expected_new_nonceN, 33, 8); // (auth_key_aux_hash)
if (!Enumerable.SequenceEqual(setClientDHparamsAnswer.new_nonce_hashN.raw, SHA1.HashData(expected_new_nonceN).Skip(4)))
throw new ApplicationException("setClientDHparamsAnswer.new_nonce_hashN mismatch");
session.AuthKeyID = BitConverter.ToInt64(authKeyHash, 12);
session.AuthKey = authKey;
session.Salt = BitConverter.ToInt64(new_nonce, 0) ^ BitConverter.ToInt64(resPQ.server_nonce, 0);
session.Save();
static (byte[] key, byte[] iv) ConstructTmpAESKeyIV(Int128 server_nonce, Int256 new_nonce)
{
byte[] tmp_aes_key = new byte[32], tmp_aes_iv = new byte[32];
using var sha1 = new SHA1Managed();
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(server_nonce, 0, 16);
sha1.Hash.CopyTo(tmp_aes_key, 0); // tmp_aes_key := SHA1(new_nonce + server_nonce)
sha1.TransformBlock(server_nonce, 0, 16, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
Array.Copy(sha1.Hash, 0, tmp_aes_key, 20, 12); // + SHA1(server_nonce, new_nonce)[0:12]
Array.Copy(sha1.Hash, 12, tmp_aes_iv, 0, 8); // tmp_aes_iv != SHA1(server_nonce, new_nonce)[12:8]
sha1.TransformBlock(new_nonce, 0, 32, null, 0);
sha1.TransformFinalBlock(new_nonce, 0, 32);
sha1.Hash.CopyTo(tmp_aes_iv, 8); // + SHA(new_nonce + new_nonce)
Array.Copy(new_nonce, 0, tmp_aes_iv, 28, 4); // + new_nonce[0:4]
return (tmp_aes_key, tmp_aes_iv);
}
}
private static void ValidityChecks(BigInteger p, int g)
{
//TODO: check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime)
// check that 2^2047 <= p < 2^2048
if (p.GetBitLength() != 2048) throw new ApplicationException("p is not 2048-bit number");
// check that g generates a cyclic subgroup of prime order (p - 1) / 2, i.e. is a quadratic residue mod p.
BigInteger mod_r;
if (g switch
{
2 => p % 8 != 7,
3 => p % 3 != 2,
4 => false,
5 => (mod_r = p % 5) != 1 && mod_r != 4,
6 => (mod_r = p % 24) != 19 && mod_r != 23,
7 => (mod_r = p % 7) != 3 && mod_r != 5 && mod_r != 6,
_ => true,
})
throw new ApplicationException("Bad prime mod 4g");
//TODO: check that g, g_a and g_b are greater than 1 and less than dh_prime - 1.
// We recommend checking that g_a and g_b are between 2^{2048-64} and dh_prime - 2^{2048-64} as well.
}
private static ReqDHParams MakeReqDHparam(long publicKey_fingerprint, RSAPublicKey publicKey, PQInnerData pqInnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(255);
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, pqInnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
if (clearLength > 255) throw new ApplicationException("PQInnerData too big");
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, 255 - clearLength);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer); // patch with SHA1
var encrypted_data = BigInteger.ModPow(new BigInteger(clearBuffer, true, true), // encrypt with RSA key
new BigInteger(publicKey.e, true, true), new BigInteger(publicKey.n, true, true)).ToByteArray(true, true);
return new ReqDHParams
{
nonce = pqInnerData.nonce,
server_nonce = pqInnerData.server_nonce,
p = pqInnerData.p,
q = pqInnerData.q,
public_key_fingerprint = publicKey_fingerprint,
encrypted_data = encrypted_data
};
}
private static SetClientDHParams MakeClientDHparams(byte[] tmp_aes_key, byte[] tmp_aes_iv, ClientDHInnerData clientDHinnerData)
{
// the following code was the way TDLib did it (and seems still accepted) until they changed on 8 July 2021
using var clearStream = new MemoryStream(512); //TODO: choose a useful capacity
clearStream.Position = 20; // skip SHA1 area (to be patched)
using var writer = new BinaryWriter(clearStream, Encoding.UTF8);
Schema.Serialize(writer, clientDHinnerData);
int clearLength = (int)clearStream.Length; // length before padding (= 20 + message_data_length)
int padding = (0x7FFFFFF0 - clearLength) % 16;
clearStream.SetLength(clearLength + padding);
byte[] clearBuffer = clearStream.GetBuffer();
RNG.GetBytes(clearBuffer, clearLength, padding);
SHA1.HashData(clearBuffer.AsSpan(20..clearLength), clearBuffer);
var encrypted_data = AES_IGE_EncryptDecrypt(clearBuffer.AsSpan(0, clearLength + padding), tmp_aes_key, tmp_aes_iv, true);
return new SetClientDHParams
{
nonce = clientDHinnerData.nonce,
server_nonce = clientDHinnerData.server_nonce,
encrypted_data = encrypted_data
};
}
public static void LoadPublicKey(string pem)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(pem);
var rsaParam = rsa.ExportParameters(false);
var publicKey = new RSAPublicKey { n = rsaParam.Modulus, e = rsaParam.Exponent };
var bareData = Schema.Serialize(publicKey).AsSpan(4); // bare serialization
var fingerprint = BitConverter.ToInt64(SHA1.HashData(bareData), 12); // 64 lower-order bits of SHA1
PublicKeys[fingerprint] = publicKey;
Console.WriteLine($"Loaded a public key with fingerprint {fingerprint:X}");
}
private static void LoadDefaultPublicKey() // fingerprint C3B42B026CE86B21
{
LoadPublicKey(@"-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwVACPi9w23mF3tBkdZz+zwrzKOaaQdr01vAbU4E1pvkfj4sqDsm6
lyDONS789sVoD/xCS9Y0hkkC3gtL1tSfTlgCMOOul9lcixlEKzwKENj1Yz/s7daS
an9tqw3bfUV/nqgbhGX81v/+7RFAEd+RwFnK7a+XYl9sluzHRyVVaTTveB2GazTw
Efzk2DWgkBluml8OREmvfraX3bkHZJTKX4EQSjBbbdJ2ZXIsRrYOXfaA+xayEGB+
8hdlLmAjbCVfaigxX0CDqWeR1yFL9kwd9P0NsZRPsmoqVwMbMu7mStFai6aIhc3n
Slv8kg9qv1m6XHVQY3PnEw+QQtqSIXklHwIDAQAB
-----END RSA PUBLIC KEY-----");
}
internal static byte[] EncryptDecryptMessage(Span<byte> input, bool encrypt, byte[] authKey, byte[] clearSha1)
{
// first, construct AES key & IV
int x = encrypt ? 0 : 8;
byte[] aes_key = new byte[32], aes_iv = new byte[32];
using var sha1 = new SHA1Managed();
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, x, 32); // authKey[x:32]
var sha1_a = sha1.Hash;
sha1.TransformBlock(authKey, 32 + x, 16, null, 0); // authKey[32+x:16]
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, 48 + x, 16); // authKey[48+x:16]
var sha1_b = sha1.Hash;
sha1.TransformBlock(authKey, 64 + x, 32, null, 0); // authKey[64+x:32]
sha1.TransformFinalBlock(clearSha1, 4, 16); // msgKey
var sha1_c = sha1.Hash;
sha1.TransformBlock(clearSha1, 4, 16, null, 0); // msgKey
sha1.TransformFinalBlock(authKey, 96 + x, 32); // authKey[96+x:32]
var sha1_d = sha1.Hash;
Array.Copy(sha1_a, 0, aes_key, 0, 8);
Array.Copy(sha1_b, 8, aes_key, 8, 12);
Array.Copy(sha1_c, 4, aes_key, 20, 12);
Array.Copy(sha1_a, 8, aes_iv, 0, 12);
Array.Copy(sha1_b, 0, aes_iv, 12, 8);
Array.Copy(sha1_c, 16, aes_iv, 20, 4);
Array.Copy(sha1_d, 0, aes_iv, 24, 8);
return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt);
}
private static byte[] AES_IGE_EncryptDecrypt(Span<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
{
using var aes = Aes.Create();
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.Zeros;
if (aes.BlockSize != 128) throw new ApplicationException("AES Blocksize is not 16 bytes");
if (input.Length % 16 != 0) throw new ApplicationException("intput size not divisible by 16");
// code adapted from PHP implementation found at https://mgp25.com/AESIGE/
var output = new byte[input.Length];
var xPrev = aes_iv.AsSpan(encrypt ? 16 : 0, 16);
var yPrev = aes_iv.AsSpan(encrypt ? 0 : 16, 16);
var aesCrypto = encrypt ? aes.CreateEncryptor(aes_key, null) : aes.CreateDecryptor(aes_key, null);
using (aesCrypto)
{
byte[] yXOR = new byte[16];
for (int i = 0; i < input.Length; i += 16)
{
var x = input.Slice(i, 16);
var y = output.AsSpan(i, 16);
for (int j = 0; j < 16; j++)
yXOR[j] = (byte)(x[j] ^ yPrev[j]);
var yFinal = aesCrypto.TransformFinalBlock(yXOR, 0, 16);
for (int j = 0; j < 16; j++)
y[j] = (byte)(yFinal[j] ^ xPrev[j]);
xPrev = x;
yPrev = y;
}
}
return output;
}
}
}

282
Generator.cs Normal file
View file

@ -0,0 +1,282 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
namespace WTelegram
{
public static class Generator
{
//TODO: generate/merge ctor mapping table to avoid creating those at runtime from TLDefAttribute
//TODO: generate BinaryReader/Writer serialization directly to avoid using Reflection
//TODO: generate partial class with methods for functions to avoid using request classes
public static void FromJson(string jsonPath, string outputCs)
{
var schema = JsonSerializer.Deserialize<SchemaJson>(File.ReadAllText(jsonPath));
using var sw = File.CreateText(outputCs);
sw.WriteLine("using System;");
sw.WriteLine();
sw.WriteLine("namespace TL");
sw.WriteLine("{");
Dictionary<string, TypeInfo> typeInfos = new();
foreach (var ctor in schema.constructors)
{
var structName = CSharpName(ctor.predicate);
var typeInfo = typeInfos.GetOrCreate(ctor.type);
if (typeInfo.ReturnName == null) typeInfo.ReturnName = CSharpName(ctor.type);
typeInfo.Structs.Add(ctor);
if (structName == typeInfo.ReturnName) typeInfo.SameName = ctor;
}
typeInfos.Remove("Bool");
typeInfos.Remove("Vector t");
foreach (var (name, typeInfo) in typeInfos)
{
if (typeInfo.SameName == null)
{
typeInfo.NeedAbstract = -1;
if (typeInfo.Structs.Count > 1)
{
List<Param> fakeCtorParams = new();
while (typeInfo.Structs[0].@params.Length > fakeCtorParams.Count)
{
fakeCtorParams.Add(typeInfo.Structs[0].@params[fakeCtorParams.Count]);
if (!typeInfo.Structs.All(ctor => HasPrefix(ctor, fakeCtorParams)))
{
fakeCtorParams.RemoveAt(fakeCtorParams.Count - 1);
break;
}
}
if (fakeCtorParams.Count > 0)
{
typeInfo.Structs.Insert(0, typeInfo.SameName = new Constructor
{ id = null, @params = fakeCtorParams.ToArray(), predicate = typeInfo.ReturnName, type = typeInfo.ReturnName });
typeInfo.NeedAbstract = fakeCtorParams.Count;
}
}
}
else if (typeInfo.Structs.Count > 1)
{
typeInfo.NeedAbstract = typeInfo.SameName.@params.Length;
foreach (var ctor in typeInfo.Structs)
{
if (ctor == typeInfo.SameName) continue;
if (!HasPrefix(ctor, typeInfo.SameName.@params)) { typeInfo.NeedAbstract = -1; typeInfo.ReturnName += "Base"; break; }
}
}
}
foreach (var typeInfo in typeInfos.Values)
WriteTypeInfo(sw, typeInfo);
sw.WriteLine("\t// ---functions---");
sw.WriteLine();
var methods = new List<TypeInfo>();
foreach (var method in schema.methods)
{
var typeInfo = new TypeInfo { ReturnName = method.type };
typeInfo.Structs.Add(new Constructor { id = method.id, @params = method.@params, predicate = method.method, type = method.type });
WriteTypeInfo(sw, typeInfo, true);
}
sw.WriteLine("}");
void WriteTypeInfo(StreamWriter sw, TypeInfo typeInfo, bool isMethod = false)
{
var parentClass = typeInfo.NeedAbstract != 0 ? typeInfo.ReturnName : "ITLObject";
var genericType = typeInfo.ReturnName.Length == 1 ? $"<{typeInfo.ReturnName}>" : null;
if (isMethod)
parentClass = $"ITLFunction<{MapType(typeInfo.ReturnName, "")}>";
if (typeInfo.NeedAbstract == -1)
sw.WriteLine($"\tpublic abstract class {parentClass} : ITLObject {{ }}");
int skipParams = 0;
foreach (var ctor in typeInfo.Structs)
{
string className = CSharpName(ctor.predicate) + genericType;
if (ctor.id == null)
sw.Write($"\tpublic abstract class {className} : ITLObject");
else
{
int ctorId = int.Parse(ctor.id);
sw.Write($"\t[TLDef(0x{ctorId:X}, \"{ctor.predicate}#{ctorId:x8} ");
if (genericType != null) sw.Write($"{{{typeInfo.ReturnName}:Type}} ");
foreach (var parm in ctor.@params) sw.Write($"{parm.name}:{parm.type} ");
sw.WriteLine($"= {ctor.type}\")]");
sw.Write($"\tpublic class {className} : ");
sw.Write(skipParams == 0 && typeInfo.NeedAbstract > 0 ? "ITLObject" : parentClass);
}
var parms = ctor.@params.Skip(skipParams).ToArray();
if (parms.Length == 0)
{
sw.WriteLine(" { }");
continue;
}
if (parms.Length == 1)
sw.Write(" { ");
else
{
sw.WriteLine();
sw.WriteLine("\t{");
}
var hasFlagEnum = parms.Any(p => p.type.StartsWith("flags."));
if (hasFlagEnum)
{
var list = new SortedList<int, string>();
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || !parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
if (!list.ContainsKey(mask)) list[mask] = MapName(parm.name);
}
foreach (var parm in parms)
{
if (!parm.type.StartsWith("flags.") || parm.type.EndsWith("?true")) continue;
var mask = 1 << int.Parse(parm.type[6..parm.type.IndexOf('?')]);
if (list.ContainsKey(mask)) continue;
var name = MapName("has_" + parm.name);
if (list.Values.Contains(name)) name += "_field";
list[mask] = name;
}
sw.Write("\t\t[Flags] public enum Flags { ");
int lineLen = 36;
foreach (var (mask, name) in list)
{
var str = $"{name} = 0x{mask:X}, ";
if (lineLen + str.Length >= 140) { sw.WriteLine(); sw.Write("\t\t\t"); lineLen = 12; }
sw.Write(str);
lineLen += str.Length;
}
sw.WriteLine(" }");
}
foreach (var parm in parms)
{
if (parm.type.EndsWith("?true")) continue;
if (parms.Length > 1) sw.Write("\t\t");
if (parm.type == "#")
sw.Write($"public {(hasFlagEnum ? "Flags" : "int")} {parm.name};");
else
{
if (parm.type.StartsWith("flags."))
{
int qm = parm.type.IndexOf('?');
sw.Write($"[IfFlag({parm.type[6..qm]})] public {MapType(parm.type[(qm + 1)..], parm.name)} {MapName(parm.name)};");
}
else
sw.Write($"public {MapType(parm.type, parm.name)} {MapName(parm.name)};");
}
if (parms.Length > 1) sw.WriteLine();
}
if (ctorNeedClone.Contains(className))
sw.WriteLine($"\t\tpublic {className} Clone() => ({className})MemberwiseClone();");
if (parms.Length == 1)
sw.WriteLine(" }");
else
sw.WriteLine("\t}");
skipParams = typeInfo.NeedAbstract;
}
sw.WriteLine();
string MapName(string name) => name switch
{
"out" => "out_",
"static" => "static_",
"long" => "long_",
"default" => "default_",
"public" => "public_",
"params" => "params_",
"private" => "private_",
_ => name
};
string MapType(string type, string name)
{
if (type.StartsWith("Vector<", StringComparison.OrdinalIgnoreCase))
return MapType(type[7..^1], name) + "[]";
else if (type == "Bool")
return "bool";
else if (type == "bytes")
return "byte[]";
else if (type == "int128")
return "Int128";
else if (type == "int256")
return "Int256";
else if (type == "Object")
return "ITLObject";
else if (type == "!X")
return "ITLFunction<X>";
else if (typeInfos.TryGetValue(type, out var typeInfo))
return typeInfo.ReturnName;
else if (type == "int")
{
var name2 = '_' + name + '_';
if (name2.EndsWith("_date_") || name2.EndsWith("_time_") || name2 == "_expires_" || name2 == "_now_" || name2.StartsWith("_valid_"))
return "DateTime";
else
return "int";
}
else
return type;
}
}
}
static readonly HashSet<string> ctorNeedClone = new() { "User" };
private static bool HasPrefix(Constructor ctor, IList<Param> prefixParams)
{
if (ctor.@params.Length < prefixParams.Count) return false;
for (int i = 0; i < prefixParams.Count; i++)
if (ctor.@params[i].name != prefixParams[i].name || ctor.@params[i].type != prefixParams[i].type)
return false;
return true;
}
private static string CSharpName(string name)
{
name = char.ToUpper(name[0]) + name[1..];
int i;
while ((i = name.IndexOf('_')) > 0)
name = name[..i] + char.ToUpper(name[i + 1]) + name[(i + 2)..];
while ((i = name.IndexOf('.')) > 0)
name = name[..i] + '_' + char.ToUpper(name[i + 1]) + name[(i + 2)..];
return name;
}
class TypeInfo
{
public string ReturnName;
public Constructor SameName;
public List<Constructor> Structs = new();
internal int NeedAbstract; // 0:no, -1:create auto, n:use first generated constructor and skip n params
}
#pragma warning disable IDE1006 // Naming Styles
public class SchemaJson
{
public Constructor[] constructors { get; set; }
public Method[] methods { get; set; }
}
public class Constructor
{
public string id { get; set; }
public string predicate { get; set; }
public Param[] @params { get; set; }
public string type { get; set; }
}
public class Param
{
public string name { get; set; }
public string type { get; set; }
}
public class Method
{
public string id { get; set; }
public string method { get; set; }
public Param[] @params { get; set; }
public string type { get; set; }
}
#pragma warning restore IDE1006 // Naming Styles
}
}

134
Helpers.cs Normal file
View file

@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
namespace WTelegram
{
public static class Helpers
{
public static readonly System.Text.Json.JsonSerializerOptions JsonOptions = new(System.Text.Json.JsonSerializerDefaults.Web) { IncludeFields = true };
public static V GetOrCreate<K, V>(this Dictionary<K, V> dictionary, K key) where V : new()
=> dictionary.TryGetValue(key, out V value) ? value : dictionary[key] = new V();
public static void LittleEndian(byte[] buffer, int offset, int value)
{
buffer[offset + 0] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
buffer[offset + 2] = (byte)(value >> 16);
buffer[offset + 3] = (byte)(value >> 24);
}
public static void LittleEndian(byte[] buffer, int offset, long value)
{
buffer[offset + 0] = (byte)value;
buffer[offset + 1] = (byte)(value >> 8);
buffer[offset + 2] = (byte)(value >> 16);
buffer[offset + 3] = (byte)(value >> 24);
buffer[offset + 4] = (byte)(value >> 32);
buffer[offset + 5] = (byte)(value >> 40);
buffer[offset + 6] = (byte)(value >> 48);
buffer[offset + 7] = (byte)(value >> 56);
}
public static byte[] ToBigEndian(ulong value)
{
int i;
var temp = value;
for (i = 1; (temp >>= 8) != 0; i++);
var result = new byte[i];
while (--i >= 0) { result[i] = (byte)value; value >>= 8; }
return result;
}
public static ulong FromBigEndian(byte[] bytes)
{
if (bytes.Length > 8) throw new ArgumentException($"expected bytes length <=8 but got {bytes.Length}");
ulong result = 0;
foreach (byte b in bytes)
result = (result << 8) + b;
return result;
}
public static ulong PQFactorize(ulong pq)
{
if (pq < 2) return 1;
var random = new Random();
ulong g = 0;
for (int i = 0, iter = 0; i < 3 || iter < 1000; i++)
{
ulong q = (ulong)random.Next(17, 32) % (pq - 1);
ulong x = ((ulong)random.Next() + (ulong)random.Next() << 31) % (pq - 1) + 1;
ulong y = x;
int lim = 1 << (Math.Min(5, i) + 18);
for (int j = 1; j < lim; j++)
{
iter++;
ulong a = x;
ulong b = x;
ulong c = q;
// c += a * b
while (b != 0)
{
if ((b & 1) != 0)
{
c += a;
if (c >= pq)
c -= pq;
}
a += a;
if (a >= pq)
a -= pq;
b >>= 1;
}
x = c;
ulong z = x < y ? pq + x - y : x - y;
g = gcd(z, pq);
if (g != 1)
break;
if ((j & (j - 1)) == 0)
y = x;
}
if (g > 1 && g < pq)
break;
}
if (g != 0)
{
ulong other = pq / g;
if (other < g)
g = other;
}
return g;
static ulong gcd(ulong a, ulong b)
{
if (a == 0) return b;
if (b == 0) return a;
int shift = 0;
while ((a & 1) == 0 && (b & 1) == 0)
{
a >>= 1;
b >>= 1;
shift++;
}
while (true)
{
while ((a & 1) == 0)
a >>= 1;
while ((b & 1) == 0)
b >>= 1;
if (a > b)
a -= b;
else if (b > a)
b -= a;
else
return a << shift;
}
}
}
}
}

67
Session.cs Normal file
View file

@ -0,0 +1,67 @@
using System;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Text.Json;
namespace WTelegram
{
internal class Session
{
public long AuthKeyID;
public byte[] AuthKey;
public long Salt;
public long Id;
public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;
public TL.DcOption DataCenter;
public byte[] User; // serialization of TL.User
public DateTime SessionStart => _sessionStart;
private readonly DateTime _sessionStart = DateTime.UtcNow;
private string _pathname;
internal static Session LoadOrCreate(string pathname)
{
if (File.Exists(pathname))
{
try
{
var json = File.ReadAllText(pathname);
var session = JsonSerializer.Deserialize<Session>(json, Helpers.JsonOptions);
session._pathname = pathname;
Console.WriteLine("Loaded previous session");
return session;
}
catch (Exception ex)
{
Console.WriteLine($"Exception while reading session file: {ex.Message}");
}
}
var sessionId = new byte[8];
Encryption.RNG.GetBytes(sessionId);
return new Session { _pathname = pathname, Id = BitConverter.ToInt64(sessionId) };
}
internal void Save()
{
//TODO: Add some encryption (with prepended SHA256) to prevent from stealing the key
File.WriteAllText(_pathname, JsonSerializer.Serialize(this, Helpers.JsonOptions));
}
internal (long msgId, int seqno) NewMsg(bool isContent)
{
long msgId = DateTime.UtcNow.Ticks + ServerTicksOffset - 621355968000000000L;
msgId = msgId * 428 + (msgId >> 24) * 25110956; // approximately unixtime*2^32 and divisible by 4
if (msgId <= LastSentMsgId) msgId = LastSentMsgId += 4; else LastSentMsgId = msgId;
int seqno = isContent ? Seqno++ * 2 + 1 : Seqno * 2;
Save();
return (msgId, seqno);
}
internal DateTime MsgIdToStamp(long serverMsgId)
=> new((serverMsgId >> 32) * 10000000 - ServerTicksOffset + 621355968000000000L, DateTimeKind.Utc);
}
}

297
TL.MTProto.cs Normal file
View file

@ -0,0 +1,297 @@
using System;
namespace TL
{
[TLDef(0x5162463, "resPQ#05162463 nonce:int128 server_nonce:int128 pq:bytes server_public_key_fingerprints:Vector<long> = ResPQ")]
public class ResPQ : ITLObject
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] pq;
public long[] server_public_key_fingerprints;
}
[TLDef(0X83C95AEC, "p_q_inner_data# pq:string p:string q:string nonce:int128 server_nonce:int128 new_nonce:int256 = P_Q_inner_data")]
public class PQInnerData : ITLObject
{
public byte[] pq;
public byte[] p;
public byte[] q;
public Int128 nonce;
public Int128 server_nonce;
public Int256 new_nonce;
}
[TLDef(0xA9F55F95, "p_q_inner_data_dc#a9f55f95 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int = P_Q_inner_data")]
public class PQInnerDataDC : PQInnerData
{
public int dc;
}
[TLDef(0x56FDDF88, "p_q_inner_data_temp_dc#56fddf88 pq:bytes p:bytes q:bytes nonce:int128 server_nonce:int128 new_nonce:int256 dc:int expires_in:int = P_Q_inner_data")]
public class PQInnerDataTempDC : PQInnerData
{
public int dc;
public int expires_in; // seconds
}
public abstract class ServerDHParams : ITLObject
{
public Int128 nonce;
public Int128 server_nonce;
}
[TLDef(0X79CB045D, "server_DH_params_fail#79cb045d nonce:int128 server_nonce:int128 new_nonce_hash:int128 = Server_DH_Params")]
public class ServerDHParamsFail : ServerDHParams { public Int128 new_nonce_hash; }
[TLDef(0XD0E8075C, "server_DH_params_ok#d0e8075c nonce:int128 server_nonce:int128 encrypted_answer:bytes = Server_DH_Params")]
public class ServerDHParamsOk : ServerDHParams { public byte[] encrypted_answer; }
[TLDef(0xB5890DBA, "server_DH_inner_data#b5890dba nonce:int128 server_nonce:int128 g:int dh_prime:bytes g_a:bytes server_time:int = Server_DH_inner_data")]
public class ServerDHInnerData : ITLObject
{
public Int128 nonce;
public Int128 server_nonce;
public int g;
public byte[] dh_prime;
public byte[] g_a;
public DateTime server_time;
}
[TLDef(0x6643B654, "client_DH_inner_data#6643b654 nonce:int128 server_nonce:int128 retry_id:long g_b:bytes = Client_DH_Inner_Data")]
public class ClientDHInnerData : ITLObject
{
public Int128 nonce;
public Int128 server_nonce;
public long retry_id;
public byte[] g_b;
}
public abstract class SetClientDHParamsAnswer : ITLObject
{
public Int128 nonce;
public Int128 server_nonce;
public Int128 new_nonce_hashN; // 16 low order bytes from SHA1(new_nonce + (01=ok, 02=retry, 03=fail) + 8 high order bytes from SHA1(auth_key))
}
[TLDef(0x3BCBF734, "dh_gen_ok#3bcbf734 nonce:int128 server_nonce:int128 new_nonce_hash1:int128 = Set_client_DH_params_answer")]
public class DHGenOk : SetClientDHParamsAnswer { }
[TLDef(0x46DC1FB9, "dh_gen_retry#46dc1fb9 nonce:int128 server_nonce:int128 new_nonce_hash2:int128 = Set_client_DH_params_answer")]
public class DHGenRetry : SetClientDHParamsAnswer { }
[TLDef(0xA69DAE02, "dh_gen_fail#a69dae02 nonce:int128 server_nonce:int128 new_nonce_hash3:int128 = Set_client_DH_params_answer")]
public class DHGenFail : SetClientDHParamsAnswer { }
[TLDef(0x75A3F765, "bind_auth_key_inner#75a3f765 nonce:long temp_auth_key_id:long perm_auth_key_id:long temp_session_id:long expires_at:int = BindAuthKeyInner")]
public class BindAuthKeyInner : ITLObject
{
public long nonce;
public long temp_auth_key_id;
public long perm_auth_key_id;
public long temp_session_id;
public DateTime expires_at;
}
[TLDef(0xF35C6D01, "rpc_result#f35c6d01 req_msg_id:long result:Object = RpcResult")]
public class RpcResult : ITLObject
{
public long req_msg_id;
public object result;
}
[TLDef(0x2144CA19, "rpc_error#2144ca19 error_code:int error_message:string = RpcError")]
public class RpcError : ITLObject
{
public int error_code;
public string error_message;
}
public abstract class RpcDropAnswer : ITLObject { }
[TLDef(0x5E2AD36E, "rpc_answer_unknown#5e2ad36e = RpcDropAnswer")]
public class RpcAnswerUnknown : RpcDropAnswer { }
[TLDef(0xCD78E586, "rpc_answer_dropped_running#cd78e586 = RpcDropAnswer")]
public class RpcAnswerDroppedRunning : RpcDropAnswer { }
[TLDef(0xA43AD8B7, "rpc_answer_dropped#a43ad8b7 msg_id:long seq_no:int bytes:int = RpcDropAnswer")]
public class RpcAnswerDropped : RpcDropAnswer
{
public long msg_id;
public int seq_no;
public int bytes;
}
[TLDef(0x949D9DC, "future_salt#0949d9dc valid_since:int valid_until:int salt:long = FutureSalt")]
public class FutureSalt : ITLObject
{
public DateTime valid_since;
public DateTime valid_until;
public long salt;
}
[TLDef(0xAE500895, "future_salts#ae500895 req_msg_id:long now:int salts:vector<future_salt> = FutureSalts")]
public class FutureSalts : ITLObject
{
public long req_msg_id;
public DateTime now;
public FutureSalt[] salts;
}
[TLDef(0x347773C5, "pong#347773c5 msg_id:long ping_id:long = Pong")]
public class Pong : ITLObject
{
public long msg_id;
public long ping_id;
}
public abstract class DestroySessionRes : ITLObject { public long session_id; }
[TLDef(0xE22045FC, "destroy_session_ok#e22045fc session_id:long = DestroySessionRes")]
public class DestroySessionOk : DestroySessionRes { }
[TLDef(0x62D350C9, "destroy_session_none#62d350c9 session_id:long = DestroySessionRes")]
public class DestroySessionNone : DestroySessionRes { }
public abstract class NewSession : ITLObject { }
[TLDef(0x9EC20908, "new_session_created#9ec20908 first_msg_id:long unique_id:long server_salt:long = NewSession")]
public class NewSessionCreated : NewSession
{
public long first_msg_id;
public long unique_id;
public long server_salt;
}
public abstract class MessageContainer : ITLObject { }
[TLDef(0x73F1F8DC, "msg_container#73f1f8dc messages:vector<%Message> = MessageContainer")]
public class MsgContainer : MessageContainer { public _Message[] messages; }
#pragma warning disable IDE1006 // Naming Styles
//[TLDef("message msg_id:long seqno:int bytes:int body:Object = Message")]
public class _Message
{
public long msg_id;
public int seqno;
public int bytes;
public ITLObject body;
}
#pragma warning restore IDE1006 // Naming Styles
public abstract class MessageCopy : ITLObject { }
[TLDef(0xE06046B2, "msg_copy#e06046b2 orig_message:Message = MessageCopy")]
public class MsgCopy : MessageCopy { public _Message orig_message; }
[TLDef(0x3072CFA1, "gzip_packed#3072cfa1 packed_data:bytes = Object")]
public class GzipPacked : ITLObject { public byte[] packed_data; }
[TLDef(0x62D6B459, "msgs_ack#62d6b459 msg_ids:Vector<long> = MsgsAck")]
public class MsgsAck : ITLObject { public long[] msg_ids; }
[TLDef(0xA7EFF811, "bad_msg_notification#a7eff811 bad_msg_id:long bad_msg_seqno:int error_code:int = BadMsgNotification")]
public class BadMsgNotification : ITLObject
{
public long bad_msg_id;
public int bad_msg_seqno;
public int error_code;
}
[TLDef(0xEDAB447B, "bad_server_salt#edab447b bad_msg_id:long bad_msg_seqno:int error_code:int new_server_salt:long = BadMsgNotification")]
public class BadServerSalt : BadMsgNotification { public long new_server_salt; }
[TLDef(0x7D861A08, "msg_resend_req#7d861a08 msg_ids:Vector<long> = MsgResendReq")]
public class MsgResendReq : ITLObject { public long[] msg_ids; }
[TLDef(0xDA69FB52, "msgs_state_req#da69fb52 msg_ids:Vector<long> = MsgsStateReq")]
public class MsgsStateReq : ITLObject { public long[] msg_ids; }
[TLDef(0x4DEB57D, "msgs_state_info#04deb57d req_msg_id:long info:bytes = MsgsStateInfo")]
public class MsgsStateInfo : ITLObject
{
public long req_msg_id;
public byte[] info;
}
[TLDef(0x8CC0D131, "msgs_all_info#8cc0d131 msg_ids:Vector<long> info:bytes = MsgsAllInfo")]
public class MsgsAllInfo : ITLObject
{
public long[] msg_ids;
public byte[] info;
}
public abstract class MsgDetailedInfoBase : ITLObject { }
[TLDef(0x276D3EC6, "msg_detailed_info#276d3ec6 msg_id:long answer_msg_id:long bytes:int status:int = MsgDetailedInfo")]
public class MsgDetailedInfo : MsgDetailedInfoBase
{
public long msg_id;
public long answer_msg_id;
public int bytes;
public int status;
}
[TLDef(0x809DB6DF, "msg_new_detailed_info#809db6df answer_msg_id:long bytes:int status:int = MsgDetailedInfo")]
public class MsgNewDetailedInfo : MsgDetailedInfoBase
{
public long answer_msg_id;
public int bytes;
public int status;
}
[TLDef(0x7A19CB76, "rsa_public_key n:string e:string = RSAPublicKey")]
public class RSAPublicKey : ITLObject
{
public byte[] n;
public byte[] e;
}
public abstract class DestroyAuthKeyRes : ITLObject { }
[TLDef(0xF660E1D4, "destroy_auth_key_ok#f660e1d4 = DestroyAuthKeyRes")]
public class DestroyAuthKeyOk : DestroyAuthKeyRes { }
[TLDef(0xA9F2259, "destroy_auth_key_none#0a9f2259 = DestroyAuthKeyRes")]
public class DestroyAuthKeyNone : DestroyAuthKeyRes { }
[TLDef(0xEA109B13, "destroy_auth_key_fail#ea109b13 = DestroyAuthKeyRes")]
public class DestroyAuthKeyFail : DestroyAuthKeyRes { }
// ---functions---
[TLDef(0X60469778, "req_pq#60469778 nonce:int128 = ResPQ")]
public class ReqPQ : ITLFunction<ResPQ> { public Int128 nonce; }
[TLDef(0xBE7E8EF1, "req_pq_multi#be7e8ef1 nonce:int128 = ResPQ")]
public class ReqPQmulti : ResPQ { }
[TLDef(0xD712E4BE, "req_DH_params#d712e4be nonce:int128 server_nonce:int128 p:bytes q:bytes public_key_fingerprint:long encrypted_data:bytes = Server_DH_Params")]
public class ReqDHParams : ITLFunction<ServerDHParams>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] p;
public byte[] q;
public long public_key_fingerprint;
public byte[] encrypted_data;
}
[TLDef(0xF5045F1F, "set_client_DH_params#f5045f1f nonce:int128 server_nonce:int128 encrypted_data:bytes = Set_client_DH_params_answer")]
public class SetClientDHParams : ITLFunction<SetClientDHParamsAnswer>
{
public Int128 nonce;
public Int128 server_nonce;
public byte[] encrypted_data;
}
[TLDef(0x58E4A740, "rpc_drop_answer#58e4a740 req_msg_id:long = RpcDropAnswer")]
public class ReqRpcDropAnswer : ITLFunction<RpcDropAnswer> { public long req_msg_id; }
[TLDef(0xB921BD04, "get_future_salts#b921bd04 num:int = FutureSalts")]
public class GetFutureSalts : ITLFunction<FutureSalts> { public int num; }
[TLDef(0x7ABE77EC, "ping#7abe77ec ping_id:long = Pong")]
public class Ping : ITLFunction<Pong> { public long ping_id; }
[TLDef(0xF3427B8C, "ping_delay_disconnect#f3427b8c ping_id:long disconnect_delay:int = Pong")]
public class PingDelayDisconnect : ITLFunction<Pong>
{
public long ping_id;
public int disconnect_delay; // seconds
}
[TLDef(0xE7512126, "destroy_session#e7512126 session_id:long = DestroySessionRes")]
public class DestroySession : ITLFunction<DestroySessionRes> { public long session_id; }
[TLDef(0x9299359F, "http_wait#9299359f max_delay:int wait_after:int max_wait:int = HttpWait")]
public class HttpWait : ITLObject
{
public int max_delay; // ms
public int wait_after; // ms
public int max_wait; // ms
}
[TLDef(0xD1435160, "destroy_auth_key#d1435160 = DestroyAuthKeyRes")]
public class DestroyAuthKey : ITLFunction<DestroyAuthKeyRes> { }
}

7429
TL.Schema.cs Normal file

File diff suppressed because it is too large Load diff

182
TL.Secret.cs Normal file
View file

@ -0,0 +1,182 @@
using System;
namespace TL
{
public abstract class DecryptedMessageBase : ITLObject { }
[TLDef(0x1F814F1F, "decryptedMessage#1f814f1f random_id:long random_bytes:bytes message:string media:DecryptedMessageMedia = DecryptedMessage")]
public class DecryptedMessage : DecryptedMessageBase
{
public long random_id;
public byte[] random_bytes;
public string message;
public DecryptedMessageMedia media;
}
[TLDef(0xAA48327D, "decryptedMessageService#aa48327d random_id:long random_bytes:bytes action:DecryptedMessageAction = DecryptedMessage")]
public class DecryptedMessageService : DecryptedMessageBase
{
public long random_id;
public byte[] random_bytes;
public DecryptedMessageAction action;
}
public abstract class DecryptedMessageMedia : ITLObject { }
[TLDef(0x89F5C4A, "decryptedMessageMediaEmpty#089f5c4a = DecryptedMessageMedia")]
public class DecryptedMessageMediaEmpty : DecryptedMessageMedia { }
[TLDef(0x32798A8C, "decryptedMessageMediaPhoto#32798a8c thumb:bytes thumb_w:int thumb_h:int w:int h:int size:int key:bytes iv:bytes = DecryptedMessageMedia")]
public class DecryptedMessageMediaPhoto : DecryptedMessageMedia
{
public byte[] thumb;
public int thumb_w;
public int thumb_h;
public int w;
public int h;
public int size;
public byte[] key;
public byte[] iv;
}
[TLDef(0x4CEE6EF3, "decryptedMessageMediaVideo#4cee6ef3 thumb:bytes thumb_w:int thumb_h:int duration:int w:int h:int size:int key:bytes iv:bytes = DecryptedMessageMedia")]
public class DecryptedMessageMediaVideo : DecryptedMessageMedia
{
public byte[] thumb;
public int thumb_w;
public int thumb_h;
public int duration;
public int w;
public int h;
public int size;
public byte[] key;
public byte[] iv;
}
[TLDef(0x35480A59, "decryptedMessageMediaGeoPoint#35480a59 lat:double long:double = DecryptedMessageMedia")]
public class DecryptedMessageMediaGeoPoint : DecryptedMessageMedia
{
public double lat;
public double long_;
}
[TLDef(0x588A0A97, "decryptedMessageMediaContact#588a0a97 phone_number:string first_name:string last_name:string user_id:int = DecryptedMessageMedia")]
public class DecryptedMessageMediaContact : DecryptedMessageMedia
{
public string phone_number;
public string first_name;
public string last_name;
public int user_id;
}
[TLDef(0xB095434B, "decryptedMessageMediaDocument#b095434b thumb:bytes thumb_w:int thumb_h:int file_name:string mime_type:string size:int key:bytes iv:bytes = DecryptedMessageMedia")]
public class DecryptedMessageMediaDocument : DecryptedMessageMedia
{
public byte[] thumb;
public int thumb_w;
public int thumb_h;
public string file_name;
public string mime_type;
public int size;
public byte[] key;
public byte[] iv;
}
[TLDef(0x6080758F, "decryptedMessageMediaAudio#6080758f duration:int size:int key:bytes iv:bytes = DecryptedMessageMedia")]
public class DecryptedMessageMediaAudio : DecryptedMessageMedia
{
public int duration;
public int size;
public byte[] key;
public byte[] iv;
}
[TLDef(0xFA95B0DD, "decryptedMessageMediaExternalDocument#fa95b0dd id:long access_hash:long date:int mime_type:string size:int thumb:PhotoSize dc_id:int attributes:Vector<DocumentAttribute> = DecryptedMessageMedia")]
public class DecryptedMessageMediaExternalDocument : DecryptedMessageMedia
{
public long id;
public long access_hash;
public DateTime date;
public string mime_type;
public int size;
public PhotoSizeBase thumb;
public int dc_id;
public DocumentAttribute[] attributes;
}
[TLDef(0x8A0DF56F, "decryptedMessageMediaVenue#8a0df56f lat:double long:double title:string address:string provider:string venue_id:string = DecryptedMessageMedia")]
public class DecryptedMessageMediaVenue : DecryptedMessageMedia
{
public double lat;
public double long_;
public string title;
public string address;
public string provider;
public string venue_id;
}
[TLDef(0xE50511D8, "decryptedMessageMediaWebPage#e50511d8 url:string = DecryptedMessageMedia")]
public class DecryptedMessageMediaWebPage : DecryptedMessageMedia { public string url; }
public abstract class DecryptedMessageAction : ITLObject { }
[TLDef(0xA1733AEC, "decryptedMessageActionSetMessageTTL#a1733aec ttl_seconds:int = DecryptedMessageAction")]
public class DecryptedMessageActionSetMessageTTL : DecryptedMessageAction { public int ttl_seconds; }
[TLDef(0xC4F40BE, "decryptedMessageActionReadMessages#0c4f40be random_ids:Vector<long> = DecryptedMessageAction")]
public class DecryptedMessageActionReadMessages : DecryptedMessageAction { public long[] random_ids; }
[TLDef(0x65614304, "decryptedMessageActionDeleteMessages#65614304 random_ids:Vector<long> = DecryptedMessageAction")]
public class DecryptedMessageActionDeleteMessages : DecryptedMessageAction { public long[] random_ids; }
[TLDef(0x8AC1F475, "decryptedMessageActionScreenshotMessages#8ac1f475 random_ids:Vector<long> = DecryptedMessageAction")]
public class DecryptedMessageActionScreenshotMessages : DecryptedMessageAction { public long[] random_ids; }
[TLDef(0x6719E45C, "decryptedMessageActionFlushHistory#6719e45c = DecryptedMessageAction")]
public class DecryptedMessageActionFlushHistory : DecryptedMessageAction { }
[TLDef(0x511110B0, "decryptedMessageActionResend#511110b0 start_seq_no:int end_seq_no:int = DecryptedMessageAction")]
public class DecryptedMessageActionResend : DecryptedMessageAction
{
public int start_seq_no;
public int end_seq_no;
}
[TLDef(0xF3048883, "decryptedMessageActionNotifyLayer#f3048883 layer:int = DecryptedMessageAction")]
public class DecryptedMessageActionNotifyLayer : DecryptedMessageAction { public int layer; }
[TLDef(0xCCB27641, "decryptedMessageActionTyping#ccb27641 action:SendMessageAction = DecryptedMessageAction")]
public class DecryptedMessageActionTyping : DecryptedMessageAction { public SendMessageAction action; }
[TLDef(0xF3C9611B, "decryptedMessageActionRequestKey#f3c9611b exchange_id:long g_a:bytes = DecryptedMessageAction")]
public class DecryptedMessageActionRequestKey : DecryptedMessageAction
{
public long exchange_id;
public byte[] g_a;
}
[TLDef(0x6FE1735B, "decryptedMessageActionAcceptKey#6fe1735b exchange_id:long g_b:bytes key_fingerprint:long = DecryptedMessageAction")]
public class DecryptedMessageActionAcceptKey : DecryptedMessageAction
{
public long exchange_id;
public byte[] g_b;
public long key_fingerprint;
}
[TLDef(0xDD05EC6B, "decryptedMessageActionAbortKey#dd05ec6b exchange_id:long = DecryptedMessageAction")]
public class DecryptedMessageActionAbortKey : DecryptedMessageAction { public long exchange_id; }
[TLDef(0xEC2E0B9B, "decryptedMessageActionCommitKey#ec2e0b9b exchange_id:long key_fingerprint:long = DecryptedMessageAction")]
public class DecryptedMessageActionCommitKey : DecryptedMessageAction
{
public long exchange_id;
public long key_fingerprint;
}
[TLDef(0xA82FDD63, "decryptedMessageActionNoop#a82fdd63 = DecryptedMessageAction")]
public class DecryptedMessageActionNoop : DecryptedMessageAction { }
[TLDef(0x1BE31789, "decryptedMessageLayer#1be31789 random_bytes:bytes layer:int in_seq_no:int out_seq_no:int message:DecryptedMessage = DecryptedMessageLayer")]
public class DecryptedMessageLayer : ITLObject
{
public byte[] random_bytes;
public int layer;
public int in_seq_no;
public int out_seq_no;
public DecryptedMessageBase message;
}
[TLDef(0x7C596B46, "fileLocationUnavailable#7c596b46 volume_id:long local_id:int secret:long = FileLocation")]
public class FileLocationUnavailable : FileLocation
{
public long volume_id;
public int local_id;
public long secret;
}
[TLDef(0x53D69076, "fileLocation#53d69076 dc_id:int volume_id:long local_id:int secret:long = FileLocation")]
public class FileLocation_ : FileLocation
{
public int dc_id;
public long volume_id;
public int local_id;
public long secret;
}
// ---functions---
}

309
TL.cs Normal file
View file

@ -0,0 +1,309 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
namespace TL
{
public interface ITLObject { }
public interface ITLFunction<R> : ITLObject { }
public static class Schema
{
public const int Layer = 121;
public readonly static Dictionary<uint, Type> Mappings = new();
public const int VectorCtor = 0X1CB5C415;
static Schema()
{
foreach (Type type in Assembly.GetExecutingAssembly().GetTypes())
{
var tlDef = type.GetCustomAttribute<TLDefAttribute>(false);
if (tlDef != null) Mappings[tlDef.CtorNb] = type;
}
}
internal static byte[] Serialize(ITLObject msg)
{
using var memStream = new MemoryStream(1024);
using (var writer = new BinaryWriter(memStream))
Serialize(writer, msg);
return memStream.ToArray();
}
internal static T Deserialize<T>(byte[] bytes)
{
using var memStream = new MemoryStream(bytes);
using var reader = new BinaryReader(memStream);
return Deserialize<T>(reader);
}
internal static void Serialize(BinaryWriter writer, ITLObject msg)
{
var type = msg.GetType();
var ctorNb = type.GetCustomAttribute<TLDefAttribute>().CtorNb;
writer.Write(ctorNb);
SerializeObject(writer, msg);
}
internal static T Deserialize<T>(BinaryReader reader)
{
var ctorNb = reader.ReadUInt32();
if (!Mappings.TryGetValue(ctorNb, out var realType))
throw new ApplicationException($"Cannot find type for ctor #{ctorNb:x}");
return (T)DeserializeObject(reader, realType);
}
internal static void SerializeObject(BinaryWriter writer, object obj)
{
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
IfFlagAttribute ifFlag;
foreach (var field in fields)
{
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
object value = field.GetValue(obj);
SerializeValue(writer, value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
}
}
internal static object DeserializeObject(BinaryReader reader, Type type)
{
var obj = Activator.CreateInstance(type);
var fields = obj.GetType().GetFields().GroupBy(f => f.DeclaringType).Reverse().SelectMany(g => g);
int flags = 0;
IfFlagAttribute ifFlag;
foreach (var field in fields)
{
if (((ifFlag = field.GetCustomAttribute<IfFlagAttribute>()) != null) && (flags & (1 << ifFlag.Bit)) == 0) continue;
object value = DeserializeValue(reader, field.FieldType);
field.SetValue(obj, value);
if (field.Name.Equals("Flags", StringComparison.OrdinalIgnoreCase)) flags = (int)value;
}
return type == typeof(GzipPacked) ? UnzipPacket((GzipPacked)obj) : obj;
}
internal static void SerializeValue(BinaryWriter writer, object value)
{
var type = value.GetType();
switch (Type.GetTypeCode(type))
{
case TypeCode.Int32: writer.Write((int)value); break;
case TypeCode.UInt32: writer.Write((uint)value); break;
case TypeCode.Int64: writer.Write((long)value); break;
case TypeCode.UInt64: writer.Write((ulong)value); break;
case TypeCode.Double: writer.Write((double)value); break;
case TypeCode.String: SerializeBytes(writer, Encoding.UTF8.GetBytes((string)value)); break;
case TypeCode.DateTime: writer.Write((uint)(((DateTime)value).ToUniversalTime().Ticks / 10000000 - 62135596800L)); break;
case TypeCode.Boolean: writer.Write((bool)value ? 0x997275b5 : 0xbc799737); break;
case TypeCode.Object:
if (type.IsArray)
{
if (value is byte[] bytes)
SerializeBytes(writer, bytes);
else
SerializeVector(writer, (Array)value);
}
else if (value is Int128 int128)
writer.Write(int128);
else if (value is Int256 int256)
writer.Write(int256);
else if (type.IsValueType)
SerializeObject(writer, value);
else if (value is ITLObject tlObject)
Serialize(writer, tlObject);
else
ShouldntBeHere();
break;
default:
ShouldntBeHere();
break;
}
}
internal static object DeserializeValue(BinaryReader reader, Type type)
{
switch (Type.GetTypeCode(type))
{
case TypeCode.Int32: return reader.ReadInt32();
case TypeCode.UInt32: return reader.ReadUInt32();
case TypeCode.Int64: return reader.ReadInt64();
case TypeCode.UInt64: return reader.ReadUInt64();
case TypeCode.Double: return reader.ReadDouble();
case TypeCode.String: return Encoding.UTF8.GetString(DeserializeBytes(reader));
case TypeCode.DateTime: return new DateTime((reader.ReadUInt32() + 62135596800L) * 10000000, DateTimeKind.Utc);
case TypeCode.Boolean:
return reader.ReadUInt32() switch
{
0x997275b5 => true,
0xbc799737 => false,
var value => throw new ApplicationException($"Invalid boolean value #{value:x}")
};
case TypeCode.Object:
if (type.IsArray)
{
if (type == typeof(byte[]))
return DeserializeBytes(reader);
else if (type == typeof(_Message[]))
return DeserializeMessages(reader);
else
return DeserializeVector(reader, type);
}
else if (type == typeof(Int128))
return new Int128(reader);
else if (type == typeof(Int256))
return new Int256(reader);
else if (type.IsValueType)
return DeserializeObject(reader, type);
else
return Deserialize<object>(reader);
default:
ShouldntBeHere();
return null;
}
}
private static void SerializeVector(BinaryWriter writer, Array array)
{
writer.Write(VectorCtor);
int count = array.Length;
writer.Write(count);
for (int i = 0; i < count; i++)
SerializeValue(writer, array.GetValue(i));
}
private static object DeserializeVector(BinaryReader reader, Type type)
{
var ctorNb = reader.ReadInt32();
if (ctorNb != VectorCtor) throw new ApplicationException($"Cannot deserialize {type.Name} with ctor #{ctorNb:x}");
var elementType = type.GetElementType();
int count = reader.ReadInt32();
Array array = (Array)Activator.CreateInstance(type, count);
for (int i = 0; i < count; i++)
array.SetValue(DeserializeValue(reader, elementType), i);
return array;
}
private static void SerializeBytes(BinaryWriter writer, byte[] bytes)
{
int length = bytes.Length;
if (length < 254)
writer.Write((byte)length);
else
{
writer.Write((byte)254);
writer.Write(BitConverter.GetBytes(length)[0..3]);
length += 3;
}
writer.Write(bytes);
while (++length % 4 != 0) writer.Write((byte)0);
}
private static byte[] DeserializeBytes(BinaryReader reader)
{
byte[] bytes;
int length = reader.ReadByte();
if (length < 254)
bytes = reader.ReadBytes(length);
else
{
length = reader.ReadInt16() + (reader.ReadByte() << 16);
bytes = reader.ReadBytes(length);
length += 3;
}
while (++length % 4 != 0) reader.ReadByte();
return bytes;
}
private static _Message[] DeserializeMessages(BinaryReader reader)
{
int count = reader.ReadInt32();
var array = new _Message[count];
for (int i = 0; i < count; i++)
{
array[i] = new _Message
{
msg_id = reader.ReadInt64(),
seqno = reader.ReadInt32(),
bytes = reader.ReadInt32(),
};
var pos = reader.BaseStream.Position;
try
{
array[i].body = (ITLObject)DeserializeValue(reader, typeof(ITLObject));
}
catch (Exception ex)
{
Console.WriteLine(ex);
}
reader.BaseStream.Position = pos + array[i].bytes;
}
return array;
}
private static object UnzipPacket(GzipPacked obj)
{
using var reader = new BinaryReader(new GZipStream(new MemoryStream(obj.packed_data), CompressionMode.Decompress));
var result = DeserializeValue(reader, typeof(object));
Console.WriteLine($" → {result.GetType().Name}");
return result;
}
#if DEBUG
private static void ShouldntBeHere() => System.Diagnostics.Debugger.Break();
#else
private static void ShouldntBeHere() => throw new NotImplementedException("You've reached an unexpected point in code");
#endif
}
[AttributeUsage(AttributeTargets.Class)]
public class TLDefAttribute : Attribute
{
public readonly uint CtorNb;
public TLDefAttribute(uint ctorNb, string _) => CtorNb = ctorNb;
/*public TLDefAttribute(string def)
{
var hash = def.IndexOfAny(new[] { '#', ' ' });
CtorNb = def[hash] == ' ' ? Force.Crc32.Crc32Algorithm.Compute(System.Text.Encoding.UTF8.GetBytes(def))
: uint.Parse(def[(hash + 1)..def.IndexOf(' ', hash)], System.Globalization.NumberStyles.HexNumber);
}*/
}
[AttributeUsage(AttributeTargets.Field)]
public class IfFlagAttribute : Attribute
{
public readonly int Bit;
public IfFlagAttribute(int bit) => Bit = bit;
}
public struct Int128
{
public byte[] raw;
public Int128(BinaryReader reader) => raw = reader.ReadBytes(16);
public Int128(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[16]);
public static bool operator ==(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return false; return true; }
public static bool operator !=(Int128 left, Int128 right) { for (int i = 0; i < 16; i++) if (left.raw[i] != right.raw[i]) return true; return false; }
public override bool Equals(object obj) => obj is Int128 other && this == other;
public override int GetHashCode() => HashCode.Combine(raw[0], raw[1]);
public static implicit operator byte[](Int128 int128) => int128.raw;
}
public struct Int256
{
public byte[] raw;
public Int256(BinaryReader reader) => raw = reader.ReadBytes(32);
public Int256(RNGCryptoServiceProvider rng) => rng.GetBytes(raw = new byte[32]);
public static bool operator ==(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return false; return true; }
public static bool operator !=(Int256 left, Int256 right) { for (int i = 0; i < 32; i++) if (left.raw[i] != right.raw[i]) return true; return false; }
public override bool Equals(object obj) => obj is Int256 other && this == other;
public override int GetHashCode() => HashCode.Combine(raw[0], raw[1]);
public static implicit operator byte[](Int256 int256) => int256.raw;
}
}

13
WTelegramClient.csproj Normal file
View file

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net5.0</TargetFramework>
<RootNamespace>WTelegram</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Crc32.NET" Version="1.2.0" />
</ItemGroup>
</Project>

25
WTelegramClient.sln Normal file
View file

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.31515.178
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WTelegramClient", "WTelegramClient.csproj", "{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ABB3CB38-A5EC-4428-BB72-06C6BA99DD81}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5A8D162E-4943-4748-A046-A17BA8C10ACC}
EndGlobalSection
EndGlobal