From b05d238c9490a253882fc73c40671a6996da5cd1 Mon Sep 17 00:00:00 2001 From: Wizou <11647984+wiz0u@users.noreply.github.com> Date: Wed, 5 Oct 2022 02:17:04 +0200 Subject: [PATCH] Secret Chats protocol implementation --- src/SecretChats.cs | 574 +++++++++++++++++++++++++++++++++++++++++++++ src/TL.Secret.cs | 64 +++++ src/TL.Table.cs | 3 + 3 files changed, 641 insertions(+) create mode 100644 src/SecretChats.cs diff --git a/src/SecretChats.cs b/src/SecretChats.cs new file mode 100644 index 0000000..1eeacb5 --- /dev/null +++ b/src/SecretChats.cs @@ -0,0 +1,574 @@ +using System; +using System.Buffers.Binary; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using TL; +using static WTelegram.Compat; +using static WTelegram.Encryption; + +namespace WTelegram +{ + public sealed class SecretChats : IDisposable + { + public event Action OnChanged; + + private readonly Client client; + private readonly FileStream storage; + private readonly Dictionary chats = new(); + private Messages_DhConfig dh; + private BigInteger dh_prime; + private readonly SHA256 sha256 = SHA256.Create(); + private readonly SHA1 sha1 = SHA1.Create(); + private readonly Random random = new(); + private const int ThresholdPFS = 100; + + [TLDef(0xFEFEFEFE)] + internal class SecretChat : IObject + { + [Flags] public enum Flags : uint { requestChat = 1, renewKey = 2, acceptKey = 4, originator = 8, commitKey = 16 } + public Flags flags; + public InputEncryptedChat peer = new(); + public byte[] salt; // contains future/discarded authKey during acceptKey/commitKey + public byte[] authKey; + public DateTime key_created; + public int key_useCount; + public long participant_id; + public int remoteLayer = 46; + public int in_seq_no = -2, out_seq_no = 0; + public long exchange_id; + + public int ChatId => peer.chat_id; + internal long key_fingerprint; + internal SortedList pendingMsgs = new(); + internal void Discarded() // clear out fields for more security + { + Array.Clear(authKey, 0, authKey.Length); + key_fingerprint = participant_id = peer.access_hash = peer.chat_id = in_seq_no = out_seq_no = remoteLayer = 0; + } + } + + /// Instantiate a Secret Chats manager + /// The Telegram client + /// File path to load/save secret chats keys/status (optional) + public SecretChats(Client client, string filename = null) + { + this.client = client; + if (filename != null) + { + storage = File.Open(filename, FileMode.OpenOrCreate); + if (storage.Length != 0) Load(storage); + OnChanged = () => { storage.SetLength(0); Save(storage); }; + } + } + public void Dispose() { OnChanged?.Invoke(); storage?.Dispose(); sha256.Dispose(); sha1.Dispose(); } + + public List Peers => chats.Values.Select(sc => sc.peer).ToList(); + + /// Return secret chats with the given remote user ID + /// remote user ID + /// List of matching secret chat ids/access_hash + public List FindChatsByParticipant(long participant_id) + => chats.Where(kvp => kvp.Value.participant_id == participant_id).Select(kvp => kvp.Value.peer).ToList(); + + public bool IsChatActive(int chat_id) => !(chats.GetValueOrDefault(chat_id)?.flags.HasFlag(SecretChat.Flags.requestChat) ?? true); + + public void Save(Stream output) + { + using var writer = new BinaryWriter(output, Encoding.UTF8, true); + writer.Write(0); + writer.WriteTLObject(dh); + writer.Write(chats.Count); + foreach (var chat in chats.Values) + writer.WriteTLObject(chat); + } + public void Load(Stream input) + { + using var reader = new TL.BinaryReader(input, null, true); + if (reader.ReadInt32() != 0) throw new ApplicationException("Unrecognized Secrets format"); + dh = (Messages_DhConfig)reader.ReadTLObject(); + if (dh?.p != null) dh_prime = BigEndianInteger(dh.p); + int count = reader.ReadInt32(); + for (int i = 0; i < count; i++) + { + var chat = (SecretChat)reader.ReadTLObject(); + if (chat.authKey?.Length > 0) chat.key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.authKey).AsSpan(12)); + chats[chat.ChatId] = chat; + } + } + + /// Terminate the secret chat + /// Secret Chat ID + /// Whether to delete the entire chat history for the other user as well + public async Task Discard(int chat_id, bool delete_history = false) + { + if (chats.TryGetValue(chat_id, out var chat)) + { + chats.Remove(chat_id); + chat.Discarded(); + } + try + { + await client.Messages_DiscardEncryption(chat_id, delete_history); + } + catch (RpcException ex) when (ex.Code == 400 && ex.Message == "ENCRYPTION_ALREADY_DECLINED") { } + } + + private async Task UpdateDHConfig() + { + var mdhcb = await client.Messages_GetDhConfig(dh?.version ?? 0, 256); + if (mdhcb is Messages_DhConfigNotModified { random: var random }) + _ = dh ?? throw new ApplicationException("DhConfigNotModified on zero version"); + else if (mdhcb is Messages_DhConfig dhc) + { + var p = BigEndianInteger(dhc.p); + CheckGoodPrime(p, dhc.g); + (dh, dh_prime, random, dh.random) = (dhc, p, dhc.random, null); + } + else throw new ApplicationException("Unexpected DHConfig response: " + mdhcb?.GetType().Name); + if (random.Length != 256) throw new ApplicationException("Invalid DHConfig random"); + var salt = new byte[256]; + RNG.GetBytes(salt); + for (int i = 0; i < 256; i++) salt[i] ^= random[i]; + return salt; + } + + /// Initiate a secret chat with the given user.
(chat must be acknowledged by remote user before being active)
+ /// The remote user + /// Secret Chat ID + /// + public async Task Request(InputUserBase user) + { + int chat_id; + do chat_id = (int)Helpers.RandomLong(); while (chats.ContainsKey(chat_id)); + var chat = chats[chat_id] = new SecretChat + { + flags = SecretChat.Flags.requestChat | SecretChat.Flags.originator, + peer = { chat_id = chat_id }, + participant_id = user.UserId ?? 0, + salt = await UpdateDHConfig(), + out_seq_no = 1, + }; + var a = BigEndianInteger(chat.salt); + var g_a = BigInteger.ModPow(dh.g, a, dh_prime); + CheckGoodGaAndGb(g_a, dh_prime); + var ecb = await client.Messages_RequestEncryption(user, chat_id, g_a.To256Bytes()); + if (ecb is not EncryptedChatWaiting ecw || ecw.id != chat_id || ecw.participant_id != chat.participant_id) + throw new ApplicationException("Invalid " + ecb?.GetType().Name); + chat.peer.access_hash = ecw.access_hash; + return chat_id; + } + + /// Processes the you received from Telegram (). + /// If update.chat is , you might want to first make sure you want to accept this secret chat initiated by user + /// Incoming requests for secret chats are automatically: accepted (), rejected () or ignored () + /// if the update was handled successfully + /// + public async Task HandleUpdate(UpdateEncryption update, bool? acceptChatRequests = true) + { + try + { + if (chats.TryGetValue(update.chat.ID, out var chat)) + { + if (update.chat is EncryptedChat ec && chat.flags.HasFlag(SecretChat.Flags.requestChat)) // remote accepted our request + { + var a = BigEndianInteger(chat.salt); + var g_b = BigEndianInteger(ec.g_a_or_b); + CheckGoodGaAndGb(g_b, dh_prime); + var gab = BigInteger.ModPow(g_b, a, dh_prime); + chat.flags &= ~SecretChat.Flags.requestChat; + SetAuthKey(chat, gab.To256Bytes()); + if (ec.key_fingerprint != chat.key_fingerprint) throw new ApplicationException("Invalid fingerprint on accepted secret chat"); + if (ec.access_hash != chat.peer.access_hash || ec.participant_id != chat.participant_id) throw new ApplicationException("Invalid peer on accepted secret chat"); + await SendNotifyLayer(chat); + return true; + } + else if (update.chat is EncryptedChatDiscarded ecd) + { + chats.Remove(chat.ChatId); + chat.Discarded(); + return true; + } + Helpers.Log(3, $"Unexpected {update.chat.GetType().Name} for secret chat {chat.ChatId}"); + return false; + } + else if (update.chat is EncryptedChatRequested ecr) // incoming request + { + switch (acceptChatRequests) + { + case null: return false; + case false: await client.Messages_DiscardEncryption(ecr.id, false); return true; + case true: + var salt = await UpdateDHConfig(); + var b = BigEndianInteger(salt); + var g_b = BigInteger.ModPow(dh.g, b, dh_prime); + var g_a = BigEndianInteger(ecr.g_a); + CheckGoodGaAndGb(g_a, dh_prime); + CheckGoodGaAndGb(g_b, dh_prime); + var gab = BigInteger.ModPow(g_a, b, dh_prime); + chat = chats[ecr.id] = new SecretChat + { + flags = 0, + peer = { chat_id = ecr.id, access_hash = ecr.access_hash }, + participant_id = ecr.admin_id, + in_seq_no = -1, + }; + SetAuthKey(chat, gab.To256Bytes()); + var ecb = await client.Messages_AcceptEncryption(chat.peer, g_b.ToByteArray(true, true), chat.key_fingerprint); + if (ecb is not EncryptedChat ec || ec.id != ecr.id || ec.access_hash != ecr.access_hash || + ec.admin_id != ecr.admin_id || ec.key_fingerprint != chat.key_fingerprint) + throw new ApplicationException("Inconsistent accepted secret chat"); + await SendNotifyLayer(chat); + return true; + } + } + else if (update.chat is EncryptedChatDiscarded) // unknown chat discarded + return true; + Helpers.Log(3, $"Unexpected {update.chat.GetType().Name} for unknown secret chat {update.chat.ID}"); + return false; + } + catch + { + await Discard(update.chat.ID); + throw; + } + finally + { + OnChanged?.Invoke(); + } + } + + private void SetAuthKey(SecretChat chat, byte[] key) + { + chat.authKey = key; + chat.key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(key).AsSpan(12)); + chat.exchange_id = 0; + chat.key_useCount = 0; + chat.key_created = DateTime.UtcNow; + } + + private async Task SendNotifyLayer(SecretChat chat) + { + await SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer17.DecryptedMessageActionNotifyLayer { layer = Layer.SecretChats } }); + if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2; + } + + /// Encrypt and send a message on a secret chat + /// You would typically pass an instance of or that you created and filled + ///
Remember to fill random_id with , and the flags field if necessary
+ /// Secret Chat ID + /// The pre-filled DecryptedMessage or DecryptedMessageService to send + /// Send encrypted message without a notification + /// Optional file attachment + /// Confirmation of sent message + public async Task SendMessage(int chatId, DecryptedMessageBase msg, bool silent = false, InputEncryptedFileBase file = null) + { + if (!chats.TryGetValue(chatId, out var chat)) throw new ApplicationException("Secret chat not found"); + try + { + var dml = new TL.Layer17.DecryptedMessageLayer + { + layer = Math.Min(chat.remoteLayer, Layer.SecretChats), + random_bytes = new byte[15], + in_seq_no = chat.in_seq_no < 0 ? chat.in_seq_no + 2 : chat.in_seq_no, + out_seq_no = chat.out_seq_no, + message = msg + }; + //Debug.WriteLine($">\t\t\t\t{dml.in_seq_no}\t{dml.out_seq_no}"); + var result = await SendMessage(chat, dml, silent, file); + chat.out_seq_no += 2; + return result; + } + finally + { + OnChanged?.Invoke(); + } + } + + private async Task SendMessage(SecretChat chat, TL.Layer17.DecryptedMessageLayer dml, bool silent = false, InputEncryptedFileBase file = null) + { + RNG.GetBytes(dml.random_bytes); + int x = 8 - (int)(chat.flags & SecretChat.Flags.originator); + using var memStream = new MemoryStream(1024); + using var writer = new BinaryWriter(memStream); + + using var clearStream = new MemoryStream(1024); + using var clearWriter = new BinaryWriter(clearStream); + clearWriter.Write(chat.authKey, 88 + x, 32); + clearWriter.Write(0); // int32 message_data_length (to be patched) + clearWriter.WriteTLObject(dml); // bytes message_data + int clearLength = (int)clearStream.Length - 32; // length before padding (= 4 + message_data_length) + int padding = (0x7FFFFFF0 - clearLength) % 16; + padding += random.Next(2, 16) * 16; // MTProto 2.0 padding must be between 12..1024 with total length divisible by 16 + clearStream.SetLength(32 + clearLength + padding); + byte[] clearBuffer = clearStream.GetBuffer(); + BinaryPrimitives.WriteInt32LittleEndian(clearBuffer.AsSpan(32), clearLength - 4); // patch message_data_length + RNG.GetBytes(clearBuffer, 32 + clearLength, padding); + var msgKeyLarge = sha256.ComputeHash(clearBuffer, 0, 32 + clearLength + padding); + const int msgKeyOffset = 8; // msg_key = middle 128-bits of SHA256(authkey_part+plaintext+padding) + byte[] encrypted_data = EncryptDecryptMessage(clearBuffer.AsSpan(32, clearLength + padding), true, x, chat.authKey, msgKeyLarge, msgKeyOffset, sha256); + + writer.Write(chat.key_fingerprint); // int64 key_fingerprint + writer.Write(msgKeyLarge, msgKeyOffset, 16); // int128 msg_key + writer.Write(encrypted_data); // bytes encrypted_data + var data = memStream.ToArray(); + + CheckPFS(chat); + if (file != null) + return await client.Messages_SendEncryptedFile(chat.peer, dml.message.RandomId, data, file, silent); + else if (dml.message is TL.Layer17.DecryptedMessageService or TL.Layer8.DecryptedMessageService) + return await client.Messages_SendEncryptedService(chat.peer, dml.message.RandomId, data); + else + return await client.Messages_SendEncrypted(chat.peer, dml.message.RandomId, data, silent); + } + + private IObject Decrypt(SecretChat chat, byte[] data, int dataLen) + { + if (dataLen < 32) // authKeyId+msgKey+(length+ctorNb) + throw new ApplicationException($"Encrypted packet too small: {data.Length}"); + var authKey = chat.authKey; + long authKeyId = BinaryPrimitives.ReadInt64LittleEndian(data); + if (authKeyId == chat.key_fingerprint) + if (!chat.flags.HasFlag(SecretChat.Flags.commitKey)) CheckPFS(chat); + else { chat.flags &= ~SecretChat.Flags.commitKey; Array.Clear(chat.salt, 0, chat.salt.Length); } + else if (chat.flags.HasFlag(SecretChat.Flags.commitKey) && authKeyId == BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12))) authKey = chat.salt; + else throw new ApplicationException($"Received a packet encrypted with unexpected key {authKeyId:X}"); + int x = (int)(chat.flags & SecretChat.Flags.originator); + byte[] decrypted_data = EncryptDecryptMessage(data.AsSpan(24, dataLen - 24), false, x, authKey, data, 8, sha256); + var length = BinaryPrimitives.ReadInt32LittleEndian(decrypted_data); + var success = length >= 4 && length <= decrypted_data.Length - 4; + if (success) + { + sha256.Initialize(); + sha256.TransformBlock(authKey, 88 + x, 32, null, 0); + sha256.TransformFinalBlock(decrypted_data, 0, decrypted_data.Length); + if (success = data.AsSpan(8, 16).SequenceEqual(sha256.Hash.AsSpan(8, 16))) + if (decrypted_data.Length - 4 - length is < 12 or > 1024) throw new ApplicationException($"Invalid MTProto2 padding length: {decrypted_data.Length - 4}-{length}"); + else if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2; + } + if (!success) throw new ApplicationException("Could not decrypt message"); + if (length % 4 != 0) throw new ApplicationException($"Invalid message_data_length: {length}"); + using var reader = new TL.BinaryReader(new MemoryStream(decrypted_data, 4, length), null); + return reader.ReadTLObject(); + } + + /// Decrypt an encrypted message obtained in + /// Encrypted + /// If messages are missing or received in wrong order, automatically request to resend missing messages + /// An array of DecryptedMessage or DecryptedMessageService from various TL.LayerXX namespaces.
+ /// You can use the generic properties to access their fields + /// May return an empty array if msg was already previously received or is not the next message in sequence. + ///
May return multiple messages if missing messages are finally received (using = true)
+ /// + public ICollection DecryptMessage(EncryptedMessageBase msg, bool fillGaps = true) + { + if (!chats.TryGetValue(msg.ChatId, out var chat)) throw new ApplicationException("Secret chat not found"); + try + { + var obj = Decrypt(chat, msg.Bytes, msg.Bytes.Length); + if (obj is not TL.Layer17.DecryptedMessageLayer dml) throw new ApplicationException("Decrypted object is not DecryptedMessageLayer"); + if (dml.random_bytes.Length < 15) throw new ApplicationException("Not enough random_bytes"); + if (((dml.out_seq_no ^ dml.in_seq_no) & 1) != 1 || ((dml.out_seq_no ^ chat.in_seq_no) & 1) != 0) throw new ApplicationException("Invalid seq_no parities"); + if (dml.layer > chat.remoteLayer) chat.remoteLayer = dml.layer; + //Debug.WriteLine($"<\t{dml.in_seq_no}\t{dml.out_seq_no}\t\t\t\t\t\texpected:{chat.out_seq_no}/{chat.in_seq_no + 2}"); + if (dml.out_seq_no <= chat.in_seq_no) return Array.Empty(); // already received message + var pendingMsgSeqNo = chat.pendingMsgs.Keys; + if (fillGaps && dml.out_seq_no > chat.in_seq_no + 2) + { + var lastPending = pendingMsgSeqNo.LastOrDefault(); + if (lastPending == 0) lastPending = chat.in_seq_no; + chat.pendingMsgs[dml.out_seq_no] = dml; + if (dml.out_seq_no > lastPending + 2) // send request to resend missing gap asynchronously + _ = SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer17.DecryptedMessageActionResend { start_seq_no = lastPending + 2, end_seq_no = dml.out_seq_no - 2 } }); + return Array.Empty(); + } + chat.in_seq_no = dml.out_seq_no; + if (pendingMsgSeqNo.Count == 0 || pendingMsgSeqNo[0] != dml.out_seq_no + 2) + if (HandleAction(chat, dml.message.Action)) return Array.Empty(); + else return new[] { dml.message }; + else // we have pendingMsgs completing the sequence in order + { + var list = new List(); + if (!HandleAction(chat, dml.message.Action)) + list.Add(dml.message); + do + { + dml = chat.pendingMsgs.Values[0]; + chat.pendingMsgs.RemoveAt(0); + chat.in_seq_no += 2; + if (!HandleAction(chat, dml.message.Action)) + list.Add(dml.message); + } while (pendingMsgSeqNo.Count != 0 && pendingMsgSeqNo[0] == chat.in_seq_no + 2); + return list; + } + } + catch (Exception) + { + _ = Discard(msg.ChatId); + throw; + } + finally + { + OnChanged?.Invoke(); + } + } + + private bool HandleAction(SecretChat chat, DecryptedMessageAction action) + { + switch (action) + { + case TL.Layer17.DecryptedMessageActionNotifyLayer dmanl: + chat.remoteLayer = dmanl.layer; + return true; + case TL.Layer17.DecryptedMessageActionResend resend: + Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Resend {resend.start_seq_no}-{resend.end_seq_no}"); + var msgSvc = new TL.Layer17.DecryptedMessageService { action = new TL.Layer20.DecryptedMessageActionNoop() }; + var dml = new TL.Layer17.DecryptedMessageLayer + { + layer = Math.Min(chat.remoteLayer, Layer.SecretChats), + random_bytes = new byte[15], + in_seq_no = chat.in_seq_no, + message = msgSvc + }; + for (dml.out_seq_no = resend.start_seq_no; dml.out_seq_no <= resend.end_seq_no; dml.out_seq_no += 2) + { + msgSvc.random_id = Helpers.RandomLong(); + _ = SendMessage(chat, dml); + } + return true; + case TL.Layer20.DecryptedMessageActionNoop: + Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Noop"); + return true; + case TL.Layer20.DecryptedMessageActionRequestKey: + case TL.Layer20.DecryptedMessageActionAcceptKey: + case TL.Layer20.DecryptedMessageActionCommitKey: + case TL.Layer20.DecryptedMessageActionAbortKey: + Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> PFS {action.GetType().Name[22..]}"); + HandlePFS(chat, action); + return true; + } + return false; + } + + private async void CheckPFS(SecretChat chat) + { + if (++chat.key_useCount < ThresholdPFS && chat.key_created >= DateTime.UtcNow.AddDays(-7)) return; + if (chat.key_useCount < ThresholdPFS) chat.key_useCount = ThresholdPFS; + if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != 0) + if (chat.key_useCount < ThresholdPFS * 2) return; + else { Helpers.Log(4, "SC{(short)chat.ChatId:X4}> PFS Failure"); _ = Discard(chat.ChatId); return; } + try + { + Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> PFS RenewKey"); + chat.salt = new byte[256]; + RNG.GetBytes(chat.salt); + var a = BigEndianInteger(chat.salt); + var g_a = BigInteger.ModPow(dh.g, a, dh_prime); + CheckGoodGaAndGb(g_a, dh_prime); + chat.flags |= SecretChat.Flags.renewKey; + chat.exchange_id = Helpers.RandomLong(); + await SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer20.DecryptedMessageActionRequestKey { exchange_id = chat.exchange_id, g_a = g_a.To256Bytes() } }); + } + catch (Exception ex) + { + Helpers.Log(4, "Error in CheckRenewKey: " + ex); + chat.flags &= ~SecretChat.Flags.renewKey; + } + } + + private async void HandlePFS(SecretChat chat, DecryptedMessageAction action) + { + try + { + switch (action) + { + case TL.Layer20.DecryptedMessageActionRequestKey request: + switch (chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) + { + case SecretChat.Flags.renewKey: // Concurrent Re-Keying + if (chat.exchange_id > request.exchange_id) return; // we won, ignore the smaller exchange_id RequestKey + chat.flags &= ~SecretChat.Flags.renewKey; + if (chat.exchange_id == request.exchange_id) // equal => silent abort both re-keing + { + Array.Clear(chat.salt, 0, chat.salt.Length); + chat.exchange_id = 0; + return; + } + break; // we lost, process with the larger exchange_id RequestKey + case 0: break; + default: throw new ApplicationException("Invalid RequestKey"); + } + var g_a = BigEndianInteger(request.g_a); + var salt = new byte[256]; + RNG.GetBytes(salt); + var b = BigEndianInteger(salt); + var g_b = BigInteger.ModPow(dh.g, b, dh_prime); + CheckGoodGaAndGb(g_a, dh_prime); + CheckGoodGaAndGb(g_b, dh_prime); + var gab = BigInteger.ModPow(g_a, b, dh_prime); + chat.flags |= SecretChat.Flags.acceptKey; + chat.salt = gab.To256Bytes(); + chat.exchange_id = request.exchange_id; + var key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12)); + await SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer20.DecryptedMessageActionAcceptKey { exchange_id = request.exchange_id, g_b = g_b.To256Bytes(), key_fingerprint = key_fingerprint } }); + break; + case TL.Layer20.DecryptedMessageActionAcceptKey accept: + if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.renewKey) + throw new ApplicationException("Invalid AcceptKey"); + if (accept.exchange_id != chat.exchange_id) + throw new ApplicationException("AcceptKey: exchange_id mismatch"); + var a = BigEndianInteger(chat.salt); + g_b = BigEndianInteger(accept.g_b); + CheckGoodGaAndGb(g_b, dh_prime); + gab = BigInteger.ModPow(g_b, a, dh_prime); + var authKey = gab.To256Bytes(); + key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(authKey).AsSpan(12)); + if (accept.key_fingerprint != key_fingerprint) + throw new ApplicationException("AcceptKey: key_fingerprint mismatch"); + _ = SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer20.DecryptedMessageActionCommitKey { exchange_id = accept.exchange_id, key_fingerprint = accept.key_fingerprint } }); + chat.salt = chat.authKey; // A may only discard the previous key after a message encrypted with the new key has been received. + SetAuthKey(chat, authKey); + chat.flags = chat.flags & ~SecretChat.Flags.renewKey | SecretChat.Flags.commitKey; + break; + case TL.Layer20.DecryptedMessageActionCommitKey commit: + if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.acceptKey) + throw new ApplicationException("Invalid RequestKey"); + key_fingerprint = BinaryPrimitives.ReadInt64LittleEndian(sha1.ComputeHash(chat.salt).AsSpan(12)); + if (commit.exchange_id != chat.exchange_id | commit.key_fingerprint != key_fingerprint) + throw new ApplicationException("CommitKey: data mismatch"); + chat.flags &= ~SecretChat.Flags.acceptKey; + authKey = chat.authKey; + SetAuthKey(chat, chat.salt); + Array.Clear(authKey, 0, authKey.Length); // the old key must be securely discarded + await SendMessage(chat.ChatId, new TL.Layer17.DecryptedMessageService { random_id = Helpers.RandomLong(), + action = new TL.Layer20.DecryptedMessageActionNoop() }); + break; + case TL.Layer20.DecryptedMessageActionAbortKey abort: + if ((chat.flags & (SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) == 0 || + chat.flags.HasFlag(SecretChat.Flags.commitKey) || abort.exchange_id != chat.exchange_id) + return; + chat.flags &= ~(SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey); + Array.Clear(chat.salt, 0, chat.salt.Length); + chat.exchange_id = 0; + break; + } + } + catch (Exception ex) + { + Helpers.Log(4, $"Error handling {action}: {ex}"); + _ = Discard(chat.ChatId); + } + } + } +} + +// TODO https://core.telegram.org/api/end-to-end#sending-encrypted-files diff --git a/src/TL.Secret.cs b/src/TL.Secret.cs index 5688752..0fa2a5d 100644 --- a/src/TL.Secret.cs +++ b/src/TL.Secret.cs @@ -5,8 +5,26 @@ namespace TL /// Object describes the contents of an encrypted message. See public abstract class DecryptedMessageBase : IObject { + /// Flags, see TL conditional fields (added in layer 45) + public virtual uint FFlags { get; } /// Random message ID, assigned by the author of message.
Must be equal to the ID passed to sending method.
public virtual long RandomId { get; } + /// Message lifetime. Has higher priority than .
Parameter added in Layer 17.
+ public virtual int Ttl { get; } + /// Message text + public virtual string Message { get; } + /// Media content + public virtual DecryptedMessageMedia Media { get; } + /// Message entities for styled text (parameter added in layer 45) + public virtual MessageEntity[] Entities { get; } + /// Specifies the ID of the inline bot that generated the message (parameter added in layer 45) + public virtual string ViaBotName { get; } + /// Random message ID of the message this message replies to (parameter added in layer 45) + public virtual long ReplyToRandom { get; } + /// Random group ID, assigned by the author of message.
Multiple encrypted messages with a photo attached and with the same group ID indicate an album or grouped media (parameter added in layer 45)
+ public virtual long Grouped { get; } + public virtual byte[] RandomBytes { get; } + public virtual DecryptedMessageAction Action { get; } } /// Object describes media contents of an encrypted message. See @@ -43,6 +61,11 @@ namespace TL /// Random message ID, assigned by the author of message.
Must be equal to the ID passed to sending method.
public override long RandomId => random_id; + /// Message text + public override string Message => message; + /// Media content + public override DecryptedMessageMedia Media => media; + public override byte[] RandomBytes => random_bytes; } ///
Contents of an encrypted service message. See [TLDef(0xAA48327D)] @@ -56,6 +79,9 @@ namespace TL /// Random message ID, assigned by the message author.
Must be equal to the ID passed to the sending method.
public override long RandomId => random_id; + public override byte[] RandomBytes => random_bytes; + /// Action relevant to the service message + public override DecryptedMessageAction Action => action; } ///
Photo attached to an encrypted message. See @@ -221,6 +247,12 @@ namespace TL /// Random message ID, assigned by the author of message.
Must be equal to the ID passed to sending method.
public override long RandomId => random_id; + /// Message lifetime. Has higher priority than .
Parameter added in Layer 17.
+ public override int Ttl => ttl; + /// Message text + public override string Message => message; + /// Media content + public override DecryptedMessageMedia Media => media; } ///
Contents of an encrypted service message. See [TLDef(0x73164160)] @@ -233,6 +265,8 @@ namespace TL /// Random message ID, assigned by the message author.
Must be equal to the ID passed to the sending method.
public override long RandomId => random_id; + /// Action relevant to the service message + public override DecryptedMessageAction Action => action; } ///
Video attached to an encrypted message. See @@ -373,8 +407,22 @@ namespace TL has_via_bot_name = 0x800, } + /// Flags, see TL conditional fields (added in layer 45) + public override uint FFlags => (uint)flags; /// Random message ID, assigned by the author of message.
Must be equal to the ID passed to sending method.
public override long RandomId => random_id; + /// Message lifetime. Has higher priority than .
Parameter added in Layer 17.
+ public override int Ttl => ttl; + /// Message text + public override string Message => message; + /// Media content + public override DecryptedMessageMedia Media => media; + /// Message entities for styled text (parameter added in layer 45) + public override MessageEntity[] Entities => entities; + /// Specifies the ID of the inline bot that generated the message (parameter added in layer 45) + public override string ViaBotName => via_bot_name; + /// Random message ID of the message this message replies to (parameter added in layer 45) + public override long ReplyToRandom => reply_to_random_id; } /// Photo attached to an encrypted message. See @@ -515,8 +563,24 @@ namespace TL has_grouped_id = 0x20000, } + /// Flags, see TL conditional fields (added in layer 45) + public override uint FFlags => (uint)flags; /// Random message ID, assigned by the author of message.
Must be equal to the ID passed to sending method.
public override long RandomId => random_id; + /// Message lifetime. Has higher priority than .
Parameter added in Layer 17.
+ public override int Ttl => ttl; + /// Message text + public override string Message => message; + /// Media content + public override DecryptedMessageMedia Media => media; + /// Message entities for styled text (parameter added in layer 45) + public override MessageEntity[] Entities => entities; + /// Specifies the ID of the inline bot that generated the message (parameter added in layer 45) + public override string ViaBotName => via_bot_name; + /// Random message ID of the message this message replies to (parameter added in layer 45) + public override long ReplyToRandom => reply_to_random_id; + /// Random group ID, assigned by the author of message.
Multiple encrypted messages with a photo attached and with the same group ID indicate an album or grouped media (parameter added in layer 45)
+ public override long Grouped => grouped_id; } } diff --git a/src/TL.Table.cs b/src/TL.Table.cs index df81f71..27ecbc9 100644 --- a/src/TL.Table.cs +++ b/src/TL.Table.cs @@ -7,6 +7,8 @@ namespace TL public static class Layer { public const int Version = 146; // fetched 14/09/2022 16:18:39 + internal const int SecretChats = 101; + internal const int MTProto2 = 73; internal const uint VectorCtor = 0x1CB5C415; internal const uint NullCtor = 0x56730BCC; internal const uint RpcResultCtor = 0xF35C6D01; @@ -23,6 +25,7 @@ namespace TL [0x73F1F8DC] = typeof(MsgContainer), [0xE06046B2] = typeof(MsgCopy), [0x3072CFA1] = typeof(GzipPacked), + [0xFEFEFEFE] = typeof(WTelegram.SecretChats.SecretChat), // from TL.MTProto: [0x05162463] = typeof(ResPQ), [0x83C95AEC] = typeof(PQInnerData),