From 3143003dfd3fd9efe1da9fdf3be806e66a826ff4 Mon Sep 17 00:00:00 2001 From: Lain Iwakura Date: Fri, 20 Jun 2025 23:19:45 +0300 Subject: [PATCH] first commit --- README-en.md | 92 +++++++++++++++++++ README.md | 92 +++++++++++++++++++ main.py | 228 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 4 files changed, 414 insertions(+) create mode 100644 README-en.md create mode 100644 README.md create mode 100644 main.py create mode 100644 requirements.txt diff --git a/README-en.md b/README-en.md new file mode 100644 index 0000000..5d7ec9a --- /dev/null +++ b/README-en.md @@ -0,0 +1,92 @@ +# Analysis and Client Implementation for Project Colette V53 + +This document provides a detailed technical breakdown of the connection process to a Project Colette V53 server. It dissects the cryptographic handshake, packet structure, and message exchange logic as implemented in the `main.py` Python client. + +## [*] Core Concepts + +The data exchange protocol is a custom protocol layered on top of standard TCP. The encryption is built using the **NaCl** cryptographic library (referenced in the C# code as `TweetNaCl`), which provides: +- **Asymmetric Encryption**: `Curve25519` for key exchange via Elliptic-curve Diffie–Hellman (ECDH). +- **Symmetric Encryption**: `XSalsa20-Poly1305` for stream encryption after the session is established. +- **Hashing**: `Blake2b` for nonce generation. + +Each message (packet) in the protocol is prefixed with a 7-byte header: +- **Message ID** (2 bytes): The identifier for the message type. +- **Payload Length** (3 bytes): The length of the upcoming payload. +- **Version** (2 bytes): The version of the message. + +## [!] Step-by-Step Breakdown of the Connection Process + +The process from the initial connection to a fully established encrypted session involves the exchange of four key packets. + +--- + +### **Step 1: `ClientHello` (ID: 10100)** + +- **Direction**: Client -> Server +- **Encryption**: None + +This is the very first packet the client sends after establishing a TCP connection. Its sole purpose is to initiate a session on the server and signal readiness to begin the handshake. The payload of this packet is irrelevant to the cryptography and can be filled with zeros. + +--- + +### **Step 2: `LoginMessage` (ID: 10101)** + +- **Direction**: Client -> Server +- **Encryption**: Asymmetric (`Box`) + +This is the key packet from the client, where the core of the key exchange magic happens. + +#### **Client-Side Preparation (Before Sending):** +1. **Server's Key**: The client uses the server's static `server_public_key`, which is known beforehand (extracted from the server's source code). +2. **Client's Keys**: The client generates a new, **ephemeral (temporary)** `Curve25519` key pair—`client_public_key` and `client_private_key`. This pair will only be used for the current session. +3. **Shared Secret (`s`)**: The client immediately computes the pre-master shared secret (`shared_secret` or `s`) using its new private key and the server's public key. This is a standard ECDH operation. In `PyNaCl`, this is done with `Box.beforenm(server_public_key, client_private_key)`. This secret `s` will be needed to decrypt the server's response. +4. **Client Nonce (`RNonce`)**: The client generates 24 random bytes, the `client_nonce`. The server refers to this as the `RNonce` (Receiver's Nonce). It is a critical part of the handshake. + +#### **Packet Structure and Transmission:** +The `10101` packet consists of two parts: +1. **Client's Public Key** (32 bytes): Sent in cleartext. +2. **Encrypted Payload**: + - **Data to Encrypt**: The plaintext to be encrypted contains: + 1. A temporary session key (24 bytes, generated by the client but ignored by the server). + 2. The `client_nonce` (`RNonce`) (24 bytes). + 3. The actual login data (account ID, client version, etc.), serialized into a bytestream. + - **Nonce for Encryption**: The nonce for this operation is calculated as a hash of the public keys: `blake2b(client_public_key + server_public_key)`. + - **Encryption Process**: The data above is encrypted using `Box(client_private_key, server_public_key).encrypt(...)`. + +--- + +### **Step 3: `ServerHello` (ID: 20100)** + +- **Direction**: Server -> Client +- **Encryption**: Asymmetric (`Box`) + +This is the server's response, which completes the asymmetric phase of the handshake. + +#### **Client-Side Decryption Process:** +1. **Nonce Calculation**: To decrypt this packet, the client must use the same `nonce` the server used. The server calculates it with the formula: `blake2b(RNonce + client_public_key + server_public_key)`. The client already has all three components: it generated `RNonce` itself, and it knows both public keys. +2. **Decryption Process**: Decryption is performed using the previously computed shared secret `s`. Formula: `Box.open_afternm(payload, nonce, shared_secret)`. + +#### **Contents of the Decrypted Packet:** +Inside are two key elements for all future communication: +1. **Final Session Key (`session_key`)** (32 bytes): This is the symmetric key that will be used to encrypt all subsequent messages. +2. **Server Nonce (`SNonce`)** (24 bytes): This nonce will be the base for encrypting messages flowing **from the server to the client**. + +--- + +### **Step 4: Transition to Symmetric Encryption & `LoginOk` (ID: 20104)** + +- **Direction**: Server -> Client +- **Encryption**: Symmetric (`SecretBox`) + +After receiving `ServerHello`, the cryptographic session is fully established. All subsequent packets are encrypted symmetrically using `SecretBox` and the `session_key`. + +#### **Nonce Logic (`NextNonce`)** +A key feature of the protocol is that the nonce is incremented **by 2** before every encryption or decryption operation. The `next_nonce` function in the code precisely replicates this server-side logic. + +#### **`LoginOk`** +This is the first packet the client receives that is symmetrically encrypted. +1. **Get Nonce**: The client takes the `SNonce` it received in the previous step. +2. **Increment Nonce**: It applies the `next_nonce` logic to it (increasing it by 2). +3. **Decrypt**: It decrypts the packet using `SecretBox(session_key).decrypt(payload, nonce)`. + +Successfully decrypting `LoginOk` confirms that the entire process has worked correctly, and the client is now in a fully encrypted session with the server. This packet contains the details of the newly created account (ID, token, etc.). \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ea2b11 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# Анализ и реализация клиента для Project Colette V53 + +Этот документ представляет подробный технический разбор процесса подключения к серверу Project Colette V53. Он анализирует криптографическое рукопожатие, структуру пакетов и логику обмена сообщениями, реализованную в Python-клиенте `main.py`. + +## [*] Основные концепции + +Протокол обмена данными — это пользовательский протокол, работающий поверх стандартного TCP. Шифрование построено с использованием криптографической библиотеки **NaCl** (в коде C# упоминается как `TweetNaCl`), которая предоставляет: +- **Асимметричное шифрование**: `Curve25519` для обмена ключами через эллиптическую кривую Диффи-Хеллмана (ECDH). +- **Симметричное шифрование**: `XSalsa20-Poly1305` для потокового шифрования после установления сессии. +- **Хеширование**: `Blake2b` для генерации одноразовых номеров (nonce). + +Каждое сообщение (пакет) в протоколе предваряется 7-байтным заголовком: +- **ID сообщения** (2 байта): Идентификатор типа сообщения. +- **Длина полезной нагрузки** (3 байта): Длина предстоящих данных. +- **Версия** (2 байта): Версия сообщения. + +## [!] Пошаговый разбор процесса подключения + +Процесс от первоначального подключения до полностью установленной зашифрованной сессии включает обмен четырьмя ключевыми пакетами. + +--- + +### **Шаг 1: `ClientHello` (ID: 10100)** + +- **Направление**: Клиент -> Сервер +- **Шифрование**: Нет + +Это самый первый пакет, который клиент отправляет после установления TCP-соединения. Его единственная цель — инициировать сессию на сервере и сообщить о готовности начать рукопожатие. Полезная нагрузка этого пакета не имеет значения для криптографии и может быть заполнена нулями. + +--- + +### **Шаг 2: `LoginMessage` (ID: 10101)** + +- **Направление**: Клиент -> Сервер +- **Шифрование**: Асимметричное (`Box`) + +Это ключевой пакет от клиента, где происходит основная магия обмена ключами. + +#### **Подготовка на стороне клиента (перед отправкой):** +1. **Ключ сервера**: Клиент использует статический `server_public_key` сервера, который известен заранее (извлечен из исходного кода сервера). +2. **Ключи клиента**: Клиент генерирует новую, **эфемерную (временную)** пару ключей `Curve25519` — `client_public_key` и `client_private_key`. Эта пара будет использоваться только для текущей сессии. +3. **Общий секрет (`s`)**: Клиент немедленно вычисляет предварительный общий секрет (`shared_secret` или `s`), используя свой новый приватный ключ и публичный ключ сервера. Это стандартная операция ECDH. В `PyNaCl` это делается с помощью `Box.beforenm(server_public_key, client_private_key)`. Этот секрет `s` понадобится для расшифровки ответа сервера. +4. **Nonce клиента (`RNonce`)**: Клиент генерирует 24 случайных байта, `client_nonce`. Сервер называет это `RNonce` (Nonce получателя). Это критически важная часть рукопожатия. + +#### **Структура и передача пакета:** +Пакет `10101` состоит из двух частей: +1. **Публичный ключ клиента** (32 байта): Отправляется в открытом виде. +2. **Зашифрованная полезная нагрузка**: + - **Данные для шифрования**: Открытый текст для шифрования содержит: + 1. Временный сеансовый ключ (24 байта, генерируется клиентом, но игнорируется сервером). + 2. `client_nonce` (`RNonce`) (24 байта). + 3. Фактические данные для входа (ID аккаунта, версия клиента и т.д.), сериализованные в байтовый поток. + - **Nonce для шифрования**: Nonce для этой операции вычисляется как хеш публичных ключей: `blake2b(client_public_key + server_public_key)`. + - **Процесс шифрования**: Вышеуказанные данные шифруются с помощью `Box(client_private_key, server_public_key).encrypt(...)`. + +--- + +### **Шаг 3: `ServerHello` (ID: 20100)** + +- **Направление**: Сервер -> Клиент +- **Шифрование**: Асимметричное (`Box`) + +Это ответ сервера, который завершает асимметричную фазу рукопожатия. + +#### **Процесс расшифровки на стороне клиента:** +1. **Вычисление Nonce**: Чтобы расшифровать этот пакет, клиент должен использовать тот же `nonce`, который использовал сервер. Сервер вычисляет его по формуле: `blake2b(RNonce + client_public_key + server_public_key)`. У клиента уже есть все три компонента: он сам сгенерировал `RNonce` и знает оба публичных ключа. +2. **Процесс расшифровки**: Расшифровка выполняется с использованием ранее вычисленного общего секрета `s`. Формула: `Box.open_afternm(payload, nonce, shared_secret)`. + +#### **Содержимое расшифрованного пакета:** +Внутри находятся два ключевых элемента для всей будущей коммуникации: +1. **Финальный сеансовый ключ (`session_key`)** (32 байта): Это симметричный ключ, который будет использоваться для шифрования всех последующих сообщений. +2. **Nonce сервера (`SNonce`)** (24 байта): Этот nonce будет основой для шифрования сообщений, идущих **от сервера к клиенту**. + +--- + +### **Шаг 4: Переход к симметричному шифрованию и `LoginOk` (ID: 20104)** + +- **Направление**: Сервер -> Клиент +- **Шифрование**: Симметричное (`SecretBox`) + +После получения `ServerHello` криптографическая сессия считается полностью установленной. Все последующие пакеты шифруются симметрично с использованием `SecretBox` и `session_key`. + +#### **Логика Nonce (`NextNonce`)** +Ключевой особенностью протокола является то, что nonce увеличивается **на 2** перед каждой операцией шифрования или дешифрования. Функция `next_nonce` в коде точно воспроизводит эту логику на стороне сервера. + +#### **`LoginOk`** +Это первый пакет, который клиент получает в зашифрованном виде. +1. **Получить Nonce**: Клиент берет `SNonce`, полученный на предыдущем шаге. +2. **Увеличить Nonce**: Он применяет к нему логику `next_nonce` (увеличивая его на 2). +3. **Расшифровать**: Он расшифровывает пакет с помощью `SecretBox(session_key).decrypt(payload, nonce)`. + +Успешная расшифровка `LoginOk` подтверждает, что весь процесс прошел корректно, и клиент теперь находится в полностью зашифрованной сессии с сервером. Этот пакет содержит данные о созданном аккаунте (ID, токен и т.д.). \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..f1b0119 --- /dev/null +++ b/main.py @@ -0,0 +1,228 @@ +import nacl.utils +import socket +import time +from nacl.public import PrivateKey, Box +from nacl.secret import SecretBox + +# --- Helper Classes --- + +class ByteStream: + """ + Helper class for reading and writing data types to a byte buffer. + Simulates the ByteStream used on the server. + """ + def __init__(self, initial_bytes=b''): + self.buffer = bytearray(initial_bytes) + self.offset = 0 + + def write(self, data): + self.buffer.extend(data) + + def write_byte(self, value): + self.write(bytes([value])) + + def write_int(self, value, length=4): + self.write(value.to_bytes(length, 'big', signed=True)) + + def write_string(self, value): + if value is None: + self.write_int(-1) + else: + encoded_str = value.encode('utf-8') + self.write_int(len(encoded_str)) + self.write(encoded_str) + + def get_bytes(self): + return bytes(self.buffer) + +# --- Crypto Implementation --- + +def next_nonce(nonce): + """ + Replicates the NextNonce logic from the C# server. + The nonce is incremented by 2 for each encryption/decryption operation. + """ + c = 2 + for i in range(len(nonce)): + c += nonce[i] + nonce[i] = c & 0xff + c >>= 8 + return nonce + +# --- Main Client Logic --- + +class Client: + def __init__(self, server_ip, server_port): + self.server_ip = server_ip + self.server_port = server_port + self.socket = None + + # Server's public key is static and known. + # It's derived from the hardcoded private key in the server source. + server_private_key_bytes = bytes([ + 158, 217, 110, 5, 87, 249, 222, 234, 204, 121, 177, 228, 59, 79, 93, 217, + 25, 33, 113, 185, 119, 171, 205, 246, 11, 185, 185, 22, 140, 152, 107, 20 + ]) + server_private_key = PrivateKey(server_private_key_bytes) + self.server_public_key = server_private_key.public_key + + # Client generates a new, temporary key pair for each session. + self.client_private_key = PrivateKey.generate() + self.client_public_key = self.client_private_key.public_key + + # Will be populated after the handshake + self.shared_secret = None + self.session_key = None + self.client_nonce = None # RNonce on server + self.server_nonce = None # SNonce on server + self.encryptor = None + self.decryptor = None + + def connect(self): + """Establishes the TCP connection.""" + print("[*] Connecting to server...") + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect((self.server_ip, self.server_port)) + print("[+] Connected!") + + def send_packet(self, message_id, payload): + """Constructs and sends a packet in the server's expected format.""" + header = message_id.to_bytes(2, 'big') + \ + len(payload).to_bytes(3, 'big') + \ + (0).to_bytes(2, 'big') # Version + self.socket.sendall(header + payload) + print(f"[*] Sent packet {message_id} with payload length {len(payload)}") + + def read_packet(self): + """Reads a packet from the server.""" + header = self.socket.recv(7) + if not header: + return None, None + + message_id = int.from_bytes(header[0:2], 'big') + length = int.from_bytes(header[2:5], 'big') + + payload = self.socket.recv(length) + print(f"[*] Received packet {message_id} with payload length {len(payload)}") + return message_id, payload + + def perform_handshake_and_login(self): + """ + Manages the entire connection, handshake, and account creation process. + """ + # --- 1. ClientHello (10100) --- + # This message is sent unencrypted. It signals the start of the login sequence. + print("\n--- Step 1: Sending ClientHello (10100) ---") + client_hello_payload = b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' # Dummy payload + self.send_packet(10100, client_hello_payload) + + # --- 2. LoginMessage (10101) --- + # This is where the cryptographic handshake truly begins. + print("\n--- Step 2: Preparing and Sending LoginMessage (10101) ---") + + from nacl.hash import blake2b + + # The client computes the shared secret 's' *before* sending the message. + # This will be used to decrypt the server's response. + self.shared_secret = Box.beforenm(self.server_public_key, self.client_private_key) + print(f"[+] Calculated Shared Secret (s): {self.shared_secret.hex()}") + + # The client must generate a nonce (called RNonce on the server) + # and include it inside the encrypted part of the login message. + self.client_nonce = nacl.utils.random(24) + print(f"[+] Generated Client Nonce (RNonce): {self.client_nonce.hex()}") + + # The actual login data. + login_data_bs = ByteStream() + login_data_bs.write_int(0) # Account ID High (0 for new account) + login_data_bs.write_int(0) # Account ID Low (0 for new account) + login_data_bs.write_string(None) # Pass Token + login_data_bs.write_int(53) # Client Major + login_data_bs.write_int(135) # Client Minor + login_data_bs.write_int(0) # Client Build + login_data_bs.write_string("2a1d35a5749747761159b36050b8b6314f849641") # Fingerprint SHA + login_data_bs.write_int(0) # Unknown + login_data_bs.write_string("en-US") # lang + + # The server expects the encrypted payload to start with a temporary session key (which it ignores) + # and our RNonce. + payload_to_encrypt_bs = ByteStream() + payload_to_encrypt_bs.write(nacl.utils.random(24)) # a temporary client key, server discards it + payload_to_encrypt_bs.write(self.client_nonce) + payload_to_encrypt_bs.write(login_data_bs.get_bytes()) + + # The nonce for the Box encryption is a hash of the public keys. + login_nonce = blake2b(self.client_public_key.encode() + self.server_public_key.encode(), encoder=nacl.encoding.RawEncoder)[:24] + + # Encrypt the payload containing RNonce and login data. + encrypted_login_payload = Box(self.client_private_key, self.server_public_key).encrypt(payload_to_encrypt_bs.get_bytes(), login_nonce) + + # The final packet payload is the client's raw public key + the encrypted data. + final_login_payload = self.client_public_key.encode() + encrypted_login_payload + self.send_packet(10101, final_login_payload) + + # --- 3. Receive and Decrypt ServerHello (20100) --- + print("\n--- Step 3: Receiving and Processing ServerHello (20100) ---") + message_id, payload = self.read_packet() + if message_id != 20100: + print(f"[!!!] Error: Expected ServerHello (20100), got {message_id}. Aborting.") + return + + # The server encrypts its response using a nonce derived from OUR RNonce, and the public keys. + server_response_nonce = blake2b(self.client_nonce + self.client_public_key.encode() + self.server_public_key.encode(), encoder=nacl.encoding.RawEncoder)[:24] + + # We decrypt the server's response using our pre-calculated shared secret 's'. + decrypted_payload = Box.open_afternm(payload, server_response_nonce, self.shared_secret) + + # The decrypted payload of ServerHello contains the server's nonce (SNonce) + # and the final symmetric session key. + self.server_nonce = decrypted_payload[0:24] + self.session_key = decrypted_payload[24:56] + + print(f"[+] Decryption successful!") + print(f"[+] Session Key: {self.session_key.hex()}") + print(f"[+] Server Nonce (SNonce): {self.server_nonce.hex()}") + + # Now we can setup the symmetric encryptor/decryptor for the rest of the session. + self.encryptor = SecretBox(self.session_key) + self.decryptor = SecretBox(self.session_key) + + # --- 4. Receive LoginOk (20104) --- + # This packet confirms login and provides account details. It is ENCRYPTED. + print("\n--- Step 4: Receiving and Processing LoginOk (20104) ---") + message_id, payload = self.read_packet() + if message_id != 20104: + print(f"[!!!] Error: Expected LoginOk (20104), got {message_id}. Aborting.") + return + + # The nonce for symmetric encryption is the SNonce, which must be incremented by 2 before use. + self.server_nonce = next_nonce(bytearray(self.server_nonce)) + + try: + decrypted_login_ok = self.decryptor.decrypt(payload, bytes(self.server_nonce)) + print("[+] LoginOk packet decrypted successfully!") + + # Here you would parse the decrypted_login_ok bytestream + # to get your new Account ID, Token, etc. + print("[SUCCESS] Account created and login complete!") + print("You are now in a fully encrypted session with the server.") + + except nacl.exceptions.CryptoError as e: + print(f"[!!!] FAILED to decrypt LoginOk packet: {e}") + + +if __name__ == '__main__': + # IMPORTANT: Replace with the actual server IP and Port + SERVER_IP = "127.0.0.1" + SERVER_PORT = 9339 + + client = Client(SERVER_IP, SERVER_PORT) + try: + client.connect() + client.perform_handshake_and_login() + except Exception as e: + print(f"\n[!!!] An error occurred: {e}") + finally: + if client.socket: + client.socket.close() + print("\n[*] Connection closed.") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..143f459 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyNaCl>=1.5.0 +