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
28 changes: 28 additions & 0 deletions benchmarks/bench_ige.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import time
import os
import sys

sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))
from crypto_standalone.symmetric.aes_ige import aes_ige_encrypt, aes_ige_decrypt

def benchmark_ige():
payload_size = 1024 * 1024 # 1MB
payload = os.urandom(payload_size)
key = os.urandom(32)
iv = os.urandom(32)

start = time.perf_counter()
ct = aes_ige_encrypt(payload, key, iv)
enc_time = time.perf_counter() - start

start = time.perf_counter()
dec = aes_ige_decrypt(ct, key, iv)
dec_time = time.perf_counter() - start

assert dec == payload

print(f"AES-256-IGE Encryption (1MB): {enc_time:.4f} seconds ({payload_size / enc_time / 1024 / 1024:.2f} MB/s)")
print(f"AES-256-IGE Decryption (1MB): {dec_time:.4f} seconds ({payload_size / dec_time / 1024 / 1024:.2f} MB/s)")

if __name__ == "__main__":
benchmark_ige()
39 changes: 39 additions & 0 deletions docs/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,42 @@
- **Standard library only**: `urllib.request`, `datetime`, `dataclasses`, `threading`, `time`, `gc`, `sys`, `socket`

No `pip install` required. No C extensions. No `hashlib`, `hmac`, `secrets`, or `os.urandom`.

## AES-IGE Mode

- **Algorithm**: AES-IGE (Infinite Garble Extension).
- **IV Structure**: Exactly 32 bytes (`IV = IV1 || IV2`, where `IV1` is `c_prev` and `IV2` is `p_prev`).
- **Key Sizes**: 128, 192, and 256 bits (16, 24, and 32 bytes).
- **Block Alignment Requirement**: Input plaintexts and ciphertexts must be exact multiples of the 16-byte AES block size. There is no silent padding or unpadding logic.
- **Security Limitations**: AES-IGE provides **confidentiality only**. It **does not** provide authentication or integrity protection. If used in a protocol that does not inherently authenticate data, it must be paired with a separate message authentication mechanism (e.g., HMAC or SHA-256 with key/salt).

### Code Example
```python
from crypto_standalone.symmetric import aes_ige_encrypt, aes_ige_decrypt
import os

key = os.urandom(32) # AES-256
iv = os.urandom(32) # 32-byte IV for IGE
plaintext = b"This is exactly 32 bytes long!!!"

# Encryption
ciphertext = aes_ige_encrypt(plaintext, key, iv)

# Decryption
decrypted = aes_ige_decrypt(ciphertext, key, iv)
assert plaintext == decrypted
```

### Configuration
MTProto operations can be configured with the `MTProtoLimits` object to prevent unbound memory scaling or DoS:
```python
from crypto_standalone.mtproto.config import MTProtoLimits

limits = MTProtoLimits(
max_decrypted_plaintext_size=8 * 1024 * 1024, # 8 MB max
max_replay_entries=1000 # Limit replay cache
)
```

**Implementation Quality Note:**
This pure Python implementation cannot guarantee constant-time execution or reliable memory zeroization. Therefore, it does not provide side-channel resistance against timing or power analysis attacks.
2 changes: 1 addition & 1 deletion src/crypto_standalone/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

__version__ = "2.0.0"

from .symmetric import AES256, AESGCM, ChaCha20Poly1305, chacha20_encrypt, TEA, RedPike, AveMariaCipher
from .symmetric import AES, AES256, AESIGE, aes_ige_encrypt, aes_ige_decrypt, AESGCM, ChaCha20Poly1305, chacha20_encrypt, TEA, RedPike, AveMariaCipher
from .hashing import *
from .asymmetric import *
from .asymmetric import _encode_signature, _decode_signature
Expand Down
33 changes: 33 additions & 0 deletions src/crypto_standalone/mtproto/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from dataclasses import dataclass

