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.
This commit is contained in:
Basraah 2017-01-26 06:10:07 +10:00 committed by Adarnof
parent 11d52d476c
commit 2c68f485e2
5 changed files with 70 additions and 33 deletions

View File

@ -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:

View File

@ -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),
),
]

View File

@ -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):

View File

@ -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)

View File

@ -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
#