Files
ISMAIL MASSERAN ca6e55f853 first init
2026-09-01 10:12:31 +08:00

232 lines
8.8 KiB
Python

"""
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
# ============================================================================