1# Copyright (c) 2020 Yubico AB 2# All rights reserved. 3# 4# Redistribution and use in source and binary forms, with or 5# without modification, are permitted provided that the following 6# conditions are met: 7# 8# 1. Redistributions of source code must retain the above copyright 9# notice, this list of conditions and the following disclaimer. 10# 2. Redistributions in binary form must reproduce the above 11# copyright notice, this list of conditions and the following 12# disclaimer in the documentation and/or other materials provided 13# with the distribution. 14# 15# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 24# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 25# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 26# POSSIBILITY OF SUCH DAMAGE. 27 28from . import Version, TRANSPORT, Connection, CommandError, ApplicationNotAvailableError 29from time import time 30from enum import Enum, IntEnum, unique 31from typing import Tuple 32import abc 33import struct 34 35 36class SmartCardConnection(Connection, metaclass=abc.ABCMeta): 37 @property 38 @abc.abstractmethod 39 def transport(self) -> TRANSPORT: 40 """Get the transport type of the connection (USB or NFC)""" 41 42 @abc.abstractmethod 43 def send_and_receive(self, apdu: bytes) -> Tuple[bytes, int]: 44 """Sends a command APDU and returns the response""" 45 46 47class ApduError(CommandError): 48 """Thrown when an APDU response has the wrong SW code""" 49 50 def __init__(self, data: bytes, sw: int): 51 self.data = data 52 self.sw = sw 53 54 def __str__(self): 55 return f"APDU error: SW=0x{self.sw:04x}" 56 57 58@unique 59class ApduFormat(str, Enum): 60 """APDU encoding format""" 61 62 SHORT = "short" 63 EXTENDED = "extended" 64 65 66@unique 67class SW(IntEnum): 68 NO_INPUT_DATA = 0x6285 69 VERIFY_FAIL_NO_RETRY = 0x63C0 70 WRONG_LENGTH = 0x6700 71 SECURITY_CONDITION_NOT_SATISFIED = 0x6982 72 AUTH_METHOD_BLOCKED = 0x6983 73 DATA_INVALID = 0x6984 74 CONDITIONS_NOT_SATISFIED = 0x6985 75 COMMAND_NOT_ALLOWED = 0x6986 76 INCORRECT_PARAMETERS = 0x6A80 77 FUNCTION_NOT_SUPPORTED = 0x6A81 78 FILE_NOT_FOUND = 0x6A82 79 NO_SPACE = 0x6A84 80 REFERENCE_DATA_NOT_FOUND = 0x6A88 81 WRONG_PARAMETERS_P1P2 = 0x6B00 82 INVALID_INSTRUCTION = 0x6D00 83 COMMAND_ABORTED = 0x6F00 84 OK = 0x9000 85 86 87INS_SELECT = 0xA4 88P1_SELECT = 0x04 89P2_SELECT = 0x00 90 91INS_SEND_REMAINING = 0xC0 92SW1_HAS_MORE_DATA = 0x61 93 94SHORT_APDU_MAX_CHUNK = 0xFF 95 96 97def _encode_short_apdu(cla, ins, p1, p2, data): 98 return struct.pack(">BBBBB", cla, ins, p1, p2, len(data)) + data 99 100 101def _encode_extended_apdu(cla, ins, p1, p2, data): 102 return struct.pack(">BBBBBH", cla, ins, p1, p2, 0, len(data)) + data 103 104 105class SmartCardProtocol: 106 def __init__( 107 self, 108 smartcard_connection: SmartCardConnection, 109 ins_send_remaining: int = INS_SEND_REMAINING, 110 ): 111 self.apdu_format = ApduFormat.SHORT 112 self.connection = smartcard_connection 113 self._ins_send_remaining = ins_send_remaining 114 self._touch_workaround = False 115 self._last_long_resp = 0.0 116 117 def close(self) -> None: 118 self.connection.close() 119 120 def enable_touch_workaround(self, version: Version) -> None: 121 self._touch_workaround = self.connection.transport == TRANSPORT.USB and ( 122 (4, 2, 0) <= version <= (4, 2, 6) 123 ) 124 125 def select(self, aid: bytes) -> bytes: 126 try: 127 return self.send_apdu(0, INS_SELECT, P1_SELECT, P2_SELECT, aid) 128 except ApduError as e: 129 if e.sw in ( 130 SW.FILE_NOT_FOUND, 131 SW.INVALID_INSTRUCTION, 132 SW.WRONG_PARAMETERS_P1P2, 133 ): 134 raise ApplicationNotAvailableError() 135 raise 136 137 def send_apdu( 138 self, cla: int, ins: int, p1: int, p2: int, data: bytes = b"" 139 ) -> bytes: 140 if ( 141 self._touch_workaround 142 and self._last_long_resp > 0 143 and time() - self._last_long_resp < 2 144 ): 145 self.connection.send_and_receive( 146 _encode_short_apdu(0, 0, 0, 0, b"") 147 ) # Dummy APDU, returns error 148 self._last_long_resp = 0 149 150 if self.apdu_format is ApduFormat.SHORT: 151 while len(data) > SHORT_APDU_MAX_CHUNK: 152 chunk, data = data[:SHORT_APDU_MAX_CHUNK], data[SHORT_APDU_MAX_CHUNK:] 153 response, sw = self.connection.send_and_receive( 154 _encode_short_apdu(0x10 | cla, ins, p1, p2, chunk) 155 ) 156 if sw != SW.OK: 157 raise ApduError(response, sw) 158 response, sw = self.connection.send_and_receive( 159 _encode_short_apdu(cla, ins, p1, p2, data) 160 ) 161 get_data = _encode_short_apdu(0, self._ins_send_remaining, 0, 0, b"") 162 elif self.apdu_format is ApduFormat.EXTENDED: 163 response, sw = self.connection.send_and_receive( 164 _encode_extended_apdu(cla, ins, p1, p2, data) 165 ) 166 get_data = _encode_extended_apdu(0, self._ins_send_remaining, 0, 0, b"") 167 else: 168 raise TypeError("Invalid ApduFormat set") 169 170 # Read chained response 171 buf = b"" 172 while sw >> 8 == SW1_HAS_MORE_DATA: 173 buf += response 174 response, sw = self.connection.send_and_receive(get_data) 175 176 if sw != SW.OK: 177 raise ApduError(response, sw) 178 buf += response 179 180 if self._touch_workaround and len(buf) > 54: 181 self._last_long_resp = time() 182 else: 183 self._last_long_resp = 0 184 185 return buf 186