Skip to content

Commit

Permalink
Implement the obfuscated MTProto transport
Browse files Browse the repository at this point in the history
  • Loading branch information
er-azh authored and Lonami committed Dec 21, 2024
1 parent 6442ac6 commit 173a544
Show file tree
Hide file tree
Showing 8 changed files with 190 additions and 3 deletions.
1 change: 1 addition & 0 deletions lib/grammers-crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pbkdf2 = "0.12.2"
sha1 = "0.10.6"
sha2 = "0.10.8"
num-traits = "0.2.19"
ctr = "0.9.2"

[dev-dependencies]
bencher = "0.1.5"
Expand Down
4 changes: 4 additions & 0 deletions lib/grammers-crypto/DEPS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ Used for methods relied on by the 2-factor offered by Telegram.
## toml

Used to test that this file lists all dependencies from `Cargo.toml`.

## ctr

Used for the AES-CTR mode needed for the obfuscated MTProto transport.
1 change: 1 addition & 0 deletions lib/grammers-crypto/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod auth_key;
pub mod deque_buffer;
pub mod factorize;
pub mod hex;
pub mod obfuscated;
pub mod rsa;
pub mod sha;
pub mod two_factor_auth;
Expand Down
43 changes: 43 additions & 0 deletions lib/grammers-crypto/src/obfuscated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2020 - developers of the `grammers` project.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use aes::cipher::{generic_array::GenericArray, KeyIvInit, StreamCipher};

/// This implements the AES-256-CTR cipher used by Telegram to encrypt data
/// when using the obfuscated transport.
///
/// You're not supposed to use this directly, You're probably looking for the
/// actual implementation in `grammers-mtproto`.
pub struct ObfuscatedCipher {
rx: ctr::Ctr128BE<aes::Aes256>,
tx: ctr::Ctr128BE<aes::Aes256>,
}

impl ObfuscatedCipher {
pub fn new(init: &[u8; 64]) -> Self {
let init_rev = init.iter().copied().rev().collect::<Vec<_>>();
Self {
rx: ctr::Ctr128BE::<aes::Aes256>::new(
GenericArray::from_slice(&init_rev[8..40]),
GenericArray::from_slice(&init_rev[40..56]),
),
tx: ctr::Ctr128BE::<aes::Aes256>::new(
GenericArray::from_slice(&init[8..40]),
GenericArray::from_slice(&init[40..56]),
),
}
}

pub fn encrypt(&mut self, buffer: &mut [u8]) {
self.tx.apply_keystream(buffer);
}

pub fn decrypt(&mut self, buffer: &mut [u8]) {
self.rx.apply_keystream(buffer);
}
}
9 changes: 8 additions & 1 deletion lib/grammers-mtproto/src/transport/abridged.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use super::{Error, Transport, UnpackedOffset};
use super::{Error, Tagged, Transport, UnpackedOffset};
use grammers_crypto::DequeBuffer;

/// The lightest MTProto transport protocol available. This is an
Expand Down Expand Up @@ -112,6 +112,13 @@ impl Transport for Abridged {
}
}