@dataclass(frozen=True)
class MTProtoLimits:
max_transport_frame_size: int = 16 * 1024 * 1024
max_encrypted_payload_size: int = 16 * 1024 * 1024
max_unencrypted_payload_size: int = 4 * 1024 * 1024
max_decrypted_plaintext_size: int = 16 * 1024 * 1024
max_container_size: int = 16 * 1024 * 1024
max_container_messages: int = 1024
max_buffered_bytes: int = 32 * 1024 * 1024
max_pending_frame_size: int = 16 * 1024 * 1024
max_replay_entries: int = 4096

def __post_init__(self) -> None:
if self.max_transport_frame_size <= 0:
raise ValueError("max_transport_frame_size must be positive")
if self.max_encrypted_payload_size <= 0:
raise ValueError("max_encrypted_payload_size must be positive")
if self.max_unencrypted_payload_size <= 0:
raise ValueError("max_unencrypted_payload_size must be positive")
if self.max_decrypted_plaintext_size <= 0:
raise ValueError("max_decrypted_plaintext_size must be positive")
if self.max_container_size <= 0:
raise ValueError("max_container_size must be positive")
if self.max_container_messages <= 0:
raise ValueError("max_container_messages must be positive")
if self.max_buffered_bytes <= 0:
raise ValueError("max_buffered_bytes must be positive")
if self.max_pending_frame_size <= 0:
raise ValueError("max_pending_frame_size must be positive")
if self.max_replay_entries <= 0:
raise ValueError("max_replay_entries must be positive")
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/containers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# Containers for MTProto messages
pass
import struct
from dataclasses import dataclass
from typing import List
Expand Down
3 changes: 3 additions & 0 deletions src/crypto_standalone/mtproto/errors.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
class MTProtoError(Exception):
"""Base class for MTProto errors."""
pass
"""MTProto exception hierarchy."""

class MTProtoFramingError(Exception):
Expand Down
3 changes: 3 additions & 0 deletions src/crypto_standalone/mtproto/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MTProtoEvent:
"""Base class for MTProto events."""
pass
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/kdf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# MTProto-specific KDF logic goes here
pass
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/messages.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Encrypted and unencrypted message envelopes
pass
25 changes: 25 additions & 0 deletions src/crypto_standalone/mtproto/session.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
from dataclasses import dataclass, field

@dataclass
class MTProtoSessionState:
session_id: int
server_salt: int
outgoing_content_count: int = 0
last_outgoing_msg_id: int = 0
last_incoming_msg_id: int = 0
# Use a set, but bounded externally via max_replay_entries limit
seen_message_ids: set[int] = field(default_factory=set)

def add_seen_message(self, msg_id: int, max_entries: int) -> None:
"""Adds a seen message ID and ensures the set doesn't grow unbounded."""
if len(self.seen_message_ids) >= max_entries:
# Simplest bound: clear the set or remove smallest. For an exact queue,
# we'd need a more complex structure (like collections.deque).
# For this requirement, we just avoid an unbounded set.
# Here we just keep the most recent elements.
sorted_msgs = sorted(list(self.seen_message_ids))
# keep the last half
half = max_entries // 2
self.seen_message_ids = set(sorted_msgs[-half:])

self.seen_message_ids.add(msg_id)
import time
from typing import Set

Expand Down
3 changes: 3 additions & 0 deletions src/crypto_standalone/mtproto/transports/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .base import FullTransportState

__all__ = ["FullTransportState"]
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/transports/abridged.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Abridged transport implementation
pass
6 changes: 6 additions & 0 deletions src/crypto_standalone/mtproto/transports/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dataclasses import dataclass

