From 2c68f485e20646139b61efc77d82f850429de7b3 Mon Sep 17 00:00:00 2001 From: Basraah Date: Thu, 26 Jan 2017 06:10:07 +1000 Subject: [PATCH] Upgrade Mumble password hashing to bcrypt (#671) Added transition to bcrypt-sha256 hashing for mumble passwords. All new passwords will be hashed by bcrypt-sha256. The existing SHA-1 hashes will continue to work as a fallback for legacy password hashes. --- services/modules/mumble/manager.py | 35 ++++++++++--------- .../migrations/0005_mumbleuser_hashfn.py | 25 +++++++++++++ services/modules/mumble/models.py | 3 +- services/modules/mumble/tests.py | 9 ++--- thirdparty/Mumble/authenticator.py | 31 +++++++++++----- 5 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 services/modules/mumble/migrations/0005_mumbleuser_hashfn.py diff --git a/services/modules/mumble/manager.py b/services/modules/mumble/manager.py index 71612ee5..a7841599 100755 --- a/services/modules/mumble/manager.py +++ b/services/modules/mumble/manager.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import random import string -import hashlib +from passlib.hash import bcrypt_sha256 from django.core.exceptions import ObjectDoesNotExist @@ -16,6 +16,8 @@ class MumbleManager: def __init__(self): pass + HASH_FN = 'bcrypt-sha256' + @staticmethod def __santatize_username(username): sanatized = username.replace(" ", "_") @@ -33,24 +35,24 @@ class MumbleManager: def __generate_username_blue(username, corp_ticker): return "[BLUE][" + corp_ticker + "]" + username - @staticmethod - def _gen_pwhash(password): - return hashlib.sha1(password.encode('utf-8')).hexdigest() + @classmethod + def _gen_pwhash(cls, password): + return bcrypt_sha256.encrypt(password.encode('utf-8')) - @staticmethod - def create_user(user, corp_ticker, username, blue=False): + @classmethod + def create_user(cls, user, corp_ticker, username, blue=False): logger.debug("Creating%s mumble user with username %s and ticker %s" % (' blue' if blue else '', username, corp_ticker)) - username_clean = MumbleManager.__santatize_username( - MumbleManager.__generate_username_blue(username, corp_ticker) if blue else - MumbleManager.__generate_username(username, corp_ticker)) - password = MumbleManager.__generate_random_pass() - pwhash = MumbleManager._gen_pwhash(password) + username_clean = cls.__santatize_username( + cls.__generate_username_blue(username, corp_ticker) if blue else + cls.__generate_username(username, corp_ticker)) + password = cls.__generate_random_pass() + pwhash = cls._gen_pwhash(password) logger.debug("Proceeding with mumble user creation: clean username %s, pwhash starts with %s" % ( username_clean, pwhash[0:5])) if not MumbleUser.objects.filter(username=username_clean).exists(): logger.info("Creating mumble user %s" % username_clean) - MumbleUser.objects.create(user=user, username=username_clean, pwhash=pwhash) + MumbleUser.objects.create(user=user, username=username_clean, pwhash=pwhash, hashfn=cls.HASH_FN) return username_clean, password else: logger.warn("Mumble user %s already exists.") @@ -66,16 +68,17 @@ class MumbleManager: logger.error("Unable to delete user %s from mumble: MumbleUser model not found" % user) return False - @staticmethod - def update_user_password(user, password=None): + @classmethod + def update_user_password(cls, user, password=None): logger.debug("Updating mumble user %s password." % user) if not password: - password = MumbleManager.__generate_random_pass() - pwhash = MumbleManager._gen_pwhash(password) + password = cls.__generate_random_pass() + pwhash = cls._gen_pwhash(password) logger.debug("Proceeding with mumble user %s password update - pwhash starts with %s" % (user, pwhash[0:5])) try: model = MumbleUser.objects.get(user=user) model.pwhash = pwhash + model.hashfn = cls.HASH_FN model.save() return password except ObjectDoesNotExist: diff --git a/services/modules/mumble/migrations/0005_mumbleuser_hashfn.py b/services/modules/mumble/migrations/0005_mumbleuser_hashfn.py new file mode 100644 index 00000000..cbf6e02c --- /dev/null +++ b/services/modules/mumble/migrations/0005_mumbleuser_hashfn.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.4 on 2017-01-23 10:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('mumble', '0004_auto_20161214_1024'), + ] + + operations = [ + migrations.AddField( + model_name='mumbleuser', + name='hashfn', + field=models.CharField(default='sha1', max_length=20), + ), + migrations.AlterField( + model_name='mumbleuser', + name='pwhash', + field=models.CharField(max_length=80), + ), + ] diff --git a/services/modules/mumble/models.py b/services/modules/mumble/models.py index 28e303b9..b711790b 100644 --- a/services/modules/mumble/models.py +++ b/services/modules/mumble/models.py @@ -7,7 +7,8 @@ from django.db import models class MumbleUser(models.Model): user = models.OneToOneField('auth.User', related_name='mumble', null=True) username = models.CharField(max_length=254, unique=True) - pwhash = models.CharField(max_length=40) + pwhash = models.CharField(max_length=80) + hashfn = models.CharField(max_length=20, default='sha1') groups = models.TextField(blank=True, null=True) def __str__(self): diff --git a/services/modules/mumble/tests.py b/services/modules/mumble/tests.py index 6b6f0339..b65569c0 100644 --- a/services/modules/mumble/tests.py +++ b/services/modules/mumble/tests.py @@ -19,15 +19,9 @@ from .auth_hooks import MumbleService from .models import MumbleUser from .tasks import MumbleTasks -import hashlib - MODULE_PATH = 'services.modules.mumble' -def gen_pwhash(password): - return hashlib.sha1(password).hexdigest() - - class MumbleHooksTestCase(TestCase): def setUp(self): self.member = 'member_user' @@ -198,4 +192,5 @@ class MumbleManagerTestCase(TestCase): def test_gen_pwhash(self): pwhash = self.manager._gen_pwhash('test') - self.assertEqual(pwhash, 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3') + self.assertEqual(pwhash[:15], '$bcrypt-sha256$') + self.assertEqual(len(pwhash), 75) diff --git a/thirdparty/Mumble/authenticator.py b/thirdparty/Mumble/authenticator.py index 58275a73..4cd3a6ab 100644 --- a/thirdparty/Mumble/authenticator.py +++ b/thirdparty/Mumble/authenticator.py @@ -70,6 +70,8 @@ try: except ImportError: # python 2.4 compat from sha import sha as sha1 +from passlib.hash import bcrypt_sha256 + def eprint(*args, **kwargs): print(*args, file=sys.stderr, **kwargs) @@ -521,7 +523,10 @@ def do_main_program(): return (FALL_THROUGH, None, None) try: - sql = 'SELECT id, pwhash, groups FROM %smumble_mumbleuser WHERE username = %%s' % cfg.database.prefix + sql = 'SELECT id, pwhash, groups, hashfn ' \ + 'FROM %smumble_mumbleuser ' \ + 'WHERE username = %%s' % cfg.database.prefix + cur = threadDB.execute(sql, [name]) except threadDbException: return (FALL_THROUGH, None, None) @@ -532,14 +537,16 @@ def do_main_program(): info('Fall through for unknown user "%s"', name) return (FALL_THROUGH, None, None) - uid, upwhash, ugroups = res + uid, upwhash, ugroups, uhashfn = res if ugroups: groups = ugroups.split(',') else: groups = [] - if allianceauth_check_hash(pw, upwhash): + debug('checking password with hash function: %s' % uhashfn) + + if allianceauth_check_hash(pw, upwhash, uhashfn): info('User authenticated: "%s" (%d)', name, uid + cfg.user.id_offset) debug('Group memberships: %s', str(groups)) return (uid + cfg.user.id_offset, entity_decode(name), groups) @@ -745,14 +752,20 @@ def do_main_program(): info('Shutdown complete') -# -# --- Python implementation of the AllianceAuth MumbleUser hash function -# -def allianceauth_check_hash(password, hash): +def allianceauth_check_hash(password, hash, hash_type): """ - Python implementation of the smf check hash function + Python implementation of the AllianceAuth MumbleUser hash function + :param password: Password to be verified + :param hash: Hash for the password to be checked against + :param hash_type: Hashing function originally used to generate the hash """ - return sha1(password).hexdigest() == hash + if hash_type == 'sha1': + return sha1(password).hexdigest() == hash + elif hash_type == 'bcrypt-sha256': + return bcrypt_sha256.verify(password, hash) + else: + warning("No valid hash function found for %s" % hash_type) + return False #