From f22990cb58c0dd0c76c25ad4277585d71992d188 Mon Sep 17 00:00:00 2001 From: Wizou <11647984+wiz0u@users.noreply.github.com> Date: Fri, 7 Oct 2022 03:00:57 +0200 Subject: [PATCH] Support for Encrypted Files/Medias --- EXAMPLES.md | 14 +++--- Examples/Program_SecretChats.cs | 22 +++++++-- README.md | 6 +-- src/Client.Helpers.cs | 2 +- src/Encryption.cs | 79 ++++++++++++++++++++++++++------- src/SecretChats.cs | 25 ++++++++++- src/TL.Helpers.cs | 1 + src/TL.SchemaFuncs.cs | 8 ++-- src/TL.Secret.cs | 23 +++++++++- 9 files changed, 143 insertions(+), 37 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index f663838..0c4a7a5 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -440,19 +440,19 @@ var chat = chats.chats[1234567890]; // the chat we want var full = await client.GetFullChat(chat); Reaction reaction = full.full_chat.AvailableReactions switch { - ChatReactionsSome some => some.reactions[0], // only some reactions are allowed => pick the first - ChatReactionsAll all => // all reactions are allowed in this chat - all.flags.HasFlag(ChatReactionsAll.Flags.allow_custom) && client.User.flags.HasFlag(TL.User.Flags.premium) - ? new ReactionCustomEmoji { document_id = 5190875290439525089 } // we can use custom emoji reactions here - : new ReactionEmoji { emoticon = all_emoji.reactions[0].reaction }, // else, pick the first standard emoji reaction - _ => null // reactions are not allowed in this chat + ChatReactionsSome some => some.reactions[0], // only some reactions are allowed => pick the first + ChatReactionsAll all => // all reactions are allowed in this chat + all.flags.HasFlag(ChatReactionsAll.Flags.allow_custom) && client.User.flags.HasFlag(TL.User.Flags.premium) + ? new ReactionCustomEmoji { document_id = 5190875290439525089 } // we can use custom emoji reactions here + : new ReactionEmoji { emoticon = all_emoji.reactions[0].reaction }, // else, pick the first standard emoji reaction + _ => null // reactions are not allowed in this chat }; if (reaction == null) return; // • Send the selected reaction on the last 2 pinned messages var messages = await client.Messages_Search(chat, limit: 2); foreach (var msg in messages.Messages) - await client.Messages_SendReaction(chat, msg.ID, reaction: new[] { reaction }); + await client.Messages_SendReaction(chat, msg.ID, reaction: new[] { reaction }); ``` *Note: you can find custom emoji document IDs via API methods like [Messages_GetFeaturedEmojiStickers](https://corefork.telegram.org/method/messages.getFeaturedEmojiStickers). Access hash is not required* diff --git a/Examples/Program_SecretChats.cs b/Examples/Program_SecretChats.cs index 721de68..6bcee65 100644 --- a/Examples/Program_SecretChats.cs +++ b/Examples/Program_SecretChats.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Threading.Tasks; using TL; @@ -31,9 +32,10 @@ namespace WTelegramClientTest var dialogs = await Client.Messages_GetAllDialogs(); // load the list of users/chats dialogs.CollectUsersChats(Users, Chats); Console.WriteLine(@"Available commands: -/request Initiate Secret Chat with user -/discard [delete] Terminate active secret chat (and delete history) +/request Initiate Secret Chat with user (see /users) +/discard [delete] Terminate active secret chat [and delete history] /select Select another Secret Chat +/photo filename.jpg Send a JPEG photo /read Mark active discussion as read /users List collected users and their IDs Type a command, or a message to send to the active secret chat:"); @@ -52,6 +54,13 @@ Type a command, or a message to send to the active secret chat:"); SelectActiveChat(await Secrets.Request(user)); else Console.WriteLine("User not found"); + else if (line.StartsWith("/photo ")) + { + var media = new TL.Layer45.DecryptedMessageMediaPhoto { caption = line[7..] }; + var file = await Secrets.UploadFile(File.OpenRead(line[7..]), media); + var sent = await Secrets.SendMessage(ActiveChat, new TL.Layer73.DecryptedMessage { random_id = WTelegram.Helpers.RandomLong(), + media = media, flags = TL.Layer73.DecryptedMessage.Flags.has_media }, file: file); + } else Console.WriteLine("Unrecognized command"); } else if (ActiveChat == null) Console.WriteLine("No active secret chat"); @@ -73,7 +82,14 @@ Type a command, or a message to send to the active secret chat:"); if (unem.message.ChatId != ActiveChat) SelectActiveChat(unem.message.ChatId); foreach (var msg in Secrets.DecryptMessage(unem.message)) { - if (msg.Action == null) Console.WriteLine($"{unem.message.ChatId}> {msg.Message}"); + if (msg.Media != null && unem.message is EncryptedMessage { file: EncryptedFile ef }) + { + Console.WriteLine($"{unem.message.ChatId}> {msg.Message} [file being downloaded to media.jpg]"); + using var output = File.OpenWrite("media.jpg"); // not necessarily a JPG, check the msg.Media mime_type + using var decryptStream = new WTelegram.AES_IGE_Stream(output, msg.Media); + await Client.DownloadFileAsync(ef, decryptStream, ef.dc_id, ef.size); + } + else if (msg.Action == null) Console.WriteLine($"{unem.message.ChatId}> {msg.Message}"); else Console.WriteLine($"{unem.message.ChatId}> Service Message {msg.Action.GetType().Name[22..]}"); } break; diff --git a/README.md b/README.md index 0006a19..2debc6f 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ After installing WTelegramClient through [Nuget](https://www.nuget.org/packages/ static async Task Main(string[] _) { using var client = new WTelegram.Client(); - var my = await client.LoginUserIfNeeded(); - Console.WriteLine($"We are logged-in as {my.username ?? my.first_name + " " + my.last_name} (id {my.id})"); + var myself = await client.LoginUserIfNeeded(); + Console.WriteLine($"We are logged-in as {myself} (id {myself.id})"); } ``` When run, this will prompt you interactively for your App **api_hash** and **api_id** (that you obtain through Telegram's @@ -152,7 +152,7 @@ See [Examples/Program_ListenUpdates.cs](https://github.com/wiz0u/WTelegramClient An invalid API request can result in a `RpcException` being raised, reflecting the [error code and status text](https://revgram.github.io/errors.html) of the problem. -The other configuration items that you can override include: **session_pathname, email, email_verification_code, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, user_id** +The other configuration items that you can override include: **session_pathname, email, email_verification_code, session_key, server_address, device_model, system_version, app_version, system_lang_code, lang_pack, lang_code, user_id, bot_token** Optional API parameters have a default value of `null` when unset. Passing `null` for a required string/array is the same as *empty* (0-length). Required API parameters/fields can sometimes be set to 0 or `null` when unused (check API documentation or experiment). diff --git a/src/Client.Helpers.cs b/src/Client.Helpers.cs index bd5be12..aae1634 100644 --- a/src/Client.Helpers.cs +++ b/src/Client.Helpers.cs @@ -86,7 +86,7 @@ namespace WTelegram bool abort = false; for (long bytesLeft = length; !abort && bytesLeft != 0; file_part++) { - var bytes = new byte[Math.Min(FilePartSize, bytesLeft)]; + var bytes = new byte[(Math.Min(FilePartSize, bytesLeft) + 15) & ~15]; read = await stream.FullReadAsync(bytes, bytes.Length, default); await _parallelTransfers.WaitAsync(); bytesLeft -= read; diff --git a/src/Encryption.cs b/src/Encryption.cs index 8cec009..2a8b7a2 100644 --- a/src/Encryption.cs +++ b/src/Encryption.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Numerics; +using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; using System.Threading.Tasks; @@ -14,9 +15,9 @@ namespace WTelegram { internal static class Encryption { - internal static readonly RNGCryptoServiceProvider RNG = new(); private static readonly Dictionary PublicKeys = new(); - private static readonly Aes AesECB = Aes.Create(); + internal static readonly RNGCryptoServiceProvider RNG = new(); + internal static readonly Aes AesECB = Aes.Create(); static Encryption() { @@ -295,25 +296,23 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB return AES_IGE_EncryptDecrypt(input, aes_key, aes_iv, encrypt); } - private static byte[] AES_IGE_EncryptDecrypt(Span input, byte[] aes_key, byte[] aes_iv, bool encrypt) + internal static byte[] AES_IGE_EncryptDecrypt(Span input, byte[] aes_key, byte[] aes_iv, bool encrypt) { if (input.Length % 16 != 0) throw new ApplicationException("AES_IGE input size not divisible by 16"); - // code adapted from PHP implementation found at https://mgp25.com/AESIGE/ - var output = new byte[input.Length]; - var xPrev = aes_iv.AsSpan(encrypt ? 16 : 0, 16); - var yPrev = aes_iv.AsSpan(encrypt ? 0 : 16, 16); using var aesCrypto = encrypt ? AesECB.CreateEncryptor(aes_key, null) : AesECB.CreateDecryptor(aes_key, null); - byte[] yXOR = new byte[16]; - for (int i = 0; i < input.Length; i += 16) + var output = new byte[input.Length]; + var prevBytes = (byte[])aes_iv.Clone(); + var span = MemoryMarshal.Cast(input); + var sout = MemoryMarshal.Cast(output); + var prev = MemoryMarshal.Cast(prevBytes); + if (!encrypt) { (prev[2], prev[0]) = (prev[0], prev[2]); (prev[3], prev[1]) = (prev[1], prev[3]); } + for (int i = 0, count = input.Length / 8; i < count;) { - for (int j = 0; j < 16; j++) - yXOR[j] = (byte)(input[i + j] ^ yPrev[j]); - aesCrypto.TransformBlock(yXOR, 0, 16, output, i); - for (int j = 0; j < 16; j++) - output[i + j] ^= xPrev[j]; - xPrev = input.Slice(i, 16); - yPrev = output.AsSpan(i, 16); + sout[i] = span[i] ^ prev[0]; sout[i + 1] = span[i + 1] ^ prev[1]; + aesCrypto.TransformBlock(output, i * 8, 16, output, i * 8); + prev[0] = sout[i] ^= prev[2]; prev[1] = sout[i + 1] ^= prev[3]; + prev[2] = span[i++]; prev[3] = span[i++]; } return output; } @@ -526,4 +525,52 @@ j4WcDuXc2CTHgH8gFTNhp/Y8/SpDOhvn9QIDAQAB } #endif } + + public class AES_IGE_Stream : Helpers.IndirectStream + { + private readonly ICryptoTransform aesCrypto; + private readonly byte[] prevBytes; + + public AES_IGE_Stream(Stream stream, DecryptedMessageMedia media) : this(stream, media.SizeKeyIV) { } + public AES_IGE_Stream(Stream innerStream, (int size, byte[] key, byte[] iv) t) : this(innerStream, t.key, t.iv) { ContentLength = t.size; } + public AES_IGE_Stream(Stream stream, byte[] key, byte[] iv, bool encrypt = false) : base(stream) + { + aesCrypto = encrypt ? Encryption.AesECB.CreateEncryptor(key, null) : Encryption.AesECB.CreateDecryptor(key, null); + if (encrypt) prevBytes = (byte[])iv.Clone(); + else { prevBytes = new byte[32]; Array.Copy(iv, 0, prevBytes, 16, 16); Array.Copy(iv, 16, prevBytes, 0, 16); } + } + + public override int Read(byte[] buffer, int offset, int count) + { + count = _innerStream.Read(buffer, offset, count); + if (count == 0) return 0; + Process(buffer, offset, count); + if (ContentLength.HasValue && _innerStream.Position == _innerStream.Length) + return count - (int)(_innerStream.Position - ContentLength.Value); + return count; + } + + public override void Write(byte[] buffer, int offset, int count) + { + Process(buffer, offset, count); + if (ContentLength.HasValue && _innerStream.Position + count > ContentLength) + count -= (int)(_innerStream.Position + count - ContentLength.Value); + _innerStream.Write(buffer, offset, count); + } + + public void Process(byte[] buffer, int offset, int count) + { + count = (count + 15) & ~15; + var span = MemoryMarshal.Cast(buffer.AsSpan(offset, count)); + var prev = MemoryMarshal.Cast(prevBytes); + for (offset = 0, count /= 8; offset < count;) + { + prev[0] ^= span[offset]; prev[1] ^= span[offset + 1]; + aesCrypto.TransformBlock(prevBytes, 0, 16, prevBytes, 0); + prev[0] ^= prev[2]; prev[1] ^= prev[3]; + prev[2] = span[offset]; prev[3] = span[offset + 1]; + span[offset++] = prev[0]; span[offset++] = prev[1]; + } + } + } } diff --git a/src/SecretChats.cs b/src/SecretChats.cs index 1eeacb5..1fb1987 100644 --- a/src/SecretChats.cs +++ b/src/SecretChats.cs @@ -568,7 +568,28 @@ namespace WTelegram _ = Discard(chat.ChatId); } } + + public async Task UploadFile(Stream stream, DecryptedMessageMedia media) + { + byte[] aes_key = new byte[32], aes_iv = new byte[32]; + RNG.GetBytes(aes_key); + RNG.GetBytes(aes_iv); + media.SizeKeyIV = (checked((int)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); + var digest = md5.Hash; + long fingerprint = BinaryPrimitives.ReadInt64LittleEndian(digest); + fingerprint ^= fingerprint >> 32; + + using var ige = new AES_IGE_Stream(stream, aes_key, aes_iv, true); + return await client.UploadFileAsync(ige, null) switch + { + InputFile ifl => new InputEncryptedFileUploaded { id = ifl.id, parts = ifl.parts, md5_checksum = ifl.md5_checksum, key_fingerprint = (int)fingerprint }, + InputFileBig ifb => new InputEncryptedFileBigUploaded { id = ifb.id, parts = ifb.parts, key_fingerprint = (int)fingerprint }, + _ => null + }; + } } } - -// TODO https://core.telegram.org/api/end-to-end#sending-encrypted-files diff --git a/src/TL.Helpers.cs b/src/TL.Helpers.cs index 0042e2d..bca7c92 100644 --- a/src/TL.Helpers.cs +++ b/src/TL.Helpers.cs @@ -389,6 +389,7 @@ namespace TL partial class EncryptedFile { public static implicit operator InputEncryptedFile(EncryptedFile file) => file == null ? null : new InputEncryptedFile { id = file.id, access_hash = file.access_hash }; + public static implicit operator InputEncryptedFileLocation(EncryptedFile file) => file == null ? null : new InputEncryptedFileLocation { id = file.id, access_hash = file.access_hash }; public InputEncryptedFileLocation ToFileLocation() => new() { id = id, access_hash = access_hash }; } diff --git a/src/TL.SchemaFuncs.cs b/src/TL.SchemaFuncs.cs index 4a02c8c..5dc865a 100644 --- a/src/TL.SchemaFuncs.cs +++ b/src/TL.SchemaFuncs.cs @@ -170,9 +170,9 @@ namespace TL /// Binds a temporary authorization key temp_auth_key_id to the permanent authorization key perm_auth_key_id. Each permanent key may only be bound to one temporary key at a time, binding a new temporary key overwrites the previous one. See [bots: ✓] Possible codes: 400 (details) /// Permanent auth_key_id to bind to - /// Random long from Binding message contents - /// Unix timestamp to invalidate temporary key, see Binding message contents - /// See Generating encrypted_message + /// Random long from Binding message contents + /// Unix timestamp to invalidate temporary key, see Binding message contents + /// See Generating encrypted_message public static Task Auth_BindTempAuthKey(this Client client, long perm_auth_key_id, long nonce, DateTime expires_at, byte[] encrypted_message) => client.Invoke(new Auth_BindTempAuthKey { @@ -3955,7 +3955,7 @@ namespace TL /// Get channels/supergroups/geogroups we're admin in. Usually called when the user exceeds the for owned public channels/supergroups/geogroups, and the user is given the choice to remove one of his channels/supergroups/geogroups. See Possible codes: 400 (details) /// Get geogroups - /// If set and the user has reached the limit of owned public channels/supergroups/geogroups, instead of returning the channel list one of the specified errors will be returned.
Useful to check if a new public channel can indeed be created, even before asking the user to enter a channel username to use in channels.checkUsername/channels.updateUsername. + /// If set and the user has reached the limit of owned public channels/supergroups/geogroups, instead of returning the channel list one of the specified errors will be returned.
Useful to check if a new public channel can indeed be created, even before asking the user to enter a channel username to use in channels.checkUsername/channels.updateUsername. public static Task Channels_GetAdminedPublicChannels(this Client client, bool by_location = false, bool check_limit = false) => client.Invoke(new Channels_GetAdminedPublicChannels { diff --git a/src/TL.Secret.cs b/src/TL.Secret.cs index 0fa2a5d..3600095 100644 --- a/src/TL.Secret.cs +++ b/src/TL.Secret.cs @@ -29,7 +29,10 @@ namespace TL /// Object describes media contents of an encrypted message. See /// a null value means decryptedMessageMediaEmpty - public abstract class DecryptedMessageMedia : IObject { } + public abstract class DecryptedMessageMedia : IObject + { + public virtual (int, byte[], byte[]) SizeKeyIV { get => default; set => throw new ApplicationException("Incompatible DecryptedMessageMedia"); } + } /// Object describes the action to which a service message is linked. See public abstract class DecryptedMessageAction : IObject { } @@ -104,6 +107,8 @@ namespace TL public byte[] key; /// Initialization vector public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Video attached to an encrypted message. See [TLDef(0x4CEE6EF3)] @@ -127,6 +132,8 @@ namespace TL public byte[] key; /// Initialization vector public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// GeoPoint attached to an encrypted message. See [TLDef(0x35480A59)] @@ -169,6 +176,8 @@ namespace TL public byte[] key; /// Initialization public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Audio file attached to a secret chat message. See [TLDef(0x6080758F)] @@ -182,6 +191,8 @@ namespace TL public byte[] key; /// Initialization vector public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Setting of a message lifetime after reading. See @@ -293,6 +304,8 @@ namespace TL public byte[] key; /// Initialization vector public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Audio file attached to a secret chat message. See [TLDef(0x57E0A9CB)] @@ -308,6 +321,8 @@ namespace TL public byte[] key; /// Initialization vector public byte[] iv; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in Sequence number is Secret Chats. See @@ -447,6 +462,8 @@ namespace TL public byte[] iv; /// Caption public string caption; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Video attached to an encrypted message. See [TLDef(0x970C8C0E)] @@ -474,6 +491,8 @@ namespace TL public byte[] iv; /// Caption public string caption; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Document attached to a message in a secret chat. See [TLDef(0x7AFE8AE2)] @@ -497,6 +516,8 @@ namespace TL public DocumentAttribute[] attributes; /// Caption public string caption; + + public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; } } /// Venue See [TLDef(0x8A0DF56F)]