Skip to content

Commit

Permalink
Remove crypt support from ansible.utils.encrypt
Browse files Browse the repository at this point in the history
  • Loading branch information
mkrizek committed Sep 19, 2023
1 parent f723496 commit cd7000f
Show file tree
Hide file tree
Showing 3 changed files with 5 additions and 139 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
removed_features:
- Remove deprecated crypt support from ansible.utils.encrypt (https://github.com/ansible/ansible/issues/81717)
106 changes: 3 additions & 103 deletions lib/ansible/utils/encrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
__metaclass__ = type

import random
import re
import string
import sys

from collections import namedtuple

Expand All @@ -17,8 +15,8 @@
from ansible.module_utils.common.text.converters import to_text, to_bytes
from ansible.utils.display import Display

PASSLIB_E = CRYPT_E = None
HAS_CRYPT = PASSLIB_AVAILABLE = False
PASSLIB_E = None
PASSLIB_AVAILABLE = False
try:
import passlib
import passlib.hash
Expand All @@ -31,12 +29,6 @@
except Exception as e:
PASSLIB_E = e

try:
import crypt
HAS_CRYPT = True
except Exception as e:
CRYPT_E = e


display = Display()

Expand Down Expand Up @@ -84,96 +76,6 @@ def __init__(self, algorithm):
self.algorithm = algorithm


class CryptHash(BaseHash):
def __init__(self, algorithm):
super(CryptHash, self).__init__(algorithm)

if not HAS_CRYPT:
raise AnsibleError("crypt.crypt cannot be used as the 'crypt' python library is not installed or is unusable.", orig_exc=CRYPT_E)

if sys.platform.startswith('darwin'):
raise AnsibleError("crypt.crypt not supported on Mac OS X/Darwin, install passlib python module")

if algorithm not in self.algorithms:
raise AnsibleError("crypt.crypt does not support '%s' algorithm" % self.algorithm)

display.deprecated(
"Encryption using the Python crypt module is deprecated. The "
"Python crypt module is deprecated and will be removed from "
"Python 3.13. Install the passlib library for continued "
"encryption functionality.",
version="2.17",
)

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("invalid salt size")
elif not self.algo_data.salt_exact and len(ret) > self.algo_data.salt_size:
raise AnsibleError("invalid 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.crypt(secret, 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)
Expand Down Expand Up @@ -274,6 +176,4 @@ 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)
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 or passlib must be installed.", orig_exc=CRYPT_E)
raise AnsibleError("Unable to encrypt nor hash, passlib must be installed.", orig_exc=PASSLIB_E)
36 changes: 0 additions & 36 deletions test/units/utils/test_encrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,6 @@ def test_encrypt_with_rounds():
secret="123", algorithm="sha512_crypt", salt="12345678", rounds=5000)


@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
def test_encrypt_default_rounds_no_passlib():
with passlib_off():
assert_hash("$1$12345678$tRy4cXc3kmcfRZVj4iFXr/",
secret="123", algorithm="md5_crypt", salt="12345678")
assert_hash("$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
secret="123", algorithm="sha256_crypt", salt="12345678")
assert_hash("$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
secret="123", algorithm="sha512_crypt", salt="12345678")

assert encrypt.CryptHash("md5_crypt").hash("123")


# If passlib is not installed. this is identical to the test_encrypt_default_rounds_no_passlib() test
@pytest.mark.skipif(not encrypt.PASSLIB_AVAILABLE, reason='passlib must be installed to run this test')
Expand Down Expand Up @@ -183,30 +171,6 @@ def test_random_salt():
assert res_char in expected_salt_candidate_chars


@pytest.mark.skipif(sys.platform.startswith('darwin'), reason='macOS requires passlib')
def test_invalid_crypt_salt():
pytest.raises(
AnsibleError,
encrypt.CryptHash('bcrypt')._salt,
'_',
None
)
encrypt.CryptHash('bcrypt')._salt('1234567890123456789012', None)
pytest.raises(
AnsibleError,
encrypt.CryptHash('bcrypt')._salt,
'kljsdf',
None
)
encrypt.CryptHash('sha256_crypt')._salt('123456', None)
pytest.raises(
AnsibleError,
encrypt.CryptHash('sha256_crypt')._salt,
'1234567890123456789012',
None
)


def test_passlib_bcrypt_salt(recwarn):
passlib_exc = pytest.importorskip("passlib.exc")

Expand Down

0 comments on commit cd7000f

Please sign in to comment.