From 7274aa159cf45abcfa702aff36ca6e7554f93d48 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 15:04:05 +0300 Subject: [PATCH 01/13] Update classifiers --- setup.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 0aba9d7..c6097cb 100644 --- a/setup.py +++ b/setup.py @@ -47,9 +47,10 @@ setup( install_requires=requires, license=about['__license__'], classifiers=[ - "Development Status :: 3 - Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "Natural Language :: Russian", + "Topic :: Software Development :: Libraries", + "Natural Language :: English", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.10", From d937479aa84b9885a9d0f5f4ac625596eb939b7f Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 16:49:54 +0300 Subject: [PATCH 02/13] Add examples with password --- examples/with_crypto/client_password.py | 27 +++++++++++++++++++++++++ examples/with_crypto/server_password.py | 21 +++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 examples/with_crypto/client_password.py create mode 100644 examples/with_crypto/server_password.py diff --git a/examples/with_crypto/client_password.py b/examples/with_crypto/client_password.py new file mode 100644 index 0000000..81faabe --- /dev/null +++ b/examples/with_crypto/client_password.py @@ -0,0 +1,27 @@ +import sys + +from loguru import logger + +from winConnect import WinConnectClient, crypto + +logger.remove() +logger.add(sys.stdout, level="DEBUG") + +crypt_mode = crypto.WinConnectCryptoPassword("test_password") + +connector = WinConnectClient('test') +connector.set_logger(logger) +connector.set_crypto(crypt_mode) + +def console(): + with connector as conn: + while True: + i = input(":> ") + if i == "exit": + break + conn.send_data(i) + data = conn.read_pipe() + print(f"({type(data)}) {data=}") + +if __name__ == '__main__': + console() diff --git a/examples/with_crypto/server_password.py b/examples/with_crypto/server_password.py new file mode 100644 index 0000000..3c155dd --- /dev/null +++ b/examples/with_crypto/server_password.py @@ -0,0 +1,21 @@ +import sys + +from loguru import logger + +from winConnect import WinConnectDaemon +from winConnect import crypto + +logger.remove() +logger.add(sys.stdout, level="DEBUG") + +crypt_mode = crypto.WinConnectCryptoPassword("test_password") + +connector = WinConnectDaemon('test') +connector.set_logger(logger) +connector.set_crypto(crypt_mode) + +for data in connector.listen(): + print(f"({type(data)}) {data=}") + if data is None and connector.closed: + break + connector.send_data(data) From d8a4d1682cc9c92713b3ecb033542934b147d901 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 17:02:41 +0300 Subject: [PATCH 03/13] [!] Update protocol: v3 (with crypto+salt) [~] WinConnectCryptoPassword [+] WinConnectCryptoBase.salt (+setter) [~] Optimize WinConnectCryptoSimple.encrypt [+] pycryptodome --- requirements.txt | 1 + setup.py | 17 +++++--- winConnect/WinConnectBase.py | 40 +++++++++++------- winConnect/crypto/WinConnectCrypto.py | 20 ++++++--- winConnect/crypto/__init__.py | 4 +- winConnect/crypto/crypto_class_base.py | 6 +++ winConnect/crypto/crypto_classes.py | 56 ++++++++++++++++++++++---- winConnect/errors.py | 2 +- 8 files changed, 110 insertions(+), 36 deletions(-) diff --git a/requirements.txt b/requirements.txt index b085198..c305067 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ # 12.03.2025 pywin32~=309 ormsgpack~=1.8.0 +pycryptodome~=3.21.0 \ No newline at end of file diff --git a/setup.py b/setup.py index c6097cb..0954f54 100644 --- a/setup.py +++ b/setup.py @@ -13,10 +13,17 @@ packages = [_name, ] package_dir = {_name: _name} lib_path = here / _name -requires = [ - "pywin32==309", - "ormsgpack==1.8.0" -] +requires = { + "install_requires": [ + "pywin32==309", + "ormsgpack==1.8.0" + ], + "extra_packages": { + "crypto": [ + "pycryptodome==3.21.0" + ] + } +} # 'setup.py publish' shortcut. if sys.argv[-1] == 'publish': @@ -44,7 +51,7 @@ setup( package_data={'': ['LICENSE']}, package_dir=package_dir, include_package_data=True, - install_requires=requires, + **requires, license=about['__license__'], classifiers=[ "Development Status :: 5 - Production/Stable", diff --git a/winConnect/WinConnectBase.py b/winConnect/WinConnectBase.py index a6a7fb6..0f864f0 100644 --- a/winConnect/WinConnectBase.py +++ b/winConnect/WinConnectBase.py @@ -25,7 +25,7 @@ class WinConnectBase: default_encoding = 'utf-8' - read_max_buffer = SimpleConvertor.to_gb(3)-1 # Max size via chunked messages + read_max_buffer = SimpleConvertor.to_gb(3)-32 # Max size via chunked messages ormsgpack_options = ormsgpack.OPT_NON_STR_KEYS | ormsgpack.OPT_NAIVE_UTC | ormsgpack.OPT_PASSTHROUGH_TUPLE # ormsgpack options @@ -33,8 +33,9 @@ class WinConnectBase: self._log = logging.getLogger(f"WinConnect:{pipe_name}") # _version: # 1 - 0.9.1 - # 2 - 0.9.2+ (with crypto) - self._version = 2 + # 2 - 0.9.2 (with crypto) + # 3 - 0.9.3+ (with crypto+salt) + self._version = 3 self._pipe_name = r'\\.\pipe\{}'.format(pipe_name) self._pipe = None self._opened = False @@ -191,35 +192,46 @@ class WinConnectBase: return self._send_error(WinConnectErrors.UNKNOWN_ACTION, f"Unknown action '{action}'") def _parse_command(self, data: bytes): + _blank_settings = { + 'version': self._version, + 'encoding': self.default_encoding, + 'header_size': self._header_size, + 'header_format': self._header_format, + 'max_buffer': self.read_max_buffer, + 'crypto': self.__crypto.crypt_name, + 'salt': self.__crypto.crypt_salt + } command, data = self.__parse_message(data) match command: case b'get_session_settings': self._log.debug(f"[{self._pipe_name}] Received get_session_settings from {data}") - settings = { - 'version': self._version, - 'encoding': self.default_encoding, - 'header_size': self._header_size, - 'header_format': self._header_format, - 'max_buffer': self.read_max_buffer, - "crypto": self.__crypto.get_info() - } - session_settings = f"set_session_settings:{json.dumps(settings)}".encode(self.init_encoding) + session_settings = f"set_session_settings:{json.dumps(_blank_settings)}".encode(self.encoding) self._send_message("command", session_settings) return True case b'set_session_settings': + self._log.debug(f"[{self._pipe_name}] Received session settings.") try: settings = json.loads(data.decode(self.init_encoding)) except json.JSONDecodeError as e: self._send_error(WinConnectErrors.BAD_DATA, f"JSONDecodeError: {e}") return self.close() - if settings.get('version') != self._version: + + if _blank_settings.keys() != settings.keys(): + self._log.error(f"{WinConnectErrors.BAD_SETTINGS}") + self._send_error(WinConnectErrors.BAD_SETTINGS, f"Setting have wrong structure") + return self.close() + + if settings['version'] != self._version: self._log.error(f"{WinConnectErrors.BAD_VERSION}") self._send_error(WinConnectErrors.BAD_VERSION, f"Version mismatch") return self.close() - if settings.get('crypto') != self.__crypto.get_info(): + if settings['crypto'] != self.__crypto.crypt_name: self._log.error(f"{WinConnectErrors.BAD_CRYPTO}") self._send_error(WinConnectErrors.BAD_CRYPTO, f"Crypto mismatch") return self.close() + if settings['salt'] != self.__crypto.crypt_salt: + self._log.debug(f"[{self._pipe_name}] Updating salt") + self.__crypto.set_salt(settings['salt']) self._session_encoding = settings.get('encoding', self.default_encoding) self._header_size = settings.get('header_size', self._header_size) self._header_format = settings.get('header_format', self._header_format) diff --git a/winConnect/crypto/WinConnectCrypto.py b/winConnect/crypto/WinConnectCrypto.py index 03a33f9..652038c 100644 --- a/winConnect/crypto/WinConnectCrypto.py +++ b/winConnect/crypto/WinConnectCrypto.py @@ -15,15 +15,26 @@ def test_crypto_class(crypto_class: "WinConnectCryptoBase"): class WinConnectCrypto: def __init__(self): - self.__crypto_class = None + self.__crypto_class: WinConnectCryptoBase = None self.__log_prefix = None self._log = logging.getLogger("WinConnectCrypto") + @property + def crypt_name(self) -> str: + return self.__crypto_class.__class__.__name__ + + @property + def crypt_salt(self) -> bytes: + return self.__crypto_class.salt + + def set_salt(self, salt: bytes): + self.__crypto_class.salt = salt + def set_crypto_class(self, crypto_class: "WinConnectCryptoBase"): - self._log.debug(f"{self.__log_prefix}Updating crypto class. {self.get_info()} -> {crypto_class.__class__.__name__}") + self._log.debug(f"{self.__log_prefix}Updating crypto class. {self.crypt_name} -> {crypto_class.__class__.__name__}") test_crypto_class(crypto_class) self.__crypto_class = crypto_class - self.__log_prefix = f"[{self.get_info()}] " + self.__log_prefix = f"[{self.crypt_name}] " self._log.debug(f"{self.__log_prefix}Crypto class updated.") def set_logger(self, logger): @@ -38,9 +49,6 @@ class WinConnectCrypto: self._log.debug(f"{self.__log_prefix}Crypto class loaded") return True - def get_info(self): - return self.__crypto_class.__class__.__name__ - def encrypt(self, data: bytes) -> bytes: return self.__crypto_class.encrypt(data) diff --git a/winConnect/crypto/__init__.py b/winConnect/crypto/__init__.py index 079eca7..1c327c8 100644 --- a/winConnect/crypto/__init__.py +++ b/winConnect/crypto/__init__.py @@ -3,6 +3,6 @@ from .crypto_classes import ( WinConnectCryptoBase, WinConnectCryptoNone, WinConnectCryptoSimple, - # WinConnectCryptoPassword, - # WinConnectCryptoCert + WinConnectCryptoPassword, + WinConnectCryptoCert ) diff --git a/winConnect/crypto/crypto_class_base.py b/winConnect/crypto/crypto_class_base.py index baab3d1..044101f 100644 --- a/winConnect/crypto/crypto_class_base.py +++ b/winConnect/crypto/crypto_class_base.py @@ -1,5 +1,11 @@ class WinConnectCryptoBase: + @property + def salt(self) -> bytes: + return b"" + @salt.setter + def salt(self, value): ... + def encrypt(self, data: bytes) -> bytes: ... def decrypt(self, data: bytes) -> bytes: ... diff --git a/winConnect/crypto/crypto_classes.py b/winConnect/crypto/crypto_classes.py index 1579b55..3a962a7 100644 --- a/winConnect/crypto/crypto_classes.py +++ b/winConnect/crypto/crypto_classes.py @@ -1,8 +1,18 @@ +import os import random +from pathlib import Path from .crypto_class_base import WinConnectCryptoBase from winConnect.exceptions import WinConnectCryptoSimpleBadHeaderException +_pip_crypto = True +try: + from Crypto.Cipher import AES + from Crypto.Protocol.KDF import PBKDF2 + from Crypto.Cipher import PKCS1_OAEP +except ImportError: + _pip_crypto = False + class WinConnectCryptoNone(WinConnectCryptoBase): def __init__(self): ... @@ -19,12 +29,10 @@ class WinConnectCryptoSimple(WinConnectCryptoBase): shift_key = random.randint(100, 749) key = random.randint(5, 250) encrypted_text = bytearray() - header = f"wccs{shift_key}{key+shift_key}:" - for char in header: - encrypted_text.append(ord(char)) + header = f"wccs{shift_key}{key+shift_key}:".encode() for char in data: encrypted_text.append(char ^ key) - return bytes(encrypted_text) + return header + bytes(encrypted_text) def decrypt(self, data: bytes) -> bytes: try: @@ -44,17 +52,49 @@ class WinConnectCryptoSimple(WinConnectCryptoBase): class WinConnectCryptoPassword(WinConnectCryptoBase): def __init__(self, password: str): - pass + if not _pip_crypto: + raise ImportError("Crypto library not installed. Install with 'pip install winConnect[crypto]'") + self.password = password + self.__salt = os.urandom(16) + self.__key = PBKDF2(password, self.__salt, dkLen=32, count=100000) + + @property + def salt(self): + return self.__salt + + @salt.setter + def salt(self, value): + self.__salt = value + self.__key = PBKDF2(self.password, self.__salt, dkLen=32, count=100000) def encrypt(self, data: bytes) -> bytes: - pass + iv = os.urandom(16) # Генерируем IV + cipher = AES.new(self.__key, AES.MODE_CBC, iv) + pad_len = 16 - len(data) % 16 + data += chr(pad_len) * pad_len + + header = f"wccp{iv.hex()}:".encode() + + return header + cipher.encrypt(data) # Шифруем def decrypt(self, data: bytes) -> bytes: - pass + try: + header, iv, content = data[:4], data[4:20], data[20:] + if header[:4] != b"wccp": + raise WinConnectCryptoSimpleBadHeaderException("Bad header in message.") + except ValueError: + raise WinConnectCryptoSimpleBadHeaderException("No header in message.") + + cipher = AES.new(self.__key, AES.MODE_CBC, iv) + decrypted = cipher.decrypt(data) + pad_len = decrypted[-1] # Убираем PKCS7 padding + return decrypted[:-pad_len] class WinConnectCryptoCert(WinConnectCryptoBase): def __init__(self, cert_file: str): - pass + if not _pip_crypto: + raise ImportError("Crypto library not installed. Install with 'pip install winConnect[crypto]'") + self.cert_file = Path(cert_file) def _open_cert(self): pass diff --git a/winConnect/errors.py b/winConnect/errors.py index 377a194..a11580f 100644 --- a/winConnect/errors.py +++ b/winConnect/errors.py @@ -14,7 +14,7 @@ class WinConnectErrors(Enum): BAD_DATA = 50 BAD_VERSION = 51 BAD_HEADER = 52 - BAD_BODY = 53 + BAD_SETTINGS = 53 BAD_CRYPTO = 54 BODY_TOO_BIG = 60 From b473fe3fa310f1705d4ecf9ec02ac37c610bd3e0 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 17:57:58 +0300 Subject: [PATCH 04/13] [!+] WinConnectCryptoPassword [~] Change init_sess algo (+salt) --- winConnect/WinConnectBase.py | 34 +++++++++++++++++++---------- winConnect/crypto/crypto_classes.py | 14 +++++------- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/winConnect/WinConnectBase.py b/winConnect/WinConnectBase.py index 0f864f0..4383d3a 100644 --- a/winConnect/WinConnectBase.py +++ b/winConnect/WinConnectBase.py @@ -31,7 +31,7 @@ class WinConnectBase: def __init__(self, pipe_name: str): self._log = logging.getLogger(f"WinConnect:{pipe_name}") - # _version: + # versions: # 1 - 0.9.1 # 2 - 0.9.2 (with crypto) # 3 - 0.9.3+ (with crypto+salt) @@ -193,23 +193,36 @@ class WinConnectBase: def _parse_command(self, data: bytes): _blank_settings = { - 'version': self._version, - 'encoding': self.default_encoding, - 'header_size': self._header_size, - 'header_format': self._header_format, - 'max_buffer': self.read_max_buffer, - 'crypto': self.__crypto.crypt_name, - 'salt': self.__crypto.crypt_salt + 'version': None, + 'encoding': None, + 'header_size': None, + 'header_format': None, + 'max_buffer': None, + 'crypto': None } command, data = self.__parse_message(data) match command: case b'get_session_settings': self._log.debug(f"[{self._pipe_name}] Received get_session_settings from {data}") - session_settings = f"set_session_settings:{json.dumps(_blank_settings)}".encode(self.encoding) + _blank_settings['version'] = self._version + _blank_settings['encoding'] = self._session_encoding + _blank_settings['header_size'] = self._header_size + _blank_settings['header_format'] = self._header_format + _blank_settings['max_buffer'] = self.read_max_buffer + _blank_settings['crypto'] = self.__crypto.crypt_name + session_settings = f"set_session_settings:{len(self.__crypto.crypt_salt)}:{json.dumps(_blank_settings)}".encode(self.encoding) + self.__crypto.crypt_salt self._send_message("command", session_settings) return True case b'set_session_settings': self._log.debug(f"[{self._pipe_name}] Received session settings.") + len_salt, data = self.__parse_message(data) + len_salt = int(len_salt) + data, salt = data[:-len_salt], data[-len_salt:] + + if salt != self.__crypto.crypt_salt: + self._log.debug(f"[{self._pipe_name}] Updating salt") + self.__crypto.set_salt(salt) + try: settings = json.loads(data.decode(self.init_encoding)) except json.JSONDecodeError as e: @@ -229,9 +242,6 @@ class WinConnectBase: self._log.error(f"{WinConnectErrors.BAD_CRYPTO}") self._send_error(WinConnectErrors.BAD_CRYPTO, f"Crypto mismatch") return self.close() - if settings['salt'] != self.__crypto.crypt_salt: - self._log.debug(f"[{self._pipe_name}] Updating salt") - self.__crypto.set_salt(settings['salt']) self._session_encoding = settings.get('encoding', self.default_encoding) self._header_size = settings.get('header_size', self._header_size) self._header_format = settings.get('header_format', self._header_format) diff --git a/winConnect/crypto/crypto_classes.py b/winConnect/crypto/crypto_classes.py index 3a962a7..54d9b73 100644 --- a/winConnect/crypto/crypto_classes.py +++ b/winConnect/crypto/crypto_classes.py @@ -38,7 +38,7 @@ class WinConnectCryptoSimple(WinConnectCryptoBase): try: header, content = data.split(b":", 1) if header[:4] != b"wccs": - raise WinConnectCryptoSimpleBadHeaderException("Bad header in message.") + raise WinConnectCryptoSimpleBadHeaderException(f"Bad header in message: {header[:4].decode()}") except ValueError: raise WinConnectCryptoSimpleBadHeaderException("No header in message.") shift_key = int(header[4:7]) @@ -69,24 +69,22 @@ class WinConnectCryptoPassword(WinConnectCryptoBase): def encrypt(self, data: bytes) -> bytes: iv = os.urandom(16) # Генерируем IV + header = b"wccp" + iv cipher = AES.new(self.__key, AES.MODE_CBC, iv) pad_len = 16 - len(data) % 16 - data += chr(pad_len) * pad_len - - header = f"wccp{iv.hex()}:".encode() - - return header + cipher.encrypt(data) # Шифруем + padded_data = data + bytes([pad_len] * pad_len) + return header + cipher.encrypt(padded_data) # Шифруем def decrypt(self, data: bytes) -> bytes: try: header, iv, content = data[:4], data[4:20], data[20:] if header[:4] != b"wccp": - raise WinConnectCryptoSimpleBadHeaderException("Bad header in message.") + raise WinConnectCryptoSimpleBadHeaderException(f"Bad header in message: {header.decode()}") except ValueError: raise WinConnectCryptoSimpleBadHeaderException("No header in message.") cipher = AES.new(self.__key, AES.MODE_CBC, iv) - decrypted = cipher.decrypt(data) + decrypted = cipher.decrypt(content) pad_len = decrypted[-1] # Убираем PKCS7 padding return decrypted[:-pad_len] From c0e6c9cc63eba8bd5f693bd41bbf9734723103e0 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 17:58:34 +0300 Subject: [PATCH 05/13] Bump version 0.9.3 --- winConnect/__meta__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/winConnect/__meta__.py b/winConnect/__meta__.py index 4cc7390..28c6f26 100644 --- a/winConnect/__meta__.py +++ b/winConnect/__meta__.py @@ -3,8 +3,8 @@ __title__ = 'winConnect' __description__ = 'Communicate Client-Server via Windows NamedPipe.' __url__ = 'https://github.com/SantaSpeen/winConnect' -__version__ = '0.9.2' -__build__ = 39 +__version__ = '0.9.3' +__build__ = 44 __author__ = 'SantaSpeen' __author_email__ = 'admin@anidev.ru' __license__ = "MIT" From 91f882b4944124fba125b489f96ab2ae398e612d Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 17:59:16 +0300 Subject: [PATCH 06/13] Update TODOs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 18b4ce4..ca56cfd 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Communicate Client-Server via Windows NamedPipe - [x] Add logging (0.9.1) - [ ] Send data in chunks (if data is too large) (0.9.3) - [x] Add support for encryption (0.9.2) - - [x] simple (via char xor'ing; auto-pairing) - - [ ] password (via AES and PBKDF2) + - [x] simple (via char xor'ing; auto-pairing) (0.9.2) + - [x] password (via AES and PBKDF2) (0.9.3) - [ ] certificate (via RSA) - [ ] Add support for multiple clients From 4a320155febfae365f5e27c3e48a77cf1075c367 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 18:25:40 +0300 Subject: [PATCH 07/13] Remove header setting) --- examples/server_echo.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/examples/server_echo.py b/examples/server_echo.py index 48cbbe7..146a36f 100644 --- a/examples/server_echo.py +++ b/examples/server_echo.py @@ -1,10 +1,6 @@ from winConnect import WinConnectDaemon connector = WinConnectDaemon('test') -# Set header settings -# see: https://docs.python.org/3.13/library/struct.html#format-characters -# Default: ">H" - Big-endian unsigned short integer (header_size: 2 bytes, max_size: 65535) -connector.set_header_settings(">H") for data in connector.listen(): print(f"({type(data)}) {data=}") From 7e12d64c05604d283ce6c181896cbf8e230b6f90 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:36:38 +0300 Subject: [PATCH 08/13] Chunk needs fixing --- winConnect/WinConnectBase.py | 174 ++++++++++++++++++++++----------- winConnect/WinConnectClient.py | 4 +- 2 files changed, 121 insertions(+), 57 deletions(-) diff --git a/winConnect/WinConnectBase.py b/winConnect/WinConnectBase.py index 4383d3a..87297a5 100644 --- a/winConnect/WinConnectBase.py +++ b/winConnect/WinConnectBase.py @@ -1,3 +1,4 @@ +import hashlib import json import logging import struct @@ -51,9 +52,9 @@ class WinConnectBase: self.__crypto = WinConnectCrypto() self.__crypto.set_crypto_class(WinConnectCryptoNone()) - # self._chunks = [] - - self._lock = threading.Lock() + self._pipe_lock = threading.Lock() + self._read_lock = threading.Lock() + self._write_lock = threading.Lock() def set_crypto(self, crypto): if self._connected: @@ -68,9 +69,13 @@ class WinConnectBase: self.__crypto.set_logger(logger) def _calc_body_max_size(self): - # Max size of body: 2 ** (8 * header_size) - 1 - header_size - 1 + # Max size of body: struct_range - header_size - crypt_fix - action_and_data # - header_size; X byte for header_size - self._body_max_size = SimpleConvertor.struct_range(self._header_format)[1] - self._header_size + # - crypt_fix; 32 byte for crypto fix (internal data) + # - action_and_data; 8 byte for action and data (internal data) act:typ: = 8 byte + self._body_max_size = SimpleConvertor.struct_range(self._header_format)[1] - self._header_size - 32 - 8 + if self._body_max_size-64 < 0: + raise exceptions.WinConnectBaseException("Header size is too small") def set_header_settings(self, fmt): if self._connected: @@ -102,91 +107,147 @@ class WinConnectBase: def closed(self): return not self._connected - def _open_pipe(self): ... - - def __pack_data(self, action, data) -> (bytes, bytes): - data_type = "msg" - data = ormsgpack.packb(data, option=self.ormsgpack_options) - compressed_data = zlib.compress(data) - return data_type.encode(self.encoding) + b":" + action + b":" + compressed_data - - def __unpack_data(self, data: bytes) -> (str, Any): - data_type, action_data = self.__parse_message(data) - if data_type != b"msg": - self._send_error(WinConnectErrors.UNKNOWN_DATA_TYPE, f"Unknown data type '{data_type}'") - raise exceptions.WinConnectBadDataTypeException('Is client using correct lib? Unknown data type') - action, data = self.__parse_message(action_data) - decompressed_data = zlib.decompress(data) - deserialized_data = ormsgpack.unpackb(decompressed_data) - return action, deserialized_data - @staticmethod def __parse_message(message: bytes): return message.split(b":", 1) - def _read_message(self) -> (str, Any): - with self._lock: - _hfmt, _hsize = self.__header_settings + def _open_pipe(self): ... + + def __handle_send_data(self, action, data) -> bytes: + t = type(data) + if t == bytes or t == bytearray: + data_type = b"raw" + ready_data = bytes(data) + else: + data_type = b"msg" + ready_data = ormsgpack.packb(data, option=self.ormsgpack_options) + return data_type + b":" + action + b":" + zlib.compress(ready_data) + + def __handle_receive_data_type(self, data): + data_type, action_data = self.__parse_message(data) + action, data = self.__parse_message(action_data) + data = zlib.decompress(data) + match data_type: + case b"raw": + ready_data = data + case b"msg": + ready_data = ormsgpack.unpackb(data) + case _: + self._send_error(WinConnectErrors.UNKNOWN_DATA_TYPE, f"Unknown data type '{data_type}'") + raise exceptions.WinConnectBadDataTypeException('Is client using correct lib? Unknown data type') + return action, ready_data + + def __raw_read(self, size): + with self._pipe_lock: try: - _, header = win32file.ReadFile(self._pipe, self._header_size) + _, data = win32file.ReadFile(self._pipe, size) + return data except pywintypes.error as e: if e.winerror == 109: exc = exceptions.WinConnectConnectionClosedException("Connection closed") exc.real_exc = e raise exc raise e + + def __read_and_decrypt(self, size): + data = self.__raw_read(size) + if self._inited: + data = self.__crypto.decrypt(data) + return data + + def _read_message(self) -> (str, Any): + with self._read_lock: + _hfmt, _hsize = self.__header_settings + # Read header + header = self.__raw_read(_hsize) if not header: - return b"" + self._send_error(WinConnectErrors.BAD_HEADER, f"No header received") + self.close() if len(header) != _hsize and self._inited: self._send_error(WinConnectErrors.BAD_HEADER, f"Bad header size. Expected: {_hsize}, got: {len(header)}") self.close() message_size = struct.unpack(_hfmt, header)[0] - if message_size > self._body_max_size or message_size > self.read_max_buffer: + if message_size > self._body_max_size: self._send_error(WinConnectErrors.BODY_TOO_BIG, f"Body is too big. Max size: {self._body_max_size}kb") self.close() if not self._connected: return None, None - _, data = win32file.ReadFile(self._pipe, message_size) - if self._inited: - data = self.__crypto.decrypt(data) - action, data = self.__unpack_data(data) + # Read body + data = self.__read_and_decrypt(message_size) + action, data = self.__handle_receive_data_type(data) self._log.debug(f"[{self._pipe_name}] Received message: {action=} {data=}") return action, data - def _send_message(self, action: str, data: Any): - action = action.encode(self.encoding) - with self._lock: + def __raw_write(self, packet): + with self._pipe_lock: if self.closed: raise exceptions.WinConnectSessionClosedException("Session is closed") - packed_data = self.__pack_data(action, data) + win32file.WriteFile(self._pipe, packet) + + def _send_message(self, action: str, data: Any): + with self._write_lock: + action = action.encode(self.encoding) + packed_data = self.__handle_send_data(action, data) if self._inited: packed_data = self.__crypto.encrypt(packed_data) + message_size = len(packed_data) if message_size > self._body_max_size: - raise ValueError('Message is too big') - # Если размер сообщения больше размера read_header_size, то ошибка - if message_size > 2 ** (8 * self._header_size): - raise ValueError('Message is too big') - _hfmt, _ = self.__header_settings - header = struct.pack(_hfmt, message_size) - packet = header + packed_data - self._log.debug(f"[{self._pipe_name}] Sending message: {action=} {data=}; {packet=}") - win32file.WriteFile(self._pipe, packet) + raise exceptions.WinConnectBaseException('Message is too big') + + self._log.debug(f"[{self._pipe_name}] Sending message: {action=} {data=}; {message_size} {packed_data=}") + # Send header + self.__raw_write(struct.pack(self.__header_settings[0], message_size)) + # Send body + self.__raw_write(packed_data) def _send_error(self, error: WinConnectErrors, error_message: str = None): e = {"error": True, "code": error.value, "message": error.name, "description": error_message} - self._send_message("error", e) + self._send_message("err", e) + + def __read_chunked_message(self, data_info: bytes): + self._log.debug(f"[{self._pipe_name}] Receive long message. Reading in chunks...") + chunk_size = self._body_max_size - 32 + sha256, data_len = data_info[:32], int(data_info[32:]) + if data_len > self.read_max_buffer: + self._send_error(WinConnectErrors.BODY_TOO_BIG, f"Body is too big. Max size: {self.read_max_buffer}kb") + self.close() + _buffer = b"" + + with self._read_lock: + for i in range(0, data_len, chunk_size): + _buffer += self.__read_and_decrypt(chunk_size) + + return _buffer + + def __send_chunked_message(self, data: bytes): + self._log.debug(f"[{self._pipe_name}] Long message. Sending in chunks...") + chunk_size = self._body_max_size - 32 + cdata = zlib.compress(data) + + cdata_len = len(cdata) + if cdata_len > self.read_max_buffer: + raise exceptions.WinConnectBaseException(f'Message is too big. Change WinConnectBase.read_max_buffer. Now is: {self.read_max_buffer/1024}kb') + cdata_sha256 = hashlib.sha256(cdata).digest() + self._send_message("dtc", cdata_sha256 + str(cdata_len).encode(self.encoding)) + + with self._write_lock: + for i in range(0, cdata_len, chunk_size): + _encrypted = self.__crypto.encrypt(cdata[i:i + chunk_size]) + self.__raw_write(_encrypted) def _parse_action(self, action, data: Any) -> (bool, Any): - # return: (internal_command, data) + # return: (internal_action, data) if not self._connected: return match action: - case b"command": + case b"cmd": # Command return True, self._parse_command(data) - case b"data": + case b"dtn": # Data normal return False, data - case b"error": + case b"dtc": # Data chunked + return False, self.__read_chunked_message(data) + case b"err": return False, WinConnectError(data['code'], data['message']) case _: return self._send_error(WinConnectErrors.UNKNOWN_ACTION, f"Unknown action '{action}'") @@ -211,7 +272,7 @@ class WinConnectBase: _blank_settings['max_buffer'] = self.read_max_buffer _blank_settings['crypto'] = self.__crypto.crypt_name session_settings = f"set_session_settings:{len(self.__crypto.crypt_salt)}:{json.dumps(_blank_settings)}".encode(self.encoding) + self.__crypto.crypt_salt - self._send_message("command", session_settings) + self._send_message("cmd", session_settings) return True case b'set_session_settings': self._log.debug(f"[{self._pipe_name}] Received session settings.") @@ -250,6 +311,7 @@ class WinConnectBase: case b"session_ready": self._inited = True return True + case b"close": self.close() return True @@ -260,15 +322,17 @@ class WinConnectBase: action, data = self._read_message() if not self._connected: return - if action != b"command": + if action != b"cmd": return self._send_error(WinConnectErrors.BAD_DATA, "Unknown data type") if not self._parse_command(data): return self._send_error(WinConnectErrors.INIT_FIRST, "Server need to init session first") - self._send_message("command", b"session_ready:") + self._send_message("cmd", b"session_ready:") self._parse_action(*self._read_message()) def send_data(self, data): - self._send_message("data", data) + if len(data) > self._body_max_size: + return self.__send_chunked_message(data) + self._send_message("dtn", data) def _close_session(self): ... diff --git a/winConnect/WinConnectClient.py b/winConnect/WinConnectClient.py index 6a41eb8..173f56e 100644 --- a/winConnect/WinConnectClient.py +++ b/winConnect/WinConnectClient.py @@ -39,13 +39,13 @@ class WinConnectClient(WinConnectBase): raise e def _init(self, program_name="NoName"): - self._send_message("command", b"get_session_settings:" + program_name.encode(self.encoding)) + self._send_message("cmd", b"get_session_settings:" + program_name.encode(self.encoding)) self._init_session() def _close_session(self): """Send close command to server""" if not self.closed: - self._send_message("command", b"close:") + self._send_message("cmd", b"close:") def __check_pipe(self): if not self._opened: From ac6ed4aea370855e040c8f95ae26aac4b5916b09 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:50:12 +0300 Subject: [PATCH 09/13] [!] Fix salt.. [+] Chunked ready [+] sha256 check --- winConnect/WinConnectBase.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/winConnect/WinConnectBase.py b/winConnect/WinConnectBase.py index 87297a5..e3f1614 100644 --- a/winConnect/WinConnectBase.py +++ b/winConnect/WinConnectBase.py @@ -208,17 +208,20 @@ class WinConnectBase: def __read_chunked_message(self, data_info: bytes): self._log.debug(f"[{self._pipe_name}] Receive long message. Reading in chunks...") chunk_size = self._body_max_size - 32 - sha256, data_len = data_info[:32], int(data_info[32:]) - if data_len > self.read_max_buffer: + cdata_sha256, cdata_len = data_info[:32], int(data_info[32:]) + if cdata_len > self.read_max_buffer: self._send_error(WinConnectErrors.BODY_TOO_BIG, f"Body is too big. Max size: {self.read_max_buffer}kb") self.close() _buffer = b"" with self._read_lock: - for i in range(0, data_len, chunk_size): + for i in range(0, cdata_len, chunk_size): _buffer += self.__read_and_decrypt(chunk_size) - return _buffer + if cdata_sha256 != hashlib.sha256(_buffer).digest(): + self._send_error(WinConnectErrors.BAD_DATA, f"Data is corrupted") + + return zlib.decompress(_buffer) def __send_chunked_message(self, data: bytes): self._log.debug(f"[{self._pipe_name}] Long message. Sending in chunks...") @@ -276,9 +279,12 @@ class WinConnectBase: return True case b'set_session_settings': self._log.debug(f"[{self._pipe_name}] Received session settings.") - len_salt, data = self.__parse_message(data) + len_salt, data_salt = self.__parse_message(data) len_salt = int(len_salt) - data, salt = data[:-len_salt], data[-len_salt:] + if len_salt > 0: + data, salt = data_salt[:-len_salt], data_salt[-len_salt:] + else: + data, salt = data_salt, b'' if salt != self.__crypto.crypt_salt: self._log.debug(f"[{self._pipe_name}] Updating salt") From 9545c6f4c37fa7f1c0ef74d5d543850563ebfacb Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:50:37 +0300 Subject: [PATCH 10/13] Add example for LOOOOOOOONG data (chunked) --- examples/client_long_data.py | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 examples/client_long_data.py diff --git a/examples/client_long_data.py b/examples/client_long_data.py new file mode 100644 index 0000000..84d8fc5 --- /dev/null +++ b/examples/client_long_data.py @@ -0,0 +1,10 @@ +from winConnect import WinConnectClient + +connector = WinConnectClient('test') + +i = b'i' * 1024 * 1024 +with connector as conn: + print(f"Sending {len(i)/1024}kb...") + conn.send_data(i) + data = conn.read_pipe() + print(f"({type(data)}) {data[:9]=}; ok={data == i}") From a66896a4a198dabce1f967a7b6a5e234b0287a07 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:51:41 +0300 Subject: [PATCH 11/13] Bump build --- winConnect/__meta__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/winConnect/__meta__.py b/winConnect/__meta__.py index 28c6f26..66470fd 100644 --- a/winConnect/__meta__.py +++ b/winConnect/__meta__.py @@ -4,7 +4,7 @@ __title__ = 'winConnect' __description__ = 'Communicate Client-Server via Windows NamedPipe.' __url__ = 'https://github.com/SantaSpeen/winConnect' __version__ = '0.9.3' -__build__ = 44 +__build__ = 82 __author__ = 'SantaSpeen' __author_email__ = 'admin@anidev.ru' __license__ = "MIT" From 68724650d27ec259239530487c8ee26a0fc32259 Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:52:10 +0300 Subject: [PATCH 12/13] Update TODOs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ca56cfd..bf62e99 100644 --- a/README.md +++ b/README.md @@ -7,11 +7,11 @@ Communicate Client-Server via Windows NamedPipe - [x] Add support for safe closing (0.9.0) - [x] Add support for other header settings (0.9.0) - [x] Add logging (0.9.1) -- [ ] Send data in chunks (if data is too large) (0.9.3) +- [x] Send data in chunks (if data is too large) (0.9.3) - [x] Add support for encryption (0.9.2) - [x] simple (via char xor'ing; auto-pairing) (0.9.2) - [x] password (via AES and PBKDF2) (0.9.3) - - [ ] certificate (via RSA) + - [ ] certificate (via RSA) - [ ] Add support for multiple clients From 038a3837fb4c6e8db31e175faae6d0250b13e69a Mon Sep 17 00:00:00 2001 From: SantaSpeen Date: Thu, 13 Mar 2025 19:52:59 +0300 Subject: [PATCH 13/13] [-] WinConnectCryptoCert --- winConnect/crypto/__init__.py | 2 +- winConnect/crypto/crypto_classes.py | 36 ++++++++++++++--------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/winConnect/crypto/__init__.py b/winConnect/crypto/__init__.py index 1c327c8..7ada28e 100644 --- a/winConnect/crypto/__init__.py +++ b/winConnect/crypto/__init__.py @@ -4,5 +4,5 @@ from .crypto_classes import ( WinConnectCryptoNone, WinConnectCryptoSimple, WinConnectCryptoPassword, - WinConnectCryptoCert + # WinConnectCryptoCert ) diff --git a/winConnect/crypto/crypto_classes.py b/winConnect/crypto/crypto_classes.py index 54d9b73..6b552f5 100644 --- a/winConnect/crypto/crypto_classes.py +++ b/winConnect/crypto/crypto_classes.py @@ -88,21 +88,21 @@ class WinConnectCryptoPassword(WinConnectCryptoBase): pad_len = decrypted[-1] # Убираем PKCS7 padding return decrypted[:-pad_len] -class WinConnectCryptoCert(WinConnectCryptoBase): - def __init__(self, cert_file: str): - if not _pip_crypto: - raise ImportError("Crypto library not installed. Install with 'pip install winConnect[crypto]'") - self.cert_file = Path(cert_file) - - def _open_cert(self): - pass - - def load(self) -> None: - self._open_cert() - - def encrypt(self, data: bytes) -> bytes: - pass - - def decrypt(self, data: bytes) -> bytes: - pass - +# class WinConnectCryptoCert(WinConnectCryptoBase): +# def __init__(self, cert_file: str): +# if not _pip_crypto: +# raise ImportError("Crypto library not installed. Install with 'pip install winConnect[crypto]'") +# self.cert_file = Path(cert_file) +# +# def _open_cert(self): +# pass +# +# def load(self) -> None: +# self._open_cert() +# +# def encrypt(self, data: bytes) -> bytes: +# pass +# +# def decrypt(self, data: bytes) -> bytes: +# pass +#