@dataclass
class FullTransportState:
outgoing_sequence: int = 0
expected_incoming_sequence: int | None = None
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/transports/full.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Full transport implementation
pass
2 changes: 2 additions & 0 deletions src/crypto_standalone/mtproto/transports/intermediate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Intermediate transport implementation
pass
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Padded intermediate transport implementation
pass
5 changes: 3 additions & 2 deletions src/crypto_standalone/symmetric/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
"""Symmetric encryption: AES-256 (CBC, CTR, GCM) and ChaCha20-Poly1305."""

from .aes import AES256
from .aes import AES, AES256
from .aes_ige import AESIGE, aes_ige_encrypt, aes_ige_decrypt
from .aes_gcm import AESGCM
from .chacha20 import ChaCha20Poly1305, chacha20_encrypt
from .legacy_ciphers import TEA, RedPike, AveMariaCipher

__all__ = ["AES256", "AESGCM", "ChaCha20Poly1305", "chacha20_encrypt", "TEA", "RedPike", "AveMariaCipher"]
__all__ = ["AES", "AES256", "AESIGE", "aes_ige_encrypt", "aes_ige_decrypt", "AESGCM", "ChaCha20Poly1305", "chacha20_encrypt", "TEA", "RedPike", "AveMariaCipher"]
47 changes: 32 additions & 15 deletions src/crypto_standalone/symmetric/aes.py
Original file line number Diff line number Diff line change
Expand Up @@ -387,12 +387,20 @@ def _sub_word(w: int) -> int:
)


def _key_expand(key: bytes) -> list[bytes]:
if len(key) != 32:
raise ValueError("AES-256 requires 32-byte key")
def _key_expand(key: bytes) -> tuple[int, list[bytes]]:
key_len = len(key)
if key_len == 16:
nk = 4
nr = 10
elif key_len == 24:
nk = 6
nr = 12
elif key_len == 32:
nk = 8
nr = 14
else:
raise ValueError("Invalid AES key size. Must be 16, 24, or 32 bytes.")
nb = 4
nk = 8
nr = 14
w = [0] * (nb * (nr + 1))

