|
|
|
@ -8,6 +8,9 @@ import string
|
|
|
|
|
|
|
|
|
|
from collections import namedtuple
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
from ansible import constants as C
|
|
|
|
|
from ansible.errors import AnsibleError, AnsibleAssertionError
|
|
|
|
|
from ansible.module_utils.six import text_type
|
|
|
|
@ -28,6 +31,15 @@ try:
|
|
|
|
|
except Exception as e:
|
|
|
|
|
PASSLIB_E = e
|
|
|
|
|
|
|
|
|
|
CRYPT_NAME: str | None = None
|
|
|
|
|
CRYPT_E = None
|
|
|
|
|
HAS_CRYPT = False
|
|
|
|
|
try:
|
|
|
|
|
from ansible.utils.crypt import CRYPT_NAME, crypt
|
|
|
|
|
HAS_CRYPT = True
|
|
|
|
|
except Exception as e:
|
|
|
|
|
CRYPT_E = e
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
display = Display()
|
|
|
|
|
|
|
|
|
@ -75,6 +87,90 @@ class BaseHash(object):
|
|
|
|
|
self.algorithm = algorithm
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class CryptHash(BaseHash):
|
|
|
|
|
def __init__(self, algorithm):
|
|
|
|
|
super(CryptHash, self).__init__(algorithm)
|
|
|
|
|
|
|
|
|
|
if not HAS_CRYPT:
|
|
|
|
|
raise AnsibleError("crypt cannot be used as the 'libcrypt' library is not installed or is unusable.", orig_exc=CRYPT_E)
|
|
|
|
|
|
|
|
|
|
if sys.platform.startswith('darwin') and CRYPT_NAME != 'crypt':
|
|
|
|
|
# crypt in macos doesn't implement what we need. Installing libxcrypt or passlib offer usable
|
|
|
|
|
# mechanisms for password hashing
|
|
|
|
|
raise AnsibleError("crypt not supported on Mac OS X/Darwin, install libxcrypt or the passlib python module")
|
|
|
|
|
|
|
|
|
|
if algorithm not in self.algorithms:
|
|
|
|
|
raise AnsibleError("crypt does not support '%s' algorithm" % self.algorithm)
|
|
|
|
|
|
|
|
|
|
self.algo_data = self.algorithms[algorithm]
|
|
|
|
|
|
|
|
|
|
def hash(self, secret, salt=None, salt_size=None, rounds=None, ident=None):
|
|
|
|
|
salt = self._salt(salt, salt_size)
|
|
|
|
|
rounds = self._rounds(rounds)
|
|
|
|
|
ident = self._ident(ident)
|
|
|
|
|
return self._hash(secret, salt, rounds, ident)
|
|
|
|
|
|
|
|
|
|
def _salt(self, salt, salt_size):
|
|
|
|
|
salt_size = salt_size or self.algo_data.salt_size
|
|
|
|
|
ret = salt or random_salt(salt_size)
|
|
|
|
|
if re.search(r'[^./0-9A-Za-z]', ret):
|
|
|
|
|
raise AnsibleError("invalid characters in salt")
|
|
|
|
|
if self.algo_data.salt_exact and len(ret) != self.algo_data.salt_size:
|
|
|
|
|
raise AnsibleError(f"invalid salt size supplied ({len(ret)}), expected {self.algo_data.salt_size}")
|
|
|
|
|
elif not self.algo_data.salt_exact and len(ret) > self.algo_data.salt_size:
|
|
|
|
|
raise AnsibleError(f"invalid salt size supplied ({len(ret)}), expected at most {self.algo_data.salt_size}")
|
|
|
|
|
return ret
|
|
|
|
|
|
|
|
|
|
def _rounds(self, rounds):
|
|
|
|
|
if self.algorithm == 'bcrypt':
|
|
|
|
|
# crypt requires 2 digits for rounds
|
|
|
|
|
return rounds or self.algo_data.implicit_rounds
|
|
|
|
|
elif rounds == self.algo_data.implicit_rounds:
|
|
|
|
|
# Passlib does not include the rounds if it is the same as implicit_rounds.
|
|
|
|
|
# Make crypt lib behave the same, by not explicitly specifying the rounds in that case.
|
|
|
|
|
return None
|
|
|
|
|
else:
|
|
|
|
|
return rounds
|
|
|
|
|
|
|
|
|
|
def _ident(self, ident):
|
|
|
|
|
if not ident:
|
|
|
|
|
return self.algo_data.crypt_id
|
|
|
|
|
if self.algorithm == 'bcrypt':
|
|
|
|
|
return ident
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
def _hash(self, secret, salt, rounds, ident):
|
|
|
|
|
saltstring = ""
|
|
|
|
|
if ident:
|
|
|
|
|
saltstring = "$%s" % ident
|
|
|
|
|
|
|
|
|
|
if rounds:
|
|
|
|
|
if self.algorithm == 'bcrypt':
|
|
|
|
|
saltstring += "$%d" % rounds
|
|
|
|
|
else:
|
|
|
|
|
saltstring += "$rounds=%d" % rounds
|
|
|
|
|
|
|
|
|
|
saltstring += "$%s" % salt
|
|
|
|
|
|
|
|
|
|
# crypt.crypt throws OSError on Python >= 3.9 if it cannot parse saltstring.
|
|
|
|
|
try:
|
|
|
|
|
result = crypt(to_bytes(secret), to_bytes(saltstring))
|
|
|
|
|
orig_exc = None
|
|
|
|
|
except OSError as e:
|
|
|
|
|
result = None
|
|
|
|
|
orig_exc = e
|
|
|
|
|
|
|
|
|
|
# None as result would be interpreted by some modules (user module)
|
|
|
|
|
# as no password at all.
|
|
|
|
|
if not result:
|
|
|
|
|
raise AnsibleError(
|
|
|
|
|
"crypt.crypt does not support '%s' algorithm" % self.algorithm,
|
|
|
|
|
orig_exc=orig_exc,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class PasslibHash(BaseHash):
|
|
|
|
|
def __init__(self, algorithm):
|
|
|
|
|
super(PasslibHash, self).__init__(algorithm)
|
|
|
|
@ -175,4 +271,6 @@ def passlib_or_crypt(secret, algorithm, salt=None, salt_size=None, rounds=None,
|
|
|
|
|
def do_encrypt(result, encrypt, salt_size=None, salt=None, ident=None, rounds=None):
|
|
|
|
|
if PASSLIB_AVAILABLE:
|
|
|
|
|
return PasslibHash(encrypt).hash(result, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
|
|
|
|
|
raise AnsibleError("Unable to encrypt nor hash, passlib must be installed", orig_exc=PASSLIB_E)
|
|
|
|
|
if HAS_CRYPT:
|
|
|
|
|
return CryptHash(encrypt).hash(result, salt=salt, salt_size=salt_size, rounds=rounds, ident=ident)
|
|
|
|
|
raise AnsibleError("Unable to encrypt nor hash, either crypt/libxcrypt or passlib must be installed", orig_exc=PASSLIB_E)
|
|
|
|
|