diff -r 659c4476c57c -r b678a1c43027 VirtualMailManager/password.py --- 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