for i in range(nk):
Expand All @@ -402,7 +410,7 @@ def _key_expand(key: bytes) -> list[bytes]:
temp = w[i - 1]
if i % nk == 0:
temp = _sub_word(_rot_word(temp)) ^ (RCON[i // nk - 1] << 24)
elif i % nk == 4:
elif nk > 6 and i % nk == 4:
temp = _sub_word(temp)
w[i] = w[i - nk] ^ temp

Expand All @@ -412,7 +420,7 @@ def _key_expand(key: bytes) -> list[bytes]:
for c in range(4):
rk.extend(w[4 * r + c].to_bytes(4, "big"))
round_keys.append(bytes(rk))
return round_keys
return nr, round_keys


def _xor_block(a: bytes, b: bytes) -> bytes:
Expand All @@ -438,13 +446,15 @@ def _unpad_pkcs7(data: bytes) -> bytes:


@dataclass(frozen=True)
class AES256:
class AES:
key: bytes

def __post_init__(self) -> None:
if len(self.key) != 32:
raise ValueError("key must be 32 bytes")
object.__setattr__(self, "_rk", _key_expand(self.key))
if len(self.key) not in (16, 24, 32):
raise ValueError("key must be 16, 24, or 32 bytes")
nr, rk = _key_expand(self.key)
object.__setattr__(self, "_nr", nr)
object.__setattr__(self, "_rk", rk)

@property
def _round_keys(self) -> list[bytes]:
Expand All @@ -455,26 +465,28 @@ def encrypt_block(self, block: bytes) -> bytes:
raise ValueError("block must be 16 bytes")
state = _bytes_to_state(block)
rks = self._round_keys
nr = self._nr

_add_round_key(state, rks[0])
for round_idx in range(1, 14):
for round_idx in range(1, nr):
_sub_bytes(state)
_shift_rows(state)
_mix_columns(state)
_add_round_key(state, rks[round_idx])
_sub_bytes(state)
_shift_rows(state)
_add_round_key(state, rks[14])
_add_round_key(state, rks[nr])
return _state_to_bytes(state)

def decrypt_block(self, block: bytes) -> bytes:
if len(block) != 16:
raise ValueError("block must be 16 bytes")
state = _bytes_to_state(block)
rks = self._round_keys
nr = self._nr

_add_round_key(state, rks[14])
for round_idx in range(13, 0, -1):
_add_round_key(state, rks[nr])
for round_idx in range(nr - 1, 0, -1):
_inv_shift_rows(state)
_inv_sub_bytes(state)
_add_round_key(state, rks[round_idx])
Expand Down Expand Up @@ -543,6 +555,11 @@ def decrypt_ctr(self, data: bytes) -> bytes:
counter += 1
return bytes(out)

class AES256(AES):
def __post_init__(self) -> None:
if len(self.key) != 32:
raise ValueError("key must be 32 bytes")
super().__post_init__()
def encrypt_ige(self, plaintext: bytes, iv: bytes) -> bytes:
if len(iv) != 32:
raise ValueError("IGE mode requires a 32-byte IV")
Expand Down
88 changes: 88 additions & 0 deletions src/crypto_standalone/symmetric/aes_ige.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from dataclasses import dataclass
from .aes import AES, _xor_block

@dataclass(frozen=True)
class AESIGE:
cipher: AES

def encrypt(self, plaintext: bytes, iv: bytes) -> bytes:
"""
Encrypts data using AES-IGE (Infinite Garble Extension).

Note: AES-IGE provides confidentiality only. It does not provide
authentication or integrity protection.
"""
if len(iv) != 32:
raise ValueError("AES-IGE requires exactly a 32-byte IV")
if len(plaintext) % 16 != 0:
raise ValueError("Plaintext length must be a multiple of the 16-byte block size")

iv1 = iv[:16]
iv2 = iv[16:]

out = bytearray()
for i in range(0, len(plaintext), 16):
p_i = plaintext[i:i+16]
c_i = _xor_block(self.cipher.encrypt_block(_xor_block(p_i, iv1)), iv2)
out.extend(c_i)
iv1 = c_i
iv2 = p_i

return bytes(out)

def decrypt(self, ciphertext: bytes, iv: bytes) -> bytes:
"""
Decrypts data using AES-IGE (Infinite Garble Extension).

Note: AES-IGE provides confidentiality only. It does not provide
authentication or integrity protection.
"""
if len(iv) != 32:
raise ValueError("AES-IGE requires exactly a 32-byte IV")
if len(ciphertext) % 16 != 0:
raise ValueError("Ciphertext length must be a multiple of the 16-byte block size")

iv1 = iv[:16]
iv2 = iv[16:]

out = bytearray()
for i in range(0, len(ciphertext), 16):
c_i = ciphertext[i:i+16]
p_i = _xor_block(self.cipher.decrypt_block(_xor_block(c_i, iv2)), iv1)
out.extend(p_i)
iv1 = c_i
iv2 = p_i

return bytes(out)

def aes_ige_encrypt(plaintext: bytes, key: bytes, iv: bytes) -> bytes:
"""
Convenience function for AES-IGE encryption.

Args:
plaintext: The data to encrypt. Length must be a multiple of 16.
key: The AES key (16, 24, or 32 bytes).
iv: The 32-byte Initialization Vector.

Returns:
The encrypted ciphertext.
"""
cipher = AES(key)
ige = AESIGE(cipher)
return ige.encrypt(plaintext, iv)

def aes_ige_decrypt(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
"""
Convenience function for AES-IGE decryption.

Args:
ciphertext: The data to decrypt. Length must be a multiple of 16.
key: The AES key (16, 24, or 32 bytes).
iv: The 32-byte Initialization Vector.

Returns:
The decrypted plaintext.
"""
cipher = AES(key)
ige = AESIGE(cipher)
return ige.decrypt(ciphertext, iv)
Loading
Loading