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 interface ISecretChat { int ChatId { get; } long RemoteUserId { get; } InputEncryptedChat Peer { get; } int RemoteLayer { get; } } [TLDef(0xFEFEFEFE)] [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")] internal sealed partial class SecretChat : IObject, ISecretChat { [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; public long RemoteUserId => participant_id; public InputEncryptedChat Peer => peer; public int RemoteLayer => remoteLayer; internal long key_fingerprint; internal SortedList pendingMsgs = []; 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; } } [System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE1006:Naming Styles")] public sealed class SecretChats : IDisposable { public event Action OnChanged; private readonly Client client; private readonly FileStream storage; private readonly Dictionary chats = []; 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; /// 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 Chats => [.. chats.Values]; 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 BinaryReader(input, Encoding.UTF8, true); if (reader.ReadInt32() != 0) throw new WTException("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 WTException("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 WTException("Unexpected DHConfig response: " + mdhcb?.GetType().Name); if (random.Length != 256) throw new WTException("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 WTException("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 WTException("Invalid fingerprint on accepted secret chat"); if (ec.access_hash != chat.peer.access_hash || ec.participant_id != chat.participant_id) throw new WTException("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 WTException("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.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.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. See method UploadFile /// 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 WTException("Secret chat not found"); try { var dml = new TL.Layer23.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.Layer23.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.Layer23.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 WTException($"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 WTException($"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 WTException($"Invalid MTProto2 padding length: {decrypted_data.Length - 4}-{length}"); else if (chat.remoteLayer < Layer.MTProto2) chat.remoteLayer = Layer.MTProto2; } if (!success) throw new WTException("Could not decrypt message"); if (length % 4 != 0) throw new WTException($"Invalid message_data_length: {length}"); using var reader = new BinaryReader(new MemoryStream(decrypted_data, 4, length)); 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 WTException("Secret chat not found"); try { var obj = Decrypt(chat, msg.Bytes, msg.Bytes.Length); if (obj is not TL.Layer23.DecryptedMessageLayer dml) throw new WTException("Decrypted object is not DecryptedMessageLayer"); if (dml.random_bytes.Length < 15) throw new WTException("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 WTException("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 []; // 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.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.DecryptedMessageActionResend { start_seq_no = lastPending + 2, end_seq_no = dml.out_seq_no - 2 } }); return []; } 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 []; else return [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.Layer23.DecryptedMessageActionNotifyLayer dmanl: chat.remoteLayer = dmanl.layer; return true; case TL.Layer23.DecryptedMessageActionResend resend: Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Resend {resend.start_seq_no}-{resend.end_seq_no}"); var msgSvc = new TL.Layer23.DecryptedMessageService { action = new TL.Layer23.DecryptedMessageActionNoop() }; var dml = new TL.Layer23.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.Layer23.DecryptedMessageActionNoop: Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> Noop"); return true; case TL.Layer23.DecryptedMessageActionRequestKey: case TL.Layer23.DecryptedMessageActionAcceptKey: case TL.Layer23.DecryptedMessageActionCommitKey: case TL.Layer23.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 { chat.flags |= SecretChat.Flags.renewKey; Helpers.Log(1, $"SC{(short)chat.ChatId:X4}> PFS RenewKey"); await Task.Delay(100); 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.exchange_id = Helpers.RandomLong(); await SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.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.Layer23.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 WTException("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.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.DecryptedMessageActionAcceptKey { exchange_id = request.exchange_id, g_b = g_b.To256Bytes(), key_fingerprint = key_fingerprint } }); break; case TL.Layer23.DecryptedMessageActionAcceptKey accept: if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.renewKey) throw new WTException("Invalid AcceptKey"); if (accept.exchange_id != chat.exchange_id) throw new WTException("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 WTException("AcceptKey: key_fingerprint mismatch"); _ = SendMessage(chat.ChatId, new TL.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.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.Layer23.DecryptedMessageActionCommitKey commit: if ((chat.flags & (SecretChat.Flags.requestChat | SecretChat.Flags.renewKey | SecretChat.Flags.acceptKey)) != SecretChat.Flags.acceptKey) throw new WTException("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 WTException("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.Layer23.DecryptedMessageService { random_id = Helpers.RandomLong(), action = new TL.Layer23.DecryptedMessageActionNoop() }); break; case TL.Layer23.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); } } /// Upload a file to Telegram in encrypted form /// Content of the file to upload. This method close/dispose the stream /// The associated media structure that will be updated with file size and the random AES key/iv /// (optional) Callback for tracking the progression of the transfer /// the uploaded file info that should be passed to method SendMessage public async Task UploadFile(Stream stream, DecryptedMessageMedia media, Client.ProgressCallback progress = null) { byte[] aes_key = new byte[32], aes_iv = new byte[32]; RNG.GetBytes(aes_key); RNG.GetBytes(aes_iv); media.SizeKeyIV = (stream.Length, aes_key, aes_iv); using var md5 = MD5.Create(); md5.TransformBlock(aes_key, 0, 32, null, 0); var res = md5.TransformFinalBlock(aes_iv, 0, 32); long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(md5.Hash); fingerprint ^= fingerprint >> 32; using var ige = new AES_IGE_Stream(stream, aes_key, aes_iv, true); var inputFile = await client.UploadFileAsync(ige, null, progress); return inputFile.ToInputEncryptedFile((int)fingerprint); } /// Download and decrypt an encrypted file from Telegram Secret Chat into the outputStream /// The encrypted file to download & decrypt /// The associated message media structure /// Stream to write the decrypted file content to. This method does not close/dispose the stream /// (optional) Callback for tracking the progression of the transfer /// The mime type of the decrypted file, if unknown public async Task DownloadFile(EncryptedFile encryptedFile, DecryptedMessageMedia media, Stream outputStream, Client.ProgressCallback progress = null) { var (size, key, iv) = media.SizeKeyIV; if (key == null || iv == null) throw new ArgumentException("Media has no information about encrypted file", nameof(media)); using var md5 = MD5.Create(); md5.TransformBlock(key, 0, 32, null, 0); var res = md5.TransformFinalBlock(iv, 0, 32); long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(md5.Hash); fingerprint ^= fingerprint >> 32; if (encryptedFile.key_fingerprint != (int)fingerprint) throw new WTException("Encrypted file fingerprint mismatch"); using var decryptStream = new AES_IGE_Stream(outputStream, size, key, iv); var fileLocation = encryptedFile.ToFileLocation(); await client.DownloadFileAsync(fileLocation, decryptStream, encryptedFile.dc_id, encryptedFile.size, progress); return media.MimeType; } } }