mechanisms.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550
  1. # -*- coding: utf-8 -*-
  2. """
  3. sleekxmpp.util.sasl.mechanisms
  4. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  5. A collection of supported SASL mechanisms.
  6. This module was originally based on Dave Cridland's Suelta library.
  7. Part of SleekXMPP: The Sleek XMPP Library
  8. :copryight: (c) 2004-2013 David Alan Cridland
  9. :copyright: (c) 2013 Nathanael C. Fritz, Lance J.T. Stout
  10. :license: MIT, see LICENSE for more details
  11. """
  12. import sys
  13. import hmac
  14. import random
  15. from base64 import b64encode, b64decode
  16. from sleekxmpp.util import bytes, hash, XOR, quote, num_to_bytes
  17. from sleekxmpp.util.sasl.client import sasl_mech, Mech, \
  18. SASLCancelled, SASLFailed, \
  19. SASLMutualAuthFailed
  20. @sasl_mech(0)
  21. class ANONYMOUS(Mech):
  22. name = 'ANONYMOUS'
  23. def process(self, challenge=b''):
  24. return b'Anonymous, Suelta'
  25. @sasl_mech(1)
  26. class LOGIN(Mech):
  27. name = 'LOGIN'
  28. required_credentials = set(['username', 'password'])
  29. def setup(self, name):
  30. self.step = 0
  31. def process(self, challenge=b''):
  32. if not challenge:
  33. return b''
  34. if self.step == 0:
  35. self.step = 1
  36. return self.credentials['username']
  37. else:
  38. return self.credentials['password']
  39. @sasl_mech(2)
  40. class PLAIN(Mech):
  41. name = 'PLAIN'
  42. required_credentials = set(['username', 'password'])
  43. optional_credentials = set(['authzid'])
  44. security = set(['encrypted', 'encrypted_plain', 'unencrypted_plain'])
  45. def setup(self, name):
  46. if not self.security_settings['encrypted']:
  47. if not self.security_settings['unencrypted_plain']:
  48. raise SASLCancelled('PLAIN without encryption')
  49. else:
  50. if not self.security_settings['encrypted_plain']:
  51. raise SASLCancelled('PLAIN with encryption')
  52. def process(self, challenge=b''):
  53. authzid = self.credentials['authzid']
  54. authcid = self.credentials['username']
  55. password = self.credentials['password']
  56. return authzid + b'\x00' + authcid + b'\x00' + password
  57. @sasl_mech(100)
  58. class EXTERNAL(Mech):
  59. name = 'EXTERNAL'
  60. optional_credentials = set(['authzid'])
  61. def process(self, challenge=b''):
  62. return self.credentials['authzid']
  63. @sasl_mech(31)
  64. class X_FACEBOOK_PLATFORM(Mech):
  65. name = 'X-FACEBOOK-PLATFORM'
  66. required_credentials = set(['api_key', 'access_token'])
  67. def process(self, challenge=b''):
  68. if challenge:
  69. values = {}
  70. for kv in challenge.split(b'&'):
  71. key, value = kv.split(b'=')
  72. values[key] = value
  73. resp_data = {
  74. b'method': values[b'method'],
  75. b'v': b'1.0',
  76. b'call_id': b'1.0',
  77. b'nonce': values[b'nonce'],
  78. b'access_token': self.credentials['access_token'],
  79. b'api_key': self.credentials['api_key']
  80. }
  81. resp = '&'.join(['%s=%s' % (k.decode("utf-8"), v.decode("utf-8")) for k, v in resp_data.items()])
  82. return bytes(resp)
  83. return b''
  84. @sasl_mech(10)
  85. class X_MESSENGER_OAUTH2(Mech):
  86. name = 'X-MESSENGER-OAUTH2'
  87. required_credentials = set(['access_token'])
  88. def process(self, challenge=b''):
  89. return self.credentials['access_token']
  90. @sasl_mech(10)
  91. class X_OAUTH2(Mech):
  92. name = 'X-OAUTH2'
  93. required_credentials = set(['username', 'access_token'])
  94. def process(self, challenge=b''):
  95. return b'\x00' + self.credentials['username'] + \
  96. b'\x00' + self.credentials['access_token']
  97. @sasl_mech(3)
  98. class X_GOOGLE_TOKEN(Mech):
  99. name = 'X-GOOGLE-TOKEN'
  100. required_credentials = set(['email', 'access_token'])
  101. def process(self, challenge=b''):
  102. email = self.credentials['email']
  103. token = self.credentials['access_token']
  104. return b'\x00' + email + b'\x00' + token
  105. @sasl_mech(20)
  106. class CRAM(Mech):
  107. name = 'CRAM'
  108. use_hashes = True
  109. required_credentials = set(['username', 'password'])
  110. security = set(['encrypted', 'unencrypted_cram'])
  111. def setup(self, name):
  112. self.hash_name = name[5:]
  113. self.hash = hash(self.hash_name)
  114. if self.hash is None:
  115. raise SASLCancelled('Unknown hash: %s' % self.hash_name)
  116. if not self.security_settings['encrypted']:
  117. if not self.security_settings['unencrypted_cram']:
  118. raise SASLCancelled('Unecrypted CRAM-%s' % self.hash_name)
  119. def process(self, challenge=b''):
  120. if not challenge:
  121. return None
  122. username = self.credentials['username']
  123. password = self.credentials['password']
  124. mac = hmac.HMAC(key=password, digestmod=self.hash)
  125. mac.update(challenge)
  126. return username + b' ' + bytes(mac.hexdigest())
  127. @sasl_mech(60)
  128. class SCRAM(Mech):
  129. name = 'SCRAM'
  130. use_hashes = True
  131. channel_binding = True
  132. required_credentials = set(['username', 'password'])
  133. optional_credentials = set(['authzid', 'channel_binding'])
  134. security = set(['encrypted', 'unencrypted_scram'])
  135. def setup(self, name):
  136. self.use_channel_binding = False
  137. if name[-5:] == '-PLUS':
  138. name = name[:-5]
  139. self.use_channel_binding = True
  140. self.hash_name = name[6:]
  141. self.hash = hash(self.hash_name)
  142. if self.hash is None:
  143. raise SASLCancelled('Unknown hash: %s' % self.hash_name)
  144. if not self.security_settings['encrypted']:
  145. if not self.security_settings['unencrypted_scram']:
  146. raise SASLCancelled('Unencrypted SCRAM')
  147. self.step = 0
  148. self._mutual_auth = False
  149. def HMAC(self, key, msg):
  150. return hmac.HMAC(key=key, msg=msg, digestmod=self.hash).digest()
  151. def Hi(self, text, salt, iterations):
  152. text = bytes(text)
  153. ui1 = self.HMAC(text, salt + b'\0\0\0\01')
  154. ui = ui1
  155. for i in range(iterations - 1):
  156. ui1 = self.HMAC(text, ui1)
  157. ui = XOR(ui, ui1)
  158. return ui
  159. def H(self, text):
  160. return self.hash(text).digest()
  161. def saslname(self, value):
  162. value = value.decode("utf-8")
  163. escaped = []
  164. for char in value:
  165. if char == ',':
  166. escaped += '=2C'
  167. elif char == '=':
  168. escaped += '=3D'
  169. else:
  170. escaped += char
  171. return "".join(escaped).encode("utf-8")
  172. def parse(self, challenge):
  173. items = {}
  174. for key, value in [item.split(b'=', 1) for item in challenge.split(b',')]:
  175. items[key] = value
  176. return items
  177. def process(self, challenge=b''):
  178. steps = [self.process_1, self.process_2, self.process_3]
  179. return steps[self.step](challenge)
  180. def process_1(self, challenge):
  181. self.step = 1
  182. data = {}
  183. self.cnonce = bytes(('%s' % random.random())[2:])
  184. gs2_cbind_flag = b'n'
  185. if self.credentials['channel_binding']:
  186. if self.use_channel_binding:
  187. gs2_cbind_flag = b'p=tls-unique'
  188. else:
  189. gs2_cbind_flag = b'y'
  190. authzid = b''
  191. if self.credentials['authzid']:
  192. authzid = b'a=' + self.saslname(self.credentials['authzid'])
  193. self.gs2_header = gs2_cbind_flag + b',' + authzid + b','
  194. nonce = b'r=' + self.cnonce
  195. username = b'n=' + self.saslname(self.credentials['username'])
  196. self.client_first_message_bare = username + b',' + nonce
  197. self.client_first_message = self.gs2_header + \
  198. self.client_first_message_bare
  199. return self.client_first_message
  200. def process_2(self, challenge):
  201. self.step = 2
  202. data = self.parse(challenge)
  203. if b'm' in data:
  204. raise SASLCancelled('Received reserved attribute.')
  205. salt = b64decode(data[b's'])
  206. iteration_count = int(data[b'i'])
  207. nonce = data[b'r']
  208. if nonce[:len(self.cnonce)] != self.cnonce:
  209. raise SASLCancelled('Invalid nonce')
  210. cbind_data = b''
  211. if self.use_channel_binding:
  212. cbind_data = self.credentials['channel_binding']
  213. cbind_input = self.gs2_header + cbind_data
  214. channel_binding = b'c=' + b64encode(cbind_input).replace(b'\n', b'')
  215. client_final_message_without_proof = channel_binding + b',' + \
  216. b'r=' + nonce
  217. salted_password = self.Hi(self.credentials['password'],
  218. salt,
  219. iteration_count)
  220. client_key = self.HMAC(salted_password, b'Client Key')
  221. stored_key = self.H(client_key)
  222. auth_message = self.client_first_message_bare + b',' + \
  223. challenge + b',' + \
  224. client_final_message_without_proof
  225. client_signature = self.HMAC(stored_key, auth_message)
  226. client_proof = XOR(client_key, client_signature)
  227. server_key = self.HMAC(salted_password, b'Server Key')
  228. self.server_signature = self.HMAC(server_key, auth_message)
  229. client_final_message = client_final_message_without_proof + \
  230. b',p=' + b64encode(client_proof)
  231. return client_final_message
  232. def process_3(self, challenge):
  233. data = self.parse(challenge)
  234. verifier = data.get(b'v', None)
  235. error = data.get(b'e', 'Unknown error')
  236. if not verifier:
  237. raise SASLFailed(error)
  238. if b64decode(verifier) != self.server_signature:
  239. raise SASLMutualAuthFailed()
  240. self._mutual_auth = True
  241. return b''
  242. @sasl_mech(30)
  243. class DIGEST(Mech):
  244. name = 'DIGEST'
  245. use_hashes = True
  246. required_credentials = set(['username', 'password', 'realm', 'service', 'host'])
  247. optional_credentials = set(['authzid', 'service-name'])
  248. security = set(['encrypted', 'unencrypted_digest'])
  249. def setup(self, name):
  250. self.hash_name = name[7:]
  251. self.hash = hash(self.hash_name)
  252. if self.hash is None:
  253. raise SASLCancelled('Unknown hash: %s' % self.hash_name)
  254. if not self.security_settings['encrypted']:
  255. if not self.security_settings['unencrypted_digest']:
  256. raise SASLCancelled('Unencrypted DIGEST')
  257. self.qops = [b'auth']
  258. self.qop = b'auth'
  259. self.maxbuf = b'65536'
  260. self.nonce = b''
  261. self.cnonce = b''
  262. self.nonce_count = 1
  263. def parse(self, challenge=b''):
  264. data = {}
  265. var_name = b''
  266. var_value = b''
  267. # States: var, new_var, end, quote, escaped_quote
  268. state = 'var'
  269. for char in challenge:
  270. if sys.version_info >= (3, 0):
  271. char = bytes([char])
  272. if state == 'var':
  273. if char.isspace():
  274. continue
  275. if char == b'=':
  276. state = 'value'
  277. else:
  278. var_name += char
  279. elif state == 'value':
  280. if char == b'"':
  281. state = 'quote'
  282. elif char == b',':
  283. if var_name:
  284. data[var_name.decode('utf-8')] = var_value
  285. var_name = b''
  286. var_value = b''
  287. state = 'var'
  288. else:
  289. var_value += char
  290. elif state == 'escaped':
  291. var_value += char
  292. elif state == 'quote':
  293. if char == b'\\':
  294. state = 'escaped'
  295. elif char == b'"':
  296. state = 'end'
  297. else:
  298. var_value += char
  299. else:
  300. if char == b',':
  301. if var_name:
  302. data[var_name.decode('utf-8')] = var_value
  303. var_name = b''
  304. var_value = b''
  305. state = 'var'
  306. else:
  307. var_value += char
  308. if var_name:
  309. data[var_name.decode('utf-8')] = var_value
  310. var_name = b''
  311. var_value = b''
  312. state = 'var'
  313. return data
  314. def MAC(self, key, seq, msg):
  315. mac = hmac.HMAC(key=key, digestmod=self.hash)
  316. seqnum = num_to_bytes(seq)
  317. mac.update(seqnum)
  318. mac.update(msg)
  319. return mac.digest()[:10] + b'\x00\x01' + seqnum
  320. def A1(self):
  321. username = self.credentials['username']
  322. password = self.credentials['password']
  323. authzid = self.credentials['authzid']
  324. realm = self.credentials['realm']
  325. a1 = self.hash()
  326. a1.update(username + b':' + realm + b':' + password)
  327. a1 = a1.digest()
  328. a1 += b':' + self.nonce + b':' + self.cnonce
  329. if authzid:
  330. a1 += b':' + authzid
  331. return bytes(a1)
  332. def A2(self, prefix=b''):
  333. a2 = prefix + b':' + self.digest_uri()
  334. if self.qop in (b'auth-int', b'auth-conf'):
  335. a2 += b':00000000000000000000000000000000'
  336. return bytes(a2)
  337. def response(self, prefix=b''):
  338. nc = bytes('%08x' % self.nonce_count)
  339. a1 = bytes(self.hash(self.A1()).hexdigest().lower())
  340. a2 = bytes(self.hash(self.A2(prefix)).hexdigest().lower())
  341. s = self.nonce + b':' + nc + b':' + self.cnonce + \
  342. b':' + self.qop + b':' + a2
  343. return bytes(self.hash(a1 + b':' + s).hexdigest().lower())
  344. def digest_uri(self):
  345. serv_type = self.credentials['service']
  346. serv_name = self.credentials['service-name']
  347. host = self.credentials['host']
  348. uri = serv_type + b'/' + host
  349. if serv_name and host != serv_name:
  350. uri += b'/' + serv_name
  351. return uri
  352. def respond(self):
  353. data = {
  354. 'username': quote(self.credentials['username']),
  355. 'authzid': quote(self.credentials['authzid']),
  356. 'realm': quote(self.credentials['realm']),
  357. 'nonce': quote(self.nonce),
  358. 'cnonce': quote(self.cnonce),
  359. 'nc': bytes('%08x' % self.nonce_count),
  360. 'qop': self.qop,
  361. 'digest-uri': quote(self.digest_uri()),
  362. 'response': self.response(b'AUTHENTICATE'),
  363. 'maxbuf': self.maxbuf,
  364. 'charset': 'utf-8'
  365. }
  366. resp = b''
  367. for key, value in data.items():
  368. if value and value != b'""':
  369. resp += b',' + bytes(key) + b'=' + bytes(value)
  370. return resp[1:]
  371. def process(self, challenge=b''):
  372. if not challenge:
  373. if self.cnonce and self.nonce and self.nonce_count and self.qop:
  374. self.nonce_count += 1
  375. return self.respond()
  376. return None
  377. data = self.parse(challenge)
  378. if 'rspauth' in data:
  379. if data['rspauth'] != self.response():
  380. raise SASLMutualAuthFailed()
  381. else:
  382. self.nonce_count = 1
  383. self.cnonce = bytes('%s' % random.random())[2:]
  384. self.qops = data.get('qop', [b'auth'])
  385. self.qop = b'auth'
  386. if 'nonce' in data:
  387. self.nonce = data['nonce']
  388. if 'realm' in data and not self.credentials['realm']:
  389. self.credentials['realm'] = data['realm']
  390. return self.respond()
  391. try:
  392. import kerberos
  393. except ImportError:
  394. pass
  395. else:
  396. @sasl_mech(75)
  397. class GSSAPI(Mech):
  398. name = 'GSSAPI'
  399. required_credentials = set(['username', 'service-name'])
  400. optional_credentials = set(['authzid'])
  401. def setup(self, name):
  402. authzid = self.credentials['authzid']
  403. if not authzid:
  404. authzid = 'xmpp@%s' % self.credentials['service-name']
  405. _, self.gss = kerberos.authGSSClientInit(authzid)
  406. self.step = 0
  407. def process(self, challenge=b''):
  408. b64_challenge = b64encode(challenge)
  409. try:
  410. if self.step == 0:
  411. result = kerberos.authGSSClientStep(self.gss, b64_challenge)
  412. if result != kerberos.AUTH_GSS_CONTINUE:
  413. self.step = 1
  414. elif not challenge:
  415. kerberos.authGSSClientClean(self.gss)
  416. return b''
  417. elif self.step == 1:
  418. username = self.credentials['username']
  419. kerberos.authGSSClientUnwrap(self.gss, b64_challenge)
  420. resp = kerberos.authGSSClientResponse(self.gss)
  421. kerberos.authGSSClientWrap(self.gss, resp, username)
  422. resp = kerberos.authGSSClientResponse(self.gss)
  423. except kerberos.GSSError as e:
  424. raise SASLCancelled('Kerberos error: %s' % e)
  425. if not resp:
  426. return b''
  427. else:
  428. return b64decode(resp)