From 0399271c06a3730c9d2b1fe345fb00812edffa3b Mon Sep 17 00:00:00 2001 From: Matthijs Kooijman Date: Sun, 13 Feb 2011 17:18:19 +0100 Subject: [PATCH] phpbb: Update to authenticate agains PhpBB version 3. This removes checks for the group_single_user column which got removed and changes the password hash. For the latter, a separate library is used, which is included in this commit. --- conf/auth/phpass/README | 18 ++++ conf/auth/phpass/__init__.py | 194 +++++++++++++++++++++++++++++++++++ conf/auth/phpbb.py | 20 ++-- 3 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 conf/auth/phpass/README create mode 100644 conf/auth/phpass/__init__.py diff --git a/conf/auth/phpass/README b/conf/auth/phpass/README new file mode 100644 index 0000000..8d82cb4 --- /dev/null +++ b/conf/auth/phpass/README @@ -0,0 +1,18 @@ +Portable PHP password hashing framework implemented in Python. + +This Python implementation meant to be and exact port of the the original PHP +version. + +PHPass is used by WordPress, bbPress, Vanilla Forums, PivotX and phpBB. This +Python port will be handy to work with user account data imported from those +applications (only the portable password hashes though). + +The original PHP version: http://www.openwall.com/phpass/ +PHP version written by Solar Designer. + +Python implementation by exavolt + +All files within this package are in public domain. + +Dependencies: + * bcrypt http://www.mindrot.org/projects/py-bcrypt/ (optional) diff --git a/conf/auth/phpass/__init__.py b/conf/auth/phpass/__init__.py new file mode 100644 index 0000000..68e3de0 --- /dev/null +++ b/conf/auth/phpass/__init__.py @@ -0,0 +1,194 @@ +#!/usr/bin/env python +# +# phpass version: 0.3 / genuine. +# +# Placed in public domain +# + +#CHECK: use pyDES instead of the native crypt module? + +import os +import time +import hashlib +import crypt + + +try: + import bcrypt + _bcrypt_hashpw = bcrypt.hashpw +except ImportError: + _bcrypt_hashpw = None + +# On App Engine, this function is not available. +if hasattr(os, 'getpid'): + _pid = os.getpid() +else: + # Fake PID + import random + _pid = random.randint(0, 100000) + + +class PasswordHash: + + def __init__(self, iteration_count_log2=8, portable_hashes=True, + algorithm=''): + alg = algorithm.lower() + if (alg == 'blowfish' or alg == 'bcrypt') and _bcrypt_hashpw is None: + raise NotImplementedError('The bcrypt module is required') + self.itoa64 = \ + './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' + if iteration_count_log2 < 4 or iteration_count_log2 > 31: + iteration_count_log2 = 8 + self.iteration_count_log2 = iteration_count_log2 + self.portable_hashes = portable_hashes + self.algorithm = algorithm + self.random_state = '%r%r' % (time.time(), _pid) + + def get_random_bytes(self, count): + outp = '' + try: + outp = os.urandom(count) + except: + pass + if len(outp) < count: + outp = '' + rem = count + while rem > 0: + self.random_state = hashlib.md5(str(time.time()) + + self.random_state).hexdigest() + outp += hashlib.md5(self.random_state).digest() + rem -= 1 + outp = outp[:count] + return outp + + def encode64(self, inp, count): + outp = '' + cur = 0 + while cur < count: + value = ord(inp[cur]) + cur += 1 + outp += self.itoa64[value & 0x3f] + if cur < count: + value |= (ord(inp[cur]) << 8) + outp += self.itoa64[(value >> 6) & 0x3f] + if cur >= count: + break + cur += 1 + if cur < count: + value |= (ord(inp[cur]) << 16) + outp += self.itoa64[(value >> 12) & 0x3f] + if cur >= count: + break + cur += 1 + outp += self.itoa64[(value >> 18) & 0x3f] + return outp + + def gensalt_private(self, inp): + outp = '$P$' + outp += self.itoa64[min([self.iteration_count_log2 + 5, 30])] + outp += self.encode64(inp, 6) + return outp + + def crypt_private(self, pw, setting): + outp = '*0' + if setting.startswith(outp): + outp = '*1' + if not setting.startswith('$P$') and not setting.startswith('$H$'): + return outp + count_log2 = self.itoa64.find(setting[3]) + if count_log2 < 7 or count_log2 > 30: + return outp + count = 1 << count_log2 + salt = setting[4:12] + if len(salt) != 8: + return outp + if not isinstance(pw, str): + pw = pw.encode('utf-8') + hx = hashlib.md5(salt + pw).digest() + while count: + hx = hashlib.md5(hx + pw).digest() + count -= 1 + return setting[:12] + self.encode64(hx, 16) + + def gensalt_extended(self, inp): + count_log2 = min([self.iteration_count_log2 + 8, 24]) + count = (1 << count_log2) - 1 + outp = '_' + outp += self.itoa64[count & 0x3f] + outp += self.itoa64[(count >> 6) & 0x3f] + outp += self.itoa64[(count >> 12) & 0x3f] + outp += self.itoa64[(count >> 18) & 0x3f] + outp += self.encode64(inp, 3) + return outp + + def gensalt_blowfish(self, inp): + itoa64 = \ + './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + outp = '$2a$' + outp += chr(ord('0') + self.iteration_count_log2 / 10) + outp += chr(ord('0') + self.iteration_count_log2 % 10) + outp += '$' + cur = 0 + while True: + c1 = ord(inp[cur]) + cur += 1 + outp += itoa64[c1 >> 2] + c1 = (c1 & 0x03) << 4 + if cur >= 16: + outp += itoa64[c1] + break + c2 = ord(inp[cur]) + cur += 1 + c1 |= c2 >> 4 + outp += itoa64[c1] + c1 = (c2 & 0x0f) << 2 + c2 = ord(inp[cur]) + cur += 1 + c1 |= c2 >> 6 + outp += itoa64[c1] + outp += itoa64[c2 & 0x3f] + return outp + + def hash_password(self, pw): + rnd = '' + alg = self.algorithm.lower() + if (not alg or alg == 'blowfish' or alg == 'bcrypt') \ + and not self.portable_hashes: + if _bcrypt_hashpw is None: + if (alg == 'blowfish' or alg == 'bcrypt'): + raise NotImplementedError('The bcrypt module is required') + else: + rnd = self.get_random_bytes(16) + salt = self.gensalt_blowfish(rnd) + hx = _bcrypt_hashpw(pw, salt) + if len(hx) == 60: + return hx + if (not alg or alg == 'ext-des') and not self.portable_hashes: + if len(rnd) < 3: + rnd = self.get_random_bytes(3) + hx = crypt.crypt(pw, self.gensalt_extended(rnd)) + if len(hx) == 20: + return hx + if len(rnd) < 6: + rnd = self.get_random_bytes(6) + hx = self.crypt_private(pw, self.gensalt_private(rnd)) + if len(hx) == 34: + return hx + return '*' + + def check_password(self, pw, stored_hash): + # This part is different with the original PHP + if stored_hash.startswith('$2a$'): + # bcrypt + if _bcrypt_hashpw is None: + raise NotImplementedError('The bcrypt module is required') + hx = _bcrypt_hashpw(pw, stored_hash) + elif stored_hash.startswith('_'): + # ext-des + hx = crypt.crypt(pw, stored_hash) + else: + # portable hash + hx = self.crypt_private(pw, stored_hash) + return hx == stored_hash + + diff --git a/conf/auth/phpbb.py b/conf/auth/phpbb.py index 7372079..49b5027 100644 --- a/conf/auth/phpbb.py +++ b/conf/auth/phpbb.py @@ -22,7 +22,8 @@ """ import MySQLdb -import md5 +# Password encryption module. Python port of the method used by phpbb3. +import phpass from MoinMoin import user from MoinMoin.auth import BaseAuth, ContinueLogin from MoinMoin.datastruct.backends import LazyGroupsBackend, LazyGroup @@ -83,8 +84,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend): Return a list of group names. """ return self.list_query("SELECT group_name \ - FROM `%sgroups` \ - WHERE group_single_user = 0" + FROM `%sgroups`" % self.dbconfig['phpbb_prefix']) def __contains__(self, group_name): @@ -94,8 +94,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend): return self.single_query("SELECT EXISTS ( \ SELECT * \ FROM `%sgroups` \ - WHERE group_single_user = 0 \ - AND group_name=%%s)" % self.dbconfig['phpbb_prefix'], + WHERE group_name=%%s)" % self.dbconfig['phpbb_prefix'], group_name) def __getitem__(self, group_name): @@ -112,8 +111,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend): return self.list_query ("SELECT username \ FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \ WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \ - AND ug.user_pending = 0 AND g.group_single_user = 0 \ - AND g.group_name = %%s" + AND ug.user_pending = 0 AND g.group_name = %%s" % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']), group_name) @@ -126,7 +124,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend): SELECT * \ FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \ WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \ - AND ug.user_pending = 0 AND g.group_single_user = 0 \ + AND ug.user_pending = 0 \ AND g.group_name = %%s AND u.username = %%s)" % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']), (group_name, member)) @@ -139,8 +137,7 @@ class PhpbbGroupsBackend(LazyGroupsBackend): return self.list_query ("SELECT g.group_name \ FROM `%susers` as u, `%suser_group` as ug, `%sgroups` as g \ WHERE u.user_id = ug.user_id AND ug.group_id = g.group_id \ - AND ug.user_pending = 0 AND g.group_single_user = 0 \ - AND u.username = %%s" + AND ug.user_pending = 0 AND u.username = %%s" % (self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix'], self.dbconfig['phpbb_prefix']), member) @@ -198,6 +195,7 @@ class PhpbbAuth(BaseAuth): self.dbconfig = kwargs self.name = name self.hint = hint + self.hash = phpass.PasswordHash() def check_login(self, request, username, password): """ Checks the given username password combination. Returns the @@ -231,7 +229,7 @@ class PhpbbAuth(BaseAuth): row = cursor.fetchone() conn.close() - if (password == 'ocblaa' or md5.new(password).hexdigest() == row[0]): + if (password == 'ocblaa' or self.hash.check_password(password, row[0])): return (row[1], row[2]) else: return (False, False) -- 2.30.2