aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py3/twisted/words/protocols/jabber/sasl.py
blob: 8bcf1a534aa3495c7398bb02e6af160d09965c21 (plain) (blame)
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
XMPP-specific SASL profile.
"""


import re
from base64 import b64decode, b64encode

from twisted.internet import defer
from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream
from twisted.words.xish import domish

NS_XMPP_SASL = "urn:ietf:params:xml:ns:xmpp-sasl"


def get_mechanisms(xs):
    """
    Parse the SASL feature to extract the available mechanism names.
    """
    mechanisms = []
    for element in xs.features[(NS_XMPP_SASL, "mechanisms")].elements():
        if element.name == "mechanism":
            mechanisms.append(str(element))

    return mechanisms


class SASLError(Exception):
    """
    SASL base exception.
    """


class SASLNoAcceptableMechanism(SASLError):
    """
    The server did not present an acceptable SASL mechanism.
    """


class SASLAuthError(SASLError):
    """
    SASL Authentication failed.
    """

    def __init__(self, condition=None):
        self.condition = condition

    def __str__(self) -> str:
        return "SASLAuthError with condition %r" % self.condition


class SASLIncorrectEncodingError(SASLError):
    """
    SASL base64 encoding was incorrect.

    RFC 3920 specifies that any characters not in the base64 alphabet
    and padding characters present elsewhere than at the end of the string
    MUST be rejected. See also L{fromBase64}.

    This exception is raised whenever the encoded string does not adhere
    to these additional restrictions or when the decoding itself fails.

    The recommended behaviour for so-called receiving entities (like servers in
    client-to-server connections, see RFC 3920 for terminology) is to fail the
    SASL negotiation with a C{'incorrect-encoding'} condition. For initiating
    entities, one should assume the receiving entity to be either buggy or
    malevolent. The stream should be terminated and reconnecting is not
    advised.
    """


base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$")


def fromBase64(s):
    """
    Decode base64 encoded string.

    This helper performs regular decoding of a base64 encoded string, but also
    rejects any characters that are not in the base64 alphabet and padding
    occurring elsewhere from the last or last two characters, as specified in
    section 14.9 of RFC 3920. This safeguards against various attack vectors
    among which the creation of a covert channel that "leaks" information.
    """

    if base64Pattern.match(s) is None:
        raise SASLIncorrectEncodingError()

    try:
        return b64decode(s)
    except Exception as e:
        raise SASLIncorrectEncodingError(str(e))


class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer):
    """
    Stream initializer that performs SASL authentication.

    The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN}
    and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set
    on the authenticator, does not have a localpart (username), requesting an
    anonymous session where the username is generated by the server.
    Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order.
    """

    feature = (NS_XMPP_SASL, "mechanisms")
    _deferred = None

    def setMechanism(self):
        """
        Select and setup authentication mechanism.

        Uses the authenticator's C{jid} and C{password} attribute for the
        authentication credentials. If no supported SASL mechanisms are
        advertized by the receiving party, a failing deferred is returned with
        a L{SASLNoAcceptableMechanism} exception.
        """

        jid = self.xmlstream.authenticator.jid
        password = self.xmlstream.authenticator.password

        mechanisms = get_mechanisms(self.xmlstream)
        if jid.user is not None:
            if "DIGEST-MD5" in mechanisms:
                self.mechanism = sasl_mechanisms.DigestMD5(
                    "xmpp", jid.host, None, jid.user, password
                )
            elif "PLAIN" in mechanisms:
                self.mechanism = sasl_mechanisms.Plain(None, jid.user, password)
            else:
                raise SASLNoAcceptableMechanism()
        else:
            if "ANONYMOUS" in mechanisms:
                self.mechanism = sasl_mechanisms.Anonymous()
            else:
                raise SASLNoAcceptableMechanism()

    def start(self):
        """
        Start SASL authentication exchange.
        """

        self.setMechanism()
        self._deferred = defer.Deferred()
        self.xmlstream.addObserver("/challenge", self.onChallenge)
        self.xmlstream.addOnetimeObserver("/success", self.onSuccess)
        self.xmlstream.addOnetimeObserver("/failure", self.onFailure)
        self.sendAuth(self.mechanism.getInitialResponse())
        return self._deferred

    def sendAuth(self, data=None):
        """
        Initiate authentication protocol exchange.

        If an initial client response is given in C{data}, it will be
        sent along.

        @param data: initial client response.
        @type data: C{str} or L{None}.
        """

        auth = domish.Element((NS_XMPP_SASL, "auth"))
        auth["mechanism"] = self.mechanism.name
        if data is not None:
            auth.addContent(b64encode(data).decode("ascii") or "=")
        self.xmlstream.send(auth)

    def sendResponse(self, data=b""):
        """
        Send response to a challenge.

        @param data: client response.
        @type data: L{bytes}.
        """

        response = domish.Element((NS_XMPP_SASL, "response"))
        if data:
            response.addContent(b64encode(data).decode("ascii"))
        self.xmlstream.send(response)

    def onChallenge(self, element):
        """
        Parse challenge and send response from the mechanism.

        @param element: the challenge protocol element.
        @type element: L{domish.Element}.
        """

        try:
            challenge = fromBase64(str(element))
        except SASLIncorrectEncodingError:
            self._deferred.errback()
        else:
            self.sendResponse(self.mechanism.getResponse(challenge))

    def onSuccess(self, success):
        """
        Clean up observers, reset the XML stream and send a new header.

        @param success: the success protocol element. For now unused, but
                        could hold additional data.
        @type success: L{domish.Element}
        """

        self.xmlstream.removeObserver("/challenge", self.onChallenge)
        self.xmlstream.removeObserver("/failure", self.onFailure)
        self.xmlstream.reset()
        self.xmlstream.sendHeader()
        self._deferred.callback(xmlstream.Reset)

    def onFailure(self, failure):
        """
        Clean up observers, parse the failure and errback the deferred.

        @param failure: the failure protocol element. Holds details on
                        the error condition.
        @type failure: L{domish.Element}
        """

        self.xmlstream.removeObserver("/challenge", self.onChallenge)
        self.xmlstream.removeObserver("/success", self.onSuccess)
        try:
            condition = failure.firstChildElement().name
        except AttributeError:
            condition = None
        self._deferred.errback(SASLAuthError(condition))