diff --git a/setup.py b/setup.py index 61e6210..503b568 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ author_email='me+zirc@lukej.me', license='GNU', packages=find_packages(), - install_requires=['six'], + install_requires=['six', 'pyxmpp2_scram>=2.0.2'], extras_require={ 'pysocks': ['pysocks'], 'pysocks:sys_platform=="win32" and python_version == "2.7"': ['win_inet_pton'] diff --git a/zirc/ext/__init__.py b/zirc/ext/__init__.py index e69de29..e397f85 100644 --- a/zirc/ext/__init__.py +++ b/zirc/ext/__init__.py @@ -0,0 +1 @@ +from . import sasl diff --git a/zirc/ext/sasl.py b/zirc/ext/sasl/__init__.py similarity index 63% rename from zirc/ext/sasl.py rename to zirc/ext/sasl/__init__.py index d0af0e4..6a10d0e 100644 --- a/zirc/ext/sasl.py +++ b/zirc/ext/sasl/__init__.py @@ -1,7 +1,8 @@ # SASL authentication for zirc import base64 -from ..errors import SASLError +from . import scram +from ...errors import SASLError class Sasl(object): name = "sasl" @@ -11,10 +12,11 @@ def __init__(self, username, password, method="plain"): self.password = password self.method = method self.retries = 0 + self.sasl_scram_state = {'step': 'uninitialized'} def run(self, bot, args=None): if args is None: - mechanisms = ["EXTERNAL", "PLAIN"] + mechanisms = ["SCRAM-SHA256-PLUS", "SCRAM-SHA256", "EXTERNAL", "PLAIN"] else: mechanisms = args self.bot = bot @@ -22,7 +24,7 @@ def run(self, bot, args=None): bot.listen(self.on_saslfailed, "saslfailed") bot.listen(self.on_saslsuccess, "saslsuccess") if self.method.upper() in mechanisms: - if self.method in ["plain", "external"]: + if self.method in ["scram-sha256-plus", "scram-sha256", "plain", "external"]: bot.send("AUTHENTICATE " + self.method.upper()) else: raise SASLError("Not implemented yet") @@ -35,12 +37,29 @@ def on_authenticate(self, event): password = base64.b64encode("{0}\x00{0}\x00{1}".format(self.username, self.password).encode("UTF-8")).decode("UTF-8") elif self.method == 'external': password = "+" + elif self.method.startswith('scram-sha'): + scram.doAuthenticateScramFirst(self, self.method) self.bot.send("AUTHENTICATE {0}".format(password)) + else: + step = self.sasl_scram_state['step'] + string = event.arguments[0] + try: + if step == 'first-sent': + scram.doAuthenticateScramChallenge(self, string) + elif step == 'final-sent': + scram.doAuthenticateScramFinish(self, string) + elif step == "authenticated": + self.bot.send("AUTHENTICATE +") + else: + assert False + except scram.ScramException: + bot.send('AUTHENTICATE *') + self.retries += 2 def on_saslfailed(self, event): self.retries += 1 if self.method == 'external': - if self.retries == 2: + if self.retries >= 2: self.retries = 1 self.method = 'plain' self.bot.send("AUTHENTICATE PLAIN") diff --git a/zirc/ext/sasl/scram.py b/zirc/ext/sasl/scram.py new file mode 100644 index 0000000..a4327f6 --- /dev/null +++ b/zirc/ext/sasl/scram.py @@ -0,0 +1,33 @@ +import pyxmpp2_scram as scram + +def doAuthenticateScramFirst(self, mechanism): + """Handle sending the client-first message of SCRAM auth.""" + hash_name = mechanism[len('scram-'):] + if hash_name.endswith('-plus'): + hash_name = hash_name[:-len('-plus')] + hash_name = hash_name.upper() + if hash_name not in scram.HASH_FACTORIES: + self.retries += 2 + return + authenticator = scram.SCRAMClientAuthenticator(hash_name, channel_binding=False) + self.sasl_scram_state['authenticator'] = authenticator + client_first = authenticator.start({ + 'username': self.sasl_username, + 'password': self.sasl_password, + }) + self.bot.send("AUTHENTICATE {0}".format(client_first)) + self.sasl_scram_state['step'] = 'first-sent' + +def doAuthenticateScramChallenge(self, challenge): + client_final = self.sasl_scram_state['authenticator'].challenge(challenge) + self.bot.send("AUTHENTICATE {0}".format(client_final)) + self.sasl_scram_state['step'] = 'final-sent' + +def doAuthenticateScramFinish(self, data): + try: + res = self.sasl_scram_state['authenticator'].finish(data) + except scram.BadSuccessException: + self.retries += 2 + else: + self.sasl_scram_state['step'] = 'authenticated' + self.bot.send("AUTHENTICATE +")