Source code for linotp.lib.phppass

#!/usr/bin/env python
#
# phpass version: 0.3 / genuine.
#
# Placed in public domain
#
# source: https://github.com/exavolt/python-phpass
#

#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
    from linotp.lib.crypt import urandom
    _pid = urandom.randint(0, 100000)


[docs]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)
[docs] 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
[docs] 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
[docs] def gensalt_private(self, inp): outp = '$P$' outp += self.itoa64[min([self.iteration_count_log2 + 5, 30])] outp += self.encode64(inp, 6) return outp
[docs] 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)
[docs] 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
[docs] 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
[docs] 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 '*'
[docs] 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
if __name__ == "__main__": import getpass while True: pw = getpass.getpass() pw2 = getpass.getpass('Retype password: ') if pw == pw2: break print "Both passwords must be the same" t_hasher = PasswordHash(8, True) print "Password hash: " + t_hasher.hash_password(pw)