Matt Martz 2 weeks ago committed by GitHub
commit d5ffb1f6eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,31 @@
# Copyright: Contributors to the Ansible project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
import ctypes
import ctypes.util
__all__ = ['CRYPT_NAME', 'crypt']
# prefer ``libcrypt``/``libxcrypt`` over ``libc``
# libc can return None, and still give us the functionality we need
for _lib_name, _allow_none in (('crypt', False), ('c', True)):
_lib_so = ctypes.util.find_library(_lib_name)
if not _lib_so and not _allow_none:
# None will load ``libc`` in LoadLibrary, if we requested ``crypt``
# we don't want to allow that becoming ``libc``
continue
_lib = ctypes.cdll.LoadLibrary(_lib_so)
try:
crypt = _lib.crypt
except AttributeError:
# Whatever lib this is exists, but is missing ``crypt``
continue
crypt.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
crypt.restype = ctypes.c_char_p
break
else:
raise ImportError('Cannot find crypt implementation')
CRYPT_NAME = _lib_name

@ -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)

@ -1,2 +1,4 @@
shippable/posix/group4
setup/always/setup_passlib_controller # required for setup_test_user
setup/always/setup_libxcrypt
destructive

@ -0,0 +1,7 @@
- hosts: localhost
tasks:
- import_role:
name: filter_core
tasks_from: password_hash.yml
vars:
is_crypt: true

@ -4,3 +4,9 @@ set -eux
ANSIBLE_ROLES_PATH=../ ansible-playbook runme.yml "$@"
ANSIBLE_ROLES_PATH=../ ansible-playbook handle_undefined_type_errors.yml "$@"
# Remove passlib installed by setup_passlib_controller
source virtualenv.sh
SITE_PACKAGES=$(python -c "import sysconfig; print(sysconfig.get_path('purelib'))")
echo "raise ImportError('passlib')" > "${SITE_PACKAGES}/passlib.py"
ANSIBLE_ROLES_PATH=../ ansible-playbook password_hash.yml "$@"

@ -453,40 +453,6 @@
- '[1,3,5,7,9]|shuffle(seed=1337)|length == 5'
- '22|shuffle == 22'
- name: Verify password_hash throws on weird salt_size type
set_fact:
foo: '{{"hey"|password_hash(salt_size=[999])}}'
ignore_errors: yes
register: password_hash_1
- name: Verify password_hash throws on weird hashtype
set_fact:
foo: '{{"hey"|password_hash(hashtype="supersecurehashtype")}}'
ignore_errors: yes
register: password_hash_2
- name: Verify password_hash
assert:
that:
- "'what in the WORLD is up?'|password_hash|length == 120 or 'what in the WORLD is up?'|password_hash|length == 106"
# This throws a vastly different error on py2 vs py3, so we just check
# that it's a failure, not a substring of the exception.
- password_hash_1 is failed
- password_hash_2 is failed
- "'not support' in password_hash_2.msg"
- name: test using passlib with an unsupported hash type
set_fact:
foo: '{{"hey"|password_hash("msdcc")}}'
ignore_errors: yes
register: unsupported_hash_type
- assert:
that:
- unsupported_hash_type.msg == msg
vars:
msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512"
- name: Verify to_uuid throws on weird namespace
set_fact:
foo: '{{"hey"|to_uuid(namespace=22)}}'
@ -739,3 +705,5 @@
splitty:
- "1,2,3"
- "4,5,6"
- include_tasks: password_hash.yml

@ -0,0 +1,45 @@
- name: Verify password_hash throws on weird salt_size type
set_fact:
foo: '{{"hey"|password_hash(salt_size=[999])}}'
ignore_errors: yes
register: password_hash_1
- name: Verify password_hash throws on weird hashtype
set_fact:
foo: '{{"hey"|password_hash(hashtype="supersecurehashtype")}}'
ignore_errors: yes
register: password_hash_2
- name: Debug the output for the next assert
debug:
msg: "{{ 'what in the WORLD is up?'|password_hash }}"
- name: Verify password_hash
assert:
that:
- "'what in the WORLD is up?'|password_hash|length == 120 or 'what in the WORLD is up?'|password_hash|length == 106"
# This throws a vastly different error on py2 vs py3, so we just check
# that it's a failure, not a substring of the exception.
- password_hash_1 is failed
- password_hash_2 is failed
- "'not support' in password_hash_2.msg"
- name: test using an unsupported hash type
set_fact:
foo: '{{"hey"|password_hash("msdcc")}}'
ignore_errors: yes
register: unsupported_hash_type
- assert:
that:
- unsupported_hash_type.msg == msg
vars:
msg: "msdcc is not in the list of supported passlib algorithms: md5, blowfish, sha256, sha512"
when: is_crypt|default(false) is false
- assert:
that:
- unsupported_hash_type.msg == msg
vars:
msg: crypt does not support 'msdcc' algorithm. crypt does not support 'msdcc' algorithm
when: is_crypt|default(false) is true

@ -0,0 +1,6 @@
- name: uninstall libxcrypt
command: brew uninstall libxcrypt
become: yes
become_user: "{{ brew_stat.stat.pw_name }}"
environment:
HOMEBREW_NO_AUTO_UPDATE: True

@ -0,0 +1,28 @@
- when: ansible_facts.distribution == 'MacOSX'
block:
- name: MACOS | Find brew binary
command: which brew
register: brew_which
- name: MACOS | Get owner of brew binary
stat:
path: "{{ brew_which.stdout }}"
register: brew_stat
- become: yes
become_user: "{{ brew_stat.stat.pw_name }}"
environment:
HOMEBREW_NO_AUTO_UPDATE: True
block:
- name: MAXOS | Install libxcrypt
command: brew install libxcrypt
- name: MACOS | Get brew prefix
command: brew --prefix
register: brew_prefix
- name: MACOS | Link libxcrypt
file:
src: '{{ brew_prefix.stdout_lines.0 }}/opt/libxcrypt/lib/libcrypt.dylib'
dest: /usr/local/lib/libcrypt.dylib
state: link
Loading…
Cancel
Save