1# Copyright (c) 2018 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 __future__ import absolute_import
29
30from enum import IntEnum, unique
31import abc
32
33
34@unique
35class STATUS(IntEnum):
36    PROCESSING = 1
37    UPNEEDED = 2
38
39
40class CtapDevice(abc.ABC):
41    """
42    CTAP-capable device. Subclasses of this should implement call, as well as
43    list_devices, which should return a generator over discoverable devices.
44    """
45
46    @abc.abstractmethod
47    def call(self, cmd, data=b"", event=None, on_keepalive=None):
48        """Sends a command to the authenticator, and reads the response.
49
50        :param cmd: The integer value of the command.
51        :param data: The payload of the command.
52        :param event: An optional threading.Event which can be used to cancel
53            the invocation.
54        :param on_keepalive: An optional callback to handle keep-alive messages
55            from the authenticator. The function is only called once for
56            consecutive keep-alive messages with the same status.
57        :return: The response from the authenticator.
58        """
59
60    def close(self):
61        """Close the device, releasing any held resources."""
62
63    def __enter__(self):
64        return self
65
66    def __exit__(self, typ, value, traceback):
67        self.close()
68
69    @classmethod
70    @abc.abstractmethod
71    def list_devices(cls):
72        """Generates instances of cls for discoverable devices."""
73
74
75class CtapError(Exception):
76    class UNKNOWN_ERR(int):
77        name = "UNKNOWN_ERR"
78
79        @property
80        def value(self):
81            return int(self)
82
83        def __repr__(self):
84            return "<ERR.UNKNOWN: %d>" % self
85
86        def __str__(self):
87            return "0x%02X - UNKNOWN" % self
88
89    @unique
90    class ERR(IntEnum):
91        SUCCESS = 0x00
92        INVALID_COMMAND = 0x01
93        INVALID_PARAMETER = 0x02
94        INVALID_LENGTH = 0x03
95        INVALID_SEQ = 0x04
96        TIMEOUT = 0x05
97        CHANNEL_BUSY = 0x06
98        LOCK_REQUIRED = 0x0A
99        INVALID_CHANNEL = 0x0B
100        CBOR_UNEXPECTED_TYPE = 0x11
101        INVALID_CBOR = 0x12
102        MISSING_PARAMETER = 0x14
103        LIMIT_EXCEEDED = 0x15
104        # UNSUPPORTED_EXTENSION = 0x16  # No longer in spec
105        FP_DATABASE_FULL = 0x17
106        LARGE_BLOB_STORAGE_FULL = 0x18
107        CREDENTIAL_EXCLUDED = 0x19
108        PROCESSING = 0x21
109        INVALID_CREDENTIAL = 0x22
110        USER_ACTION_PENDING = 0x23
111        OPERATION_PENDING = 0x24
112        NO_OPERATIONS = 0x25
113        UNSUPPORTED_ALGORITHM = 0x26
114        OPERATION_DENIED = 0x27
115        KEY_STORE_FULL = 0x28
116        # NOT_BUSY = 0x29  # No longer in spec
117        # NO_OPERATION_PENDING = 0x2A  # No longer in spec
118        UNSUPPORTED_OPTION = 0x2B
119        INVALID_OPTION = 0x2C
120        KEEPALIVE_CANCEL = 0x2D
121        NO_CREDENTIALS = 0x2E
122        USER_ACTION_TIMEOUT = 0x2F
123        NOT_ALLOWED = 0x30
124        PIN_INVALID = 0x31
125        PIN_BLOCKED = 0x32
126        PIN_AUTH_INVALID = 0x33
127        PIN_AUTH_BLOCKED = 0x34
128        PIN_NOT_SET = 0x35
129        PUAT_REQUIRED = 0x36
130        PIN_POLICY_VIOLATION = 0x37
131        PIN_TOKEN_EXPIRED = 0x38
132        REQUEST_TOO_LARGE = 0x39
133        ACTION_TIMEOUT = 0x3A
134        UP_REQUIRED = 0x3B
135        UV_BLOCKED = 0x3C
136        INTEGRITY_FAILURE = 0x3D
137        INVALID_SUBCOMMAND = 0x3E
138        UV_INVALID = 0x3F
139        UNAUTHORIZED_PERMISSION = 0x40
140        OTHER = 0x7F
141        SPEC_LAST = 0xDF
142        EXTENSION_FIRST = 0xE0
143        EXTENSION_LAST = 0xEF
144        VENDOR_FIRST = 0xF0
145        VENDOR_LAST = 0xFF
146
147        def __str__(self):
148            return "0x%02X - %s" % (self.value, self.name)
149
150    def __init__(self, code):
151        try:
152            code = CtapError.ERR(code)
153        except ValueError:
154            code = CtapError.UNKNOWN_ERR(code)
155        self.code = code
156        super(CtapError, self).__init__("CTAP error: %s" % code)
157