# -*- coding: UTF-8 -*-# Copyright (c) 2010 - 2011, 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)"""fromcryptimportcryptfromrandomimportSystemRandomfromsubprocessimportPopen,PIPEtry:importhashlibexceptImportError:fromVirtualMailManager.pycompatimporthashlibfromVirtualMailManagerimportENCODINGfromVirtualMailManager.emailaddressimportEmailAddressfromVirtualMailManager.commonimportget_unicode,version_strfromVirtualMailManager.constantsimportVMM_ERRORfromVirtualMailManager.errorsimportVMMErrorCOMPAT=hasattr(hashlib,'compat')SALTCHARS='./0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'PASSWDCHARS='._-+#*23456789abcdefghikmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'DEFAULT_B64=(None,'B64','BASE64')DEFAULT_HEX=(None,'HEX')CRYPT_ID_MD5=1CRYPT_ID_BLF='2a'CRYPT_ID_SHA256=5CRYPT_ID_SHA512=6CRYPT_SALT_LEN=2CRYPT_BLF_ROUNDS_MIN=4CRYPT_BLF_ROUNDS_MAX=31CRYPT_BLF_SALT_LEN=22CRYPT_MD5_SALT_LEN=8CRYPT_SHA2_ROUNDS_DEFAULT=5000CRYPT_SHA2_ROUNDS_MIN=1000CRYPT_SHA2_ROUNDS_MAX=999999999CRYPT_SHA2_SALT_LEN=16SALTED_ALGO_SALT_LEN=4_=lambdamsg:msgcfg_dget=lambdaoption:None_sys_rand=SystemRandom()_choice=_sys_rand.choice_get_salt=lambdas_len:''.join(_choice(SALTCHARS)forxinxrange(s_len))def_dovecotpw(password,scheme,encoding):"""Communicates with dovecotpw (Dovecot 2.0: `doveadm pw`) and returns the hashed password: {scheme[.encoding]}hash """ifencoding:scheme='.'.join((scheme,encoding))cmd_args=[cfg_dget('bin.dovecotpw'),'-s',scheme,'-p',get_unicode(password)]ifcfg_dget('misc.dovecot_version')>=0x20000a01:cmd_args.insert(1,'pw')process=Popen(cmd_args,stdout=PIPE,stderr=PIPE)stdout,stderr=process.communicate()ifprocess.returncode:raiseVMMError(stderr.strip(),VMM_ERROR)hashed=stdout.strip()ifnothashed.startswith('{%s}'%scheme):raiseVMMError('Unexpected result from %s: %s'%(cfg_dget('bin.dovecotpw'),hashed),VMM_ERROR)returnhasheddef_md4_new():"""Returns an new MD4-hash object if supported by the hashlib or provided by PyCrypto - other `None`. """try:returnhashlib.new('md4')exceptValueError,err:ifstr(err)=='unsupported hash type':ifnotCOMPAT:try:fromCrypto.HashimportMD4returnMD4.new()exceptImportError:returnNoneelse:raisedef_sha256_new(data=''):"""Returns a new sha256 object from the hashlib. Returns `None` if the PyCrypto in pycompat.hashlib is too old."""ifnotCOMPAT:returnhashlib.sha256(data)try:returnhashlib.new('sha256',data)exceptValueError,err:ifstr(err)=='unsupported hash type':returnNoneelse:raisedef_format_digest(digest,scheme,encoding):"""Formats the arguments to a string: {scheme[.encoding]}digest."""ifnotencoding:return'{%s}%s'%(scheme,digest)return'{%s.%s}%s'%(scheme,encoding,digest)def_clear_hash(password,scheme,encoding):"""Generates a (encoded) CLEARTEXT/PLAIN 'hash'."""ifencoding:ifencoding=='HEX':password=password.encode('hex')else:password=password.encode('base64').replace('\n','')return_format_digest(password,scheme,encoding)returnget_unicode('{%s}%s'%(scheme,password))def_get_crypt_blowfish_salt():"""Generates a salt for Blowfish crypt."""rounds=cfg_dget('misc.crypt_blowfish_rounds')ifrounds<CRYPT_BLF_ROUNDS_MIN:rounds=CRYPT_BLF_ROUNDS_MINelifrounds>CRYPT_BLF_ROUNDS_MAX:rounds=CRYPT_BLF_ROUNDS_MAXreturn'$%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). """assertcrypt_idin(CRYPT_ID_SHA256,CRYPT_ID_SHA512),'invalid crypt ' \'id: %r'%crypt_idifcrypt_idisCRYPT_ID_SHA512:rounds=cfg_dget('misc.crypt_sha512_rounds')else:rounds=cfg_dget('misc.crypt_sha256_rounds')ifrounds<CRYPT_SHA2_ROUNDS_MIN:rounds=CRYPT_SHA2_ROUNDS_MINelifrounds>CRYPT_SHA2_ROUNDS_MAX:rounds=CRYPT_SHA2_ROUNDS_MAXifrounds==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."""ifscheme=='CRYPT':salt=_get_salt(CRYPT_SALT_LEN)elifscheme=='BLF-CRYPT':salt=_get_crypt_blowfish_salt()elifschemein('MD5-CRYPT','MD5'):salt='$%d$%s'%(CRYPT_ID_MD5,_get_salt(CRYPT_MD5_SALT_LEN))elifscheme=='SHA256-CRYPT':salt=_get_crypt_sha2_salt(CRYPT_ID_SHA256)else:salt=_get_crypt_sha2_salt(CRYPT_ID_SHA512)encrypted=crypt(password,salt)ifencoding:ifencoding=='HEX':encrypted=encrypted.encode('hex')else:encrypted=encrypted.encode('base64').replace('\n','')ifschemein('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()ifmd4:md4.update(password)ifencodinginDEFAULT_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()ifscheme=='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/2b0043ba89aeifcfg_dget('misc.dovecot_version')>=0x1010cf00:md5.update('%s:%s:'%(user.localpart,user.domainname))else:md5.update('%s::'%user)md5.update(password)if(schemein('PLAIN-MD5','DIGEST-MD5')andencodinginDEFAULT_HEX)or \(scheme=='LDAP-MD5'andencoding=='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()ifmd4:password=''.join('%s\x00'%cforcinpassword)md4.update(password)ifencodinginDEFAULT_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)ifencodinginDEFAULT_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)ifsha256:ifencodinginDEFAULT_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."""ifnotCOMPAT:sha512=hashlib.sha512(password)ifencodinginDEFAULT_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)ifencodinginDEFAULT_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)ifencodinginDEFAULT_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)ifsha256:salt=_get_salt(SALTED_ALGO_SALT_LEN)sha256.update(salt)ifencodinginDEFAULT_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."""ifnotCOMPAT:salt=_get_salt(SALTED_ALGO_SALT_LEN)sha512=hashlib.sha512(password+salt)ifencodinginDEFAULT_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),}defverify_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 """assertisinstance(scheme,basestring),'Not a str/unicode: %r'%schemescheme_encoding=scheme.upper().split('.')scheme=scheme_encoding[0]ifschemenotin_scheme_info:raiseVMMError(_(u"Unsupported password scheme: '%s'")%scheme,VMM_ERROR)ifcfg_dget('misc.dovecot_version')<_scheme_info[scheme][1]:raiseVMMError(_(u"The password scheme '%(scheme)s' requires Dovecot "u">= v%(version)s.")%{'scheme':scheme,'version':version_str(_scheme_info[scheme][1])},VMM_ERROR)iflen(scheme_encoding)>1:ifcfg_dget('misc.dovecot_version')<0x10100a01:raiseVMMError(_(u'Encoding suffixes for password schemes require 'u'Dovecot >= v1.1.alpha1.'),VMM_ERROR)ifscheme_encoding[1]notin('B64','BASE64','HEX'):raiseVMMError(_(u"Unsupported password encoding: '%s'")%scheme_encoding[1],VMM_ERROR)encoding=scheme_encoding[1]else:encoding=Nonereturnscheme,encodingdefpwhash(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. """ifnotisinstance(password,basestring):raiseTypeError('Password is not a string: %r'%password)ifisinstance(password,unicode):password=password.encode(ENCODING)password=password.strip()ifnotpassword:raiseValueError("Could not accept empty password.")ifschemeisNone:scheme=cfg_dget('misc.password_scheme')scheme,encoding=verify_scheme(scheme)ifscheme=='DIGEST-MD5':assertisinstance(user,EmailAddress)return_md5_hash(password,scheme,encoding,user)return_scheme_info[scheme][0](password,scheme,encoding)defrandompw():"""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')ifpw_len<8:pw_len=8return''.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'ifcrypt('08/15!test~4711','$2a$04$0123456789abcdefABCDEF$')==_blowfish:_scheme_info['BLF-CRYPT']=(_crypt_hash,0x10000f00)ifcrypt('08/15!test~4711','$5$rounds=1000$0123456789abcdef$')==_sha256:_scheme_info['SHA256-CRYPT']=(_crypt_hash,0x10000f00)ifcrypt('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