Skip to content
Open
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
7 changes: 7 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,10 @@

# Качество сжатия
COMPRESS_QUALITY = 11


# Ограничение на время ожидания шага рукопожатия (в секундах).
HANDSHAKE_TIMEOUT = 60

# Ограничение для шага, который ждёт действия человека (в секундах).
HANDSHAKE_USER_CHECK_TIMEOUT = 900
120 changes: 101 additions & 19 deletions src/crypto_layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
# node id состоит только из шестнадцатеричных символов
NODE_ID_PATTERN = re.compile(r"\A[0-9a-f]+\Z")

# Длина точки кривой SECP256R1 в сжатом формате X9.62: префикс + координата X.
# Обе стороны отправляют ключи только в этом формате, поэтому всё остальное
# отбрасывается ещё до разбора
EC_COMPRESSED_POINT_LENGTH = 33


class CryptoLayer:

Expand Down Expand Up @@ -91,6 +96,12 @@ def __init__(self, ui_provider: UIProvider, data_dir: str, module_class: BaseMod
self.COMPANION_SIGN_RECEIVED = threading.Event()
self.COMPANION_PUBLIC_KEY_RECEIVED = threading.Event()

# Ограничение на время ожидания шагов рукопожатия (в секундах).
self.HANDSHAKE_TIMEOUT = config.HANDSHAKE_TIMEOUT

# Отдельный лимит для шага, который ждёт действия человека
self.HANDSHAKE_USER_CHECK_TIMEOUT = config.HANDSHAKE_USER_CHECK_TIMEOUT

# Уровни
self.TRANSITIONAL_LEVEL = None
self.TRANSPORT_LEVEL = None
Expand All @@ -113,11 +124,23 @@ def init(self):
# Настройка модуля
self.init_module()

# Работа с подписями
self.signatures_setup()
# Рукопожатие может не состояться: собеседник не ответил, прислал данные,
# которые ядро не приняло, или пользователь не подтвердил отпечаток.
# Тогда init() не оставляет после себя работающий стек и полуготовый
# объект: сообщаем в UI, гасим уровни и пробрасываем ошибку вызывающему
try:

# Работа с подписями
self.signatures_setup()

# Ключи шифрования
self.generate_and_exchange_ecc_keys()

# Ключи шифрования
self.generate_and_exchange_ecc_keys()
except Exception as e:
self.LOGGER.error(f"Init: initialization failed: {e}")
self.ui_provider.update_status("CryptoLayer", f"Initialization failed: {e}", "error")
self.abort_init()
raise

# удаление пароля из RAM
self.remove_password_from_ram()
Expand All @@ -128,6 +151,15 @@ def init(self):
self.ui_provider.on_ready()


# Свертываем незавершённую инициализацию.
def abort_init(self):

self.remove_password_from_ram()

Base.stop_event.set()
BaseModule.stop_event.set()


def init_levels(self):

self.ui_provider.update_status("Levels", "Loading...", "in_progress")
Expand Down Expand Up @@ -267,7 +299,7 @@ def node_id_exchange(self):
self.APPLICATION_LEVEL.send_my_node_id(self.NODE_ID)
self.ui_provider.update_status("Signatures", "Waiting for companion node id...", "in_progress")
self.LOGGER.info("Signatures: Waiting for companion node id...")
self.COMPANION_NODE_ID_RECEIVED.wait()
self.wait_handshake_step(self.COMPANION_NODE_ID_RECEIVED, "companion node id")
self.ui_provider.update_status("Signatures", "Companion node id received!", "in_progress")
self.LOGGER.info("Signatures: Companion node id received!")

Expand All @@ -290,7 +322,7 @@ def check_and_exchange_companion_sign(self):
self.APPLICATION_LEVEL.send_my_sign(my_sign_public_bytes_X962)
self.ui_provider.update_status("Signatures", "Waiting for companion signature...", "in_progress")
self.LOGGER.info("Signatures: Waiting for companion signature...")
self.COMPANION_SIGN_RECEIVED.wait()
self.wait_handshake_step(self.COMPANION_SIGN_RECEIVED, "companion signature")
self.ui_provider.update_status("Signatures", "Companion signature received!", "in_progress")
self.LOGGER.info("Signatures: Companion signature received!")

Expand Down Expand Up @@ -384,7 +416,13 @@ def generate_and_exchange_ecc_keys(self):

self.ui_provider.update_status("Encryption", "Waiting for companion public key...", "in_progress")
self.LOGGER.info("Encryption: Waiting for companion public key...")
self.COMPANION_PUBLIC_KEY_RECEIVED.wait()
# Собеседник отправляет ключ только после ручной сверки отпечатка,
# поэтому здесь ждём дольше, чем на остальных шагах
self.wait_handshake_step(
self.COMPANION_PUBLIC_KEY_RECEIVED,
"companion public key",
self.HANDSHAKE_USER_CHECK_TIMEOUT
)

self.ui_provider.update_status("Encryption", "Companion public key received!", "in_progress")
self.LOGGER.info("Encryption: Companion public key received!")
Expand Down Expand Up @@ -437,32 +475,76 @@ def send(self, text):
self.APPLICATION_LEVEL.send_text(text)


def receive_node_id(self, node_id: str):
# Ожидание шага рукопожатия с ограничением по времени. Возвращает управление
# при получении данных, а при таймауте прерывает рукопожатие исключением,
# чтобы поток инициализации не оставался заблокированным навсегда.
# Шаг, ждущий действия человека, передаёт свой лимит явно
def wait_handshake_step(self, event: threading.Event, step_name: str, timeout: float = None):

if timeout is None:
timeout = self.HANDSHAKE_TIMEOUT

if not event.wait(timeout):
self.LOGGER.error(f"Handshake timed out after {timeout}s while waiting for {step_name}")
raise TimeoutError(f"handshake timed out after {timeout}s while waiting for {step_name}")


def receive_node_id(self, node_id: str) -> bool:

# node id собеседника приходит по сети и используется как имя файла
# в known_nodes, поэтому принимается только node id ожидаемого вида
if not self.check_node_id(node_id):
self.LOGGER.error("companion node id is not valid: dropped")
return
return False

self.COMPANION_NODE_ID = node_id
self.COMPANION_NODE_ID_RECEIVED.set()
return True


def receive_sign(self, sign: bytes):
self.COMPANION_SIGN = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
sign
)
def receive_sign(self, sign: bytes) -> bool:

