From 30618cb316f9ec0109cc11bb9d40c38174b5e543 Mon Sep 17 00:00:00 2001
From: Wizou <11647984+wiz0u@users.noreply.github.com>
Date: Tue, 2 May 2023 22:49:38 +0200
Subject: [PATCH] Implement Future Salts mecanism to prevent replay attacks
---
.github/dev.yml | 2 +-
.github/release.yml | 2 +-
src/Client.Helpers.cs | 16 +++++++--------
src/Client.cs | 47 ++++++++++++++++++++++++++++++++++---------
src/Session.cs | 1 +
5 files changed, 49 insertions(+), 19 deletions(-)
diff --git a/.github/dev.yml b/.github/dev.yml
index c992487..ffec59b 100644
--- a/.github/dev.yml
+++ b/.github/dev.yml
@@ -2,7 +2,7 @@ pr: none
trigger:
- master
-name: 3.4.3-dev.$(Rev:r)
+name: 3.5.1-dev.$(Rev:r)
pool:
vmImage: ubuntu-latest
diff --git a/.github/release.yml b/.github/release.yml
index f5b9a10..78c152a 100644
--- a/.github/release.yml
+++ b/.github/release.yml
@@ -1,7 +1,7 @@
pr: none
trigger: none
-name: 3.4.$(Rev:r)
+name: 3.5.$(Rev:r)
pool:
vmImage: ubuntu-latest
diff --git a/src/Client.Helpers.cs b/src/Client.Helpers.cs
index bfb1d5e..4512f87 100644
--- a/src/Client.Helpers.cs
+++ b/src/Client.Helpers.cs
@@ -684,7 +684,7 @@ namespace WTelegram
private static readonly char[] QueryOrFragment = new[] { '?', '#' };
/// Return information about a chat/channel based on Invite Link
- /// Channel or Invite Link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname
+ /// Public link or Invite link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname
Also work without https:// prefix
/// to also join the chat/channel
/// a Chat or Channel, possibly partial Channel information only (with flag )
public async Task AnalyzeInviteLink(string url, bool join = false)
@@ -693,7 +693,7 @@ namespace WTelegram
start = url.IndexOf('/', start + 2) + 1;
int end = url.IndexOfAny(QueryOrFragment, start);
if (end == -1) end = url.Length;
- if (start == 0 || end == start) throw new ArgumentException("Invalid URI");
+ if (start == 0 || end == start) throw new ArgumentException("Invalid URL");
string hash;
if (url[start] == '+')
hash = url[(start + 1)..end];
@@ -740,11 +740,11 @@ namespace WTelegram
return !ci.flags.HasFlag(ChatInvite.Flags.channel)
? new Chat { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count }
: new Channel { title = ci.title, photo = chatPhoto, participants_count = ci.participants_count,
- restriction_reason = rrAbout,
- flags = (ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast | Channel.Flags.min : Channel.Flags.min) |
- (ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
- (ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
- (ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
+ restriction_reason = rrAbout, flags = Channel.Flags.min |
+ (ci.flags.HasFlag(ChatInvite.Flags.broadcast) ? Channel.Flags.broadcast : 0) |
+ (ci.flags.HasFlag(ChatInvite.Flags.public_) ? Channel.Flags.has_username : 0) |
+ (ci.flags.HasFlag(ChatInvite.Flags.megagroup) ? Channel.Flags.megagroup : 0) |
+ (ci.flags.HasFlag(ChatInvite.Flags.request_needed) ? Channel.Flags.join_request : 0) };
}
return null;
}
@@ -758,7 +758,7 @@ namespace WTelegram
int start = url.IndexOf("//");
start = url.IndexOf('/', start + 2) + 1;
int slash = url.IndexOf('/', start + 2);
- if (start == 0 || slash == -1) throw new ArgumentException("Invalid URI");
+ if (start == 0 || slash == -1) throw new ArgumentException("Invalid URL");
int end = url.IndexOfAny(QueryOrFragment, slash + 1);
if (end == -1) end = url.Length;
ChatBase chat;
diff --git a/src/Client.cs b/src/Client.cs
index ecf75a7..83431ac 100644
--- a/src/Client.cs
+++ b/src/Client.cs
@@ -430,13 +430,13 @@ namespace WTelegram
_dcSession.ServerTicksOffset = (msgId >> 32) * 10000000 - DateTime.UtcNow.Ticks + 621355968000000000L;
var msgStamp = MsgIdToStamp(_lastRecvMsgId = msgId);
- if (serverSalt != _dcSession.Salt) // salt change happens every 30 min
+ if (serverSalt != _dcSession.Salt && serverSalt != _dcSession.Salts?.Values.ElementAtOrDefault(1))
{
- Helpers.Log(2, $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}");
+ Helpers.Log(3, $"{_dcSession.DcID}>Server salt has changed: {_dcSession.Salt:X} -> {serverSalt:X}");
_dcSession.Salt = serverSalt;
- _saltChangeCounter += 1200; // counter is decreased by KeepAlive (we have margin of 10 min)
- if (_saltChangeCounter >= 1800)
+ if (++_saltChangeCounter >= 10)
throw new WTException("Server salt changed too often! Security issue?");
+ CheckSalt();
}
if ((seqno & 1) != 0) lock (_msgsToAck) _msgsToAck.Add(msgId);
@@ -473,6 +473,35 @@ namespace WTelegram
};
}
+ internal void CheckSalt()
+ {
+ lock (_session)
+ {
+ _dcSession.Salts ??= new();
+ if (_dcSession.Salts.Count != 0)
+ {
+ var keys = _dcSession.Salts.Keys;
+ if (keys[^1] == DateTime.MaxValue) return; // GetFutureSalts ongoing
+ var now = DateTime.UtcNow.AddTicks(_dcSession.ServerTicksOffset);
+ for (; keys.Count > 1 && keys[1] < now; _dcSession.Salt = _dcSession.Salts.Values[0])
+ _dcSession.Salts.RemoveAt(0);
+ if (_dcSession.Salts.Count > 48) return;
+ }
+ _dcSession.Salts[DateTime.MaxValue] = 0;
+ }
+ Task.Delay(5000).ContinueWith(_ => this.GetFutureSalts(128).ContinueWith(gfs =>
+ {
+ lock (_session)
+ {
+ _dcSession.Salts.Remove(DateTime.MaxValue);
+ foreach (var entry in gfs.Result.salts)
+ _dcSession.Salts[entry.valid_since] = entry.salt;
+ _dcSession.Salt = _dcSession.Salts.Values[0];
+ _session.Save();
+ }
+ }));
+ }
+
internal MsgContainer ReadMsgContainer(BinaryReader reader)
{
int count = reader.ReadInt32();
@@ -648,7 +677,8 @@ namespace WTelegram
}
break;
case 48: // incorrect server salt (in this case, the bad_server_salt response is received with the correct salt, and the message is to be re-sent with it)
- _dcSession.Salt = ((BadServerSalt)badMsgNotification).new_server_salt; //TODO: GetFutureSalts
+ _dcSession.Salt = ((BadServerSalt)badMsgNotification).new_server_salt;
+ CheckSalt();
break;
default:
retryLast = false;
@@ -817,7 +847,7 @@ namespace WTelegram
#endif
await _networkStream.WriteAsync(preamble, 0, preamble.Length, _cts.Token);
- _saltChangeCounter = 0;
+ _dcSession.Salts?.Remove(DateTime.MaxValue);
_reactorTask = Reactor(_networkStream, _cts);
_sendSemaphore.Release();
@@ -840,7 +870,6 @@ namespace WTelegram
query = new TL.Methods.Help_GetConfig()
});
_session.DcOptions = TLConfig.dc_options;
- _saltChangeCounter = 0;
if (_dcSession.DataCenter == null)
{
_dcSession.DataCenter = _session.DcOptions.Where(dc => dc.id == TLConfig.this_dc)
@@ -856,7 +885,7 @@ namespace WTelegram
{
lock (_session) _session.Save();
}
- Helpers.Log(2, $"Connected to {(TLConfig.test_mode ? "Test DC" : "DC")} {TLConfig.this_dc}... {TLConfig.flags & (Config.Flags)~0xE00U}");
+ Helpers.Log(2, $"Connected to {(TLConfig.test_mode ? "Test DC" : "DC")} {TLConfig.this_dc}... {TLConfig.flags & (Config.Flags)~0x18E00U}");
}
private async Task KeepAlive(CancellationToken ct)
@@ -865,7 +894,6 @@ namespace WTelegram
while (!ct.IsCancellationRequested)
{
await Task.Delay(Math.Abs(PingInterval) * 1000, ct);
- if (_saltChangeCounter > 0) _saltChangeCounter -= Math.Abs(PingInterval);
if (PingInterval <= 0)
await this.Ping(ping_id++);
else // see https://core.telegram.org/api/optimisation#grouping-updates
@@ -1224,6 +1252,7 @@ namespace WTelegram
}
else
{
+ CheckSalt();
using var clearStream = new MemoryStream(1024);
using var clearWriter = new BinaryWriter(clearStream);
clearWriter.Write(_dcSession.AuthKey, 88, 32);
diff --git a/src/Session.cs b/src/Session.cs
index 8b06975..535374b 100644
--- a/src/Session.cs
+++ b/src/Session.cs
@@ -24,6 +24,7 @@ namespace WTelegram
public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId;
public long Salt;
+ public SortedList Salts;
public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;