view MoinMoin/util/ @ 174:e8f61cbd661b

modularize crypto/random stuff, move it to MoinMoin.util.crypto, pw change bugfix on password change, the new password was not saved to the profile User / crypto code: minor optimizations / refactorings crypto module contents: password hashing/encryption, validation, pw hash upgrades: * pw_hash = crypt_password(password) * is_valid = valid_password(password, pw_hash) * upgraded_pw_hash = upgrade_password(password, pw_hash) password recovery: * key, token = generate_token() * is_valid = valid_token(key, token) random strings: * rs = random_string(length, chars) compute ascii cache keys: * key = cache_key(**kw)
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Thu, 07 Apr 2011 20:56:35 +0200
children 316c839a6f62
line wrap: on
line source
# Copyright: 2000-2004 Juergen Hermann <>
# Copyright: 2003-2011 MoinMoin:ThomasWaldmann
# Copyright: 2007 MoinMoin:JohannesBerg
# Copyright: 2007 MoinMoin:HeinrichWendel
# Copyright: 2008 MoinMoin:ChristopherDenter
# Copyright: 2010 MoinMoin:DiogenesAugusto
# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.

MoinMoin - Cryptographic and random functions

from __future__ import absolute_import, division

import base64
import hashlib
import hmac
import random

    import crypt
except ImportError:
    crypt = None

from . import md5crypt

# random stuff

def random_string(length, allowed_chars=None):
    Generate a random string with given length consisting of the given characters.

    :param length: length of the string
    :param allowed_chars: string with allowed characters or None
                          to indicate all 256 byte values should be used
    :returns: random string
    if allowed_chars is None:
        s = ''.join([chr(random.randint(0, 255)) for dummy in xrange(length)])
        s = ''.join([random.choice(allowed_chars) for dummy in xrange(length)])
    return s

# password stuff

def crypt_password(password, salt=None):
    Crypt/Hash a cleartext password

    :param password: cleartext password [unicode]
    :param salt: salt for the password [str] or None to generate a random salt
    :rtype: str
    :returns: the SSHA256 password hash
    password = password.encode('utf-8')
    if salt is None:
        salt = random_string(32)
    assert isinstance(salt, str)
    h ='sha256', password)
    return '{SSHA256}' + base64.encodestring(h.digest() + salt).rstrip()

def upgrade_password(password, pw_hash):
    Upgrade a password to a better hash, if needed

    :param password: cleartext password [unicode]
    :param pw_hash: password hash (with hash type prefix)
    :rtype: str
    :returns: new password hash (or None, if unchanged)
    if not pw_hash.startswith('{SSHA256}'):
        # pw_hash using some old hash method, upgrade to better method
        return crypt_password(password)

def valid_password(password, pw_hash):
    Validate a user password.

    :param password: cleartext password to verify [unicode]
    :param pw_hash: password hash (with hash type prefix)
    :rtype: bool
    :returns: password is valid
    # encode password
    pw_utf8 = password.encode('utf-8')

    methods = ['{SSHA256}', '{SSHA}', '{SHA}', '{APR1}', '{MD5}', ]
    if crypt:
        # we have the crypt module

    for method in methods:
        if pw_hash.startswith(method):
            d = pw_hash[len(method):]
            if method == '{SSHA256}':
                ph = base64.decodestring(d)
                # ph is of the form "<hash><salt>"
                salt = ph[32:]
                h ='sha256', pw_utf8)
                enc = base64.encodestring(h.digest() + salt).rstrip()
            elif method == '{SSHA}':
                ph = base64.decodestring(d)
                # ph is of the form "<hash><salt>"
                salt = ph[20:]
                h ='sha1', pw_utf8)
                enc = base64.encodestring(h.digest() + salt).rstrip()
            elif method == '{SHA}':
                h ='sha1', pw_utf8)
                enc = base64.encodestring(h.digest()).rstrip()
            elif method == '{APR1}':
                # d is of the form "$apr1$<salt>$<hash>"
                salt = d.split('$')[2]
                enc = md5crypt.apache_md5_crypt(pw_utf8, salt.encode('ascii'))
            elif method == '{MD5}':
                # d is of the form "$1$<salt>$<hash>"
                salt = d.split('$')[2]
                enc = md5crypt.unix_md5_crypt(pw_utf8, salt.encode('ascii'))
            elif method == '{DES}':
                # d is 2 characters salt + 11 characters hash
                salt = d[:2]
                enc = crypt.crypt(pw_utf8, salt.encode('ascii'))
                raise ValueError("missing password hash method %s handler" % method)

            return pw_hash == method + enc
        idx = pw_hash.index('}') + 1
        raise ValueError("unsupported password hash method %r" % pw_hash[:idx])

# password recovery token

def generate_token(key=None, stamp=None):
    generate a pair of a secret key and a crypto token.

    you can use this to implement a password recovery functionality by
    calling generate_token() and transmitting the returned token to the
    (correct) user (e.g. by email) and storing the returned (secret) key
    into the user's profile on the server side.

    after the user received the token, he returns to the wiki, gives his
    user name or email address and the token he received. read the (secret)
    key from the user profile and call valid_token(key, token) to verify
    if the token is valid. if it is, consider the user authenticated, remove
    the secret key from his profile and let him reset his password.

    :param key: give it to recompute some specific token for verification
    :param stamp: give it to recompute some specific token for verification
    :rtype: 2-tuple
    :returns: key, token
    if key is None:
        key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
    if stamp is None:
        stamp = int(time.time())
    h =, str(stamp), digestmod=hashlib.sha1).hexdigest()
    token = stamp + '-' + h
    return key, token

def valid_token(key, token, timeout=12*60*60):
    check if token is valid with respect to the secret key,
    the token must not be older than timeout seconds.

    :param key: give the secret key to verify the token
    :param token: the token to verify
    :rtype: bool
    :returns: token is valid and not timed out
    parts = token.split('-')
    if len(parts) != 2:
        return False
        stamp = int(parts[0])
    except ValueError:
        return False
    if stamp + timeout < time.time():
        return False
    expected_token = generate_token(key, stamp)[1]
    return token == expected_token

# miscellaneous

def cache_key(**kw):
    Calculate a cache key (ascii only)

    Important key properties:

    * The key must be different for different <kw>.
    * Key is pure ascii

    :param kw: keys/values to compute cache key from
    return hashlib.md5(repr(kw)).hexdigest()