Implement Future Salts mecanism to prevent replay attacks

This commit is contained in:
Wizou 2023-05-02 22:49:38 +02:00
parent 753ac12eb1
commit 30618cb316
5 changed files with 49 additions and 19 deletions

2
.github/dev.yml vendored
View file

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

2
.github/release.yml vendored
View file

@ -1,7 +1,7 @@
pr: none
trigger: none
name: 3.4.$(Rev:r)
name: 3.5.$(Rev:r)
pool:
vmImage: ubuntu-latest

View file

@ -684,7 +684,7 @@ namespace WTelegram
private static readonly char[] QueryOrFragment = new[] { '?', '#' };
/// <summary>Return information about a chat/channel based on Invite Link</summary>
/// <param name="url">Channel or Invite Link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname</param>
/// <param name="url">Public link or Invite link, like https://t.me/+InviteHash, https://t.me/joinchat/InviteHash or https://t.me/channelname<br/>Also work without https:// prefix</param>
/// <param name="join"><see langword="true"/> to also join the chat/channel</param>
/// <returns>a Chat or Channel, possibly partial Channel information only (with flag <see cref="Channel.Flags.min"/>)</returns>
public async Task<ChatBase> 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;

View file

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

View file

@ -24,6 +24,7 @@ namespace WTelegram
public byte[] AuthKey; // 2048-bit = 256 bytes
public long UserId;
public long Salt;
public SortedList<DateTime, long> Salts;
public int Seqno;
public long ServerTicksOffset;
public long LastSentMsgId;