<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cloudyy - Blog</title><description/><link>https://cloudyy.dev/</link><atom:link href="https://cloudyy.dev/posts/rss.xml" rel="self" type="application/rss+xml"/><item><title>Pi-Hole instance on the cloud &amp; security</title><link>https://cloudyy.dev/posts/pi-hole-instance-on-the-cloud-and-security/</link><guid isPermaLink="true">https://cloudyy.dev/posts/pi-hole-instance-on-the-cloud-and-security/</guid><description>The steps I took to secure my Pi-Hole instance on the cloud</description><pubDate>Sun, 02 Mar 2025 14:39:57 GMT</pubDate><content:encoded>&lt;h2&gt;Backstory&lt;/h2&gt;
&lt;p&gt;Since I decided to sell my old Dell Optiplex that I used to self host things, the only service that I kept hosting at home was a Pi-Hole instance on an old phone.&lt;/p&gt;
&lt;p&gt;It works, sort of.. Like it worked as expected after I found &lt;a href=&quot;https://github.com/DesktopECHO/Pi-hole-for-Android&quot;&gt;this project&lt;/a&gt;, but even for something as well optimized and made to run on a Raspberry Pi, it was horrendously slow. But it was kind of ok, not very reliable or fast, but it got the job done at the end of the day..&lt;/p&gt;
&lt;p&gt;But there&apos;s a little problem with hosting things at home: when you&apos;re not at home. I know that something like ZeroTier could solve this, especially since the mobile client has the option to set custom DNS addresses. I was just not brave enough to create a bomb, since the phone could barely run the Pi-Hole instance, adding something like that could potentially create a fire hazard.&lt;/p&gt;
&lt;p&gt;So I decided to host it in the cloud, solving both speed and external access problems. At this point I was well aware that doing this was a terrible idea since your instance could be a public resolver, but with DNSSEC and rate limiting I thought it was okay.&lt;/p&gt;
&lt;p&gt;Then, very quickly, the thing that ruins the internet began to show up: bots. Some were just friendly security research bots that just ping the server, others were not clear about their purpose and multiple instances of them kept pinging, and then there were the Chinese ones that actually pissed me off a bit.&lt;/p&gt;
&lt;p&gt;It&apos;s not that I want to generalize, but every single one of the annoying bots had a Chinese IP address. The majority made a bunch of queries from a handful of different IP addresses, quite a few from the same CIDR.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Now that I think about it, one of them had a kind of natural use, as if a person was actually using it, which makes me think if someone was actually using it...&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Anyway, back to the Chinese bots. One day I went to do the usual check on the instance, everything was fine until I looked at the client list. A new bot had made about 500k queries, turning on the query log (which I had turned off since the log file size skyrockets with DNSSEC), it was slamming the service with hundreds of bogus domains, just different enough from each other to not fall into the Pi-Hole cache.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Actually, congratulations to the Pi-Hole development team for making it so optimized. If I hadn&apos;t checked the client list, I wouldn&apos;t have noticed a difference as it handled those hundreds of queries per minute like it was nothing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And what about now? How do we block all queries from unknown clients without too much work?&lt;/p&gt;
&lt;p&gt;The solution was behind a neglected feature that I never thought to use: groups. This way I could create a blocked group that all new clients would fall into, and a whitelisted group with all my clients.&lt;/p&gt;
&lt;h2&gt;Meet &lt;code&gt;(.*?)&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;By adding this regex pattern to your domain filter and regex denying it on the &lt;code&gt;Default&lt;/code&gt; group, all queries from all clients will be blocked.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just make sure you don&apos;t enable it before creating a whitelisted group and adding yourself to it, otherwise you may end up blocking yourself as well. Yes, I&apos;m talking from experience 🥲.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Then all you need to do is create another group where this rule doesn&apos;t apply, add your clients to it, and you&apos;ve successfully created a whitelist.&lt;/p&gt;
&lt;p&gt;Now you can sit back and relax, right?&lt;/p&gt;
&lt;h2&gt;IP addresses change&lt;/h2&gt;
&lt;p&gt;IP addresses change over time and across networks, making manual whitelisting a pain, especially since you are automatically blocked if you are on a different client/IP address.&lt;/p&gt;
&lt;p&gt;The laziest solution would be to whitelist your *CIDR, since at least in my experience, just doing that handles any eventual small changes well.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;*: Imagine this is your public IP address: &lt;code&gt;192.123.45.67&lt;/code&gt;, the CIDR would be &lt;code&gt;192.123.45.0/24&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But what about if you are on mobile data or on public wi-fi? These IPs change all the time and in mobile data, just allowing a CIDR will not do the trick because the whole IP changes frequently.&lt;/p&gt;
&lt;h2&gt;Engineers solve practical problems&lt;/h2&gt;
&lt;p&gt;Keeping track of changing addresses... doesn&apos;t that sound like a task that could be automated? If your answer was yes, it really is! If it was no... umm, isn&apos;t this post extremely boring for you?&lt;/p&gt;
&lt;p&gt;So I decided to write a simple Bash script just as a proof of concept, I was then shot 57 times.&lt;/p&gt;
&lt;p&gt;Sorry, that meme was the first way I thought of expressing how I felt about looking for documentation for the Pi-Hole API for longer than it should be, only to find out a day later that it lives inside your instance... Why? I don&apos;t know, maybe they wanted to take the Forgejo approach and make it local and interactive, which is good to be able to test things directly in your browser, but Forgejo makes it clean with a link in the footer.&lt;/p&gt;
&lt;p&gt;Anyway, finally with the documentation, I decided to code it in Rust. Yes, at this point I gave up on doing it in Bash, since it would actually be easier to implement more complex stuff and make it more portable with Rust.&lt;/p&gt;
&lt;p&gt;The steps are actually quite simple:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Authenticate with the Pi-Hole instance.&lt;/li&gt;
&lt;li&gt;Find the ID of the allowed group.&lt;/li&gt;
&lt;li&gt;Get your public IP address.&lt;/li&gt;
&lt;li&gt;Put yourself in the whitelist.&lt;/li&gt;
&lt;li&gt;&lt;s&gt;Read the compiler&apos;s death threats about your bad code, optional.&lt;/s&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I&apos;ll do my best to keep the explanation as simple and short as possible. Also don&apos;t mind if the code is bad, it&apos;s been a while since I used Rust, you could say my skills are kind of... you know the joke 😐.&lt;/p&gt;
&lt;h3&gt;Storing some configs&lt;/h3&gt;
&lt;p&gt;Before getting started, it&apos;s good to organize what variables our program will use. For this use case we need to store:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The app password for authentication&lt;/li&gt;
&lt;li&gt;The URL of the API&lt;/li&gt;
&lt;li&gt;The name of the whitelisted group&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;With that in mind, let&apos;s create a &lt;code&gt;conf.toml&lt;/code&gt; file with the following contents:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;api_url = &quot;https://pihole.example.org/api&quot;
app_password = &quot;secret password&quot;
whitelisted_group_name = &quot;whitelisted group name&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And to be able to load this config file into the program, let&apos;s create a simple wrapper function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use serde::Deserialize;

