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