diff --git a/ci.yml b/ci.yml index fabcbae..1c49031 100644 --- a/ci.yml +++ b/ci.yml @@ -2,7 +2,7 @@ pr: none trigger: - master -name: 0.9.2-ci.$(Rev:r) +name: 0.9.3-ci.$(Rev:r) pool: vmImage: ubuntu-latest diff --git a/src/Client.cs b/src/Client.cs index 5fa27df..b97abde 100644 --- a/src/Client.cs +++ b/src/Client.cs @@ -233,7 +233,6 @@ namespace WTelegram } var buffer = memStream.GetBuffer(); int frameLength = (int)memStream.Length; - //TODO: support Quick Ack? BinaryPrimitives.WriteInt32LittleEndian(buffer, frameLength + 4); // patch frame_len with correct value uint crc = Force.Crc32.Crc32Algorithm.Compute(buffer, 0, frameLength); writer.Write(crc); // int32 frame_crc @@ -298,8 +297,9 @@ namespace WTelegram 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 = BinaryPrimitives.ReadInt64LittleEndian(data); + if (authKeyId != _session.AuthKeyID) + throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}"); if (authKeyId == 0) // Unencrypted message { using var reader = new BinaryReader(new MemoryStream(data, 8, data.Length - 8)); @@ -312,8 +312,6 @@ namespace WTelegram Helpers.Log(1, $"Receiving {obj.GetType().Name,-50} {_session.MsgIdToStamp(msgId):u} {((msgId & 2) == 0 ? "": "NAR")} unencrypted"); return obj; } - else if (authKeyId != _session.AuthKeyID) - throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}"); else { #if MTPROTO1 @@ -330,6 +328,7 @@ namespace WTelegram var msgId = _lastRecvMsgId = reader.ReadInt64();// int64 message_id var seqno = reader.ReadInt32(); // int32 msg_seqno var length = reader.ReadInt32(); // int32 message_data_length + var msgStamp = _session.MsgIdToStamp(msgId); if (serverSalt != _session.Salt) { @@ -341,6 +340,8 @@ namespace WTelegram 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 ((msgStamp - DateTime.UtcNow).Ticks / TimeSpan.TicksPerSecond is > 30 or < -300) + return null; #if MTPROTO1 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(Sha1Recv.ComputeHash(decrypted_data, 0, 32 + length).AsSpan(4))) @@ -355,18 +356,18 @@ namespace WTelegram var ctorNb = reader.ReadUInt32(); if (ctorNb == Schema.MsgContainer) { - Helpers.Log(1, $"Receiving {"MsgContainer",-50} {_session.MsgIdToStamp(msgId):u} (svc)"); + Helpers.Log(1, $"Receiving {"MsgContainer",-50} {msgStamp:u} (svc)"); return ReadMsgContainer(reader); } else if (ctorNb == Schema.RpcResult) { - Helpers.Log(1, $"Receiving {"RpcResult",-50} {_session.MsgIdToStamp(msgId):u}"); + Helpers.Log(1, $"Receiving {"RpcResult",-50} {msgStamp:u}"); return ReadRpcResult(reader); } else { var obj = reader.ReadTLObject(ctorNb); - Helpers.Log(1, $"Receiving {obj.GetType().Name,-50} {_session.MsgIdToStamp(msgId):u} {((seqno & 1) != 0 ? "" : "(svc)")} {((msgId & 2) == 0 ? "" : "NAR")}"); + Helpers.Log(1, $"Receiving {obj.GetType().Name,-50} {msgStamp:u} {((seqno & 1) != 0 ? "" : "(svc)")} {((msgId & 2) == 0 ? "" : "NAR")}"); return obj; } } @@ -534,10 +535,20 @@ namespace WTelegram private async Task Reactor(CancellationToken ct) { - while (!ct.IsCancellationRequested) + try { - var obj = await RecvInternalAsync(ct); - await HandleMessageAsync(obj); + while (!ct.IsCancellationRequested) + { + var obj = await RecvInternalAsync(ct); + if (obj == null) continue; // ignored message :| + await HandleMessageAsync(obj); + } + } + catch (OperationCanceledException) + { } + catch (Exception ex) + { + Helpers.Log(5, $"An exception occured in the reactor: {ex}"); } } diff --git a/src/Compat.cs b/src/Compat.cs index 704ed4a..1f6f200 100644 --- a/src/Compat.cs +++ b/src/Compat.cs @@ -30,10 +30,10 @@ namespace WTelegram return result; } - internal static int GetBitLength(this BigInteger bigInteger) + internal static long GetBitLength(this BigInteger bigInteger) { var bytes = bigInteger.ToByteArray(); - var length = bytes.Length * 8; + var length = bytes.LongLength * 8L; int lastByte = bytes[^1]; while ((lastByte & 0x80) == 0) { length--; lastByte = (lastByte << 1) + 1; } return length; diff --git a/src/Encryption.cs b/src/Encryption.cs index afdfdf9..581e6d3 100644 --- a/src/Encryption.cs +++ b/src/Encryption.cs @@ -129,6 +129,7 @@ namespace WTelegram RNG.GetBytes(bData); var b = BigEndianInteger(bData); var g_b = BigInteger.ModPow(serverDHinnerData.g, b, dh_prime); + ValidityChecksDH(g_a, g_b, dh_prime); var clientDHinnerData = new ClientDHInnerData { nonce = nonce, @@ -193,7 +194,7 @@ namespace WTelegram 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) + Helpers.Log(2, "Verifying encryption key safety... (this happens only during session negociation)"); // 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. @@ -209,8 +210,19 @@ namespace WTelegram _ => 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. + // check whether p is a safe prime (meaning that both p and (p - 1) / 2 are prime) + if (!p.IsProbablePrime()) throw new ApplicationException("p is not a prime number"); + if (!((p - 1) / 2).IsProbablePrime()) throw new ApplicationException("(p - 1) / 2 is not a prime number"); + } + + private static void ValidityChecksDH(BigInteger g_a, BigInteger g_b, BigInteger dh_prime) + { + // 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. + var l = BigInteger.One << (2048 - 64); + var r = dh_prime - l; + if (g_a < l || g_a > r || g_b < l || g_b > r) + throw new ApplicationException("g^a or g^b is not between 2^{2048-64} and dh_prime - 2^{2048-64}"); } [TLDef(0x7A19CB76)] //RSA_public_key#7a19cb76 n:bytes e:bytes = RSAPublicKey diff --git a/src/Generator.cs b/src/Generator.cs index ba0eb72..df38a2a 100644 --- a/src/Generator.cs +++ b/src/Generator.cs @@ -11,7 +11,6 @@ namespace WTelegram { public class Generator { - //TODO: generate BinaryReader/Writer serialization for objects too? readonly Dictionary ctorToTypes = new(); readonly HashSet allTypes = new(); readonly Dictionary> typeInfosByLayer = new(); @@ -25,7 +24,7 @@ namespace WTelegram { Console.WriteLine("Fetch web pages..."); #if DEBUG - currentLayer = await Task.FromResult(0); + currentLayer = await Task.FromResult(TL.Schema.Layer); #else using var http = new HttpClient(); var html = await http.GetStringAsync("https://core.telegram.org/api/layers"); diff --git a/src/Helpers.cs b/src/Helpers.cs index 4e82f0c..e98b2ad 100644 --- a/src/Helpers.cs +++ b/src/Helpers.cs @@ -6,6 +6,7 @@ namespace WTelegram { public static class Helpers { + // int argument is the LogLevel: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel public static Action Log { get; set; } = DefaultLogger; public static readonly System.Text.Json.JsonSerializerOptions JsonOptions = new(System.Text.Json.JsonSerializerDefaults.Web) { IncludeFields = true, WriteIndented = true }; @@ -39,7 +40,7 @@ namespace WTelegram { int i; var temp = value; - for (i = 1; (temp >>= 8) != 0; i++); + for (i = 1; (temp >>= 8) != 0; i++) ; var result = new byte[i]; while (--i >= 0) { result[i] = (byte)value; value >>= 8; } return result; @@ -54,6 +55,15 @@ namespace WTelegram return result; } + internal static byte[] To256Bytes(this BigInteger bi) + { + var bigEndian = bi.ToByteArray(true, true); + if (bigEndian.Length == 256) return bigEndian; + var result = new byte[256]; + bigEndian.CopyTo(result, 256 - bigEndian.Length); + return result; + } + internal static ulong PQFactorize(ulong pq) // ported from https://github.com/tdlib/td/blob/master/tdutils/td/utils/crypto.cpp#L90 { if (pq < 2) return 1; @@ -136,13 +146,44 @@ namespace WTelegram } } - internal static byte[] To256Bytes(this BigInteger bi) + // Miller–Rabin primality test + public static bool IsProbablePrime(this BigInteger n, int k = 64) { - var bigEndian = bi.ToByteArray(true, true); - if (bigEndian.Length == 256) return bigEndian; - var result = new byte[256]; - bigEndian.CopyTo(result, 256 - bigEndian.Length); - return result; + var n_minus_one = n - BigInteger.One; + if (n_minus_one.Sign <= 0) return false; + + int s; + var d = n_minus_one; + for (s = 0; d.IsEven; s++) d >>= 1; + + var bitLen = n.GetBitLength(); + var randomBytes = new byte[bitLen / 8 + 1]; + var lastByteMask = (byte)((1 << (int)(bitLen % 8)) - 1); + BigInteger a; + for (int i = 0; i < k; i++) + { + do + { + Encryption.RNG.GetBytes(randomBytes); + randomBytes[^1] &= lastByteMask; // we don't want more bits than necessary + a = new BigInteger(randomBytes); + } + while (a < 3 || a >= n_minus_one); + a--; + + var x = BigInteger.ModPow(a, d, n); + if (x.IsOne || x == n_minus_one) continue; + + int r; + for (r = s - 1; r > 0; r--) + { + x = BigInteger.ModPow(x, 2, n); + if (x.IsOne) return false; + if (x == n_minus_one) break; + } + if (r == 0) return false; + } + return true; } } }