# Подпись приходит по сети как байты точки кривой и может быть
# некорректной. Разбор изолируем: при ошибке возвращаем False, чтобы
# уровень приложения не переключал этап и сохранил возможность принять
# корректную подпись
if len(sign) != EC_COMPRESSED_POINT_LENGTH:
self.LOGGER.error("companion signature has unexpected length: dropped")
return False

try:
self.COMPANION_SIGN = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
sign
)
except (ValueError, TypeError):
self.LOGGER.error("companion signature is not a valid EC point: dropped")
return False

self.COMPANION_SIGN_RECEIVED.set()
return True


def receive_public_key(self, public_key: bytes):
self.COMPANION_PUBLIC_KEY = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
public_key
)
def receive_public_key(self, public_key: bytes) -> bool:

# ECDH-ключ собеседника тоже приходит по сети и может быть некорректным.
# Разбор изолируем и сообщаем результат наверх по той же причине, что и
# для подписи
if len(public_key) != EC_COMPRESSED_POINT_LENGTH:
self.LOGGER.error("companion public key has unexpected length: dropped")
return False

try:
self.COMPANION_PUBLIC_KEY = ec.EllipticCurvePublicKey.from_encoded_point(
ec.SECP256R1(),
public_key
)
except (ValueError, TypeError):
self.LOGGER.error("companion public key is not a valid EC point: dropped")
return False

self.COMPANION_PUBLIC_KEY_RECEIVED.set()
return True


def receive_text(self, timestamp: int, text: str):
Expand Down
22 changes: 19 additions & 3 deletions src/levels/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,28 @@ def handle_packet(self, data):
if not self.check_stage(HandshakeStages.WAIT_NODE_ID, "MY_NODE_ID"):
return

# node id приходит по сети и может быть мусором
try:
node_id = packet.payload.decode()
except UnicodeDecodeError:
self.logger.warning("MY_NODE_ID payload is not valid utf-8: dropped")
return

if not self.UPPER_LEVEL.receive_node_id(node_id):
return

self.HANDSHAKE_STAGE = HandshakeStages.WAIT_SIGN
self.UPPER_LEVEL.receive_node_id(packet.payload.decode())

elif packet.data_type == CMDTypes.MY_SIGN.value:

if not self.check_stage(HandshakeStages.WAIT_SIGN, "MY_SIGN"):
return

# Та же логика, что и для node id: разбор подписи может завершиться ошибкой
if not self.UPPER_LEVEL.receive_sign(packet.payload):
return

self.HANDSHAKE_STAGE = HandshakeStages.SIGN_RECEIVED
self.UPPER_LEVEL.receive_sign(packet.payload)

elif packet.data_type == CMDTypes.MY_PUBLIC_KEY.value:

Expand All @@ -133,8 +145,12 @@ def handle_packet(self, data):
self.logger.info(f"MY_PUBLIC_KEY packet at stage {self.HANDSHAKE_STAGE.name}: dropped")
return

# Аналогично node id и подписи: ECDH-ключ разбирается в ядре и
# может оказаться некорректным
if not self.UPPER_LEVEL.receive_public_key(packet.payload):
return

self.HANDSHAKE_STAGE = HandshakeStages.READY
self.UPPER_LEVEL.receive_public_key(packet.payload)

# Наш ключ собеседник мог отбросить, если получил его до того,
# как включил у себя проверку подписей. Отправляем ещё раз
Expand Down