diff --git a/README.md b/README.md index 77bcfdf..18b4ce4 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,15 @@ -# winConnect (Windows Only) +# winConnect Communicate Client-Server via Windows NamedPipe ## ToDo: -- [x] Add support for sending and receiving data -- [x] Add support for other header settings -- [x] Add support for safe closing -- [x] Add logging -- [ ] Send data in chunks (if data is too large) -- [ ] Add support for encryption - - [ ] simple (via char shift; and auto-pairing) +- [x] Add support for sending and receiving data (0.1.0) +- [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] Add support for encryption (0.9.2) + - [x] simple (via char xor'ing; auto-pairing) - [ ] password (via AES and PBKDF2) - [ ] certificate (via RSA) - [ ] Add support for multiple clients diff --git a/examples/echo_server.py b/examples/server_echo.py similarity index 72% rename from examples/echo_server.py rename to examples/server_echo.py index 140dc8d..48cbbe7 100644 --- a/examples/echo_server.py +++ b/examples/server_echo.py @@ -3,9 +3,8 @@ from winConnect import WinConnectDaemon connector = WinConnectDaemon('test') # Set header settings # see: https://docs.python.org/3.13/library/struct.html#format-characters -# Default: ">L" -# >L - Big-endian long integer (header_size: 4 bytes, max_size: 4294967295) -connector.set_header_settings(">L") +# 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=}") diff --git a/examples/with_crypto/client_simple.py b/examples/with_crypto/client_simple.py new file mode 100644 index 0000000..1ca740a --- /dev/null +++ b/examples/with_crypto/client_simple.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.WinConnectCryptoSimple() + +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_simple.py b/examples/with_crypto/server_simple.py new file mode 100644 index 0000000..3a9d97e --- /dev/null +++ b/examples/with_crypto/server_simple.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.WinConnectCryptoSimple() + +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) diff --git a/winConnect/WinConnectBase.py b/winConnect/WinConnectBase.py index 28803fb..a6a7fb6 100644 --- a/winConnect/WinConnectBase.py +++ b/winConnect/WinConnectBase.py @@ -9,9 +9,11 @@ import ormsgpack import pywintypes import win32file -from winConnect.errors import WinConnectErrors, WinConnectClientError -from winConnect import exceptions -from winConnect.utils import SimpleConvertor +from .crypto.WinConnectCrypto import WinConnectCrypto +from .crypto.crypto_classes import WinConnectCryptoNone +from .errors import WinConnectErrors, WinConnectError +from . import exceptions +from .utils import SimpleConvertor # header: len(data) in struct.pack via header_format # data: action:data @@ -19,17 +21,20 @@ from winConnect.utils import SimpleConvertor class WinConnectBase: init_encoding = 'utf-8' - init_header_format = ">L" # Format for reading header (big-endian, unsigned long; 4 bytes) + init_header_format = ">H" # Format for reading header (big-endian, unsigned long; 4 bytes) default_encoding = 'utf-8' - read_max_buffer = SimpleConvertor.to_gb(4) # Max size of buffer for message + read_max_buffer = SimpleConvertor.to_gb(3)-1 # Max size via chunked messages ormsgpack_options = ormsgpack.OPT_NON_STR_KEYS | ormsgpack.OPT_NAIVE_UTC | ormsgpack.OPT_PASSTHROUGH_TUPLE # ormsgpack options def __init__(self, pipe_name: str): self._log = logging.getLogger(f"WinConnect:{pipe_name}") - self._version = 1 + # _version: + # 1 - 0.9.1 + # 2 - 0.9.2+ (with crypto) + self._version = 2 self._pipe_name = r'\\.\pipe\{}'.format(pipe_name) self._pipe = None self._opened = False @@ -42,12 +47,24 @@ class WinConnectBase: self._inited = False self._session_encoding = self.init_encoding - self._parts_buffer = None # Buffer for parts of message (If message is too big) + self.__crypto = WinConnectCrypto() + self.__crypto.set_crypto_class(WinConnectCryptoNone()) + + # self._chunks = [] self._lock = threading.Lock() + def set_crypto(self, crypto): + if self._connected: + raise exceptions.WinConnectConnectionAlreadyOpenException("Can't change crypto while session is active") + self.__crypto.set_crypto_class(crypto) + if not self.__crypto.test_and_load(): + raise exceptions.WinConnectCryptoException("Crypto failed test") + def set_logger(self, logger): + logger.debug(f"[{self._pipe_name}] Update logger") self._log = logger + self.__crypto.set_logger(logger) def _calc_body_max_size(self): # Max size of body: 2 ** (8 * header_size) - 1 - header_size - 1 @@ -129,8 +146,10 @@ class WinConnectBase: 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) - # self._log.debug(f"[{self._pipe_name}] Received message: {action=} {data=}") + self._log.debug(f"[{self._pipe_name}] Received message: {action=} {data=}") return action, data def _send_message(self, action: str, data: Any): @@ -139,6 +158,8 @@ class WinConnectBase: if self.closed: raise exceptions.WinConnectSessionClosedException("Session is closed") packed_data = self.__pack_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') @@ -147,9 +168,9 @@ class WinConnectBase: raise ValueError('Message is too big') _hfmt, _ = self.__header_settings header = struct.pack(_hfmt, message_size) - # self._log.debug(f"[{self._pipe_name}] Sending message: {action=} {data=}") - win32file.WriteFile(self._pipe, header) - win32file.WriteFile(self._pipe, packed_data) + packet = header + packed_data + self._log.debug(f"[{self._pipe_name}] Sending message: {action=} {data=}; {packet=}") + win32file.WriteFile(self._pipe, packet) def _send_error(self, error: WinConnectErrors, error_message: str = None): e = {"error": True, "code": error.value, "message": error.name, "description": error_message} @@ -165,7 +186,7 @@ class WinConnectBase: case b"data": return False, data case b"error": - return False, WinConnectClientError(data['code'], data['message']) + return False, WinConnectError(data['code'], data['message']) case _: return self._send_error(WinConnectErrors.UNKNOWN_ACTION, f"Unknown action '{action}'") @@ -179,7 +200,8 @@ class WinConnectBase: 'encoding': self.default_encoding, 'header_size': self._header_size, 'header_format': self._header_format, - 'max_buffer': self.read_max_buffer + 'max_buffer': self.read_max_buffer, + "crypto": self.__crypto.get_info() } session_settings = f"set_session_settings:{json.dumps(settings)}".encode(self.init_encoding) self._send_message("command", session_settings) @@ -191,15 +213,20 @@ class WinConnectBase: self._send_error(WinConnectErrors.BAD_DATA, f"JSONDecodeError: {e}") return self.close() if settings.get('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(): + self._log.error(f"{WinConnectErrors.BAD_CRYPTO}") + self._send_error(WinConnectErrors.BAD_CRYPTO, f"Crypto mismatch") + return self.close() 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) self.read_max_buffer = settings.get('max_buffer', self.read_max_buffer) - self._send_message("command", b"ready:") return True - case b"ready": + case b"session_ready": + self._inited = True return True case b"close": self.close() @@ -215,7 +242,8 @@ class WinConnectBase: 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._inited = True + self._send_message("command", b"session_ready:") + self._parse_action(*self._read_message()) def send_data(self, data): self._send_message("data", data) diff --git a/winConnect/WinConnectClient.py b/winConnect/WinConnectClient.py index 4362c55..6a41eb8 100644 --- a/winConnect/WinConnectClient.py +++ b/winConnect/WinConnectClient.py @@ -1,8 +1,8 @@ import pywintypes import win32file -from winConnect.WinConnectBase import WinConnectBase -from winConnect.exceptions import WinConnectConnectionNoPipeException +from .WinConnectBase import WinConnectBase +from .exceptions import WinConnectConnectionNoPipeException class WinConnectClient(WinConnectBase): @@ -30,7 +30,7 @@ class WinConnectClient(WinConnectBase): ) self._opened = True self._connected = True - self._log.debug(f"Pipe '{self._pipe_name}' opened") + self._log.debug(f"[{self._pipe_name}] Pipe opened") except pywintypes.error as e: if e.winerror == 2: exc = WinConnectConnectionNoPipeException(f"Error while opening pipe: Pipe not found") diff --git a/winConnect/WinConnectDaemon.py b/winConnect/WinConnectDaemon.py index ea21a6c..c974a74 100644 --- a/winConnect/WinConnectDaemon.py +++ b/winConnect/WinConnectDaemon.py @@ -1,16 +1,14 @@ import win32pipe -from winConnect.WinConnectBase import WinConnectBase -from winConnect.utils import SimpleConvertor +from .WinConnectBase import WinConnectBase +from .crypto import WinConnectCrypto class WinConnectDaemon(WinConnectBase): # see: https://mhammond.github.io/pywin32/win32pipe__CreateNamedPipe_meth.html pipe_openMode = win32pipe.PIPE_ACCESS_DUPLEX # Open mode (read/write) pipe_pipeMode = win32pipe.PIPE_TYPE_MESSAGE | win32pipe.PIPE_READMODE_MESSAGE | win32pipe.PIPE_WAIT # Pipe mode (message type, message read mode, blocking mode) - pipe_nMaxInstances = 2 # Max number of instances - pipe_nOutBufferSize = SimpleConvertor.to_kb(64) # Max size of output buffer - pipe_nInBufferSize = SimpleConvertor.to_kb(64) # Max size of input buffer + pipe_nMaxInstances = 1 # Max number of instances pipe_nDefaultTimeOut = 0 # ~ ms pipe_sa = None # Security attributes @@ -19,13 +17,17 @@ class WinConnectDaemon(WinConnectBase): self.run = True def _open_pipe(self): + pipe_nOutBufferSize, pipe_nInBufferSize = self._body_max_size+20, self._body_max_size+20 + self._log.debug(f"[{self._pipe_name}] Creating pipe. " + f"Settings: {self.pipe_openMode=}, {self.pipe_pipeMode=}, {self.pipe_nMaxInstances=}, " + f"{pipe_nOutBufferSize=}, {pipe_nInBufferSize=}, {self.pipe_nDefaultTimeOut=}, {self.pipe_sa=}") self._pipe = win32pipe.CreateNamedPipe( self._pipe_name, self.pipe_openMode, self.pipe_pipeMode, self.pipe_nMaxInstances, - self.pipe_nOutBufferSize, - self.pipe_nInBufferSize, + pipe_nOutBufferSize, + pipe_nInBufferSize, self.pipe_nDefaultTimeOut, self.pipe_sa ) diff --git a/winConnect/__init__.py b/winConnect/__init__.py index c3da51b..e812505 100644 --- a/winConnect/__init__.py +++ b/winConnect/__init__.py @@ -1,4 +1,6 @@ from .WinConnectDaemon import WinConnectDaemon from .WinConnectClient import WinConnectClient +from . import crypto + from .__meta__ import * diff --git a/winConnect/__meta__.py b/winConnect/__meta__.py index 64957c2..4cc7390 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.1' -__build__ = 16 +__version__ = '0.9.2' +__build__ = 39 __author__ = 'SantaSpeen' __author_email__ = 'admin@anidev.ru' __license__ = "MIT" diff --git a/winConnect/crypto/WinConnectCrypto.py b/winConnect/crypto/WinConnectCrypto.py new file mode 100644 index 0000000..03a33f9 --- /dev/null +++ b/winConnect/crypto/WinConnectCrypto.py @@ -0,0 +1,48 @@ +import logging + +from .crypto_class_base import WinConnectCryptoBase +from winConnect.exceptions import ( + WinConnectCryptoBadModeException, + WinConnectCryptoException, +) + +def test_crypto_class(crypto_class: "WinConnectCryptoBase"): + if not isinstance(crypto_class, WinConnectCryptoBase): + raise WinConnectCryptoBadModeException("crypto_class must be a subclass of WinConnectCryptoBase") + if not crypto_class.test(): + raise WinConnectCryptoException("crypto_class failed test (test_bytes != decrypt_bytes)") + +class WinConnectCrypto: + + def __init__(self): + self.__crypto_class = None + self.__log_prefix = None + self._log = logging.getLogger("WinConnectCrypto") + + 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__}") + test_crypto_class(crypto_class) + self.__crypto_class = crypto_class + self.__log_prefix = f"[{self.get_info()}] " + self._log.debug(f"{self.__log_prefix}Crypto class updated.") + + def set_logger(self, logger): + logger.debug(f"{self.__log_prefix}Setting logger") + self._log = logger + + def test_and_load(self) -> bool: + self._log.debug(f"{self.__log_prefix}Testing and loading crypto class") + if not self.__crypto_class.test(): + return False + self.__crypto_class.load() + 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) + + def decrypt(self, data: bytes) -> bytes: + return self.__crypto_class.decrypt(data) diff --git a/winConnect/crypto/__init__.py b/winConnect/crypto/__init__.py new file mode 100644 index 0000000..079eca7 --- /dev/null +++ b/winConnect/crypto/__init__.py @@ -0,0 +1,8 @@ +from .WinConnectCrypto import WinConnectCrypto +from .crypto_classes import ( + WinConnectCryptoBase, + WinConnectCryptoNone, + WinConnectCryptoSimple, + # WinConnectCryptoPassword, + # WinConnectCryptoCert +) diff --git a/winConnect/crypto/crypto_class_base.py b/winConnect/crypto/crypto_class_base.py new file mode 100644 index 0000000..baab3d1 --- /dev/null +++ b/winConnect/crypto/crypto_class_base.py @@ -0,0 +1,14 @@ +class WinConnectCryptoBase: + + def encrypt(self, data: bytes) -> bytes: ... + def decrypt(self, data: bytes) -> bytes: ... + + def test(self, test_bytes: bytes = b"test_string") -> bool: + encrypted = self.encrypt(test_bytes) + decrypted = self.decrypt(encrypted) + if decrypted != test_bytes: + return False + return True + + def load(self) -> None: ... + def unload(self) -> None: ... diff --git a/winConnect/crypto/crypto_classes.py b/winConnect/crypto/crypto_classes.py new file mode 100644 index 0000000..1579b55 --- /dev/null +++ b/winConnect/crypto/crypto_classes.py @@ -0,0 +1,70 @@ +import random + +from .crypto_class_base import WinConnectCryptoBase +from winConnect.exceptions import WinConnectCryptoSimpleBadHeaderException + + +class WinConnectCryptoNone(WinConnectCryptoBase): + def __init__(self): ... + def encrypt(self, data: bytes) -> bytes: + return data + def decrypt(self, data: bytes) -> bytes: + return data + +class WinConnectCryptoSimple(WinConnectCryptoBase): + def __init__(self): + pass + + def encrypt(self, data: bytes) -> bytes: + 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)) + for char in data: + encrypted_text.append(char ^ key) + return bytes(encrypted_text) + + def decrypt(self, data: bytes) -> bytes: + try: + header, content = data.split(b":", 1) + if header[:4] != b"wccs": + raise WinConnectCryptoSimpleBadHeaderException("Bad header in message.") + except ValueError: + raise WinConnectCryptoSimpleBadHeaderException("No header in message.") + shift_key = int(header[4:7]) + key = int(header[7:]) - shift_key + decrypted_text = bytearray() + for char in content: + decrypted_text.append(char ^ key) + return bytes(decrypted_text) + + +class WinConnectCryptoPassword(WinConnectCryptoBase): + + def __init__(self, password: str): + pass + + def encrypt(self, data: bytes) -> bytes: + pass + + def decrypt(self, data: bytes) -> bytes: + pass + +class WinConnectCryptoCert(WinConnectCryptoBase): + def __init__(self, cert_file: str): + pass + + 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 + diff --git a/winConnect/errors.py b/winConnect/errors.py index 1e79b1a..377a194 100644 --- a/winConnect/errors.py +++ b/winConnect/errors.py @@ -14,11 +14,13 @@ class WinConnectErrors(Enum): BAD_DATA = 50 BAD_VERSION = 51 BAD_HEADER = 52 + BAD_BODY = 53 + BAD_CRYPTO = 54 BODY_TOO_BIG = 60 @dataclass -class WinConnectClientError: +class WinConnectError: code: WinConnectErrors message: str diff --git a/winConnect/exceptions.py b/winConnect/exceptions.py index e04484b..433bc2c 100644 --- a/winConnect/exceptions.py +++ b/winConnect/exceptions.py @@ -31,3 +31,26 @@ class WinConnectSessionAlreadyActiveException(WinConnectBaseException): ... class WinConnectSessionClosedException(WinConnectBaseException): ... +# Crypto + +class WinConnectCryptoException(WinConnectBaseException): ... + +class WinConnectCryptoBadModeException(WinConnectCryptoException): ... + +## Simple + +class WinConnectCryptoSimpleBadHeaderException(WinConnectCryptoException): ... + +## key +class WinConnectCryptoKeyRequiredException(WinConnectCryptoException): ... + +class WinConnectCryptoKeyInvalidException(WinConnectCryptoException): ... + +## cert +class WinConnectCryptoCertificationRequiredException(WinConnectCryptoException): ... + +class WinConnectCryptoCertificationNotFoundException(WinConnectCryptoException): ... + +class WinConnectCryptoCertificationInvalidException(WinConnectCryptoException): ... + +