Support for Encrypted Files/Medias

This commit is contained in:
Wizou 2022-10-07 03:00:57 +02:00
parent c6adeb1f31
commit f22990cb58
9 changed files with 143 additions and 37 deletions

View file

@ -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<InputMessagesFilterPinned>(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*

View file

@ -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 <UserID> Initiate Secret Chat with user
/discard [delete] Terminate active secret chat (and delete history)
/request <UserID> Initiate Secret Chat with user (see /users)
/discard [delete] Terminate active secret chat [and delete history]
/select <ChatID> 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;

View file

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

View file

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

View file

@ -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<long, RSAPublicKey> 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<byte> input, byte[] aes_key, byte[] aes_iv, bool encrypt)
internal static byte[] AES_IGE_EncryptDecrypt(Span<byte> 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<byte, long>(input);
var sout = MemoryMarshal.Cast<byte, long>(output);
var prev = MemoryMarshal.Cast<byte, long>(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<byte, long>(buffer.AsSpan(offset, count));
var prev = MemoryMarshal.Cast<byte, long>(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];
}
}
}
}

View file

@ -568,7 +568,28 @@ namespace WTelegram
_ = Discard(chat.ChatId);
}
}
public async Task<InputEncryptedFileBase> 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

View file

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

View file

@ -170,9 +170,9 @@ namespace TL
/// <summary>Binds a temporary authorization key <c>temp_auth_key_id</c> to the permanent authorization key <c>perm_auth_key_id</c>. Each permanent key may only be bound to one temporary key at a time, binding a new temporary key overwrites the previous one. <para>See <a href="https://corefork.telegram.org/method/auth.bindTempAuthKey"/> [bots: ✓]</para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/auth.bindTempAuthKey#possible-errors">details</a>)</para></summary>
/// <param name="perm_auth_key_id">Permanent auth_key_id to bind to</param>
/// <param name="nonce">Random long from <a href="#binding-message-contents">Binding message contents</a></param>
/// <param name="expires_at">Unix timestamp to invalidate temporary key, see <a href="#binding-message-contents">Binding message contents</a></param>
/// <param name="encrypted_message">See <a href="#generating-encrypted-message">Generating encrypted_message</a></param>
/// <param name="nonce">Random long from <a href="https://corefork.telegram.org/method/auth.bindTempAuthKey#binding-message-contents">Binding message contents</a></param>
/// <param name="expires_at">Unix timestamp to invalidate temporary key, see <a href="https://corefork.telegram.org/method/auth.bindTempAuthKey#binding-message-contents">Binding message contents</a></param>
/// <param name="encrypted_message">See <a href="https://corefork.telegram.org/method/auth.bindTempAuthKey#generating-encrypted-message">Generating encrypted_message</a></param>
public static Task<bool> 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
/// <summary>Get <a href="https://corefork.telegram.org/api/channel">channels/supergroups/geogroups</a> we're admin in. Usually called when the user exceeds the <see cref="Config"/> for owned public <a href="https://corefork.telegram.org/api/channel">channels/supergroups/geogroups</a>, and the user is given the choice to remove one of his channels/supergroups/geogroups. <para>See <a href="https://corefork.telegram.org/method/channels.getAdminedPublicChannels"/></para> <para>Possible <see cref="RpcException"/> codes: 400 (<a href="https://corefork.telegram.org/method/channels.getAdminedPublicChannels#possible-errors">details</a>)</para></summary>
/// <param name="by_location">Get geogroups</param>
/// <param name="check_limit">If set and the user has reached the limit of owned public <a href="https://corefork.telegram.org/api/channel">channels/supergroups/geogroups</a>, instead of returning the channel list one of the specified <a href="#possible-errors">errors</a> will be returned.<br/>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 <a href="https://corefork.telegram.org/method/channels.checkUsername">channels.checkUsername</a>/<a href="https://corefork.telegram.org/method/channels.updateUsername">channels.updateUsername</a>.</param>
/// <param name="check_limit">If set and the user has reached the limit of owned public <a href="https://corefork.telegram.org/api/channel">channels/supergroups/geogroups</a>, instead of returning the channel list one of the specified <a href="https://corefork.telegram.org/method/channels.getAdminedPublicChannels#possible-errors">errors</a> will be returned.<br/>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 <a href="https://corefork.telegram.org/method/channels.checkUsername">channels.checkUsername</a>/<a href="https://corefork.telegram.org/method/channels.updateUsername">channels.updateUsername</a>.</param>
public static Task<Messages_Chats> Channels_GetAdminedPublicChannels(this Client client, bool by_location = false, bool check_limit = false)
=> client.Invoke(new Channels_GetAdminedPublicChannels
{

View file

@ -29,7 +29,10 @@ namespace TL
/// <summary>Object describes media contents of an encrypted message. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageMedia"/></para></summary>
/// <remarks>a <c>null</c> value means <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaEmpty">decryptedMessageMediaEmpty</a></remarks>
public abstract class DecryptedMessageMedia : IObject { }
public abstract class DecryptedMessageMedia : IObject
{
public virtual (int, byte[], byte[]) SizeKeyIV { get => default; set => throw new ApplicationException("Incompatible DecryptedMessageMedia"); }
}
/// <summary>Object describes the action to which a service message is linked. <para>See <a href="https://corefork.telegram.org/type/DecryptedMessageAction"/></para></summary>
public abstract class DecryptedMessageAction : IObject { }
@ -104,6 +107,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization vector</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x4CEE6EF3)]
@ -127,6 +132,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization vector</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>GeoPoint attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaGeoPoint"/></para></summary>
[TLDef(0x35480A59)]
@ -169,6 +176,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x6080758F)]
@ -182,6 +191,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization vector</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Setting of a message lifetime after reading. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionSetMessageTTL"/></para></summary>
@ -293,6 +304,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization vector</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Audio file attached to a secret chat message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaAudio"/></para></summary>
[TLDef(0x57E0A9CB)]
@ -308,6 +321,8 @@ namespace TL
public byte[] key;
/// <summary>Initialization vector</summary>
public byte[] iv;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Request for the other party in a Secret Chat to automatically resend a contiguous range of previously sent messages, as explained in <a href="https://corefork.telegram.org/api/end-to-end/seq_no">Sequence number is Secret Chats</a>. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageActionResend"/></para></summary>
@ -447,6 +462,8 @@ namespace TL
public byte[] iv;
/// <summary>Caption</summary>
public string caption;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Video attached to an encrypted message. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVideo"/></para></summary>
[TLDef(0x970C8C0E)]
@ -474,6 +491,8 @@ namespace TL
public byte[] iv;
/// <summary>Caption</summary>
public string caption;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Document attached to a message in a secret chat. <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaDocument"/></para></summary>
[TLDef(0x7AFE8AE2)]
@ -497,6 +516,8 @@ namespace TL
public DocumentAttribute[] attributes;
/// <summary>Caption</summary>
public string caption;
public override (int, byte[], byte[]) SizeKeyIV { get => (size, key, iv); set => (size, key, iv) = value; }
}
/// <summary>Venue <para>See <a href="https://corefork.telegram.org/constructor/decryptedMessageMediaVenue"/></para></summary>
[TLDef(0x8A0DF56F)]