232 lines
8.8 KiB
Python
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
|
|
# ============================================================================
|
|
|