aboutsummaryrefslogtreecommitdiff
path: root/pyecsca/sca/target/ISO7816.py
blob: 950aa059da776687f131ca8f96f72048d470f9ea (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
"""Provides classes for working with ISO7816-4 APDUs and an abstract base class for an ISO7816-4 based target."""

from abc import abstractmethod, ABC
from dataclasses import dataclass
from enum import IntEnum
from typing import Optional

from public import public

from pyecsca.sca.target.base import Target


@public
class CardConnectionException(Exception):
    """Card could not be connected."""

    pass


@public
class CardProtocol(IntEnum):
    """Card protocol to use/negotiate."""

    T0 = 0
    T1 = 1


@public
@dataclass
class CommandAPDU:  # pragma: no cover
    """Command APDU that can be sent to an ISO7816-4 target."""

    cls: int
    ins: int
    p1: int
    p2: int
    data: Optional[bytes] = None
    ne: Optional[int] = None

    def __bytes__(self):
        if self.data is None or len(self.data) == 0:
            if self.ne is None or self.ne == 0:
                # Case 1
                return bytes([self.cls, self.ins, self.p1, self.p2])
            elif self.ne <= 256:
                # Case 2s
                return bytes(
                    [
                        self.cls,
                        self.ins,
                        self.p1,
                        self.p2,
                        self.ne if self.ne != 256 else 0,
                    ]
                )
            else:
                # Case 2e
                return bytes([self.cls, self.ins, self.p1, self.p2]) + (
                    self.ne.to_bytes(2, "big") if self.ne != 65536 else bytes([0, 0])
                )
        elif self.ne is None or self.ne == 0:
            if len(self.data) <= 255:
                # Case 3s
                return (
                    bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
                    + self.data
                )
            else:
                # Case 3e
                return (
                    bytes([self.cls, self.ins, self.p1, self.p2, 0])
                    + len(self.data).to_bytes(2, "big")
                    + self.data
                )
        else:
            if len(self.data) <= 255 and self.ne <= 256:
                # Case 4s
                return (
                    bytes([self.cls, self.ins, self.p1, self.p2, len(self.data)])
                    + self.data
                    + bytes([self.ne if self.ne != 256 else 0])
                )
            else:
                # Case 4e
                return (
                    bytes([self.cls, self.ins, self.p1, self.p2, 0])
                    + len(self.data).to_bytes(2, "big")
                    + self.data
                    + (
                        self.ne.to_bytes(2, "big")
                        if self.ne != 65536
                        else bytes([0, 0])
                    )
                )


@public
@dataclass
class ResponseAPDU:
    """Response APDU that can be received from an ISO7816-4 target."""

    data: bytes
    sw: int


@public
class ISO7816Target(Target, ABC):
    """ISO7816-4 target."""

    @abstractmethod
    def connect(self, protocol: Optional[CardProtocol] = None):
        """
        Connect to the card.

        :param protocol: CardProtocol to use.
        """
        raise NotImplementedError

    @property
    @abstractmethod
    def atr(self) -> bytes:
        """Return the ATR (Answer To Reset) of the target."""
        raise NotImplementedError

    @abstractmethod
    def select(self, aid: bytes) -> bool:
        """
        Select an applet with :paramref:`~.select.aid`.

        :param aid: The AID of the applet to select.
        :return: Whether the selection was successful.
        """
        raise NotImplementedError

    @abstractmethod
    def send_apdu(self, apdu: CommandAPDU) -> ResponseAPDU:
        """
        Send an APDU to the selected applet.

        :param apdu: The APDU to send.
        :return: The response.
        """
        raise NotImplementedError


@public
class ISO7816:
    """Bunch of ISO7816-4 constants (status words)."""

    SW_FILE_FULL = 0x6A84
    SW_UNKNOWN = 0x6F00
    SW_CLA_NOT_SUPPORTED = 0x6E00
    SW_INS_NOT_SUPPORTED = 0x6D00
    SW_CORRECT_LENGTH_00 = 0x6C00
    SW_WRONG_P1P2 = 0x6B00
    SW_INCORRECT_P1P2 = 0x6A86
    SW_RECORD_NOT_FOUND = 0x6A83
    SW_FILE_NOT_FOUND = 0x6A82
    SW_FUNC_NOT_SUPPORTED = 0x6A81
    SW_WRONG_DATA = 0x6A80
    SW_APPLET_SELECT_FAILED = 0x6999
    SW_COMMAND_NOT_ALLOWED = 0x6986
    SW_CONDITIONS_NOT_SATISFIED = 0x6985
    SW_DATA_INVALID = 0x6984
    SW_FILE_INVALID = 0x6983
    SW_SECURITY_STATUS_NOT_SATISFIED = 0x6982
    SW_WRONG_LENGTH = 0x6700
    SW_BYTES_REMAINING_00 = 0x6100
    SW_NO_ERROR = 0x9000