Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,20 @@ These legacy ciphers are for compatibility/migration workflows and are not recom
## License

crimimal use only lol

### MTProto 2.0

```python
from crypto_standalone.mtproto import encode_encrypted_message, decode_encrypted_message, MessageDirection, SessionValidationState

state = SessionValidationState()
enc = encode_encrypted_message(
auth_key=b"\x00" * 256,
server_salt=123,
session_id=456,
msg_id=state.generate_msg_id(),
seq_no=1,
body=b"ping",
direction=MessageDirection.CLIENT_TO_SERVER
)
```
48 changes: 48 additions & 0 deletions docs/MTPROTO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# MTProto 2.0 Framing Architecture

The MTProto 2.0 implementation in `crypto_standalone` provides a secure, zero-dependency, pure-Python implementation of the framing and cryptographic envelope layers of the Telegram MTProto protocol.

## Supported Transports

All four standard MTProto TCP transports are supported:

- **Abridged**: The lightest protocol. Envelope includes 1-byte marker `0xef` and short/extended lengths.
- **Intermediate**: 4-byte lengths. Envelope includes 4-byte marker `0xeeeeeeee`.
- **Padded-Intermediate**: Padded version to bypass ISP blocks. Envelope includes marker `0xdddddddd` and random padding of 0-15 bytes.
- **Full**: Includes sequence numbers and CRC32 checksums.

Quick ACKs are supported across all transports. The codecs implement incremental, strict parsers suitable for real TCP streams without allocating unbounded buffers.

## Cryptographic Messages

The module implements the MTProto 2.0 message envelope:

- **MTProto 2.0 Key Derivation**: Derives the 256-bit AES key and IV from the authorization key and message key.
- **AES-IGE**: Uses the existing pure-Python AES-256 implementation with Infinite Garble Extension (IGE).
- **Validation**: Enforces mandatory, strictly ordered validation gates (bounds, auth_key resolution, AES decryption, msg_key verification, internal field validation, and msg_id checks) to prevent padding or lengths from serving as side-channel oracles.

### Security Limitations

Because this library is written entirely in Python, it cannot guarantee complete side-channel immunity at the interpreter level:
- There is no hardware-level constant-time execution guarantee (though constant-time comparisons like `compare_digest` are used where applicable).
- Memory cannot be securely zeroized out of Python's garbage collector.
- Immutable strings may leave remnants of plaintext, keys, and message contents in memory.

## Usage Example

