Add support for crypt/libxcrypt via ctypes, as an alternative to passlib (#85970)

* Add support for crypt/libxcrypt via ctypes, as an alternative to passlib

* move verbosity message to BaseHash

* Don't require DYLD_LIBRARY_PATH mods for standard homebrew installs on macos

* improve crypt_gensalt error handling
pull/85939/head
sivel / Matt Martz 1 month ago committed by GitHub
parent f1f5b934c2
commit d6051b18dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,2 @@
minor_changes:
- password hashing - Add support back for using the ``crypt`` implmentation from the C library used to build Python, or with expanded functionality using ``libxcrypt``

@ -0,0 +1,159 @@
# 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
import os
import sys
from dataclasses import dataclass
__all__ = ['CRYPT_NAME', 'crypt', 'crypt_gensalt', 'HAS_CRYPT_GENSALT']
_FAILURE_TOKENS = frozenset({b'*0', b'*1'})
@dataclass(frozen=True)
class _CryptLib:
name: str | None
exclude_platforms: frozenset[str] = frozenset()
include_platforms: frozenset[str] = frozenset()
is_path: bool = False
_CRYPT_LIBS = (
_CryptLib('crypt'), # libxcrypt
_CryptLib(None, exclude_platforms=frozenset({'darwin'})), # fallback to default libc
_CryptLib( # macOS Homebrew (Apple Silicon)
'/opt/homebrew/opt/libxcrypt/lib/libcrypt.dylib',
include_platforms=frozenset({'darwin'}),
is_path=True,
),
_CryptLib( # macOS Homebrew (Intel)
'/usr/local/opt/libxcrypt/lib/libcrypt.dylib',
include_platforms=frozenset({'darwin'}),
is_path=True,
),
)
for _lib_config in _CRYPT_LIBS:
if sys.platform in _lib_config.exclude_platforms:
continue
if _lib_config.include_platforms and sys.platform not in _lib_config.include_platforms:
continue
if _lib_config.name is None:
_lib_so = None
elif _lib_config.is_path:
if os.path.exists(_lib_config.name):
_lib_so = _lib_config.name
else:
continue
else:
_lib_so = ctypes.util.find_library(_lib_config.name)
if not _lib_so:
continue
_lib = ctypes.cdll.LoadLibrary(_lib_so)
_use_crypt_r = False
try:
_crypt_impl = _lib.crypt_r
_use_crypt_r = True
except AttributeError:
try:
_crypt_impl = _lib.crypt
except AttributeError:
continue
if _use_crypt_r:
class _crypt_data(ctypes.Structure):
_fields_ = [('_opaque', ctypes.c_char * 131072)]
_crypt_impl.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.POINTER(_crypt_data)]
_crypt_impl.restype = ctypes.c_char_p
else:
_crypt_impl.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
_crypt_impl.restype = ctypes.c_char_p
# Try to load crypt_gensalt (available in libxcrypt)
_use_crypt_gensalt_rn = False
HAS_CRYPT_GENSALT = False
try:
_crypt_gensalt_impl = _lib.crypt_gensalt_rn
_crypt_gensalt_impl.argtypes = [ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p, ctypes.c_int, ctypes.c_char_p, ctypes.c_int]
_crypt_gensalt_impl.restype = ctypes.c_char_p
_use_crypt_gensalt_rn = True
HAS_CRYPT_GENSALT = True
except AttributeError:
try:
_crypt_gensalt_impl = _lib.crypt_gensalt
_crypt_gensalt_impl.argtypes = [ctypes.c_char_p, ctypes.c_ulong, ctypes.c_char_p, ctypes.c_int]
_crypt_gensalt_impl.restype = ctypes.c_char_p
HAS_CRYPT_GENSALT = True
except AttributeError:
_crypt_gensalt_impl = None
CRYPT_NAME = _lib_config.name
break
else:
raise ImportError('Cannot find crypt implementation')
def crypt(word: bytes, salt: bytes) -> bytes:
"""Hash a password using the system's crypt function."""
ctypes.set_errno(0)
if _use_crypt_r:
data = _crypt_data()
ctypes.memset(ctypes.byref(data), 0, ctypes.sizeof(data))
result = _crypt_impl(word, salt, ctypes.byref(data))
else:
result = _crypt_impl(word, salt)
errno = ctypes.get_errno()
if errno:
error_msg = os.strerror(errno)
raise OSError(errno, f'crypt failed: {error_msg}')
if result is None:
raise ValueError('crypt failed: invalid salt or unsupported algorithm')
if result in _FAILURE_TOKENS:
raise ValueError('crypt failed: invalid salt or unsupported algorithm')
return result
def crypt_gensalt(prefix: bytes, count: int, rbytes: bytes) -> bytes:
"""Generate a salt string for use with crypt."""
if not HAS_CRYPT_GENSALT:
raise NotImplementedError('crypt_gensalt not available (requires libxcrypt)')
ctypes.set_errno(0)
if _use_crypt_gensalt_rn:
output = ctypes.create_string_buffer(256)
result = _crypt_gensalt_impl(prefix, count, rbytes, len(rbytes), output, len(output))
if result is not None:
result = output.value
else:
result = _crypt_gensalt_impl(prefix, count, rbytes, len(rbytes))
errno = ctypes.get_errno()
if errno:
error_msg = os.strerror(errno)
raise OSError(errno, f'crypt_gensalt failed: {error_msg}')
if result is None:
raise ValueError('crypt_gensalt failed: unable to generate salt')
if result in _FAILURE_TOKENS:
raise ValueError('crypt_gensalt failed: invalid prefix or unsupported algorithm')
return result
del _lib_config

