--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/VirtualMailManager/password.py Wed Apr 28 03:34:57 2010 +0000
@@ -0,0 +1,342 @@
+# -*- coding: UTF-8 -*-
+# Copyright (c) 2010, Pascal Volk
+# See COPYING for distribution information.
+
+"""
+ VirtualMailManager.password
+
+ VirtualMailManager's password module to generate password hashes from
+ passwords or random passwords. There are two functions:
+
+ hashed_password = pwhash(password[, scheme][, user])
+ random_password = randompw()
+"""
+
+from crypt import crypt
+from random import choice, shuffle
+from subprocess import Popen, PIPE
+
+try:
+ import hashlib
+except ImportError:
+ from VirtualMailManager.pycompat import hashlib
+
+from VirtualMailManager import ENCODING, Configuration
+from VirtualMailManager.EmailAddress import EmailAddress
+from VirtualMailManager.common import get_unicode, version_str
+from VirtualMailManager.constants.ERROR 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')
+
+_ = lambda msg: msg
+_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 = [Configuration.dget('bin.dovecotpw'), '-s', scheme, '-p',
+ get_unicode(password)]
+ if Configuration.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)
+ return stdout.strip()
+
+
+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 _crypt_hash(password, scheme, encoding):
+ """Generates (encoded) CRYPT/MD5/MD5-CRYPT hashes."""
+ if scheme == 'CRYPT':
+ salt = _get_salt(2)
+ else:
+ salt = '$1$%s$' % _get_salt(8)
+ encrypted = crypt(password, salt)
+ if encoding:
+ if encoding == 'HEX':
+ encrypted = encrypted.encode('hex')
+ else:
+ encrypted = encrypted.encode('base64').rstrip()
+ 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 Configuration.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(4)
+ 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(4)
+ 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(4)
+ 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(4)
+ 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),
+ '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 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.
+ """
+ assert Configuration is not None
+ 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("Couldn't accept empty password.")
+ if scheme is None:
+ scheme = Configuration.dget('misc.password_scheme')
+ scheme_encoding = scheme.split('.')
+ scheme = scheme_encoding[0].upper()
+ if not scheme in _scheme_info:
+ raise VMMError(_(u"Unsupported password scheme: '%s'") % scheme,
+ VMM_ERROR)
+ if Configuration.dget('misc.dovecot_version') < _scheme_info[scheme][1]:
+ raise VMMError(_(u"The scheme '%s' requires Dovecot >= v%s") %
+ (scheme, version_str(_scheme_info[scheme][1])),
+ VMM_ERROR)
+ if len(scheme_encoding) > 1:
+ if Configuration.dget('misc.dovecot_version') < 0x10100a01:
+ raise VMMError(_(u'Encoding suffixes for password schemes require \
+Dovecot >= v1.1.alpha1'),
+ VMM_ERROR)
+ if scheme_encoding[1].upper() not in ('B64', 'BASE64', 'HEX'):
+ raise ValueError('Unsupported encoding: %r' % scheme_encoding[1])
+ encoding = scheme_encoding[1].upper()
+ else:
+ encoding = None
+ 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).
+ """
+ assert Configuration is not None
+ pw_chars = list(PASSWDCHARS)
+ shuffle(pw_chars)
+ pw_len = Configuration.dget('account.password_length')
+ if pw_len < 8:
+ pw_len = 8
+ return ''.join(choice(pw_chars) for x in xrange(pw_len))
+
+del _