""" Smart Card Communication Library Unified cross-platform library for smart card communication. REQUIRES: pyscard (pip install pyscard) See README.md for platform setup instructions. """ import platform import sys # Constants DEFAULT_IOCTL_CODE = 3500 # CCID escape command IOCTL code for smart card readers # Try to import smartcard library with platform-specific error handling try: from smartcard.System import readers from smartcard.util import toHexString from smartcard.CardConnection import CardConnection from smartcard.scard import ( SCARD_SCOPE_USER, SCARD_S_SUCCESS, SCARD_SHARE_DIRECT, SCARD_PROTOCOL_T0, SCARD_PROTOCOL_T1, SCARD_PROTOCOL_UNDEFINED, SCARD_LEAVE_CARD, SCARD_CTL_CODE, SCardEstablishContext, SCardConnect, SCardControl, SCardDisconnect, SCardReleaseContext, SCardGetErrorMessage ) import smartcard.Exceptions except ImportError: error_msg = "pyscard library not found!\n\n" system = platform.system() if system == "Darwin": # macOS error_msg += "For macOS, install using:\n" error_msg += " pip install pyscard\n\n" error_msg += "Note: PCSC framework should be pre-installed on macOS." elif system == "Linux": # Ubuntu/Linux error_msg += "For Ubuntu/Linux, first install system dependencies:\n" error_msg += " sudo apt-get install pcscd libpcsclite1 libpcsclite-dev\n" error_msg += " sudo systemctl start pcscd\n" error_msg += " sudo systemctl enable pcscd\n\n" error_msg += "Then install Python package:\n" error_msg += " pip install pyscard" else: # Windows error_msg += "For Windows, install using:\n" error_msg += " pip install pyscard\n\n" error_msg += "Note: Windows Smart Card API should be available by default." print(error_msg) if sys.stdout.isatty(): input("Press Enter to exit...") sys.exit(1) def list_readers(): """ List all available smart card readers. Returns: List of reader objects. """ try: reader_list = readers() return list(reader_list) except smartcard.Exceptions.NoReadersException: return [] except Exception as e: print(f"Error listing readers: {str(e)}") return [] def parse_command_string(hex_string): """ Parse a hexadecimal string into a list of bytes. Args: hex_string: Hex string (spaces optional) Returns: List of integers representing bytes Raises: ValueError: If string contains invalid characters or has odd length """ hex_string = hex_string.replace(" ", "") if not all(c in '0123456789ABCDEFabcdef' for c in hex_string): raise ValueError("Invalid characters in hex string. Use hexadecimal values only.") if len(hex_string) % 2 != 0: raise ValueError("Hex string length must be even (each byte is 2 hex digits).") return [int(hex_string[i:i + 2], 16) for i in range(0, len(hex_string), 2)] # ============================================================================ # CONNECTION FUNCTIONS # ============================================================================ def connect_to_reader(reader, mode='share', protocol='auto'): """ Connect to a smart card reader. Args: reader: Reader object from list_readers() or reader name string. mode: 'share' for APDU communication (default), 'direct' for escape commands. protocol: 'auto' (default), 't0t1', or 'undefined'. Returns: tuple: (connection, error_msg, context). """ if reader is None: return None, "Reader is None.", None protocol_map = { 't0t1': SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1, 'undefined': SCARD_PROTOCOL_UNDEFINED, 'auto': (SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1) if platform.system() == "Darwin" else SCARD_PROTOCOL_UNDEFINED } protocol_value = protocol_map.get(protocol, protocol_map['auto']) if mode == 'share': try: connection = reader.createConnection() connection.connect(CardConnection.T0_protocol | CardConnection.T1_protocol if protocol_value == (SCARD_PROTOCOL_T0 | SCARD_PROTOCOL_T1) else None) return connection, None, None except smartcard.Exceptions.NoCardException: return None, "No card in reader. Please insert a smart card.", None except Exception as e: return None, f"Connection error: {str(e)}", None elif mode == 'direct': hcontext = None connection_successful = False try: hresult, hcontext = SCardEstablishContext(SCARD_SCOPE_USER) if hresult != SCARD_S_SUCCESS: return None, f"Context setup failed: {SCardGetErrorMessage(hresult)}", None reader_name = str(reader) if not isinstance(reader, str) else reader hresult, hcard, _ = SCardConnect(hcontext, reader_name, SCARD_SHARE_DIRECT, protocol_value) if hresult == SCARD_S_SUCCESS: connection_successful = True return hcard, None, hcontext else: return None, f"Failed to connect: {SCardGetErrorMessage(hresult)}", None except Exception as e: return None, f"Exception connecting: {str(e)}", None finally: if hcontext and not connection_successful: try: SCardReleaseContext(hcontext) except: pass else: return None, f"Invalid mode: {mode}. Use 'share' or 'direct'.", None def disconnect_reader(connection, hcontext=None): """ Disconnects from a smart card reader and releases context if applicable. Args: connection: CardConnection object (APDU mode) or card handle (escape mode). hcontext: The established context handle for direct connections. """ try: if connection is None: return # pyscard returns CardConnectionDecorator, not CardConnection directly if hasattr(connection, 'disconnect'): connection.disconnect() else: SCardDisconnect(connection, SCARD_LEAVE_CARD) except Exception as e: print(f"Error disconnecting from reader: {str(e)}") finally: if hcontext: try: SCardReleaseContext(hcontext) except Exception as e: print(f"Error releasing context: {str(e)}") # ============================================================================ # COMMAND FUNCTIONS # ============================================================================ def send_command(connection, command, mode='apdu', ioctl_code=DEFAULT_IOCTL_CODE): """ Send a command to a smart card device. Args: connection: CardConnection object (APDU mode) or card handle (escape mode). command: Hex string (APDU mode) or byte array/list (escape mode). mode: 'apdu' for APDU commands, 'escape' for escape commands. ioctl_code: IOCTL code for escape commands (default: 3500 - CCID escape). Returns: For APDU mode: (success, response, sw1, sw2, error_msg). For escape mode: (success, response, error_msg). """ if mode not in ('apdu', 'escape'): raise ValueError(f"Invalid mode: {mode}. Must be 'apdu' or 'escape'") if connection is None: error_msg = "No connection. Please connect to a reader first." return (False, [], 0, 0, error_msg) if mode == 'apdu' else (False, [], error_msg) try: if mode == 'apdu': apdu = parse_command_string(command) response, sw1, sw2 = connection.transmit(apdu) return True, response, sw1, sw2, None elif mode == 'escape': IOCTL_CCID_ESCAPE = SCARD_CTL_CODE(ioctl_code) hresult, response = SCardControl(connection, IOCTL_CCID_ESCAPE, command) return (True, response, None) if hresult == SCARD_S_SUCCESS else (False, response, SCardGetErrorMessage(hresult)) else: error_msg = f"Invalid mode: {mode}. Use 'apdu' or 'escape'." return (False, [], 0, 0, error_msg) if mode == 'apdu' else (False, [], error_msg) except ValueError as e: error_msg = f"Invalid command format: {str(e)}" return (False, [], 0, 0, error_msg) if mode == 'apdu' else (False, [], error_msg) except Exception as e: error_msg = f"Transmission error: {str(e)}" return (False, [], 0, 0, error_msg) if mode == 'apdu' else (False, [], error_msg) # ============================================================================ # UTILITY FUNCTIONS # ============================================================================