@ -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': _Algo(crypt_id='1', salt_size=8),
'bcrypt': _Algo(crypt_id='2b', salt_size=22, implicit_rounds=12, salt_exact=True, implicit_ident='2b', rounds_format='cost'),
'sha256_crypt': _Algo(crypt_id='5', salt_size=16, implicit_rounds=535000, rounds_format='rounds'),
'sha512_crypt': _Algo(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

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

@ -0,0 +1,24 @@
- hosts: localhost
tasks:
- name: Setup libxcrypt if requested
include_role:
name: setup_libxcrypt
when: use_libxcrypt|default(false)
- name: Skip macOS when not using libxcrypt (no usable crypt)
meta: end_host
when:
- not expect_libxcrypt|default(false)
- ansible_facts.distribution == 'MacOSX'
- name: Skip Alpine and FreeBSD when libxcrypt is expected (not available)
meta: end_host
when:
- expect_libxcrypt | default(false)
- ansible_facts.distribution == 'Alpine' or ansible_facts.system == 'FreeBSD'
- import_role:
name: filter_core
tasks_from: password_hash.yml
vars:
is_crypt: true

@ -4,3 +4,32 @@ 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"
# Test with libc (without libxcrypt)
ANSIBLE_ROLES_PATH=../ ansible-playbook password_hash.yml "$@"
# Install libxcrypt and capture output
INSTALL_OUTPUT=$(ANSIBLE_ROLES_PATH=../ ansible localhost -m include_role -a name=setup_libxcrypt 2>&1)
echo "$INSTALL_OUTPUT"
# Check if libxcrypt was installed by looking for the handler output
if echo "$INSTALL_OUTPUT" | grep -q 'LIBXCRYPT_WAS_INSTALLED'; then
# Setup cleanup trap
cleanup_libxcrypt() {
echo "Cleaning up libxcrypt..."
ANSIBLE_ROLES_PATH=../ ansible localhost -m include_role -a 'name=setup_libxcrypt tasks_from=uninstall' || true
}
trap cleanup_libxcrypt EXIT
fi
# Test with libxcrypt (new ansible-playbook process will discover it)
if echo "$INSTALL_OUTPUT" | grep -q 'LIBXCRYPT_WAS_INSTALLED'; then
ANSIBLE_ROLES_PATH=../ ansible-playbook password_hash.yml -e '{expect_libxcrypt: true}' "$@"
else
ANSIBLE_ROLES_PATH=../ ansible-playbook password_hash.yml "$@"
fi

@ -546,12 +546,6 @@
ignore_errors: yes
register: password_hash_2
- name: Verify password_hash throws on weird rounds
set_fact:
foo: '{{ "hey" | password_hash(rounds=1) }}'
ignore_errors: yes
register: password_hash_3
- name: test using passlib with an unsupported hash type
set_fact:
foo: '{{"hey"|password_hash("msdcc")}}'
@ -566,8 +560,6 @@
- "'salt_size must be an integer' in password_hash_1.msg"
- password_hash_2 is failed
- "'is not in the list of supported passlib algorithms' in password_hash_2.msg"
- password_hash_3 is failed
- "'Could not hash the secret' in password_hash_3.msg"
- "'msdcc is not in the list of supported passlib algorithms' in unsupported_hash_type.msg"
- name: Verify to_uuid throws on weird namespace
@ -845,6 +837,7 @@
- "1,2,3"
- "4,5,6"
- name: test to_yaml and to_nice_yaml
include_tasks: to_yaml.yml
@ -867,3 +860,5 @@
that:
- '"/foo/bar" in commonpath_01.ansible_facts.msg'
- "'|commonpath expects' in commonpath_02.msg"
- include_tasks: password_hash.yml

@ -0,0 +1,89 @@
- 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 in [crypt_len, passlib_len]"
- password_hash_1 is failed
- password_hash_2 is failed
- "'is not in the list of supported' in password_hash_2.msg or 'does not support' in password_hash_2.msg"
vars:
# omitted rounds
crypt_len: 106
# included $rounds=656000
passlib_len: 120
- name: test using an unsupported hash type
set_fact:
foo: '{{"hey"|password_hash("msdcc")}}'
ignore_errors: yes
register: unsupported_hash_type
- assert:
that:
- "'is not in the list of supported' in unsupported_hash_type.msg or 'does not support' in unsupported_hash_type.msg"
- name: Test if yescrypt is available
set_fact:
yescrypt_test: "{{ 'test' | password_hash('yescrypt') }}"
ignore_errors: yes
register: yescrypt_available
- name: Determine if yescrypt is supported
set_fact:
yescrypt_supported: "{{ yescrypt_available is not failed }}"
- name: Fail if libxcrypt was installed but yescrypt is not supported
when: expect_libxcrypt | default(false)
assert:
that:
- yescrypt_supported
fail_msg: "libxcrypt was installed but yescrypt is not supported"
- name: Test yescrypt hashing
when:
- is_crypt|default(false)
- yescrypt_supported
block:
- name: Generate yescrypt hash with default cost
set_fact:
yescrypt_hash: "{{ 'test_password' | password_hash('yescrypt') }}"
- name: Verify yescrypt hash format
assert:
that:
- yescrypt_hash.startswith('$y$')
- yescrypt_hash | length > 60
- name: Generate yescrypt hash with custom cost
set_fact:
yescrypt_hash_cost7: "{{ 'test_password' | password_hash('yescrypt', rounds=7) }}"
- name: Verify yescrypt hash with custom cost
assert:
that:
- yescrypt_hash_cost7.startswith('$y$jBT$')
- yescrypt_hash_cost7 | length > 60
- name: Generate yescrypt hash with custom salt
set_fact:
yescrypt_hash_salt: "{{ 'test_password' | password_hash('yescrypt', salt='abcdefghijklmnop') }}"
- name: Verify yescrypt hash with custom salt
assert:
that:
- yescrypt_hash_salt == '$y$j9T$V7qMYJaNbVKOeh4PhtqPk/$x7rTqZ.RpI07.dkBSxcg.jLM8ODUfx25rCN0cFsAUg0'

@ -0,0 +1,3 @@
- name: libxcrypt was installed
debug:
msg: "LIBXCRYPT_WAS_INSTALLED"

@ -0,0 +1,39 @@
- name: Gather facts
setup:
gather_subset: [min]
- name: Install libxcrypt on RedHat/CentOS/Fedora
when: ansible_facts.os_family == 'RedHat'
become: yes
package:
name: libxcrypt
state: present
notify: libxcrypt was installed
- name: Install libxcrypt on Debian/Ubuntu
when: ansible_facts.os_family == 'Debian'
become: yes
package:
name: libcrypt1
state: present
notify: libxcrypt was installed
- name: Install libxcrypt on macOS
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
- name: MACOS | Install libxcrypt
command: brew install libxcrypt
notify: libxcrypt was installed
become: yes
become_user: "{{ brew_stat.stat.pw_name }}"
environment:
HOMEBREW_NO_AUTO_UPDATE: True

@ -0,0 +1,36 @@
- name: Gather facts
setup:
gather_subset: [min]
- name: Uninstall libxcrypt on RedHat/CentOS/Fedora
when: ansible_facts.os_family == 'RedHat'
become: yes
package:
name: libxcrypt
state: absent
- name: Uninstall libxcrypt on Debian/Ubuntu
when: ansible_facts.os_family == 'Debian'
become: yes
package:
name: libcrypt1
state: absent
- name: Uninstall libxcrypt on macOS
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
- name: MACOS | Uninstall libxcrypt
command: brew uninstall libxcrypt
become: yes
become_user: "{{ brew_stat.stat.pw_name }}"
environment:
HOMEBREW_NO_AUTO_UPDATE: True

@ -14,8 +14,16 @@ from ansible.utils import encrypt
def assert_hash(expected, secret, algorithm, **settings):
assert encrypt.do_encrypt(secret, algorithm, **settings) == expected
assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected
if isinstance(expected, tuple):
expected_crypt, expected_passlib = expected
else:
expected_crypt = expected_passlib = expected
if encrypt.HAS_CRYPT and algorithm in encrypt.CryptHash.algorithms:
assert encrypt.CryptHash(algorithm).hash(secret, **settings) == expected_crypt
if encrypt.PASSLIB_AVAILABLE:
assert encrypt.PasslibHash(algorithm).hash(secret, **settings) == expected_passlib
@pytest.mark.parametrize(
@ -26,7 +34,10 @@ def assert_hash(expected, secret, algorithm, **settings):
None,
"1234567890123456789012",
None,
"$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
(
"$2b$12$KRGxLBS0Lxe3KBCwKxOzLe6odk8yM9lJBgNtLuDQxUkLDkpGI6twK",
"$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
),
id="bcrypt_default",
),
pytest.param(
@ -36,13 +47,17 @@ def assert_hash(expected, secret, algorithm, **settings):
None,
"$2$12$123456789012345678901ufd3hZRrev.WXCbemqGIV/gmWaTGLImm",
id="bcrypt_ident_2",
marks=pytest.mark.xfail(reason="crypt_gensalt rejects old bcrypt ident '2', unlike passlib"),
),
pytest.param(
"bcrypt",
"2y",
"1234567890123456789012",
None,
"$2y$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
(
"$2y$12$KRGxLBS0Lxe3KBCwKxOzLe6odk8yM9lJBgNtLuDQxUkLDkpGI6twK",
"$2y$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
),
id="bcrypt_ident_2y",
),
pytest.param(
@ -50,7 +65,10 @@ def assert_hash(expected, secret, algorithm, **settings):
"2a",
"1234567890123456789012",
None,
"$2a$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
(
"$2a$12$KRGxLBS0Lxe3KBCwKxOzLe6odk8yM9lJBgNtLuDQxUkLDkpGI6twK",
"$2a$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
),
id="bcrypt_ident_2a",
),
pytest.param(
@ -58,7 +76,10 @@ def assert_hash(expected, secret, algorithm, **settings):
"2b",
"1234567890123456789012",
None,
"$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
(
"$2b$12$KRGxLBS0Lxe3KBCwKxOzLe6odk8yM9lJBgNtLuDQxUkLDkpGI6twK",
"$2b$12$123456789012345678901ugbM1PeTfRQ0t6dCJu5lQA8hwrZOYgDu",
),
id="bcrypt_ident_2b",
),
pytest.param(
@ -66,8 +87,9 @@ def assert_hash(expected, secret, algorithm, **settings):
"invalid_ident",
"12345678",
5000,
"$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
"$5$l6nAoIXB$HtZrcvuIcvGwySXwnxGHuwyIha2FvzAt4QebHp43Wq4",
id="sha256_crypt_invalid_ident",
marks=pytest.mark.xfail(reason="crypt_gensalt rejects invalid idents, unlike passlib"),
),
pytest.param(
"crypt16",
@ -84,44 +106,55 @@ def test_encrypt_with_ident(algorithm, ident, salt, rounds, expected):
assert_hash(expected, secret="123", algorithm=algorithm, salt=salt, rounds=rounds, ident=ident)
# If passlib is not installed. this is identical to the test_encrypt_with_rounds_no_passlib() test
@pytest.mark.parametrize(
("algorithm", "rounds", "expected"),
[
pytest.param(
"sha256_crypt",
None,
"$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv.",
(
"$5$rounds=535000$l6nAoIXB$A2HBLoxGx60mfezwjZ6VFd9vI1.V4oKpd.iwYU4n776",
"$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv.",
),
id="sha256_crypt_default_rounds",
),
pytest.param(
"sha256_crypt",
5000,
"$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
("$5$l6nAoIXB$HtZrcvuIcvGwySXwnxGHuwyIha2FvzAt4QebHp43Wq4", "$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7"),
id="sha256_crypt_rounds_5000",
),
pytest.param(
"sha256_crypt",
10000,
"$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
(
"$5$rounds=10000$l6nAoIXB$//KYvMXLmzwUUYyWJmAJAeZuP7rsroMayX9hUhpwRC.",
"$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
),
id="sha256_crypt_rounds_10000",
),
pytest.param(
"sha512_crypt",
None,
"$6$rounds=656000$12345678$InMy49UwxyCh2pGJU1NpOhVSElDDzKeyuC6n6E9O34BCUGVNYADnI.rcA3m.Vro9BiZpYmjEoNhpREqQcbvQ80",
(
"$6$rounds=656000$l6nAoIXB$wDP2gGfore3TlBdPvi0wqot7zj8oodeHnEPerC1blJBRGEodsNewNfzxM5nYdfPMkvCh5Af/w82wvG2U3PpCT0",
"$6$rounds=656000$12345678$InMy49UwxyCh2pGJU1NpOhVSElDDzKeyuC6n6E9O34BCUGVNYADnI.rcA3m.Vro9BiZpYmjEoNhpREqQcbvQ80",
),
id="sha512_crypt_default_rounds",
),
pytest.param(
"sha512_crypt",
5000,
"$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
(
"$6$l6nAoIXB$Zva3RkTY95C0FM0fV9WhLyrQO7/jMt1sICo8bQZvpbRIhDYcgiNdL7IzdlGxG6j6CdkWdqeAk4/W49qkAuv/h/",
"$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
),
id="sha512_crypt_rounds_5000",
),
pytest.param(
"md5_crypt",
None,
"$1$12345678$tRy4cXc3kmcfRZVj4iFXr/",
("$1$l6nAoIXB$FCE6sRsviKJtqJWkAK6ff0", "$1$12345678$tRy4cXc3kmcfRZVj4iFXr/"),
id="md5_crypt_default_rounds",
),
],
@ -144,31 +177,31 @@ def test_password_hash_filter_passlib_with_exception():
pytest.param(
"sha256",
None,
"$5$rounds=535000$12345678$uy3TurUPaY71aioJi58HvUY8jkbhSQU8HepbyaNngv.",
"$5$rounds=535000$l6nAoIXB$A2HBLoxGx60mfezwjZ6VFd9vI1.V4oKpd.iwYU4n776",
id="sha256_default_rounds",
),
pytest.param(
"sha256",
5000,
"$5$12345678$uAZsE3BenI2G.nA8DpTl.9Dc8JiqacI53pEqRr5ppT7",
"$5$l6nAoIXB$HtZrcvuIcvGwySXwnxGHuwyIha2FvzAt4QebHp43Wq4",
id="sha256_rounds_5000",
),
pytest.param(
"sha256",
10000,
"$5$rounds=10000$12345678$JBinliYMFEcBeAXKZnLjenhgEhTmJBvZn3aR8l70Oy/",
"$5$rounds=10000$l6nAoIXB$//KYvMXLmzwUUYyWJmAJAeZuP7rsroMayX9hUhpwRC.",
id="sha256_rounds_10000",
),
pytest.param(
"sha512",
5000,
"$6$12345678$LcV9LQiaPekQxZ.OfkMADjFdSO2k9zfbDQrHPVcYjSLqSdjLYpsgqviYvTEP/R41yPmhH3CCeEDqVhW1VHr3L.",
"$6$l6nAoIXB$Zva3RkTY95C0FM0fV9WhLyrQO7/jMt1sICo8bQZvpbRIhDYcgiNdL7IzdlGxG6j6CdkWdqeAk4/W49qkAuv/h/",
id="sha512_rounds_5000",
),
pytest.param(
"sha512",
6000,
"$6$rounds=6000$12345678$l/fC67BdJwZrJ7qneKGP1b6PcatfBr0dI7W6JLBrsv8P1wnv/0pu4WJsWq5p6WiXgZ2gt9Aoir3MeORJxg4.Z/",
"$6$rounds=6000$l6nAoIXB$DMD7Me00a9FHa5hF22mKek6.Hf7dD6UvJBuRLKus7K//G2kRwcKx5pkp5vGDk5E/MEQgR0yaEsv6ooq3iGRqp/",
id="sha512_rounds_6000",
),
],

Loading…
Cancel
Save