@ -4,10 +4,11 @@
from __future__ import annotations
import random
import secrets
import string
import warnings
from collections import namedtuple
from dataclasses import dataclass
from ansible import constants as C
from ansible . errors import AnsibleError , AnsibleAssertionError
@ -34,6 +35,14 @@ try:
except Exception as e :
PASSLIB_E = e
CRYPT_E = None
HAS_CRYPT = False
try :
from ansible . _internal . _encryption import _crypt
HAS_CRYPT = True
except Exception as e :
CRYPT_E = e
display = Display ( )
@ -59,26 +68,135 @@ def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHA
return u ' ' . join ( random_generator . choice ( chars ) for dummy in range ( length ) )
_SALT_CHARS = string . ascii_letters + string . digits + ' ./ '
_VALID_SALT_CHARS = frozenset ( _SALT_CHARS )
def random_salt ( length = 8 ) :
""" Return a text string suitable for use as a salt for the hash functions we use to encrypt passwords.
"""
# Note passlib salt values must be pure ascii so we can't let the user
# configure this
salt_chars = string . ascii_letters + string . digits + u ' ./ '
return random_password ( length = length , chars = salt_chars )
return random_password ( length = length , chars = _SALT_CHARS )
@dataclass ( frozen = True )
class _Algo :
crypt_id : str
salt_size : int
implicit_rounds : int | None = None
salt_exact : bool = False
implicit_ident : str | None = None
rounds_format : str | None = None
requires_gensalt : bool = False
class BaseHash ( object ) :
algo = namedtuple ( ' algo ' , [ ' crypt_id ' , ' salt_size ' , ' implicit_rounds ' , ' salt_exact ' , ' implicit_ident ' ] )
algorithms = {
' md5_crypt ' : algo ( crypt_id = ' 1 ' , salt_size = 8 , implicit_rounds = None , salt_exact = False , implicit_ident = None ) ,
' bcrypt ' : algo ( crypt_id = ' 2b ' , salt_size = 22 , implicit_rounds = 12 , salt_exact = True , implicit_ident = ' 2b ' ) ,
' sha256_crypt ' : algo ( crypt_id = ' 5 ' , salt_size = 16 , implicit_rounds = 535000 , salt_exact = False , implicit_ident = None ) ,
' sha512_crypt ' : algo ( crypt_id = ' 6 ' , salt_size = 16 , implicit_rounds = 656000 , salt_exact = False , implicit_ident = None ) ,
' md5_crypt ' : _A lgo( crypt_id = ' 1 ' , salt_size = 8 ) ,
' bcrypt ' : _A lgo( crypt_id = ' 2b ' , salt_size = 22 , implicit_rounds = 12 , salt_exact = True , implicit_ident = ' 2b ' , rounds_format = ' cost ' ) ,
' sha256_crypt ' : _A lgo( crypt_id = ' 5 ' , salt_size = 16 , implicit_rounds = 535000 , rounds_format= ' rounds ' ) ,
' sha512_crypt ' : _A lgo( crypt_id = ' 6 ' , salt_size = 16 , implicit_rounds = 656000 , rounds_format= ' rounds ' ) ,
}
def __init__ ( self , algorithm ) :
self . algorithm = algorithm
display . vv ( f " Using { self . __class__ . __name__ } to hash input with { algorithm !r} " )
class CryptHash ( BaseHash ) :
algorithms = {
* * BaseHash . algorithms ,
' yescrypt ' : _Algo ( crypt_id = ' y ' , salt_size = 16 , implicit_rounds = 5 , rounds_format = ' cost ' , requires_gensalt = True , salt_exact = True ) ,
}
def __init__ ( self , algorithm : str ) - > None :
super ( CryptHash , self ) . __init__ ( algorithm )
if not HAS_CRYPT :
raise AnsibleError ( " crypt cannot be used as the ' libxcrypt ' library is not installed or is unusable. " ) from CRYPT_E
if algorithm not in self . algorithms :
raise AnsibleError ( f " crypt does not support { self . algorithm !r} algorithm " )
self . algo_data = self . algorithms [ algorithm ]
if self . algo_data . requires_gensalt and not _crypt . HAS_CRYPT_GENSALT :
raise AnsibleError ( f " { self . algorithm !r} algorithm requires libxcrypt " )
def hash ( self , secret : str , salt : str | None = None , salt_size : int | None = None , rounds : int | None = None , ident : str | None = None ) - > str :
rounds = self . _rounds ( rounds )
ident = self . _ident ( ident )
if _crypt . HAS_CRYPT_GENSALT :
saltstring = self . _gensalt ( ident , rounds , salt , salt_size )
else :
saltstring = self . _build_saltstring ( ident , rounds , salt , salt_size )
return self . _hash ( secret , saltstring )
def _validate_salt_size ( self , salt_size : int | None ) - > int :
if salt_size is not None and not isinstance ( salt_size , int ) :
raise TypeError ( ' salt_size must be an integer ' )
salt_size = salt_size or self . algo_data . salt_size
if self . algo_data . salt_exact and salt_size != self . algo_data . salt_size :
raise AnsibleError ( f " invalid salt size supplied ( { salt_size } ), expected { self . algo_data . salt_size } " )
elif not self . algo_data . salt_exact and salt_size > self . algo_data . salt_size :
raise AnsibleError ( f " invalid salt size supplied ( { salt_size } ), expected at most { self . algo_data . salt_size } " )
return salt_size
def _salt ( self , salt : str | None , salt_size : int | None ) - > str :
salt_size = self . _validate_salt_size ( salt_size )
ret = salt or random_salt ( salt_size )
if not set ( ret ) . issubset ( _VALID_SALT_CHARS ) :
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 : int | None ) - > int | None :
return rounds or self . algo_data . implicit_rounds
def _ident ( self , ident : str | None ) - > str | None :
return ident or self . algo_data . crypt_id
def _gensalt ( self , ident : str , rounds : int | None , salt : str | None , salt_size : int | None ) - > str :
if salt is None :
salt_size = self . _validate_salt_size ( salt_size )
rbytes = secrets . token_bytes ( salt_size )
else :
salt = self . _salt ( salt , salt_size )
rbytes = to_bytes ( salt )
prefix = f ' $ { ident } $ '
count = rounds or 0
try :
salt_bytes = _crypt . crypt_gensalt ( to_bytes ( prefix ) , count , rbytes )
return to_text ( salt_bytes , errors = ' strict ' )
except ( NotImplementedError , ValueError ) as e :
raise AnsibleError ( f " Failed to generate salt for { self . algorithm !r} algorithm " ) from e
def _build_saltstring ( self , ident : str , rounds : int | None , salt : str | None , salt_size : int | None ) - > str :
salt = self . _salt ( salt , salt_size )
saltstring = f ' $ { ident } ' if ident else ' '
if rounds :
if self . algo_data . rounds_format == ' cost ' :
saltstring + = f ' $ { rounds } '
else :
saltstring + = f ' $rounds= { rounds } '
saltstring + = f ' $ { salt } '
return saltstring
def _hash ( self , secret : str , saltstring : str ) - > str :
try :
result = _crypt . crypt ( to_bytes ( secret ) , to_bytes ( saltstring ) )
except ( OSError , ValueError ) as e :
raise AnsibleError ( f " crypt does not support { self . algorithm !r} algorithm " ) from e
return to_text ( result , errors = ' strict ' )
class PasslibHash ( BaseHash ) :
@ -88,8 +206,6 @@ class PasslibHash(BaseHash):
if not PASSLIB_AVAILABLE :
raise AnsibleError ( f " The passlib Python package must be installed to hash with the { algorithm !r} algorithm. " ) from PASSLIB_E
display . vv ( " Using passlib to hash input with ' %s ' " % algorithm )
try :
self . crypt_algo = getattr ( passlib . hash , algorithm )
except Exception :
@ -176,8 +292,13 @@ class PasslibHash(BaseHash):
return to_text ( result , errors = ' strict ' )
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. " ) from PASSLIB_E
def do_encrypt ( result , algorithm , salt_size = None , salt = None , ident = None , rounds = None ) :
if HAS_CRYPT and algorithm in CryptHash . algorithms :
return CryptHash ( algorithm ) . hash ( result , salt = salt , salt_size = salt_size , rounds = rounds , ident = ident )
elif PASSLIB_AVAILABLE :
# TODO: deprecate passlib
return PasslibHash ( algorithm ) . hash ( result , salt = salt , salt_size = salt_size , rounds = rounds , ident = ident )
elif not PASSLIB_AVAILABLE and algorithm not in CryptHash . algorithms :
# When passlib support is removed, this branch can be removed too
raise AnsibleError ( f " crypt does not support { algorithm !r} algorithm " )
raise AnsibleError ( " Unable to encrypt nor hash, either libxcrypt (recommended), crypt, or passlib must be installed. " ) from CRYPT_E