mirror of
https://github.com/wiz0u/WTelegramClient.git
synced 2025-12-06 06:52:01 +01:00
627 lines
29 KiB
C#
627 lines
29 KiB
C#
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<int, TL.Layer23.DecryptedMessageLayer> 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<int, SecretChat> 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;
|
|
|
|
/// <summary>Instantiate a Secret Chats manager</summary>
|
|
/// <param name="client">The Telegram client</param>
|
|
/// <param name="filename">File path to load/save secret chats keys/status (optional)</param>
|
|
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<ISecretChat> 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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Terminate the secret chat</summary>
|
|
/// <param name="chat_id">Secret Chat ID</param>
|
|
/// <param name="delete_history">Whether to delete the entire chat history for the other user as well</param>
|
|
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<byte[]> 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;
|
|
}
|
|
|
|
/// <summary>Initiate a secret chat with the given user.<br/>(chat must be acknowledged by remote user before being active)</summary>
|
|
/// <param name="user">The remote user</param>
|
|
/// <returns>Secret Chat ID</returns>
|
|
/// <exception cref="WTException"></exception>
|
|
public async Task<int> 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;
|
|
}
|
|
|
|
/// <summary>Processes the <see cref="UpdateEncryption"/> you received from Telegram (<see cref="Client.OnUpdates"/>).</summary>
|
|
/// <param name="update">If update.chat is <see cref="EncryptedChatRequested"/>, you might want to first make sure you want to accept this secret chat initiated by user <see cref="EncryptedChatRequested.admin_id"/></param>
|
|
/// <param name="acceptChatRequests">Incoming requests for secret chats are automatically: accepted (<see langword="true"/>), rejected (<see langword="false"/>) or ignored (<see langword="null"/>)</param>
|
|
/// <returns><see langword="true"/> if the update was handled successfully</returns>
|
|
/// <exception cref="WTException"></exception>
|
|
public async Task<bool> 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;
|
|
}
|
|
|
|
/// <summary>Encrypt and send a message on a secret chat</summary>
|
|
/// <remarks>You would typically pass an instance of <see cref="TL.Layer73.DecryptedMessage"/> or <see cref="TL.Layer23.DecryptedMessageService"/> that you created and filled
|
|
/// <br/>Remember to fill <c>random_id</c> with <see cref="WTelegram.Helpers.RandomLong"/>, and the <c>flags</c> field if necessary</remarks>
|
|
/// <param name="chatId">Secret Chat ID</param>
|
|
/// <param name="msg">The pre-filled <see cref="TL.Layer73.DecryptedMessage">DecryptedMessage</see> or <see cref="TL.Layer23.DecryptedMessageService">DecryptedMessageService </see> to send</param>
|
|
/// <param name="silent">Send encrypted message without a notification</param>
|
|
/// <param name="file">Optional file attachment. See method <see cref="UploadFile">UploadFile</see></param>
|
|
/// <returns>Confirmation of sent message</returns>
|
|
public async Task<Messages_SentEncryptedMessage> 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<Messages_SentEncryptedMessage> 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();
|
|
}
|
|
|
|
/// <summary>Decrypt an encrypted message obtained in <see cref="UpdateNewEncryptedMessage"/></summary>
|
|
/// <param name="msg">Encrypted <see cref="UpdateNewEncryptedMessage.message"/></param>
|
|
/// <param name="fillGaps">If messages are missing or received in wrong order, automatically request to resend missing messages</param>
|
|
/// <returns>An array of <see cref="TL.Layer73.DecryptedMessage">DecryptedMessage</see> or <see cref="TL.Layer23.DecryptedMessageService">DecryptedMessageService </see> from various TL.LayerXX namespaces.<br/>
|
|
/// You can use the generic properties to access their fields
|
|
/// <para>May return an empty array if msg was already previously received or is not the next message in sequence.
|
|
/// <br/>May return multiple messages if missing messages are finally received (using <paramref name="fillGaps"/> = true)</para></returns>
|
|
/// <exception cref="WTException"></exception>
|
|
public ICollection<DecryptedMessageBase> 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<DecryptedMessageBase>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Upload a file to Telegram in encrypted form</summary>
|
|
/// <param name="stream">Content of the file to upload. This method close/dispose the stream</param>
|
|
/// <param name="media">The associated media structure that will be updated with file size and the random AES key/iv</param>
|
|
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
|
/// <returns>the uploaded file info that should be passed to method <see cref="SendMessage">SendMessage</see></returns>
|
|
public async Task<InputEncryptedFileBase> 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);
|
|
}
|
|
|
|
/// <summary>Download and decrypt an encrypted file from Telegram Secret Chat into the outputStream</summary>
|
|
/// <param name="encryptedFile">The encrypted file to download & decrypt</param>
|
|
/// <param name="media">The associated message media structure</param>
|
|
/// <param name="outputStream">Stream to write the decrypted file content to. This method does not close/dispose the stream</param>
|
|
/// <param name="progress">(optional) Callback for tracking the progression of the transfer</param>
|
|
/// <returns>The mime type of the decrypted file, <see langword="null"/> if unknown</returns>
|
|
public async Task<string> 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;
|
|
}
|
|
}
|
|
}
|