first commit
This commit is contained in:
commit
3143003dfd
92
README-en.md
Normal file
92
README-en.md
Normal file
@ -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.).
|
92
README.md
Normal file
92
README.md
Normal file
@ -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, токен и т.д.).
|
228
main.py
Normal file
228
main.py
Normal file
@ -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.")
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
@ -0,0 +1,2 @@
|
||||
PyNaCl>=1.5.0
|
||||
|
Loading…
x
Reference in New Issue
Block a user