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;