sasl.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. # Copyright (c) Twisted Matrix Laboratories.
  2. # See LICENSE for details.
  3. """
  4. XMPP-specific SASL profile.
  5. """
  6. from __future__ import absolute_import, division
  7. from base64 import b64decode, b64encode
  8. import re
  9. from twisted.internet import defer
  10. from twisted.python.compat import unicode
  11. from twisted.words.protocols.jabber import sasl_mechanisms, xmlstream
  12. from twisted.words.xish import domish
  13. NS_XMPP_SASL = 'urn:ietf:params:xml:ns:xmpp-sasl'
  14. def get_mechanisms(xs):
  15. """
  16. Parse the SASL feature to extract the available mechanism names.
  17. """
  18. mechanisms = []
  19. for element in xs.features[(NS_XMPP_SASL, 'mechanisms')].elements():
  20. if element.name == 'mechanism':
  21. mechanisms.append(unicode(element))
  22. return mechanisms
  23. class SASLError(Exception):
  24. """
  25. SASL base exception.
  26. """
  27. class SASLNoAcceptableMechanism(SASLError):
  28. """
  29. The server did not present an acceptable SASL mechanism.
  30. """
  31. class SASLAuthError(SASLError):
  32. """
  33. SASL Authentication failed.
  34. """
  35. def __init__(self, condition=None):
  36. self.condition = condition
  37. def __str__(self):
  38. return "SASLAuthError with condition %r" % self.condition
  39. class SASLIncorrectEncodingError(SASLError):
  40. """
  41. SASL base64 encoding was incorrect.
  42. RFC 3920 specifies that any characters not in the base64 alphabet
  43. and padding characters present elsewhere than at the end of the string
  44. MUST be rejected. See also L{fromBase64}.
  45. This exception is raised whenever the encoded string does not adhere
  46. to these additional restrictions or when the decoding itself fails.
  47. The recommended behaviour for so-called receiving entities (like servers in
  48. client-to-server connections, see RFC 3920 for terminology) is to fail the
  49. SASL negotiation with a C{'incorrect-encoding'} condition. For initiating
  50. entities, one should assume the receiving entity to be either buggy or
  51. malevolent. The stream should be terminated and reconnecting is not
  52. advised.
  53. """
  54. base64Pattern = re.compile("^[0-9A-Za-z+/]*[0-9A-Za-z+/=]{,2}$")
  55. def fromBase64(s):
  56. """
  57. Decode base64 encoded string.
  58. This helper performs regular decoding of a base64 encoded string, but also
  59. rejects any characters that are not in the base64 alphabet and padding
  60. occurring elsewhere from the last or last two characters, as specified in
  61. section 14.9 of RFC 3920. This safeguards against various attack vectors
  62. among which the creation of a covert channel that "leaks" information.
  63. """
  64. if base64Pattern.match(s) is None:
  65. raise SASLIncorrectEncodingError()
  66. try:
  67. return b64decode(s)
  68. except Exception as e:
  69. raise SASLIncorrectEncodingError(str(e))
  70. class SASLInitiatingInitializer(xmlstream.BaseFeatureInitiatingInitializer):
  71. """
  72. Stream initializer that performs SASL authentication.
  73. The supported mechanisms by this initializer are C{DIGEST-MD5}, C{PLAIN}
  74. and C{ANONYMOUS}. The C{ANONYMOUS} SASL mechanism is used when the JID, set
  75. on the authenticator, does not have a localpart (username), requesting an
  76. anonymous session where the username is generated by the server.
  77. Otherwise, C{DIGEST-MD5} and C{PLAIN} are attempted, in that order.
  78. """
  79. feature = (NS_XMPP_SASL, 'mechanisms')
  80. _deferred = None
  81. def setMechanism(self):
  82. """
  83. Select and setup authentication mechanism.
  84. Uses the authenticator's C{jid} and C{password} attribute for the
  85. authentication credentials. If no supported SASL mechanisms are
  86. advertized by the receiving party, a failing deferred is returned with
  87. a L{SASLNoAcceptableMechanism} exception.
  88. """
  89. jid = self.xmlstream.authenticator.jid
  90. password = self.xmlstream.authenticator.password
  91. mechanisms = get_mechanisms(self.xmlstream)
  92. if jid.user is not None:
  93. if 'DIGEST-MD5' in mechanisms:
  94. self.mechanism = sasl_mechanisms.DigestMD5('xmpp', jid.host, None,
  95. jid.user, password)
  96. elif 'PLAIN' in mechanisms:
  97. self.mechanism = sasl_mechanisms.Plain(None, jid.user, password)
  98. else:
  99. raise SASLNoAcceptableMechanism()
  100. else:
  101. if 'ANONYMOUS' in mechanisms:
  102. self.mechanism = sasl_mechanisms.Anonymous()
  103. else:
  104. raise SASLNoAcceptableMechanism()
  105. def start(self):
  106. """
  107. Start SASL authentication exchange.
  108. """
  109. self.setMechanism()
  110. self._deferred = defer.Deferred()
  111. self.xmlstream.addObserver('/challenge', self.onChallenge)
  112. self.xmlstream.addOnetimeObserver('/success', self.onSuccess)
  113. self.xmlstream.addOnetimeObserver('/failure', self.onFailure)
  114. self.sendAuth(self.mechanism.getInitialResponse())
  115. return self._deferred
  116. def sendAuth(self, data=None):
  117. """
  118. Initiate authentication protocol exchange.
  119. If an initial client response is given in C{data}, it will be
  120. sent along.
  121. @param data: initial client response.
  122. @type data: C{str} or L{None}.
  123. """
  124. auth = domish.Element((NS_XMPP_SASL, 'auth'))
  125. auth['mechanism'] = self.mechanism.name
  126. if data is not None:
  127. auth.addContent(b64encode(data).decode('ascii') or u'=')
  128. self.xmlstream.send(auth)
  129. def sendResponse(self, data=b''):
  130. """
  131. Send response to a challenge.
  132. @param data: client response.
  133. @type data: L{bytes}.
  134. """
  135. response = domish.Element((NS_XMPP_SASL, 'response'))
  136. if data:
  137. response.addContent(b64encode(data).decode('ascii'))
  138. self.xmlstream.send(response)
  139. def onChallenge(self, element):
  140. """
  141. Parse challenge and send response from the mechanism.
  142. @param element: the challenge protocol element.
  143. @type element: L{domish.Element}.
  144. """
  145. try:
  146. challenge = fromBase64(unicode(element))
  147. except SASLIncorrectEncodingError:
  148. self._deferred.errback()
  149. else:
  150. self.sendResponse(self.mechanism.getResponse(challenge))
  151. def onSuccess(self, success):
  152. """
  153. Clean up observers, reset the XML stream and send a new header.
  154. @param success: the success protocol element. For now unused, but
  155. could hold additional data.
  156. @type success: L{domish.Element}
  157. """
  158. self.xmlstream.removeObserver('/challenge', self.onChallenge)
  159. self.xmlstream.removeObserver('/failure', self.onFailure)
  160. self.xmlstream.reset()
  161. self.xmlstream.sendHeader()
  162. self._deferred.callback(xmlstream.Reset)
  163. def onFailure(self, failure):
  164. """
  165. Clean up observers, parse the failure and errback the deferred.
  166. @param failure: the failure protocol element. Holds details on
  167. the error condition.
  168. @type failure: L{domish.Element}
  169. """
  170. self.xmlstream.removeObserver('/challenge', self.onChallenge)
  171. self.xmlstream.removeObserver('/success', self.onSuccess)
  172. try:
  173. condition = failure.firstChildElement().name
  174. except AttributeError:
  175. condition = None
  176. self._deferred.errback(SASLAuthError(condition))