impl Tagged for Abridged {
fn init_tag(&mut self) -> [u8; 4] {
self.init = true;
[0xef, 0xef, 0xef, 0xef]
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
13 changes: 11 additions & 2 deletions lib/grammers-mtproto/src/transport/intermediate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use super::{Error, Transport, UnpackedOffset};
use super::{Error, Tagged, Transport, UnpackedOffset};
use grammers_crypto::DequeBuffer;

/// A light MTProto transport protocol available that guarantees data padded
Expand All @@ -31,6 +31,8 @@ pub struct Intermediate {

#[allow(clippy::new_without_default)]
impl Intermediate {
const TAG: [u8; 4] = 0xee_ee_ee_ee_u32.to_le_bytes();

pub fn new() -> Self {
Self { init: false }
}
Expand All @@ -44,7 +46,7 @@ impl Transport for Intermediate {
buffer.extend_front(&(len as i32).to_le_bytes());

if !self.init {
buffer.extend_front(&0xee_ee_ee_ee_u32.to_le_bytes());
buffer.extend_front(&Self::TAG);
self.init = true;
}
}
Expand Down Expand Up @@ -84,6 +86,13 @@ impl Transport for Intermediate {
}
}

impl Tagged for Intermediate {
fn init_tag(&mut self) -> [u8; 4] {
self.init = true;
Self::TAG
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
11 changes: 11 additions & 0 deletions lib/grammers-mtproto/src/transport/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
mod abridged;
mod full;
mod intermediate;
mod obfuscated;

pub use abridged::Abridged;
pub use full::Full;
use grammers_crypto::DequeBuffer;
pub use intermediate::Intermediate;
pub use obfuscated::Obfuscated;
use std::fmt;

/// The error type reported by the different transports when something is wrong.
Expand Down Expand Up @@ -91,8 +93,17 @@ pub trait Transport {
fn pack(&mut self, buffer: &mut DequeBuffer<u8>);

/// Unpacks the input buffer in-place.
/// Subsequent calls to `unpack` should be made with the same buffer,
/// with the data on the ranges from previous `UnpackedOffset` removed.
fn unpack(&mut self, buffer: &mut [u8]) -> Result<UnpackedOffset, Error>;

/// Reset the state, as if a new instance was just created.
fn reset(&mut self);
}

/// The trait used by the obfuscated transport to get the transport tags.
pub trait Tagged {
/// Gets the transport tag for use in the obfuscated transport and
/// changes the internal state to avoid sending the tag again.
fn init_tag(&mut self) -> [u8; 4];
}
111 changes: 111 additions & 0 deletions lib/grammers-mtproto/src/transport/obfuscated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// Copyright 2020 - developers of the `grammers` project.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use grammers_crypto::{obfuscated::ObfuscatedCipher, DequeBuffer};
use log::debug;

use super::{Error, Tagged, Transport, UnpackedOffset};

/// An obfuscation protocol made by telegram to avoid ISP blocks.
/// This is needed to connect to the Telegram servers using websockets or
/// when conecting to MTProto proxies (not yet supported).
///
/// It is simply a wrapper around another transport, which encrypts the data
/// using AES-256-CTR with a randomly generated key that is then sent at the
/// beginning of the connection.
///
/// Obfuscated transport can only be used with "tagged" transports, which
/// provide a way to get the obfuscated tag that is used in the encryption.
/// See the linked documentation for more information.
///
/// [Transport Obfuscation](https://core.telegram.org/mtproto/mtproto-transports#transport-obfuscation)
pub struct Obfuscated<T: Transport + Tagged> {
inner: T,
head: Option<[u8; 64]>,
decrypt_tail: usize,
cipher: ObfuscatedCipher,
}

const FORBIDDEN_FIRST_INTS: [[u8; 4]; 7] = [
[b'H', b'E', b'A', b'D'], // HTTP HEAD
[b'P', b'O', b'S', b'T'], // HTTP POST
[b'G', b'E', b'T', b' '], // HTTP GET
[b'O', b'P', b'T', b'I'], // HTTP OPTIONS
[0x16, 0x03, 0x01, 0x02], // TLS handshake
[0xdd, 0xdd, 0xdd, 0xdd], // Padded Intermediate
[0xee, 0xee, 0xee, 0xee], // Intermediate
];

impl<T: Transport + Tagged> Obfuscated<T> {
fn generate_keys(inner: &mut T) -> ([u8; 64], ObfuscatedCipher) {
let mut init = [0; 64];

while init[4..8] == [0; 4] // Full
|| init[0] == 0xef // Abridged
|| FORBIDDEN_FIRST_INTS.iter().any(|start| start == &init[..4])
{
getrandom::getrandom(&mut init).unwrap();
}

init[56..60].copy_from_slice(&inner.init_tag());

let mut cipher = ObfuscatedCipher::new(&init);

let mut encrypted_init = init.to_vec();
cipher.encrypt(&mut encrypted_init);
init[56..64].copy_from_slice(&encrypted_init[56..64]);

(init, cipher)
}

pub fn new(mut inner: T) -> Self {
let (init, cipher) = Self::generate_keys(&mut inner);

Self {
inner,
head: Some(init),
decrypt_tail: 0,
cipher,
}
}
}

impl<T: Transport + Tagged> Transport for Obfuscated<T> {
fn pack(&mut self, buffer: &mut DequeBuffer<u8>) {
self.inner.pack(buffer);
self.cipher.encrypt(buffer.as_mut());
if let Some(head) = self.head.take() {
buffer.extend_front(&head);
}
}

fn unpack(&mut self, buffer: &mut [u8]) -> Result<UnpackedOffset, Error> {
if buffer.len() < self.decrypt_tail {
panic!("buffer is smaller than what was decrypted");
}

self.cipher.decrypt(&mut buffer[self.decrypt_tail..]);
self.decrypt_tail = buffer.len();

match self.inner.unpack(buffer) {
Ok(offset) => {
self.decrypt_tail -= offset.next_offset;
Ok(offset)
}
Err(e) => Err(e),
}
}

fn reset(&mut self) {
self.inner.reset();
debug!("regenerating keys for obfuscated transport");

let (init, cipher) = Self::generate_keys(&mut self.inner);
self.head = Some(init);
self.cipher = cipher;
}
}

0 comments on commit 173a544

Please sign in to comment.