#[derive(Deserialize)]
pub struct Config {
    pub api_url: String,
    pub app_password: String,
    pub whitelisted_group_name: String,
}

fn config() -&amp;gt; Config {
    let conf_file = std::fs::read_to_string(&quot;./conf.toml&quot;).expect(&quot;Failed to read conf.toml&quot;);

    let parsed: Config =
        toml::from_str(&amp;amp;conf_file).expect(&quot;Failed to parse conf.toml&quot;);

    parsed
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With that properly set up, we can jump right into the first step of the process: authenticate with the Pi-Hole API.&lt;/p&gt;
&lt;h3&gt;Authentication with the API&lt;/h3&gt;
&lt;p&gt;Pi-Hole has a pretty straightforward approach to auth, POST your app password to &lt;code&gt;/api/auth&lt;/code&gt; and get an SID. So, based on our config:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use reqwest;

async fn auth() {
	let body = serde_json::json!({
		&quot;password&quot;: config.app_password
	});
	let res = reqwest::Client::new()
		.post(format!(&quot;{}/auth&quot;, conf.api_url))
		.json(&amp;amp;body)
		.send()
		.await
		.unwrap();
	println!(&quot;{:?}&quot;, res.text().await);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then in your console you should see something like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;session&quot;: {
		&quot;valid&quot;: true,
		&quot;sid&quot;: &quot;.......&quot;,
		...
	}
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you didn&apos;t got any errors, you are all good to handle the response and proceed to step 2: fetching the group ID.&lt;/p&gt;
&lt;h3&gt;Group IDs&lt;/h3&gt;
&lt;p&gt;In order to add ourselves to the correct group, we need to know which is the correct ID of that group. In reality, it&apos;s quite easy to know, since the &lt;code&gt;Default&lt;/code&gt; group has an ID of 1. But just knowing the answer is no fun, and there&apos;s probably someone with a dozen groups, so let&apos;s use the API for that.&lt;/p&gt;
&lt;p&gt;With our SID in hands, we could call &lt;code&gt;/api/groups/[group name]&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn fetch_group_id(sid: String) {
	let mut headers = header::HeaderMap::new();

	headers.insert(&quot;X-FTL-SID&quot;, sid.parse().unwrap());

	let res = reqwest::Client::new()
		.get(format!(
			&quot;{}/groups/{}&quot;,
			conf.api_url, conf.whitelisted_group_name
		))
		.headers(headers)
		.send()
		.await
		.unwrap();
	println!(&quot;{:?}&quot;, res.text().await);
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
	&quot;groups&quot;: [
		{
			&quot;name&quot;: &quot;Whitelist&quot;,
			&quot;id&quot;: 2
		}
	]
	...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;No errors? Awesome! Now to the next step: find your IP address.&lt;/p&gt;
&lt;h3&gt;What is my IP address&lt;/h3&gt;
&lt;p&gt;Despite the title, we will not use any of these websites for this example. Instead, we will use an instance of &lt;a href=&quot;https://hub.docker.com/r/traefik/whoami&quot;&gt;&lt;code&gt;traefik/whoami&lt;/code&gt;&lt;/a&gt;. Why do this? I just had one running for some testing and since it already has an API, why not?&lt;/p&gt;
&lt;p&gt;Actually, since this step might be different for your use case, I&apos;m going to assume that you already have your public IPv4 address, and now you&apos;re just getting the CIDR. This step actually took me a minute before I realized &quot;wait, I just want to put a zero and a forward slash, just split the string bro&quot;.&lt;/p&gt;
&lt;p&gt;And after resisting the urge to search &quot;How to split a string in Rust&quot; on StackOverflow, the code is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let ip_parts: Vec&amp;lt;&amp;amp;str&amp;gt; = ip_addr.split(&apos;.&apos;).collect();

if ip_parts.len() != 4 {
	println!(&quot;Sir that&apos;s not a valid IPv4 address&quot;);
}

let cidr = format!(&quot;{}.{}.{}.0/24&quot;, ip_parts[0], ip_parts[1], ip_parts[2]);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And with that we&apos;ve completed all the requirement steps, just one more to go.&lt;/p&gt;
&lt;h3&gt;Whitelisting the client&lt;/h3&gt;
&lt;p&gt;Just as simple as the other steps: POST to &lt;code&gt;/api/clients&lt;/code&gt; with some info:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn whitelist_client(sid: String, group: i32, client_cidr: String) {
	let mut headers = header::HeaderMap::new();
	let body = serde_json::json!({
		&quot;client&quot;: client_cidr,
		&quot;comment&quot;: &quot;Comment&quot;,
		&quot;groups&quot;: [group]
	});

	headers.insert(&quot;X-FTL-SID&quot;, sid.parse().unwrap());

	let res = reqwest::Client::new()
		.post(format!(&quot;{}/clients&quot;, conf.api_url))
		.json(&amp;amp;body)
		.headers(headers)
		.send()
		.await
		.unwrap();
	println!(&quot;{:?}&quot;, res.text().await)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If that was successful, we are done! This endpoint likes to &quot;fail with grace&quot; by returning a 200 but error-ed, this usually happens when the &lt;code&gt;client&lt;/code&gt; is not unique, but it&apos;s no big deal.&lt;/p&gt;
&lt;h2&gt;What if I&apos;m already blocked?&lt;/h2&gt;
&lt;p&gt;Let&apos;s say you did a little opsie, blocked yourself, and your Pi-Hole instance is only accessible through a reverse proxy (a situation that has never happened to me, of course), how about now?&lt;/p&gt;
&lt;p&gt;Well, if you are blocked, it means that you are not able to resolve any domain name (unless you have it set up in &lt;code&gt;/etc/hosts&lt;/code&gt;), but you still have internet access. This means we can use a little (unsafe) trick: just contact the server directly by it&apos;s IP address.&lt;/p&gt;
&lt;p&gt;To do that with cURL it&apos;s pretty simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;curl --request GET \
	--url https://&amp;lt;ip&amp;gt;/api \
	--header &apos;Host: pihole.example.org&apos; \
	-k
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since we are explicitly specifying the &lt;code&gt;Host&lt;/code&gt; header, TLS will freak out and fail. So we need to ignore it by using the &lt;code&gt;-k&lt;/code&gt; flag. This is of course insecure, but it solves our problem.&lt;/p&gt;
&lt;p&gt;To implement it with &lt;code&gt;reqwest&lt;/code&gt; we will just need to create a client with this option:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let req_client = reqwest::Client::builder()
	.danger_accept_invalid_certs(true)
	.build()
	.unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, by replacing the usual &lt;code&gt;Client::new()&lt;/code&gt; with this client, it will be possible to ignore TLS errors. (Did I mentioned that this is insecure?)&lt;/p&gt;
&lt;p&gt;Then you could pass the host with:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use reqwest::header;

let mut headers = header::HeaderMap::new();
headers.insert(header::HOST, &quot;pihole.example.org&quot;.parse.unwrap())
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Well, this is it. It was pretty fun to come up with this excuse to do some coding but solve this persistent problem. Hopefully this will be useful to someone else.&lt;/p&gt;
&lt;p&gt;The full code is also available on my &lt;a href=&quot;https://codeberg.org/CloudyyUw/pihole-client-whitelist-api&quot;&gt;Codeberg profile&lt;/a&gt; if you want to check out everything.&lt;/p&gt;
&lt;p&gt;See ya!&lt;/p&gt;
</content:encoded><category>programming</category><category>rust</category><author>CloudyyUw</author></item><item><title>Using Telegram as infinite cloud storage</title><link>https://cloudyy.dev/posts/using-telegram-as-infinite-cloud-storage/</link><guid isPermaLink="true">https://cloudyy.dev/posts/using-telegram-as-infinite-cloud-storage/</guid><description>Transforming Telegram into an E2EE infinite cloud storage</description><pubDate>Wed, 14 Feb 2024 20:51:59 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;17/08/24 Edit&lt;/strong&gt;: I decided to rewrite this post because it was very technical and difficult to understand.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A while ago I was backing up my homelab and the total size was about 300 GB, which was no problem to store locally, but I still needed an external backup. So I started looking for a cloud storage provider, but they are too expensive for sporadic things like huge backups, and there is also the issue of trust.&lt;/p&gt;
&lt;p&gt;While researching, I remembered a project a friend told me about a long time ago called &lt;a href=&quot;https://github.com/mgilangjanuar/teledrive&quot;&gt;Teledrive&lt;/a&gt;, a project for using Telegram to store files. But this project is dead and I was felling like doing some tinkering&lt;/p&gt;
&lt;h2&gt;How that works?&lt;/h2&gt;
&lt;p&gt;Telegram has 2 APIs, one for bots and one for clients. The bot API is usually easier to use, but has more limitations, with the limit of 20 MB for downloads and 50 MB for upload being the most impactful, and when we talk about sending and downloading files they are very important. On the other hand the API for clients has only a 2 GB per file limit.&lt;/p&gt;
&lt;p&gt;With that in mind I decided to write some code in rust to do a proof-of-concept.&lt;/p&gt;
&lt;h2&gt;Connecting to Telegram&lt;/h2&gt;
&lt;p&gt;The first and most important step is to figure out how to connect to Telegram as a client, and for that we can use &lt;a href=&quot;https://github.com/Lonami/grammers&quot;&gt;grammers&lt;/a&gt;, a very powerfull and simple to use library to interact with the Telegram API (both for bots and clients). Now that we have everything we can start writing some code.&lt;/p&gt;
&lt;h3&gt;Receiving the OTP code&lt;/h3&gt;
&lt;p&gt;Since we are using the client&apos;s API, we will need to authenticate with a phone number and then confirm with a code. For that let&apos;s create first a simple function that prompts a message to the user:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use std::io::{self, BufRead, Write};

fn prompt(message: &amp;amp;str) -&amp;gt; io::Result&amp;lt;String&amp;gt; {
    let mut stdout = io::stdout().lock();

    stdout.write_all(message.as_bytes())?;
    stdout.flush()?;

    let mut stdin = io::stdin().lock();
    let mut line = String::new();

    stdin.read_line(&amp;amp;mut line)?;

    Ok(line)
}
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;From &lt;a href=&quot;https://github.com/Lonami/grammers/blob/f2ad7a37a2ad466623dcaef014e8075102723a30/lib/grammers-client/examples/downloader.rs#L158&quot;&gt;grammers examples/downloader.rs&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Ok done, now we can start the login step:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Dependencies
use grammers_client::{Client, Config, SignInError};
use grammers_session::Session;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To persist the session, we can store it in a file to retrieve later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const SESSION_FILE: &amp;amp;str = &quot;/path/to/telegram.session&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For everything to work, you will need to use a async runtime. At the time I was using actix but you could replace it with regular a regular &lt;code&gt;#[tokio::main]&lt;/code&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;#[actix_web::main]
async fn main() -&amp;gt; std::io::Result&amp;lt;()&amp;gt; {
    // ...
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we can initialize our client:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;let telegram_client = Client::connect(Config {
    session: Session::load_file_or_create(SESSION_FILE)?,
    api_id: env::var(&quot;API_ID&quot;)
        .expect(&quot;API_ID not set&quot;)
        .parse()
        .expect(&quot;Failed to parse API_ID&quot;),
    api_hash: env::var(&quot;API_HASH&quot;)
        .expect(&quot;API_HASH not set&quot;)
        .parse()
        .expect(&quot;Failed to parse API_HASH&quot;),
    params: Default::default(),
}).await.unwrap();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And then sign in:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// Since we are using a file to persist the session, there is no need to login twice right?
if !telegram_client.is_authorized().await.unwrap() {
    println!(&quot;Signing in&quot;);
    let phone_number = prompt(&quot;Enter your phone number: &quot;)?;
    let token = telegram_client
        .request_login_code(&amp;amp;phone_number)
        .await
        .unwrap();
    let code = prompt(&quot;Enter the code that you received: &quot;)?;
    let signed_in = telegram_client.sign_in(&amp;amp;token, &amp;amp;code).await;

    match signed_in {
        Err(SignInError::PasswordRequired(pass_token)) =&amp;gt; {
            let hint = pass_token.hint().unwrap();
            let prompt_msg = format!(&quot;Enter your password (hint {}): &quot;, &amp;amp;hint);
            let password = prompt(prompt_msg.as_str())?;

            telegram_client
                .check_password(pass_token, password.trim())
                .await
                .unwrap();
        }
        Ok(_) =&amp;gt; (),
        Err(err) =&amp;gt; panic!(&quot;{}&quot;, err),
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Encrypting files&lt;/h2&gt;
&lt;p&gt;Another part of the idea is end-to-end encryption, and for this I will use &lt;a href=&quot;https://github.com/orion-rs/orion&quot;&gt;orion&lt;/a&gt; to create some helper functions that will be used later:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use orion::aead;
use sha2::{Digest, Sha256};
use std::env;

fn encrypt(data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {
    let mut hasher = Sha256::new();
    hasher.update(
        env::var(&quot;ENCRYPTION_KEY&quot;)
            .expect(&quot;Missing environment variable ENCRYPTION_KEY&quot;)
            .as_bytes(),
    );
    let pass_hash = hasher.finalize();
    let key = aead::SecretKey::from_slice(&amp;amp;pass_hash).unwrap();

    aead::seal(&amp;amp;key, data).unwrap()
}

fn decrypt(data: &amp;amp;[u8]) -&amp;gt; Vec&amp;lt;u8&amp;gt; {
    let mut hasher = Sha256::new();
    hasher.update(
        env::var(&quot;ENCRYPTION_KEY&quot;)
            .expect(&quot;Missing environment variable ENCRYPTION_KEY&quot;)
            .as_bytes(),
    );
    let pass_hash = hasher.finalize();
    let key = aead::SecretKey::from_slice(&amp;amp;pass_hash).unwrap();

    aead::open(&amp;amp;key, data).unwrap()
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Is there a better way to handle this? Probably, but for now I will use it this way.&lt;/p&gt;
&lt;h2&gt;Sending messages&lt;/h2&gt;
&lt;p&gt;Now that we are connected and have the functions to encrypt, its time to actually send some messages:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// dependencies
use grammers_client::{
    client::auth::InvocationError,
    types::{Downloadable, Message},
    Client, InputMessage,
};
use grammers_session::{PackedChat, PackedType};
use std::{env, sync::Arc};
use tokio::sync::Mutex;

// This is only required when using it with actix
type TelegramClient = Arc&amp;lt;Mutex&amp;lt;Client&amp;gt;&amp;gt;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To send a message, we need to specify a &lt;code&gt;PackedChat&lt;/code&gt; struct for the &lt;code&gt;send_message&lt;/code&gt; function. As it is very annoying to have to type everything over and over again, let&apos;s create a helper function to return the correct struct:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fn message_packet() -&amp;gt; PackedChat {
    PackedChat {
        ty: PackedType::Chat,
        id: env::var(&quot;GROUP_ID&quot;)
            .expect(&quot;Missing environment variable GROUP_ID&quot;)
            .parse()
            .expect(&quot;Failed to parse GROUP_ID to i64&quot;),
        access_hash: None,
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now let&apos;s create a function to send an empty message with a file:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn send_message_with_document(
    client: TelegramClient,
    filepath: &amp;amp;String,
) -&amp;gt; Result&amp;lt;Message, InvocationError&amp;gt; {
    let client = client.lock().await;
    let uploaded_file = (*client).upload_file(filepath).await.unwrap();
    (*client)
        .send_message(
            message_packet(),
            InputMessage::text(&quot;&quot;).document(uploaded_file),
        )
        .await
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And to download the file later it is very simple:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async fn download_message_document(
    client: TelegramClient,
    message_id: i32,
    output_path: String,
) {
    let client = client.lock().await;
    let message = &amp;amp;(*client)
        .get_messages_by_id(message_packet(), &amp;amp;[message_id])
        .await
        .unwrap()[0];
    // No need to loop
    // There is only one message with the id

    match message {
        Some(msg) =&amp;gt; {
            if let Some(media) = msg.media() {
                (*client)
                    .download_media(&amp;amp;Downloadable::Media(media), output_path)
                    .await
                    .expect(&quot;Failed to download file&quot;)
            }
        }
        None =&amp;gt; {}
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Splitting the files into chunks&lt;/h2&gt;
&lt;p&gt;When the file is bigger than our upload limit or for faster upload/downloads, we will need to split the file into smaller chunks. For that we just need to open the file, split the bytes and then upload the chunks, simple enough right?&lt;/p&gt;
&lt;p&gt;Not quite, we are talking of larger files over 2 GB. With that approach the file would need to be loaded in RAM first that could cause &lt;a href=&quot;https://www.kernel.org/doc/gorman/html/understand/understand016.html&quot;&gt;OOM&lt;/a&gt; to kill the process. And to solve this we will use &lt;a href=&quot;https://crates.io/crates/mmap&quot;&gt;mmap&lt;/a&gt; and memory mapped files, allowing us to read files without loading them entire into RAM, at the cost of more disk usage.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;use memmap::Mmap;
use std::{fs, path::PathBuf};

// ~ 200 MB
const TARGET_CHUNK_SIZE: i64 = 209715200;
// 2GB
const TELEGRAM_MAX_FILESIZE: u64 = 2000000000;

async fn parse(...) {
    let file = fs::File::open(&amp;amp;filepath).unwrap();
    let filesize = file.metadata().unwrap().len();

    // If the file is smaller than 2 GB
    // we can just encrypt and upload
    if filesize &amp;lt; TELEGRAM_MAX_FILESIZE {
        let mmap_file = unsafe { Mmap::map(&amp;amp;file).unwrap() };
        let encrypted = encryption::encrypt(&amp;amp;mmap_file);

        // Send and store metadata about the file
        // in a database
        // ...
    } else {
        // If the file is bigger than 2GB
        // we will need to split the file into smaller chunks.
        let mmap_file = unsafe { Mmap::map(&amp;amp;file).unwrap() };
        let filesize = filesize as i64;
        // Calculate the amount of chunks based on the
        // target chunk size.
        // Smaller TARGET_CHUNK_SIZE equals more chunks
        let num_chunks = (filesize + TARGET_CHUNK_SIZE - 1) / TARGET_CHUNK_SIZE;
        let chunksize = (filesize + num_chunks - 1) / num_chunks;
        // Split the contents in chunks
        let chunks = mmap_file.chunks(chunksize as usize).collect::&amp;lt;Vec&amp;lt;&amp;amp;[u8]&amp;gt;&amp;gt;();

        for (index, chunk) in chunks.iter().enumerate() {
            let encrypted = encryption::encrypt(chunk);

            // Again, send and store metadata about the file
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To assemble the file later you just need to do the opposite: download the chunks, loop over them and write the content (in order) into a single file.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This post is just a very basic showcase of the concept of a solution that is not perfect, download and upload speeds are slow, mostly because of Telegram being slow, and (again) there is also the issue of trust. There is also already a good project if you are looking for storing files on Telegram called &lt;a href=&quot;https://github.com/divyam234/teldrive&quot;&gt;Teldrive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Overall, this is just a experiment that I made to try some new things and maybe learn something in the process.&lt;/p&gt;
&lt;p&gt;See ya!&lt;/p&gt;
</content:encoded><category>programming</category><category>rust</category><author>CloudyyUw</author></item></channel></rss>