1""" 2Use pycrypto to generate random passwords on the fly. 3""" 4import logging 5import random 6import re 7import string 8 9import salt.utils.platform 10import salt.utils.stringutils 11from salt.exceptions import CommandExecutionError, SaltInvocationError 12 13try: 14 try: 15 from M2Crypto.Rand import rand_bytes as get_random_bytes 16 except ImportError: 17 try: 18 from Cryptodome.Random import get_random_bytes 19 except ImportError: 20 from Crypto.Random import get_random_bytes # nosec 21 HAS_RANDOM = True 22except ImportError: 23 HAS_RANDOM = False 24 25try: 26 import crypt 27 28 HAS_CRYPT = True 29except ImportError: 30 HAS_CRYPT = False 31 32try: 33 import passlib.context 34 35 HAS_PASSLIB = True 36except ImportError: 37 HAS_PASSLIB = False 38 39log = logging.getLogger(__name__) 40 41 42def secure_password( 43 length=20, 44 use_random=True, 45 chars=None, 46 lowercase=True, 47 uppercase=True, 48 digits=True, 49 punctuation=True, 50 whitespace=False, 51 printable=False, 52): 53 """ 54 Generate a secure password. 55 """ 56 chars = chars or "" 57 if printable: 58 # as printable includes all other string character classes 59 # the other checks can be skipped 60 chars = string.printable 61 if not chars: 62 if lowercase: 63 chars += string.ascii_lowercase 64 if uppercase: 65 chars += string.ascii_uppercase 66 if digits: 67 chars += string.digits 68 if punctuation: 69 chars += string.punctuation 70 if whitespace: 71 chars += string.whitespace 72 try: 73 length = int(length) 74 pw = "" 75 while len(pw) < length: 76 if HAS_RANDOM and use_random: 77 encoding = None 78 if salt.utils.platform.is_windows(): 79 encoding = "UTF-8" 80 while True: 81 try: 82 char = salt.utils.stringutils.to_str( 83 get_random_bytes(1), encoding=encoding 84 ) 85 break 86 except UnicodeDecodeError: 87 continue 88 pw += re.sub( 89 salt.utils.stringutils.to_str( 90 r"[^{}]".format(re.escape(chars)), encoding=encoding 91 ), 92 "", 93 char, 94 ) 95 else: 96 pw += random.SystemRandom().choice(chars) 97 return pw 98 except Exception as exc: # pylint: disable=broad-except 99 log.exception("Failed to generate secure passsword") 100 raise CommandExecutionError(str(exc)) 101 102 103if HAS_CRYPT: 104 methods = {m.name.lower(): m for m in crypt.methods} 105else: 106 methods = {} 107known_methods = ["sha512", "sha256", "blowfish", "md5", "crypt"] 108 109 110def _gen_hash_passlib(crypt_salt=None, password=None, algorithm=None): 111 """ 112 Generate a /etc/shadow-compatible hash for a non-local system 113 """ 114 # these are the passlib equivalents to the 'known_methods' defined in crypt 115 schemes = ["sha512_crypt", "sha256_crypt", "bcrypt", "md5_crypt", "des_crypt"] 116 117 ctx = passlib.context.CryptContext(schemes=schemes) 118 119 kwargs = {"secret": password, "scheme": schemes[known_methods.index(algorithm)]} 120 if crypt_salt and "$" in crypt_salt: 121 # this salt has a rounds specifier. 122 # passlib takes it as a separate parameter, split it out 123 roundsstr, split_salt = crypt_salt.split("$") 124 rounds = int(roundsstr.split("=")[-1]) 125 kwargs.update({"salt": split_salt, "rounds": rounds}) 126 else: 127 # relaxed = allow salts that are too long 128 kwargs.update({"salt": crypt_salt, "relaxed": True}) 129 return ctx.hash(**kwargs) 130 131 132def _gen_hash_crypt(crypt_salt=None, password=None, algorithm=None): 133 """ 134 Generate /etc/shadow hash using the native crypt module 135 """ 136 if crypt_salt is None: 137 # setting crypt_salt to the algorithm makes crypt generate 138 # a salt compatible with the specified algorithm. 139 crypt_salt = methods[algorithm] 140 else: 141 if algorithm != "crypt": 142 # all non-crypt algorithms are specified as part of the salt 143 crypt_salt = "${}${}".format(methods[algorithm].ident, crypt_salt) 144 145 try: 146 ret = crypt.crypt(password, crypt_salt) 147 except OSError: 148 ret = None 149 return ret 150 151 152def gen_hash(crypt_salt=None, password=None, algorithm=None): 153 """ 154 Generate /etc/shadow hash 155 """ 156 if password is None: 157 password = secure_password() 158 159 if algorithm is None: 160 # prefer the most secure natively supported method 161 algorithm = crypt.methods[0].name.lower() if HAS_CRYPT else known_methods[0] 162 163 if algorithm == "crypt" and crypt_salt and len(crypt_salt) != 2: 164 log.warning("Hash salt is too long for 'crypt' hash.") 165 166 if HAS_CRYPT and algorithm in methods: 167 return _gen_hash_crypt( 168 crypt_salt=crypt_salt, password=password, algorithm=algorithm 169 ) 170 elif HAS_PASSLIB and algorithm in known_methods: 171 return _gen_hash_passlib( 172 crypt_salt=crypt_salt, password=password, algorithm=algorithm 173 ) 174 else: 175 raise SaltInvocationError( 176 "Cannot hash using '{}' hash algorithm. Natively supported " 177 "algorithms are: {}. If passlib is installed ({}), the supported " 178 "algorithms are: {}.".format( 179 algorithm, list(methods), HAS_PASSLIB, known_methods 180 ) 181 ) 182