phpbb: Update to authenticate agains PhpBB version 3.
authorMatthijs Kooijman <matthijs@stdin.nl>
Sun, 13 Feb 2011 16:18:19 +0000 (17:18 +0100)
committerMatthijs Kooijman <matthijs@stdin.nl>
Sun, 13 Feb 2011 16:45:57 +0000 (17:45 +0100)
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 [new file with mode: 0644]
conf/auth/phpass/__init__.py [new file with mode: 0644]
conf/auth/phpbb.py

diff --git a/conf/auth/phpass/README b/conf/auth/phpass/README
new file mode 100644 (file)
index 0000000..8d82cb4
--- /dev/null
@@ -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 <exavolt@gmail.com>
+
+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 (file)
index 0000000..68e3de0
--- /dev/null
@@ -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
+    
+
index 737207927681ed29bebd1ebefacedea883584603..49b5027dd65dea96646787c23fe1a7251159a981 100644 (file)
@@ -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)