web2py password encryption/decryption

1,635 views
Skip to first unread message

farmy zdrowia

unread,
May 30, 2014, 10:22:40 AM5/30/14
to web...@googlegroups.com
Hello,
I'm trying to integrate web2py users to be stored in joomla  "_users" database instead of auth_user. I can see joomla and web2py use different algorithm do code/decode passwords.
Joomla password looks like:
  $P$DryHu7D3LgdPOK//FPvuVMcMR13HgU1
, while web2py
  pbkdf2(1000,20,sha512)$a76b573005c73906$01f33be064bd2a283350206fd29355f9fa2b30fe

I'd like to change web2py default algorithm to code/decode passwords to be similar to joomla simply to have common users database.
Could you help a bit and guide me where this function is located and how to change it?

 

Marin Pranjić

unread,
May 30, 2014, 3:29:00 PM5/30/14
to web2py-users
You can change current behavior by changing db.auth_user.password.requires.

This is a default validator:
https://github.com/web2py/web2py/blob/master/gluon/tools.py#L1786-L1787

it's being used here:
https://github.com/web2py/web2py/blob/master/gluon/tools.py#L1850-L1852


Check if you can get desired fromat from CRYPT validator:
https://github.com/web2py/web2py/blob/master/gluon/validators.py#L2850


Marin


--
Resources:
- http://web2py.com
- http://web2py.com/book (Documentation)
- http://github.com/web2py/web2py (Source code)
- https://code.google.com/p/web2py/issues/list (Report Issues)
---
You received this message because you are subscribed to the Google Groups "web2py-users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to web2py+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Massimo Di Pierro

unread,
May 31, 2014, 2:31:27 AM5/31/14
to web...@googlegroups.com
We can help you read and validate Joomla passwords (you need a custom validator instead of CRYPT) but we do not know how:
$P$DryHu7D3LgdPOK//FPvuVMcMR13HgU1
was generated. What algorithm?
It does not appear to be compatible with what the docs say: http://stackoverflow.com/questions/10428126/joomla-password-encryption

In the case of web2py:
pbkdf2(1000,20,sha512)$a76b573005c73906$01f33be064bd2a283350206fd29355f9fa2b30fe

pbkdf2(1000,20,sha512) is the algorithm
a76b573005c73906 is the salt
01f33be064bd2a283350206fd29355f9fa2b30fe is the hashed password+salt.

farmy zdrowia

unread,
Jun 22, 2014, 5:53:53 AM6/22/14
to web...@googlegroups.com

I'm so sorry for late answer. I was out of office/home for a while. Busy time this 2014 I can see :).

Anyway, Massimo is absolutely right. I have two joomla sites.
One "Joomla  _user original" and indeed passwords are according to standard described in link.
Example (c563e965be1369f9030863daca32a544:fwQkHlQqimvzfDBisPZkruuYCTvTsxSU)

Second one is with "Community Builder" module installed. And this is root cause why password handling is different.
I sheel write how to integrate then "Community Builder" and web2py :(





 
 

farmy zdrowia

unread,
Jun 22, 2014, 4:40:32 PM6/22/14
to web...@googlegroups.com
I did kind of investigation by myself.
I can see CB uses new Joomla "Portable PHP password hashing framework" functionality to crypt password. I noticed CB run on joomla 3.2.1,
while my other site is on Joomla 2

Anyway at the end of pasword cryption chain there is a function hashPassword and verifyPassword in libraries/joomla/user/helper.php

abstract class JUserHelper
        public static function hashPassword($password)
        {
                // Use PHPass's portable hashes with a cost of 10.
                $phpass = new PasswordHash(10, true);

                return $phpass->HashPassword($password);
        }


        public static function verifyPassword($password, $hash, $user_id = 0)
        {
                $rehash = false;
                $match = false;

                // If we are using phpass
                if (strpos($hash, '$P$') === 0)
                {
                        // Use PHPass's portable hashes with a cost of 10.
                        $phpass = new PasswordHash(10, true);

                        $match = $phpass->CheckPassword($password, $hash);

                        $rehash = false;
                }
   

Indeed all my passwords starts with "$P$"

Whole algorithm to crypt CB/Joomla3.2.1 password is in file   libraries/phpass/PasswordHash.php



Question now is how to transform it to web2py CUSTOMER validator. I'll need your help




 

Massimo Di Pierro

unread,
Jun 23, 2014, 5:21:42 AM6/23/14
to web...@googlegroups.com
Hello Farmy,

The code you posted helps and this examples the PHP algorithm:

I recorded this in Python:

import random, hashlib

class PHPHash(object):
    CHARS = '0123456789abcdefghijklmoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
    def __init__(self,secret,rounds=10):
        self.secret = secret
        self.rounds = rounds
    def hash(self,password, salt=None):
        if salt is None:
            salt = ''.join(random.choice(self.CHARS) for i in range(8))
        checksum = hashlib.md5(salt+self.secret).hexdigest()
        for k in range(2**self.rounds):
            checksum = hashlib.md5(checksum+password).hexdigest()
        hashed = '$P$%s%s%s' % (chr(self.rounds+ord('0')-5),salt,checksum)
        return hashed

p = PHPHash('mysecret', rounds=13)
print p.hash('mypassword')

Please check it an make sure you can reproduce the PHP passwords. Once that's done we can try implement a custom validator, based on CRYPT that will work with them.





Massimo

farmy zdrowia

unread,
Jun 23, 2014, 11:22:18 PM6/23/14
to web...@googlegroups.com
THX a lot Massimo, it is very much appreciated. I'll check this ASAP. Be patient please.

farmy zdrowia

unread,
Jun 25, 2014, 3:44:22 PM6/25/14
to web...@googlegroups.com
Massimo,
Your code hash password,  but it is not recognized by Joomla. :)

Anyway I have got this class in py from phpass fremwork web page. It works!
Now CUSTOMIZE CRYPT is the last effort. hash_password and check_password shell be used.






import os
import time
import hashlib
import crypt

try:
    import bcrypt
    _bcrypt_hashpw = bcrypt.hashpw
except ImportError:
    _bcrypt_hashpw = None


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(), os.getpid())

    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




phpass = PasswordHash(10, True)

print phpass.hash_password('Nokia123')

Reply all
Reply to author
Forward
0 new messages