# -*- coding: UTF-8 -*-# Copyright (c) 2010 - 2013, 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()"""importhashlibfrombase64importb64encodefrombinasciiimportb2a_hexfromcryptimportcryptfromrandomimportSystemRandomfromsubprocessimportPopen,PIPEfromVirtualMailManagerimportENCODINGfromVirtualMailManager.emailaddressimportEmailAddressfromVirtualMailManager.commonimportget_unicode,version_strfromVirtualMailManager.constantsimportVMM_ERRORfromVirtualMailManager.errorsimportVMMErrorSALTCHARS='./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)forxinrange(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().decode(ENCODING),VMM_ERROR)hashed=stdout.strip().decode(ENCODING)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 - otherwise `None`. """try:returnhashlib.new('md4')exceptValueErroraserr:iferr.args[0].startswith('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'."""password=password.decode(ENCODING)ifencoding:ifencoding=='HEX':password=b2a_hex(password.encode()).decode()else:password=b64encode(password.encode()).decode()return_format_digest(password,scheme,encoding)return'{%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.decode(ENCODING),salt)ifencoding:ifencoding=='HEX':encrypted=b2a_hex(encrypted.encode()).decode()else:encrypted=b64encode(encrypted.encode()).decode()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=b64encode(md4.digest()).decode()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':md5.update(user.localpart.encode()+b':'+user.domainname.encode()+b':')md5.update(password)if(schemein('PLAIN-MD5','DIGEST-MD5')andencodinginDEFAULT_HEX)or \(scheme=='LDAP-MD5'andencoding=='HEX'):digest=md5.hexdigest()else:digest=b64encode(md5.digest()).decode()return_format_digest(digest,scheme,encoding)def_ntlm_hash(password,scheme,encoding):"""Generates NTLM hashes."""md4=_md4_new()ifmd4:password=b''.join(bytes(x)forxinzip(password,bytes(len(password))))md4.update(password)ifencodinginDEFAULT_HEX:digest=md4.hexdigest()else:digest=b64encode(md4.digest()).decode()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=b64encode(sha1.digest()).decode()else:digest=sha1.hexdigest()return_format_digest(digest,scheme,encoding)def_sha256_hash(password,scheme,encoding):"""Generates SHA256 hashes."""sha256=hashlib.sha256(password)ifencodinginDEFAULT_B64:digest=b64encode(sha256.digest()).decode()else:digest=sha256.hexdigest()return_format_digest(digest,scheme,encoding)def_sha512_hash(password,scheme,encoding):"""Generates SHA512 hashes."""sha512=hashlib.sha512(password)ifencodinginDEFAULT_B64:digest=b64encode(sha512.digest()).decode()else:digest=sha512.hexdigest()return_format_digest(digest,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).encode()md5.update(salt)ifencodinginDEFAULT_B64:digest=b64encode(md5.digest()+salt).decode()else:digest=md5.hexdigest()+b2a_hex(salt).decode()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).encode()sha1.update(salt)ifencodinginDEFAULT_B64:digest=b64encode(sha1.digest()+salt).decode()else:digest=sha1.hexdigest()+b2a_hex(salt).decode()return_format_digest(digest,scheme,encoding)def_ssha256_hash(password,scheme,encoding):"""Generates SSHA256 (salted SHA256) hashes."""sha256=hashlib.sha256(password)salt=_get_salt(SALTED_ALGO_SALT_LEN).encode()sha256.update(salt)ifencodinginDEFAULT_B64:digest=b64encode(sha256.digest()+salt).decode()else:digest=sha256.hexdigest()+b2a_hex(salt).decode()return_format_digest(digest,scheme,encoding)def_ssha512_hash(password,scheme,encoding):"""Generates SSHA512 (salted SHA512) hashes."""salt=_get_salt(SALTED_ALGO_SALT_LEN).encode()sha512=hashlib.sha512(password+salt)ifencodinginDEFAULT_B64:digest=b64encode(sha512.digest()+salt).decode()else:digest=sha512.hexdigest()+b2a_hex(salt).decode()return_format_digest(digest,scheme,encoding)_scheme_info={'CLEAR':(_clear_hash,0x2010df00),'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),}deflist_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. """dcv=cfg_dget('misc.dovecot_version')schemes=(kfor(k,v)in_scheme_info.items()ifv[1]<=dcv)encodings=('.B64','.BASE64','.HEX')returnschemes,encodingsdefverify_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,str),'Not a str: {!r}'.format(scheme)scheme_encoding=scheme.upper().split('.')scheme=scheme_encoding[0]ifschemenotin_scheme_info:raiseVMMError(_("Unsupported password scheme: '%s'")%scheme,VMM_ERROR)ifcfg_dget('misc.dovecot_version')<_scheme_info[scheme][1]:raiseVMMError(_("The password scheme '%(scheme)s' requires Dovecot "">= v%(version)s.")%{'scheme':scheme,'version':version_str(_scheme_info[scheme][1])},VMM_ERROR)iflen(scheme_encoding)>1:ifscheme_encoding[1]notin('B64','BASE64','HEX'):raiseVMMError(_("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,str):raiseTypeError('Password is not a string: %r'%password)password=password.encode(ENCODING).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