We used to think of Telegram as a reliable and secure transmission medium for messages of any sort. But under the hood, it has a rather common combination of a- and symmetric encryptions. Where’s fun in that? And why would anyone trust their messages to the third-party anyway?
TL;DR — inventing a private covert channel over users blocking each other.
There are many workarounds to transmit data between two users avoiding direct contact. You can use middlemen, crypto- and steganography methods, broadcasting relay networks, and other extensions of existing protocols. But sometimes it’s useful being able to establish secure contact using only officially documented features. Or as one should say, set up a covert channel.
We can see an example of it in a Soviet spy movie “Seventeen Moments of Spring” (this one is, like, really good, give it a chance). In it, a flower in a window of the safe house was used to signal if the spy had failed his mission or not. The flower by itself does not mean anything: it can be there and could be not, such symbiosis is a common thing and only telling us about the owner’s love for flowers. Only a predetermined interpretation distinguishes the information received by a spy from the one received by a random passerby.
To organize your own covert channel by the same principle you’ll need only two things: a window and a flower. The window represents an object you can change the state of seen by others and the flower — possible states and a way of changing them.
So what Alice could change in Telegram that Bob can see? Many things, actually: avatars, usernames, last visited time and more. But usually, these things are available to everyone at the same time, limiting dialog privacy — if one possesses the transition method, he could read anything Alice sends. Surprisingly, it is possible to get around this limitation without any kind of encryption involved.
Every user has its own blacklist, and if the reader was annoying enough, he should have noticed after being blocked that his not-already-a-friend ‘last visited’ status changed to ‘last seen a long time ago’. The truth is, he could have been online just a few seconds ago or even be right now, but Telegram API will not send this information to your app anymore. That way, it is protecting other user’s privacy from unwanted ones. In exchange, they can see if they are blacklisted or not.
Possibility to send and receive bits is fun and all, but we still need to describe its exploitation mechanism. Telegram refuses to notice you when blocked, so every ‘receive bit’ action should be initialized by the recipient (let’s call him Bob) and not depend on the sender (and she will be Alice), i. e. by independent. It also follows that Alice and Bob should do requests at the same frequency.
Bit exchange algorithm on every clock looks something like this:
- A checks sending a bit and if has different from the previous value changing it depending on a value:
- A -> T: block B if bit is 1;
- A -> T: unblock B if bit is 0.
- B receives a bit:
- B -> T: resolve A;
- T -> B: available to B information about A;
- B: checks if the received information has a status it:
- B: if it is -> he is not blocked and the bit is 0
- B: if it is not -> he is blocked and the bit is 1
Most modern PCs have good frequency generators (a system clock, for example) so we can synchronize our clocks with them while not using the channel to transmit anything except for the message bits. Only worth noticing that Telegram API requests, both (un)blocking and user status resolving, are network calls and do not tend to work quickly, especially if you are using proxies or VPN. This produces a limitation: clock length should be longer, than an average response time (since we need to fit one into another) and that’s why our data transmission speed will be limited.
Texts in natural languages have pretty high redundancy, and messages received with errors will still be mostly readable by a human. And since Telegram is a messenger (ignoring some crazy stuff), we can neglect error correction limiting possible transmitting data to simple text messages.
Our channel has extremely low bandwidth, so why we need to use the most effective message encoding available for possible messages. Lucky us, the name of the messenger is reminding about times such problem was a common one.
That’s why we, living in the 21st century, will encode our texts with one of the most efficient available to telegraphers a hundred years ago encodings — the Baudot code. More precisely, its final variation ITA-2 created by Donald Murray to perform fewer API calls at the most frequent symbols of the language.
The only left to successfully transmit a message is to find session boundaries so the recipient could find a sent one among the continuous bit stream. Before the transmission has started, Bob is either blocked or not, and this state will not change by itself anytime soon. That’s why Alice can indicate session start by swapping it to an opposite for only one clock. At the successful end of the session, she will unblock him and leave with peace. He, on the other side, will continue to receive zero bits until decides they are not a part of the message — the Baudot code has no
Drawbacks of the method are a practical impossibility to connect (you can, but it will likely require manual error correction due to the bit shift) to ongoing translation and a need to separate null symbols received with errors from ones been sent. But there all problems of implementation.
After several hours spent trying to use an official library API, I got tired and wrote everything with a Python using more human-friendly Telethon. It even has a synchronous-style API, which is rare today for some strange reasons. Message encoding with ITA-2 I wrote by myself since have found nothing useful on the Internet.
Clock synchronization made with system clocks (and yes, it sleep()s! in between) since it is precise enough considering the time required on every network API call is more than a tenth of a second in most cases. User can set transmission speed as he wants to, but I recommend following ‘no more than a request per second’ rule if you don’t want to both see errors on the other side and find yourself banned by a flood prevention system. Telegram turned out to be very picky about API usage, freezing my access for a day from even a few simple (successful!) authorization attempts in a row and just random blocking for a flood during the transmission for an unknown reason. You should always declare your API usage limits, guys.
If the user decided to use such a weird channel to exchange messages, he should not care about any graphical user interface features. And not all systems have it anyway, that’s why I wrote my application in the form of terminal tool. It allows to both send and receive messages via a channel by a given in command-line arguments user id, but only one operation per launch. Of course, no one will limit you to running only one copy of a program at ones and use multiple channels simultaneously in both ways, you’ll just need to run several copies of the same script with different parameters.
You can read more about using this thing as both command-line utility and a python3 library through the API at the GitHub (repository linked at the end of the post). The only problem is to acquire your own API credentials (simple manual is helpful enough) since Telegram does not allow to disclose mine and set according values in your local copy of a script. Everything passed through the command line arguments except for the authorization part (which by default made through the stdio) and looks like this:
For Alice: | For Bob: Enter your phone number: XXX | Enter your phone number: XXX Enter auth code: YYY | Enter auth code: YYY Started message transmission... | Listening for the message... ---++ ('O', '9') | ---++ ('O', '9') --+-+ ('H', '#') | --+-+ ('H', '#') +++++ (1, 1) | +++++ (1, 1) --++- ('N', ',') | --++- ('N', ',') --+-- (' ', ' ') | --+-- (' ', ' ') ++-++ (0, 0) | ++-++ (0, 0) --+-+ ('H', '#') | --+-+ ('H', '#') -++-- ('I', '8') | -++-- ('I', '8') --+-- (' ', ' ') | --+-- (' ', ' ') --+++ ('M', '.') | --+++ ('M', '.') ++--- ('A', '-') | ++--- ('A', '-') -+-+- ('R', "'") | -+-+- ('R', "'") ++++- ('K', '(') | ++++- ('K', '(') +++++ (1, 1) | +++++ (1, 1) +-++- ('F', '!') | +-++- ('F', '!') --+++ ('M', '.') | --+++ ('M', '.') --+++ ('M', '.') | --+++ ('M', '.') Done, exiting... | ----- ('', '') | ----- ('', '') | Automatically decoded: OH, HI MARK!..
Received message will be decoded automatically, but if you want to track progress and/or correct some errors manually, take look at command line output.
Worth noticing that it is possible to implement before-mentioned channel over any messenger and/or social network in which one can detect if he got blocked by others or not. You can use my code to do so and not reinvent the wheel. Low python’s performance (compared to usual for such things C/++) will not be a limiting factor due to low transmission speed and API calls response time.