diff --git a/changelogs/fragments/hashing-changes.yaml b/changelogs/fragments/hashing-changes.yaml
new file mode 100644
index 0000000000..87ebd41cc6
--- /dev/null
+++ b/changelogs/fragments/hashing-changes.yaml
@@ -0,0 +1,18 @@
+---
+bugfixes:
+- vars_prompt with encrypt does not require passlib for the algorithms
+ supported by crypt.
+- Additional checks ensure that there is always a result of hashing passwords
+ in the password_hash filter and vars_prompt, otherwise an error is returned.
+ Some modules (like user module) interprets None as no password at all,
+ which can be dangerous if the password given above is passed directly into
+ those modules.
+- Avoids deprecated functionality of passlib with newer library versions.
+- password_hash does not hard-code the salt-length, which fixes bcrypt
+ in connection with passlib as bcrypt requires a salt with length 22.
+minor_changes:
+- The password_hash filter supports all parameters of passlib.
+ This allows users to provide a rounds parameter.
+ (https://github.com/ansible/ansible/issues/15326)
+- password_hash is not restricted to the subset provided by crypt.crypt
+ (https://github.com/ansible/ansible/issues/17266)
diff --git a/docs/docsite/rst/porting_guides/porting_guide_2.7.rst b/docs/docsite/rst/porting_guides/porting_guide_2.7.rst
index 21a78dab9c..a6001f322c 100644
--- a/docs/docsite/rst/porting_guides/porting_guide_2.7.rst
+++ b/docs/docsite/rst/porting_guides/porting_guide_2.7.rst
@@ -71,6 +71,16 @@ In Ansible 2.7 a new module argument named ``public`` was added to the ``include
There is an important difference in the way that ``include_role`` (dynamic) will expose the role's variables, as opposed to ``import_role`` (static). ``import_role`` is a pre-processor, and the ``defaults`` and ``vars`` are evaluated at playbook parsing, making the variables available to tasks and roles listed at any point in the play. ``include_role`` is a conditional task, and the ``defaults`` and ``vars`` are evaluated at execution time, making the variables available to tasks and roles listed *after* the ``include_role`` task.
+vars_prompt with unknown algorithms
+-----------------------------------
+
+vars_prompt now throws an error if the hash algorithm specified in encrypt is not supported by
+the controller. This increases the safety of vars_prompt as it previously returned None if the
+algorithm was unknown. Some modules, notably the user module, treated a password of None as
+a request not to set a password. If your playbook starts erroring because of this, change the
+hashing algorithm being used with this filter.
+
+
Deprecated
==========
@@ -81,7 +91,7 @@ Expedited Deprecation: Use of ``__file__`` in ``AnsibleModule``
We are deprecating the use of the ``__file__`` variable to refer to the file containing the currently-running code. This common Python technique for finding a filesystem path does not always work (even in vanilla Python). Sometimes a Python module can be imported from a virtual location (like inside of a zip file). When this happens, the ``__file__`` variable will reference a virtual location pointing to inside of the zip file. This can cause problems if, for instance, the code was trying to use ``__file__`` to find the directory containing the python module to write some temporary information.
-Before the introduction of AnsiBallZ in Ansible 2.1, using ``__file__`` worked in ``AnsibleModule`` sometimes, but any module that used it would fail when pipelining was turned on (because the module would be piped into the python interpreter's standard input, so ``__file__`` wouldn't contain a file path). AnsiBallZ unintentionally made using ``__file__`` always work, by always creating a temporary file for ``AnsibleModule`` to reside in.
+Before the introduction of AnsiBallZ in Ansible 2.1, using ``__file__`` worked in ``AnsibleModule`` sometimes, but any module that used it would fail when pipelining was turned on (because the module would be piped into the python interpreter's standard input, so ``__file__`` wouldn't contain a file path). AnsiBallZ unintentionally made using ``__file__`` work, by always creating a temporary file for ``AnsibleModule`` to reside in.
Ansible 2.8 will no longer create a temporary file for ``AnsibleModule``; instead it will read the file out of a zip file. This change should speed up module execution, but it does mean that starting with Ansible 2.8, referencing ``__file__`` will always fail in ``AnsibleModule``.
@@ -190,7 +200,12 @@ Noteworthy module changes
Plugins
=======
-No notable changes.
+* The hash_password filter now throws an error if the hash algorithm specified is not supported by
+ the controller. This increases the safety of the filter as it previously returned None if the
+ algorithm was unknown. Some modules, notably the user module, treated a password of None as
+ a request not to set a password. If your playbook starts erroring because of this, change the
+ hashing algorithm being used with this filter.
+
Porting custom scripts
======================
diff --git a/docs/docsite/rst/user_guide/playbooks_filters.rst b/docs/docsite/rst/user_guide/playbooks_filters.rst
index 3916cf82b9..d8cbebcc42 100644
--- a/docs/docsite/rst/user_guide/playbooks_filters.rst
+++ b/docs/docsite/rst/user_guide/playbooks_filters.rst
@@ -783,6 +783,18 @@ An idempotent method to generate unique hashes per system is to use a salt that
Hash types available depend on the master system running ansible,
'hash' depends on hashlib password_hash depends on passlib (https://passlib.readthedocs.io/en/stable/lib/passlib.hash.html).
+.. versionadded:: 2.7
+
+Some hash types allow providing a rounds parameter::
+
+ {{ 'secretpassword'|password_hash('sha256', 'mysecretsalt', rounds=10000) }}
+
+When`Passlib `_ is installed
+`password_hash` supports any crypt scheme and parameter supported by 'Passlib'::
+
+ {{ 'secretpassword'|password_hash('sha256_crypt', 'mysecretsalt', rounds=5000) }}
+ {{ 'secretpassword'|password_hash('bcrypt', ident='2b', rounds=14) }}
+
.. _combine_filter:
Combining hashes/dictionaries
diff --git a/docs/docsite/rst/user_guide/playbooks_prompts.rst b/docs/docsite/rst/user_guide/playbooks_prompts.rst
index 3430059159..6ae1655fe7 100644
--- a/docs/docsite/rst/user_guide/playbooks_prompts.rst
+++ b/docs/docsite/rst/user_guide/playbooks_prompts.rst
@@ -90,6 +90,16 @@ However, the only parameters accepted are 'salt' or 'salt_size'. You can use you
'salt', or have one generated automatically using 'salt_size'. If nothing is specified, a salt
of size 8 will be generated.
+.. versionadded:: 2.7
+
+When Passlib is not installed the `crypt `_ library is used as fallback.
+Depending on your platform at most the following crypt schemes are supported:
+
+- *bcrypt* - BCrypt
+- *md5_crypt* - MD5 Crypt
+- *sha256_crypt* - SHA-256 Crypt
+- *sha512_crypt* - SHA-512 Crypt
+
.. seealso::
:doc:`playbooks`
diff --git a/lib/ansible/plugins/filter/core.py b/lib/ansible/plugins/filter/core.py
index b56c5d527b..17309dd8aa 100644
--- a/lib/ansible/plugins/filter/core.py
+++ b/lib/ansible/plugins/filter/core.py
@@ -41,19 +41,14 @@ from random import Random, SystemRandom, shuffle, random
from jinja2.filters import environmentfilter, do_groupby as _do_groupby
-try:
- import passlib.hash
- HAS_PASSLIB = True
-except ImportError:
- HAS_PASSLIB = False
-
-from ansible.errors import AnsibleFilterError
-from ansible.module_utils.six import iteritems, string_types, integer_types
+from ansible.errors import AnsibleError, AnsibleFilterError
+from ansible.module_utils.six import iteritems, string_types, integer_types, reraise
from ansible.module_utils.six.moves import reduce, shlex_quote
from ansible.module_utils._text import to_bytes, to_text
from ansible.module_utils.common.collections import is_sequence
from ansible.parsing.ajson import AnsibleJSONEncoder
from ansible.parsing.yaml.dumper import AnsibleDumper
+from ansible.utils.encrypt import passlib_or_crypt
from ansible.utils.hashing import md5s, checksum_s
from ansible.utils.unicode import unicode_wrap
from ansible.utils.vars import merge_hash
@@ -252,42 +247,19 @@ def get_hash(data, hashtype='sha1'):
return h.hexdigest()
-def get_encrypted_password(password, hashtype='sha512', salt=None):
-
- # TODO: find a way to construct dynamically from system
- cryptmethod = {
- 'md5': '1',
- 'blowfish': '2a',
- 'sha256': '5',
- 'sha512': '6',
+def get_encrypted_password(password, hashtype='sha512', salt=None, salt_size=None, rounds=None):
+ passlib_mapping = {
+ 'md5': 'md5_crypt',
+ 'blowfish': 'bcrypt',
+ 'sha256': 'sha256_crypt',
+ 'sha512': 'sha512_crypt',
}
- if hashtype in cryptmethod:
- if salt is None:
- r = SystemRandom()
- if hashtype in ['md5']:
- saltsize = 8
- else:
- saltsize = 16
- saltcharset = string.ascii_letters + string.digits + '/.'
- salt = ''.join([r.choice(saltcharset) for _ in range(saltsize)])
-
- if not HAS_PASSLIB:
- if sys.platform.startswith('darwin'):
- raise AnsibleFilterError('|password_hash requires the passlib python module to generate password hashes on macOS/Darwin')
- saltstring = "$%s$%s" % (cryptmethod[hashtype], salt)
- encrypted = crypt.crypt(password, saltstring)
- else:
- if hashtype == 'blowfish':
- cls = passlib.hash.bcrypt
- else:
- cls = getattr(passlib.hash, '%s_crypt' % hashtype)
-
- encrypted = cls.encrypt(password, salt=salt)
-
- return encrypted
-
- return None
+ hashtype = passlib_mapping.get(hashtype, hashtype)
+ try:
+ return passlib_or_crypt(password, hashtype, salt=salt, salt_size=salt_size, rounds=rounds)
+ except AnsibleError as e:
+ reraise(AnsibleFilterError, AnsibleFilterError(str(e), orig_exc=e), sys.exc_info()[2])
def to_uuid(string):
diff --git a/lib/ansible/plugins/lookup/password.py b/lib/ansible/plugins/lookup/password.py
index 2d41ba9262..3895e2ebd0 100644
--- a/lib/ansible/plugins/lookup/password.py
+++ b/lib/ansible/plugins/lookup/password.py
@@ -100,7 +100,7 @@ from ansible.errors import AnsibleError, AnsibleAssertionError
from ansible.module_utils._text import to_bytes, to_native, to_text
from ansible.parsing.splitter import parse_kv
from ansible.plugins.lookup import LookupBase
-from ansible.utils.encrypt import do_encrypt, random_password
+from ansible.utils.encrypt import do_encrypt, random_password, random_salt
from ansible.utils.path import makedirs_safe
@@ -207,15 +207,6 @@ def _gen_candidate_chars(characters):
return chars
-def _random_salt():
- """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 = _gen_candidate_chars(['ascii_letters', 'digits', './'])
- return random_password(length=8, chars=salt_chars)
-
-
def _parse_content(content):
'''parse our password data format into password and salt
@@ -329,7 +320,7 @@ class LookupModule(LookupBase):
if params['encrypt'] and not salt:
changed = True
- salt = _random_salt()
+ salt = random_salt()
if changed and b_path != to_bytes('/dev/null'):
content = _format_content(plaintext_password, salt, encrypt=params['encrypt'])
diff --git a/lib/ansible/utils/encrypt.py b/lib/ansible/utils/encrypt.py
index 5e34beddbd..4445fb87c8 100644
--- a/lib/ansible/utils/encrypt.py
+++ b/lib/ansible/utils/encrypt.py
@@ -4,18 +4,25 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
+import crypt
import multiprocessing
import random
+import string
+import sys
+
+from collections import namedtuple
from ansible import constants as C
from ansible.errors import AnsibleError, AnsibleAssertionError
from ansible.module_utils.six import text_type
from ansible.module_utils._text import to_text, to_bytes
-
PASSLIB_AVAILABLE = False
try:
+ import passlib
import passlib.hash
+ from passlib.utils.handlers import HasRawSalt
+
PASSLIB_AVAILABLE = True
except:
pass
@@ -33,33 +40,6 @@ _LOCK = multiprocessing.Lock()
DEFAULT_PASSWORD_LENGTH = 20
-def do_encrypt(result, encrypt, salt_size=None, salt=None):
- if PASSLIB_AVAILABLE:
- try:
- crypt = getattr(passlib.hash, encrypt)
- except:
- raise AnsibleError("passlib does not support '%s' algorithm" % encrypt)
-
- if salt_size:
- result = crypt.encrypt(result, salt_size=salt_size)
- elif salt:
- if crypt._salt_is_bytes:
- salt = to_bytes(salt, encoding='ascii', errors='strict')
- else:
- salt = to_text(salt, encoding='ascii', errors='strict')
- result = crypt.encrypt(result, salt=salt)
- else:
- result = crypt.encrypt(result)
- else:
- raise AnsibleError("passlib must be installed to encrypt vars_prompt values")
-
- # Hashes from passlib.hash should be represented as ascii strings of hex
- # digits so this should not traceback. If it's not representable as such
- # we need to traceback and then blacklist such algorithms because it may
- # impact calling code.
- return to_text(result, errors='strict')
-
-
def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHARS):
'''Return a random password string of length containing only chars
@@ -72,3 +52,149 @@ def random_password(length=DEFAULT_PASSWORD_LENGTH, chars=C.DEFAULT_PASSWORD_CHA
random_generator = random.SystemRandom()
return u''.join(random_generator.choice(chars) for dummy in range(length))
+
+
+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)
+
+
+class BaseHash(object):
+ algo = namedtuple('algo', ['crypt_id', 'salt_size', 'implicit_rounds'])
+ algorithms = {
+ 'md5_crypt': algo(crypt_id='1', salt_size=8, implicit_rounds=None),
+ 'bcrypt': algo(crypt_id='2a', salt_size=22, implicit_rounds=None),
+ 'sha256_crypt': algo(crypt_id='5', salt_size=16, implicit_rounds=5000),
+ 'sha512_crypt': algo(crypt_id='6', salt_size=16, implicit_rounds=5000),
+ }
+
+ def __init__(self, algorithm):
+ self.algorithm = algorithm
+
+
+class CryptHash(BaseHash):
+ def __init__(self, algorithm):
+ super(CryptHash, self).__init__(algorithm)
+
+ 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)
+ self.algo_data = self.algorithms[algorithm]
+
+ def hash(self, secret, salt=None, salt_size=None, rounds=None):
+ salt = self._salt(salt, salt_size)
+ rounds = self._rounds(rounds)
+ return self._hash(secret, salt, rounds)
+
+ def _salt(self, salt, salt_size):
+ salt_size = salt_size or self.algo_data.salt_size
+ return salt or random_salt(salt_size)
+
+ def _rounds(self, rounds):
+ if rounds == self.algo_data.implicit_rounds:
+ # Passlib does not include the rounds if it is the same as implict_rounds.
+ # Make crypt lib behave the same, by not explicitly specifying the rounds in that case.
+ return None
+ else:
+ return rounds
+
+ def _hash(self, secret, salt, rounds):
+ if rounds is None:
+ saltstring = "$%s$%s" % (self.algo_data.crypt_id, salt)
+ else:
+ saltstring = "$%s$rounds=%d$%s" % (self.algo_data.crypt_id, rounds, salt)
+ result = crypt.crypt(secret, saltstring)
+
+ # crypt.crypt returns None if it cannot parse saltstring
+ # None as result would be interpreted by the some modules (user module)
+ # as no password at all.
+ if not result:
+ raise AnsibleError("crypt.crypt does not support '%s' algorithm" % self.algorithm)
+
+ return result
+
+
+class PasslibHash(BaseHash):
+ def __init__(self, algorithm):
+ super(PasslibHash, self).__init__(algorithm)
+
+ if not PASSLIB_AVAILABLE:
+ raise AnsibleError("passlib must be installed to hash with '%s'" % algorithm)
+
+ try:
+ self.crypt_algo = getattr(passlib.hash, algorithm)
+ except:
+ raise AnsibleError("passlib does not support '%s' algorithm" % algorithm)
+
+ def hash(self, secret, salt=None, salt_size=None, rounds=None):
+ salt = self._clean_salt(salt)
+ rounds = self._clean_rounds(rounds)
+ return self._hash(secret, salt=salt, salt_size=salt_size, rounds=rounds)
+
+ def _clean_salt(self, salt):
+ if not salt:
+ return None
+ elif issubclass(self.crypt_algo, HasRawSalt):
+ return to_bytes(salt, encoding='ascii', errors='strict')
+ else:
+ return to_text(salt, encoding='ascii', errors='strict')
+
+ def _clean_rounds(self, rounds):
+ algo_data = self.algorithms.get(self.algorithm)
+ if rounds:
+ return rounds
+ elif algo_data and algo_data.implicit_rounds:
+ # The default rounds used by passlib depend on the passlib version.
+ # For consistency ensure that passlib behaves the same as crypt in case no rounds were specified.
+ # Thus use the crypt defaults.
+ return algo_data.implicit_rounds
+ else:
+ return None
+
+ def _hash(self, secret, salt, salt_size, rounds):
+ # Not every hash algorithm supports every paramter.
+ # Thus create the settings dict only with set parameters.
+ settings = {}
+ if salt:
+ settings['salt'] = salt
+ if salt_size:
+ settings['salt_size'] = salt_size
+ if rounds:
+ settings['rounds'] = rounds
+
+ # starting with passlib 1.7 'using' and 'hash' should be used instead of 'encrypt'
+ if hasattr(self.crypt_algo, 'hash'):
+ result = self.crypt_algo.using(**settings).hash(secret)
+ elif hasattr(self.crypt_algo, 'encrypt'):
+ result = self.crypt_algo.encrypt(secret, **settings)
+ else:
+ raise AnsibleError("installed passlib version %s not supported" % passlib.__version__)
+
+ # passlib.hash should always return something or raise an exception.
+ # Still ensure that there is always a result.
+ # Otherwise an empty password might be assumed by some modules, like the user module.
+ if not result:
+ raise AnsibleError("failed to hash with algorithm '%s'" % self.algorithm)
+
+ # Hashes from passlib.hash should be represented as ascii strings of hex
+ # digits so this should not traceback. If it's not representable as such
+ # we need to traceback and then blacklist such algorithms because it may
+ # impact calling code.
+ return to_text(result, errors='strict')
+
+
+def passlib_or_crypt(secret, algorithm, salt=None, salt_size=None, rounds=None):
+ if PASSLIB_AVAILABLE:
+ return PasslibHash(algorithm).hash(secret, salt=salt, salt_size=salt_size, rounds=rounds)
+ else:
+ return CryptHash(algorithm).hash(secret, salt=salt, salt_size=salt_size, rounds=rounds)
+
+
+def do_encrypt(result, encrypt, salt_size=None, salt=None):
+ return passlib_or_crypt(result, encrypt, salt_size=salt_size, salt=salt)
diff --git a/test/integration/targets/vars_prompt/test-vars_prompt.py b/test/integration/targets/vars_prompt/test-vars_prompt.py
index 39f834af4c..45b9f017ff 100644
--- a/test/integration/targets/vars_prompt/test-vars_prompt.py
+++ b/test/integration/targets/vars_prompt/test-vars_prompt.py
@@ -94,7 +94,7 @@ tests = [
'test_spec': [
[('password', 'Scenic-Improving-Payphone\r'),
('confirm password', 'Scenic-Improving-Payphone\r')],
- r'"password": "\$6\$rounds=']},
+ r'"password": "\$6\$']},
# Test variables in prompt field
# https://github.com/ansible/ansible/issues/32723
diff --git a/test/units/plugins/lookup/test_password.py b/test/units/plugins/lookup/test_password.py
index 9dcd783ee2..8f3e31750d 100644
--- a/test/units/plugins/lookup/test_password.py
+++ b/test/units/plugins/lookup/test_password.py
@@ -292,15 +292,6 @@ class TestRandomPassword(unittest.TestCase):
(char, candidate_chars, params['chars']))
-class TestRandomSalt(unittest.TestCase):
- def test(self):
- res = password._random_salt()
- expected_salt_candidate_chars = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./'
- self.assertEquals(len(res), 8)
- for res_char in res:
- self.assertIn(res_char, expected_salt_candidate_chars)
-
-
class TestParseContent(unittest.TestCase):
def test_empty_password_file(self):
plaintext_password, salt = password._parse_content(u'')
diff --git a/test/units/utils/test_encrypt.py b/test/units/utils/test_encrypt.py
new file mode 100644
index 0000000000..22dd76287f
--- /dev/null
+++ b/test/units/utils/test_encrypt.py
@@ -0,0 +1,133 @@
+# (c) 2018, Matthias Fuchs
+#
+# This file is part of Ansible
+#
+# Ansible is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Ansible is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Ansible. If not, see .
+
+import pytest
+import sys
+
+from ansible.errors import AnsibleError, AnsibleFilterError
+from ansible.plugins.filter.core import get_encrypted_password
+from ansible.utils import encrypt
+
+
+class passlib_off(object):
+ def __init__(self):
+ self.orig = encrypt.PASSLIB_AVAILABLE
+
+ def __enter__(self):
+ encrypt.PASSLIB_AVAILABLE = False
+ return self
+
+ def __exit__(self, exception_type, exception_value, traceback):
+ encrypt.PASSLIB_AVAILABLE = self.orig
+
+
+def assert_hash(expected, secret, algorithm, **settings):
+ assert encrypt.CryptHash(algorithm).hash(secret, **settings) == expected
+
+ if encrypt.PASSLIB_AVAILABLE:
+ assert encrypt.passlib_or_crypt(secret, algorithm, **settings) == expected
+ assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected
+ else:
+ with pytest.raises(AnsibleFilterError):
+ encrypt.PasslibHash(algorithm).hash(secret, **settings)
+
+
+def test_encrypt_with_rounds():
+ assert_hash("$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=5000)
+ assert_hash("$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
+ secret="123", algorithm="sha256_crypt", salt="12345678", rounds=10000)
+ assert_hash("$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
+ secret="123", algorithm="sha512_crypt", salt="12345678", rounds=5000)
+
+
+def test_encrypt_default_rounds():
+ 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")
+
+
+def test_password_hash_filter_no_passlib():
+ with passlib_off():
+ assert not encrypt.PASSLIB_AVAILABLE
+ assert get_encrypted_password("123", "md5", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ with pytest.raises(AnsibleFilterError):
+ get_encrypted_password("123", "crypt16", salt="12")
+
+
+def test_password_hash_filter_passlib():
+ if not encrypt.PASSLIB_AVAILABLE:
+ pytest.skip("passlib not available")
+
+ with pytest.raises(AnsibleFilterError):
+ get_encrypted_password("123", "sha257", salt="12345678")
+
+ # Uses 5000 rounds by default for sha256 matching crypt behaviour
+ assert get_encrypted_password("123", "sha256", salt="12345678") == "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+ assert get_encrypted_password("123", "sha256", salt="12345678", rounds=5000) == "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+
+ assert (get_encrypted_password("123", "sha256", salt="12345678", rounds=10000) ==
+ "$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/")
+
+ assert (get_encrypted_password("123", "sha512", salt="12345678", rounds=6000) ==
+ "$6$rounds=6000$12345678$l/fC67BdJwZrJ7qneKGP1b6PcatfBr0dI7W6JLBrsv8P1wnv/0pu4WJsWq5p6WiXgZ2gt9Aoir3MeORJxg4.Z/")
+
+ assert (get_encrypted_password("123", "sha512", salt="12345678", rounds=5000) ==
+ "$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.")
+
+ assert get_encrypted_password("123", "crypt16", salt="12") == "12pELHK2ME3McUFlHxel6uMM"
+
+ # Try algorithm that uses a raw salt
+ assert get_encrypted_password("123", "pbkdf2_sha256")
+
+
+def test_do_encrypt_no_passlib():
+ with passlib_off():
+ assert not encrypt.PASSLIB_AVAILABLE
+ assert encrypt.do_encrypt("123", "md5_crypt", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ with pytest.raises(AnsibleError):
+ encrypt.do_encrypt("123", "crypt16", salt="12")
+
+
+def test_do_encrypt_passlib():
+ if not encrypt.PASSLIB_AVAILABLE:
+ pytest.skip("passlib not available")
+
+ with pytest.raises(AnsibleError):
+ encrypt.do_encrypt("123", "sha257_crypt", salt="12345678")
+
+ # Uses 5000 rounds by default for sha256 matching crypt behaviour.
+ assert encrypt.do_encrypt("123", "sha256_crypt", salt="12345678") == "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"
+
+ assert encrypt.do_encrypt("123", "md5_crypt", salt="12345678") == "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"
+
+ assert encrypt.do_encrypt("123", "crypt16", salt="12") == "12pELHK2ME3McUFlHxel6uMM"
+
+
+def test_random_salt():
+ res = encrypt.random_salt()
+ expected_salt_candidate_chars = u'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./'
+ assert len(res) == 8
+ for res_char in res:
+ assert res_char in expected_salt_candidate_chars