VirtualMailManager/password.py
branchv0.6.x
changeset 268 beb8f4421f92
child 272 446483386914
equal deleted inserted replaced
267:084300a00ee1 268:beb8f4421f92
       
     1 # -*- coding: UTF-8 -*-
       
     2 # Copyright (c) 2010, Pascal Volk
       
     3 # See COPYING for distribution information.
       
     4 
       
     5 """
       
     6     VirtualMailManager.password
       
     7 
       
     8     VirtualMailManager's password module to generate password hashes from
       
     9     passwords or random passwords. There are two functions:
       
    10 
       
    11         hashed_password = pwhash(password[, scheme][, user])
       
    12         random_password = randompw()
       
    13 """
       
    14 
       
    15 from crypt import crypt
       
    16 from random import choice, shuffle
       
    17 from subprocess import Popen, PIPE
       
    18 
       
    19 try:
       
    20     import hashlib
       
    21 except ImportError:
       
    22     from VirtualMailManager.pycompat import hashlib
       
    23 
       
    24 from VirtualMailManager import ENCODING, Configuration
       
    25 from VirtualMailManager.EmailAddress import EmailAddress
       
    26 from VirtualMailManager.common import get_unicode, version_str
       
    27 from VirtualMailManager.constants.ERROR import VMM_ERROR
       
    28 from VirtualMailManager.errors import VMMError
       
    29 
       
    30 COMPAT = hasattr(hashlib, 'compat')
       
    31 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
       
    32 PASSWDCHARS = '._-+#*23456789abcdefghikmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'
       
    33 DEFAULT_B64 = (None, 'B64', 'BASE64')
       
    34 DEFAULT_HEX = (None, 'HEX')
       
    35 
       
    36 _ = lambda msg: msg
       
    37 _get_salt = lambda s_len: ''.join(choice(SALTCHARS) for x in xrange(s_len))
       
    38 
       
    39 
       
    40 def _dovecotpw(password, scheme, encoding):
       
    41     """Communicates with dovecotpw (Dovecot 2.0: `doveadm pw`) and returns
       
    42     the hashed password: {scheme[.encoding]}hash
       
    43     """
       
    44     if encoding:
       
    45         scheme = '.'.join((scheme, encoding))
       
    46     cmd_args = [Configuration.dget('bin.dovecotpw'), '-s', scheme, '-p',
       
    47                 get_unicode(password)]
       
    48     if Configuration.dget('misc.dovecot_version') >= 0x20000a01:
       
    49         cmd_args.insert(1, 'pw')
       
    50     process = Popen(cmd_args, stdout=PIPE, stderr=PIPE)
       
    51     stdout, stderr = process.communicate()
       
    52     if process.returncode:
       
    53         raise VMMError(stderr.strip(), VMM_ERROR)
       
    54     return stdout.strip()
       
    55 
       
    56 
       
    57 def _md4_new():
       
    58     """Returns an new MD4-hash object if supported by the hashlib or
       
    59     provided by PyCrypto - other `None`.
       
    60     """
       
    61     try:
       
    62         return hashlib.new('md4')
       
    63     except ValueError, err:
       
    64         if str(err) == 'unsupported hash type':
       
    65             if not COMPAT:
       
    66                 try:
       
    67                     from Crypto.Hash import MD4
       
    68                     return MD4.new()
       
    69                 except ImportError:
       
    70                     return None
       
    71         else:
       
    72             raise
       
    73 
       
    74 
       
    75 def _sha256_new(data=''):
       
    76     """Returns a new sha256 object from the hashlib.
       
    77 
       
    78     Returns `None` if the PyCrypto in pycompat.hashlib is too old."""
       
    79     if not COMPAT:
       
    80         return hashlib.sha256(data)
       
    81     try:
       
    82         return hashlib.new('sha256', data)
       
    83     except ValueError, err:
       
    84         if str(err) == 'unsupported hash type':
       
    85             return None
       
    86         else:
       
    87             raise
       
    88 
       
    89 
       
    90 def _format_digest(digest, scheme, encoding):
       
    91     """Formats the arguments to a string: {scheme[.encoding]}digest."""
       
    92     if not encoding:
       
    93         return '{%s}%s' % (scheme, digest)
       
    94     return '{%s.%s}%s' % (scheme, encoding, digest)
       
    95 
       
    96 
       
    97 def _clear_hash(password, scheme, encoding):
       
    98     """Generates a (encoded) CLEARTEXT/PLAIN 'hash'."""
       
    99     if encoding:
       
   100         if encoding == 'HEX':
       
   101             password = password.encode('hex')
       
   102         else:
       
   103             password = password.encode('base64').replace('\n', '')
       
   104         return _format_digest(password, scheme, encoding)
       
   105     return get_unicode('{%s}%s' % (scheme, password))
       
   106 
       
   107 
       
   108 def _crypt_hash(password, scheme, encoding):
       
   109     """Generates (encoded) CRYPT/MD5/MD5-CRYPT hashes."""
       
   110     if scheme == 'CRYPT':
       
   111         salt = _get_salt(2)
       
   112     else:
       
   113         salt = '$1$%s$' % _get_salt(8)
       
   114     encrypted = crypt(password, salt)
       
   115     if encoding:
       
   116         if encoding == 'HEX':
       
   117             encrypted = encrypted.encode('hex')
       
   118         else:
       
   119             encrypted = encrypted.encode('base64').rstrip()
       
   120     return _format_digest(encrypted, scheme, encoding)
       
   121 
       
   122 
       
   123 def _md4_hash(password, scheme, encoding):
       
   124     """Generates encoded PLAIN-MD4 hashes."""
       
   125     md4 = _md4_new()
       
   126     if md4:
       
   127         md4.update(password)
       
   128         if encoding in DEFAULT_HEX:
       
   129             digest = md4.hexdigest()
       
   130         else:
       
   131             digest = md4.digest().encode('base64').rstrip()
       
   132         return _format_digest(digest, scheme, encoding)
       
   133     return _dovecotpw(password, scheme, encoding)
       
   134 
       
   135 
       
   136 def _md5_hash(password, scheme, encoding, user=None):
       
   137     """Generates DIGEST-MD5 aka PLAIN-MD5 and LDAP-MD5 hashes."""
       
   138     md5 = hashlib.md5()
       
   139     if scheme == 'DIGEST-MD5':
       
   140         #  Prior to Dovecot v1.1.12/v1.2.beta2 there was a problem with a
       
   141         #  empty auth_realms setting in dovecot.conf and user@domain.tld
       
   142         #  usernames. So we have to generate different hashes for different
       
   143         #  versions. See also:
       
   144         #       http://dovecot.org/list/dovecot-news/2009-March/000103.html
       
   145         #       http://hg.dovecot.org/dovecot-1.1/rev/2b0043ba89ae
       
   146         if Configuration.dget('misc.dovecot_version') >= 0x1010cf00:
       
   147             md5.update('%s:%s:' % (user.localpart, user.domainname))
       
   148         else:
       
   149             md5.update('%s::' % user)
       
   150     md5.update(password)
       
   151     if (scheme in ('PLAIN-MD5', 'DIGEST-MD5') and encoding in DEFAULT_HEX) \
       
   152       or (scheme == 'LDAP-MD5' and encoding == 'HEX'):
       
   153         digest = md5.hexdigest()
       
   154     else:
       
   155         digest = md5.digest().encode('base64').rstrip()
       
   156     return _format_digest(digest, scheme, encoding)
       
   157 
       
   158 
       
   159 def _ntlm_hash(password, scheme, encoding):
       
   160     """Generates NTLM hashes."""
       
   161     md4 = _md4_new()
       
   162     if md4:
       
   163         password = ''.join('%s\x00' % c for c in password)
       
   164         md4.update(password)
       
   165         if encoding in DEFAULT_HEX:
       
   166             digest = md4.hexdigest()
       
   167         else:
       
   168             digest = md4.digest().encode('base64').rstrip()
       
   169         return _format_digest(digest, scheme, encoding)
       
   170     return _dovecotpw(password, scheme, encoding)
       
   171 
       
   172 
       
   173 def _sha1_hash(password, scheme, encoding):
       
   174     """Generates SHA1 aka SHA hashes."""
       
   175     sha1 = hashlib.sha1(password)
       
   176     if encoding in DEFAULT_B64:
       
   177         digest = sha1.digest().encode('base64').rstrip()
       
   178     else:
       
   179         digest = sha1.hexdigest()
       
   180     return _format_digest(digest, scheme, encoding)
       
   181 
       
   182 
       
   183 def _sha256_hash(password, scheme, encoding):
       
   184     """Generates SHA256 hashes."""
       
   185     sha256 = _sha256_new(password)
       
   186     if sha256:
       
   187         if encoding in DEFAULT_B64:
       
   188             digest = sha256.digest().encode('base64').rstrip()
       
   189         else:
       
   190             digest = sha256.hexdigest()
       
   191         return _format_digest(digest, scheme, encoding)
       
   192     return _dovecotpw(password, scheme, encoding)
       
   193 
       
   194 
       
   195 def _sha512_hash(password, scheme, encoding):
       
   196     """Generates SHA512 hashes."""
       
   197     if not COMPAT:
       
   198         sha512 = hashlib.sha512(password)
       
   199         if encoding in DEFAULT_B64:
       
   200             digest = sha512.digest().encode('base64').replace('\n', '')
       
   201         else:
       
   202             digest = sha512.hexdigest()
       
   203         return _format_digest(digest, scheme, encoding)
       
   204     return _dovecotpw(password, scheme, encoding)
       
   205 
       
   206 
       
   207 def _smd5_hash(password, scheme, encoding):
       
   208     """Generates SMD5 (salted PLAIN-MD5) hashes."""
       
   209     md5 = hashlib.md5(password)
       
   210     salt = _get_salt(4)
       
   211     md5.update(salt)
       
   212     if encoding in DEFAULT_B64:
       
   213         digest = (md5.digest() + salt).encode('base64').rstrip()
       
   214     else:
       
   215         digest = md5.hexdigest() + salt.encode('hex')
       
   216     return _format_digest(digest, scheme, encoding)
       
   217 
       
   218 
       
   219 def _ssha1_hash(password, scheme, encoding):
       
   220     """Generates SSHA (salted SHA/SHA1) hashes."""
       
   221     sha1 = hashlib.sha1(password)
       
   222     salt = _get_salt(4)
       
   223     sha1.update(salt)
       
   224     if encoding in DEFAULT_B64:
       
   225         digest = (sha1.digest() + salt).encode('base64').rstrip()
       
   226     else:
       
   227         digest = sha1.hexdigest() + salt.encode('hex')
       
   228     return _format_digest(digest, scheme, encoding)
       
   229 
       
   230 
       
   231 def _ssha256_hash(password, scheme, encoding):
       
   232     """Generates SSHA256 (salted SHA256) hashes."""
       
   233     sha256 = _sha256_new(password)
       
   234     if sha256:
       
   235         salt = _get_salt(4)
       
   236         sha256.update(salt)
       
   237         if encoding in DEFAULT_B64:
       
   238             digest = (sha256.digest() + salt).encode('base64').rstrip()
       
   239         else:
       
   240             digest = sha256.hexdigest() + salt.encode('hex')
       
   241         return _format_digest(digest, scheme, encoding)
       
   242     return _dovecotpw(password, scheme, encoding)
       
   243 
       
   244 
       
   245 def _ssha512_hash(password, scheme, encoding):
       
   246     """Generates SSHA512 (salted SHA512) hashes."""
       
   247     if not COMPAT:
       
   248         salt = _get_salt(4)
       
   249         sha512 = hashlib.sha512(password + salt)
       
   250         if encoding in DEFAULT_B64:
       
   251             digest = (sha512.digest() + salt).encode('base64').replace('\n',
       
   252                                                                        '')
       
   253         else:
       
   254             digest = sha512.hexdigest() + salt.encode('hex')
       
   255         return _format_digest(digest, scheme, encoding)
       
   256     return _dovecotpw(password, scheme, encoding)
       
   257 
       
   258 _scheme_info = {
       
   259     'CLEARTEXT': (_clear_hash, 0x10000f00),
       
   260     'CRAM-MD5': (_dovecotpw, 0x10000f00),
       
   261     'CRYPT': (_crypt_hash, 0x10000f00),
       
   262     'DIGEST-MD5': (_md5_hash, 0x10000f00),
       
   263     'HMAC-MD5': (_dovecotpw, 0x10000f00),
       
   264     'LANMAN': (_dovecotpw, 0x10000f00),
       
   265     'LDAP-MD5': (_md5_hash, 0x10000f00),
       
   266     'MD5': (_crypt_hash, 0x10000f00),
       
   267     'MD5-CRYPT': (_crypt_hash, 0x10000f00),
       
   268     'NTLM': (_ntlm_hash, 0x10000f00),
       
   269     'OTP': (_dovecotpw, 0x10100a01),
       
   270     'PLAIN': (_clear_hash, 0x10000f00),
       
   271     'PLAIN-MD4': (_md4_hash, 0x10000f00),
       
   272     'PLAIN-MD5': (_md5_hash, 0x10000f00),
       
   273     'RPA': (_dovecotpw, 0x10000f00),
       
   274     'SHA': (_sha1_hash, 0x10000f00),
       
   275     'SHA1': (_sha1_hash, 0x10000f00),
       
   276     'SHA256': (_sha256_hash, 0x10100a01),
       
   277     'SHA512': (_sha512_hash, 0x20000b03),
       
   278     'SKEY': (_dovecotpw, 0x10100a01),
       
   279     'SMD5': (_smd5_hash, 0x10000f00),
       
   280     'SSHA': (_ssha1_hash, 0x10000f00),
       
   281     'SSHA256': (_ssha256_hash, 0x10200a04),
       
   282     'SSHA512': (_ssha512_hash, 0x20000b03),
       
   283 }
       
   284 
       
   285 
       
   286 def pwhash(password, scheme=None, user=None):
       
   287     """Generates a password hash from the plain text *password* string.
       
   288 
       
   289     If no *scheme* is given the password scheme from the configuration will
       
   290     be used for the hash generation.  When 'DIGEST-MD5' is used as scheme,
       
   291     also an EmailAddress instance must be given as *user* argument.
       
   292     """
       
   293     assert Configuration is not None
       
   294     if not isinstance(password, basestring):
       
   295         raise TypeError('Password is not a string: %r' % password)
       
   296     if isinstance(password, unicode):
       
   297         password = password.encode(ENCODING)
       
   298     password = password.strip()
       
   299     if not password:
       
   300         raise ValueError("Couldn't accept empty password.")
       
   301     if scheme is None:
       
   302         scheme = Configuration.dget('misc.password_scheme')
       
   303     scheme_encoding = scheme.split('.')
       
   304     scheme = scheme_encoding[0].upper()
       
   305     if not scheme in _scheme_info:
       
   306         raise VMMError(_(u"Unsupported password scheme: '%s'") % scheme,
       
   307                        VMM_ERROR)
       
   308     if Configuration.dget('misc.dovecot_version') < _scheme_info[scheme][1]:
       
   309         raise VMMError(_(u"The scheme '%s' requires Dovecot >= v%s") %
       
   310                        (scheme, version_str(_scheme_info[scheme][1])),
       
   311                        VMM_ERROR)
       
   312     if len(scheme_encoding) > 1:
       
   313         if Configuration.dget('misc.dovecot_version') < 0x10100a01:
       
   314             raise VMMError(_(u'Encoding suffixes for password schemes require \
       
   315 Dovecot >= v1.1.alpha1'),
       
   316                            VMM_ERROR)
       
   317         if scheme_encoding[1].upper() not in ('B64', 'BASE64', 'HEX'):
       
   318             raise ValueError('Unsupported encoding: %r' % scheme_encoding[1])
       
   319         encoding = scheme_encoding[1].upper()
       
   320     else:
       
   321         encoding = None
       
   322     if scheme == 'DIGEST-MD5':
       
   323         assert isinstance(user, EmailAddress)
       
   324         return _md5_hash(password, scheme, encoding, user)
       
   325     return _scheme_info[scheme][0](password, scheme, encoding)
       
   326 
       
   327 
       
   328 def randompw():
       
   329     """Generates a plain text random password.
       
   330 
       
   331     The length of the password can be configured in the ``vmm.cfg``
       
   332     (account.password_length).
       
   333     """
       
   334     assert Configuration is not None
       
   335     pw_chars = list(PASSWDCHARS)
       
   336     shuffle(pw_chars)
       
   337     pw_len = Configuration.dget('account.password_length')
       
   338     if pw_len < 8:
       
   339         pw_len = 8
       
   340     return ''.join(choice(pw_chars) for x in xrange(pw_len))
       
   341 
       
   342 del _