VirtualMailManager/password.py
changeset 760 b678a1c43027
parent 748 659c4476c57c
child 761 e4e656f19771
--- a/VirtualMailManager/password.py	Mon Mar 24 19:22:04 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,460 +0,0 @@
-# -*- coding: UTF-8 -*-
-# Copyright (c) 2010 - 2014, Pascal Volk
-# See COPYING for distribution information.
-"""
-    VirtualMailManager.password
-    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-    VirtualMailManager's password module to generate password hashes from
-    passwords or random passwords. This module provides following
-    functions:
-
-        hashed_password = pwhash(password[, scheme][, user])
-        random_password = randompw()
-        scheme, encoding = verify_scheme(scheme)
-        schemes, encodings = list_schemes()
-"""
-
-from crypt import crypt
-from random import SystemRandom
-from subprocess import Popen, PIPE
-
-try:
-    import hashlib
-except ImportError:
-    from VirtualMailManager.pycompat import hashlib
-
-from VirtualMailManager import ENCODING
-from VirtualMailManager.emailaddress import EmailAddress
-from VirtualMailManager.common import get_unicode, version_str
-from VirtualMailManager.constants import VMM_ERROR
-from VirtualMailManager.errors import VMMError
-
-COMPAT = hasattr(hashlib, 'compat')
-SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
-PASSWDCHARS = '._-+#*23456789abcdefghikmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
-DEFAULT_B64 = (None, 'B64', 'BASE64')
-DEFAULT_HEX = (None, 'HEX')
-CRYPT_ID_MD5 = 1
-CRYPT_ID_BLF = '2a'
-CRYPT_ID_SHA256 = 5
-CRYPT_ID_SHA512 = 6
-CRYPT_SALT_LEN = 2
-CRYPT_BLF_ROUNDS_MIN = 4
-CRYPT_BLF_ROUNDS_MAX = 31
-CRYPT_BLF_SALT_LEN = 22
-CRYPT_MD5_SALT_LEN = 8
-CRYPT_SHA2_ROUNDS_DEFAULT = 5000
-CRYPT_SHA2_ROUNDS_MIN = 1000
-CRYPT_SHA2_ROUNDS_MAX = 999999999
-CRYPT_SHA2_SALT_LEN = 16
-SALTED_ALGO_SALT_LEN = 4
-
-
-_ = lambda msg: msg
-cfg_dget = lambda option: None
-_sys_rand = SystemRandom()
-_choice = _sys_rand.choice
-_get_salt = lambda s_len: ''.join(_choice(SALTCHARS) for x in xrange(s_len))
-
-
-def _dovecotpw(password, scheme, encoding):
-    """Communicates with dovecotpw (Dovecot 2.0: `doveadm pw`) and returns
-    the hashed password: {scheme[.encoding]}hash
-    """
-    if encoding:
-        scheme = '.'.join((scheme, encoding))
-    cmd_args = [cfg_dget('bin.dovecotpw'), '-s', scheme, '-p',
-                get_unicode(password)]
-    if cfg_dget('misc.dovecot_version') >= 0x20000a01:
-        cmd_args.insert(1, 'pw')
-    process = Popen(cmd_args, stdout=PIPE, stderr=PIPE)
-    stdout, stderr = process.communicate()
-    if process.returncode:
-        raise VMMError(stderr.strip(), VMM_ERROR)
-    hashed = stdout.strip()
-    if not hashed.startswith('{%s}' % scheme):
-        raise VMMError('Unexpected result from %s: %s' %
-                       (cfg_dget('bin.dovecotpw'), hashed), VMM_ERROR)
-    return hashed
-
-
-def _md4_new():
-    """Returns an new MD4-hash object if supported by the hashlib or
-    provided by PyCrypto - other `None`.
-    """
-    try:
-        return hashlib.new('md4')
-    except ValueError, err:
-        if str(err) == 'unsupported hash type':
-            if not COMPAT:
-                try:
-                    from Crypto.Hash import MD4
-                    return MD4.new()
-                except ImportError:
-                    return None
-        else:
-            raise
-
-
-def _sha256_new(data=''):
-    """Returns a new sha256 object from the hashlib.
-
-    Returns `None` if the PyCrypto in pycompat.hashlib is too old."""
-    if not COMPAT:
-        return hashlib.sha256(data)
-    try:
-        return hashlib.new('sha256', data)
-    except ValueError, err:
-        if str(err) == 'unsupported hash type':
-            return None
-        else:
-            raise
-
-
-def _format_digest(digest, scheme, encoding):
-    """Formats the arguments to a string: {scheme[.encoding]}digest."""
-    if not encoding:
-        return '{%s}%s' % (scheme, digest)
-    return '{%s.%s}%s' % (scheme, encoding, digest)
-
-
-def _clear_hash(password, scheme, encoding):
-    """Generates a (encoded) CLEARTEXT/PLAIN 'hash'."""
-    if encoding:
-        if encoding == 'HEX':
-            password = password.encode('hex')
-        else:
-            password = password.encode('base64').replace('\n', '')
-        return _format_digest(password, scheme, encoding)
-    return get_unicode('{%s}%s' % (scheme, password))
-
-
-def _get_crypt_blowfish_salt():
-    """Generates a salt for Blowfish crypt."""
-    rounds = cfg_dget('misc.crypt_blowfish_rounds')
-    if rounds < CRYPT_BLF_ROUNDS_MIN:
-        rounds = CRYPT_BLF_ROUNDS_MIN
-    elif rounds > CRYPT_BLF_ROUNDS_MAX:
-        rounds = CRYPT_BLF_ROUNDS_MAX
-    return '$%s$%02d$%s' % (CRYPT_ID_BLF, rounds,
-                            _get_salt(CRYPT_BLF_SALT_LEN))
-
-
-def _get_crypt_sha2_salt(crypt_id):
-    """Generates a salt for crypt using the SHA-256 or SHA-512 encryption
-    method.
-    *crypt_id* must be either `5` (SHA-256) or `6` (SHA-512).
-    """
-    assert crypt_id in (CRYPT_ID_SHA256, CRYPT_ID_SHA512), 'invalid crypt ' \
-           'id: %r' % crypt_id
-    if crypt_id is CRYPT_ID_SHA512:
-        rounds = cfg_dget('misc.crypt_sha512_rounds')
-    else:
-        rounds = cfg_dget('misc.crypt_sha256_rounds')
-    if rounds < CRYPT_SHA2_ROUNDS_MIN:
-        rounds = CRYPT_SHA2_ROUNDS_MIN
-    elif rounds > CRYPT_SHA2_ROUNDS_MAX:
-        rounds = CRYPT_SHA2_ROUNDS_MAX
-    if rounds == CRYPT_SHA2_ROUNDS_DEFAULT:
-        return '$%d$%s' % (crypt_id, _get_salt(CRYPT_SHA2_SALT_LEN))
-    return '$%d$rounds=%d$%s' % (crypt_id, rounds,
-                                 _get_salt(CRYPT_SHA2_SALT_LEN))
-
-
-def _crypt_hash(password, scheme, encoding):
-    """Generates (encoded) CRYPT/MD5/{BLF,MD5,SHA{256,512}}-CRYPT hashes."""
-    if scheme == 'CRYPT':
-        salt = _get_salt(CRYPT_SALT_LEN)
-    elif scheme == 'BLF-CRYPT':
-        salt = _get_crypt_blowfish_salt()
-    elif scheme in ('MD5-CRYPT', 'MD5'):
-        salt = '$%d$%s' % (CRYPT_ID_MD5, _get_salt(CRYPT_MD5_SALT_LEN))
-    elif scheme == 'SHA256-CRYPT':
-        salt = _get_crypt_sha2_salt(CRYPT_ID_SHA256)
-    else:
-        salt = _get_crypt_sha2_salt(CRYPT_ID_SHA512)
-    encrypted = crypt(password, salt)
-    if encoding:
-        if encoding == 'HEX':
-            encrypted = encrypted.encode('hex')
-        else:
-            encrypted = encrypted.encode('base64').replace('\n', '')
-    if scheme in ('BLF-CRYPT', 'SHA256-CRYPT', 'SHA512-CRYPT') and \
-       cfg_dget('misc.dovecot_version') < 0x20000b06:
-        scheme = 'CRYPT'
-    return _format_digest(encrypted, scheme, encoding)
-
-
-def _md4_hash(password, scheme, encoding):
-    """Generates encoded PLAIN-MD4 hashes."""
-    md4 = _md4_new()
-    if md4:
-        md4.update(password)
-        if encoding in DEFAULT_HEX:
-            digest = md4.hexdigest()
-        else:
-            digest = md4.digest().encode('base64').rstrip()
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-
-def _md5_hash(password, scheme, encoding, user=None):
-    """Generates DIGEST-MD5 aka PLAIN-MD5 and LDAP-MD5 hashes."""
-    md5 = hashlib.md5()
-    if scheme == 'DIGEST-MD5':
-        #  Prior to Dovecot v1.1.12/v1.2.beta2 there was a problem with a
-        #  empty auth_realms setting in dovecot.conf and user@domain.tld
-        #  usernames. So we have to generate different hashes for different
-        #  versions. See also:
-        #       http://dovecot.org/list/dovecot-news/2009-March/000103.html
-        #       http://hg.dovecot.org/dovecot-1.1/rev/2b0043ba89ae
-        if cfg_dget('misc.dovecot_version') >= 0x1010cf00:
-            md5.update('%s:%s:' % (user.localpart, user.domainname))
-        else:
-            md5.update('%s::' % user)
-    md5.update(password)
-    if (scheme in ('PLAIN-MD5', 'DIGEST-MD5') and encoding in DEFAULT_HEX) or \
-       (scheme == 'LDAP-MD5' and encoding == 'HEX'):
-        digest = md5.hexdigest()
-    else:
-        digest = md5.digest().encode('base64').rstrip()
-    return _format_digest(digest, scheme, encoding)
-
-
-def _ntlm_hash(password, scheme, encoding):
-    """Generates NTLM hashes."""
-    md4 = _md4_new()
-    if md4:
-        password = ''.join('%s\x00' % c for c in password)
-        md4.update(password)
-        if encoding in DEFAULT_HEX:
-            digest = md4.hexdigest()
-        else:
-            digest = md4.digest().encode('base64').rstrip()
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-
-def _sha1_hash(password, scheme, encoding):
-    """Generates SHA1 aka SHA hashes."""
-    sha1 = hashlib.sha1(password)
-    if encoding in DEFAULT_B64:
-        digest = sha1.digest().encode('base64').rstrip()
-    else:
-        digest = sha1.hexdigest()
-    return _format_digest(digest, scheme, encoding)
-
-
-def _sha256_hash(password, scheme, encoding):
-    """Generates SHA256 hashes."""
-    sha256 = _sha256_new(password)
-    if sha256:
-        if encoding in DEFAULT_B64:
-            digest = sha256.digest().encode('base64').rstrip()
-        else:
-            digest = sha256.hexdigest()
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-
-def _sha512_hash(password, scheme, encoding):
-    """Generates SHA512 hashes."""
-    if not COMPAT:
-        sha512 = hashlib.sha512(password)
-        if encoding in DEFAULT_B64:
-            digest = sha512.digest().encode('base64').replace('\n', '')
-        else:
-            digest = sha512.hexdigest()
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-
-def _smd5_hash(password, scheme, encoding):
-    """Generates SMD5 (salted PLAIN-MD5) hashes."""
-    md5 = hashlib.md5(password)
-    salt = _get_salt(SALTED_ALGO_SALT_LEN)
-    md5.update(salt)
-    if encoding in DEFAULT_B64:
-        digest = (md5.digest() + salt).encode('base64').rstrip()
-    else:
-        digest = md5.hexdigest() + salt.encode('hex')
-    return _format_digest(digest, scheme, encoding)
-
-
-def _ssha1_hash(password, scheme, encoding):
-    """Generates SSHA (salted SHA/SHA1) hashes."""
-    sha1 = hashlib.sha1(password)
-    salt = _get_salt(SALTED_ALGO_SALT_LEN)
-    sha1.update(salt)
-    if encoding in DEFAULT_B64:
-        digest = (sha1.digest() + salt).encode('base64').rstrip()
-    else:
-        digest = sha1.hexdigest() + salt.encode('hex')
-    return _format_digest(digest, scheme, encoding)
-
-
-def _ssha256_hash(password, scheme, encoding):
-    """Generates SSHA256 (salted SHA256) hashes."""
-    sha256 = _sha256_new(password)
-    if sha256:
-        salt = _get_salt(SALTED_ALGO_SALT_LEN)
-        sha256.update(salt)
-        if encoding in DEFAULT_B64:
-            digest = (sha256.digest() + salt).encode('base64').rstrip()
-        else:
-            digest = sha256.hexdigest() + salt.encode('hex')
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-
-def _ssha512_hash(password, scheme, encoding):
-    """Generates SSHA512 (salted SHA512) hashes."""
-    if not COMPAT:
-        salt = _get_salt(SALTED_ALGO_SALT_LEN)
-        sha512 = hashlib.sha512(password + salt)
-        if encoding in DEFAULT_B64:
-            digest = (sha512.digest() + salt).encode('base64').replace('\n',
-                                                                       '')
-        else:
-            digest = sha512.hexdigest() + salt.encode('hex')
-        return _format_digest(digest, scheme, encoding)
-    return _dovecotpw(password, scheme, encoding)
-
-_scheme_info = {
-    'CLEARTEXT': (_clear_hash, 0x10000f00),
-    'CRAM-MD5': (_dovecotpw, 0x10000f00),
-    'CRYPT': (_crypt_hash, 0x10000f00),
-    'DIGEST-MD5': (_md5_hash, 0x10000f00),
-    'HMAC-MD5': (_dovecotpw, 0x10000f00),
-    'LANMAN': (_dovecotpw, 0x10000f00),
-    'LDAP-MD5': (_md5_hash, 0x10000f00),
-    'MD5': (_crypt_hash, 0x10000f00),
-    'MD5-CRYPT': (_crypt_hash, 0x10000f00),
-    'NTLM': (_ntlm_hash, 0x10000f00),
-    'OTP': (_dovecotpw, 0x10100a01),
-    'PLAIN': (_clear_hash, 0x10000f00),
-    'PLAIN-MD4': (_md4_hash, 0x10000f00),
-    'PLAIN-MD5': (_md5_hash, 0x10000f00),
-    'RPA': (_dovecotpw, 0x10000f00),
-    'SCRAM-SHA-1': (_dovecotpw, 0x20200a01),
-    'SHA': (_sha1_hash, 0x10000f00),
-    'SHA1': (_sha1_hash, 0x10000f00),
-    'SHA256': (_sha256_hash, 0x10100a01),
-    'SHA512': (_sha512_hash, 0x20000b03),
-    'SKEY': (_dovecotpw, 0x10100a01),
-    'SMD5': (_smd5_hash, 0x10000f00),
-    'SSHA': (_ssha1_hash, 0x10000f00),
-    'SSHA256': (_ssha256_hash, 0x10200a04),
-    'SSHA512': (_ssha512_hash, 0x20000b03),
-}
-
-
-def list_schemes():
-    """Returns the tuple (schemes, encodings).
-
-    `schemes` is an iterator for all supported password schemes (depends on
-    the used Dovecot version and features of the libc).
-    `encodings` is a tuple with all usable encoding suffixes. The tuple may
-    be empty.
-    """
-    dcv = cfg_dget('misc.dovecot_version')
-    schemes = (k for (k, v) in _scheme_info.iteritems() if v[1] <= dcv)
-    if dcv >= 0x10100a01:
-        encodings = ('.B64', '.BASE64', '.HEX')
-    else:
-        encodings = ()
-    return schemes, encodings
-
-
-def verify_scheme(scheme):
-    """Checks if the password scheme *scheme* is known and supported by the
-    configured `misc.dovecot_version`.
-
-    The *scheme* maybe a password scheme's name (e.g.: 'PLAIN') or a scheme
-    name with a encoding suffix (e.g. 'PLAIN.BASE64').  If the scheme is
-    known and supported by the used Dovecot version,
-    a tuple ``(scheme, encoding)`` will be returned.
-    The `encoding` in the tuple may be `None`.
-
-    Raises a `VMMError` if the password scheme:
-      * is unknown
-      * depends on a newer Dovecot version
-      * has a unknown encoding suffix
-    """
-    assert isinstance(scheme, basestring), 'Not a str/unicode: %r' % scheme
-    scheme_encoding = scheme.upper().split('.')
-    scheme = scheme_encoding[0]
-    if scheme not in _scheme_info:
-        raise VMMError(_(u"Unsupported password scheme: '%s'") % scheme,
-                       VMM_ERROR)
-    if cfg_dget('misc.dovecot_version') < _scheme_info[scheme][1]:
-        raise VMMError(_(u"The password scheme '%(scheme)s' requires Dovecot "
-                         u">= v%(version)s.") % {'scheme': scheme,
-                       'version': version_str(_scheme_info[scheme][1])},
-                       VMM_ERROR)
-    if len(scheme_encoding) > 1:
-        if cfg_dget('misc.dovecot_version') < 0x10100a01:
-            raise VMMError(_(u'Encoding suffixes for password schemes require '
-                             u'Dovecot >= v1.1.alpha1.'), VMM_ERROR)
-        if scheme_encoding[1] not in ('B64', 'BASE64', 'HEX'):
-            raise VMMError(_(u"Unsupported password encoding: '%s'") %
-                           scheme_encoding[1], VMM_ERROR)
-        encoding = scheme_encoding[1]
-    else:
-        encoding = None
-    return scheme, encoding
-
-
-def pwhash(password, scheme=None, user=None):
-    """Generates a password hash from the plain text *password* string.
-
-    If no *scheme* is given the password scheme from the configuration will
-    be used for the hash generation.  When 'DIGEST-MD5' is used as scheme,
-    also an EmailAddress instance must be given as *user* argument.
-    """
-    if not isinstance(password, basestring):
-        raise TypeError('Password is not a string: %r' % password)
-    if isinstance(password, unicode):
-        password = password.encode(ENCODING)
-    password = password.strip()
-    if not password:
-        raise ValueError("Could not accept empty password.")
-    if scheme is None:
-        scheme = cfg_dget('misc.password_scheme')
-    scheme, encoding = verify_scheme(scheme)
-    if scheme == 'DIGEST-MD5':
-        assert isinstance(user, EmailAddress)
-        return _md5_hash(password, scheme, encoding, user)
-    return _scheme_info[scheme][0](password, scheme, encoding)
-
-
-def randompw():
-    """Generates a plain text random password.
-
-    The length of the password can be configured in the ``vmm.cfg``
-    (account.password_length).
-    """
-    pw_len = cfg_dget('account.password_length')
-    if pw_len < 8:
-        pw_len = 8
-    return ''.join(_sys_rand.sample(PASSWDCHARS, pw_len))
-
-
-def _test_crypt_algorithms():
-    """Check for Blowfish/SHA-256/SHA-512 support in crypt.crypt()."""
-    _blowfish = '$2a$04$0123456789abcdefABCDE.N.drYX5yIAL1LkTaaZotW3yI0hQhZru'
-    _sha256 = '$5$rounds=1000$0123456789abcdef$K/DksR0DT01hGc8g/kt9McEgrbFMKi\
-9qrb1jehe7hn4'
-    _sha512 = '$6$rounds=1000$0123456789abcdef$ZIAd5WqfyLkpvsVCVUU1GrvqaZTqvh\
-JoouxdSqJO71l9Ld3tVrfOatEjarhghvEYADkq//LpDnTeO90tcbtHR1'
-
-    if crypt('08/15!test~4711', '$2a$04$0123456789abcdefABCDEF$') == _blowfish:
-        _scheme_info['BLF-CRYPT'] = (_crypt_hash, 0x10000f00)
-    if crypt('08/15!test~4711', '$5$rounds=1000$0123456789abcdef$') == _sha256:
-        _scheme_info['SHA256-CRYPT'] = (_crypt_hash, 0x10000f00)
-    if crypt('08/15!test~4711', '$6$rounds=1000$0123456789abcdef$') == _sha512:
-        _scheme_info['SHA512-CRYPT'] = (_crypt_hash, 0x10000f00)
-
-_test_crypt_algorithms()
-del _, cfg_dget, _test_crypt_algorithms