```python
from crypto_standalone.mtproto import AbridgedTransportCodec, PayloadFrame

codec = AbridgedTransportCodec()
header = codec.connection_header()
# send header to socket

encrypted_payload = b"..."
wire_packet = codec.encode(encrypted_payload)
# send wire_packet to socket

events = codec.feed_data(received_chunk)
for event in events:
if isinstance(event, PayloadFrame):
print("Received payload of length", len(event.payload))
```
23 changes: 23 additions & 0 deletions src/crypto_standalone/mtproto/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""MTProto 2.0 framing implementation."""

from .core import MessageDirection, ResourceLimits, PayloadFrame, QuickAck, TransportError
from .errors import (
MTProtoFramingError, IncompleteFrameError, InvalidFrameLengthError,
FrameTooLargeError, TransportSequenceError, TransportChecksumError,
MessageKeyMismatchError, SessionMismatchError, ReplayDetectedError
)
from .transports import AbridgedTransportCodec, IntermediateTransportCodec, PaddedIntermediateTransportCodec, FullTransportCodec
from .session import SessionValidationState
from .envelope import UnencryptedMessage, EncryptedMessage, encode_unencrypted_message, decode_unencrypted_message, encode_encrypted_message, decode_encrypted_message
from .containers import ContainerMessage, encode_msg_container, decode_msg_container

__all__ = [
"MessageDirection", "ResourceLimits", "PayloadFrame", "QuickAck", "TransportError",
"MTProtoFramingError", "IncompleteFrameError", "InvalidFrameLengthError",
"FrameTooLargeError", "TransportSequenceError", "TransportChecksumError",
"MessageKeyMismatchError", "SessionMismatchError", "ReplayDetectedError",
"AbridgedTransportCodec", "IntermediateTransportCodec", "PaddedIntermediateTransportCodec", "FullTransportCodec",
"SessionValidationState",
"UnencryptedMessage", "EncryptedMessage", "encode_unencrypted_message", "decode_unencrypted_message", "encode_encrypted_message", "decode_encrypted_message",
"ContainerMessage", "encode_msg_container", "decode_msg_container"
]
91 changes: 91 additions & 0 deletions src/crypto_standalone/mtproto/containers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import struct
from dataclasses import dataclass
from typing import List

from .errors import InvalidFrameLengthError, MTProtoFramingError
from .core import ResourceLimits, DEFAULT_LIMITS

@dataclass
class ContainerMessage:
msg_id: int
seq_no: int
body: bytes

def encode_msg_container(
messages: List[ContainerMessage],
limits: ResourceLimits = DEFAULT_LIMITS
) -> bytes:
"""
Encodes a msg_container containing a list of messages.
msg_container constructor ID is 0x73f1f8dc.
"""
if len(messages) > limits.max_contained_message_count:
raise MTProtoFramingError(f"Too many messages in container: {len(messages)}")

out = bytearray(struct.pack("<II", 0x73f1f8dc, len(messages)))

for msg in messages:
if len(msg.body) % 4 != 0:
raise InvalidFrameLengthError("Nested message body length must be divisible by 4")
out.extend(struct.pack("<QQI", msg.msg_id, msg.seq_no, len(msg.body)))
out.extend(msg.body)

if len(out) > limits.max_container_size:
raise MTProtoFramingError("Container aggregate size exceeds limit")

return bytes(out)

def decode_msg_container(
data: bytes,
container_msg_id: int | None = None,
limits: ResourceLimits = DEFAULT_LIMITS
) -> List[ContainerMessage]:
"""
Decodes a msg_container constructor.
Does not parse recursively.
"""
if len(data) < 8:
raise InvalidFrameLengthError("Container data too small")

constructor, count = struct.unpack("<II", data[:8])
if constructor != 0x73f1f8dc:
raise MTProtoFramingError(f"Not a msg_container constructor: {hex(constructor)}")

if count > limits.max_contained_message_count:
raise MTProtoFramingError(f"Container declares too many messages: {count}")

messages = []
offset = 8

for _ in range(count):
if len(data) - offset < 20:
raise InvalidFrameLengthError("Truncated nested message header")

msg_id, seq_no, length = struct.unpack("<QQI", data[offset:offset+20])
offset += 20

if length < 0 or length % 4 != 0:
raise InvalidFrameLengthError(f"Invalid nested message length: {length}")

if len(data) - offset < length:
raise InvalidFrameLengthError("Truncated nested message body")

if container_msg_id is not None and msg_id >= container_msg_id:
raise MTProtoFramingError(f"Nested msg_id {msg_id} not strictly lower than container msg_id {container_msg_id}")

# Optional: verify not a nested simple container.
# We can look at the first 4 bytes if length >= 4
if length >= 4:
inner_constructor = struct.unpack("<I", data[offset:offset+4])[0]
if inner_constructor == 0x73f1f8dc:
raise MTProtoFramingError("Nested simple containers are rejected")

body = data[offset:offset+length]
messages.append(ContainerMessage(msg_id=msg_id, seq_no=seq_no, body=body))
offset += length

# Check for trailing bytes
if offset != len(data):
raise InvalidFrameLengthError("Trailing bytes after declared final nested message")

return messages
50 changes: 50 additions & 0 deletions src/crypto_standalone/mtproto/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Core data types and limits for MTProto framing."""

import enum
from dataclasses import dataclass
from typing import Optional


class MessageDirection(enum.Enum):
"""Direction of the MTProto message."""
CLIENT_TO_SERVER = 1
SERVER_TO_CLIENT = 2


@dataclass(frozen=True)
class ResourceLimits:
"""Configurable limits for parsing and container unpacking to prevent resource exhaustion."""
max_transport_frame_size: int = 1048576 * 2 # 2 MiB
max_encrypted_payload_size: int = 1048576 * 2
max_unencrypted_payload_size: int = 65536
max_decrypted_plaintext_size: int = 1048576 * 2
max_container_size: int = 1048576 * 2
max_contained_message_count: int = 1024
max_retained_incremental_buffer_size: int = 1048576 * 4 # 4 MiB


DEFAULT_LIMITS = ResourceLimits()


class TransportEvent:
"""Base class for events emitted by a transport codec."""
pass


@dataclass(frozen=True)
class PayloadFrame(TransportEvent):
"""A standard MTProto payload frame."""
payload: bytes
transport_padding: Optional[bytes] = None


@dataclass(frozen=True)
class QuickAck(TransportEvent):
"""A quick acknowledgment frame."""
token: int


@dataclass(frozen=True)
class TransportError(TransportEvent):
"""A transport-level error frame."""
code: int
Loading
Loading