Implement Telegram protocol safety checks

This commit is contained in:
Wizou 2021-08-20 02:13:58 +02:00
parent e205244cf7
commit f2a1dbc20d
6 changed files with 87 additions and 24 deletions

2
ci.yml
View file

@ -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

View file

@ -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}");
}
}

View file

@ -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;

View file

@ -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

View file

@ -11,7 +11,6 @@ namespace WTelegram
{
public class Generator
{
//TODO: generate BinaryReader/Writer serialization for objects too?
readonly Dictionary<int, string> ctorToTypes = new();
readonly HashSet<string> allTypes = new();
readonly Dictionary<int, Dictionary<string, TypeInfo>> 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");

View file

@ -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<int, string> 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)
// MillerRabin 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;
}
}
}