[Dave Cridland] basic SCRAM-SHA-1 implementation (no channel binding). Fixes #5622
This commit is contained in:
parent
a51c59b3ba
commit
50c4a2fa42
|
@ -29,6 +29,8 @@ import random
|
||||||
import itertools
|
import itertools
|
||||||
import dispatcher_nb
|
import dispatcher_nb
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
log = logging.getLogger('gajim.c.x.auth_nb')
|
log = logging.getLogger('gajim.c.x.auth_nb')
|
||||||
|
@ -113,6 +115,11 @@ def challenge_splitter(data):
|
||||||
quotes_open = False
|
quotes_open = False
|
||||||
return dict_
|
return dict_
|
||||||
|
|
||||||
|
def scram_parse(chatter):
|
||||||
|
stuff = {}
|
||||||
|
for k, v in [s.split('=', 1) for s in chatter.split(',')]:
|
||||||
|
stuff[k] = v
|
||||||
|
return stuff
|
||||||
|
|
||||||
class SASL(PlugIn):
|
class SASL(PlugIn):
|
||||||
"""
|
"""
|
||||||
|
@ -231,6 +238,13 @@ class SASL(PlugIn):
|
||||||
raise NodeProcessed
|
raise NodeProcessed
|
||||||
except kerberos.GSSError, e:
|
except kerberos.GSSError, e:
|
||||||
log.info('GSSAPI authentication failed: %s' % str(e))
|
log.info('GSSAPI authentication failed: %s' % str(e))
|
||||||
|
if 'SCRAM-SHA-1' in self.mecs:
|
||||||
|
self.mecs.remove('SCRAM-SHA-1')
|
||||||
|
self.mechanism = 'SCRAM-SHA-1'
|
||||||
|
self._owner._caller.get_password(self.set_password)
|
||||||
|
self.scram_step = 0
|
||||||
|
self.startsasl = SASL_IN_PROCESS
|
||||||
|
raise NodeProcessed
|
||||||
if 'DIGEST-MD5' in self.mecs:
|
if 'DIGEST-MD5' in self.mecs:
|
||||||
self.mecs.remove('DIGEST-MD5')
|
self.mecs.remove('DIGEST-MD5')
|
||||||
node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'})
|
node = Node('auth', attrs={'xmlns': NS_SASL, 'mechanism': 'DIGEST-MD5'})
|
||||||
|
@ -245,8 +259,8 @@ class SASL(PlugIn):
|
||||||
self.startsasl = SASL_IN_PROCESS
|
self.startsasl = SASL_IN_PROCESS
|
||||||
raise NodeProcessed
|
raise NodeProcessed
|
||||||
self.startsasl = SASL_FAILURE
|
self.startsasl = SASL_FAILURE
|
||||||
log.info('I can only use EXTERNAL, DIGEST-MD5, GSSAPI and PLAIN '
|
log.info('I can only use EXTERNAL, SCRAM-SHA-1, DIGEST-MD5, GSSAPI and '
|
||||||
'mecanisms.')
|
'PLAIN mecanisms.')
|
||||||
if self.on_sasl:
|
if self.on_sasl:
|
||||||
self.on_sasl()
|
self.on_sasl()
|
||||||
return
|
return
|
||||||
|
@ -273,6 +287,8 @@ class SASL(PlugIn):
|
||||||
self.on_sasl()
|
self.on_sasl()
|
||||||
raise NodeProcessed
|
raise NodeProcessed
|
||||||
elif challenge.getName() == 'success':
|
elif challenge.getName() == 'success':
|
||||||
|
# TODO: Need to validate any data-with-success.
|
||||||
|
# TODO: Important for DIGEST-MD5 and SCRAM.
|
||||||
self.startsasl = SASL_SUCCESS
|
self.startsasl = SASL_SUCCESS
|
||||||
log.info('Successfully authenticated with remote server.')
|
log.info('Successfully authenticated with remote server.')
|
||||||
handlers = self._owner.Dispatcher.dumpHandlers()
|
handlers = self._owner.Dispatcher.dumpHandlers()
|
||||||
|
@ -310,9 +326,69 @@ class SASL(PlugIn):
|
||||||
response = kerberos.authGSSClientResponse(self.gss_vc)
|
response = kerberos.authGSSClientResponse(self.gss_vc)
|
||||||
if not response:
|
if not response:
|
||||||
response = ''
|
response = ''
|
||||||
self._owner.send(Node('response', attrs={'xmlns':NS_SASL},
|
self._owner.send(Node('response', attrs={'xmlns': NS_SASL},
|
||||||
payload=response).__str__())
|
payload=response).__str__())
|
||||||
raise NodeProcessed
|
raise NodeProcessed
|
||||||
|
if self.mechanism == 'SCRAM-SHA-1':
|
||||||
|
hashfn = hashlib.sha1
|
||||||
|
def HMAC(k, s):
|
||||||
|
return hmac.HMAC(key=k, msg=s, digestmod=hashfn).digest()
|
||||||
|
def XOR(x, y):
|
||||||
|
r = []
|
||||||
|
for i in range(len(x)):
|
||||||
|
r.append(chr(ord(x[i]) ^ ord(y[i])))
|
||||||
|
return ''.join(r)
|
||||||
|
def Hi(s, salt, iters):
|
||||||
|
ii = 1
|
||||||
|
p = s
|
||||||
|
try:
|
||||||
|
p = s.encode('utf-8')
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
ui_1 = HMAC(p, salt + '\0\0\0\01')
|
||||||
|
ui = ui_1
|
||||||
|
for i in range(iters - 1):
|
||||||
|
ii += 1
|
||||||
|
ui_1 = HMAC(p, ui_1)
|
||||||
|
ui = XOR(ui, ui_1)
|
||||||
|
return ui
|
||||||
|
def H(s):
|
||||||
|
return hashfn(s).digest()
|
||||||
|
def scram_base64(s):
|
||||||
|
return ''.join(s.encode('base64').split('\n'))
|
||||||
|
if self.scram_step == 0:
|
||||||
|
self.scram_step = 1
|
||||||
|
self.scram_soup += ',' + data + ','
|
||||||
|
data = scram_parse(data)
|
||||||
|
# TODO: Should check cnonce here.
|
||||||
|
# TODO: Channel binding data goes in here too.
|
||||||
|
r = 'c=' + scram_base64(self.scram_gs2)
|
||||||
|
r += ',r=' + data['r']
|
||||||
|
self.scram_soup += r
|
||||||
|
salt = data['s'].decode('base64')
|
||||||
|
iter = int(data['i'])
|
||||||
|
SaltedPassword = Hi(self.password, salt, iter)
|
||||||
|
# TODO: Could cache this, along with salt+iter.
|
||||||
|
ClientKey = HMAC(SaltedPassword, 'Client Key')
|
||||||
|
StoredKey = H(ClientKey)
|
||||||
|
ClientSignature = HMAC(StoredKey, self.scram_soup)
|
||||||
|
ClientProof = XOR(ClientKey, ClientSignature)
|
||||||
|
r += ',p=' + scram_base64(ClientProof)
|
||||||
|
ServerKey = HMAC(SaltedPassword, 'Server Key')
|
||||||
|
self.scram_ServerSignature = HMAC(ServerKey, self.scram_soup)
|
||||||
|
sasl_data = scram_base64(r)
|
||||||
|
node = Node('response', attrs={'xmlns': NS_SASL},
|
||||||
|
payload=[sasl_data])
|
||||||
|
self._owner.send(str(node))
|
||||||
|
raise NodeProcessed
|
||||||
|
if self.scram_step == 1:
|
||||||
|
data = scram_parse(data)
|
||||||
|
if data['v'].decode('base64') != self.scram_ServerSignature:
|
||||||
|
# TODO: Not clear what to do here - need to abort.
|
||||||
|
raise 'Hell'
|
||||||
|
node = Node('response', attrs={'xmlns': NS_SASL});
|
||||||
|
self._owner.send(str(node))
|
||||||
|
raise NodeProcessed
|
||||||
|
|
||||||
# magic foo...
|
# magic foo...
|
||||||
chal = challenge_splitter(data)
|
chal = challenge_splitter(data)
|
||||||
|
@ -350,7 +426,16 @@ class SASL(PlugIn):
|
||||||
self.password = ''
|
self.password = ''
|
||||||
else:
|
else:
|
||||||
self.password = password
|
self.password = password
|
||||||
if self.mechanism == 'DIGEST-MD5':
|
if self.mechanism == 'SCRAM-SHA-1':
|
||||||
|
nonce = ''.join('%x' % randint(0, 2**28) for randint in \
|
||||||
|
itertools.repeat(random.randint, 7))
|
||||||
|
self.scram_soup = 'n=' + self.username + ',r=' + nonce
|
||||||
|
self.scram_gs2 = 'n,,' # No CB yet.
|
||||||
|
sasl_data = (self.scram_gs2 + self.scram_soup).encode('base64').\
|
||||||
|
replace('\n','')
|
||||||
|
node = Node('auth', attrs={'xmlns': NS_SASL,
|
||||||
|
'mechanism': self.mechanism}, payload=[sasl_data])
|
||||||
|
elif self.mechanism == 'DIGEST-MD5':
|
||||||
def convert_to_iso88591(string):
|
def convert_to_iso88591(string):
|
||||||
try:
|
try:
|
||||||
string = string.decode('utf-8').encode('iso-8859-1')
|
string = string.decode('utf-8').encode('iso-8859-1')
|
||||||
|
|
Loading…
Reference in New Issue